Files
settled-reach/client/ui/implant/implant_registry.gd
T
jpmschweitzerandClaude Opus 4.6 284ac44412 feat(ui): PR #131 review round 4 — scene_path, schema_version, screens absorption
Three architectural review comments implemented per Tyre's proposals:

#3 — scene_path consumption (full, Option A)
- ImplantRegistry.instantiate_all(parent) loads and instantiates all
  registered apps with a declared scene_path. Manifests with empty
  scene_path are treated as metadata-only (silently skipped).
- ImplantRegistry.get_app_instance(app_path) returns the live instance.
- hud.tscn no longer direct-instances AtlasApp or EconomicsApp — an
  AppsContainer Control holds the registry-managed children.
- hud.gd._ready() calls ImplantRegistry.instantiate_all($AppsContainer).
- main.gd drops @onready vars for atlas_app/economics_app; looks up
  both from the registry at the top of _ready().

#6c — schema_version on manifest
- ImplantAppManifest: @export var schema_version: int = 1 (first field).
- ImplantRegistry: const CURRENT_SCHEMA_VERSION := 1; tiered check in
  _scan() — older-than-current emits print_verbose, newer-than-current
  emits push_warning, both proceed best-effort.
- apps/atlas/app.tres + apps/economics/app.tres: schema_version = 1.

#10 — absorb _on_screen_changed boilerplate into ImplantApp
- Base class gains _screens: Dictionary, _current_screen_id: String,
  register_screen(id, screen), current_screen_id(), and a real default
  _on_screen_changed implementation that handles leave+hide+enter+show
  with has_method guards and same-screen-replace detection.
- atlas_app: deletes _current_screen_id, _get_screen(),
  _on_screen_changed() override; on_install() collapses to construct →
  setup → wire → register_screen(id, screen) per screen. Preserves
  direct screen refs for atlas-specific signal wiring and method calls.
- economics_app: deletes same scaffolding; on_install() reduces to three
  lines (construct overview_screen, register_screen, nav.set_default).

Also: replaced Resource.get(name, default) dict-style calls with direct
property access on typed ImplantAppManifest reads (2-arg get() is
Dictionary-only; causes "Too many arguments" parse errors on Resources).
_is_valid_manifest gained a Resource type guard and a property-exists
check.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-19 15:11:12 +02:00

120 lines
3.9 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() — not from _ready() here.
func instantiate_all(parent: Node) -> void:
for m in get_manifests():
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()