extends SceneTree
## Gate: entering a map brings its declared bots back to LIFE, not just to sight.
##
## A bot's declaration is durable and map-scoped: map:<id>:world:bots:registry:<bot>.
## Teardown was wired (map_changing despawns every map-bound bot, spec/bot-testbed.md
## (e1): "a bot stays in the map it was created in") but SETUP never was. Re-entering
## the map delivered the row to _on_state_changed, which built a passive REMOTE
## MARKER -- a coloured box with no brain -- because that path exists to render bots
## OTHER peers are simulating. So a declared bot could never run again after the
## first map change, and the map with the nav volume was the one no bot lived in.
##
## Two things are asserted, because either alone is a false pass:
##
## 1. The authority REVIVES: the bot is in the roster and its BRAIN IS RUNNING.
## beacon.gd writes state:bot:beacon:alive in _bot_init, so a running brain is
## observable from shared state rather than inferred from a node existing.
##
## 2. Revival RETRIES while the container is missing. _container_for resolves
## Game.current_map, and the durable reload can land before the map node is in
## the tree -- the same one-shot trap that made walker.gd dead on arrival, and
## the reason the old remote-marker path silently rendered nothing. A row that
## arrives too early must be held and revived once the map exists.
##
## ⚠ A LEGACY ROW CANNOT BE REVIVED and this gate says so rather than hiding it.
## Rows written before this change carry "brain": "script_file" -- the KIND only,
## with no path and no params -- so there is nothing to start. They stay markers.
var ok := true
var _frame := 0
var _staged := false
var _container: Node3D = null
const LIVE_ID := "revived-beacon"
const LATE_ID := "revived-late"
const LEGACY_ID := "revived-legacy"
const BEACON_ALIVE := "state:bot:beacon:alive"
const MAP_ID := "gatemap"
func _p(c: bool, m: String) -> void:
print(("ok " if c else "FAIL: ") + m)
if not c:
ok = false
func _reg(br: Node) -> Node:
return br
func _has_bot(br: Node, id: String) -> bool:
for row: Variant in br.list():
if row is Dictionary and String((row as Dictionary).get("id", "")) == id:
return true
return false
func _process(_delta: float) -> bool:
_frame += 1
var br: Node = root.get_node_or_null("BotRegistry")
var gs: Node = root.get_node_or_null("GameState")
if br == null or gs == null:
print("FAIL: BotRegistry/GameState autoloads missing")
print("GATE bot_revive: FAIL")
quit(1)
return true
if not _staged:
_staged = true
gs.install_routes()
_stage(br, gs)
return false
# Frame 2: the body=none revival needs no container and must already have run.
if _frame == 2:
_p(_has_bot(br, LIVE_ID), "the declared bot was revived into the roster")
_p(bool(gs.get_state(BEACON_ALIVE, false)),
"and its BRAIN ran -- %s is set" % BEACON_ALIVE)
_p(not _has_bot(br, LEGACY_ID),
"a legacy row (brain kind only, no path) is NOT revived")
# The position-bodied row arrived with no map in the tree. It must be held,
# not dropped -- and must not have been revived yet.
_p(not _has_bot(br, LATE_ID),
"a row whose map does not exist yet is not revived early")
# Now the map appears, exactly as _restore_saved_map makes it appear.
_container = Node3D.new()
_container.name = "GateMap"
root.add_child(_container)
br.map_container = _container
return false
if _frame >= 5:
_p(_has_bot(br, LATE_ID),
"and IS revived once its map exists (the held row was retried)")
print("GATE bot_revive: %s" % ("PASS" if ok else "FAIL"))
if not ok:
quit(1)
else:
quit(0)
return true
return false
func _stage(br: Node, gs: Node) -> void:
_p(not _has_bot(br, LIVE_ID), "the bot does not exist before its row arrives")
# ⛔ THE ROWS MUST BE MAP-SCOPED, as a real room stores them. world:bots: is a
# map-scoped prefix, so _unscope DROPS an un-namespaced key ("legacy, fresh
# start per map") and the gate would deliver nothing at all while appearing to.
gs.current_map_id = MAP_ID
var entries := {}
# Full declaration: kind + path, so there is something to start.
entries["map:" + MAP_ID + ":world:bots:registry:" + LIVE_ID] = {
"body": "none",
"brain": {"kind": "script_file", "path": "res://bots/beacon.gd"},
"ts": 1.0,
}
# Same, but position-bodied, so it needs a container that does not exist yet.
entries["map:" + MAP_ID + ":world:bots:registry:" + LATE_ID] = {
"body": "position",
"brain": {"kind": "none"},
"ts": 2.0,
}
# Legacy shape: the kind as a bare String, no path.
entries["map:" + MAP_ID + ":world:bots:registry:" + LEGACY_ID] = {
"body": "none",
"brain": "script_file",
"ts": 3.0,
}
gs._on_durable_loaded("gate-room", entries)