fix(paperdoll): restore authored slot backgrounds
Port the retail UIItem catalog path so every supported equipment location resolves its exact empty-state silhouette from live DAT. Share one definition table between equip behavior and presentation, wire runtime plus UI Studio, pin all 21 prototypes and surfaces, and retire AP-66. Co-Authored-By: Codex <codex@openai.com>
This commit is contained in:
parent
6ba4148b24
commit
1be18cc4db
11 changed files with 335 additions and 52 deletions
|
|
@ -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.
|
||||
|
||||
|
|
|
|||
|
|
@ -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 |
|
||||
|
|
|
|||
|
|
@ -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 <datdir> [--layout 0xNNNN | --dump <slug>] [--screenshot <png>]` 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.
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -40,9 +40,21 @@ public static class ItemListCellTemplate
|
|||
uint protoId = ReadCellTemplateId(listElem); // attr 0x1000000e
|
||||
if (protoId == 0) return 0;
|
||||
|
||||
return ResolvePrototypeEmptySprite(dats, protoId);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Resolves <c>ItemSlot_Empty</c> directly from an authored UIItem prototype in
|
||||
/// <see cref="CatalogLayoutId"/>. Paperdoll lists select distinct prototypes for their
|
||||
/// jewelry, weapon, clothing, and armor locations even though the lists share one base.
|
||||
/// </summary>
|
||||
public static uint ResolvePrototypeEmptySprite(DatCollection dats, uint prototypeElementId)
|
||||
{
|
||||
if (prototypeElementId == 0) return 0;
|
||||
|
||||
var catalog = dats.Get<LayoutDesc>(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
|
||||
|
|
|
|||
|
|
@ -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<uint> _playerGuid;
|
||||
private readonly Func<ItemType, uint, uint, uint, uint, uint> _iconIds;
|
||||
|
|
@ -98,7 +67,8 @@ public sealed class PaperdollController : IItemListDragHandler, IRetainedPanelCo
|
|||
Action<uint, uint>? sendWield,
|
||||
uint emptySlotSprite, UiDatFont? datFont, ItemInteractionController? itemInteraction,
|
||||
PaperdollClickMap? clickMap,
|
||||
Func<ItemType, uint, uint, uint, uint, uint>? dragIconIds)
|
||||
Func<ItemType, uint, uint, uint, uint, uint>? dragIconIds,
|
||||
IReadOnlyDictionary<uint, uint>? 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<ItemType, uint, uint, uint, uint, uint>? dragIconIds = null)
|
||||
Func<ItemType, uint, uint, uint, uint, uint>? dragIconIds = null,
|
||||
IReadOnlyDictionary<uint, uint>? 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)
|
||||
{
|
||||
|
|
|
|||
86
src/AcDream.App/UI/Layout/PaperdollSlotBackgrounds.cs
Normal file
86
src/AcDream.App/UI/Layout/PaperdollSlotBackgrounds.cs
Normal file
|
|
@ -0,0 +1,86 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using AcDream.Core.Items;
|
||||
using DatReaderWriter;
|
||||
|
||||
namespace AcDream.App.UI.Layout;
|
||||
|
||||
/// <summary>
|
||||
/// Retail paperdoll slot definitions: one source of truth for the paperdoll element,
|
||||
/// accepted <see cref="EquipMask"/>, and authored UIItem prototype whose
|
||||
/// <c>ItemSlot_Empty</c> state supplies that location's background.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Element-to-location: <c>gmPaperDollUI::GetLocationInfoFromElementID @ 0x004A37F0</c>.
|
||||
/// Cell creation: <c>UIElement_ItemList::InternalCreateItem @ 0x004E3570</c>.
|
||||
/// Prototype media: live DAT catalog <c>LayoutDesc 0x21000037</c>.
|
||||
/// </remarks>
|
||||
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),
|
||||
};
|
||||
|
||||
/// <summary>Resolves every supported slot's exact empty RenderSurface from the live DAT.</summary>
|
||||
public static IReadOnlyDictionary<uint, uint> ResolveEmptySprites(DatCollection dats)
|
||||
{
|
||||
var result = new Dictionary<uint, uint>(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;
|
||||
}
|
||||
|
||||
/// <summary>Exposes the authored prototype identity for dat-free conformance tests.</summary>
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
|
@ -691,12 +691,14 @@ public sealed class RetailUiRuntime : IDisposable
|
|||
});
|
||||
|
||||
uint contents, sideBag, mainPack;
|
||||
IReadOnlyDictionary<uint, uint> 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));
|
||||
|
|
|
|||
|
|
@ -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<uint, uint>? 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<uint, uint>
|
||||
{
|
||||
[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
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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<uint, uint> 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<uint, uint> actual = PaperdollSlotBackgrounds.ResolveEmptySprites(dats);
|
||||
var expected = new Dictionary<uint, uint>
|
||||
{
|
||||
[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]);
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue