fix(paperdoll): port quest-gated Aetheria slots

Add the three authored blue, yellow, and red sigil backgrounds and drive their visibility from player PropertyInt.AetheriaBitfield bits 1/2/4 at login and on live updates, matching gmPaperDollUI::UpdateAetheria. Include the sigil equip masks in the shared slot table and narrow AP-108.

Co-Authored-By: Codex <codex@openai.com>
This commit is contained in:
Erik 2026-07-13 09:55:20 +02:00
parent 1be18cc4db
commit edc9be3008
11 changed files with 208 additions and 16 deletions

View file

@ -6905,7 +6905,7 @@ The burden bar now reads the server's wire `EncumbranceVal` (PropertyInt 5) inst
## Inventory + equipment slots show the wrong empty-slot background art ## Inventory + equipment slots show the wrong empty-slot background art
**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. **Status:** CORE PAPERDOLL BACKGROUNDS VISUALLY CONFIRMED 2026-07-13; AETHERIA EXTENSION IMPLEMENTED, LIVE GATE PENDING. Inventory contents/containers were visually confirmed 2026-06-22. The paperdoll now resolves all 24 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 **Filed:** 2026-06-21
**Component:** ui — D.2b inventory (UiItemSlot empty sprite + paperdoll equip slots) **Component:** ui — D.2b inventory (UiItemSlot empty sprite + paperdoll equip slots)
@ -6917,11 +6917,13 @@ The burden bar now reads the server's wire `EncumbranceVal` (PropertyInt 5) inst
**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`. **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`.
**Aetheria extension (2026-07-13):** The first visual gate passed for the original 21 locations and exposed the intentionally deferred Aetheria row. The same definition table now includes sigil elements `0x10000595..97`, equip masks `0x10000000/0x20000000/0x40000000`, prototypes `0x10000592..94`, and exact blue/yellow/red surfaces `0x06006BEF..F1`. Visibility ports `gmPaperDollUI::UpdateAetheria @ 0x004A3E50`: player `PropertyInt.AetheriaBitfield (322)` bits 1/2/4 independently expose the three slots at login and on live `0x02CD` updates. A missing property hides all three. AP-108 narrowed.
**Files:** `src/AcDream.App/UI/UiItemSlot.cs`; `src/AcDream.App/UI/Layout/ItemListCellTemplate.cs`; `InventoryController.cs`; `PaperdollSlotBackgrounds.cs`; `PaperdollController.cs`. **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`. **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. **Acceptance:** empty inventory + side-bag cells show the retail pack-slot frame; all 24 equip locations show their authored backgrounds; Aetheria slots remain hidden until their individual unlock bits arrive.
--- ---

View file

@ -210,7 +210,7 @@ AP-94..AP-112 for the confirmed retail-UI completion gaps.
| AP-105 | **PARTIAL 2026-07-10 (Wave 2.2)** — inherited scrollbar media/roles now come from DAT (decrement/top `0x06004C69`, increment/bottom `0x06004C6C`). Retained chat still lacks complete tab/filter/unread, social availability, squelch, and focus-opacity behavior; a second ChatVM still loses FPS/position providers. | `src/AcDream.App/UI/Layout/DatWidgetFactory.cs`; `ChatWindowController.cs`; chat mount in `GameWindow.cs` | Shared log/send path, wrapping, scrollbar roles, and outer maximize geometry work; Wave 5 consolidates remaining chat state | Commands degrade, tabs are no-ops, moderation/channel state is wrong, and focus visuals diverge | `gmMainChatUI @ 0x004CCCC0..0x004CE2A0`; `UIElement_Scrollbar::OnSetAttribute @ 0x004714D0`; `ChatInterface` methods | | AP-105 | **PARTIAL 2026-07-10 (Wave 2.2)** — inherited scrollbar media/roles now come from DAT (decrement/top `0x06004C69`, increment/bottom `0x06004C6C`). Retained chat still lacks complete tab/filter/unread, social availability, squelch, and focus-opacity behavior; a second ChatVM still loses FPS/position providers. | `src/AcDream.App/UI/Layout/DatWidgetFactory.cs`; `ChatWindowController.cs`; chat mount in `GameWindow.cs` | Shared log/send path, wrapping, scrollbar roles, and outer maximize geometry work; Wave 5 consolidates remaining chat state | Commands degrade, tabs are no-ops, moderation/channel state is wrong, and focus visuals diverge | `gmMainChatUI @ 0x004CCCC0..0x004CE2A0`; `UIElement_Scrollbar::OnSetAttribute @ 0x004714D0`; `ChatInterface` methods |
| AP-106 | The retained UI has no external/ground-container window lifecycle, while the original owned-side-bag `NoLongerViewingContents` premise was incorrect (#196) | `src/AcDream.App/UI`; window runtime; absent external-container controller | Owned inventory navigation remains usable and must not gain an unproven `0x0195` send; Wave 2/6 adds lifecycle ownership and the separate external surface | External container replacement/close cannot notify the server exactly once; adding the packet to owned bag close would itself diverge from the named retail call graph | `ClientUISystem.groundObject`; `CM_Inventory::Event_NoLongerViewingContents @ 0x006ABC50`; `docs/research/2026-07-10-retail-panel-behavior-pseudocode.md` | | AP-106 | The retained UI has no external/ground-container window lifecycle, while the original owned-side-bag `NoLongerViewingContents` premise was incorrect (#196) | `src/AcDream.App/UI`; window runtime; absent external-container controller | Owned inventory navigation remains usable and must not gain an unproven `0x0195` send; Wave 2/6 adds lifecycle ownership and the separate external surface | External container replacement/close cannot notify the server exactly once; adding the packet to owned bag close would itself diverge from the named retail call graph | `ClientUISystem.groundObject`; `CM_Inventory::Event_NoLongerViewingContents @ 0x006ABC50`; `docs/research/2026-07-10-retail-panel-behavior-pseudocode.md` |
| ~~AP-107~~ | **RETIRED 2026-07-11 (Wave 3.3 / #197)** — typed `OfferPrimaryClick` returns `NotActive`, `ConsumedSuccess`, or `ConsumedRejected`; every retained item surface plus radar/world offers active target mode before local selection/open/use fallback. Rejections are consumed and cannot drift selection. | `src/AcDream.App/UI/ItemInteractionController.cs`; inventory/paperdoll/toolbar/radar/world call sites | — | — | `UIElement_ItemList::HandleTargetedUseLeftClick @ 0x004E24D0` | | ~~AP-107~~ | **RETIRED 2026-07-11 (Wave 3.3 / #197)** — typed `OfferPrimaryClick` returns `NotActive`, `ConsumedSuccess`, or `ConsumedRejected`; every retained item surface plus radar/world offers active target mode before local selection/open/use fallback. Rejections are consumed and cannot drift selection. | `src/AcDream.App/UI/ItemInteractionController.cs`; inventory/paperdoll/toolbar/radar/world call sites | — | — | `UIElement_ItemList::HandleTargetedUseLeftClick @ 0x004E24D0` |
| AP-108 | Paperdoll/AutoWield still omit Aetheria, full `AutoWieldIsLegal`/dual-wield rules, double-click examine/drag from the doll, and body-part selection lighting | `src/AcDream.App/UI/Layout/PaperdollController.cs`; `src/AcDream.App/UI/AutoWieldController.cs` | Basic equip slots and live doll work; inventory double-click now faithfully sequences the primary weapon, incompatible shield, and mismatched ammo blockers through server-confirmed dequip→wield in both peace and war | Special slots, illegal/off-hand cases, doll examine/drag, and selection lighting still differ functionally | `CPlayerSystem::AutoWield @ 0x00560A60`; `ACCWeenieObject::BlocksUseOfShield @ 0x0055D3E0`; `gmPaperDollUI @ 0x004A3590..0x004A5F90` | | AP-108 | Paperdoll/AutoWield still omit full `AutoWieldIsLegal`/dual-wield rules, double-click examine/drag from the doll, and body-part selection lighting. **Aetheria retired from this row 2026-07-13:** all three sigil slots use exact backgrounds/equip masks and live `PropertyInt.AetheriaBitfield` visibility. | `src/AcDream.App/UI/Layout/PaperdollController.cs`; `src/AcDream.App/UI/AutoWieldController.cs` | Basic equip slots, Aetheria, and live doll work; inventory double-click faithfully sequences the primary weapon, incompatible shield, and mismatched ammo blockers through server-confirmed dequip→wield in both peace and war | Remaining illegal/off-hand cases, doll examine/drag, and selection lighting still differ functionally | `CPlayerSystem::AutoWield @ 0x00560A60`; `ACCWeenieObject::BlocksUseOfShield @ 0x0055D3E0`; `gmPaperDollUI @ 0x004A3590..0x004A5F90` |
| AP-109 | Character Titles page is inert and live displayed-title/luminance state is absent | `src/AcDream.App/UI/Layout/CharacterStatController.cs`; `CharacterSheetProvider.cs` | Attributes/skills core output is user-accepted | Titles cannot be selected/displayed and level-200 luminance fields are missing | `gmCharacterTitleUI @ 0x0049A610`; `gmStatManagementUI::UpdateExperience @ 0x004F0A70` | | AP-109 | Character Titles page is inert and live displayed-title/luminance state is absent | `src/AcDream.App/UI/Layout/CharacterStatController.cs`; `CharacterSheetProvider.cs` | Attributes/skills core output is user-accepted | Titles cannot be selected/displayed and level-200 luminance fields are missing | `gmCharacterTitleUI @ 0x0049A610`; `gmStatManagementUI::UpdateExperience @ 0x004F0A70` |
| AP-110 | Remaining retained gameplay panels and world HUD are absent: advanced-combat powerbar, spellbook/effects/favorite spell bars, residual social/examine/floating chat, quests/map/vitae/options/smartbox, vendor/trade/salvage/tinkering, and D.6 nameplates/floaters | D.5/D.6 roadmap; retained layout registration set | Basic `gmCombatUI` now covers the active M2 melee/missile loop; Wave 10 lands each remaining surface against authoritative state | Large portions of retail gameplay still have no production UI | Named `gm*UI::PostInit` methods and LayoutDesc catalog | | AP-110 | Remaining retained gameplay panels and world HUD are absent: advanced-combat powerbar, spellbook/effects/favorite spell bars, residual social/examine/floating chat, quests/map/vitae/options/smartbox, vendor/trade/salvage/tinkering, and D.6 nameplates/floaters | D.5/D.6 roadmap; retained layout registration set | Basic `gmCombatUI` now covers the active M2 melee/missile loop; Wave 10 lands each remaining surface against authoritative state | Large portions of retail gameplay still have no production UI | Named `gm*UI::PostInit` methods and LayoutDesc catalog |
| ~~AP-111~~ | **RETIRED 2026-07-11 (M2 held-object parenting)** — equipped hand items are no longer omitted from the render world. CreateObject now preserves Placement/Parent/position timestamp bootstrap; live `0xF749` ParentEvent is parsed with retail sequence freshness; a focused render controller resolves `Setup.HoldingLocations`, applies the child's placement frame, and recomposes the separate child entity after every parent animation tick. Pickup retains the weenie's visual metadata for a later wield. | `src/AcDream.Core.Net/Messages/{CreateObject,ParentEvent}.cs`; `src/AcDream.Core/Meshing/EquippedChildAttachment.cs`; `src/AcDream.App/Rendering/EquippedChildRenderController.cs` | — | — | `ClientCombatSystem::GetDefaultCombatMode @ 0x0056B310`; `SmartBox::HandleParentEvent @ 0x004535D0`; `CPhysicsObj::set_parent @ 0x00515A90`; `CPhysicsObj::UpdateChild @ 0x00512D50` | | ~~AP-111~~ | **RETIRED 2026-07-11 (M2 held-object parenting)** — equipped hand items are no longer omitted from the render world. CreateObject now preserves Placement/Parent/position timestamp bootstrap; live `0xF749` ParentEvent is parsed with retail sequence freshness; a focused render controller resolves `Setup.HoldingLocations`, applies the child's placement frame, and recomposes the separate child entity after every parent animation tick. Pickup retains the weenie's visual metadata for a later wield. | `src/AcDream.Core.Net/Messages/{CreateObject,ParentEvent}.cs`; `src/AcDream.Core/Meshing/EquippedChildAttachment.cs`; `src/AcDream.App/Rendering/EquippedChildRenderController.cs` | — | — | `ClientCombatSystem::GetDefaultCombatMode @ 0x0056B310`; `SmartBox::HandleParentEvent @ 0x004535D0`; `CPhysicsObj::set_parent @ 0x00515A90`; `CPhysicsObj::UpdateChild @ 0x00512D50` |

View file

@ -482,6 +482,7 @@ behavior. Estimated 1726 days focused work, 35 weeks calendar.
- **✓ SHIPPED — D.5.1 — Toolbar (action bar).** Shipped 2026-06-16/17 (`30b28c2``0e7a083`, branch claude/hopeful-maxwell-214a12). First data-driven *game* panel: `gmToolbarUI` (`LayoutDesc 0x21000016`) — 18 shortcut slots from the persisted `PlayerDescription` SHORTCUT block, real **composited** item icons (opaque type-default underlay via the `EnumIDMap 0x10000004` resolve), **occupancy-gated slot numbers 19** (occupied = dark-box peace/war `0x10000042/43`; empty = background `0x1000005e` from cell composite `0x10000341`), **click-to-use** (`ItemHolder::UseObject``0x0036`), **peace/war stance** indicator live-wired to `CombatState`, **movable**, and a **chrome frame** (UiNineSlicePanel drawn over content via the new `UiElement.OnDrawAfterChildren` hook). New shared widgets `UiItemSlot` (`UIElement_UIItem` 0x10000032, procedural leaf) + `UiItemList` (`UIElement_ItemList` 0x10000031, factory branch) + `IconComposer` (CPU layered composite). `CreateObject.TryParse` extended to the full ACE-order weenie-header tail to capture `IconId`/`IconOverlay`/`IconUnderlay``ItemRepository.EnrichItem` → re-render. Spec/plan `docs/superpowers/{specs,plans}/2026-06-16-d2b-toolbar-phase1*.md`; research drop `docs/research/2026-06-16-*deep-dive.md` + synthesis. Divergence IA-16/IA-17 added. **User-confirmed** (numbers, icons, frame). Per-task spec+code-review throughout. - **✓ SHIPPED — D.5.1 — Toolbar (action bar).** Shipped 2026-06-16/17 (`30b28c2``0e7a083`, branch claude/hopeful-maxwell-214a12). First data-driven *game* panel: `gmToolbarUI` (`LayoutDesc 0x21000016`) — 18 shortcut slots from the persisted `PlayerDescription` SHORTCUT block, real **composited** item icons (opaque type-default underlay via the `EnumIDMap 0x10000004` resolve), **occupancy-gated slot numbers 19** (occupied = dark-box peace/war `0x10000042/43`; empty = background `0x1000005e` from cell composite `0x10000341`), **click-to-use** (`ItemHolder::UseObject``0x0036`), **peace/war stance** indicator live-wired to `CombatState`, **movable**, and a **chrome frame** (UiNineSlicePanel drawn over content via the new `UiElement.OnDrawAfterChildren` hook). New shared widgets `UiItemSlot` (`UIElement_UIItem` 0x10000032, procedural leaf) + `UiItemList` (`UIElement_ItemList` 0x10000031, factory branch) + `IconComposer` (CPU layered composite). `CreateObject.TryParse` extended to the full ACE-order weenie-header tail to capture `IconId`/`IconOverlay`/`IconUnderlay``ItemRepository.EnrichItem` → re-render. Spec/plan `docs/superpowers/{specs,plans}/2026-06-16-d2b-toolbar-phase1*.md`; research drop `docs/research/2026-06-16-*deep-dive.md` + synthesis. Divergence IA-16/IA-17 added. **User-confirmed** (numbers, icons, frame). Per-task spec+code-review throughout.
- **✓ 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`. - **✓ 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) ✅, 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. - **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.
- **D.5 paperdoll background correction (2026-07-13):** the original 21 per-location backgrounds passed the live visual gate. The three Aetheria locations are now also implemented with exact blue/yellow/red UIItem prototypes and independent player `AetheriaBitfield (322)` login/live visibility; their corrective live gate is pending. AP-66 retired and Aetheria removed from AP-108.
- **✓ 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 — 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 — 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. - **✓ 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.

View file

@ -2,8 +2,8 @@
Date: 2026-07-13 Date: 2026-07-13
Scope: the empty-cell art for the 21 currently supported equipment lists in Scope: the empty-cell art for all 24 equipment lists in `gmPaperDollUI`, including
`gmPaperDollUI`. This corrects the earlier visual inference that the slots had no the three quest-unlocked Aetheria sigil locations. This corrects the earlier visual inference that the slots had no
per-location art. The paperdoll layout elements themselves inherit the same ItemList per-location art. The paperdoll layout elements themselves inherit the same ItemList
base, but retail creates their cells from distinct `UIElement_UIItem` prototypes in base, but retail creates their cells from distinct `UIElement_UIItem` prototypes in
the shared catalog `0x21000037`. the shared catalog `0x21000037`.
@ -72,6 +72,9 @@ UIItem empty draw:
| `0x100001E3` | UpperLegWear | `0x1000044F` | `0x060032C4` | | `0x100001E3` | UpperLegWear | `0x1000044F` | `0x060032C4` |
| `0x1000058E` | TrinketOne | `0x1000058F` | `0x06006A6C` | | `0x1000058E` | TrinketOne | `0x1000058F` | `0x06006A6C` |
| `0x100005E9` | Cloak | `0x100005EA` | `0x0600708F` | | `0x100005E9` | Cloak | `0x100005EA` | `0x0600708F` |
| `0x10000595` | SigilOne / blue | `0x10000592` | `0x06006BEF` |
| `0x10000596` | SigilTwo / yellow | `0x10000593` | `0x06006BF0` |
| `0x10000597` | SigilThree / red | `0x10000594` | `0x06006BF1` |
| `0x100005AB` | HeadWear | `0x100005B4` | `0x06006D7F` | | `0x100005AB` | HeadWear | `0x100005B4` | `0x06006D7F` |
| `0x100005AC` | ChestArmor | `0x100005B5` | `0x06006D7B` | | `0x100005AC` | ChestArmor | `0x100005B5` | `0x06006D7B` |
| `0x100005AD` | AbdomenArmor | `0x100005B6` | `0x06006D79` | | `0x100005AD` | AbdomenArmor | `0x100005B6` | `0x06006D79` |
@ -82,9 +85,23 @@ UIItem empty draw:
| `0x100005B2` | LowerLegArmor | `0x100005BB` | `0x06006D83` | | `0x100005B2` | LowerLegArmor | `0x100005BB` | `0x06006D83` |
| `0x100005B3` | FootWear | `0x100005BD` | `0x06006D85` | | `0x100005B3` | FootWear | `0x100005BD` | `0x06006D85` |
The three Aetheria lists remain outside the current controller scope (AP-108). Their ## Aetheria unlock visibility
authored backgrounds are blue/yellow/red prototypes `0x10000592..94`, but this fix
does not make those otherwise unimplemented slots reachable. `gmPaperDollUI::UpdateAetheria @ 0x004A3E50` reads player int quality `0x142`
(`PropertyInt.AetheriaBitfield`, 322). `RecvNotice_PlayerDescReceived @ 0x004A43E0`
applies the login value, and `OnQualityChanged @ 0x004A4490` reapplies it after a
live quest-completion update:
```text
bits = player.intQuality(0x142, default=0)
blue.visible = (bits & 1) != 0
yellow.visible = (bits & 2) != 0
red.visible = (bits & 4) != 0
```
ACE independently confirms the property is `[SendOnLogin] AetheriaBitfield = 322`
and defines the same Blue=1, Yellow=2, Red=4 flags. The existing generic private
PropertyInt wire path (`0x02CD`) already stores the live value on the player object.
## Port mapping ## Port mapping
@ -94,7 +111,7 @@ does not make those otherwise unimplemented slots reachable.
- `ItemListCellTemplate.ResolvePrototypeEmptySprite` reuses the already-shipped - `ItemListCellTemplate.ResolvePrototypeEmptySprite` reuses the already-shipped
inheritance-aware `ItemSlot_Empty` resolver instead of hardcoding surface IDs in inheritance-aware `ItemSlot_Empty` resolver instead of hardcoding surface IDs in
the controller. the controller.
- `RetailUiRuntime` and UI Studio resolve the 21 surfaces from the live DAT once at - `RetailUiRuntime` and UI Studio resolve the 24 surfaces from the live DAT once at
bind time and inject them into the controller. A missing authored surface is a startup 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 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. for isolated controller bindings that do not supply the paperdoll catalog map.

View file

@ -83,6 +83,11 @@ public static class SampleData
ItemsCapacity = 102, ItemsCapacity = 102,
ContainersCapacity = 7, ContainersCapacity = 7,
}); });
// Show all three authored sigil locations in inventory/paperdoll previews.
t.UpdateIntProperty(
PlayerGuid,
AetheriaUnlocks.PropertyId,
(int)AetheriaUnlockState.All);
// ── Loose items in main pack (slots 05) ────────────────────────── // ── Loose items in main pack (slots 05) ──────────────────────────

View file

@ -7,7 +7,7 @@ using AcDream.Core.Selection;
namespace AcDream.App.UI.Layout; namespace AcDream.App.UI.Layout;
/// <summary> /// <summary>
/// Binds the ~21 equip slots mounted under the paperdoll (gmPaperDollUI 0x21000024, nested in the /// Binds the 24 equip slots mounted under the paperdoll (gmPaperDollUI 0x21000024, nested in the
/// inventory frame 0x21000023) to live equipped-item data and makes them drag-drop WIELD targets. /// inventory frame 0x21000023) to live equipped-item data and makes them drag-drop WIELD targets.
/// The acdream analogue of gmPaperDollUI::PostInit + GetLocationInfoFromElementID (named-retail decomp /// The acdream analogue of gmPaperDollUI::PostInit + GetLocationInfoFromElementID (named-retail decomp
/// 175480 / 173620). Slice 1: equip slots only — no 3D doll viewport (that's Slice 2). /// 175480 / 173620). Slice 1: equip slots only — no 3D doll viewport (that's Slice 2).
@ -22,8 +22,8 @@ public sealed class PaperdollController : IItemListDragHandler, IRetainedPanelCo
/// <summary> /// <summary>
/// The 9 armor-slot element-ids whose Visible state the Slots button (0x100005BE) toggles. /// The 9 armor-slot element-ids whose Visible state the Slots button (0x100005BE) toggles.
/// Doll-view: hidden. Slot-view: shown. Source: gmPaperDollUI::ListenToElementMessage decomp /// Doll-view: hidden. Slot-view: shown. Source: gmPaperDollUI::ListenToElementMessage decomp
/// 175674-175706 — these are the only 9 ids that element flips; the other ~12 equip slots /// 175674-175706 — these are the only 9 ids that element flips. The 12 ordinary non-armor
/// (jewelry, weapon, wrists, feet…) are NON-armor and remain visible in both views. /// lists remain visible in both views; the three Aetheria lists are independently unlock-gated.
/// </summary> /// </summary>
public static readonly uint[] ArmorSlotElementIds = public static readonly uint[] ArmorSlotElementIds =
{ {
@ -53,6 +53,7 @@ public sealed class PaperdollController : IItemListDragHandler, IRetainedPanelCo
private readonly SelectionState _selection; private readonly SelectionState _selection;
private readonly PaperdollClickMap? _clickMap; private readonly PaperdollClickMap? _clickMap;
private readonly List<(EquipMask Mask, UiItemList List)> _slots = new(); private readonly List<(EquipMask Mask, UiItemList List)> _slots = new();
private readonly List<(AetheriaUnlockState Bit, UiItemList List)> _aetheriaSlots = new();
// ── Slots-toggle state ──────────────────────────────────────────────────────────────────────── // ── Slots-toggle state ────────────────────────────────────────────────────────────────────────
private readonly PaperdollViewState _viewState = new(); private readonly PaperdollViewState _viewState = new();
@ -78,7 +79,7 @@ public sealed class PaperdollController : IItemListDragHandler, IRetainedPanelCo
for (int i = 0; i < PaperdollSlotBackgrounds.Definitions.Length; i++) for (int i = 0; i < PaperdollSlotBackgrounds.Definitions.Length; i++)
{ {
var (element, mask, _) = PaperdollSlotBackgrounds.Definitions[i]; var (element, mask, _, unlockBit) = PaperdollSlotBackgrounds.Definitions[i];
if (layout.FindElement(element) is not UiItemList list) continue; if (layout.FindElement(element) is not UiItemList list) continue;
list.RegisterDragHandler(this); list.RegisterDragHandler(this);
list.Cell.SourceKind = ItemDragSource.Equipment; list.Cell.SourceKind = ItemDragSource.Equipment;
@ -108,6 +109,8 @@ public sealed class PaperdollController : IItemListDragHandler, IRetainedPanelCo
// insert-arrow 0x060011F7) are already wired by DatWidgetFactory when it built the UiItemList; // insert-arrow 0x060011F7) are already wired by DatWidgetFactory when it built the UiItemList;
// no need to re-set them here. // no need to re-set them here.
_slots.Add((mask, list)); _slots.Add((mask, list));
if (unlockBit != AetheriaUnlockState.None)
_aetheriaSlots.Add((unlockBit, list));
} }
_objects.ObjectAdded += OnObjectChanged; _objects.ObjectAdded += OnObjectChanged;
@ -198,7 +201,13 @@ public sealed class PaperdollController : IItemListDragHandler, IRetainedPanelCo
_selection.Select(hitObject, SelectionChangeSource.Paperdoll); _selection.Select(hitObject, SelectionChangeSource.Paperdoll);
} }
private void OnObjectChanged(ClientObject o) { if (Concerns(o)) Populate(); } private void OnObjectChanged(ClientObject o)
{
if (o.ObjectId == _playerGuid())
ApplyAetheriaVisibility();
else if (Concerns(o))
Populate();
}
private void OnObjectMoved(ClientObject o, uint from, uint to) private void OnObjectMoved(ClientObject o, uint from, uint to)
{ if (Concerns(o) || from == _playerGuid() || to == _playerGuid()) Populate(); } { if (Concerns(o) || from == _playerGuid() || to == _playerGuid()) Populate(); }
private void OnSelectionChanged(SelectionTransition _) => ApplySelectionIndicators(); private void OnSelectionChanged(SelectionTransition _) => ApplySelectionIndicators();
@ -238,9 +247,22 @@ public sealed class PaperdollController : IItemListDragHandler, IRetainedPanelCo
worn.Type, worn.IconId, worn.IconUnderlayId, worn.IconOverlayId, worn.Effects) ?? 0u; worn.Type, worn.IconId, worn.IconUnderlayId, worn.IconOverlayId, worn.Effects) ?? 0u;
list.Cell.SetItem(worn.ObjectId, tex, dragIconTexture: dragTex); list.Cell.SetItem(worn.ObjectId, tex, dragIconTexture: dragTex);
} }
ApplyAetheriaVisibility();
ApplySelectionIndicators(); ApplySelectionIndicators();
} }
/// <summary>
/// Retail <c>gmPaperDollUI::UpdateAetheria @ 0x004A3E50</c>: property 0x142
/// bits 1/2/4 independently expose the blue/yellow/red sigil lists. Missing
/// player state is the locked state, matching PostInit before PlayerDescription.
/// </summary>
private void ApplyAetheriaVisibility()
{
AetheriaUnlockState unlocked = AetheriaUnlocks.Read(_objects.Get(_playerGuid()));
foreach (var (bit, list) in _aetheriaSlots)
list.Visible = (unlocked & bit) != AetheriaUnlockState.None;
}
private void ApplySelectionIndicators() private void ApplySelectionIndicators()
{ {
foreach (var (_, list) in _slots) foreach (var (_, list) in _slots)

View file

@ -20,7 +20,8 @@ public static class PaperdollSlotBackgrounds
internal readonly record struct Definition( internal readonly record struct Definition(
uint Element, uint Element,
EquipMask Mask, EquipMask Mask,
uint EmptyPrototype); uint EmptyPrototype,
AetheriaUnlockState UnlockBit = AetheriaUnlockState.None);
// WEAPON_READY_SLOT_LOC (acclient.h:3235): any wieldable weapon in the hand slot. // WEAPON_READY_SLOT_LOC (acclient.h:3235): any wieldable weapon in the hand slot.
private const EquipMask WeaponSlotMask = private const EquipMask WeaponSlotMask =
@ -49,6 +50,9 @@ public static class PaperdollSlotBackgrounds
new(0x100001DFu, WeaponSlotMask, 0x1000044Bu), new(0x100001DFu, WeaponSlotMask, 0x1000044Bu),
new(0x1000058Eu, EquipMask.TrinketOne, 0x1000058Fu), new(0x1000058Eu, EquipMask.TrinketOne, 0x1000058Fu),
new(0x100005E9u, EquipMask.Cloak, 0x100005EAu), new(0x100005E9u, EquipMask.Cloak, 0x100005EAu),
new(0x10000595u, EquipMask.SigilOne, 0x10000592u, AetheriaUnlockState.Blue),
new(0x10000596u, EquipMask.SigilTwo, 0x10000593u, AetheriaUnlockState.Yellow),
new(0x10000597u, EquipMask.SigilThree, 0x10000594u, AetheriaUnlockState.Red),
}; };
/// <summary>Resolves every supported slot's exact empty RenderSurface from the live DAT.</summary> /// <summary>Resolves every supported slot's exact empty RenderSurface from the live DAT.</summary>

View file

@ -0,0 +1,33 @@
using System;
namespace AcDream.Core.Items;
/// <summary>
/// Player <c>PropertyInt.AetheriaBitfield (322 / 0x142)</c>. Each bit independently
/// unlocks one paperdoll sigil location after its corresponding retail quest.
/// </summary>
/// <remarks>
/// Retail: <c>gmPaperDollUI::UpdateAetheria @ 0x004A3E50</c>.
/// ACE: <c>PropertyInt.AetheriaBitfield</c> + <c>AetheriaBitfield</c>.
/// </remarks>
[Flags]
public enum AetheriaUnlockState : uint
{
None = 0x0,
Blue = 0x1,
Yellow = 0x2,
Red = 0x4,
All = Blue | Yellow | Red,
}
public static class AetheriaUnlocks
{
public const uint PropertyId = 0x142u;
public static AetheriaUnlockState Read(ClientObject? player)
{
if (player?.Properties.Ints.TryGetValue(PropertyId, out int value) != true)
return AetheriaUnlockState.None;
return (AetheriaUnlockState)(uint)value;
}
}

View file

@ -17,12 +17,19 @@ public class PaperdollControllerTests
private const uint ShieldSlot = 0x100001E1u; // Shield 0x200000 private const uint ShieldSlot = 0x100001E1u; // Shield 0x200000
private const uint WeaponSlot = 0x100001DFu; // composite 0x3500000 private const uint WeaponSlot = 0x100001DFu; // composite 0x3500000
private const uint FingerLSlot= 0x100001DCu; // FingerWearLeft 0x40000 private const uint FingerLSlot= 0x100001DCu; // FingerWearLeft 0x40000
private const uint BlueAetheriaSlot = 0x10000595u;
private const uint YellowAetheriaSlot = 0x10000596u;
private const uint RedAetheriaSlot = 0x10000597u;
private sealed class RootElement : UiElement { } private sealed class RootElement : UiElement { }
private static (ImportedLayout layout, Dictionary<uint, UiItemList> lists) BuildLayout() private static (ImportedLayout layout, Dictionary<uint, UiItemList> lists) BuildLayout()
{ {
var ids = new[] { HeadSlot, ChestSlot, ChestArmorSlot, ShieldSlot, WeaponSlot, FingerLSlot }; var ids = new[]
{
HeadSlot, ChestSlot, ChestArmorSlot, ShieldSlot, WeaponSlot, FingerLSlot,
BlueAetheriaSlot, YellowAetheriaSlot, RedAetheriaSlot,
};
var lists = new Dictionary<uint, UiItemList>(); var lists = new Dictionary<uint, UiItemList>();
var byId = new Dictionary<uint, UiElement>(); var byId = new Dictionary<uint, UiElement>();
var root = new RootElement { Width = 224, Height = 214 }; var root = new RootElement { Width = 224, Height = 214 };
@ -241,6 +248,72 @@ public class PaperdollControllerTests
Assert.Equal(sprite, lists[element].Cell.EmptySprite); Assert.Equal(sprite, lists[element].Cell.EmptySprite);
} }
[Fact]
public void Aetheria_slots_default_hidden_without_unlock_property()
{
var (layout, lists) = BuildLayout();
Bind(layout, new ClientObjectTable());
Assert.False(lists[BlueAetheriaSlot].Visible);
Assert.False(lists[YellowAetheriaSlot].Visible);
Assert.False(lists[RedAetheriaSlot].Visible);
}
[Fact]
public void Aetheria_slots_follow_independent_player_unlock_bits_at_login()
{
var (layout, lists) = BuildLayout();
var objects = new ClientObjectTable();
objects.AddOrUpdate(new ClientObject { ObjectId = Player });
objects.UpdateIntProperty(
Player,
AetheriaUnlocks.PropertyId,
(int)(AetheriaUnlockState.Blue | AetheriaUnlockState.Red));
Bind(layout, objects);
Assert.True(lists[BlueAetheriaSlot].Visible);
Assert.False(lists[YellowAetheriaSlot].Visible);
Assert.True(lists[RedAetheriaSlot].Visible);
}
[Fact]
public void Live_aetheria_property_update_unlocks_new_slots()
{
var (layout, lists) = BuildLayout();
var objects = new ClientObjectTable();
objects.AddOrUpdate(new ClientObject { ObjectId = Player });
Bind(layout, objects);
objects.UpdateIntProperty(
Player,
AetheriaUnlocks.PropertyId,
(int)(AetheriaUnlockState.Blue | AetheriaUnlockState.Yellow));
Assert.True(lists[BlueAetheriaSlot].Visible);
Assert.True(lists[YellowAetheriaSlot].Visible);
Assert.False(lists[RedAetheriaSlot].Visible);
}
[Fact]
public void Unlocked_aetheria_slot_populates_its_equipped_sigil()
{
var (layout, lists) = BuildLayout();
var objects = new ClientObjectTable();
objects.AddOrUpdate(new ClientObject { ObjectId = Player });
objects.UpdateIntProperty(
Player,
AetheriaUnlocks.PropertyId,
(int)AetheriaUnlockState.Blue);
SeedEquipped(objects, 0xA37u, EquipMask.SigilOne);
Bind(layout, objects);
Assert.True(lists[BlueAetheriaSlot].Visible);
Assert.Equal(0xA37u, lists[BlueAetheriaSlot].Cell.ItemId);
}
[Fact] [Fact]
public void Live_player_wield_repaints_the_slot() // event-driven: ObjectMoved → Concerns → Populate public void Live_player_wield_repaints_the_slot() // event-driven: ObjectMoved → Concerns → Populate
{ {

View file

@ -24,6 +24,9 @@ public class PaperdollSlotBackgroundTests
{ 0x100001E3u, 0x1000044Fu }, // pants { 0x100001E3u, 0x1000044Fu }, // pants
{ 0x1000058Eu, 0x1000058Fu }, // trinket { 0x1000058Eu, 0x1000058Fu }, // trinket
{ 0x100005E9u, 0x100005EAu }, // cloak { 0x100005E9u, 0x100005EAu }, // cloak
{ 0x10000595u, 0x10000592u }, // blue Aetheria
{ 0x10000596u, 0x10000593u }, // yellow Aetheria
{ 0x10000597u, 0x10000594u }, // red Aetheria
{ 0x100005ABu, 0x100005B4u }, // head { 0x100005ABu, 0x100005B4u }, // head
{ 0x100005ACu, 0x100005B5u }, // chest armor { 0x100005ACu, 0x100005B5u }, // chest armor
{ 0x100005ADu, 0x100005B6u }, // abdomen armor { 0x100005ADu, 0x100005B6u }, // abdomen armor
@ -67,6 +70,9 @@ public class PaperdollSlotBackgroundTests
[0x100001E3u] = 0x060032C4u, [0x100001E3u] = 0x060032C4u,
[0x1000058Eu] = 0x06006A6Cu, [0x1000058Eu] = 0x06006A6Cu,
[0x100005E9u] = 0x0600708Fu, [0x100005E9u] = 0x0600708Fu,
[0x10000595u] = 0x06006BEFu,
[0x10000596u] = 0x06006BF0u,
[0x10000597u] = 0x06006BF1u,
[0x100005ABu] = 0x06006D7Fu, [0x100005ABu] = 0x06006D7Fu,
[0x100005ACu] = 0x06006D7Bu, [0x100005ACu] = 0x06006D7Bu,
[0x100005ADu] = 0x06006D79u, [0x100005ADu] = 0x06006D79u,

View file

@ -0,0 +1,29 @@
using AcDream.Core.Items;
using Xunit;
namespace AcDream.Core.Tests.Items;
public sealed class AetheriaUnlockStateTests
{
[Fact]
public void Missing_property_means_all_slots_locked()
{
var player = new ClientObject { ObjectId = 1 };
Assert.Equal(AetheriaUnlockState.None, AetheriaUnlocks.Read(player));
}
[Fact]
public void Property_322_preserves_the_three_independent_retail_bits()
{
var player = new ClientObject { ObjectId = 1 };
player.Properties.Ints[AetheriaUnlocks.PropertyId] =
(int)(AetheriaUnlockState.Blue | AetheriaUnlockState.Red);
AetheriaUnlockState actual = AetheriaUnlocks.Read(player);
Assert.True((actual & AetheriaUnlockState.Blue) != 0);
Assert.False((actual & AetheriaUnlockState.Yellow) != 0);
Assert.True((actual & AetheriaUnlockState.Red) != 0);
}
}