extends SceneTree ## A second player with a real world, for tests/test_net_shared.gd. Joins, ## loads the island like the game does, then follows commands the test ## writes as files (so the test can line up both players' actions): ## /cmd_.json -> runs it -> /res_.json ## Commands: walk, report, take, chest_take, bump, hop, leave. ## Args (after --): --identity --name --address host:port --dir var net: Node var world: Node3D var player: CharacterBody3D var dir := "" var corrections := 0 func _initialize() -> void: _run.call_deferred() func _run() -> void: net = root.get_node("Net") var args: Dictionary = net.parse_args(OS.get_cmdline_user_args()) dir = String(args.dir) var me = load("res://scripts/net/identity.gd").open(String(args.identity)) me.name = String(args.get("name", "Bob")) net.identity = me var outcome := {"v": ""} net.joined.connect(func(): outcome.v = "joined") net.join_failed.connect(func(r): outcome.v = "failed: " + r) net.join(String(args.address)) var t0 := Time.get_ticks_msec() while outcome.v.is_empty() and Time.get_ticks_msec() - t0 < 20000: await process_frame if outcome.v != "joined": _write(0, {"ok": false, "why": outcome.v}) quit() return 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 us net.corrected.connect(func(_p): corrections += 1) t0 = Time.get_ticks_msec() while world.get_node("/root/WorldNet").received < 1 and Time.get_ticks_msec() - t0 < 10000: await process_frame _write(0, {"ok": true, "pos": _v(player.global_position)}) var n := 1 while net.state == "connected": var path := "%s/cmd_%d.json" % [dir, n] if not FileAccess.file_exists(path): await create_timer(0.05).timeout continue await create_timer(0.05).timeout var cmd: Dictionary = JSON.parse_string(FileAccess.get_file_as_string(path)) var res: Dictionary = await _do(cmd) _write(n, res) n += 1 if cmd.get("do") == "leave": break quit() func _write(n: int, d: Dictionary) -> void: var f := FileAccess.open("%s/res_%d.json" % [dir, n], FileAccess.WRITE) f.store_string(JSON.stringify(d)) f.close() 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 _do(cmd: Dictionary) -> Dictionary: var WN: Node = root.get_node("WorldNet") match String(cmd.get("do", "")): "walk": await walk(player, _p(cmd.to), world) return {"pos": _v(player.global_position), "corrections": corrections} "report": return report(world, WN, cmd, corrections) "take": var pk: Node3D = _pickup(world, WN, String(cmd.key)) var ok: bool = pk != null and root.get_node("Actions").pickup(player, pk) await create_timer(0.6).timeout return {"asked": ok, "inv": player.inventory.to_data()} "chest_take": var c: Node3D = WN.pieces.get(int(cmd.nid)) if c == null: return {"error": "no chest %d" % int(cmd.nid)} root.get_node("Actions").transfer(player, c.inventory, int(cmd.slot), player.inventory, c) await create_timer(0.6).timeout return {"inv": player.inventory.to_data()} "bump": # Walk into whatever's in front, with real collisions. var start := player.global_position var d: Vector3 = _p(cmd.dir).normalized() for _i in range(int(float(cmd.dist) / 0.1)): player.move_and_collide(d * 0.1) await physics_frame await create_timer(0.3).timeout return {"moved": Vector2(player.global_position.x - start.x, player.global_position.z - start.z).length()} "hop": # Cheat: step straight through to the other side. var c0 := corrections player.global_position = _p(cmd.to) await create_timer(1.0).timeout return {"corrected": corrections - c0, "pos": _v(player.global_position)} "leave": net.leave() await create_timer(0.3).timeout return {"left": true} # M2.5 (tests/test_net_life.gd). "tp": root.get_node("PlayerNet").ask("debug_tp", [_p(cmd.to)]) await create_timer(0.6).timeout player.set_physics_process(false) return {"pos": _v(player.global_position)} "ask": var args: Array = [] for x in cmd.get("args", []): args.append(_p(x.v) if x is Dictionary and x.has("v") else x) root.get_node("PlayerNet").ask(String(cmd.what), args) await create_timer(0.4).timeout return {"inv": player.inventory.to_data()} "bed": var b: Node3D = WN.pieces.get(int(cmd.nid)) if b == null: return {"error": "no bed %d" % int(cmd.nid)} root.get_node("Actions").use_bed(player, b) await create_timer(0.5).timeout return {"asked": true} "life": return life(world, root.get_node("CreatureNet"), root.get_node("PlayerNet")) # M2.6 (tests/test_net_rules.gd). "say": root.get_node("Chat").say(String(cmd.text)) await create_timer(0.6).timeout return {"heard": root.get_node("Chat").heard.duplicate()} "heard": return {"heard": root.get_node("Chat").heard.duplicate(), "pvp": bool(net.server_info.get("pvp", false))} return {"error": "unknown command"} ## What this player's game shows of the server's wildlife. static func life(w: Node3D, CN: Node, PN: Node) -> Dictionary: var out := {"puppets": {}, "floats": [], "projectiles": CN.projectiles_seen, "howls": CN.howls_heard, "hurts": PN.hurts, "health": w.player.survival.health, "day": w.day_night.day, "hours": w.day_night.hours(), "statuses": w.player.survival.statuses.duplicate(), "weather": w.weather.state} for cid in CN.puppets: var c: Node3D = CN.puppets[cid] if is_instance_valid(c): out.puppets[str(cid)] = {"kind": c.kind, "pos": _v(c.global_position), "state": c.state, "hp": c.hp} for peer in CN.floats: out.floats.append(peer) return out # Shared with test_net_shared.gd (it loads this script for these). ## Walks (by setting positions, like the game sending its moves) at 5 m/s. static func walk(p: CharacterBody3D, to: Vector3, w: Node3D) -> void: var tree := p.get_tree() var from := p.global_position var dist := Vector2(to.x - from.x, to.z - from.z).length() var steps := maxi(1, int(dist / (5.0 / 60.0))) for i in range(1, steps + 1): var q := from.lerp(to, float(i) / steps) q.y = w.terrain.sample_height(q.x, q.z) + 0.05 p.global_position = q await tree.physics_frame await tree.create_timer(0.4).timeout # let the server hear about it static func _pickup(w: Node3D, WN: Node, key: String) -> Node3D: if key.begins_with("#"): return WN.dropped.get(int(key.substr(1))) return w.props.pickups.get(key) static func report(w: Node3D, WN: Node, cmd: Dictionary, corr: int) -> Dictionary: var out := {"nodes": {}, "pieces": {}, "taken": {}, "inv": w.player.inventory.to_data(), "received": WN.received, "corrections": corr, "dropped": []} for uid in cmd.get("uids", []): var n: Node3D = w.props.resource_nodes.get(uid) out.nodes[uid] = {"hp": n.hp, "depleted": n.depleted} if n != null else null for key in cmd.get("keys", []): var pk: Node3D = _pickup(w, WN, key) out.taken[key] = pk == null or not is_instance_valid(pk) or pk.taken for nid in WN.pieces: var p: Node3D = WN.pieces[nid] if p == null or not is_instance_valid(p) or p.is_queued_for_deletion(): continue var e := {"item": p.item_id, "pos": _v(p.global_position)} if p.get("inventory") != null: e.inv = p.inventory.to_data() if p.get("open") != null: e.open = p.open if p.get("fuel") != null: e.fuel = p.fuel out.pieces[str(nid)] = e for nid in WN.dropped: var d: Node3D = WN.dropped[nid] if d != null and is_instance_valid(d) and not d.taken: out.dropped.append({"nid": nid, "item": d.item_id, "count": d.amount}) return out