extends SceneTree
## GATE — the Outliner's SYSTEM rows are LOCKED and TOGGLEABLE (hyper-games #109,
## 2026-10-12: "canvaslayer, NavVolume, EntityLinks cannot be deleted, also what is
## entitylinks?" — user: "show them as locked, make them toggleable").
##
## A plain node row lives in the map's .tscn or is built at runtime (the bot nav grid,
## the teleporter link lines, the command palette): there is no record to remove, so
## Delete stays refused. What changes is that the row SAYS so before you try, and says
## what the node is.
##
## 1. NavVolume, the command palette, DCLevel and SceneObjects rows are locked and
## carry a note; object rows are not locked
## 2. a locked row reads as locked in the tree, and its tooltip is its note
## 3. Delete on a locked row refuses, the node stays, and the status line explains why
## 4. the palette (editor UI) has no eye; NavVolume has one
## 5. EntityLinks: its eye hides it, and re-entering build mode keeps it hidden
## (map_dc_level forced it visible on every mode change); unhiding brings it back
## 0. THE SYSTEM FILTER (#120 F1): off, NavVolume / EntityLinks / the palette are not
## in the tree (the model still lists them); DCLevel and SceneObjects always are;
## on, they are. The rest of this gate runs with it on.
##
## RUN: CI=true HYPERTPS_DAEMON_TCP=127.0.0.1:1 "$GODOT" --headless --path . --script res://scenes/test/gate_outliner_locked.gd
var _frame := 0
var _map: Node = null
var fails := 0
var checks := 0
func _p(c: bool, m: String, got: Variant = "") -> void:
checks += 1
if not c:
fails += 1
print(" %-66s %s %s" % [m, "OK" if c else "**FAIL**", str(got)])
func _process(_d: float) -> bool:
_frame += 1
if _frame == 1:
root.get_node("/root/GameMode").set_mode("build")
_map = (load("res://scenes/maps/map_dev_test.tscn") as PackedScene).instantiate()
root.add_child(_map)
return false
if _frame < 40:
return false
_run()
if checks < 14:
print(" **FAIL** only %d checks ran -- setup died before the assertions" % checks)
fails += 1
print("\nGATE outliner_locked: %s (%d checks, %d failed)" % ["PASS" if fails == 0 else "FAIL", checks, fails])
quit(0 if fails == 0 else 1)
return true
func _rows_by_id(model: Variant) -> Dictionary:
var out := {}
for r: Dictionary in model.rows():
out[String(r["id"])] = r
return out
func _palette_id(rows: Dictionary) -> String:
for id: String in rows:
var n: Node = _map.get_node_or_null((rows[id] as Dictionary).get("node_path", NodePath("")) as NodePath)
if n is CanvasLayer:
return id
return ""
func _select(panel: Node, id: String) -> TreeItem:
var tree: Tree = panel.get("_tree")
tree.deselect_all()
var it: TreeItem = (panel.get("_items") as Dictionary).get(id, null)
if it != null:
it.select(0)
return it
func _run() -> void:
var panel: Node = _map.get("_outliner")
var model: Variant = panel.get("model") if panel != null else null
_p(panel != null and model != null, "setup: the map has an Outliner")
if model == null:
return
var rows := _rows_by_id(model)
var pal := _palette_id(rows)
_p(pal != "", "setup: the command palette has a row", pal)
# 1. locked + noted
for id: String in ["node:NavVolume", pal, "node:DCLevel", "node:SceneObjects"]:
var r: Dictionary = rows.get(id, {})
_p(bool(r.get("locked", false)) and String(r.get("note", "")) != "",
"1. %s is locked with a note" % id, String(r.get("note", "<none>")))
var obj: Dictionary = rows.get("obj:scene:SpawnPoint1", {})
_p(not obj.is_empty() and not bool(obj.get("locked", false)), "1. an object row (SpawnPoint1) is not locked")
# 0. the System filter
panel.set_show_system(false)
var items0: Dictionary = panel.get("_items")
_p(not items0.has("node:NavVolume") and not items0.has("node:EntityLinks") and not items0.has(pal),
"0. filter off: no NavVolume / EntityLinks / palette rows")
_p(rows.has("node:NavVolume"), "0. ... though the model still lists them")
_p(items0.has("node:DCLevel") and items0.has("node:SceneObjects"), "0. DCLevel and SceneObjects always show")
panel.set_show_system(true)
_p((panel.get("_items") as Dictionary).has("node:NavVolume"), "0. filter on: they are back")
# 2. reads as locked
panel.refresh()
var it: TreeItem = (panel.get("_items") as Dictionary).get("node:NavVolume", null)
var note := String((rows.get("node:NavVolume", {}) as Dictionary).get("note", "?"))
_p(it != null and it.get_text(0).contains("locked"), "2. the NavVolume row reads as locked", it.get_text(0) if it else "")
_p(it != null and it.get_tooltip_text(0).contains(note), "2. its tooltip is its note", it.get_tooltip_text(0) if it else "")
# 3. delete refuses and explains
_select(panel, "node:NavVolume")
var n: int = panel.delete_selected()
var status: Label = panel.get("_status")
_p(n == 0 and _map.get_node_or_null("NavVolume") != null, "3. Delete refuses; NavVolume stays", n)
_p(status != null and status.text.contains("locked") and status.text.contains(note),
"3. the status line says it is locked and what it is", status.text if status else "")
# 4. eyes
var pit: TreeItem = (panel.get("_items") as Dictionary).get(pal, null)
_p(pit != null and not pit.is_editable(1), "4. the palette row has no eye")
_p(it != null and it.is_editable(1), "4. the NavVolume row has an eye")
# 5. EntityLinks: hide survives a mode change
var links: Node3D = _map.call("_ensure_entity_links")
model.set_row_hidden("node:EntityLinks", true)
model.apply()
_p(links != null and not links.visible, "5. the eye hides EntityLinks")
var gm: Node = root.get_node("/root/GameMode")
gm.set_mode("play")
gm.set_mode("build")
_p(links != null and not links.visible, "5. re-entering build mode keeps it hidden", links.visible if links else "")
model.set_row_hidden("node:EntityLinks", false)
model.apply()
_p(links != null and links.visible, "5. unhiding brings it back (build mode)")