OverviewFilesCommitsIssuesDocsReleasesPeers

hyper-tps / scenes / test / gate_bot_revive.gd

129 lines · gd ref:
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)