docs(D.5.2): retire IA-16, add IA-18/AP-43..45, roadmap + memory

Divergence register:
- Retire IA-16 (item-icon composite PARTIAL — D.5.2 now complete).
- Add IA-18 (effect overlay = ReplaceColor tint SOURCE, faithful retail
  behavior; anti-regression guard — do NOT re-implement as a blit layer;
  cites IconData::RenderIcons 0x0058d180 + ReplaceColor 0x00441530).
- Add AP-43 (effect tint = mean-opaque color; exact retail byte
  decompiler-ambiguous, visual/cdb confirmation pending).
- Add AP-44 (effects==0 black-fallback recolor skipped; regression-risk
  avoidance, pending visual/cdb confirm).
- Add AP-45 (0x02CE sequence byte not honored, latest-wins).
Section header counts updated: IA 15→17, AP 41→44.

Roadmap: mark D.5.2 shipped (419c3ac..2f789da; appraise dropped as no-op;
effect recolor + live 0x02CE).

Tests: update ToolbarControllerTests iconIds lambda arity 4→5 to match the
D.5.2 GetIcon signature change (was caught by the build).

Memory: project_d2b_retail_ui.md updated with D.5.2 shipped entry
(via claude-memory symlink to ~/.claude/projects/.../memory/).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Erik 2026-06-17 18:52:15 +02:00
parent 2f789da73d
commit 73adc3768c
3 changed files with 19 additions and 16 deletions

View file

@ -37,7 +37,7 @@ accepted-divergence entries (#96, #49, #50).
---
## 1. Intentional architecture (IA) — 15 rows
## 1. Intentional architecture (IA) — 17 rows
| # | Divergence | Where (file:line) | Why it is safe / justified | Risk if assumption breaks | Retail oracle |
|---|---|---|---|---|---|
@ -56,8 +56,8 @@ accepted-divergence entries (#96, #49, #50).
| IA-13 | GameEventType registry deliberately omits event types retail ignores; unknown events fall through unhandled | `src/AcDream.Core.Net/Messages/GameEventType.cs:11` | Retail also ignores them — dropping matches retail by construction | If the "retail ignores X" judgment is wrong for any opcode (or a server mod uses one), the event is silently dropped with no diagnostic pointing at the omission | retail GameEvent dispatch (ignored-event set) |
| IA-14 | Rendering + dat-handling base is WorldBuilder's tested port, not a fresh retail-decomp port (Phase N.4/O design stance) | `docs/architecture/worldbuilder-inventory.md` (code at `src/AcDream.{Core,App}/Rendering/Wb/`) | WB visually verified on the AC world, MIT, same stack; known WB↔retail deltas resolved case-by-case — terrain split kept retail `FSplitNESW` (**#51**, pinned by `SplitFormulaDivergenceTest`), scenery drift accepted (AP-31) | A WB-upstream divergence not yet caught ships silently as "our" behavior; guard = the inventory doc's 🟢/🔴 split + per-formula divergence tests | retail decomp per algorithm; `tests/.../SplitFormulaDivergenceTest.cs` |
| IA-15 | D.2b retail UI is our own UiHost/UiElement retained-mode tree drawing dat-sprite window frames, not a byte-port of keystone.dll's LayoutDesc binary tree. Both the vitals window (`LayoutDesc 0x2100006C`) and the chat window (`LayoutDesc 0x21000006`) are rendered by the LayoutDesc importer; `UiNineSlicePanel`/`RetailChromeSprites` now back only plugin panels | `src/AcDream.App/UI/Layout/LayoutImporter.cs` (vitals + chat) + `src/AcDream.App/UI/Layout/ChatWindowController.cs` | keystone.dll has no PDB/decomp so a byte-port is impossible by definition; we mirror retail's ElementDesc field model + controls.ini tokens, and the chrome sprites ARE the real dat RenderSurfaces (Step-0 prove-out 2026-06-14 confirmed 0x06004CC2 center + 0x060074BF..C6 bevel). The 8-piece edge/corner→position mapping is DATA-DRIVEN from the dat: the `LayoutImporter` reads `LayoutDesc 0x2100006C`/`0x21000006` and resolves chrome element positions + sprite ids directly from parsed dat fields; vitals locked by the conformance fixture `tests/AcDream.App.Tests/UI/Layout/fixtures/vitals_2100006C.json` | Remaining residual risk: anchor resolution at non-800×600 and the controls.ini cascade still lack an oracle — layout scaling at non-reference resolution and stylesheet token inheritance differ silently | `LayoutDesc 0x2100006C`/`0x21000006` (SHIPPED); `docs/research/2026-06-15-layoutdesc-format.md`; controls.ini tokens; keystone.dll layout eval (no PDB) |
| IA-16 | Item-icon composite is PARTIAL — layers 1-4 (type-default underlay via EnumIDMap 0x10000004 + custom underlay/base/overlay) only; the effect-overlay layer (0x10000005 by _effects) and the overlay ReplaceColor tint are DEFERRED to D.5.2 | `src/AcDream.App/UI/IconComposer.cs` | Layers 1-4 cover the common item (opaque tile + base icon); the effect glow + overlay tint are state-overlays for charged/enchanted items. Full system handed off: docs/research/2026-06-17-stateful-icon-system-handoff.md (D.5.2) | An item whose distinctive look is the effect glow or the tinted overlay renders without it (the pinned-scroll "missing overlay" symptom). Retired when D.5.2 lands the effect layer + tint + appraise-driven re-composition | `IconData::RenderIcons` acclient_2013_pseudo_c.txt:407524 (layer 5 GetByEnum 0x10000005 @407575; ReplaceColor @407614) |
| IA-17 | Toolbar window FRAME is toolkit-supplied (per-window UiNineSlicePanel 8-piece bevel, drawn over content via UiElement.OnDrawAfterChildren) rather than the window-manager-owned chrome retail paints uniformly around every window | `src/AcDream.App/Rendering/GameWindow.cs` (toolbar mount) + `src/AcDream.App/UI/UiNineSlicePanel.cs` | LayoutDesc 0x21000016 has NO baked frame; retail's toolbar frame is window-manager chrome (keystone.dll). We draw the same reusable 8-piece bevel chat/vitals use; border drawn over content so the toolbar's 2px-wide row-2 right cap (W=8) can't poke through. Same pattern as the chat window. | Until a central window manager owns chrome uniformly, per-window wraps can drift (size/offset/z-order) from each other and from retail; the border-over-content rule is the toolkit's, not the WM's | gmToolbarUI WM chrome (keystone.dll, no PDB); no bevel ids in LayoutDesc 0x21000016 (toolbar dump) |
| IA-18 | Effect overlay tile (enum 0x10000005) is used as a `ReplaceColor` tint SOURCE — white pixels in the composited drag icon are replaced with the tile's representative color; the tile itself is NOT blitted as an additional layer. This IS faithful retail behavior (`IconData::RenderIcons` 0x0058d180 / `SurfaceWindow::ReplaceColor` 0x00441530). **Anti-regression: do NOT re-implement this as a separate blit layer.** | `src/AcDream.App/UI/IconComposer.cs` (`GetIcon`) | Faithful port of `IconData::RenderIcons` @407575 / `ReplaceColor` @407614; confirmed in `docs/research/2026-06-17-stateful-icon-RESOLVED.md`. Recorded here as a guard against a future dev "fixing" it back to a blit. | A blit-layer re-implementation would show the tile's colors additively over the icon instead of recoloring the base white highlights — wrong effect look, masked by the fact that the tile IS mostly the right color but composites differently | `IconData::RenderIcons` acclient_2013_pseudo_c.txt:407524; `ReplaceColor` @407614 (fixed src=white); `docs/research/2026-06-17-stateful-icon-RESOLVED.md` |
---
@ -96,7 +96,7 @@ accepted-divergence entries (#96, #49, #50).
---
## 3. Documented approximation (AP) — 41 rows
## 3. Documented approximation (AP) — 44 rows
| # | Divergence | Where (file:line) | Why it is safe / justified | Risk if assumption breaks | Retail oracle |
|---|---|---|---|---|---|
@ -142,6 +142,9 @@ accepted-divergence entries (#96, #49, #50).
| AP-40 | Single default translucency for the chat window chrome; no focused/unfocused opacity transition; dat font face/size taken from the vitals `vitalsDatFont` (same dat font, not a chat-specific size lookup) | `src/AcDream.App/Rendering/GameWindow.cs` (chatController binding line) | Retail fades the chat window to ~80% alpha when unfocused (`gmMainChatUI::UpdateAlpha @0x4cdea0`); the opacity animation deferred to the Plan-2 window-manager input integration; sharing `vitalsDatFont` is safe — retail uses the same AC-default font for both | The chat window is always fully opaque/same-font rather than subtly fading when idle; no wrong text, but the focused/unfocused breathing rhythm is absent | `gmMainChatUI::UpdateAlpha` @0x4cdea0; `UCF::SetAceFont @0x4d3940` |
| AP-41 | Scrollbar thumb 3-slice cap fallback only: single-tile draw (`0x06004C63`) used only when `ThumbTopSprite`/`ThumbBotSprite` are unset; the chat controller passes all three cap ids so the 3-slice path is drawn in practice | `src/AcDream.App/UI/UiScrollbar.cs:35` | The fallback single-tile path is unreachable when caps are bound (chat controller always sets them); the 3-slice path is the active code path | Only if a future caller omits the cap ids will the fallback fire — no visual regression in the chat window | `UIElement_Scrollbar::UpdateLayout @0x4710d0`; cap sprites `0x06004C60` (top) + `0x06004C66` (bottom) from base layout `0x2100003E` |
| AP-42 | `UiMenu` item model is flat (label + opaque payload, single-level popup); retail `UIElement_Menu::MakePopup @0x46d310` supports hierarchical nested submenus via recursive popup chain | `src/AcDream.App/UI/UiMenu.cs` | The chat talk-focus menu is single-level (14 rows, 2 columns, no submenu); hierarchy is latent and unreachable through the chat window — no behavioral difference in the current usage | A future menu with nested submenus would render flat (only the top-level items drawn, no drill-down) | `UIElement_Menu::MakePopup` @0x46d310 |
| AP-43 | Effect tint color = the effect tile's mean-opaque RGB (average of non-transparent pixels); the exact retail color is an `effectTile + 0xac` pointer reinterpreted as `RGBAColor` — decompiler-ambiguous in the BN pseudo-C (field offset vs pointer). Visual/cdb confirmation pending (D.5.2 lever: DR-2) | `src/AcDream.App/UI/IconComposer.cs` (`TryGetEffectColor`) | The tile IS the per-effect coloring authority (Magical=blue, Poisoned=green, …); its mean-opaque color is the representative color. The ambiguity only affects hue saturation slightly. | Effect tints could be subtly wrong vs retail (too washed-out or oversaturated) if the header field is a precomputed key color rather than the pixel mean — visible on items with distinctive effect glows | `IconData::RenderIcons` 0x0058d180 (`effectTile+0xac` usage); `docs/research/2026-06-17-stateful-icon-RESOLVED.md` |
| AP-44 | The `effects==0` black-fallback recolor retail nominally runs (white→black on every item when no effect bit is set) is SKIPPED; we gate on `effects != 0` | `src/AcDream.App/UI/IconComposer.cs` (`GetIcon` `effects != 0` gate) | The white→black recolor on a no-effect item is presumed a no-op in practice (items without magic are unlikely to have white-opaque pixels in the base icon); skipping avoids a regression risk pending visual/cdb confirmation (DR-3). Filed as a known deviation — not an oversight. | If retail does darken non-effect items' white highlights, those highlights will appear brighter than retail until a cdb session confirms or refutes | `IconData::RenderIcons` 0x0058d180 (fallback-0x21 path leads to ReplaceColor with a near-black tile) |
| AP-45 | `PublicUpdatePropertyInt (0x02CE)` sequence byte parsed-past but not honored; last update wins (no freshness check against sequence number) | `src/AcDream.Core.Net/Messages/PublicUpdatePropertyInt.cs` | Loopback ACE rarely reorders; latest-wins matches `PrivateUpdateVital`/`UpdatePosition`'s existing non-sequence behavior. Sequence tracking added when needed alongside TS-26. | A reordered 0x02CE on a real network could apply a stale UiEffects value — item icon temporarily shows the wrong effect state, corrected on next update | `PublicUpdatePropertyInt` sequence byte (ACE GameMessagePublicUpdatePropertyInt) |
---

View file

@ -432,7 +432,7 @@ behavior. Estimated 1726 days focused work, 35 weeks calendar.
- **✓ SHIPPED — D.4 — Dat sprites + 9-slice panel backgrounds.** Shipped 2026-06-14 with D.2b. `TextureCache.GetOrUploadRenderSurface(id, out w, out h)` decodes `RenderSurface` (`0x06xxxxxx`) **directly** (not via the Surface→SurfaceTexture chain — the prove-out finding) → GL `Texture2D`; `DrawSprite` (explicit UV-rect, per-texture batch) added to `TextRenderer` + `UiRenderContext` + a `uUseTexture=2` RGBA frag branch; `UiNineSlicePanel` composes the universal 8-piece bevel (corners `0x060074C3..C6`, edges `0x060074BF/C0/C1/C2`, center `0x06004CC2`). Remaining art polish: the glassy gradient bar fill sprite (D.2b polish).
- **D.5 — Core panels.** Attributes (`chunk_00470000.c:FUN_0047ba70`), Skills (same), Paperdoll (`chunk_004A0000.c:FUN_004A5200`), Inventory, Spellbook (`chunk_004C0000.c`), Fellowship, Allegiance. Each uses the port sketches in slice 05. **(Targets `AcDream.UI.Abstractions` — ships with D.2a using ImGui-rendered widgets; reskinned by D.2b.)** The *chat* panel originally listed under D.5 shipped early in Phase I (I.4 input + I.7 combat translator superseded the chat-panel design here); this entry now covers Attributes / Skills / Paperdoll / Inventory / Spellbook / Fellowship / Allegiance only.
- **✓ SHIPPED — D.5.1 — Toolbar (action bar).** Shipped 2026-06-16/17 (`30b28c2``0e7a083`, branch claude/hopeful-maxwell-214a12). First data-driven *game* panel: `gmToolbarUI` (`LayoutDesc 0x21000016`) — 18 shortcut slots from the persisted `PlayerDescription` SHORTCUT block, real **composited** item icons (opaque type-default underlay via the `EnumIDMap 0x10000004` resolve), **occupancy-gated slot numbers 19** (occupied = dark-box peace/war `0x10000042/43`; empty = background `0x1000005e` from cell composite `0x10000341`), **click-to-use** (`ItemHolder::UseObject``0x0036`), **peace/war stance** indicator live-wired to `CombatState`, **movable**, and a **chrome frame** (UiNineSlicePanel drawn over content via the new `UiElement.OnDrawAfterChildren` hook). New shared widgets `UiItemSlot` (`UIElement_UIItem` 0x10000032, procedural leaf) + `UiItemList` (`UIElement_ItemList` 0x10000031, factory branch) + `IconComposer` (CPU layered composite). `CreateObject.TryParse` extended to the full ACE-order weenie-header tail to capture `IconId`/`IconOverlay`/`IconUnderlay``ItemRepository.EnrichItem` → re-render. Spec/plan `docs/superpowers/{specs,plans}/2026-06-16-d2b-toolbar-phase1*.md`; research drop `docs/research/2026-06-16-*deep-dive.md` + synthesis. Divergence IA-16/IA-17 added. **User-confirmed** (numbers, icons, frame). Per-task spec+code-review throughout.
- **D.5.2 — Stateful item-icon system [NEXT — handoff ready].** The full retail icon composite (`IconData::RenderIcons` @407524, 5 layers). D.5.1 built layers 14 (type-default underlay + custom underlay/base/overlay) + the `CreateObject` parse. **Remaining:** the effect layer (`_effects``GetByEnum 0x10000005`, the "item with mana vs out-of-mana" state), the overlay `ReplaceColor` tint, and **appraise-driven enrichment + icon re-composition** (overlay/effects likely arrive at Appraise `0x00C9`, not the bare `CreateObject` — capture with WireMCP first). Shared by inventory/equipment/vendor/trade — do before those panels. **Handoff: [`docs/research/2026-06-17-stateful-icon-system-handoff.md`](../research/2026-06-17-stateful-icon-system-handoff.md).**
- **✓ SHIPPED — D.5.2 — Stateful item-icon system.** Shipped 2026-06-17 (`419c3ac`..`2f789da`, branch claude/hopeful-maxwell-214a12). Full retail icon composite (`IconData::RenderIcons` @407524, 5 layers + recolor): (1) `UiEffects` bitfield captured from `CreateObject` weenie header (was discarded) → `ItemInstance.Effects`; (2) `IconComposer.GetIcon` rewritten as a 2-stage composite — Stage 1 = drag icon (base + overlay) + effect `ReplaceColor` tint (tile from `EnumIDMap 0x10000005` submap, mean-opaque color → white pixels recolored), Stage 2 = underlay + drag; (3) `PublicUpdatePropertyInt (0x02CE)` parser + `WorldSession.ObjectIntPropertyUpdated` event + `GameWindow` subscription → `ItemRepository.UpdateIntProperty` → icon re-composites live. **Appraise (`0x00C9`) carries NO icon data** (WireMCP confirmed, no overlay/effects in appraise payload) — dropped from scope as a no-op. Retire IA-16 (partial composite); add IA-18 (ReplaceColor anti-regression), AP-43 (effect mean-color), AP-44 (effects==0 recolor skipped), AP-45 (0x02CE sequence not honored). Spec: `docs/superpowers/specs/2026-06-17-d2b-stateful-icon-design.md`; plan: `docs/superpowers/plans/2026-06-17-d2b-stateful-icon.md`; research: `docs/research/2026-06-17-stateful-icon-RESOLVED.md`.
- **D.5.3 — Toolbar interactivity [NEXT].** The toolbar is the **selected-object display**: wire the B.4 `WorldPicker`/selection state → the two hidden meters (`0x100001A1` health / `0x100001A2` mana) + the stack slider (`0x100001A4`) + the object-name line, so the bar shows what the player has selected in the world. (Click-to-use + the peace/war stance indicator already landed in D.5.1.)
- **D.5.4+ — remaining core panels.** Inventory (`gmInventoryUI`/`gmBackpackUI`), equipment/paperdoll (`gmPaperDollUI`/`gm3DItemsUI` + the `UiViewport` 3D doll), spellbook, etc. — research drop done (`docs/research/2026-06-16-*`); depends on D.5.2 (the stateful icon) + the item-slot/list spine (shipped in D.5.1) + the window manager. Deferred from D.5.1: drag/reorder, the AddShortcut/RemoveShortcut mutate wire, spell shortcuts, the faithful grip/dragbar window manager.
- **D.6 — HUD.** Vital orbs (scissor-rect partial fill, dat sprites `0x060013B2`), radar (`0x06001388` / `0x06004CC1`, 1.18× range factor), compass strip (scrolling U), target name plate, damage floaters, selection indicator. See slice 06. **(Targets `AcDream.UI.Abstractions` — ships with D.2a; reskinned by D.2b.)** Phase I.2 retired the StbTrueTypeSharp `DebugOverlay` but kept `TextRenderer` + `BitmapFont` alive specifically for D.6's world-space HUD elements (damage floaters, name plates) — they need raw GL text drawing that ImGui can't reach into the 3D scene.