extends SceneTree
## Gate H6 (rules half) — docs/home-world-design.md §6: a release's STARTER MAP.
##
## build(): a dev's home-room map -> the starter that ships
## 1. keeps the map's records and plain keys, verbatim (keys AND raw values)
## 2. drops world:players:* (positions are per player)
## 3. drops undone journal records (the starter is what you see, not your history)
## 4. keeps library entries (room-global world:assets:*) only if the map uses them
## 5. finds the content hashes the map references
## seed(): the starter -> a player's home, ONCE per map
## 6. writes only keys the home does not have; keeps what is there
## 7. sets world:starter:applied, and a second seed writes nothing
## 8. refuses a starter with an unknown format, writing nothing
##
## Pure functions over a fake state; no daemon, room or filesystem.
const S := preload("res://scenes/maps/home_starter.gd")
const MAP := "map_test"
const HASH_A := "aaaa000000000000000000000000000000000000000000000000000000000001"
const HASH_B := "bbbb000000000000000000000000000000000000000000000000000000000002"
var ok := true
var _ran := false
var _finished := false
class FakeState:
extends RefCounted
var kv: Dictionary = {}
var writes: Array[String] = []
var refuse := "" # a key this state drops, as GameState's can_write does
func has_state(k: String) -> bool:
return kv.has(k)
func get_state(k: String, d: Variant = null) -> Variant:
return kv.get(k, d)
func set_state(k: String, v: Variant) -> void:
writes.append(k)
if k == refuse:
return
kv[k] = v
## The real GameState + KeetService autoloads (no daemon: the durable send fails
## harmlessly, the local mirror is what is checked).
func _gamestate(st: Dictionary) -> void:
var gs: Node = root.get_node_or_null("GameState")
var keet: Node = root.get_node_or_null("KeetService")
_p(gs != null and keet != null, "10. GameState + KeetService autoloads present")
if gs == null or keet == null:
return
gs.install_routes()
gs.current_map_id = MAP
keet.home_room_id = "gate-home"
keet.current_role = "admin"
var r0: Dictionary = gs.seed_home_starter("gate-other", st)
_p(r0.is_empty() and not gs.has_state("world:level:dev:op:10"), "10. another room is never seeded")
keet.current_role = "peer" # the role RPC has not answered yet
var r1: Dictionary = gs.seed_home_starter("gate-home", st)
_p(bool(r1.get("deferred", false)) and not gs.has_state(S.MARKER),
"10. before the role is known: deferred, no marker (r=%s)" % str(r1))
keet.current_role = "admin"
keet.role_changed.emit("gate-home", "admin")
_p(gs.has_state("world:level:dev:op:10") and gs.has_state("world:level:dev:op:12")
and gs.has_state(S.MARKER), "10. role arrives (admin): seeded, dev-authored records accepted")
var r2: Dictionary = gs.seed_home_starter("gate-home", st)
_p(bool(r2.get("skipped", false)), "10. second entry: skipped")
keet.home_room_id = ""
keet.current_role = "peer"
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()
_p(_finished, "the gate ran to the end (no script error aborted it)")
print("GATE home_starter: %s" % ("PASS" if ok else "FAIL"))
quit(0 if ok else 1)
return true
func _k(rest: String) -> String:
return "map:%s:%s" % [MAP, rest]
func _run() -> void:
var live := var_to_str({"v": 2, "kind": "brush.create", "undone": false, "g": "dev:10", "planes": [0.0, 1.0, 0.0, 2.0]})
var undone := var_to_str({"v": 2, "kind": "brush.create", "undone": true, "undone_at": 12, "g": "dev:11"})
var obj := var_to_str({"v": 2, "kind": "object.create", "undone": false, "type": "model",
"props": {"model": HASH_A}})
var sky := var_to_str(11.5)
var entries := {
_k("world:level:dev:op:10"): live,
_k("world:level:dev:op:11"): undone,
_k("world:level:dev:op:12"): obj,
_k("world:sky:current_time"): sky,
_k("world:players:dev"): var_to_str({"pos": [1, 2, 3]}),
}
var assets := {
"world:assets:model:crate": var_to_str({"oid": HASH_A, "size": 10, "ext": "glb"}),
"world:assets:model:unused": var_to_str({"oid": HASH_B, "size": 10, "ext": "glb"}),
}
var st: Dictionary = S.build(MAP, "res://scenes/maps/map_test.tscn", entries, assets, "0.0.test")
var e: Dictionary = st.get("entries", {})
_p(int(st.get("format", 0)) == S.FORMAT, "format stamped")
_p(e.get(_k("world:level:dev:op:10")) == live and e.get(_k("world:sky:current_time")) == sky,
"1. records and plain keys kept verbatim")
_p(not e.has(_k("world:players:dev")), "2. player positions dropped")
_p(not e.has(_k("world:level:dev:op:11")), "3. undone records dropped")
var a: Dictionary = st.get("assets", {})
_p(a.has("world:assets:model:crate") and not a.has("world:assets:model:unused"),
"4. only the library entries the map uses (assets=%s)" % str(a.keys()))
var hs: Array = S.referenced_hashes(st)
_p(hs.has(HASH_A) and not hs.has(HASH_B), "5. referenced content found (%s)" % str(hs))
# seed
var home := FakeState.new()
home.kv["world:sky:current_time"] = 7.0 # the player already changed the sky
var r: Dictionary = S.seed(st, home)
_p(home.kv.has("world:level:dev:op:10") and home.kv.has("world:level:dev:op:12"),
"6. absent records written (plain keys)")
_p(float(home.kv["world:sky:current_time"]) == 7.0, "6. a key the home has is kept (sky stays 7.0)")
_p(home.kv["world:level:dev:op:10"] is Dictionary, "6. values written as Variants, not raw strings")
_p(home.kv.has("world:assets:model:crate"), "6. the used library entry is written")
_p(home.kv.has(S.MARKER), "7. world:starter:applied set (r=%s)" % str(r))
var n := home.writes.size()
var r2: Dictionary = S.seed(st, home)
_p(home.writes.size() == n and bool(r2.get("skipped", false)), "7. a second seed writes nothing")
# 7b. the file round trip: the starter ships as JSON (format comes back a float)
var from_file: Variant = JSON.parse_string(JSON.stringify(st, " ", true))
var viajson := FakeState.new()
var rj: Dictionary = S.seed(from_file, viajson)
_p(int(rj.get("written", 0)) == 4 and viajson.kv.has(S.MARKER)
and viajson.kv["world:level:dev:op:10"] is Dictionary,
"7b. a starter read back from JSON seeds the same (r=%s)" % str(rj))
# 9. a write the home REFUSES: no marker, so the next load tries again
var picky := FakeState.new()
picky.refuse = "world:level:dev:op:12"
var r4: Dictionary = S.seed(st, picky)
_p(int(r4.get("refused", 0)) == 1 and not picky.kv.has(S.MARKER),
"9. a refused write leaves no marker (r=%s)" % str(r4))
# 10. GameState: only at Home, and only once the role (admin at Home) is known
_gamestate(st)
var bad := st.duplicate(true)
bad["format"] = S.FORMAT + 1
var fresh := FakeState.new()
var r3: Dictionary = S.seed(bad, fresh)
_p(fresh.writes.is_empty() and String(r3.get("error", "")) != "",
"8. unknown format refused, nothing written (r=%s)" % str(r3))
_finished = true