chore(plugin-ui): review cleanup — hermetic memo tests, shelf button anchors, outline pin, bounded miss cache; file #486/#487; correct #461
- Split the two hermetic RetailMarkupIconResolver memoization tests (and their counting fakes) out of the Lane=InstalledDat class into a new untagged RetailMarkupIconResolverMemoizationTests.cs so CI's portable filter (Lane!=InstalledDat) actually runs them. - PluginSidePanel: move the entry button's Anchors = AnchorEdges.None from the Add() call site into PluginShelfButton's own constructor (same comment carried over) so a second construction path cannot miss it. - UiRectOutlinePainterOrderTests: assert the back panel's border segment carries exactly 4 quads (24 vertices, FloatsPerVertex each) so a partial outline cannot pass the painter-order check. - RetailMarkupIconResolver: document the type as UI-thread-only (every caller is a draw-time icon source) and bound the MISS cache to 256 entries with FIFO eviction — HIT entries stay unbounded (bounded by the DAT's own surface count already). New test proves the 257th distinct miss evicts the first (re-probe count rises); verified failing first against the un-bounded code (Expected 258, Actual 257) before restoring the fix. - docs/plugin-ui-markup.md: split the icon-binding row's failure mode into Build-time (missing property only — the binder never checks CLR type) vs. draw-time (a resolved value that cannot convert to a number throws from the draw, not from Build). - docs/ISSUES.md: filed #486 (credits picture scroll frozen by the per-draw anchor pass) and #487 (radar compass tokens candidate, same mechanism, unconfirmed); corrected #461's causality — the graceful logout/reveal-cancel log lines are printed by LiveSessionController.Tick's catch -> StopAfterFailure -> StopCore AFTER the motion-update exception, then it rethrows, so the logout is a consequence of the crash, not its cause; real chain is the #462 stalled login-reveal materialization leaving PlayerMovementController in RuntimeOwnedDormant outside its SetPosition ground phase when an inbound 0xF74C arrives. - Plan doc: recorded the three fix-round commits' verdicts (all PASS) and the Smoke-plugin cleanup commit SHA in the Review ledger, plus a pointer to the two newly filed issues. Verified: dotnet build AcDream.slnx -c Release (0/0), targeted filter 85/0/0, full App suite 7364 passed / 97 skipped / 36 failed (36 pre-existing InstalledDat/Manual/Linux-only failures, unchanged by name from baseline; net +1 passed test from the new eviction test). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
parent
ece2104189
commit
f2d7562c86
8 changed files with 416 additions and 158 deletions
113
docs/ISSUES.md
113
docs/ISSUES.md
|
|
@ -39,6 +39,65 @@ confirmed closed by the owner, 11 need a focused live gate, and 43 are safe to
|
|||
remain closed. See
|
||||
[`docs/research/2026-08-28-owner-closed-issue-validity-audit.md`](research/2026-08-28-owner-closed-issue-validity-audit.md).
|
||||
|
||||
## #486 — Credits picture scroll is frozen by the per-draw anchor pass
|
||||
|
||||
**Status:** OPEN — found 2026-09-06 by the Opus review of `47eb2d575` (the
|
||||
plugin-shelf anchor fix), while confirming no other controller shares the
|
||||
same mechanism.
|
||||
**Severity:** LOW (credits screen only)
|
||||
**Component:** `src/AcDream.App/UI/Layout/CreditsUiController.cs`
|
||||
|
||||
**Description.** `AddPicture` (~331-345) creates each scrolling `UiPanel`
|
||||
picture with the DEFAULT `Left|Top` anchors (never `AnchorEdges.None`), and
|
||||
`ScrollPictures` (~293-297) writes `picture.Top` every tick to move it up the
|
||||
screen. `UiElement.ApplyAnchor` (`UiElement.cs` ~829-856) runs for every
|
||||
anchored child on every draw: it captures the Left/Top/Width/Height margins
|
||||
ONCE on the first draw, then re-applies that frozen snapshot on every later
|
||||
draw — silently overwriting whatever `ScrollPictures` had just written. After
|
||||
the first draw, the picture's position can no longer actually change, so the
|
||||
credits scroll freezes.
|
||||
|
||||
`_textArea.Top` (~208-209) has the same write pattern but a different
|
||||
mechanism gap: `_textArea` is the IMPORTED-layout flavour (its
|
||||
`UiElement.LayoutPolicy` is set, from `ImportedLayout`/`LayoutImporter`), so
|
||||
`ApplyAnchor` takes the `LayoutPolicy.Apply` branch instead of the anchor-
|
||||
margin capture — but nothing ever calls `ResetAnchorCapture()`/
|
||||
`UiLayoutPolicy.Rebase` after `ScrollPictures` writes `Top`, so the imported
|
||||
layout's own baseline goes stale the same way.
|
||||
|
||||
**Fix direction:** `Anchors = AnchorEdges.None` on `AddPicture`'s
|
||||
programmatically-repositioned children (the controller is their sole layout
|
||||
owner, exactly the reasoning `PluginSidePanel` already documents for its own
|
||||
runtime-positioned children); `ResetAnchorCapture()`/`Rebase` on `_textArea`
|
||||
after each deliberate `Top` write for the imported-layout element. Precedent:
|
||||
`PluginSidePanel` (commit `47eb2d575`), `UiItemList.cs:170-172`,
|
||||
`MapPageController.cs:235-249` (the same fix already landed for other
|
||||
runtime-repositioned imported/programmatic elements).
|
||||
|
||||
## #487 — Radar compass tokens may be pinned by the anchor pass (candidate)
|
||||
|
||||
**Status:** OPEN — CANDIDATE, found 2026-09-06 by the Opus review of
|
||||
`47eb2d575`, not confirmed by a live gate.
|
||||
**Severity:** LOW
|
||||
**Component:** `src/AcDream.App/UI/Layout/RadarController.cs`
|
||||
|
||||
**Description.** `ApplyPresentation` (~130-144) repositions the four
|
||||
imported compass-letter tokens (`token.Element.Left`/`.Top`) on every
|
||||
heading change, and `CreateToken` (~185-194) never marks them as runtime-
|
||||
positioned — same imported-layout (`LayoutPolicy`) shape as
|
||||
`MapPageController.PrepareIcon`'s F1 finding, which needed the same fix.
|
||||
|
||||
**Symptom if real:** compass letters (N/E/S/W or similar) that never
|
||||
rotate with the player's heading, staying pinned at their first-draw
|
||||
position — the same class of bug as #486, just on the radar's imported
|
||||
tokens instead of the credits screen's programmatic pictures.
|
||||
|
||||
**Not established:** whether `ApplyAnchor`'s snapshot-freeze actually bites
|
||||
here — it depends on whether these tokens' `LayoutPolicy`/anchor state ends
|
||||
up captured before `ApplyPresentation`'s first write, which needs the
|
||||
owner's eyes on a live heading change to confirm. Filed as a candidate
|
||||
rather than assumed broken.
|
||||
|
||||
## #485 — Gitea portable CI races console capture and selects Vulkan-only tests
|
||||
|
||||
**Status:** DONE — correction implemented and locally verified, 2026-09-06;
|
||||
|
|
@ -732,11 +791,55 @@ movement controller cannot be mutated.` at `PlayerMovementController.EnsureConfi
|
|||
then the crash — a `0xF74C`/`0xF625`-class motion update for the local player arrived after the
|
||||
controller was retired by the logout, and the inbound route still forwards it to the controller.
|
||||
|
||||
**Fix direction:** the inbound motion route must consult the controller's lifetime (the same
|
||||
sealed/retired state `EnsureConfigurationMutable` throws on) and drop local-player motion once the
|
||||
session is logging out — the J5.4/J5.7 terminal ledger owns that state; retail discards inbound
|
||||
movement for a character that has left the world. Add the ordered-teardown test: logout confirmed →
|
||||
a late `UpdateMotion` for the local player → no throw, no mutation.
|
||||
**Corrected root cause (2026-09-06).** The original "logout → crash" framing above has the causality
|
||||
backwards. Verified against `.claude/worktrees/peaceful-blackburn-5333f0/logs/selfgate-20260903-165745-g3c/`
|
||||
(`client.log`, `client.err.log`) and `tools/overhaul-selfgate/route-g3c.txt` in that same worktree:
|
||||
`LiveSessionController.Tick` (`LiveSessionController.cs:715-751`) runs `_operations.Tick(scope.Session)`
|
||||
inside a `try` at line 730; THAT call is what throws (the `WorldSession.ProcessDatagram` → `OnMotion` →
|
||||
`EnsureConfigurationMutable` chain above). The `catch (Exception tickError)` at line 735 calls
|
||||
`StopAfterFailure(tickError)` (line 737), which runs `StopCore()` (line 1001) — `StopCore` is what
|
||||
prints `[session] graceful logout requested`/`confirmed` (`WorldSession.cs:3759/3761`) and cancels the
|
||||
Login reveal — and only THEN does `StopAfterFailure` rethrow, unwinding out through `GameWindow.OnUpdate`
|
||||
to the unhandled-exception crash. So the log's `[session] graceful logout requested/confirmed` and
|
||||
`[world-reveal] event=cancel` lines are a CONSEQUENCE of the already-thrown exception (the crash-recovery
|
||||
path's own teardown attempt), not its trigger — nothing about logging out caused this crash.
|
||||
|
||||
The real chain: the login reveal into `0xA9B40176` stalled (#462: `ready=True materialized=False` for
|
||||
180 s — `client.log:204-205` never advance to `materialized=True`). The local player's
|
||||
`PlayerMovementController` therefore never left `PlayerMovementControllerPublicationLifecycle.RuntimeOwnedDormant`
|
||||
(`PlayerMovementController.cs:135`) — the one path that clears it,
|
||||
`RuntimeLocalPlayerPhysicsPublicationState`'s dormant-activation dispatch
|
||||
(`RuntimeSetPositionState.TryApplyDormantLocalActivationCommit`, bracketed by
|
||||
`PlayerMovementController.BeginDormantSetPositionGroundPhase`/`EndDormantSetPositionGroundPhase` —
|
||||
`RuntimeLocalPlayerPhysicsPublicationState.cs:590-625`), never ran because it is gated on the same
|
||||
reveal materialization that never completed. The route's `wait world-visible` step then timed out and
|
||||
the script's next verb (`command /teleloc ...`) made ACE send a `0xF74C` for the player. That reached
|
||||
`LiveEntityNetworkUpdateController.OnMotion` → `PlayerMovementController.SetLastMoveWasAutonomous` →
|
||||
`EnsureConfigurationMutable` (`PlayerMovementController.cs:1038-1049`), whose predicate allows mutation
|
||||
only for `StandalonePublished`/`CandidatePreparing`/`RuntimePublished`, OR `RuntimeOwnedDormant` while
|
||||
`_dormantSetPositionGroundPhase` is true (i.e. actively inside that one bracketed activation window) —
|
||||
by elimination, a `RuntimeOwnedDormant` controller OUTSIDE that window is the only lifecycle state a
|
||||
live `_controller` can be in here, and it throws.
|
||||
|
||||
Retail's `CPhysics::SetObjectMovement @0x00509690` stores the autonomous byte and calls
|
||||
`unpack_movement` as soon as the object exists — there is no dormant window at all. acdream's accepted
|
||||
equivalent for exactly this "motion arrived before the entity finished materializing" shape already
|
||||
exists for every OTHER entity: `RuntimeEntityObjectLifetime.TryApplyMotion`
|
||||
(`RuntimeEntityObjectLifetime.cs:1568-1606`) checks `TryGetPendingInitialResidence` for the guid and, if
|
||||
the entity still has one, enqueues the motion as a deferred continuation
|
||||
(`EnqueueDormant(..., RuntimeInitialCreateContinuationKind.Movement, ...)`) instead of applying it
|
||||
immediately or throwing. The dormant LOCAL player bypasses this retention entirely: its motion is routed
|
||||
through the player-specific `LiveEntityNetworkUpdateController.OnMotion` → `PlayerMovementController`
|
||||
path, not through the guid-keyed `RuntimeEntityObjectLifetime.TryApplyMotion` retention every other
|
||||
entity already gets.
|
||||
|
||||
**Fix direction:** route an accepted local-player motion that arrives while the controller is dormant
|
||||
into that SAME kind of retention (replayed once `TryApplyDormantLocalActivationCommit` actually
|
||||
activates the controller), never a drop guard and never a silent no-op — a workaround here would just
|
||||
trade a crash for silently losing a real wire motion. Test: dormant controller + inbound player
|
||||
`0xF74C` → no throw, and the motion is applied after activation completes (not dropped). This defect is
|
||||
reachable in ordinary play only through the #462 stalled-materialization window, so fix #462 alongside
|
||||
it — without #462, the dormant window this bug lives in should not exist in the first place.
|
||||
|
||||
## #460 — Shutdown hangs windowless at 100 % of one core after a close request that follows an aborted automation script (Nanto pose)
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue