diff --git a/docs/ISSUES.md b/docs/ISSUES.md index f1a36036..62a9ca7a 100644 --- a/docs/ISSUES.md +++ b/docs/ISSUES.md @@ -6905,19 +6905,21 @@ The burden bar now reads the server's wire `EncumbranceVal` (PropertyInt 5) inst ## Inventory + equipment slots show the wrong empty-slot background art -**Status:** INVENTORY PORTION FIXED + VISUALLY CONFIRMED 2026-06-22 — contents grid + side-bag + main-pack empty art dat-resolved (`ItemListCellTemplate`, commits `b862640`→`22d9231`; user-confirmed the container column reads correct). STILL OPEN: paperdoll equip-slot silhouettes (Sub-phase C) + main-pack backpack icon (AP-51) + the selected-container triangle/selection-square (container-switching sub-phase, AP-56). +**Status:** IMPLEMENTATION COMPLETE 2026-07-13; PAPERDOLL LIVE VISUAL GATE PENDING. Inventory contents/containers were visually confirmed 2026-06-22. The paperdoll now resolves all 21 supported location-specific backgrounds from the authored UIItem catalog instead of painting the generic inventory frame. The main-pack icon and selected-container indicators were completed in their later sub-phases. **Filed:** 2026-06-21 **Component:** ui — D.2b inventory (UiItemSlot empty sprite + paperdoll equip slots) **Description:** User-flagged at the Stage-1 visual gate: the empty cells in the contents grid + side-bag column use `UiItemSlot.EmptySprite = 0x060074CF` (the TOOLBAR empty-slot border) — the wrong art for inventory pack slots. The paperdoll equipment slots render a generic blue `UiDatElement` border, not per-slot equip silhouettes. Both look wrong vs retail. -**Root cause / status:** `UiItemSlot.EmptySprite` is a single hardcoded default (`0x060074CF`) shared by all empty cells. The inventory/side-bag pack slots need the gm3DItemsUI/gmBackpackUI cell-template (`0x21000037`) empty-state background instead. The equip slots need the `0x10000032` `UIElement_UIItem` registration + per-slot silhouette art (Sub-phase C). +**Root cause / status:** `UiItemSlot.EmptySprite` began as one shared hardcoded default. Inventory lists were corrected first by resolving their list-selected prototype. Paperdoll retained the contents-grid fallback because the earlier investigation inspected the ItemList elements' own media and missed retail's cell-creation step: each paperdoll list clones a distinct `UIElement_UIItem` prototype from catalog `0x21000037`. **Resolution (inventory portion, 2026-06-22):** Ported retail's `UIElement_ItemList::InternalCreateItem` (`0x004e3570`) resolver into `ItemListCellTemplate.ResolveEmptySprite` — reads attribute `0x1000000e` off each list element → catalog `0x21000037` prototype's `ItemSlot_Empty`. **Correction to the original diagnosis:** `0x060074CF` is the *generic shared* item-slot empty (the `ItemSlot_Empty` of many catalog prototypes), not "the toolbar's"; `0x21000037` is a catalog of ~50 per-slot-kind empties — so it was never a one-constant swap. Pinned per-list (real-dat test): contents `0x100001C6` → `0x06004D20`; side-bag `0x100001CA` + main-pack `0x100001C9` → `0x06000F6E` (the inner dark slot, resolved through `BaseElement` inheritance). **Visual-gate correction (2026-06-22):** the first cut's frame-first heuristic grabbed the container prototype's DirectState child `0x06005D9C` — which is the open/**selected**-container TRIANGLE indicator, not a background — and stamped it onto every empty container cell. Fixed: resolve the inner `ItemSlot_Empty` through inheritance (`FindIconEmpty`); the triangle + green/yellow selection square is deferred to the container-switching sub-phase (AP-56). `UiItemList.CellEmptySprite` carries it; `InventoryController` + `GameWindow` wire it. Divergence AP-55 (toolbar still hardcoded) + AP-56 (selected-container indicator deferred). Spec/plan `docs/superpowers/{specs,plans}/2026-06-22-d2b-empty-slot-art*.md`. -**Files:** `src/AcDream.App/UI/UiItemSlot.cs` (`EmptySprite`); `src/AcDream.App/UI/Layout/InventoryController.cs` (set the inventory pack-slot empty sprite on the cells it creates); paperdoll equip slots → Sub-phase C. +**Resolution (paperdoll portion, 2026-07-13):** Re-read named retail `gmPaperDollUI::GetLocationInfoFromElementID @ 0x004A37F0`, `PostInit @ 0x004A5360`, and `UIElement_ItemList::InternalCreateItem @ 0x004E3570`, then enumerated the live `0x21000037` catalog. `PaperdollSlotBackgrounds` is now the single element/location/prototype definition table; runtime and UI Studio resolve the exact 21 authored surfaces through the existing inheritance-aware `FindIconEmpty` path. Real-DAT tests pin every prototype and RenderSurface. AP-66 retired. Research/pseudocode: `docs/research/2026-07-13-retail-paperdoll-slot-backgrounds-pseudocode.md`. -**Research:** `.layout-dumps/uiitem-0x21000037.txt` (cell template states); `docs/research/2026-06-16-equipment-paperdoll-deep-dive.md`. Also pending: the main-pack backpack icon (AP-51). +**Files:** `src/AcDream.App/UI/UiItemSlot.cs`; `src/AcDream.App/UI/Layout/ItemListCellTemplate.cs`; `InventoryController.cs`; `PaperdollSlotBackgrounds.cs`; `PaperdollController.cs`. + +**Research:** `.layout-dumps/uiitem-0x21000037.txt`; `docs/research/2026-06-16-equipment-paperdoll-deep-dive.md`; `docs/research/2026-07-13-retail-paperdoll-slot-backgrounds-pseudocode.md`. **Acceptance:** empty inventory + side-bag cells show the retail pack-slot frame; equip slots show per-slot silhouettes. diff --git a/docs/architecture/retail-divergence-register.md b/docs/architecture/retail-divergence-register.md index 390d7ef5..59863544 100644 --- a/docs/architecture/retail-divergence-register.md +++ b/docs/architecture/retail-divergence-register.md @@ -171,7 +171,7 @@ AP-94..AP-112 for the confirmed retail-UI completion gaps. | AP-63 | **Dual-wield-into-shield-slot special not implemented** — retail `OnItemListDragOver` (decomp 174302) lets a melee-capable item also drop on the Shield slot; acdream's `wieldMask = ValidLocations & slotMask` rejects it. | `src/AcDream.App/UI/Layout/PaperdollController.cs` (`HandleDropRelease`) | A dual-wielder cannot off-hand a melee weapon via the doll. | Dual-wield via drag-onto-shield-slot is blocked — functional gap for dual-wield characters. | `gmPaperDollUI::OnItemListDragOver` decomp 174302 | | AP-64 | **Wield-reject rollback assumes `InventoryServerSaveFailed 0x00A0`** — an optimistic wield rolls back only if ACE emits `0x00A0` for a `GetAndWieldItem` rejection; otherwise corrected by the next authoritative message. Gate-verify via WireMCP. | `src/AcDream.Core.Net/GameEventWiring.cs` (0x00A0 handler) | If ACE uses a different reject opcode for wield, the optimistic state is left dangling until the next full update. | Rejected wield briefly shows the item as equipped in the doll — cosmetic flicker or stuck state if `0x00A0` is not the rejection path. | `InventoryServerSaveFailed 0x00A0`; WireMCP gate-verify | | AP-65 | **PickupEvent (0xF74A) no longer evicts the weenie from `ClientObjectTable`** — only `DeleteObject (0xF747)` evicts, matching the retail `object_table`-vs-`weenie_object_table` split. An item another player picks up near you (only ever gets `PickupEvent`, never `DeleteObject`) lingers as a data-only entry (`ContainerId 0`, not in any view) until teleport/relog `Clear()`. | `src/AcDream.Core.Net/ObjectTableWiring.cs` (EntityDeleted handler); `src/AcDream.Core.Net/WorldSession.cs` (PickupEvent branch) | The weenie entry for nearby pickups is a harmless data ghost — no UI shows it (no container, not wielded). Retail evicts it from `weenie_object_table` when the object fully leaves interest range via `DeleteObject`; acdream defers to teleport/relog clear. | Slight memory growth in long sessions if many world items are picked up by other players near you; item data ghosts cannot cause functional issues because no UI queries `ContainerId 0`. | `CACObjectMaint::DeleteObject` / `SmartBox::HandleDeleteObject`; ACE `Player_Inventory.cs TryDequipObjectWithNetworking` | -| AP-66 | Paperdoll doll-view and Slots toggle are present, but non-armor slots remain always visible and empty armor cells in slot-view still paint generic inventory frame `0x06004D20` instead of the retail DAT/state visibility and empty presentation | `src/AcDream.App/UI/Layout/PaperdollController.cs` | The live doll and two-view structure are correct; remaining empty/visibility policy is isolated to controller presentation | Empty/locked positions remain visibly framed or visible in states where retail hides them | `gmPaperDollUI::PostInit @ 0x004A5360`; live retail screenshots | +| ~~AP-66~~ | **RETIRED 2026-07-13 — authored paperdoll empty-slot presentation.** The earlier “no silhouettes” conclusion inspected the ItemList elements' own media but missed `UIElement_ItemList::InternalCreateItem`, which clones a distinct `UIElement_UIItem` catalog prototype for each location. All 21 supported jewelry, weapon, ammo, shield, clothing, cloak, trinket, and armor lists now resolve their exact `ItemSlot_Empty` surface from live DAT; `PostInit` confirms non-armor lists remain visible while the nine armor lists toggle with Slots. | `src/AcDream.App/UI/Layout/PaperdollSlotBackgrounds.cs`; `ItemListCellTemplate.cs`; `PaperdollController.cs` | — | — | `gmPaperDollUI::GetLocationInfoFromElementID @ 0x004A37F0`; `PostInit @ 0x004A5360`; `UIElement_ItemList::InternalCreateItem @ 0x004E3570`; `LayoutDesc 0x21000037` | | AP-67 | Server-spawned weenie fixtures (lanterns, braziers, glowing items carrying `Setup.Lights`) register their lights at their SPAWN-frame world position via `RegisterOwnedLight` in `OnLiveEntitySpawnedLocked` (mirroring the dat-static path in `ApplyLoadedTerrainLocked`); the light does NOT follow the object if it later moves. Torn down on despawn/respawn by the now-unconditional `UnregisterOwner` in `RemoveLiveEntityByServerGuid`. | `src/AcDream.App/Rendering/GameWindow.cs` (`OnLiveEntitySpawnedLocked` weenie-light block; `RemoveLiveEntityByServerGuid`) | Closes the prior gap where ONLY dat-baked statics registered lights, so server-placed lanterns cast nothing; the overwhelming majority of light-bearing weenies (wall lanterns, braziers, candelabra) are stationary, so spawn-frame placement matches retail; retail's `add_dynamic_light` re-places the light from the object frame each frame | A moving light-bearing weenie (e.g. a torch-carrying NPC) leaves its light at the spawn position instead of following — rare; the faithful fix is to re-place on `OnLivePositionUpdated` | retail object-borne lights `insert_light` 0x0054d1b0 / `add_dynamic_light` 0x0054d420 (per-frame object frame) | | AP-68 | acdream keeps the 128 nearest-to-CAMERA point lights live (`MaxGlobalLights=128`, `BuildPointLightSnapshot`) and selects per cell CAMERA-INDEPENDENTLY (by the cell's own bounds), so a building interior stays lit at any distance within a town; retail keeps only the 40 nearest-to-PLAYER static lights (`Render::max_static_lights=0x28`, distance-sorted replace-farthest `insert_light`) and re-bakes a cell when its live light set changes, so distant interiors are baked dark and "light up" only as the player approaches and their torches enter the live 40. INTENTIONAL — acdream's always-lit interiors are the preferred behavior (no 1999-era light-budget pop-in); user-confirmed 2026-06-20. | `src/AcDream.Core/Lighting/LightManager.cs` (`MaxGlobalLights=128`, `BuildPointLightSnapshot`, `SelectForObject`) | The retail pop-in is a fixed-function light-budget artifact, not an intended aesthetic; revert to retail by clamping the global set to 40 + distance-to-player sort if ever desired | Distant town interiors are lit in acdream where retail's are dark until approached — a deliberate, user-preferred divergence | `Render::max_static_lights` 0x28; `insert_light` 0x0054d1b0 (distance-sorted, replace-farthest); bake re-trigger `SetStaticLightingVertexColors` cache `burnedInStaticLights != num_static_lights` | | AP-69 | On a landblock (re)load, acdream re-projects its retained server-object spawns (`GameWindow._lastSpawnByGuid` — our `weenie_object_table` for world objects, carrying position + Setup + appearance) into the render world via `LandblockEntityRehydrator`. The dungeon collapse (#133/#135) and Near→Far demote drop a landblock's RENDER entities for FPS but keep the parsed spawns; ACE will NOT re-broadcast objects whose guid is still in its per-player `KnownObjects` set (never cleared on a normal teleport — ACE relies on the client retaining its table + doing its own visibility cull). Re-projecting from our own table is the retail "client keeps its weenie_object_table and re-renders from it" model. DIVERGENCE: acdream has NO retail 25 s / 384 m visibility cull — the retained spawn table is pruned ONLY by an explicit server `DeleteObject` (0xF747) or a spawn de-dup, never by distance/time. (#138) | `src/AcDream.App/Streaming/LandblockEntityRehydrator.cs` + `GameWindow.RehydrateServerEntitiesForLandblock` + `StreamingController` (`onLandblockLoaded`, Loaded + Promoted) | Re-projection from our own table is retail-faithful AND the only reliable path (ACE won't re-send a known object); fixes the #138 "doors/NPCs/portals gone after a dungeon exit" symptom independent of any server re-broadcast | An object the server silently stopped tracking WITHOUT a `DeleteObject` (e.g. a creature that wandered out of PVS) is re-hydrated at its last-known position on reload, where retail's 25 s cull would have dropped it — a stale ghost until a real `DeleteObject` or the next session. Close it by porting holtburger's 25 s/384 m `ACE_DESTRUCTION_TIMEOUT` self-cull | ACE `ObjectMaint.KnownObjects` never cleared on teleport (`Physics/Common/ObjectMaint.cs`, `WorldObjects/Player_Tracking.cs`); holtburger client 25 s cull `liveness.rs` `ACE_DESTRUCTION_TIMEOUT_SECS`; retail client weenie_object_table re-render | diff --git a/docs/plans/2026-04-11-roadmap.md b/docs/plans/2026-04-11-roadmap.md index 0ba23242..0a54c20f 100644 --- a/docs/plans/2026-04-11-roadmap.md +++ b/docs/plans/2026-04-11-roadmap.md @@ -481,7 +481,7 @@ behavior. Estimated 17–26 days focused work, 3–5 weeks calendar. - **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 1–9** (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. - **✓ SHIPPED — D.5.2 — Stateful item-icon system.** Shipped 2026-06-17/18 (`419c3ac`..`fb288ad`, branch claude/hopeful-maxwell-214a12; **visually verified on a live Coldeve server**). Faithful retail icon composite (`IconData::RenderIcons` @0x0058d180): (1) `UiEffects` bitfield captured from the `CreateObject` weenie header (was discarded) → `ItemInstance.Effects`; (2) `IconComposer.GetIcon` rewritten as a 2-stage composite — Stage 1 = drag icon (base + custom overlay) + the effect treatment, Stage 2 = type-default underlay + custom underlay + drag. The effect treatment ports the **surface overload** of `SurfaceWindow::ReplaceColor` (`0x004415b0`): the textured effect tile (`EnumIDMap 0x10000005` by `LowestSetBit(effects)+1`, fallback `0x21` solid-black) is copied **per-pixel** into the icon's pure-white pixels — magical items take the tile's GRADIENT hue, mundane items go black; (3) `PublicUpdatePropertyInt (0x02CE)` parser + `WorldSession.ObjectIntPropertyUpdated` event + `GameWindow` subscription → `ItemRepository.UpdateIntProperty` → icon re-composites live. **Appraise (`0x00C9`) carries NO icon data** (ACE proof: `Icon`/`IconOverlay`/`IconUnderlay`/`UiEffects` all lack `[AssessmentProperty]`) — dropped as a no-op. **Two visual-verification fixes landed after the subagent build:** the `effects==0` recolor MUST run (mundane white edges → black, `40c97a5`) and the tint is a per-pixel GRADIENT not a flat color (the surface overload, `fb288ad`) — both confirmed via clean Ghidra + named decomp. Divergence: IA-16 retired; IA-18 (per-pixel surface-copy anti-regression) + AP-45 (0x02CE sequence) added; **AP-43/AP-44 retired by the visual fixes**. Spec/plan/research: `docs/superpowers/{specs,plans}/2026-06-17-d2b-stateful-icon*.md`, `docs/research/2026-06-17-stateful-icon-RESOLVED.md`. -- **D.5 remaining — sub-phase ledger.** D.5.1 (toolbar + the `UiItemSlot`/`UiItemList`/`IconComposer` spine) ✅, D.5.2 (stateful icons) ✅, D.5.4 (client object/item data model) ✅, D.2b-B window manager (`abbd97b`) ✅, D.2b-B B-Grid (inventory sub-window mount + `UiItemList` grid) ✅, D.2b-B B-Controller (inventory population + burden meter + captions, `03fbf44`; **visually confirmed 2026-06-21** — two render bugs fixed at the gate `417b137`: backdrop wash-out [a #145 continuation — mounted sub-window slots must keep their own frame ZLevel, not inherit the base root's 1000] + captions [drive the host UiText directly]) ✅, D.2b-B B-Wire (`EncumbranceVal`/PropertyInt 5 wire delivery, AP-48/49 retired) ✅, inventory window finish Stage 1 (scroll/frame/vertical-resize/102-slot grid) ✅, empty-slot art ✅, container-switching + open/selected indicators + main-pack icon + per-bag capacity bar ✅, B-Drag (inventory drag-drop / item moving, including retail select-before-waiting direct-drag order; **visually confirmed 2026-07-13**, `609ed8bf`) ✅, **Sub-phase C (paperdoll): Slice 1 equip slots ✅ + Slice 2 3-D doll `UiViewport` (Type 0xD via the `IUiViewportRenderer` RTT seam) + Slots toggle ✅ (visually confirmed 2026-06-25, `8fa66c2` — pose/camera/heading all retail-verbatim)** ✅, **UI Studio** (`AcDream.App ui-studio`) ✅, **importer dat-fidelity (Fix A/B/C)** ✅, **Character window** (`LayoutDesc 0x2100002E`, `CharacterStatController`) ✅, **Wave 3.1 unified selection** (Core `SelectionState`; AP-58 retired) ✅, and **Wave 3.2 pure item-use/drop policy** (`ItemInteractionPolicy`; AP-99 retired) ✅ are shipped. Build order from here: **finish Wave 3 activation router/cursors, then D.5.3 selected-object mana + stack entry/slider and the separate favorite-spell bars; ISSUES #146/#147/#197.** Each ☐ below gets its own focused oracle/spec/plan/gate. +- **D.5 remaining — sub-phase ledger.** D.5.1 (toolbar + the `UiItemSlot`/`UiItemList`/`IconComposer` spine) ✅, D.5.2 (stateful icons) ✅, D.5.4 (client object/item data model) ✅, D.2b-B window manager (`abbd97b`) ✅, D.2b-B B-Grid (inventory sub-window mount + `UiItemList` grid) ✅, D.2b-B B-Controller (inventory population + burden meter + captions, `03fbf44`; **visually confirmed 2026-06-21** — two render bugs fixed at the gate `417b137`: backdrop wash-out [a #145 continuation — mounted sub-window slots must keep their own frame ZLevel, not inherit the base root's 1000] + captions [drive the host UiText directly]) ✅, D.2b-B B-Wire (`EncumbranceVal`/PropertyInt 5 wire delivery, AP-48/49 retired) ✅, inventory window finish Stage 1 (scroll/frame/vertical-resize/102-slot grid) ✅, inventory empty-slot art ✅, container-switching + open/selected indicators + main-pack icon + per-bag capacity bar ✅, B-Drag (inventory drag-drop / item moving, including retail select-before-waiting direct-drag order; **visually confirmed 2026-07-13**, `609ed8bf`) ✅, **Sub-phase C (paperdoll): Slice 1 equip slots ✅ + Slice 2 3-D doll `UiViewport` (Type 0xD via the `IUiViewportRenderer` RTT seam) + Slots toggle ✅ (visually confirmed 2026-06-25, `8fa66c2` — pose/camera/heading all retail-verbatim) + exact per-location empty backgrounds implemented 2026-07-13 (live gate pending; AP-66 retired)** ✅, **UI Studio** (`AcDream.App ui-studio`) ✅, **importer dat-fidelity (Fix A/B/C)** ✅, **Character window** (`LayoutDesc 0x2100002E`, `CharacterStatController`) ✅, **Wave 3.1 unified selection** (Core `SelectionState`; AP-58 retired) ✅, and **Wave 3.2 pure item-use/drop policy** (`ItemInteractionPolicy`; AP-99 retired) ✅ are shipped. Build order from here: **finish Wave 3 activation router/cursors, then D.5.3 selected-object mana + stack entry/slider and the separate favorite-spell bars; ISSUES #146/#147/#197.** Each ☐ below gets its own focused oracle/spec/plan/gate. - **✓ SHIPPED — UI Studio** (2026-06-26, branch `claude/hopeful-maxwell-214a12`, ~33693c6→HEAD). Standalone `AcDream.App ui-studio [--layout 0xNNNN | --dump ] [--screenshot ]` Silk tool that previews any panel through the **production renderer** (`RenderBootstrap.cs` + GameWindow untouched). Sources: 26-window retail dump (`docs/research/2026-06-25-retail-ui-layout-dump.json`) via `--dump`, or live dat import + fixtures via `--layout`. Interactive canvas (click-routing to `UiHost`, Interact/Inspect ImGui toggle), headless `--screenshot`. Architecture: `src/AcDream.App/Studio/` (`StudioWindow`, `PanelFbo`, `FixtureProvider`, `LayoutSource`) + `src/AcDream.App/Rendering/RenderBootstrap.cs`. ISSUES #156 (inventory all-black in studio) and #157 (GameWindow font resolver) filed. - **✓ SHIPPED — Importer dat-fidelity** (Fix A/B/C, 2026-06-26, same branch). **Boundary established: look = importer (font/justification/color from dat); state/behavior = runtime controller logic.** Fix A: justification property `0x14`/`0x15` → `UiText` Centered/RightAligned/VerticalJustify. Fix B: FontColor property `0x1B` → `UiText.DefaultColor`; character colors proved RUNTIME (no importer fix). Fix C: FontDid → per-element dat font via a resolver in `DatWidgetFactory` — **STUDIO path only**; GameWindow passes `null` (Issue #157). Fix 4 (UIState-group activation): INVESTIGATED — no importer fix exists; the dat has no Visible encoding; runtime `gm*UI` is correct. - **✓ SHIPPED — Wave 3.3 unified primary-click router.** `ItemInteractionController.OfferPrimaryClick` returns `NotActive` / `ConsumedSuccess` / `ConsumedRejected`; inventory, bags, main pack, paperdoll, toolbar, radar, and world clicks all offer target mode before local fallback. Rejected targets cannot drift selection or open/use the clicked object. Issue #197 and AP-107 retired; retained matrix and live ACE-provisioned healing gate user-confirmed. The 3-D doll routes through authored hit mask `0x100001D6` (resolved `UiButton` above viewport `0x100001D5`), and local health prefers authoritative private-vital state. diff --git a/docs/research/2026-07-13-retail-paperdoll-slot-backgrounds-pseudocode.md b/docs/research/2026-07-13-retail-paperdoll-slot-backgrounds-pseudocode.md new file mode 100644 index 00000000..756776bd --- /dev/null +++ b/docs/research/2026-07-13-retail-paperdoll-slot-backgrounds-pseudocode.md @@ -0,0 +1,100 @@ +# Retail paperdoll slot backgrounds — pseudocode + +Date: 2026-07-13 + +Scope: the empty-cell art for the 21 currently supported equipment lists in +`gmPaperDollUI`. This corrects the earlier visual inference that the slots had no +per-location art. The paperdoll layout elements themselves inherit the same ItemList +base, but retail creates their cells from distinct `UIElement_UIItem` prototypes in +the shared catalog `0x21000037`. + +## Retail anchors + +- `gmPaperDollUI::GetLocationInfoFromElementID` @ `0x004A37F0` + (`named-retail/acclient_2013_pseudo_c.txt:173620`) — exact paperdoll element-id to + `INVENTORY_LOC`/`EquipMask` mapping. +- `gmPaperDollUI::PostInit` @ `0x004A5360` + (`named-retail/acclient_2013_pseudo_c.txt:175232`) — binds each element as a + single-cell `UIElement_ItemList`; hides only the nine armor lists in doll view. +- `UIElement_ItemList::InternalCreateItem` @ `0x004E3570` + (`named-retail/acclient_2013_pseudo_c.txt:231486`) — resolves the UIItem catalog + through enum-map `0x10000038`, category `5`, key `0x23`, then clones the selected + `UIElement_UIItem` prototype. +- `UIElement_ItemList::ItemList_AddItem` @ `0x004E3F50` and + `gmPaperDollUI::SetUIItemIntoLocation` @ `0x004A5F90` — even an empty location is + represented by a real UIItem cell in its list. +- Live DAT `LayoutDesc 0x21000037` — each prototype's child `0x1000033B`, state + `ItemSlot_Empty`, supplies the exact 32×32 RenderSurface below. + +The retained `references/WorldBuilder` tree has no UI implementation. ACE/ACViewer +are interpretation aids for `EquipMask` and surface decoding; neither owns the retail +paperdoll widget behavior. The named client plus live DAT are therefore the oracle. + +## Literal pseudocode + +```text +PaperDoll.PostInit: + for every authored equipment-list element: + list = GetChildRecursive(elementID).DynamicCast(UIElement_ItemList) + RegisterItemListDragHandler(list, paperDoll) + UpdateItemSlotTooltip(list, empty) + + hide the nine armor-location lists in doll view + +ItemList.InternalCreateItem: + prototypeID = GetAttribute_Enum(ItemListCellTemplate) + catalog = GetByEnum(UIElementCatalog, LayoutDesc, UIItemCatalog) + return CreateChildElement(list, catalog, prototypeID).DynamicCast(UIElement_UIItem) + +PaperDoll.SetUIItemIntoLocation(itemID, locationMask): + for each paperdoll slot whose location bit intersects locationMask: + flush that slot's one-cell item list + add itemID to that list + update its tooltip + +UIItem empty draw: + draw prototype.child(0x1000033B).state(ItemSlot_Empty).image +``` + +## Authored slot-template table + +| Paperdoll element | Equip location | UIItem prototype | Empty RenderSurface | +|---|---|---|---| +| `0x100001DA` | NeckWear | `0x10000446` | `0x06000F68` | +| `0x100001DB` | WristWearLeft | `0x10000447` | `0x06000F5D` | +| `0x100001DC` | FingerWearLeft | `0x10000448` | `0x06000F5A` | +| `0x100001DD` | WristWearRight | `0x10000449` | `0x06000F6A` | +| `0x100001DE` | FingerWearRight | `0x1000044A` | `0x06000F6B` | +| `0x100001DF` | weapon composite | `0x1000044B` | `0x06000F66` | +| `0x100001E0` | MissileAmmo | `0x1000044C` | `0x06000F5E` | +| `0x100001E1` | Shield | `0x1000044D` | `0x06000F6C` | +| `0x100001E2` | ChestWear | `0x1000044E` | `0x060032C5` | +| `0x100001E3` | UpperLegWear | `0x1000044F` | `0x060032C4` | +| `0x1000058E` | TrinketOne | `0x1000058F` | `0x06006A6C` | +| `0x100005E9` | Cloak | `0x100005EA` | `0x0600708F` | +| `0x100005AB` | HeadWear | `0x100005B4` | `0x06006D7F` | +| `0x100005AC` | ChestArmor | `0x100005B5` | `0x06006D7B` | +| `0x100005AD` | AbdomenArmor | `0x100005B6` | `0x06006D79` | +| `0x100005AE` | UpperArmArmor | `0x100005B7` | `0x06006D87` | +| `0x100005AF` | LowerArmArmor | `0x100005B8` | `0x06006D81` | +| `0x100005B0` | HandWear | `0x100005B9` | `0x06006D7D` | +| `0x100005B1` | UpperLegArmor | `0x100005BA` | `0x06006D89` | +| `0x100005B2` | LowerLegArmor | `0x100005BB` | `0x06006D83` | +| `0x100005B3` | FootWear | `0x100005BD` | `0x06006D85` | + +The three Aetheria lists remain outside the current controller scope (AP-108). Their +authored backgrounds are blue/yellow/red prototypes `0x10000592..94`, but this fix +does not make those otherwise unimplemented slots reachable. + +## Port mapping + +- `PaperdollSlotBackgrounds` is the single table for element-id, `EquipMask`, and + UIItem prototype. `PaperdollController` consumes the same definitions for behavior, + preventing the visual and equip-location maps from drifting apart. +- `ItemListCellTemplate.ResolvePrototypeEmptySprite` reuses the already-shipped + inheritance-aware `ItemSlot_Empty` resolver instead of hardcoding surface IDs in + the controller. +- `RetailUiRuntime` and UI Studio resolve the 21 surfaces from the live DAT once at + bind time and inject them into the controller. A missing authored surface is a startup + error rather than a silently generic slot; the old inventory-grid sprite remains only + for isolated controller bindings that do not supply the paperdoll catalog map. diff --git a/src/AcDream.App/Studio/FixtureProvider.cs b/src/AcDream.App/Studio/FixtureProvider.cs index e4e65aae..92f11e82 100644 --- a/src/AcDream.App/Studio/FixtureProvider.cs +++ b/src/AcDream.App/Studio/FixtureProvider.cs @@ -136,6 +136,7 @@ public static class FixtureProvider uint contentsEmpty = ItemListCellTemplate.ResolveEmptySprite(dats, 0x21000021u, 0x100001C6u); uint sideBagEmpty = ItemListCellTemplate.ResolveEmptySprite(dats, 0x21000022u, 0x100001CAu); uint mainPackEmpty = ItemListCellTemplate.ResolveEmptySprite(dats, 0x21000022u, 0x100001C9u); + var paperdollEmpty = PaperdollSlotBackgrounds.ResolveEmptySprites(dats); var iconIds = MakeIconIds(stack); var selection = new AcDream.Core.Selection.SelectionState(); @@ -152,9 +153,7 @@ public static class FixtureProvider sideBagEmptySprite: sideBagEmpty, mainPackEmptySprite: mainPackEmpty); - // Bind the paperdoll equip slots (same imported subtree as the inventory). - // Mirrors GameWindow:2257-2265: PaperdollController.Bind with contentsEmpty as - // emptySlotSprite (each slot shows the same square-frame placeholder as the grid). + // Bind the paperdoll equip slots with their authored per-location UIItem prototypes. PaperdollController.Bind( layout, objects, @@ -163,7 +162,8 @@ public static class FixtureProvider selection: selection, sendWield: null, // no live session in the studio emptySlotSprite: contentsEmpty, - datFont: stack.VitalsDatFont); + datFont: stack.VitalsDatFont, + emptySlotSprites: paperdollEmpty); break; } diff --git a/src/AcDream.App/UI/Layout/ItemListCellTemplate.cs b/src/AcDream.App/UI/Layout/ItemListCellTemplate.cs index 29b33f3a..5775611c 100644 --- a/src/AcDream.App/UI/Layout/ItemListCellTemplate.cs +++ b/src/AcDream.App/UI/Layout/ItemListCellTemplate.cs @@ -40,9 +40,21 @@ public static class ItemListCellTemplate uint protoId = ReadCellTemplateId(listElem); // attr 0x1000000e if (protoId == 0) return 0; + return ResolvePrototypeEmptySprite(dats, protoId); + } + + /// + /// Resolves ItemSlot_Empty directly from an authored UIItem prototype in + /// . Paperdoll lists select distinct prototypes for their + /// jewelry, weapon, clothing, and armor locations even though the lists share one base. + /// + public static uint ResolvePrototypeEmptySprite(DatCollection dats, uint prototypeElementId) + { + if (prototypeElementId == 0) return 0; + var catalog = dats.Get(CatalogLayoutId); if (catalog is null) return 0; - var proto = FindDesc(catalog, protoId); + var proto = FindDesc(catalog, prototypeElementId); if (proto is null) return 0; // The empty-slot BACKGROUND is the m_elem_Icon's ItemSlot_Empty media, resolved THROUGH diff --git a/src/AcDream.App/UI/Layout/PaperdollController.cs b/src/AcDream.App/UI/Layout/PaperdollController.cs index afb6c31a..301755ab 100644 --- a/src/AcDream.App/UI/Layout/PaperdollController.cs +++ b/src/AcDream.App/UI/Layout/PaperdollController.cs @@ -44,37 +44,6 @@ public sealed class PaperdollController : IItemListDragHandler, IRetainedPanelCo public void Toggle() => SlotView = !SlotView; } - // WEAPON_READY_SLOT_LOC (acclient.h:3235): the weapon-hand doll slot accepts any wieldable weapon. - private const EquipMask WeaponSlotMask = - EquipMask.MeleeWeapon | EquipMask.MissileWeapon | EquipMask.Held | EquipMask.TwoHanded; - - // element-id → EquipMask (verified: dump paperdoll-0x21000024.txt ↔ deep-dive §3a ↔ acclient.h:3193). - // The 3 Aetheria sigil slots (0x10000595/96/97) are SetVisible(0) in retail — skipped (Slice 1 scope). - private static readonly (uint Element, EquipMask Mask)[] SlotMap = - { - (0x100005ABu, EquipMask.HeadWear), // 0x1 - (0x100001E2u, EquipMask.ChestWear), // 0x2 - (0x100001E3u, EquipMask.UpperLegWear), // 0x40 - (0x100005B0u, EquipMask.HandWear), // 0x20 - (0x100005B3u, EquipMask.FootWear), // 0x100 - (0x100005ACu, EquipMask.ChestArmor), // 0x200 - (0x100005ADu, EquipMask.AbdomenArmor), // 0x400 - (0x100005AEu, EquipMask.UpperArmArmor), // 0x800 - (0x100005AFu, EquipMask.LowerArmArmor), // 0x1000 - (0x100005B1u, EquipMask.UpperLegArmor), // 0x2000 - (0x100005B2u, EquipMask.LowerLegArmor), // 0x4000 - (0x100001DAu, EquipMask.NeckWear), // 0x8000 - (0x100001DBu, EquipMask.WristWearLeft), // 0x10000 - (0x100001DDu, EquipMask.WristWearRight), // 0x20000 - (0x100001DCu, EquipMask.FingerWearLeft), // 0x40000 - (0x100001DEu, EquipMask.FingerWearRight), // 0x80000 - (0x100001E1u, EquipMask.Shield), // 0x200000 - (0x100001E0u, EquipMask.MissileAmmo), // 0x800000 (LIKELY — gate-verify, AP row) - (0x100001DFu, WeaponSlotMask), // 0x3500000 composite - (0x1000058Eu, EquipMask.TrinketOne), // 0x4000000 - (0x100005E9u, EquipMask.Cloak), // 0x8000000 - }; - private readonly ClientObjectTable _objects; private readonly Func _playerGuid; private readonly Func _iconIds; @@ -98,7 +67,8 @@ public sealed class PaperdollController : IItemListDragHandler, IRetainedPanelCo Action? sendWield, uint emptySlotSprite, UiDatFont? datFont, ItemInteractionController? itemInteraction, PaperdollClickMap? clickMap, - Func? dragIconIds) + Func? dragIconIds, + IReadOnlyDictionary? emptySlotSprites) { _objects = objects; _playerGuid = playerGuid; _iconIds = iconIds; _sendWield = sendWield; _dragIconIds = dragIconIds; @@ -106,14 +76,17 @@ public sealed class PaperdollController : IItemListDragHandler, IRetainedPanelCo _selection = selection ?? throw new ArgumentNullException(nameof(selection)); _clickMap = clickMap; - for (int i = 0; i < SlotMap.Length; i++) + for (int i = 0; i < PaperdollSlotBackgrounds.Definitions.Length; i++) { - var (element, mask) = SlotMap[i]; + var (element, mask, _) = PaperdollSlotBackgrounds.Definitions[i]; if (layout.FindElement(element) is not UiItemList list) continue; list.RegisterDragHandler(this); list.Cell.SourceKind = ItemDragSource.Equipment; - list.Cell.SlotIndex = i; // SlotMap position = this equipped item's drag-payload SourceSlot when dragged out to unwield - list.Cell.EmptySprite = emptySlotSprite; // visible empty-slot frame so every position is seen + usable. The live + list.Cell.SlotIndex = i; // definition position = equipped drag-payload SourceSlot + list.Cell.EmptySprite = emptySlotSprites is not null + && emptySlotSprites.TryGetValue(element, out uint authoredSprite) + ? authoredSprite + : emptySlotSprite; list.Cell.Clicked = () => { uint itemId = list.Cell.ItemId; @@ -199,10 +172,11 @@ public sealed class PaperdollController : IItemListDragHandler, IRetainedPanelCo uint emptySlotSprite = 0u, UiDatFont? datFont = null, ItemInteractionController? itemInteraction = null, PaperdollClickMap? clickMap = null, - Func? dragIconIds = null) + Func? dragIconIds = null, + IReadOnlyDictionary? emptySlotSprites = null) => new PaperdollController( layout, objects, playerGuid, iconIds, selection, sendWield, emptySlotSprite, - datFont, itemInteraction, clickMap, dragIconIds); + datFont, itemInteraction, clickMap, dragIconIds, emptySlotSprites); private void HandleDollClick(int x, int y) { diff --git a/src/AcDream.App/UI/Layout/PaperdollSlotBackgrounds.cs b/src/AcDream.App/UI/Layout/PaperdollSlotBackgrounds.cs new file mode 100644 index 00000000..eba93ada --- /dev/null +++ b/src/AcDream.App/UI/Layout/PaperdollSlotBackgrounds.cs @@ -0,0 +1,86 @@ +using System; +using System.Collections.Generic; +using AcDream.Core.Items; +using DatReaderWriter; + +namespace AcDream.App.UI.Layout; + +/// +/// Retail paperdoll slot definitions: one source of truth for the paperdoll element, +/// accepted , and authored UIItem prototype whose +/// ItemSlot_Empty state supplies that location's background. +/// +/// +/// Element-to-location: gmPaperDollUI::GetLocationInfoFromElementID @ 0x004A37F0. +/// Cell creation: UIElement_ItemList::InternalCreateItem @ 0x004E3570. +/// Prototype media: live DAT catalog LayoutDesc 0x21000037. +/// +public static class PaperdollSlotBackgrounds +{ + internal readonly record struct Definition( + uint Element, + EquipMask Mask, + uint EmptyPrototype); + + // WEAPON_READY_SLOT_LOC (acclient.h:3235): any wieldable weapon in the hand slot. + private const EquipMask WeaponSlotMask = + EquipMask.MeleeWeapon | EquipMask.MissileWeapon | EquipMask.Held | EquipMask.TwoHanded; + + internal static readonly Definition[] Definitions = + { + new(0x100005ABu, EquipMask.HeadWear, 0x100005B4u), + new(0x100001E2u, EquipMask.ChestWear, 0x1000044Eu), + new(0x100001E3u, EquipMask.UpperLegWear, 0x1000044Fu), + new(0x100005B0u, EquipMask.HandWear, 0x100005B9u), + new(0x100005B3u, EquipMask.FootWear, 0x100005BDu), + new(0x100005ACu, EquipMask.ChestArmor, 0x100005B5u), + new(0x100005ADu, EquipMask.AbdomenArmor, 0x100005B6u), + new(0x100005AEu, EquipMask.UpperArmArmor, 0x100005B7u), + new(0x100005AFu, EquipMask.LowerArmArmor, 0x100005B8u), + new(0x100005B1u, EquipMask.UpperLegArmor, 0x100005BAu), + new(0x100005B2u, EquipMask.LowerLegArmor, 0x100005BBu), + new(0x100001DAu, EquipMask.NeckWear, 0x10000446u), + new(0x100001DBu, EquipMask.WristWearLeft, 0x10000447u), + new(0x100001DDu, EquipMask.WristWearRight, 0x10000449u), + new(0x100001DCu, EquipMask.FingerWearLeft, 0x10000448u), + new(0x100001DEu, EquipMask.FingerWearRight, 0x1000044Au), + new(0x100001E1u, EquipMask.Shield, 0x1000044Du), + new(0x100001E0u, EquipMask.MissileAmmo, 0x1000044Cu), + new(0x100001DFu, WeaponSlotMask, 0x1000044Bu), + new(0x1000058Eu, EquipMask.TrinketOne, 0x1000058Fu), + new(0x100005E9u, EquipMask.Cloak, 0x100005EAu), + }; + + /// Resolves every supported slot's exact empty RenderSurface from the live DAT. + public static IReadOnlyDictionary ResolveEmptySprites(DatCollection dats) + { + var result = new Dictionary(Definitions.Length); + foreach (Definition definition in Definitions) + { + uint sprite = ItemListCellTemplate.ResolvePrototypeEmptySprite( + dats, + definition.EmptyPrototype); + if (sprite == 0) + { + throw new InvalidOperationException( + $"Retail paperdoll UIItem prototype 0x{definition.EmptyPrototype:X8} " + + $"for slot 0x{definition.Element:X8} has no ItemSlot_Empty surface."); + } + result.Add(definition.Element, sprite); + } + return result; + } + + /// Exposes the authored prototype identity for dat-free conformance tests. + public static bool TryGetPrototype(uint slotElementId, out uint prototypeElementId) + { + foreach (Definition definition in Definitions) + { + if (definition.Element != slotElementId) continue; + prototypeElementId = definition.EmptyPrototype; + return true; + } + prototypeElementId = 0; + return false; + } +} diff --git a/src/AcDream.App/UI/RetailUiRuntime.cs b/src/AcDream.App/UI/RetailUiRuntime.cs index cedeb10a..c2423ed4 100644 --- a/src/AcDream.App/UI/RetailUiRuntime.cs +++ b/src/AcDream.App/UI/RetailUiRuntime.cs @@ -691,12 +691,14 @@ public sealed class RetailUiRuntime : IDisposable }); uint contents, sideBag, mainPack; + IReadOnlyDictionary paperdollEmptySprites; PaperdollClickMap? paperdollClickMap; lock (_bindings.Assets.DatLock) { contents = ItemListCellTemplate.ResolveEmptySprite(_bindings.Assets.Dats, 0x21000021u, 0x100001C6u); sideBag = ItemListCellTemplate.ResolveEmptySprite(_bindings.Assets.Dats, 0x21000022u, 0x100001CAu); mainPack = ItemListCellTemplate.ResolveEmptySprite(_bindings.Assets.Dats, 0x21000022u, 0x100001C9u); + paperdollEmptySprites = PaperdollSlotBackgrounds.ResolveEmptySprites(_bindings.Assets.Dats); paperdollClickMap = PaperdollClickMap.Load(_bindings.Assets.Dats); } @@ -716,7 +718,7 @@ public sealed class RetailUiRuntime : IDisposable PaperdollController paperdoll = PaperdollController.Bind( layout, b.Objects, b.PlayerGuid, b.ResolveIcon, b.Selection, b.SendWield, contents, _bindings.Assets.DefaultFont, b.ItemInteraction, paperdollClickMap, - b.ResolveDragIcon); + b.ResolveDragIcon, paperdollEmptySprites); Host.WindowManager.AttachController( WindowNames.Inventory, new RetainedPanelControllerGroup(inventory, paperdoll)); diff --git a/tests/AcDream.App.Tests/UI/Layout/PaperdollControllerTests.cs b/tests/AcDream.App.Tests/UI/Layout/PaperdollControllerTests.cs index 950e740f..4d6194c2 100644 --- a/tests/AcDream.App.Tests/UI/Layout/PaperdollControllerTests.cs +++ b/tests/AcDream.App.Tests/UI/Layout/PaperdollControllerTests.cs @@ -36,12 +36,14 @@ public class PaperdollControllerTests private static PaperdollController Bind(ImportedLayout layout, ClientObjectTable objects, List<(uint item, uint mask)>? wields = null, uint emptySlot = 0u, - SelectionState? selection = null) + SelectionState? selection = null, + IReadOnlyDictionary? emptySlotSprites = null) => PaperdollController.Bind(layout, objects, () => Player, iconIds: (_, _, _, _, _) => 0x1234u, sendWield: wields is null ? null : (i, m) => wields.Add((i, m)), emptySlotSprite: emptySlot, - selection: selection ?? new SelectionState()); + selection: selection ?? new SelectionState(), + emptySlotSprites: emptySlotSprites); private static void SeedEquipped(ClientObjectTable t, uint guid, EquipMask loc) { @@ -219,6 +221,26 @@ public class PaperdollControllerTests Assert.Equal(0x06004D20u, lists[HeadSlot].Cell.EmptySprite); } + [Fact] + public void PerLocation_empty_sprites_override_the_generic_fallback() + { + var (layout, lists) = BuildLayout(); + var sprites = new Dictionary + { + [HeadSlot] = 0x06006D7Fu, + [ChestSlot] = 0x060032C5u, + [ChestArmorSlot] = 0x06006D7Bu, + [ShieldSlot] = 0x06000F6Cu, + [WeaponSlot] = 0x06000F66u, + [FingerLSlot] = 0x06000F5Au, + }; + + Bind(layout, new ClientObjectTable(), emptySlot: 0x06004D20u, emptySlotSprites: sprites); + + foreach (var (element, sprite) in sprites) + Assert.Equal(sprite, lists[element].Cell.EmptySprite); + } + [Fact] public void Live_player_wield_repaints_the_slot() // event-driven: ObjectMoved → Concerns → Populate { diff --git a/tests/AcDream.App.Tests/UI/Layout/PaperdollSlotBackgroundTests.cs b/tests/AcDream.App.Tests/UI/Layout/PaperdollSlotBackgroundTests.cs new file mode 100644 index 00000000..910f44bc --- /dev/null +++ b/tests/AcDream.App.Tests/UI/Layout/PaperdollSlotBackgroundTests.cs @@ -0,0 +1,85 @@ +using System; +using System.Collections.Generic; +using System.IO; +using AcDream.App.UI.Layout; +using DatReaderWriter; +using DatReaderWriter.Options; +using Xunit; + +namespace AcDream.App.Tests.UI.Layout; + +public class PaperdollSlotBackgroundTests +{ + public static TheoryData SlotPrototypeCases => new() + { + { 0x100001DAu, 0x10000446u }, // neck + { 0x100001DBu, 0x10000447u }, // left wrist + { 0x100001DCu, 0x10000448u }, // left ring + { 0x100001DDu, 0x10000449u }, // right wrist + { 0x100001DEu, 0x1000044Au }, // right ring + { 0x100001DFu, 0x1000044Bu }, // weapon + { 0x100001E0u, 0x1000044Cu }, // ammo + { 0x100001E1u, 0x1000044Du }, // shield + { 0x100001E2u, 0x1000044Eu }, // shirt + { 0x100001E3u, 0x1000044Fu }, // pants + { 0x1000058Eu, 0x1000058Fu }, // trinket + { 0x100005E9u, 0x100005EAu }, // cloak + { 0x100005ABu, 0x100005B4u }, // head + { 0x100005ACu, 0x100005B5u }, // chest armor + { 0x100005ADu, 0x100005B6u }, // abdomen armor + { 0x100005AEu, 0x100005B7u }, // upper arm armor + { 0x100005AFu, 0x100005B8u }, // lower arm armor + { 0x100005B0u, 0x100005B9u }, // hands + { 0x100005B1u, 0x100005BAu }, // upper legs + { 0x100005B2u, 0x100005BBu }, // lower legs + { 0x100005B3u, 0x100005BDu }, // feet + }; + + [Theory] + [MemberData(nameof(SlotPrototypeCases))] + public void Slot_uses_authored_UIItem_prototype(uint slotElement, uint prototypeElement) + { + Assert.True(PaperdollSlotBackgrounds.TryGetPrototype(slotElement, out uint actual)); + Assert.Equal(prototypeElement, actual); + } + + [Fact] + public void Live_dat_resolves_all_pinned_retail_backgrounds() + { + string datDir = Environment.GetEnvironmentVariable("ACDREAM_DAT_DIR") + ?? Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), + "Documents", "Asheron's Call"); + if (!Directory.Exists(datDir)) return; + + using var dats = new DatCollection(datDir, DatAccessType.Read); + IReadOnlyDictionary actual = PaperdollSlotBackgrounds.ResolveEmptySprites(dats); + var expected = new Dictionary + { + [0x100001DAu] = 0x06000F68u, + [0x100001DBu] = 0x06000F5Du, + [0x100001DCu] = 0x06000F5Au, + [0x100001DDu] = 0x06000F6Au, + [0x100001DEu] = 0x06000F6Bu, + [0x100001DFu] = 0x06000F66u, + [0x100001E0u] = 0x06000F5Eu, + [0x100001E1u] = 0x06000F6Cu, + [0x100001E2u] = 0x060032C5u, + [0x100001E3u] = 0x060032C4u, + [0x1000058Eu] = 0x06006A6Cu, + [0x100005E9u] = 0x0600708Fu, + [0x100005ABu] = 0x06006D7Fu, + [0x100005ACu] = 0x06006D7Bu, + [0x100005ADu] = 0x06006D79u, + [0x100005AEu] = 0x06006D87u, + [0x100005AFu] = 0x06006D81u, + [0x100005B0u] = 0x06006D7Du, + [0x100005B1u] = 0x06006D89u, + [0x100005B2u] = 0x06006D83u, + [0x100005B3u] = 0x06006D85u, + }; + + Assert.Equal(expected.Count, actual.Count); + foreach (var (slot, sprite) in expected) + Assert.Equal(sprite, actual[slot]); + } +}