203 lines
7.6 KiB
GDScript
203 lines
7.6 KiB
GDScript
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):
|
|
## <dir>/cmd_<n>.json -> runs it -> <dir>/res_<n>.json
|
|
## Commands: walk, report, take, chest_take, bump, hop, leave.
|
|
## Args (after --): --identity <dir> --name <name> --address host:port --dir <cmd 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
|