extends SceneTree
## Gate: a headless peer's map is a DECLARATION, and the state namespace matches
## the map that is actually loaded.
##
## THE BUG THIS EXISTS FOR, two of them in six lines. game.gd's `_ready` had:
##
## if DisplayServer.get_name() == "headless":
## for child in maps_node.get_children(): child.queue_free()
## var server_map := load("res://scenes/maps/map_server.tscn")
##
## 1. The map is hardcoded. spec/bot-testbed.md (e1) lists "a map argument for
## headless, so a peer-bot's map is a declaration rather than a hardcoded
## branch" as an obligation of the containment work, and it was never built.
## A peer-bot reaches map_flat only if a map change happens to replicate to
## it, which is the containment question from the other side -- the process
## that runs the bots cannot say which world it is in.
##
## 2. current_map_path was NOT updated. It still held _initial_map_path()
## (map_test, or HYPER_TPS_MAP), while map_server was the scene in the tree.
## Twelve lines later `GameState.current_map_id =
## map_id_from_path(current_map_path)` seeds the per-map state namespace from
## it -- so every map-scoped key on a headless peer, world:bots: and
## world:creatures: included, was namespaced to a map that was not loaded.
## Bot containment is map-scoped, so this lands squarely on the peer-bot.
##
## HYPER_TPS_MAP is reused rather than a new variable invented: it is already the
## declaration for the graphical path, and two ways to say the same thing is how
## the headless and graphical branches drift apart again.
var ok := true
var _ran := false
const HEADLESS_DEFAULT := "res://scenes/maps/map_server.tscn"
const A_REAL_MAP := "res://scenes/maps/map_flat.tscn"
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 headless_map: %s" % ("PASS" if ok else "FAIL"))
if not ok:
quit(1)
return true
func _run() -> void:
var gs: GDScript = load("res://scenes/game/game.gd")
_p(gs != null, "game.gd loads")
if gs == null:
return
_p(ResourceLoader.exists(HEADLESS_DEFAULT), "map_server.tscn exists")
_p(ResourceLoader.exists(A_REAL_MAP), "map_flat.tscn exists")
var had := OS.has_environment("HYPER_TPS_MAP")
var prior := OS.get_environment("HYPER_TPS_MAP") if had else ""
# Presence is checked BEFORE calling. A .call() for a function that does not
# exist raises a SCRIPT ERROR that aborts _run() where it stands -- which on
# the first run of this gate meant no FAIL was ever recorded and it printed
# PASS against a game.gd that had none of this. A gate that cannot fail is
# worse than no gate.
var declared_ok: bool = gs.has_method("_headless_map_path")
_p(declared_ok, "game.gd exposes _headless_map_path()")
if not declared_ok:
return
# ---- undeclared: the server map, i.e. today's behaviour preserved ----
OS.set_environment("HYPER_TPS_MAP", "")
var undeclared: Variant = gs.call("_headless_map_path")
_p(String(undeclared) == HEADLESS_DEFAULT,
"undeclared headless map is map_server (got %s)" % undeclared)
# ---- declared: the declaration wins ----
OS.set_environment("HYPER_TPS_MAP", A_REAL_MAP)
var declared: String = String(gs.call("_headless_map_path"))
_p(declared == A_REAL_MAP,
"HYPER_TPS_MAP declares the headless map (got %s)" % declared)
# ---- declared but nonexistent: fall back, never load nothing ----
OS.set_environment("HYPER_TPS_MAP", "res://scenes/maps/does_not_exist.tscn")
var bogus: String = String(gs.call("_headless_map_path"))
_p(bogus == HEADLESS_DEFAULT,
"a nonexistent declaration falls back to map_server (got %s)" % bogus)
if had:
OS.set_environment("HYPER_TPS_MAP", prior)
else:
OS.set_environment("HYPER_TPS_MAP", "")
# ---- the namespace must follow the loaded map ----
var src: String = FileAccess.get_file_as_string("res://scenes/game/game.gd")
var idx: int = src.find('DisplayServer.get_name() == "headless"')
_p(idx >= 0, "the headless branch is still findable")
if idx < 0:
return
var branch: String = src.substr(idx, 900)
_p(branch.contains("current_map_path"),
"the headless branch assigns current_map_path -- or the state namespace " +
"names a map that is not loaded")
# SELF-CHECK: the pre-fix branch must be detected as missing that assignment.
var old: String = 'if DisplayServer.get_name() == "headless":\n\t\tfor child in maps_node.get_children():\n\t\t\tchild.queue_free()\n\t\tvar server_map := load("res://scenes/maps/map_server.tscn")\n'
_p(not old.contains("current_map_path"),
"self-check: the pre-fix headless branch is detected as not assigning it")