fix(ui): PR #209 review round — current-screen guards, pending-aware settle, one body guard (T-971)

Every screen-targeted intent now routes through one
_require_current_screen() check and returns the structured error shape
instead of silently mutating an off-screen viewer (hoshe's finding:
scroll_rung from the reach screen fired real IPC and reported ok). The
reference driver's fixed 4-frame settle becomes is_pending()-aware with
a 600-frame bound, the keep-waiting decision extracted as a pure
testable function — restoring the proven eyeball-driver discipline. The
terrain_reference guard moves into AtlasApp._on_body_selected(), the
shared tail for double-click, Enter, AND the intent path — closing a
pre-existing click/Enter divergence hoshe caught this PR formalizing;
the intent layer pre-checks via the new SystemScreen.find_body() and
reports structured errors for unknown ids and terrain-less bodies.
after_test() resets AtlasAgentBridge.current_app (tyre's freed-pending
footgun). Suites 58/58 + 14/14; full suite 3,638.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-25 19:14:02 +02:00
co-authored by Claude Fable 5
parent 80974dfe5a
commit 52304d3e37
6 changed files with 440 additions and 44 deletions
@@ -151,6 +151,23 @@ static func _walk_affordances_recursive(node: Node, out: Array) -> void:
## meant to be driven by a fallible external caller (a curl-style JSON body,
## a test fixture), and a malformed intent name is exactly the kind of input
## it must handle gracefully.
##
## **PR #209 review (Hoshe finding 1, the off-screen dispatch bug):**
## app.get_screen(id) is a REGISTRY lookup — every screen is registered (and
## therefore reachable) for the app's whole lifetime, regardless of which one
## is currently visible/current. Before this fix, every screen-targeted
## intent resolved its target via get_screen() alone, so e.g. scroll_rung
## while "reach" was showing silently mutated the off-screen "regional"
## viewer — including firing a real SimBridge.request_step_canvas() IPC
## call — and returned {"ok": true}, as if the player had actually been
## looking at the map. Every screen-targeted intent below now checks
## app.current_screen_id() against the screen it targets FIRST, returning
## the same structured {"ok": false, "error": ...} shape the null-app/
## unknown-intent paths already use. This is a per-intent expectation, not a
## single global gate, because different intents target different screens
## (select_system/open_system expect "reach"; select_body/open_body expect
## "system"; scroll_rung/jump_to_center/reset_view/set_overlay expect
## "regional") — see _require_current_screen()'s own doc.
static func act(app: Node, intent: String, params: Dictionary = {}) -> Dictionary:
if app == null and intent != "open_atlas":
return {"ok": false, "error": "no AtlasApp instance available"}
@@ -162,35 +179,35 @@ static func act(app: Node, intent: String, params: Dictionary = {}) -> Dictionar
HudGroups.close_app()
return {"ok": true}
"select_system":
app.get_screen("reach").select_system_by_id(str(params.get("system_id", "")))
return {"ok": true}
return _act_on_current_screen(
app, "reach", func(s: Node) -> void: s.select_system_by_id(str(params.get("system_id", "")))
)
"open_system":
app.get_screen("reach").open_system_by_id(str(params.get("system_id", "")))
return {"ok": true}
return _act_on_current_screen(
app, "reach", func(s: Node) -> void: s.open_system_by_id(str(params.get("system_id", "")))
)
"select_body":
app.get_screen("system").select_body_by_id(str(params.get("body_id", "")))
return {"ok": true}
return _act_on_current_screen(
app, "system", func(s: Node) -> void: s.select_body_by_id(str(params.get("body_id", "")))
)
"open_body":
app.get_screen("system").open_body_by_id(str(params.get("body_id", "")))
return {"ok": true}
return _act_open_body(app, params)
"scroll_rung":
return _act_scroll_rung(app, params)
"jump_to_center":
return _act_jump_to_center(app, params)
"reset_view":
var viewer: Variant = _get_viewer(app)
if viewer == null:
return {"ok": false, "error": "regional screen not active"}
viewer._reset_to_global()
return {"ok": true}
return _act_on_current_screen(
app, "regional", func(s: Node) -> void: s.get_viewer()._reset_to_global()
)
"set_overlay":
var overlay_viewer: Variant = _get_viewer(app)
if overlay_viewer == null:
return {"ok": false, "error": "regional screen not active"}
overlay_viewer.set_overlay_visible(
str(params.get("overlay_id", "")), bool(params.get("visible", true))
return _act_on_current_screen(
app,
"regional",
func(s: Node) -> void: s.get_viewer().set_overlay_visible(
str(params.get("overlay_id", "")), bool(params.get("visible", true))
)
)
return {"ok": true}
"back":
app.nav.pop()
return {"ok": true}
@@ -198,14 +215,85 @@ static func act(app: Node, intent: String, params: Dictionary = {}) -> Dictionar
return {"ok": false, "error": "unknown intent '%s'" % intent} # gdlint:ignore = max-returns
## The shared current-screen guard (PR #209 review, Hoshe finding 1): returns
## the structured {"ok": false, "error": ...} shape if `expected_screen_id`
## isn't the CURRENT screen (app.current_screen_id()), otherwise runs
## `body` against the resolved screen instance and returns {"ok": true}.
## `body` is a Callable taking the screen Node — every screen-targeted intent
## that has no extra result fields to report (select_system/open_system/
## select_body/reset_view/set_overlay) routes through this single check
## rather than five copies of the same "is this screen current" branch.
## scroll_rung/jump_to_center/open_body still need their own wrappers (they
## report extra fields — rung/world_center — or a body-specific guard) but
## reuse _require_current_screen() for the identical check.
static func _act_on_current_screen(
app: Node, expected_screen_id: String, body: Callable
) -> Dictionary:
var screen: Variant = _require_current_screen(app, expected_screen_id)
if screen == null:
return _not_current_screen_error(app, expected_screen_id)
body.call(screen)
return {"ok": true}
## Returns the registered screen instance for `expected_screen_id` ONLY if it
## is also the CURRENTLY showing screen (app.current_screen_id() ==
## expected_screen_id) — null otherwise (either unregistered, per
## get_screen()'s own contract, or registered-but-not-current, the bug this
## whole guard exists to close). This is the ONE place "is this screen
## current" is checked — every act() branch above and every _act_* helper
## below calls this rather than checking current_screen_id() inline.
static func _require_current_screen(app: Node, expected_screen_id: String) -> Variant:
if app.current_screen_id() != expected_screen_id:
return null
return app.get_screen(expected_screen_id)
static func _not_current_screen_error(app: Node, expected_screen_id: String) -> Dictionary:
return {
"ok": false,
"error": (
"intent requires screen '%s' to be current, but '%s' is showing"
% [expected_screen_id, app.current_screen_id()]
),
}
## `open_body` — PR #209 review (Hoshe finding 3, lead ruling): the
## Enter-key path has always gated body entry on `terrain_reference != null`
## (AtlasApp._handle_enter()'s "system" branch); double-click (and therefore
## this intent, which drives the identical SystemScreen.body_selected signal)
## did not — a pre-existing click/Enter divergence this PR formalizes into a
## contract, so it fixes it. The guard itself now lives in the SHARED tail
## (AtlasApp._on_body_selected(), the signal handler both double-click and
## this intent funnel through) — a guarded body is a silent no-op there,
## matching what the Enter path always did. This intent does its OWN
## pre-check via SystemScreen.find_body() so it can report a STRUCTURED
## error instead of masking "nothing happened" as {"ok": true} the way a bare
## click has no way to report either way.
static func _act_open_body(app: Node, params: Dictionary) -> Dictionary:
var screen: Variant = _require_current_screen(app, "system")
if screen == null:
return _not_current_screen_error(app, "system")
var body_id: String = str(params.get("body_id", ""))
var body: Dictionary = screen.find_body(body_id)
if body.is_empty():
return {"ok": false, "error": "unrecognized body_id '%s'" % body_id}
if body.get("terrain_reference") == null:
return {"ok": false, "error": "body '%s' has no terrain reference" % body_id}
screen.open_body_by_id(body_id)
return {"ok": true}
## `scroll_rung` — direction is required; cursor_local defaults to a
## reasonable canvas-center guess (Vector2(400, 300), matching this cluster's
## own gdUnit test fixtures' convention, see test_step_canvas_viewer.gd) since
## an agent driver has no real cursor position to anchor on.
static func _act_scroll_rung(app: Node, params: Dictionary) -> Dictionary:
var viewer: Variant = _get_viewer(app)
if viewer == null:
return {"ok": false, "error": "regional screen not active"}
var screen: Variant = _require_current_screen(app, "regional")
if screen == null:
return _not_current_screen_error(app, "regional")
var viewer: Variant = screen.get_viewer()
var direction: int = int(params.get("direction", 1))
var cursor_raw: Variant = params.get("cursor_local")
var cursor_local: Vector2 = (
@@ -225,21 +313,24 @@ static func _act_scroll_rung(app: Node, params: Dictionary) -> Dictionary:
## every other navigation intent uses — no parallel request-building code
## exists in this file.
static func _act_jump_to_center(app: Node, params: Dictionary) -> Dictionary:
var viewer: Variant = _get_viewer(app)
if viewer == null:
return {"ok": false, "error": "regional screen not active"}
var screen: Variant = _require_current_screen(app, "regional")
if screen == null:
return _not_current_screen_error(app, "regional")
var center_raw: Variant = params.get("world_center")
if not (center_raw is Array and (center_raw as Array).size() >= 2):
return {"ok": false, "error": "jump_to_center requires world_center: [x, y]"}
var viewer: Variant = screen.get_viewer()
var world_center := Vector2(float(center_raw[0]), float(center_raw[1]))
var rung: String = str(params.get("rung", ""))
viewer.jump_to(world_center, rung)
return {"ok": true, "rung": viewer.get_held_rung(), "world_center": [world_center.x, world_center.y]}
## Shared "regional" screen -> StepCanvasViewer accessor — returns null if
## "regional" isn't the registered screen (defensive; every intent that needs
## the viewer checks this rather than assuming the screen tree shape).
## Shared "regional" screen -> StepCanvasViewer accessor, used ONLY by
## observe() (which already knows "regional" is current — it matched on
## screen_id itself — so it doesn't need _require_current_screen()'s guard,
## just a null-safe read of a screen that may not even be registered yet
## early in app lifecycle).
static func _get_viewer(app: Node) -> Variant:
var regional_screen: Node = app.get_screen("regional")
if regional_screen == null:
+22 -5
View File
@@ -127,11 +127,7 @@ func _handle_enter() -> void:
nav.replace("system", {"mode": "orbital", "system": sys})
elif _system_screen and _system_screen.has_body_panel_open():
var body: Dictionary = _system_screen.get_selected_body()
if body.get("terrain_reference") != null:
nav.push("regional", {
"body": body,
"system": nav.current_payload().get("system", {}),
})
_on_body_selected(body)
# =============================================================================
@@ -147,7 +143,28 @@ func _on_system_selected(system_id: String) -> void:
nav.push("system", {"mode": "orbital", "system": system})
## PR #209 review (Hoshe finding 3, lead ruling): the SHARED tail every path
## that "opens" a body funnels through — double-click (SystemScreen.
## body_selected), the Enter-key path (_handle_enter()'s "system" branch,
## which used to gate on terrain_reference itself and now just calls this),
## and AtlasAgentInterface's open_body/open_body_by_id intent (also via this
## same signal). Gating HERE, once, closes a pre-existing divergence: the
## Enter-key path already required `terrain_reference != null` before this
## PR, but double-click (and therefore the new open_body intent) did not —
## both silently pushed "regional" for a body with no heightmap. Formalizing
## the intent contract is what surfaced it, so this PR fixes it for both
## callers at once rather than reintroducing the same split.
##
## A guarded body (no terrain_reference) is a silent no-op here — same
## behavior the Enter path always had (it simply never called nav.push() for
## that case). AtlasAgentInterface's open_body/open_body_by_id wrap this at
## the intent layer with a STRUCTURED error instead of a silent no-op (see
## atlas_agent_interface.gd's own _act_open_body()) — an agent driver must be
## able to tell "nothing happened" from "call succeeded", which a bare click
## has no way to report either way.
func _on_body_selected(body: Dictionary) -> void:
if body.get("terrain_reference") == null:
return
nav.push("regional", {
"body": body,
"system": nav.current_payload().get("system", {}),
@@ -427,8 +427,14 @@ func select_body_by_id(body_id: String) -> void:
## T-971 (AtlasAgentInterface `open_body` intent) — the SAME state mutation
## _handle_orbital_double_click() performs (double-click equivalent: emits
## body_selected, which AtlasApp._on_body_selected() turns into
## nav.push("regional", ...)). A no-op for an unrecognized id.
## body_selected, which AtlasApp._on_body_selected() now gates on
## terrain_reference — PR #209 review, Hoshe finding 3 — before pushing
## "regional"). A no-op for an unrecognized id. Emits unconditionally for a
## RECOGNIZED body regardless of terrain_reference, same as the double-click
## handler always has — the guard lives in the shared _on_body_selected()
## tail, not here, so this stays a pure "found it, emit" lookup identical to
## the click path (AtlasAgentInterface does its OWN pre-check via
## find_body() to report a structured error instead of a silent no-op).
func open_body_by_id(body_id: String) -> void:
for b: Dictionary in _orbital_bodies:
if str(b.get("body_id", "")) == body_id:
@@ -436,6 +442,20 @@ func open_body_by_id(body_id: String) -> void:
return
## Generic body lookup by id — {} if not found. Shared read used by both
## select_body_by_id()/open_body_by_id()'s own linear scans (kept inline in
## each for now, matching this file's existing per-method scan style) and by
## AtlasAgentInterface's open_body pre-check (PR #209 review, Hoshe finding
## 3) so the intent layer can build a structured "no terrain reference"
## error without re-deriving the emit-vs-no-op logic already owned by
## open_body_by_id() above.
func find_body(body_id: String) -> Dictionary:
for b: Dictionary in _orbital_bodies:
if str(b.get("body_id", "")) == body_id:
return b
return {}
func _close_detail_panels() -> void:
_selected_body = {}
_selected_station = {}