extends SceneTree
## Gate: a brain takes parameters from its declaration.
##
## BotRegistry handed ScriptHost a context of exactly {"bot_id": id}, so every
## published brain was hard-coded: builder.gd's MAX_BLOCKS was a const, and
## wanting a different count meant editing the brain rather than the
## registration. The brain dictionary is the natural place — it already carries
## kind and path — so it gains an optional `params`, merged into the context
## ScriptHost already applies with set().
##
## ⛔ bot_id IS APPLIED LAST AND WINS. params arrive from an admin:cmd, so a
## caller could otherwise pass {"bot_id": "someone-else"} and have the brain act
## as another bot -- move() and despawn() are keyed on it.
var ok := true
var _ran := false
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 brain_params: %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 sh: Node = root.get_node_or_null("ScriptHost")
_p(br != null and sh != null, "BotRegistry and ScriptHost autoloads present")
if br == null or sh == null:
return
var real_state: Object = br.state
br.state = load("res://scenes/test/permissive_state.gd").new()
# ---- params reach the brain ----
var r: Dictionary = br.register("p_bot", ["session"], "none", {
"kind": "script_file",
"path": "res://scenes/test/fixture_brain.gd",
"params": {"tuned": 42},
})
_p(bool(r.get("ok", false)), "a brain with params registers (%s)" % str(r.get("error", "")))
var inst: Variant = sh.get_instance("p_bot") if sh.has_method("get_instance") else null
if inst == null:
# ScriptHost keeps them privately; reach the loaded record the same way
# _process does.
var loaded: Dictionary = sh.get("_loaded") as Dictionary
if loaded.has("p_bot"):
inst = (loaded["p_bot"] as Dictionary).get("instance")
_p(inst != null, "the brain instance is reachable")
if inst != null:
_p(int((inst as Object).get("tuned")) == 42,
"the declared param reached it (tuned=%s)" % str((inst as Object).get("tuned")))
_p(str((inst as Object).get("bot_id")) == "p_bot",
"and bot_id still arrived (%s)" % str((inst as Object).get("bot_id")))
br.despawn("p_bot")
# ---- bot_id cannot be overridden by a caller ----
br.register("p_safe", ["session"], "none", {
"kind": "script_file",
"path": "res://scenes/test/fixture_brain.gd",
"params": {"bot_id": "somebody-else"},
})
var loaded2: Dictionary = sh.get("_loaded") as Dictionary
var inst2: Variant = (loaded2.get("p_safe", {}) as Dictionary).get("instance")
_p(inst2 != null, "the second brain loaded")
if inst2 != null:
_p(str((inst2 as Object).get("bot_id")) == "p_safe",
"params CANNOT hijack bot_id (%s)" % str((inst2 as Object).get("bot_id")))
br.despawn("p_safe")
# ---- and the builder honours a count ----
var builder: GDScript = load("res://bots/builder.gd")
_p(builder != null, "builder.gd loads")
if builder != null:
var b: Object = builder.new()
_p(b.get("count") != null, "builder declares a `count` property, not a const")
var dflt: Variant = b.get("count")
_p(dflt != null and int(dflt) > 0, "with a sane default (%s)" % str(dflt))
# And the param must actually override it, which is the point.
b.set("count", 3)
_p(int(b.get("count")) == 3, "and count is settable, so params can override it")
# SELF-CHECK: a brain given NO params must still load and keep its defaults,
# or the merge would be making params mandatory.
br.register("p_none", ["session"], "none", {
"kind": "script_file", "path": "res://scenes/test/fixture_brain.gd",
})
var loaded3: Dictionary = sh.get("_loaded") as Dictionary
var inst3: Variant = (loaded3.get("p_none", {}) as Dictionary).get("instance")
_p(inst3 != null, "self-check: a brain with no params still loads")
if inst3 != null:
_p(int((inst3 as Object).get("tuned")) == 0,
"self-check: and keeps its own default (tuned=%s)" % str((inst3 as Object).get("tuned")))
br.despawn("p_none")
br.state = real_state