extends SceneTree
## HEADLESS GATE — the state table is complete, and it still says what the three
## hand-kept declarations used to say.
##
## Run: "$HYPER_TPS_GODOT" --headless --script scenes/test/state_routes_gate.gd
##
## #32 §7 asked for this in as many words: "A single table plus a gate asserting
## every routed prefix has decided its scope and authorship would have prevented at
## least two of today's bugs." Both of those bugs were SILENT — a key with no route
## falls through to {REALTIME, "peer"} and is broadcast to whoever is in the mesh
## and then lost forever, and a key missing from the authorship list reads as
## "shared content, anyone may write". Neither prints anything. world:level: was
## wrong in both ways at once for months.
##
## So this gate is not about tidiness. It is the thing that makes a missing column
## LOUD, and it runs without a room, a map or a peer.
var fails := 0
func _ok(what: String, cond: bool) -> void:
if not cond:
fails += 1
print(" %-62s %s" % [what, "OK" if cond else "**FAIL**"])
func _eqs(what: String, got: String, want: String) -> void:
var c := got == want
if not c:
fails += 1
print(" %-62s %-22s want %-22s %s" % [what, got, want, "OK" if c else "**FAIL**"])
## DEFERRED, and against the REAL autoload. Loading game_state.gd with load() from
## _init instead compiles it before the autoloads exist, so `KeetService` is an
## unknown identifier — and the failed compile is then CACHED, so the engine hands
## the actual GameState singleton a broken script and every subsystem that touches
## it fails afterwards. The gate would have been testing a copy in a world where
## the original had been destroyed by the act of testing it.
func _init() -> void:
_start.call_deferred()
func _start() -> void:
print("=== State routes ===\n")
# By PATH, not by name: a --script gate is compiled before the autoloads are
# registered, so `GameState` is not an identifier yet even though the singleton
# exists by the time this deferred call runs.
var gs: Node = root.get_node_or_null("/root/GameState")
if gs == null:
print(" no GameState autoload — cannot gate the table")
quit(1)
return
var table: Array = gs.STATE_ROUTES
print("-- every row decides all four questions --")
var complete := true
var typed := true
var seen := {}
var dupes := ""
for row: Dictionary in table:
# A row that omits a field is the bug this gate exists for: `get(field, X)`
# elsewhere would supply a plausible default and nobody would ever know the
# question had not been answered.
for field: String in ["prefix", "channel", "role", "map_scope", "author"]:
if not row.has(field):
complete = false
print(" row %s is missing '%s'" % [str(row.get("prefix", "?")), field])
if not (row.get("prefix", 0) is String and row.get("role", 0) is String
and row.get("map_scope", 0) is String and row.get("author", 0) is Array):
typed = false
var p: String = String(row.get("prefix", ""))
if seen.has(p):
dupes += p + " "
seen[p] = true
_ok("every row names a prefix, channel, role, scope and authorship", complete)
_ok("and each of those has the type the readers expect", typed)
_ok("no prefix is declared twice (%s)" % ("none" if dupes == "" else dupes), dupes == "")
print("\n-- a prefix that persists has decided WHO OWNS IT --")
# The pairing that actually bites: DURABLE means the write survives and reaches
# other peers, and that is exactly when authorship stops being cosmetic. A
# durable row may still be shared — one sky, last write wins — but it must say
# so by carrying an empty list rather than by omitting the field, which the
# check above already refused.
var durable := 0
for row: Dictionary in table:
if int(row["channel"]) == gs.Channel.DURABLE:
durable += 1
_ok("%d durable rows, each with an explicit authorship list" % durable, durable > 0)
print("\n-- the derived lists still say what the hand-kept ones said --")
# Pinned deliberately. The point of the refactor is that behaviour did not
# change, and the only way to know that is to compare against what was there
# before rather than against the table that now generates it.
var want_map: Array[String] = [
"world:terrain:", "world:players:", "world:creatures:", "world:objects:",
"world:level:", "world:sky:",
# Added with the bot registry (0.1139.0). Both world:bots: rows are
# map-scoped deliberately -- that scoping IS bot containment, so the
# registry row drops with the map's durable state and the pos row is
# evicted with the map's realtime state. This list is hand-written on
# purpose, so a new map scope has to be acknowledged here; it was not,
# and this gate had been failing since that release.
"world:bots:",
# The Home starter marker (2026-10-05): per map, so each map at Home is
# seeded from its own release starter exactly once. docs/home-world-design.md §6.
"world:starter:"]
# admin:cmd: since the admin-role change (2026-10-04): an admin command's key carries
# its author's memberId, so its role is the AUTHOR's, never one it claims.
var want_author: Array[String] = [
"world:terrain:edit:", "world:objects:", "world:players:", "world:level:", "admin:cmd:"]
var got_map: Array = gs.MAP_SCOPED_PREFIXES.duplicate()
var got_author: Array = gs.AUTHOR_SCOPED_PREFIXES.duplicate()
got_map.sort()
got_author.sort()
var wm: Array = want_map.duplicate()
var wa: Array = want_author.duplicate()
wm.sort()
wa.sort()
_eqs("map-scoped prefixes", str(got_map), str(wm))
_eqs("author-scoped prefixes", str(got_author), str(wa))
print("\n-- and a key of each kind behaves the way its row says --")
gs.current_map_id = "map_dc_level"
# Map-scoped: stored under the map, so two maps in one room do not share it.
_eqs("a sky key is namespaced by map", gs._scoped("world:sky:preset"),
"map:map_dc_level:world:sky:preset")
# Room-global: presence is the room's, not the map's — namespacing it would
# hide half the room's members every time someone walked through a teleporter.
_eqs("a presence key is not", gs._scoped("presence:abc"), "presence:abc")
# Authored: the segment after the prefix is the author, and can_write compares
# it to the writer's member id.
_eqs("a level key names its author", gs._author_of("world:level:MEMBER7:brush:9"), "MEMBER7")
# Shared: no author, so the role gate alone decides — which is correct for one
# sky, and catastrophic for brushes, which is why they are separate columns.
_eqs("a sky key names none", gs._author_of("world:sky:preset"), "")
# Finer than the route: world:terrain: is routed and map-scoped as a whole, but
# only edits underneath it are owned by a peer.
_eqs("a terrain EDIT names its author", gs._author_of("world:terrain:edit:M3:12"), "M3")
_eqs("terrain settings name none", gs._author_of("world:terrain:noise:seed"), "")
print("\n-- installing the table routes every prefix in it --")
gs.install_routes()
var routed := true
var missing := ""
for row: Dictionary in table:
var pattern: String = String(row["prefix"]) + "*"
if not gs._routes.has(pattern):
routed = false
missing += pattern + " "
else:
var got: Dictionary = gs._routes[pattern]
var want_role: String = String(row["role"])
if want_role == "":
want_role = gs.default_write_role
if int(got["channel"]) != int(row["channel"]) or String(got["role"]) != want_role:
routed = false
missing += pattern + "(mismatch) "
_ok("every row is installed with its own channel and role (%s)"
% ("all" if missing == "" else missing), routed)
# Nothing EXTRA: a route installed from somewhere else is a fourth declaration,
# which is the thing this table exists to stop being possible.
_ok("and nothing else is routed", gs._routes.size() == table.size())
print("\n-- the asset library is durable and room-global --")
# world:assets:<kind>:<name> (AssetSync's MUTABLE pointers: models, PBR textures,
# scripts) had NO ROW, so it fell through to {REALTIME, peer}: a model added to
# the library reached whoever was meshed and was gone on reload. Room-global, as
# asset_sync.gd says (a WAD is not part of one map); newest publish wins, so no
# author. docs/home-world-design.md §9 P6.
_eqs("an asset pointer is DURABLE", str(int(gs._resolve("world:assets:model:crate")["channel"])),
str(int(gs.Channel.DURABLE)))
_eqs("and room-global (not namespaced by map)", gs._scoped("world:assets:model:crate"),
"world:assets:model:crate")
print("\n%s" % ("PASS" if fails == 0 else "FAILED (%d)" % fails))
quit(0 if fails == 0 else 1)