extends Node
## GATE — the map scope follows the LOADED map (user 2026-10-06: "we are saving map_test
## in user and also in the game directory and we are having link issues").
##
## The race: change_map -> GameState.enter_map(<new>) starts a room fetch; change_map
## writes world:settings:current_map = <new> only AFTER the map has loaded. The fetch's
## answer carries the OLD saved map, and _on_durable_loaded's "pass 1" adopted it —
## current_map_id flipped back to map_test while map_mountain was loaded, and every
## map-scoped write after that (the sky panel's world:sky:*, terrain edits, objects)
## landed in map_test's namespace. Seen in a settings dump: map_scene map_mountain.tscn,
## meta.map_id "map_test".
## R1 after enter_map("map_mountain"), a room load whose saved current_map is
## map_test does NOT move the scope: current_map_id stays map_mountain
## R2 that load's map_mountain keys are un-scoped (visible), map_test's are not
## R3 a COLD join (no map entered yet) still adopts the room's saved map
##
## CI=true HYPERTPS_DAEMON_TCP=127.0.0.1:1 "$HYPER_TPS_GODOT" --headless --path . \
## res://scenes/test/gate_map_scope_race.tscn
var _fails := 0
var _checks := 0
func _p(c: bool, m: String, got: Variant = "") -> void:
_checks += 1
print(("ok " if c else "FAIL: ") + m + ("" if str(got) == "" else " (" + str(got) + ")"))
if not c:
_fails += 1
func _ready() -> void:
_run.call_deferred()
func _stale_load() -> Dictionary:
return {
"world:settings:current_map": {"path": "res://scenes/maps/map_test.tscn", "ts": 1.0},
"map:map_mountain:world:sky:fog_end": 3000.0,
"map:map_test:world:sky:fog_end": 1000.0,
}
func _run() -> void:
var gs: Node = get_node("/root/GameState")
# ---- R3 first: a cold join (fresh scope state) adopts the room's map ----
gs.set("_map_entered", false)
gs.current_map_id = "map_test"
gs._on_durable_loaded("gate-room", {"world:settings:current_map": {"path": "res://scenes/maps/map_flat.tscn"}})
_p(String(gs.current_map_id) == "map_flat", "R3 a cold join adopts the room's saved map", gs.current_map_id)
# ---- R1 / R2: an explicit map entry is not overridden by a stale load ----
gs.enter_map("map_mountain")
gs._on_durable_loaded("gate-room", _stale_load())
_p(String(gs.current_map_id) == "map_mountain", "R1 a stale room load does not move the scope off the loaded map", gs.current_map_id)
# The durable store directly: world:sky:* is routed DURABLE by game.gd at runtime,
# which this gate does not load, so get_state would read the realtime store.
var dur: Dictionary = gs.get("_durable")
_p(is_equal_approx(float(dur.get("world:sky:fog_end", -1.0)), 3000.0),
"R2 the loaded map's own keys are the ones loaded", dur.get("world:sky:fog_end", -1.0))
print("GATE map_scope_race: %s (%d checks, %d failed)" % ["PASS" if _fails == 0 else "FAIL", _checks, _fails])
get_tree().quit(0 if _fails == 0 else 1)