docs: UI architecture review report + follow-up issues #159-#165
Full 6-dimension review (34 confirmed findings, 0 refuted) in docs/research/2026-07-02-ui-architecture-review.md. Verdict: widget toolkit / importer / controller-binding / cursor seams are sound; rot concentrates in GameWindow composition growth, state-write ownership, input-stack arbitration, and design-doc drift. New issues: #159 retail-chat ChatVM providers, #160 window-close notification + NoLongerViewingContents, #161 target-mode click chokepoint, #162 world-hover cursor, #163 raise ledger, #164 RetailWindowFrame migration, #165 UI doc reconciliation. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
b7dc91a053
commit
59ea3252cd
2 changed files with 345 additions and 0 deletions
133
docs/ISSUES.md
133
docs/ISSUES.md
|
|
@ -46,6 +46,139 @@ Copy this block when adding a new issue:
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## #159 — Retail chat ChatVM lacks Fps/Position providers — /framerate and /loc degrade
|
||||||
|
|
||||||
|
**Status:** OPEN
|
||||||
|
**Severity:** MEDIUM
|
||||||
|
**Filed:** 2026-07-02
|
||||||
|
**Component:** ui
|
||||||
|
|
||||||
|
**Description:** Two live `ChatVM` instances exist over the same `ChatLog`: the ImGui panel's (gets `FpsProvider` + `PositionProvider`) and the retail chat window's (`new ChatVM(Chat, displayLimit: 200)` — gets neither, and the providers are init-only). Typing `/framerate` or `/loc` in the retail chat window returns "(provider unavailable)".
|
||||||
|
|
||||||
|
**Root cause / status:** Found by the 2026-07-02 UI architecture review (theme T8). Fix direction: construct ONE `ChatVM` (with providers) before either UI block and share the instance.
|
||||||
|
|
||||||
|
**Files:** `src/AcDream.App/Rendering/GameWindow.cs:1438` (ImGui VM), `:1937` (retail VM), `src/AcDream.UI.Abstractions/Panels/Chat/ChatVM.cs:63/70`.
|
||||||
|
|
||||||
|
**Research:** `docs/research/2026-07-02-ui-architecture-review.md`.
|
||||||
|
|
||||||
|
**Acceptance:** `/framerate` and `/loc` produce real values in the retail chat window; one ChatVM instance per ChatLog.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## #160 — Inventory window close never notifies the controller (NoLongerViewingContents skipped)
|
||||||
|
|
||||||
|
**Status:** OPEN
|
||||||
|
**Severity:** MEDIUM
|
||||||
|
**Filed:** 2026-07-02
|
||||||
|
**Component:** ui / net
|
||||||
|
|
||||||
|
**Description:** `UiRoot`'s window registry flips `Visible` with no event, and the inventory close button routes to `GameWindow.CloseRetailWindow` (HideWindow + toolbar sync only). `InventoryController` never learns the window closed: `NoLongerViewingContents (0x0195)` is not sent for the viewed side pack, `_openContainer` stays stale, and reopening shows the old bag grid without re-sending Use.
|
||||||
|
|
||||||
|
**Root cause / status:** Review finding (theme T2). Fix direction: add `WindowShown`/`WindowHidden` events to the UiRoot window registry; InventoryController closes out the viewed container on hide; toolbar-button sync subscribes instead of the manual `SyncToolbarWindowButtons()` push at every mutation site.
|
||||||
|
|
||||||
|
**Files:** `src/AcDream.App/UI/UiRoot.cs:541-572`, `src/AcDream.App/UI/Layout/InventoryController.cs:404`, `src/AcDream.App/Rendering/GameWindow.cs` (`CloseRetailWindow`/`SyncToolbarWindowButtons`).
|
||||||
|
|
||||||
|
**Research:** `docs/research/2026-07-02-ui-architecture-review.md`.
|
||||||
|
|
||||||
|
**Acceptance:** closing the inventory window sends NoLongerViewingContents for the open side pack; reopening shows the main pack; no manual Sync call sites remain.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## #161 — Target-mode clicks: inventory grid cells bypass ItemInteractionController while the cursor says TargetValid
|
||||||
|
|
||||||
|
**Status:** OPEN
|
||||||
|
**Severity:** MEDIUM
|
||||||
|
**Filed:** 2026-07-02
|
||||||
|
**Component:** ui
|
||||||
|
|
||||||
|
**Description:** Target-mode interception is per-call-site opt-in. Toolbar, paperdoll, and the main-pack cell consult `ItemInteractionController` first; `InventoryController.AddCell` grid cells wire `Clicked = () => SelectItem(guid)` (and bags to `OpenContainer`) with no target-mode check — while `CursorFeedbackController.ResolveUseTargetGuid` returns `slot.ItemId` for ANY occupied slot, so the cursor shows the valid-target bullseye over cells whose click just selects.
|
||||||
|
|
||||||
|
**Root cause / status:** Review finding (theme T3). Fix direction: one chokepoint — `UiItemSlot` (or UiRoot) offers the click to `ItemInteractionController.AcquireTarget` first while target mode is active, before the per-window `Clicked` callback.
|
||||||
|
|
||||||
|
**Files:** `src/AcDream.App/UI/Layout/InventoryController.cs:307-308`, `src/AcDream.App/UI/UiItemSlot.cs:187-189`, `src/AcDream.App/UI/CursorFeedbackController.cs:240-254`.
|
||||||
|
|
||||||
|
**Research:** `docs/research/2026-07-02-ui-architecture-review.md`.
|
||||||
|
|
||||||
|
**Acceptance:** with a health kit armed, clicking any inventory item applies the kit to that item (or rejects per useability), matching what the cursor promised.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## #162 — Target-mode cursor is blind to world hover (always pending crosshair over world entities)
|
||||||
|
|
||||||
|
**Status:** OPEN
|
||||||
|
**Severity:** MEDIUM
|
||||||
|
**Filed:** 2026-07-02
|
||||||
|
**Component:** ui
|
||||||
|
|
||||||
|
**Description:** `CursorFeedbackController.Update(UiRoot)` builds `HoverTargetGuid` exclusively from the UI tree; hovering a world entity (e.g. a drudge with a health kit armed) shows the pending crosshair instead of valid/invalid. Retail keys the 0x28/0x29 cursors off the SmartBox target under the cursor — a world object (`ClientUISystem::UpdateCursorState` 0x00564630).
|
||||||
|
|
||||||
|
**Root cause / status:** Review finding (theme T7). Fix direction: inject a world-hover-guid provider (`Func<uint>`, from the existing world-pick machinery) into `CursorFeedbackController` and merge it into the snapshot when the UI pick misses.
|
||||||
|
|
||||||
|
**Files:** `src/AcDream.App/UI/CursorFeedbackController.cs:53-72`, `src/AcDream.App/Rendering/GameWindow.cs` (`UpdateRetailCursorFeedback`; world pick at the B.4b select path).
|
||||||
|
|
||||||
|
**Research:** `docs/research/2026-07-02-ui-architecture-review.md`.
|
||||||
|
|
||||||
|
**Acceptance:** hovering a valid world target in target mode shows the yellow bullseye (0x28); invalid shows the red blocked cursor (0x29).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## #163 — Character raise flow: adopt the pending/confirm/rollback ledger
|
||||||
|
|
||||||
|
**Status:** OPEN
|
||||||
|
**Severity:** MEDIUM
|
||||||
|
**Filed:** 2026-07-02
|
||||||
|
**Component:** ui / core
|
||||||
|
|
||||||
|
**Description:** Item moves reconcile optimistic state through `RecordPending`/`ConfirmMove`/`RollbackMove` (0x0022/0x0023/0x00A0); character raises apply optimistically (`CharacterSheetProvider.HandleRaiseRequest` → `ApplyLocalRaise`) with no pending snapshot and no rollback. A server-rejected raise leaves ranks/XP wrong until the next full property refresh. Register row AP-73.
|
||||||
|
|
||||||
|
**Root cause / status:** Review finding (theme T2); the spends now at least go through eventful store APIs (extraction commit). Fix direction: pending snapshot + confirm on the server's stat-update echo + rollback on failure, mirroring the item-move ledger.
|
||||||
|
|
||||||
|
**Files:** `src/AcDream.App/UI/Layout/CharacterSheetProvider.cs` (`HandleRaiseRequest`), `src/AcDream.Core/Player/LocalPlayerState.cs` (`Apply*Raise`).
|
||||||
|
|
||||||
|
**Research:** `docs/research/2026-07-02-ui-architecture-review.md`.
|
||||||
|
|
||||||
|
**Acceptance:** a raise the server rejects (e.g. insufficient XP race) visibly reverts within one echo round-trip; a confirmed raise never flickers.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## #164 — Migrate remaining retail-window mounts (vitals/chat/toolbar/inventory + MockupDesktop) to RetailWindowFrame
|
||||||
|
|
||||||
|
**Status:** OPEN
|
||||||
|
**Severity:** LOW
|
||||||
|
**Filed:** 2026-07-02
|
||||||
|
**Component:** ui
|
||||||
|
|
||||||
|
**Description:** `RetailWindowFrame.Mount` (extraction commit) is the shared nine-slice mount recipe; the character window uses it. Vitals, chat, toolbar, and inventory still inline the same block in `GameWindow.OnLoad`, and `Studio/MockupDesktop` hand-copies the recipes (already drifting). Each is a mechanical migration; do them one window at a time with visual confirmation (chat has Opacity + shrinkable MinWidth quirks).
|
||||||
|
|
||||||
|
**Root cause / status:** Review finding (theme T1).
|
||||||
|
|
||||||
|
**Files:** `src/AcDream.App/Rendering/GameWindow.cs` (~1973 chat, ~2120 toolbar, ~2290 inventory), `src/AcDream.App/Studio/MockupDesktop.cs:184`, `src/AcDream.App/UI/Layout/RetailWindowFrame.cs`.
|
||||||
|
|
||||||
|
**Research:** `docs/research/2026-07-02-ui-architecture-review.md`.
|
||||||
|
|
||||||
|
**Acceptance:** zero inline `new UiNineSlicePanel` window mounts left in GameWindow/MockupDesktop; all windows pixel-identical before/after (user visual check).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## #165 — Reconcile the UI design docs with the shipped D.2b shape
|
||||||
|
|
||||||
|
**Status:** OPEN
|
||||||
|
**Severity:** MEDIUM
|
||||||
|
**Filed:** 2026-07-02
|
||||||
|
**Component:** docs
|
||||||
|
|
||||||
|
**Description:** `docs/plans/2026-04-24-ui-framework.md` still says D.2b "remains design-only" and promises an `AcDream.UI.Retail` `IPanelRenderer` backend; `docs/architecture/acdream-architecture.md` still shows the swappable-backend diagram. Reality (acknowledged only in the 2026-06-14 panel-frame spec §0): the retail UI is the UiRoot tree in `src/AcDream.App/UI/`, the ViewModels are the shared seam, IPanel/IPanelRenderer is the devtools contract. The plugin UI story also fractured: `IUiRegistry.AddMarkupPanel` only renders under `ACDREAM_RETAIL_UI=1`, IPanel only under devtools — decide + document the plugin contract.
|
||||||
|
|
||||||
|
**Root cause / status:** Review findings (theme T8). One doc commit; CLAUDE.md requires the architecture doc never stay out of sync.
|
||||||
|
|
||||||
|
**Files:** `docs/plans/2026-04-24-ui-framework.md:7-8/244`, `docs/architecture/acdream-architecture.md:80-99`, `src/AcDream.Plugin.Abstractions/IUiRegistry.cs`.
|
||||||
|
|
||||||
|
**Research:** `docs/research/2026-07-02-ui-architecture-review.md`.
|
||||||
|
|
||||||
|
**Acceptance:** both docs describe the two-stack reality; the plugin markup-vs-IPanel decision is written down.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## #158 — Character window — deferred polish
|
## #158 — Character window — deferred polish
|
||||||
|
|
||||||
**Status:** OPEN
|
**Status:** OPEN
|
||||||
|
|
|
||||||
212
docs/research/2026-07-02-ui-architecture-review.md
Normal file
212
docs/research/2026-07-02-ui-architecture-review.md
Normal file
|
|
@ -0,0 +1,212 @@
|
||||||
|
# UI System Architecture Review — 2026-07-02
|
||||||
|
|
||||||
|
**Scope:** the D.2b retail UI engine (`src/AcDream.App/UI/` widgets + `UI/Layout/`
|
||||||
|
importer + controllers), `AcDream.UI.Abstractions`, the ImGui backend, the new
|
||||||
|
uncommitted item-interaction + cursor slice, Studio/MockupDesktop, and the
|
||||||
|
GameWindow composition root — reviewed against the CLAUDE.md Code Structure Rules
|
||||||
|
and the three-layer UI strategy (`docs/plans/2026-04-24-ui-framework.md`).
|
||||||
|
|
||||||
|
**Method:** 6 parallel dimension reviewers (layering / widget-tree / importer /
|
||||||
|
controllers / cursor / abstractions-fit), every medium+high finding adversarially
|
||||||
|
verified by an independent agent reading the cited code (40 agents total).
|
||||||
|
Result: **34 findings confirmed, 0 refuted** (2 high, 32 medium), plus 11 low.
|
||||||
|
Five load-bearing findings were additionally spot-checked by hand in this session
|
||||||
|
(GameWindow +490 diff, raw property writes, dead fall-through seam, dual ChatVM,
|
||||||
|
dual useability truth) — all held.
|
||||||
|
|
||||||
|
**Worktree state reviewed:** branch `claude/peaceful-visvesvaraya-e0a196` at
|
||||||
|
`e3fc7ac5` + the uncommitted D.2b item-interaction/cursor diff (51 modified files,
|
||||||
|
19 new files) copied from the codex worktree.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Verdict
|
||||||
|
|
||||||
|
**The middle of the UI system is architecturally sound. The edges are not.**
|
||||||
|
|
||||||
|
The widget toolkit's core abstractions, the importer's two-layer split, the
|
||||||
|
controller binding pattern, the cursor semantic/presentation split, and the
|
||||||
|
ViewModel reuse are all genuinely good — GL-free, testable, single-responsibility,
|
||||||
|
with the Studio proving the bindings run headless. The structural rot concentrates
|
||||||
|
in four places:
|
||||||
|
|
||||||
|
1. **GameWindow as composition root keeps absorbing feature bodies** — the
|
||||||
|
uncommitted work alone added +490 lines (now 14,214), most of it a character-sheet
|
||||||
|
data provider that belongs in its own class. Growth is linear in window count.
|
||||||
|
2. **State ownership around the character sheet is violated** — the UI writes raw
|
||||||
|
property dictionaries, bypassing both designed change-event chokepoints, with no
|
||||||
|
rollback contract (item moves have one; raises don't).
|
||||||
|
3. **Three input stacks coexist without an arbiter** — InputDispatcher (Phase K),
|
||||||
|
UiRoot (retail), ImGui — glued by order-dependent boolean gates; the designed
|
||||||
|
fall-through seam is dead code.
|
||||||
|
4. **The design SSOT has silently drifted** — the ui-framework plan and architecture
|
||||||
|
doc still promise an `IPanelRenderer` backend swap that the shipped D.2b pivoted
|
||||||
|
away from; the plugin UI contract fractured along the same line.
|
||||||
|
|
||||||
|
None of this is load-bearing-broken *today* — the app works — but every finding is
|
||||||
|
a compounding tax on the next window, the next input consumer, the next plugin.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## What is sound (keep doing this)
|
||||||
|
|
||||||
|
- **Widget layer is GL-free and testable**: sprites/fonts arrive via injected
|
||||||
|
`Func<uint,(tex,w,h)>` resolve delegates; headless tests exist at the right layer.
|
||||||
|
- **Draw vs game-state mutation separated**: widget draw paths only render; wire
|
||||||
|
actions flow through injected controller delegates.
|
||||||
|
- **Drag policy split is right**: UiRoot owns only the pointer state machine
|
||||||
|
(item-agnostic via `GetDragPayload`/`GetDragGhost` virtuals); semantics live in
|
||||||
|
controllers.
|
||||||
|
- **Two-pass overlay draw** reproduces retail's root-level MakePopup semantics
|
||||||
|
without reparenting.
|
||||||
|
- **Importer two-layer split is clean**: pure layer (`Build`/`BuildFromInfos` over
|
||||||
|
`ElementInfo` POCOs) is GL-free and dat-free; `DatReaderWriter` types never leak
|
||||||
|
past the importer — which is exactly why the same path runs in Studio.
|
||||||
|
- **`DatWidgetFactory.Create` is a single localized type-registration switch** —
|
||||||
|
adding a retail widget type is one case + one widget class.
|
||||||
|
- **SSOT largely holds for items**: controllers read `ClientObjectTable` live via
|
||||||
|
its events and keep only view state; controller-to-controller coupling is
|
||||||
|
mediated (events + injected services), never direct.
|
||||||
|
- **ItemInteractionController is a well-shaped seam**: constructor-injected
|
||||||
|
delegates (player guid, clock, wire sends, toast) — unit-testable without GL or
|
||||||
|
session.
|
||||||
|
- **Cursor split is real**: `CursorFeedbackController.ResolveKind` is the single
|
||||||
|
priority chain for cursor MEANING (pure over a snapshot record, tested);
|
||||||
|
EnumIDMap resolution is dat-faithful with a decomp citation
|
||||||
|
(`ClientUISystem::UpdateCursorState` 0x00564630), cached, lock-disciplined;
|
||||||
|
`RetailCursorManager` dedupes OS writes. Its GameWindow wiring is exemplary
|
||||||
|
rule-1 compliance (2 fields + 7-line helper).
|
||||||
|
- **The ViewModel layer survived the D.2b pivot**: ChatWindowController consumes
|
||||||
|
`ChatVM`/`ChatCommandRouter`/`ChatInputParser`; VitalsVM reused via thin Func
|
||||||
|
adapters; VitalsController is pure data binding by dat element id.
|
||||||
|
- **D.2a boundary discipline held for two years**: no Abstractions panel imports a
|
||||||
|
backend namespace; exactly one IPanelRenderer implementation, isolated in
|
||||||
|
AcDream.UI.ImGui.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Findings by theme
|
||||||
|
|
||||||
|
Severities are post-verification. Every row was confirmed by an independent
|
||||||
|
adversarial reader at the cited location.
|
||||||
|
|
||||||
|
### T1 — GameWindow god-object growth (the dominant theme; both HIGHs)
|
||||||
|
|
||||||
|
| Sev | Finding | Where |
|
||||||
|
|---|---|---|
|
||||||
|
| **HIGH** | ~430 lines of character-sheet feature body added directly to GameWindow (uncommitted): `CurrentCharacterSheet`, `ComputeLevelXp`, `BuildAttributeRaiseCosts`, `BuildLiveCharacterSkills`, `RaiseCostFromXpCurve`, raise send/apply flow. Pure dat-table domain math trapped untestable in a GL-bound 14K-line class. Two bare `catch {}` swallows in `LoadCharacterExperienceTable`; `IsUsableUntrained` hardcodes skill ids 18/37–40. File grew 13,719 → 14,214 in this stream; committed-only delta was +13 — all growth is this feature. | [GameWindow.cs:11766](src/AcDream.App/Rendering/GameWindow.cs:11766) |
|
||||||
|
| **HIGH** | The whole retail-UI composition block in `OnLoad` is ~590 lines of real layout logic (per-window mount recipes: nine-slice frame construction, anchor rewrites, layout constants `Left=540`, `MaxHeight=760`). Each new retail window adds another mount block — growth is linear in the D.2b window count. | [GameWindow.cs:1834](src/AcDream.App/Rendering/GameWindow.cs:1834) |
|
||||||
|
| MED | Per-window mount recipes duplicated verbatim between GameWindow and Studio's MockupDesktop (same element-id anchor rewrites in two files). | [MockupDesktop.cs:184](src/AcDream.App/Studio/MockupDesktop.cs:184), [GameWindow.cs:2161](src/AcDream.App/Rendering/GameWindow.cs:2161) |
|
||||||
|
| MED | GameWindow reaches back into raw LayoutDesc structures after import, duplicating the importer's property parsing inline. | [GameWindow.cs:2013](src/AcDream.App/Rendering/GameWindow.cs:2013) |
|
||||||
|
| MED | Studio's `RenderBootstrap` is a hand-maintained copy of GameWindow.OnLoad wiring — already drifting. | [RenderBootstrap.cs:147](src/AcDream.App/Rendering/RenderBootstrap.cs:147) |
|
||||||
|
|
||||||
|
### T2 — State ownership / event flow
|
||||||
|
|
||||||
|
| Sev | Finding | Where |
|
||||||
|
|---|---|---|
|
||||||
|
| MED | Single-owner-state violation: GameWindow writes raw `PropertyBundle` dictionaries (`props.Int64s[2u]`, `props.Ints[...]` with a 0x18→0xC0→0xB5 guess-chain), bypassing BOTH designed change-event chokepoints (`ClientObjectTable.UpdateIntProperty` fires ObjectUpdated; `LocalPlayerState` mutators fire CharacterChanged). The only raw writes into live bundles outside ClientObjectTable, repo-wide. Same bug class as `feedback_single_owner_state_per_entity_write`. | [GameWindow.cs:12106](src/AcDream.App/Rendering/GameWindow.cs:12106) |
|
||||||
|
| MED | Optimistic-mutation contract inconsistent: item moves have the `RecordPending`/`ConfirmMove`/`RollbackMove` ledger (reconciled on 0x0022/0x0023/0x00A0); character raises apply immediately with no pending snapshot and no rollback — a rejected raise leaves the sheet permanently wrong until an unrelated full refresh. | [GameWindow.cs:12080](src/AcDream.App/Rendering/GameWindow.cs:12080) |
|
||||||
|
| MED | Two parallel "use an item" stacks with two useability truths: world double-click keys off GameWindow-private `_lastSpawnByGuid` (24 refs); the retail UI path keys off `ClientObjectTable`. | [GameWindow.cs:13324](src/AcDream.App/Rendering/GameWindow.cs:13324) |
|
||||||
|
| MED | `LocalPlayerState.CharacterChanged` has zero subscribers while the character sheet polls a full sheet rebuild via ~30 per-widget closures per frame. `Properties` returns the internal mutable reference despite its "snapshot" doc. | [LocalPlayerState.cs:134](src/AcDream.Core/Player/LocalPlayerState.cs:134) |
|
||||||
|
| MED | Window open/close state has no notification channel to controllers: inventory close button routes straight to GameWindow's `CloseRetailWindow`, so `NoLongerViewingContents` is never sent for the viewed container and `_openContainer` goes stale. Toolbar-button sync is manual push at every mutation site. | [InventoryController.cs:404](src/AcDream.App/UI/Layout/InventoryController.cs:404) |
|
||||||
|
| LOW | Window-visibility/toolbar sync manual push, no event seam on UiHost/UiRoot. | [GameWindow.cs:12023](src/AcDream.App/Rendering/GameWindow.cs:12023) |
|
||||||
|
| LOW | `ShortcutStore` (session-scoped state) trapped as a private field of ToolbarController. | [ToolbarController.cs:70](src/AcDream.App/UI/Layout/ToolbarController.cs:70) |
|
||||||
|
|
||||||
|
### T3 — Input arbitration (three stacks, no arbiter)
|
||||||
|
|
||||||
|
| Sev | Finding | Where |
|
||||||
|
|---|---|---|
|
||||||
|
| MED | Retail UI is a parallel Silk input subscription arbitrated by order-dependent boolean gates; the designed `WorldMouseFallThrough`/`WorldKeyFallThrough` seam exists only in a doc comment — dead code. | [UiHost.cs:78](src/AcDream.App/UI/UiHost.cs:78) |
|
||||||
|
| MED | UiRoot bypasses the Phase-K InputDispatcher scope stack and hardcodes its own chords. | [UiRoot.cs:463](src/AcDream.App/UI/UiRoot.cs:463) |
|
||||||
|
| MED | No arbitration between ImGui and UiRoot input stacks — both subscribe raw Silk devices; neither yields to the other's capture. | [UiHost.cs:78](src/AcDream.App/UI/UiHost.cs:78) |
|
||||||
|
| MED | Target-mode click interception is per-call-site opt-in: Toolbar/Paperdoll/main-pack consult ItemInteractionController first; InventoryController grid cells don't (`Clicked = () => SelectItem(guid)`), while the cursor shows TargetValid over those same cells — the first observable drift. | [InventoryController.cs:308](src/AcDream.App/UI/Layout/InventoryController.cs:308) |
|
||||||
|
|
||||||
|
### T4 — Widget engine internals
|
||||||
|
|
||||||
|
| Sev | Finding | Where |
|
||||||
|
|---|---|---|
|
||||||
|
| MED | Layout runs inside the draw traversal with no invalidation model; hit-testing/input consume draw-produced geometry (stale rects between frames; hidden subtrees' layout frozen). Already forced the `Anchors=None` exemption hack, repeated in UiScrollablePanel. | [UiElement.cs:323](src/AcDream.App/UI/UiElement.cs:323) |
|
||||||
|
| MED | Hit-testing allocates + sorts per node per call (`_children.ToArray()` + `Array.Sort` at every level), hidden behind property getters composed multiplicatively — the cursor controller added ~5 full walks/frame without touching UiRoot. | [UiRoot.cs:66](src/AcDream.App/UI/UiRoot.cs:66) |
|
||||||
|
| MED | Pointer-intent disambiguation is an accreting five-flag boolean lattice, hand-duplicated between press-time (`OnMouseDown`) and hover-time (`HoverWindowMove`) — drift shows as "cursor says move but press does drag". | [UiRoot.cs:288](src/AcDream.App/UI/UiRoot.cs:288) |
|
||||||
|
| LOW | Dead Device seams: `RegisterTimerEvent` accumulates timers that never fire; `FireEvent` has no callers. | [UiRoot.cs:675](src/AcDream.App/UI/UiRoot.cs:675) |
|
||||||
|
| LOW | `ItemDragPayload` carries a live widget back-ref no consumer reads, while grids rebuild mid-drag. | [ItemDragPayload.cs:24](src/AcDream.App/UI/ItemDragPayload.cs:24) |
|
||||||
|
|
||||||
|
### T5 — Importer policy scatter
|
||||||
|
|
||||||
|
| Sev | Finding | Where |
|
||||||
|
|---|---|---|
|
||||||
|
| MED | "Which dat children become live widgets" policy has three owners: class-level `ConsumesDatChildren` flag, an importer carve-out naming `UiMeter` concretely, and controllers re-creating swallowed children from hardcoded sprite ids (CharacterStatController tab sprites 0x06005D92 etc.) — a dat-fidelity seam regression. | [LayoutImporter.cs:133](src/AcDream.App/UI/Layout/LayoutImporter.cs:133) |
|
||||||
|
| MED | `FindElement`'s last-wins `byId` table breaks on duplicate dat ids; the workaround is a second, type-incomplete lookup (per-widget `ElementId` switch returning 0 for four widget types) duplicating the base `UiElement.DatElementId` that already exists. Three parallel id-lookup mechanisms coexist. | [LayoutImporter.cs:116](src/AcDream.App/UI/Layout/LayoutImporter.cs:116) |
|
||||||
|
| MED | Scrollbar/menu chrome sprites hardcoded in three controllers although the dat carries them and the importer already resolves the inheritance chain that delivers them. | [DatWidgetFactory.cs:82](src/AcDream.App/UI/Layout/DatWidgetFactory.cs:82) |
|
||||||
|
| MED | Sub-window mount + slot-size cascade are structural heuristics tuned to the windows shipped so far, accreting inline patches. | [LayoutImporter.cs:300](src/AcDream.App/UI/Layout/LayoutImporter.cs:300) |
|
||||||
|
| LOW | Synthetic multi-top-level root leaks a hidden contract (consumers pluck the real window; rely on AddChild re-parenting to orphan stray siblings). | [LayoutImporter.cs:206](src/AcDream.App/UI/Layout/LayoutImporter.cs:206) |
|
||||||
|
| LOW | WindowChromeController accumulates per-window button ids in a shared static, binds by whole-tree sweep, and binds ALL ids (incl. Max/Min) to close — while ChatWindowController binds the same MaxMin id with a different meaning. | [WindowChromeController.cs:13](src/AcDream.App/UI/Layout/WindowChromeController.cs:13) |
|
||||||
|
|
||||||
|
### T6 — Controllers
|
||||||
|
|
||||||
|
| Sev | Finding | Where |
|
||||||
|
|---|---|---|
|
||||||
|
| MED | CharacterStatController is five controllers welded into one 1,959-line **static** `Bind` with closure-boxed state (`new[] { -1 }` mutable-array boxes threaded through ~33 static helpers). No instance → no teardown/rebind path, tab/selection state untestable. The god-object pattern relocated from GameWindow into a controller. | [CharacterStatController.cs:247](src/AcDream.App/UI/Layout/CharacterStatController.cs:247) |
|
||||||
|
| MED | Controller lifecycle contract inconsistent: ToolbarController subscribes with inline lambdas + no Dispose (unremovable by construction); Inventory/Paperdoll declare unsubscribing `Dispose()` but don't implement `IDisposable` and nothing ever calls them. Any relog/rebind or Studio hot-reload double-subscribes. | [ToolbarController.cs:152](src/AcDream.App/UI/Layout/ToolbarController.cs:152) |
|
||||||
|
| MED | Equip-rule logic split across two App-layer files (`ItemEquipRules` + ItemInteractionController's coverage scans) and sits below its natural Core home. | [ItemEquipRules.cs:5](src/AcDream.App/UI/ItemEquipRules.cs:5) |
|
||||||
|
| LOW | ItemInteractionController is pure game-rule logic living in the App.UI namespace (Core candidate). | [ItemInteractionController.cs:11](src/AcDream.App/UI/ItemInteractionController.cs:11) |
|
||||||
|
|
||||||
|
### T7 — Cursor pipeline (new slice)
|
||||||
|
|
||||||
|
| Sev | Finding | Where |
|
||||||
|
|---|---|---|
|
||||||
|
| MED | Cursor-art precedence is an implicit contract split across two layers; generic widget hover art shadows target-mode global cursors. | [CursorFeedbackController.cs:154](src/AcDream.App/UI/CursorFeedbackController.cs:154) |
|
||||||
|
| MED | OS cursor has no single arbiter: RetailCursorManager writes Type/Image/Hotspot across ALL mice; GameWindow independently writes `CursorMode` from three sites on the FIRST mouse only. Works only because the writers touch disjoint properties — an undocumented contract. | [RetailCursorManager.cs:28](src/AcDream.App/Rendering/RetailCursorManager.cs:28) |
|
||||||
|
| MED | Target-mode cursor cannot see world hover: `HoverTargetGuid` is built exclusively from the UI tree, so hovering a drudge with an active health kit shows pending crosshair instead of valid/invalid — retail keys 0x28/0x29 off the SmartBox target (a world object) per the catalog's own citation. | [CursorFeedbackController.cs:67](src/AcDream.App/UI/CursorFeedbackController.cs:67) |
|
||||||
|
| MED | Semantic controller hardcodes dat LayoutDesc state-name priority lists as string literals. | [CursorFeedbackController.cs:165](src/AcDream.App/UI/CursorFeedbackController.cs:165) |
|
||||||
|
| LOW | Silent permanent negative caches + OS-cursor fallback mask dat resolution failures; the fallback deviation has **no divergence-register row** (register rule breach). | [RetailCursorResolver.cs:47](src/AcDream.App/Rendering/RetailCursorResolver.cs:47) |
|
||||||
|
| LOW | Per-frame cursor update runs ~4 full UI hit-tests (property-getter recomputation). | [CursorFeedbackController.cs:57](src/AcDream.App/UI/CursorFeedbackController.cs:57) |
|
||||||
|
| LOW | OS-application half of the seam untested; the golden dat test silently passes when dats are absent. | [RetailCursorCatalogTests.cs:34](tests/AcDream.App.Tests/UI/RetailCursorCatalogTests.cs:34) |
|
||||||
|
|
||||||
|
### T8 — Strategy drift (three-layer plan vs reality)
|
||||||
|
|
||||||
|
| Sev | Finding | Where |
|
||||||
|
|---|---|---|
|
||||||
|
| MED | Shipped D.2b is a parallel fourth stack, not the Abstractions backend the design SSOT promises: ui-framework.md still says D.2b "remains design-only" / "AcDream.UI.Retail implements IPanelRenderer"; the architecture doc still shows the swappable-backend diagram. The pivot is acknowledged only in the 2026-06-14 panel-frame spec §0. The real stable seam is now the **ViewModels**, not IPanel/IPanelRenderer. | [ui-framework.md:8](docs/plans/2026-04-24-ui-framework.md) |
|
||||||
|
| MED | Plugin UI contract fractured: plugin surface exposes only `AddMarkupPanel`; markup panels render only under `ACDREAM_RETAIL_UI=1`, IPanel panels only under devtools. Plugin.Abstractions has zero project references — no backend-agnostic IPanel registration exists despite both docs promising it. | [IUiRegistry.cs:13](src/AcDream.Plugin.Abstractions/IUiRegistry.cs:13) |
|
||||||
|
| MED | Two live ChatVM instances with asymmetric wiring: the retail ChatVM lacks Fps/Position providers (init-only, can't be wired later) → `/framerate` and `/loc` silently degrade in the retail chat window — the surface meant to replace the ImGui one. | [GameWindow.cs:1937](src/AcDream.App/Rendering/GameWindow.cs:1937) |
|
||||||
|
| MED | Vivid Target Indicator (retail gameplay UI, decomp 0x004d6165) is implemented on the ImGui backend inside the devtools gate — in the end-state (RETAIL_UI=1, DEVTOOLS=0), selection has no visual indicator at all. | [TargetIndicatorPanel.cs:4](src/AcDream.App/UI/TargetIndicatorPanel.cs:4) |
|
||||||
|
| LOW | WindowChromeController hardwires a global close-button id list; binds Max/Min to close for every window. | [WindowChromeController.cs:52](src/AcDream.App/UI/Layout/WindowChromeController.cs:52) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Recommended remediation sequence (awaiting approval — report only)
|
||||||
|
|
||||||
|
Ordered by leverage; R1 is time-sensitive because the breach is still uncommitted.
|
||||||
|
|
||||||
|
- **R1 — Extract the character-sheet provider BEFORE committing this work.** Move
|
||||||
|
`CurrentCharacterSheet` + XP-curve/raise-cost math + raise send/apply into a
|
||||||
|
`CharacterSheetProvider` (App/UI/Layout; the pure curve math is Core-eligible),
|
||||||
|
and the repeated window-mount block into a small `RetailWindowMounter` shared
|
||||||
|
with MockupDesktop. This addresses both HIGHs + the Studio duplication at once,
|
||||||
|
and avoids cementing a rule-1 breach into history.
|
||||||
|
- **R2 — State-write chokepoints.** Route XP/credit debits through eventful APIs
|
||||||
|
(`ClientObjectTable.UpdateIntProperty` + an Int64 twin + a `LocalPlayerState`
|
||||||
|
spend API firing CharacterChanged); adopt the pending/confirm/rollback ledger
|
||||||
|
for raises; make `LocalPlayerState.Properties` read-only.
|
||||||
|
- **R3 — Input arbitration.** Decide the ownership model (single Silk owner
|
||||||
|
fanning out, or explicit documented gate order); delete or wire the dead
|
||||||
|
fall-through seam; route target-mode click interception through one chokepoint
|
||||||
|
(UiItemSlot/UiRoot offers the click to ItemInteractionController first).
|
||||||
|
- **R4 — Doc + register reconciliation.** One commit: ui-framework.md status
|
||||||
|
header + architecture-doc UI section rewritten to the real shape (two coexisting
|
||||||
|
stacks, VMs shared, retail UI = UiRoot tree); divergence-register row for the
|
||||||
|
cursor OS-fallback; decide the plugin markup-vs-IPanel contract question.
|
||||||
|
- **R5 — Structural debt (phase-scale, schedule separately).** CharacterStatController
|
||||||
|
→ instance controller; importer policy consolidation (per-child consume predicate,
|
||||||
|
duplicate-aware FindElement); layout pass out of draw; single ChatVM; Vivid Target
|
||||||
|
Indicator onto the retail path; controller IDisposable standardization.
|
||||||
|
|
||||||
|
## What this is NOT
|
||||||
|
|
||||||
|
This is NOT an assessment of the reported **terrain regression** — none of the 70
|
||||||
|
copied files touch terrain/streaming/rendering-world code (the diff is confined to
|
||||||
|
UI, Core.Net messages, and Core item/player state), so that remains a separate
|
||||||
|
investigation on its own evidence. It is also NOT a retail-fidelity gap list — the
|
||||||
|
cursor EnumIDMap chain, equip-mask semantics, and useability gates reviewed here
|
||||||
|
are dat/decomp-faithful in approach; the issues are structural, not behavioral.
|
||||||
Loading…
Add table
Add a link
Reference in a new issue