M2.3: the shared world (trees, pickups, pieces, chests, fires, doors, graves, clock on the server)
This commit is contained in:
159
tests/net_world_bot.gd
Normal file
159
tests/net_world_bot.gd
Normal file
@@ -0,0 +1,159 @@
|
||||
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}
|
||||
return {"error": "unknown command"}
|
||||
|
||||
# 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
|
||||
Reference in New Issue
Block a user