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>
120 lines
3.9 KiB
GDScript
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()
|