extends SceneTree
## Gate: a map change ends the BOT, not its DECLARATION.
##
## despawn() erased the durable registry row for any position-bodied bot:
##
## # the durable row is what tells every OTHER peer the bot is gone
## state.erase_state(_key_registry(db) + id)
##
## which is right for an EXPLICIT despawn and wrong for a containment teardown.
## _on_map_changing despawns every map-bound bot on a map change, so leaving a map
## DELETED the declarations of the bots that belong to it -- and re-entering had
## nothing left to read. Measured on stortron: map_flat's durable load delivered
## 145 entries every poll and revived nothing, because the rows had been erased on
## the way out.
##
## The row was doing two incompatible jobs:
## DECLARATION — "this bot belongs in this map", written by register(), retired
## only when someone actually retires the bot.
## PRESENCE — "this bot is alive right now", which is the POSITION row and is
## realtime.
##
## Erasing the position row still tells other peers the body is gone, so the
## original concern (a stale visual resurrecting on a peer that reloads) is kept.
##
## ⚠ body=none bots write no rows at all, so they are not part of this.
var ok := true
var _ran := false
const MAP_ID := "gatemap"
const BOT := "decl-survivor"
func _p(c: bool, m: String) -> void:
print(("ok " if c else "FAIL: ") + m)
if not c:
ok = false
func _process(_delta: float) -> bool:
if _ran:
return true
_ran = true
_run()
print("GATE bot_declaration_survives: %s" % ("PASS" if ok else "FAIL"))
if not ok:
quit(1)
return true
func _run() -> void:
var br: Node = root.get_node_or_null("BotRegistry")
var gs: Node = root.get_node_or_null("GameState")
_p(br != null and gs != null, "BotRegistry and GameState are present")
if br == null or gs == null:
return
# world:bots:registry: is role "mod" and a gate holds no room role, so the real
# GameState correctly refuses the row. The subject here is WHICH rows despawn
# erases, not the role gate, so use the established double.
var st: Node = load("res://scenes/test/permissive_state.gd").new()
br.state = st
var map := Node3D.new()
map.name = "GateMap"
root.add_child(map)
br.map_container = map
var res: Dictionary = br.register(BOT, ["map"], "position",
{"kind": "script_file", "path": "res://bots/beacon.gd"})
_p(bool(res.get("ok", false)), "a map-bound position bot registers (%s)" % str(res.get("error", "")))
if not bool(res.get("ok", false)):
return
var reg_key: String = "world:bots:registry:" + BOT
var pos_key: String = "world:bots:pos:" + BOT
_p(bool(st.has_state(reg_key)), "its DECLARATION is durable")
_p(bool(st.has_state(pos_key)), "and its position row exists")
# The map ends, exactly as _on_map_changing does it.
br._on_map_changing(MAP_ID)
_p(not bool(st.has_state(pos_key)),
"a map change clears the POSITION row (other peers stop rendering it)")
_p(bool(st.has_state(reg_key)),
"but the DECLARATION survives, so re-entering the map can revive it")
# And an explicit retirement really does retire it.
br.map_container = map
var res2: Dictionary = br.register(BOT, ["map"], "position",
{"kind": "script_file", "path": "res://bots/beacon.gd"})
_p(bool(res2.get("ok", false)), "it can be registered again")
br.despawn(BOT, "explicit")
_p(not bool(st.has_state(reg_key)),
"an EXPLICIT despawn DOES retire the declaration")