Files
settled-reach/client/ui/implant/implant_registry.gd
T
jpmschweitzerandClaude Fable 5 cffe760d36 feat(client): T-1132 standalone Atlas companion shell + make atlas (D-254 SS3)
New atlas_standalone scene/script: attach-or-spawn boot (one REAL
connect_to_sim attempt at SR_PORT-or-9876 — a separate throwaway TCP
probe was proven by live run to kill a pre-accept-loop server via
broken handshake pipe; never abandon a connected socket), else spawn
--port 0 via new ServerProcess.start_with_pipe + LISTENING:{port}
stdout parse, retry against the resolved port. Reader role wired end
to end: protocol.encode_startup_message optional role param (empty
omits the wire key — byte-identical for all existing callers),
sim_bridge.connection_role suppresses the post-handshake
RequestAllSettings auto-send, hud_groups skips AutoPause/AutoResume
sends for readers (all three would otherwise burn Reader violation
strikes per the T-1130 matrix — endorsed by Oscar).

Generic implant host per D-254 SS3: the shell instantiates ALL
registered implant apps; implant_app_manifest gains
available_in_companion (opt-out, default true) and
implant_registry.instantiate_all a standalone filter param (default
preserves hud.gd behavior byte-identically). Boot order is
instantiate_all THEN open_app (reverse renders a permanently black
window — app_changed fires with no listener; matches hud.gd's order).
Owned-server lifecycle: _exit_tree stops a spawned child, attached
servers survive companion close. Known engine limitation documented:
raw SIGTERM bypasses all Godot notifications and orphans a spawned
server; WM close paths verified clean.

Live-verified: spawn-mode (301 systems rendered from systems.db over
the wire), attach-mode, two simultaneous readers, clean shutdown with
zero orphan processes. 14 new gdUnit tests (port/LISTENING parsing).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-17 09:09:09 +02:00

126 lines
4.3 KiB
GDScript

extends Node
## Lazy registry of installed implant apps (#844, D-191).
## Autoload — scans apps/*/app.tres on first get_manifests() call.
## Does NOT reference ImplantAppManifest or ImplantApp class_names at load time
## (autoload parse-order rule; see CLAUDE.md).
const CURRENT_SCHEMA_VERSION := 1
const _MODE_MAP: Dictionary = {"gameplay": 0, "insert": 1, "fullscreen": 2}
var _manifests: Array = [] # Array[ImplantAppManifest]
var _resolved_modes: Dictionary = {} # app_path -> HudGroups.Mode int
var _instances: Dictionary = {} # app_path -> ImplantApp
var _scanned: bool = false
func get_manifests() -> Array:
if not _scanned:
_scan()
return _manifests
func get_resolved_mode(app_path: String) -> int:
if not _scanned:
_scan()
return _resolved_modes.get(app_path, HudGroups.Mode.FULLSCREEN)
## Instantiate all registered apps whose manifest declares a scene_path and add
## them as children of parent. Manifests without scene_path are metadata-only
## and are silently skipped. Called from hud.gd._ready() in the normal game
## (standalone=false, default — every app is hosted, the flag below is
## irrelevant) and from atlas_standalone.gd (standalone=true, D-254 §3
## amendment) — in the companion, manifests with available_in_companion=false
## are skipped entirely (not instantiated, so never reachable by app-path).
func instantiate_all(parent: Node, standalone: bool = false) -> void:
for m in get_manifests():
if standalone and not m.available_in_companion:
continue
var scene_path: String = m.scene_path
if scene_path.is_empty():
continue
if not ResourceLoader.exists(scene_path):
push_warning(
"ImplantRegistry: scene_path not found for %s: %s" % [m.app_path, scene_path]
)
continue
var packed = load(scene_path)
if not (packed is PackedScene):
push_warning("ImplantRegistry: %s is not a PackedScene" % scene_path)
continue
var instance = packed.instantiate()
_instances[m.app_path] = instance
parent.add_child(instance)
## Return the live ImplantApp instance for app_path, or null if not instantiated.
func get_app_instance(app_path: String): # returns ImplantApp
return _instances.get(app_path, null)
func _scan() -> void:
_scanned = true
_manifests.clear()
_resolved_modes.clear()
var key_owners: Dictionary = {} # default_key -> app_path (collision detection)
var dir := DirAccess.open("res://ui/implant/apps")
if dir == null:
push_warning("ImplantRegistry: could not open res://ui/implant/apps")
return
dir.list_dir_begin()
var entry := dir.get_next()
while not entry.is_empty():
if dir.current_is_dir() and not entry.begins_with("."):
var tres_path: String = "res://ui/implant/apps/%s/app.tres" % entry
if ResourceLoader.exists(tres_path):
var m = load(tres_path)
if not _is_valid_manifest(m):
push_warning("ImplantRegistry: invalid or missing app_path in %s" % tres_path)
else:
var app_path: String = m.app_path
var mode_str: String = m.default_mode
var default_key: int = m.default_key
var schema_ver: int = m.schema_version
# Schema version check — best-effort in both directions
if schema_ver < CURRENT_SCHEMA_VERSION:
print_verbose(
"ImplantRegistry: backfilled manifest from v%d at %s" % [
schema_ver, tres_path
]
)
elif schema_ver > CURRENT_SCHEMA_VERSION:
push_warning(
"ImplantRegistry: manifest at %s declares schema_version %d; this build supports v%d; proceeding best-effort" % [
tres_path, schema_ver, CURRENT_SCHEMA_VERSION
]
)
if not _MODE_MAP.has(mode_str):
push_warning(
"ImplantRegistry: invalid default_mode \"%s\" in %s — skipping" % [
mode_str, tres_path
]
)
elif default_key >= 0 and key_owners.has(default_key):
push_warning(
"ImplantRegistry: key binding collision — key %d already bound to %s, skipping %s" % [
default_key, key_owners[default_key], app_path
]
)
else:
if default_key >= 0:
key_owners[default_key] = app_path
_resolved_modes[app_path] = _MODE_MAP[mode_str]
_manifests.append(m)
entry = dir.get_next()
dir.list_dir_end()
func _is_valid_manifest(m: Variant) -> bool:
if m == null:
return false
if not (m is Resource):
return false
if not "app_path" in m:
return false
return not (m.app_path as String).is_empty()