Seven nit-level fixes from PR review: - Remove dead signal insert_deactivated from ImplantApp; the hook method on_insert_deactivated() is the actual contract. - Rewrite _on_atlas_economics_link comment in main.gd to reflect the actual flow (AtlasApp closes as a consequence of HudGroups single- active-app, not before emitting anything). - Document the "pop never empties below default" invariant on ImplantNavStack.pop() with a pointer to reset_to_default. - Add _mutating re-entrancy guard on ImplantNavStack mutation methods. push_error + early return if called during a screen_changed emission. - main.gd registry loop now uses typed ImplantAppManifest property access (manifest.app_path, manifest.default_key) instead of dictionary-style .get() calls. Empty app_path triggers push_warning. - Document the economics [/] hotkey exception in main.gd and reference the planned handle_global_key lifecycle hook. Arch doc Follow-up section gains a bullet for the new hook. - Comment the independent-version-read rationale above client_ver and proto_ver in loading_screen.gd. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
94 lines
2.3 KiB
GDScript
94 lines
2.3 KiB
GDScript
class_name ImplantNavStack
|
|
extends Node
|
|
## Intra-app navigation stack for ImplantApp subclasses (#844, D-191).
|
|
## Mutation is synchronous: screen_changed fires before push/pop/replace returns.
|
|
|
|
signal screen_changed(current_screen_id: String)
|
|
|
|
var _stack: Array[String] = []
|
|
var _payloads: Array[Dictionary] = []
|
|
var _default_screen_id: String = ""
|
|
var _mutating: bool = false # re-entrancy guard: set while screen_changed is emitting
|
|
|
|
|
|
func set_default(screen_id: String) -> void:
|
|
_default_screen_id = screen_id
|
|
|
|
|
|
func push(screen_id: String, payload: Dictionary = {}) -> void:
|
|
if _mutating:
|
|
push_error(
|
|
"ImplantNavStack: nested mutation detected — use call_deferred from screen_changed handler"
|
|
)
|
|
return
|
|
_stack.append(screen_id)
|
|
_payloads.append(payload)
|
|
_mutating = true
|
|
screen_changed.emit(screen_id)
|
|
_mutating = false
|
|
|
|
|
|
# pop() never empties the stack below the default screen — this app always
|
|
# has at least one screen visible. To truly reset, use reset_to_default().
|
|
func pop() -> void:
|
|
if _mutating:
|
|
push_error(
|
|
"ImplantNavStack: nested mutation detected — use call_deferred from screen_changed handler"
|
|
)
|
|
return
|
|
if _stack.is_empty():
|
|
push_warning("ImplantNavStack: pop() on empty stack")
|
|
return
|
|
_stack.pop_back()
|
|
_payloads.pop_back()
|
|
if not _stack.is_empty():
|
|
_mutating = true
|
|
screen_changed.emit(_stack.back())
|
|
_mutating = false
|
|
elif not _default_screen_id.is_empty():
|
|
push(_default_screen_id)
|
|
|
|
|
|
func replace(screen_id: String, payload: Dictionary = {}) -> void:
|
|
if _mutating:
|
|
push_error(
|
|
"ImplantNavStack: nested mutation detected — use call_deferred from screen_changed handler"
|
|
)
|
|
return
|
|
if not _stack.is_empty():
|
|
_stack.pop_back()
|
|
_payloads.pop_back()
|
|
_stack.append(screen_id)
|
|
_payloads.append(payload)
|
|
_mutating = true
|
|
screen_changed.emit(screen_id)
|
|
_mutating = false
|
|
|
|
|
|
func reset_to_default() -> void:
|
|
_stack.clear()
|
|
_payloads.clear()
|
|
if not _default_screen_id.is_empty():
|
|
push(_default_screen_id)
|
|
|
|
|
|
func is_empty() -> bool:
|
|
return _stack.is_empty()
|
|
|
|
|
|
func push_default() -> void:
|
|
if not _default_screen_id.is_empty():
|
|
push(_default_screen_id)
|
|
|
|
|
|
func current() -> String:
|
|
if _stack.is_empty():
|
|
return ""
|
|
return _stack.back()
|
|
|
|
|
|
func current_payload() -> Dictionary:
|
|
if _payloads.is_empty():
|
|
return {}
|
|
return _payloads.back()
|