M2.4: the server decides online (characters, inventory, stats, death, owned graves, per-player saves); death screen with Respawn after 3 s
This commit is contained in:
@@ -5,6 +5,10 @@ extends SceneTree
|
||||
## into them, for real and by cheating), doors, chests, fires, dismantling,
|
||||
## and the whole lot surviving a server restart.
|
||||
##
|
||||
## M2.4: the server decides. Alice's inventory, crafting, eating and stats
|
||||
## live on the server; a game that edits its own bag gets nothing for it;
|
||||
## graves belong to whoever died; characters are saved per player.
|
||||
##
|
||||
## 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.
|
||||
@@ -22,6 +26,7 @@ var _finished := false
|
||||
var _pids: Array[int] = []
|
||||
var Net: Node
|
||||
var WN: Node
|
||||
var PN: Node
|
||||
var A: Node
|
||||
var world: Node3D
|
||||
var player: CharacterBody3D
|
||||
@@ -41,6 +46,7 @@ func _run() -> void:
|
||||
Engine.max_fps = 60
|
||||
Net = root.get_node("Net")
|
||||
WN = root.get_node("WorldNet")
|
||||
PN = root.get_node("PlayerNet")
|
||||
A = root.get_node("Actions")
|
||||
A.notify.connect(func(t): _toasts.append(t))
|
||||
for d in [SRV, BOB]:
|
||||
@@ -62,7 +68,7 @@ func _run() -> void:
|
||||
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"])
|
||||
"--server-name", "Shared Test", "--seed", str(SEED), "--debug-regrow", "0.002", "--debug-cheats"])
|
||||
|
||||
func _body() -> void:
|
||||
var me = load("res://scripts/net/identity.gd").open(DIR + "/a")
|
||||
@@ -83,6 +89,7 @@ func _body() -> void:
|
||||
if not bool(b0.get("ok", false)):
|
||||
return
|
||||
|
||||
await _server_decides()
|
||||
await _trees()
|
||||
await _building()
|
||||
await _containers_and_fires()
|
||||
@@ -90,6 +97,66 @@ func _body() -> void:
|
||||
await _restart()
|
||||
_finished = true
|
||||
|
||||
# --- the server decides -----------------------------------------------------------
|
||||
|
||||
func _server_decides() -> void:
|
||||
print("the server decides")
|
||||
await _sleep(1.0)
|
||||
check(not player.survival.is_processing() and player.survival.hunger < 100.0 and player.survival.hunger > 90.0,
|
||||
"Alice's stats run on the server and come to her (hunger %.2f)" % player.survival.hunger)
|
||||
await _give("stick", 2)
|
||||
await _give("flint", 2)
|
||||
A.craft(player, "stone_axe")
|
||||
await _until(func(): return player.inventory.count("stone_axe") == 1, 2.0)
|
||||
check(player.inventory.count("stone_axe") == 1 and player.inventory.count("stick") == 0 and player.inventory.count("flint") == 0,
|
||||
"she crafts a stone axe: the server takes the sticks and flint and gives her the axe")
|
||||
# A modified game puts walls in its own bag and tries to build with them.
|
||||
var fake := 7
|
||||
player.inventory.set_slot(fake, {"id": "wood_wall", "count": 5})
|
||||
var n0: int = WN.pieces.size()
|
||||
var front := player.global_position - player.global_basis.z * 3.0
|
||||
A.place(player, fake, Transform3D(Basis(), Vector3(front.x, world.terrain.sample_height(front.x, front.z), front.z)), world)
|
||||
await _sleep(0.8)
|
||||
check(WN.pieces.size() == n0 and player.inventory.get_slot(fake) == null,
|
||||
"walls a game invents in its own bag build nothing, and the server's copy of her bag replaces it")
|
||||
var bs: int = await _give("berries", 3)
|
||||
PN.ask("debug_stats", [{"hunger": 50.0, "diet": {"plants": 10.0}}])
|
||||
await _until(func(): return player.survival.hunger < 51.0, 2.0)
|
||||
A.eat(player, bs)
|
||||
await _until(func(): return player.survival.hunger > 55.0, 2.0)
|
||||
check(player.survival.hunger > 55.0 and float(player.survival.diet.plants) > 20.0 and player.inventory.count("berries") == 2,
|
||||
"she eats a berry on the server: hunger %.0f, greens %.0f" % [player.survival.hunger, float(player.survival.diet.plants)])
|
||||
|
||||
## Test-only (the server runs with --debug-cheats): items into Alice's bag on
|
||||
## the server. Returns the slot they're in.
|
||||
func _give(id: String, n: int) -> int:
|
||||
var before: int = player.inventory.count(id)
|
||||
PN.ask("debug_give", [id, n])
|
||||
await _until(func(): return player.inventory.count(id) >= before + n, 3.0)
|
||||
return _slot_of(id)
|
||||
|
||||
func _clear() -> void:
|
||||
PN.ask("debug_clear")
|
||||
await _until(func():
|
||||
for s in player.inventory.slots:
|
||||
if s != null:
|
||||
return false
|
||||
return true, 3.0)
|
||||
|
||||
func _slot_of(id: String) -> int:
|
||||
for i in range(player.inventory.size()):
|
||||
var s = player.inventory.get_slot(i)
|
||||
if s != null and s.id == id:
|
||||
return i
|
||||
return -1
|
||||
|
||||
func _until(cond: Callable, limit: float) -> bool:
|
||||
var t := 0.0
|
||||
while not cond.call() and t < limit:
|
||||
await _sleep(0.05)
|
||||
t += 0.05
|
||||
return cond.call()
|
||||
|
||||
# --- trees --------------------------------------------------------------------
|
||||
|
||||
func _trees() -> void:
|
||||
@@ -99,13 +166,12 @@ func _trees() -> void:
|
||||
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)
|
||||
player.select(_slot_of("stone_axe"))
|
||||
await _sleep(0.2)
|
||||
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)
|
||||
@@ -185,12 +251,14 @@ func _place(id: String, at: Vector3, yaw: float) -> Node3D:
|
||||
print(" (can't place %s: %s)" % [id, res.reason])
|
||||
return null
|
||||
xf = res.xf
|
||||
player.inventory.set_slot(5, {"id": id, "count": 1})
|
||||
var slot: int = await _give(id, 1)
|
||||
var had: int = player.inventory.count(id)
|
||||
var before: Array = WN.pieces.keys()
|
||||
if not A.place(player, 5, xf, world):
|
||||
if not A.place(player, slot, xf, world):
|
||||
print(" (Actions.place refused %s)" % id)
|
||||
return null
|
||||
check(player.inventory.get_slot(5) == null, "(%s taken from Alice's bag)" % id)
|
||||
await _until(func(): return player.inventory.count(id) == had - 1, 2.0)
|
||||
check(player.inventory.count(id) == had - 1, "(%s taken from Alice's bag by the server)" % id)
|
||||
var t := 0.0
|
||||
while t < 3.0:
|
||||
for k in WN.pieces:
|
||||
@@ -213,18 +281,22 @@ func _containers_and_fires() -> void:
|
||||
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)")
|
||||
await _clear()
|
||||
var ws: int = await _give("wood", 10)
|
||||
A.transfer(player, player.inventory, ws, _chest.inventory, _chest)
|
||||
await _until(func(): return _chest.inventory.count("wood") == 10, 2.0)
|
||||
check(player.inventory.count("wood") == 0 and _chest.inventory.count("wood") == 10, "she puts 10 wood in (shift-click)")
|
||||
var ss: int = await _give("stone", 3)
|
||||
var cur := {"c": "waiting"}
|
||||
var on_cur := func(c): cur.c = c
|
||||
PN.cursor_changed.connect(on_cur)
|
||||
PN.ask("inv_click", [0, ss, false]) # pick the stone up...
|
||||
PN.ask("inv_click", [WN.nid(_chest), 3, false]) # ...and put it in slot 4
|
||||
await _until(func(): return _chest.inventory.get_slot(3) != null, 2.0)
|
||||
await _sleep(0.2)
|
||||
PN.cursor_changed.disconnect(on_cur)
|
||||
check(_chest.inventory.get_slot(3) != null and _chest.inventory.get_slot(3).id == "stone" and cur.c == null,
|
||||
"and drags 3 stone into slot 4 by hand (the server does both clicks)")
|
||||
var nid: int = WN.nid(_chest)
|
||||
await _bob({"do": "walk", "to": _v(site + Vector3(1.8, 0, 0))})
|
||||
var rep := await _bob({"do": "report"})
|
||||
@@ -248,20 +320,23 @@ func _containers_and_fires() -> void:
|
||||
if _fire == null:
|
||||
return
|
||||
var f0: float = _fire.fuel
|
||||
player.inventory.set_slot(7, {"id": "wood", "count": 2})
|
||||
await _give("wood", 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)))
|
||||
await _until(func(): return "Warm" in player.survival.statuses, 2.0)
|
||||
check("Warm" in player.survival.statuses and player.survival.heat > 0.25,
|
||||
"standing by it, the server says she's Warm (heat %.2f)" % player.survival.heat)
|
||||
|
||||
# --- 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))
|
||||
PN.ask("debug_drop", ["flint", 2, at]) # (only the server drops things now)
|
||||
var t := 0.0
|
||||
var key := ""
|
||||
while key.is_empty() and t < 3.0:
|
||||
@@ -270,7 +345,7 @@ func _items_on_the_ground() -> void:
|
||||
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")
|
||||
check(not key.is_empty(), "2 flint dropped on the ground: 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))})
|
||||
@@ -295,11 +370,12 @@ func _items_on_the_ground() -> void:
|
||||
"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})
|
||||
await _clear()
|
||||
await _give("stone", 7)
|
||||
await _give("berries", 4)
|
||||
var died_at := player.global_position
|
||||
world._on_player_died()
|
||||
PN.ask("debug_die")
|
||||
var died_t := Time.get_ticks_msec()
|
||||
var grave: Node3D = null
|
||||
var t2 := 0.0
|
||||
while grave == null and t2 < 3.0:
|
||||
@@ -308,14 +384,24 @@ func _items_on_the_ground() -> void:
|
||||
grave = p
|
||||
await _sleep(0.05)
|
||||
t2 += 0.05
|
||||
await _sleep(0.3)
|
||||
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")
|
||||
"Alice dies (on the server): her things go into a grave in the shared world")
|
||||
check(world.dead_waiting and player.global_position.distance_to(died_at) < 1.0,
|
||||
"she stays down on the death screen for a moment")
|
||||
await _until(func(): return not world.dead_waiting, 6.0)
|
||||
var down := (Time.get_ticks_msec() - died_t) / 1000.0
|
||||
check(not world.dead_waiting and player.global_position.distance_to(died_at) > 3.0 and down >= world.RESPAWN_DELAY - 0.2,
|
||||
"then respawns (after %.1f s), and the server wakes her at the island start" % down)
|
||||
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 _bob({"do": "walk", "to": _v(grave.global_position + Vector3(-1.5, 0, 0))})
|
||||
var bt := await _bob({"do": "chest_take", "nid": gnid, "slot": 0})
|
||||
check(_count(bt.get("inv", []), "stone") == 0 and grave.inventory.count("stone") == 7, "but it's hers: Bob can't take from it")
|
||||
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:
|
||||
@@ -355,6 +441,9 @@ func _restart() -> void:
|
||||
for k in WN.pieces:
|
||||
expect[k] = WN.pieces[k].item_id
|
||||
var chest_nid: int = WN.nid(_chest) if _chest != null else -1
|
||||
await _give("flint", 3) # (a change, so the save comes after it)
|
||||
var bag: Array = player.inventory.to_data()
|
||||
var where := player.global_position
|
||||
# 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.
|
||||
@@ -385,6 +474,9 @@ func _restart() -> void:
|
||||
if p.item_id == "wood_door":
|
||||
d = p
|
||||
check(d != null and d.open, "and the door is still open")
|
||||
await _sleep(0.5)
|
||||
check(player.inventory.to_data() == bag, "Alice's bag is just as she left it (her character is saved on the server)")
|
||||
check(player.global_position.distance_to(where) < 2.0, "and she's back where she was (%.1f m off)" % player.global_position.distance_to(where))
|
||||
world.queue_free()
|
||||
await process_frame
|
||||
Net.leave()
|
||||
|
||||
Reference in New Issue
Block a user