M2.6: server rules (PvP, raiding, locks and friends, death penalty, difficulty, wolf grace), chat, admin commands, server console
This commit is contained in:
@@ -131,13 +131,21 @@ func _do(cmd: Dictionary) -> Dictionary:
|
||||
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()}
|
||||
"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):
|
||||
|
||||
@@ -9,7 +9,7 @@ if [ ! -f .godot/global_script_class_cache.cfg ]; then
|
||||
echo "Importing the project first (fresh copy)..."
|
||||
godot --headless --path . --import >/dev/null 2>&1
|
||||
fi
|
||||
for t in test_survival_core test_survival_stats test_save_load test_weather_building test_creatures_combat test_wolves test_den_sites test_fishing_diet test_net_lobby test_net_world test_net_shared test_net_life test_terrain_edges test_boundary; do
|
||||
for t in test_survival_core test_survival_stats test_save_load test_weather_building test_creatures_combat test_wolves test_den_sites test_fishing_diet test_net_lobby test_net_world test_net_shared test_net_life test_net_rules test_terrain_edges test_boundary; do
|
||||
extra=""
|
||||
[ "$t" = "test_boundary" ] && extra="--fixed-fps 30"
|
||||
# A suite that waits on a world that failed to load would hang forever,
|
||||
|
||||
419
tests/test_net_rules.gd
Normal file
419
tests/test_net_rules.gd
Normal file
@@ -0,0 +1,419 @@
|
||||
extends SceneTree
|
||||
## M2.6: server rules, chat and admin commands. Two players on a dedicated
|
||||
## server: chat; who may use and take down whose things (locks, friends,
|
||||
## raiding); PvP off and on; what dying costs (drop_all, keep); the clock
|
||||
## and the weather; kicking, banning and unbanning.
|
||||
##
|
||||
## A real headless server (with --debug-cheats), Alice (this test: the first
|
||||
## player, so the admin) and Bob (tests/net_world_bot.gd).
|
||||
## Run: godot --headless --path . --script res://tests/test_net_rules.gd
|
||||
|
||||
const PORT := 24349
|
||||
const DIR := "user://test_net"
|
||||
const SRV := DIR + "/server7"
|
||||
const BOBS := DIR + "/bob_rules"
|
||||
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 PN: Node
|
||||
var A: Node
|
||||
var C: Node
|
||||
var world: Node3D
|
||||
var player: CharacterBody3D
|
||||
var _bob_dir := ""
|
||||
var _cmd := 0
|
||||
var _bobs := 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")
|
||||
PN = root.get_node("PlayerNet")
|
||||
A = root.get_node("Actions")
|
||||
C = root.get_node("Chat")
|
||||
A.notify.connect(func(t): _toasts.append(t))
|
||||
for d in [SRV, BOBS]:
|
||||
_rm_tree(d)
|
||||
_spawn(["--server", "--port", str(PORT), "--config", SRV + "/server.json",
|
||||
"--server-name", "Rules Test", "--seed", str(SEED), "--debug-cheats", "--no-creatures"])
|
||||
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(BOBS)
|
||||
print("RESULT: %s (%d failures)" % ["PASS" if _fails == 0 else "FAIL", _fails])
|
||||
quit(1 if _fails > 0 else 0)
|
||||
|
||||
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, the first player, so she's the admin (%s)" % r)
|
||||
if r != "":
|
||||
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)
|
||||
await _until(func(): return WN.received >= 1, 10.0)
|
||||
var b0 := await _new_bob()
|
||||
check(bool(b0.get("ok", false)), "Bob joins (%s)" % b0.get("why", "ok"))
|
||||
if not bool(b0.get("ok", false)):
|
||||
return
|
||||
await _chat()
|
||||
await _locks()
|
||||
await _pvp()
|
||||
await _death()
|
||||
await _clock_and_weather()
|
||||
await _kick_and_ban()
|
||||
world.queue_free()
|
||||
await process_frame
|
||||
Net.leave()
|
||||
_finished = true
|
||||
|
||||
# --- chat ------------------------------------------------------------------------
|
||||
|
||||
func _chat() -> void:
|
||||
print("chat")
|
||||
C.say("hello everyone")
|
||||
var h := await _bob_heard("Alice: hello everyone")
|
||||
check(h, "Alice says hello: Bob sees 'Alice: hello everyone'")
|
||||
await _bob({"do": "say", "text": "hi Alice"})
|
||||
await _until(func(): return C.heard.has("Bob: hi Alice"), 2.0)
|
||||
check(C.heard.has("Bob: hi Alice"), "and Alice sees Bob's answer")
|
||||
await _bob({"do": "say", "text": "/rules"})
|
||||
check(await _bob_heard_prefix("PvP off · raiding off · locks owner_and_friends"), "Bob asks for /rules: co-op by default (PvP off, raiding off, locks for owner and friends)")
|
||||
await _bob({"do": "say", "text": "/set pvp true"})
|
||||
check(await _bob_heard("Only admins can use /set"), "Bob isn't an admin: he can't /set")
|
||||
|
||||
# --- locks, friends, raiding ---------------------------------------------------------
|
||||
|
||||
func _locks() -> void:
|
||||
print("locks and friends")
|
||||
var site: Vector3 = world.build.debug_find_site(player.global_position + Vector3(8, 0, 0), 3.0)
|
||||
await _tp(site + Vector3(-2.5, 0, 0))
|
||||
var chest := await _place("chest", site)
|
||||
var door := await _place("wood_door", site + Vector3(0, 0, 4))
|
||||
check(chest != null and door != null, "Alice places a chest and a door")
|
||||
if chest == null or door == null:
|
||||
return
|
||||
check(String(chest.get_meta("owner_name", "")) == "Alice", "they're hers (the server says so, and everyone's game knows)")
|
||||
var ss: int = await _give("stone", 6)
|
||||
A.transfer(player, player.inventory, ss, chest.inventory, chest)
|
||||
await _until(func(): return chest.inventory.count("stone") == 6, 2.0)
|
||||
await _bob({"do": "tp", "to": _v(site + Vector3(2.0, 0, 0))})
|
||||
var took := await _bob({"do": "chest_take", "nid": WN.nid(chest), "slot": 0})
|
||||
check(_count(took.get("inv", []), "stone") == 0 and chest.inventory.count("stone") == 6, "Bob can't take from Alice's chest (locked)")
|
||||
await _bob({"do": "ask", "what": "door", "args": [WN.nid(door)]})
|
||||
await _sleep(0.3)
|
||||
check(not door.open, "or open her door")
|
||||
await _bob({"do": "ask", "what": "dismantle", "args": [WN.nid(door)]})
|
||||
await _sleep(0.3)
|
||||
check(WN.pieces.has(WN.nid(door)), "or take it down (raiding is off)")
|
||||
C.say("/friend Bob")
|
||||
check(await _bob_heard("Alice added you as a friend: you can use their doors, chests and beds"), "Alice makes Bob a friend")
|
||||
took = await _bob({"do": "chest_take", "nid": WN.nid(chest), "slot": 0})
|
||||
check(_count(took.get("inv", []), "stone") == 6, "now he can take from her chest")
|
||||
await _bob({"do": "ask", "what": "door", "args": [WN.nid(door)]})
|
||||
await _until(func(): return door.open, 2.0)
|
||||
check(door.open, "and open her door")
|
||||
C.say("/unfriend Bob")
|
||||
await _sleep(0.4)
|
||||
await _bob({"do": "ask", "what": "door", "args": [WN.nid(door)]})
|
||||
await _sleep(0.4)
|
||||
check(door.open, "unfriended: the door stays as it was")
|
||||
print("raiding")
|
||||
C.say("/set raiding true")
|
||||
check(await _bob_heard("Alice set raiding to true"), "Alice turns raiding on (everyone's told)")
|
||||
var dn: int = WN.nid(door)
|
||||
await _bob({"do": "ask", "what": "dismantle", "args": [dn]})
|
||||
await _until(func(): return not WN.pieces.has(dn), 2.0)
|
||||
check(not WN.pieces.has(dn), "now Bob can take her door down")
|
||||
C.say("/set raiding false")
|
||||
await _sleep(0.4)
|
||||
|
||||
## Places `id` on the ground at `at` (build pieces snap) through Actions.
|
||||
func _place(id: String, at: Vector3) -> Node3D:
|
||||
var def: Dictionary = root.get_node("ItemDB").get_item(id)
|
||||
var xf := Transform3D(Basis(), 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, 0.0)
|
||||
if not res.ok:
|
||||
print(" (can't place %s: %s)" % [id, res.reason])
|
||||
return null
|
||||
xf = res.xf
|
||||
var slot: int = await _give(id, 1)
|
||||
var before: Array = WN.pieces.keys()
|
||||
A.place(player, slot, xf, world)
|
||||
var found := {"p": null}
|
||||
await _until(func():
|
||||
for k in WN.pieces:
|
||||
if not k in before and WN.pieces[k].item_id == id:
|
||||
found.p = WN.pieces[k]
|
||||
return true
|
||||
return false, 3.0)
|
||||
return found.p
|
||||
|
||||
# --- PvP ---------------------------------------------------------------------------
|
||||
|
||||
func _pvp() -> void:
|
||||
print("PvP")
|
||||
PN.ask("debug_stats", [{"health": 100.0}])
|
||||
await _bob({"do": "ask", "what": "debug_give", "args": ["club", 1]})
|
||||
await _bob({"do": "ask", "what": "debug_stats", "args": [{"stamina": 100.0}]})
|
||||
var near := player.global_position + Vector3(1.4, 0, 0)
|
||||
near.y = world.terrain.sample_height(near.x, near.z)
|
||||
await _bob({"do": "tp", "to": _v(near)})
|
||||
var slot := await _bob_slot("club")
|
||||
await _bob({"do": "ask", "what": "select", "args": [slot]})
|
||||
await _until(func(): return player.survival.health >= 99.0, 2.0)
|
||||
var h0: float = player.survival.health
|
||||
var hurts0: int = PN.hurts
|
||||
await _bob({"do": "ask", "what": "hit_player", "args": [Net.local_peer]})
|
||||
await _sleep(0.6)
|
||||
check(PN.hurts == hurts0 and player.survival.health >= h0 - 0.5, "with PvP off, Bob's club does nothing to Alice")
|
||||
C.say("/set pvp true")
|
||||
await _until(func(): return bool(Net.server_info.get("pvp", false)), 2.0)
|
||||
check(bool(Net.server_info.get("pvp", false)), "Alice turns PvP on, and every game hears the new rules")
|
||||
await _bob({"do": "ask", "what": "hit_player", "args": [Net.local_peer]})
|
||||
await _until(func(): return PN.hurts > hurts0, 2.0)
|
||||
await _sleep(0.3)
|
||||
check(PN.hurts > hurts0 and player.survival.health < h0 - 5.0, "now his club hurts her (health %.0f)" % player.survival.health)
|
||||
# An arrow, too.
|
||||
await _bob({"do": "ask", "what": "debug_give", "args": ["bow", 1]})
|
||||
await _bob({"do": "ask", "what": "debug_give", "args": ["arrow", 3]})
|
||||
var far := player.global_position + Vector3(7.0, 0, 0)
|
||||
far.y = world.terrain.sample_height(far.x, far.z)
|
||||
await _bob({"do": "tp", "to": _v(far)})
|
||||
await _bob({"do": "ask", "what": "select", "args": [await _bob_slot("bow")]})
|
||||
var h1: float = player.survival.health
|
||||
var aim: Vector3 = (player.global_position + Vector3(0, 1.0, 0)) - (far + Vector3(0, 1.6, 0))
|
||||
await _bob({"do": "ask", "what": "shoot", "args": [{"v": _v(aim.normalized())}, 1.0]})
|
||||
await _until(func(): return player.survival.health < h1 - 1.0, 3.0)
|
||||
check(player.survival.health < h1 - 1.0, "and so does an arrow (health %.0f)" % player.survival.health)
|
||||
C.say("/set pvp false")
|
||||
await _sleep(0.4)
|
||||
|
||||
# --- death penalties -------------------------------------------------------------------
|
||||
|
||||
func _death() -> void:
|
||||
print("death penalties")
|
||||
C.say("/set death_penalty drop_all")
|
||||
await _sleep(0.4)
|
||||
await _clear()
|
||||
await _give("stone", 5)
|
||||
var at := player.global_position
|
||||
var graves0 := _graves()
|
||||
var d0: int = WN.dropped.size()
|
||||
PN.ask("debug_die")
|
||||
await _until(func(): return WN.dropped.size() > d0, 3.0)
|
||||
check(_graves() == graves0 and WN.dropped.size() > d0 and player.inventory.count("stone") == 0,
|
||||
"drop_all: no grave, her stone is lying on the ground where she fell, for anyone")
|
||||
var near_drop: bool = WN.dropped.values().any(func(d): return is_instance_valid(d) and d.item_id == "stone" and d.global_position.distance_to(at) < 3.0)
|
||||
check(near_drop, "(right where she died)")
|
||||
await _until(func(): return not world.dead_waiting, 6.0)
|
||||
C.say("/set death_penalty keep")
|
||||
await _sleep(0.4)
|
||||
await _give("flint", 4)
|
||||
PN.ask("debug_die")
|
||||
await _until(func(): return world.dead_waiting, 3.0)
|
||||
await _until(func(): return not world.dead_waiting, 6.0)
|
||||
await _sleep(0.4)
|
||||
check(player.inventory.count("flint") == 4 and _graves() == graves0, "keep: she wakes with her flint, and there's no grave")
|
||||
C.say("/set death_penalty grave")
|
||||
await _sleep(0.4)
|
||||
|
||||
func _graves() -> int:
|
||||
return WN.pieces.values().filter(func(p): return is_instance_valid(p) and p.item_id == "grave").size()
|
||||
|
||||
# --- clock and weather ---------------------------------------------------------------
|
||||
|
||||
func _clock_and_weather() -> void:
|
||||
print("the clock and the weather")
|
||||
C.say("/time 22:30")
|
||||
await _until(func(): return absf(world.day_night.hours() - 22.5) < 0.2, 2.0)
|
||||
check(absf(world.day_night.hours() - 22.5) < 0.2, "/time 22:30: it's night for Alice (%s)" % world.day_night.clock_text())
|
||||
var life := await _bob({"do": "life"})
|
||||
check(absf(float(life.get("hours", 0.0)) - 22.5) < 0.2, "and for Bob (%.2f h)" % float(life.get("hours", 0.0)))
|
||||
C.say("/weather storm")
|
||||
await _until(func(): return world.weather.state == "storm", 3.0)
|
||||
life = await _bob({"do": "life"})
|
||||
check(world.weather.state == "storm" and String(life.get("weather", "")) == "storm", "/weather storm: a storm for both (Alice %s, Bob %s)" % [world.weather.state, life.get("weather", "?")])
|
||||
C.say("/weather auto")
|
||||
await _sleep(0.4)
|
||||
check(world.weather.override == "", "/weather auto: back to the island's own weather")
|
||||
|
||||
# --- kick, ban ---------------------------------------------------------------------------
|
||||
|
||||
func _kick_and_ban() -> void:
|
||||
print("kick and ban")
|
||||
C.say("/kick Bob")
|
||||
await _until(func(): return Net.players.values().all(func(p): return p.name != "Bob"), 4.0)
|
||||
check(Net.players.values().all(func(p): return p.name != "Bob"), "/kick Bob: he's gone")
|
||||
var b := await _new_bob()
|
||||
check(bool(b.get("ok", false)), "he can come back")
|
||||
C.say("/ban Bob")
|
||||
await _until(func(): return Net.players.values().all(func(p): return p.name != "Bob"), 4.0)
|
||||
check(Net.players.values().all(func(p): return p.name != "Bob"), "/ban Bob: gone again")
|
||||
b = await _new_bob()
|
||||
check(not bool(b.get("ok", true)) and String(b.get("why", "")).contains("banned"), "and can't come back (%s)" % b.get("why", ""))
|
||||
C.say("/unban Bob")
|
||||
await _sleep(0.5)
|
||||
b = await _new_bob()
|
||||
check(bool(b.get("ok", false)), "/unban Bob: he can join again")
|
||||
await _bob({"do": "leave"})
|
||||
|
||||
# --- helpers --------------------------------------------------------------------
|
||||
|
||||
## A fresh Bob (his own command folder; the same identity every time).
|
||||
func _new_bob() -> Dictionary:
|
||||
_bobs += 1
|
||||
_bob_dir = "%s/b%d" % [BOBS, _bobs]
|
||||
DirAccess.make_dir_recursive_absolute(_bob_dir)
|
||||
_cmd = 0
|
||||
_spawn(["--identity", DIR + "/bob", "--name", "Bob", "--address", "127.0.0.1:%d" % PORT, "--dir", _bob_dir],
|
||||
"res://tests/net_world_bot.gd")
|
||||
return await _bob_result(0, 30.0)
|
||||
|
||||
func _bob_heard(line: String) -> bool:
|
||||
for _i in range(15):
|
||||
var h := await _bob({"do": "heard"})
|
||||
if (h.get("heard", []) as Array).has(line):
|
||||
return true
|
||||
await _sleep(0.2)
|
||||
return false
|
||||
|
||||
func _bob_heard_prefix(start: String) -> bool:
|
||||
for _i in range(15):
|
||||
var h := await _bob({"do": "heard"})
|
||||
if (h.get("heard", []) as Array).any(func(l): return String(l).begins_with(start)):
|
||||
return true
|
||||
await _sleep(0.2)
|
||||
return false
|
||||
|
||||
func _bob_slot(id: String) -> int:
|
||||
var rep := await _bob({"do": "report"})
|
||||
var inv: Array = rep.get("inv", [])
|
||||
for i in range(inv.size()):
|
||||
if inv[i] != null and inv[i].id == id:
|
||||
return i
|
||||
return -1
|
||||
|
||||
func _tp(at: Vector3) -> void:
|
||||
PN.ask("debug_tp", [at + Vector3(0, 0.05, 0)])
|
||||
await _until(func(): return player.global_position.distance_to(at) < 1.0, 2.0)
|
||||
player.set_physics_process(false)
|
||||
await _sleep(0.3)
|
||||
|
||||
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(): return player.inventory.slots.all(func(s): return s == null), 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()
|
||||
|
||||
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]
|
||||
|
||||
func _bob(cmd: Dictionary) -> Dictionary:
|
||||
_cmd += 1
|
||||
var f := FileAccess.open("%s/cmd_%d.json" % [_bob_dir, _cmd], FileAccess.WRITE)
|
||||
f.store_string(JSON.stringify(cmd))
|
||||
f.close()
|
||||
return await _bob_result(_cmd, 20.0)
|
||||
|
||||
func _bob_result(n: int, limit: float) -> Dictionary:
|
||||
var path := "%s/res_%d.json" % [_bob_dir, 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, "heard": [], "inv": [], "pieces": {}}
|
||||
|
||||
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
|
||||
|
||||
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)
|
||||
@@ -307,8 +307,12 @@ func _containers_and_fires() -> void:
|
||||
var s = _chest.inventory.get_slot(i)
|
||||
if s != null and s.id == "wood":
|
||||
slot = i
|
||||
# (Chests are locked to their owner and friends by default: M2.6.)
|
||||
root.get_node("Chat").say("/friend Bob")
|
||||
await _sleep(0.5)
|
||||
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)")
|
||||
check(_count(took.get("inv", []), "wood") == 10, "Bob (her friend) takes the wood out (shift-click)")
|
||||
root.get_node("Chat").say("/unfriend Bob") # (friends can loot each other's graves too)
|
||||
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")
|
||||
|
||||
|
||||
Reference in New Issue
Block a user