OverviewFilesCommitsIssuesDocsReleasesPeers

hyper-tps / scenes / test / gate_bot_declaration_survives.gd

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