M2.3: the shared world (trees, pickups, pieces, chests, fires, doors, graves, clock on the server)
This commit is contained in:
494
tests/test_net_shared.gd
Normal file
494
tests/test_net_shared.gd
Normal file
@@ -0,0 +1,494 @@
|
||||
extends SceneTree
|
||||
## M2.3: the shared world. Two players on a dedicated server see the same
|
||||
## island change: a tree one of them fells (and its regrowth), pickups and
|
||||
## dropped items (first come, first served), building pieces (and walking
|
||||
## into them, for real and by cheating), doors, chests, fires, dismantling,
|
||||
## and the whole lot surviving a server restart.
|
||||
##
|
||||
## A real headless server, plus "Bob" (tests/net_world_bot.gd) as another
|
||||
## process with his own world, following this test's commands. This test is
|
||||
## "Alice" with the real world scene.
|
||||
## Run: godot --headless --path . --script res://tests/test_net_shared.gd
|
||||
|
||||
const PORT := 24346
|
||||
const DIR := "user://test_net"
|
||||
const SRV := DIR + "/server5"
|
||||
const BOB := DIR + "/bob_cmds"
|
||||
const SEED := 777
|
||||
const Bot = preload("res://tests/net_world_bot.gd")
|
||||
|
||||
var _fails := 0
|
||||
var _finished := false
|
||||
var _pids: Array[int] = []
|
||||
var Net: Node
|
||||
var WN: Node
|
||||
var A: Node
|
||||
var world: Node3D
|
||||
var player: CharacterBody3D
|
||||
var _cmd := 0
|
||||
var _server_pid := 0
|
||||
var _toasts: Array[String] = []
|
||||
|
||||
func check(cond: bool, what: String) -> void:
|
||||
print((" ok " if cond else " FAIL ") + what)
|
||||
if not cond:
|
||||
_fails += 1
|
||||
|
||||
func _initialize() -> void:
|
||||
_run.call_deferred()
|
||||
|
||||
func _run() -> void:
|
||||
Engine.max_fps = 60
|
||||
Net = root.get_node("Net")
|
||||
WN = root.get_node("WorldNet")
|
||||
A = root.get_node("Actions")
|
||||
A.notify.connect(func(t): _toasts.append(t))
|
||||
for d in [SRV, BOB]:
|
||||
_rm_tree(d)
|
||||
DirAccess.make_dir_recursive_absolute(BOB)
|
||||
_start_server()
|
||||
await _sleep(4.0)
|
||||
await _body()
|
||||
Net.stop()
|
||||
for pid in _pids:
|
||||
if OS.is_process_running(pid):
|
||||
OS.kill(pid)
|
||||
check(_finished, "test ran to the end (no script errors)")
|
||||
_rm_tree(SRV)
|
||||
_rm_tree(BOB)
|
||||
print("RESULT: %s (%d failures)" % ["PASS" if _fails == 0 else "FAIL", _fails])
|
||||
quit(1 if _fails > 0 else 0)
|
||||
|
||||
func _start_server() -> void:
|
||||
# --debug-regrow: things grow back in seconds instead of half an hour.
|
||||
_server_pid = _spawn(["--server", "--port", str(PORT), "--config", SRV + "/server.json",
|
||||
"--server-name", "Shared Test", "--seed", str(SEED), "--debug-regrow", "0.002"])
|
||||
|
||||
func _body() -> void:
|
||||
var me = load("res://scripts/net/identity.gd").open(DIR + "/a")
|
||||
me.name = "Alice"
|
||||
Net.identity = me
|
||||
var r := await _join()
|
||||
check(r == "", "Alice joins (%s)" % r)
|
||||
if r != "":
|
||||
return
|
||||
await _load_world()
|
||||
check(WN.received >= 1 and WN.world == world, "and gets the server's island")
|
||||
|
||||
print("Bob, with his own world")
|
||||
_spawn(["--identity", DIR + "/bob", "--name", "Bob", "--address", "127.0.0.1:%d" % PORT, "--dir", BOB],
|
||||
"res://tests/net_world_bot.gd")
|
||||
var b0 := await _bob_result(0, 30.0)
|
||||
check(bool(b0.get("ok", false)), "Bob joins and loads it too (%s)" % b0.get("why", "ok"))
|
||||
if not bool(b0.get("ok", false)):
|
||||
return
|
||||
|
||||
await _trees()
|
||||
await _building()
|
||||
await _containers_and_fires()
|
||||
await _items_on_the_ground()
|
||||
await _restart()
|
||||
_finished = true
|
||||
|
||||
# --- trees --------------------------------------------------------------------
|
||||
|
||||
func _trees() -> void:
|
||||
print("felling a tree")
|
||||
var tree := _nearest(world.props.resource_nodes.values().filter(func(n): return n.kind == "tree" and not n.depleted))
|
||||
var uid: String = tree.uid
|
||||
var to_tree: Vector3 = (tree.global_position - player.global_position)
|
||||
to_tree.y = 0.0
|
||||
await Bot.walk(player, tree.global_position - to_tree.normalized() * 1.6, world)
|
||||
player.inventory.set_slot(0, {"id": "stone_axe", "count": 1})
|
||||
player.select(0)
|
||||
var wood0: int = player.inventory.count("wood")
|
||||
var hits := 0
|
||||
var t := 0.0
|
||||
while not tree.depleted and t < 20.0:
|
||||
player.survival.stamina = 100.0
|
||||
if A.harvest_hit(player, tree, tree.global_position + Vector3(0, 1.0, 0)):
|
||||
hits += 1
|
||||
await _sleep(0.25)
|
||||
t += 0.25
|
||||
check(tree.depleted, "Alice fells it in %d hits (her world shows it when the server says so)" % hits)
|
||||
await _sleep(0.3)
|
||||
check(player.inventory.count("wood") > wood0, "and the server gives her the wood (%d)" % (player.inventory.count("wood") - wood0))
|
||||
var rep := await _bob({"do": "report", "uids": [uid]})
|
||||
var bn: Variant = rep.nodes.get(uid)
|
||||
check(bn != null and bool(bn.depleted), "Bob sees it fall too")
|
||||
|
||||
print("regrowth (sped up for the test)")
|
||||
await _sleep(6.0) # (trees take 4.8 s at this speed-up)
|
||||
check(tree.depleted, "it doesn't grow back with Alice standing next to it")
|
||||
var walk_from := Time.get_ticks_msec()
|
||||
await Bot.walk(player, player.global_position + Vector3(14, 0, 0), world)
|
||||
t = 0.0
|
||||
while tree.depleted and t < 15.0:
|
||||
await _sleep(0.2)
|
||||
t += 0.2
|
||||
check(not tree.depleted, "once she walks off, it grows back (%.1f s after she set off)" % ((Time.get_ticks_msec() - walk_from) / 1000.0))
|
||||
rep = await _bob({"do": "report", "uids": [uid]})
|
||||
bn = rep.nodes.get(uid)
|
||||
check(bn != null and not bool(bn.depleted), "for Bob as well")
|
||||
|
||||
# --- building -----------------------------------------------------------------
|
||||
|
||||
var _wall: Node3D
|
||||
var _door: Node3D
|
||||
|
||||
func _building() -> void:
|
||||
print("building a wall")
|
||||
var site: Vector3 = world.build.debug_find_site(player.global_position + Vector3(0, 0, 10), 3.0)
|
||||
await Bot.walk(player, site + Vector3(-3.5, 0, 0), world)
|
||||
_wall = await _place("wood_wall", site, 0.0)
|
||||
check(_wall != null, "Alice puts up a wall (the server agrees and tells everyone)")
|
||||
if _wall == null:
|
||||
return
|
||||
var rep := await _bob({"do": "report"})
|
||||
var seen := _find(rep, "wood_wall")
|
||||
check(not seen.is_empty() and _p(seen.pos).distance_to(_wall.global_position) < 0.01, "Bob has the same wall, in the same place")
|
||||
|
||||
print("Bob walks into it")
|
||||
var n: Vector3 = _wall.global_basis.z.normalized()
|
||||
var base := _wall.global_position
|
||||
await _bob({"do": "walk", "to": _v(base + n * 2.5)})
|
||||
var bump := await _bob({"do": "bump", "dir": _v(-n), "dist": 4.0})
|
||||
check(float(bump.get("moved", 9.0)) < 2.3, "and it stops him (moved %.2f of 4 m)" % float(bump.get("moved", 9.0)))
|
||||
var hop := await _bob({"do": "hop", "to": _v(base - n * 0.6 + Vector3(0, 0.05, 0))})
|
||||
var hp: Vector3 = _p(hop.get("pos", [0, 0, 0]))
|
||||
check(int(hop.get("corrected", 0)) >= 1 and (hp - base).dot(n) > 0.0,
|
||||
"a game that steps him through anyway is refused by the server, and he's put back on his side")
|
||||
await _dismantle()
|
||||
|
||||
print("a door")
|
||||
var dsite: Vector3 = world.build.debug_find_site(site + Vector3(0, 0, 8), 2.0)
|
||||
await Bot.walk(player, dsite + Vector3(-2.5, 0, 0), world)
|
||||
_door = await _place("wood_door", dsite, 0.0)
|
||||
check(_door != null, "Alice puts up a door")
|
||||
if _door == null:
|
||||
return
|
||||
A.toggle_door(player, _door)
|
||||
await _sleep(0.6)
|
||||
check(_door.open, "she opens it")
|
||||
rep = await _bob({"do": "report"})
|
||||
seen = _find(rep, "wood_door")
|
||||
check(not seen.is_empty() and bool(seen.get("open", false)), "and it's open for Bob")
|
||||
|
||||
## Places `id` on the ground at `at` through Actions, the way the game does.
|
||||
func _place(id: String, at: Vector3, yaw: float) -> Node3D:
|
||||
var def: Dictionary = root.get_node("ItemDB").get_item(id)
|
||||
var xf := Transform3D(Basis(Vector3.UP, yaw), Vector3(at.x, world.terrain.sample_height(at.x, at.z), at.z))
|
||||
if def.place.has("kind"):
|
||||
var hit := {"position": xf.origin, "normal": Vector3.UP, "collider": world.terrain.get_node("TerrainBody")}
|
||||
var res: Dictionary = world.build.placement(def, hit, yaw)
|
||||
if not res.ok:
|
||||
print(" (can't place %s: %s)" % [id, res.reason])
|
||||
return null
|
||||
xf = res.xf
|
||||
player.inventory.set_slot(5, {"id": id, "count": 1})
|
||||
var before: Array = WN.pieces.keys()
|
||||
if not A.place(player, 5, xf, world):
|
||||
print(" (Actions.place refused %s)" % id)
|
||||
return null
|
||||
check(player.inventory.get_slot(5) == null, "(%s taken from Alice's bag)" % id)
|
||||
var t := 0.0
|
||||
while t < 3.0:
|
||||
for k in WN.pieces:
|
||||
if not k in before and WN.pieces[k].item_id == id:
|
||||
return WN.pieces[k]
|
||||
await _sleep(0.05)
|
||||
t += 0.05
|
||||
return null
|
||||
|
||||
# --- chests and fires ------------------------------------------------------------
|
||||
|
||||
var _chest: Node3D
|
||||
var _fire: Node3D
|
||||
|
||||
func _containers_and_fires() -> void:
|
||||
print("a shared chest")
|
||||
var site: Vector3 = world.build.debug_find_site(player.global_position + Vector3(6, 0, 0), 1.5)
|
||||
await Bot.walk(player, site + Vector3(-2.0, 0, 0), world)
|
||||
_chest = await _place("chest", site, 0.0)
|
||||
check(_chest != null, "Alice places a chest")
|
||||
if _chest == null:
|
||||
return
|
||||
player.inventory.set_slot(6, {"id": "wood", "count": 10})
|
||||
A.transfer(player, player.inventory, 6, _chest.inventory, _chest)
|
||||
await _sleep(0.5)
|
||||
check(player.inventory.get_slot(6) == null and _chest.inventory.count("wood") == 10, "she puts 10 wood in")
|
||||
var got := {"c": "waiting"}
|
||||
WN.chest_click(_chest, 3, {"id": "stone", "count": 3}, false, func(c): got.c = c)
|
||||
var t := 0.0
|
||||
while got.c is String and t < 3.0:
|
||||
await _sleep(0.05)
|
||||
t += 0.05
|
||||
check(got.c == null and _chest.inventory.get_slot(3) != null and _chest.inventory.get_slot(3).id == "stone",
|
||||
"and drops 3 stone in slot 4 by hand (the server does the click and hands back an empty cursor)")
|
||||
var nid: int = WN.nid(_chest)
|
||||
await _bob({"do": "walk", "to": _v(site + Vector3(1.8, 0, 0))})
|
||||
var rep := await _bob({"do": "report"})
|
||||
var seen: Dictionary = rep.pieces.get(str(nid), {})
|
||||
check(_count(seen.get("inv", []), "wood") == 10 and _count(seen.get("inv", []), "stone") == 3, "Bob sees what's in it")
|
||||
var slot := -1
|
||||
for i in range(_chest.inventory.size()):
|
||||
var s = _chest.inventory.get_slot(i)
|
||||
if s != null and s.id == "wood":
|
||||
slot = i
|
||||
var took := await _bob({"do": "chest_take", "nid": nid, "slot": slot})
|
||||
check(_count(took.get("inv", []), "wood") == 10, "Bob takes the wood out (shift-click)")
|
||||
await _sleep(0.3)
|
||||
check(_chest.inventory.count("wood") == 0 and _chest.inventory.count("stone") == 3, "and it's gone from Alice's view of the chest")
|
||||
|
||||
print("a fire")
|
||||
var fsite: Vector3 = world.build.debug_find_site(site + Vector3(0, 0, -5), 1.5)
|
||||
await Bot.walk(player, fsite + Vector3(-2.0, 0, 0), world)
|
||||
_fire = await _place("campfire", fsite, 0.0)
|
||||
check(_fire != null, "Alice lights a campfire")
|
||||
if _fire == null:
|
||||
return
|
||||
var f0: float = _fire.fuel
|
||||
player.inventory.set_slot(7, {"id": "wood", "count": 2})
|
||||
A.add_fuel(player, _fire)
|
||||
await _sleep(0.5)
|
||||
check(_fire.fuel > f0 + 30.0, "and feeds it a log (%.0f s -> %.0f s of fuel)" % [f0, _fire.fuel])
|
||||
rep = await _bob({"do": "report"})
|
||||
seen = rep.pieces.get(str(WN.nid(_fire)), {})
|
||||
check(absf(float(seen.get("fuel", -100.0)) - _fire.fuel) < 5.0, "Bob's fire has the same fuel (%.0f s)" % float(seen.get("fuel", -1.0)))
|
||||
|
||||
# --- things on the ground --------------------------------------------------------
|
||||
|
||||
func _items_on_the_ground() -> void:
|
||||
print("dropped items")
|
||||
var at := player.global_position + Vector3(1.0, 0.1, 0)
|
||||
world.drop_item("flint", 2, Transform3D(Basis(), at))
|
||||
var t := 0.0
|
||||
var key := ""
|
||||
while key.is_empty() and t < 3.0:
|
||||
for k in WN.dropped:
|
||||
if WN.dropped[k].item_id == "flint":
|
||||
key = "#%d" % k
|
||||
await _sleep(0.05)
|
||||
t += 0.05
|
||||
check(not key.is_empty(), "Alice drops 2 flint: it's in the shared world")
|
||||
var rep := await _bob({"do": "report"})
|
||||
check(rep.dropped.any(func(d): return d.item == "flint" and int(d.count) == 2), "Bob sees it")
|
||||
await _bob({"do": "walk", "to": _v(at + Vector3(1.0, 0, 0))})
|
||||
var took := await _bob({"do": "take", "key": key})
|
||||
check(_count(took.get("inv", []), "flint") == 2, "Bob picks it up")
|
||||
await _sleep(0.3)
|
||||
check(WN.dropped.is_empty() or not WN.dropped.has(int(key.substr(1))), "and it's gone for Alice")
|
||||
|
||||
print("first come, first served")
|
||||
var pk := _nearest(world.props.pickups.values().filter(func(p): return not p.taken))
|
||||
await Bot.walk(player, pk.global_position + Vector3(-1.0, 0, 0), world)
|
||||
await _bob({"do": "walk", "to": _v(pk.global_position + Vector3(1.0, 0, 0))})
|
||||
var id: String = pk.item_id
|
||||
var a0: int = player.inventory.count(id)
|
||||
_bob_send({"do": "take", "key": pk.uid}) # both reach for it at once
|
||||
A.pickup(player, pk)
|
||||
var br := await _bob_wait(_cmd)
|
||||
await _sleep(0.4)
|
||||
var a_got: int = player.inventory.count(id) - a0
|
||||
var b_got: int = _count(br.get("inv", []), id) - (2 if id == "flint" else 0)
|
||||
check(a_got + b_got == pk.amount and (a_got == 0 or b_got == 0),
|
||||
"only one of them gets the %s (Alice %d, Bob %d)" % [id, a_got, b_got])
|
||||
|
||||
print("dying online")
|
||||
player.inventory.load_data(Inventory.new(player.inventory.size()).to_data())
|
||||
player.inventory.set_slot(0, {"id": "stone", "count": 7})
|
||||
player.inventory.set_slot(1, {"id": "berries", "count": 4})
|
||||
var died_at := player.global_position
|
||||
world._on_player_died()
|
||||
var grave: Node3D = null
|
||||
var t2 := 0.0
|
||||
while grave == null and t2 < 3.0:
|
||||
for p in WN.pieces.values():
|
||||
if p.item_id == "grave":
|
||||
grave = p
|
||||
await _sleep(0.05)
|
||||
t2 += 0.05
|
||||
check(grave != null and grave.global_position.distance_to(died_at) < 3.0 and player.inventory.count("stone") == 0,
|
||||
"Alice dies: her things go into a grave in the shared world")
|
||||
if grave == null:
|
||||
return
|
||||
var gnid: int = WN.nid(grave)
|
||||
var rep2 := await _bob({"do": "report"})
|
||||
var g: Dictionary = rep2.pieces.get(str(gnid), {})
|
||||
check(_count(g.get("inv", []), "stone") == 7 and _count(g.get("inv", []), "berries") == 4, "Bob sees the grave, with what she had")
|
||||
await Bot.walk(player, grave.global_position + Vector3(1.5, 0, 0), world)
|
||||
for i in range(grave.inventory.size()):
|
||||
if is_instance_valid(grave) and grave.inventory.get_slot(i) != null:
|
||||
A.transfer(player, grave.inventory, i, player.inventory, grave)
|
||||
await _sleep(0.15)
|
||||
await _sleep(0.5)
|
||||
check(player.inventory.count("stone") == 7 and player.inventory.count("berries") == 4, "she takes it all back")
|
||||
check(not WN.pieces.has(gnid), "and the empty grave crumbles")
|
||||
rep2 = await _bob({"do": "report"})
|
||||
check(not rep2.pieces.has(str(gnid)), "for Bob too")
|
||||
|
||||
# --- dismantling ---------------------------------------------------------------
|
||||
|
||||
func _dismantle() -> void:
|
||||
if _wall == null:
|
||||
return
|
||||
print("taking the wall down")
|
||||
# (Straight after building it, while she's still beside it: a test walk
|
||||
# back from somewhere else can cut through the door frame, and the
|
||||
# server rightly sends her back.)
|
||||
var nid: int = WN.nid(_wall)
|
||||
var w0: int = player.inventory.count("wood_wall")
|
||||
_toasts.clear()
|
||||
var ok: bool = A.dismantle(player, _wall)
|
||||
check(ok, "Alice dismantles the wall (%.1f m away%s)" % [player.global_position.distance_to(_wall.global_position),
|
||||
"" if ok else ", refused: %s" % [_toasts]])
|
||||
await _sleep(0.5)
|
||||
check(not WN.pieces.has(nid) and player.inventory.count("wood_wall") == w0 + 1, "it's gone, and back in her bag")
|
||||
var rep := await _bob({"do": "report"})
|
||||
check(not rep.pieces.has(str(nid)), "gone for Bob too")
|
||||
|
||||
# --- persistence -------------------------------------------------------------------
|
||||
|
||||
func _restart() -> void:
|
||||
print("the server restarts")
|
||||
var expect := {}
|
||||
for k in WN.pieces:
|
||||
expect[k] = WN.pieces[k].item_id
|
||||
var chest_nid: int = WN.nid(_chest) if _chest != null else -1
|
||||
# Killed outright while everyone's still on (like Ctrl+C, which gives
|
||||
# Godot no chance to save): the server saved within a few seconds of the
|
||||
# last change anyway.
|
||||
await _sleep(WN.SAVE_AFTER + 1.5)
|
||||
check(FileAccess.file_exists(SRV + "/worlds/server-world/world.json"), "the server saved the island soon after the last change")
|
||||
OS.kill(_server_pid)
|
||||
_pids.erase(_server_pid)
|
||||
await _bob({"do": "leave"})
|
||||
world.queue_free()
|
||||
await process_frame
|
||||
Net.leave()
|
||||
await _sleep(1.0)
|
||||
_start_server()
|
||||
await _sleep(4.0)
|
||||
var r := await _join()
|
||||
check(r == "", "Alice rejoins (%s)" % r)
|
||||
if r != "":
|
||||
return
|
||||
await _load_world()
|
||||
var same: bool = expect.size() == WN.pieces.size()
|
||||
for k in expect:
|
||||
same = same and WN.pieces.has(k) and WN.pieces[k].item_id == expect[k]
|
||||
check(same, "everything built is still there, with the same ids (%d pieces)" % WN.pieces.size())
|
||||
var c: Node3D = WN.pieces.get(chest_nid)
|
||||
check(c != null and c.inventory.count("stone") == 3 and c.inventory.count("wood") == 0, "the chest still has the 3 stone")
|
||||
var d: Node3D = null
|
||||
for p in WN.pieces.values():
|
||||
if p.item_id == "wood_door":
|
||||
d = p
|
||||
check(d != null and d.open, "and the door is still open")
|
||||
world.queue_free()
|
||||
await process_frame
|
||||
Net.leave()
|
||||
|
||||
# --- helpers --------------------------------------------------------------------
|
||||
|
||||
func _load_world() -> void:
|
||||
world = load("res://scenes/world.tscn").instantiate()
|
||||
root.add_child(world)
|
||||
for _i in range(8):
|
||||
await physics_frame
|
||||
player = world.player
|
||||
player.set_physics_process(false) # the test moves Alice
|
||||
var t := 0.0
|
||||
while WN.received < 1 and t < 10.0:
|
||||
await _sleep(0.05)
|
||||
t += 0.05
|
||||
|
||||
func _nearest(nodes: Array) -> Node3D:
|
||||
var best: Node3D = null
|
||||
for n in nodes:
|
||||
if best == null or n.global_position.distance_to(player.global_position) < best.global_position.distance_to(player.global_position):
|
||||
best = n
|
||||
return best
|
||||
|
||||
func _find(rep: Dictionary, item: String) -> Dictionary:
|
||||
for k in rep.get("pieces", {}):
|
||||
if rep.pieces[k].item == item:
|
||||
return rep.pieces[k]
|
||||
return {}
|
||||
|
||||
static func _count(inv: Array, id: String) -> int:
|
||||
var n := 0
|
||||
for s in inv:
|
||||
if s != null and s.id == id:
|
||||
n += int(s.count)
|
||||
return n
|
||||
|
||||
static func _v(p: Vector3) -> Array:
|
||||
return [p.x, p.y, p.z]
|
||||
|
||||
static func _p(a: Array) -> Vector3:
|
||||
return Vector3(a[0], a[1], a[2])
|
||||
|
||||
func _bob_send(cmd: Dictionary) -> void:
|
||||
_cmd += 1
|
||||
var f := FileAccess.open("%s/cmd_%d.json" % [BOB, _cmd], FileAccess.WRITE)
|
||||
f.store_string(JSON.stringify(cmd))
|
||||
f.close()
|
||||
|
||||
func _bob(cmd: Dictionary) -> Dictionary:
|
||||
_bob_send(cmd)
|
||||
return await _bob_wait(_cmd)
|
||||
|
||||
func _bob_wait(n: int) -> Dictionary:
|
||||
return await _bob_result(n, 20.0)
|
||||
|
||||
func _bob_result(n: int, limit: float) -> Dictionary:
|
||||
var path := "%s/res_%d.json" % [BOB, n]
|
||||
var t := 0.0
|
||||
while not FileAccess.file_exists(path) and t < limit:
|
||||
await _sleep(0.05)
|
||||
t += 0.05
|
||||
await _sleep(0.05)
|
||||
var d: Variant = JSON.parse_string(FileAccess.get_file_as_string(path)) if FileAccess.file_exists(path) else null
|
||||
return d if d is Dictionary else {"error": "Bob gave no answer to command %d" % n, "nodes": {}, "pieces": {}, "dropped": []}
|
||||
|
||||
func _join() -> String:
|
||||
var state := {"done": false, "reason": ""}
|
||||
var ok := func(): state.done = true
|
||||
var bad := func(why): state.done = true; state.reason = why
|
||||
Net.joined.connect(ok)
|
||||
Net.join_failed.connect(bad)
|
||||
Net.join("127.0.0.1:%d" % PORT)
|
||||
var t := 0.0
|
||||
while not state.done and t < 25.0:
|
||||
await _sleep(0.05)
|
||||
t += 0.05
|
||||
Net.joined.disconnect(ok)
|
||||
Net.join_failed.disconnect(bad)
|
||||
return state.reason if state.done else "test timed out"
|
||||
|
||||
func _spawn(user_args: Array, script: String = "") -> int:
|
||||
var args := PackedStringArray(["--headless", "--path", ProjectSettings.globalize_path("res://")])
|
||||
if not script.is_empty():
|
||||
args.append_array(["--script", script])
|
||||
args.append("--")
|
||||
for x in user_args:
|
||||
args.append(str(x))
|
||||
args.append_array(["--exit-with", str(OS.get_process_id())])
|
||||
var pid := OS.create_process(OS.get_executable_path(), args)
|
||||
_pids.append(pid)
|
||||
return pid
|
||||
|
||||
func _sleep(s: float) -> void:
|
||||
await create_timer(s).timeout
|
||||
|
||||
## Deletes a folder and everything in it.
|
||||
static func _rm_tree(d: String) -> void:
|
||||
var da := DirAccess.open(d)
|
||||
if da == null:
|
||||
return
|
||||
for sub in da.get_directories():
|
||||
_rm_tree(d.path_join(sub))
|
||||
for f in da.get_files():
|
||||
da.remove(f)
|
||||
DirAccess.remove_absolute(d)
|
||||
Reference in New Issue
Block a user