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:
parent
1be18cc4db
commit
edc9be3008
11 changed files with 208 additions and 16 deletions
|
|
@ -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
|
||||
|
||||
**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
|
||||
**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`.
|
||||
|
||||
**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`.
|
||||
|
||||
**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.
|
||||
|
||||
---
|
||||
|
||||
|
|
|
|||
|
|
@ -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-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-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-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` |
|
||||
|
|
|
|||
|
|
@ -482,6 +482,7 @@ behavior. Estimated 17–26 days focused work, 3–5 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 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) ✅, 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 — 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.
|
||||
|
|
|
|||
|
|
@ -2,8 +2,8 @@
|
|||
|
||||
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
|
||||
Scope: the empty-cell art for all 24 equipment lists in `gmPaperDollUI`, including
|
||||
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
|
||||
base, but retail creates their cells from distinct `UIElement_UIItem` prototypes in
|
||||
the shared catalog `0x21000037`.
|
||||
|
|
@ -72,6 +72,9 @@ UIItem empty draw:
|
|||
| `0x100001E3` | UpperLegWear | `0x1000044F` | `0x060032C4` |
|
||||
| `0x1000058E` | TrinketOne | `0x1000058F` | `0x06006A6C` |
|
||||
| `0x100005E9` | Cloak | `0x100005EA` | `0x0600708F` |
|
||||
| `0x10000595` | SigilOne / blue | `0x10000592` | `0x06006BEF` |
|
||||
| `0x10000596` | SigilTwo / yellow | `0x10000593` | `0x06006BF0` |
|
||||
| `0x10000597` | SigilThree / red | `0x10000594` | `0x06006BF1` |
|
||||
| `0x100005AB` | HeadWear | `0x100005B4` | `0x06006D7F` |
|
||||
| `0x100005AC` | ChestArmor | `0x100005B5` | `0x06006D7B` |
|
||||
| `0x100005AD` | AbdomenArmor | `0x100005B6` | `0x06006D79` |
|
||||
|
|
@ -82,9 +85,23 @@ UIItem empty draw:
|
|||
| `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.
|
||||
## Aetheria unlock visibility
|
||||
|
||||
`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
|
||||
|
||||
|
|
@ -94,7 +111,7 @@ does not make those otherwise unimplemented slots reachable.
|
|||
- `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
|
||||
- `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
|
||||
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.
|
||||
|
|
|
|||
|
|
@ -83,6 +83,11 @@ public static class SampleData
|
|||
ItemsCapacity = 102,
|
||||
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 0–5) ──────────────────────────
|
||||
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ using AcDream.Core.Selection;
|
|||
namespace AcDream.App.UI.Layout;
|
||||
|
||||
/// <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.
|
||||
/// 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).
|
||||
|
|
@ -22,8 +22,8 @@ public sealed class PaperdollController : IItemListDragHandler, IRetainedPanelCo
|
|||
/// <summary>
|
||||
/// The 9 armor-slot element-ids whose Visible state the Slots button (0x100005BE) toggles.
|
||||
/// 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
|
||||
/// (jewelry, weapon, wrists, feet…) are NON-armor and remain visible in both views.
|
||||
/// 175674-175706 — these are the only 9 ids that element flips. The 12 ordinary non-armor
|
||||
/// lists remain visible in both views; the three Aetheria lists are independently unlock-gated.
|
||||
/// </summary>
|
||||
public static readonly uint[] ArmorSlotElementIds =
|
||||
{
|
||||
|
|
@ -53,6 +53,7 @@ public sealed class PaperdollController : IItemListDragHandler, IRetainedPanelCo
|
|||
private readonly SelectionState _selection;
|
||||
private readonly PaperdollClickMap? _clickMap;
|
||||
private readonly List<(EquipMask Mask, UiItemList List)> _slots = new();
|
||||
private readonly List<(AetheriaUnlockState Bit, UiItemList List)> _aetheriaSlots = new();
|
||||
|
||||
// ── Slots-toggle state ────────────────────────────────────────────────────────────────────────
|
||||
private readonly PaperdollViewState _viewState = new();
|
||||
|
|
@ -78,7 +79,7 @@ public sealed class PaperdollController : IItemListDragHandler, IRetainedPanelCo
|
|||
|
||||
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;
|
||||
list.RegisterDragHandler(this);
|
||||
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;
|
||||
// no need to re-set them here.
|
||||
_slots.Add((mask, list));
|
||||
if (unlockBit != AetheriaUnlockState.None)
|
||||
_aetheriaSlots.Add((unlockBit, list));
|
||||
}
|
||||
|
||||
_objects.ObjectAdded += OnObjectChanged;
|
||||
|
|
@ -198,7 +201,13 @@ public sealed class PaperdollController : IItemListDragHandler, IRetainedPanelCo
|
|||
_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)
|
||||
{ if (Concerns(o) || from == _playerGuid() || to == _playerGuid()) Populate(); }
|
||||
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;
|
||||
list.Cell.SetItem(worn.ObjectId, tex, dragIconTexture: dragTex);
|
||||
}
|
||||
ApplyAetheriaVisibility();
|
||||
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()
|
||||
{
|
||||
foreach (var (_, list) in _slots)
|
||||
|
|
|
|||
|
|
@ -20,7 +20,8 @@ public static class PaperdollSlotBackgrounds
|
|||
internal readonly record struct Definition(
|
||||
uint Element,
|
||||
EquipMask Mask,
|
||||
uint EmptyPrototype);
|
||||
uint EmptyPrototype,
|
||||
AetheriaUnlockState UnlockBit = AetheriaUnlockState.None);
|
||||
|
||||
// WEAPON_READY_SLOT_LOC (acclient.h:3235): any wieldable weapon in the hand slot.
|
||||
private const EquipMask WeaponSlotMask =
|
||||
|
|
@ -49,6 +50,9 @@ public static class PaperdollSlotBackgrounds
|
|||
new(0x100001DFu, WeaponSlotMask, 0x1000044Bu),
|
||||
new(0x1000058Eu, EquipMask.TrinketOne, 0x1000058Fu),
|
||||
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>
|
||||
|
|
|
|||
33
src/AcDream.Core/Items/AetheriaUnlockState.cs
Normal file
33
src/AcDream.Core/Items/AetheriaUnlockState.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
|
|
@ -17,12 +17,19 @@ public class PaperdollControllerTests
|
|||
private const uint ShieldSlot = 0x100001E1u; // Shield 0x200000
|
||||
private const uint WeaponSlot = 0x100001DFu; // composite 0x3500000
|
||||
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 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 byId = new Dictionary<uint, UiElement>();
|
||||
var root = new RootElement { Width = 224, Height = 214 };
|
||||
|
|
@ -241,6 +248,72 @@ public class PaperdollControllerTests
|
|||
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]
|
||||
public void Live_player_wield_repaints_the_slot() // event-driven: ObjectMoved → Concerns → Populate
|
||||
{
|
||||
|
|
|
|||
|
|
@ -24,6 +24,9 @@ public class PaperdollSlotBackgroundTests
|
|||
{ 0x100001E3u, 0x1000044Fu }, // pants
|
||||
{ 0x1000058Eu, 0x1000058Fu }, // trinket
|
||||
{ 0x100005E9u, 0x100005EAu }, // cloak
|
||||
{ 0x10000595u, 0x10000592u }, // blue Aetheria
|
||||
{ 0x10000596u, 0x10000593u }, // yellow Aetheria
|
||||
{ 0x10000597u, 0x10000594u }, // red Aetheria
|
||||
{ 0x100005ABu, 0x100005B4u }, // head
|
||||
{ 0x100005ACu, 0x100005B5u }, // chest armor
|
||||
{ 0x100005ADu, 0x100005B6u }, // abdomen armor
|
||||
|
|
@ -67,6 +70,9 @@ public class PaperdollSlotBackgroundTests
|
|||
[0x100001E3u] = 0x060032C4u,
|
||||
[0x1000058Eu] = 0x06006A6Cu,
|
||||
[0x100005E9u] = 0x0600708Fu,
|
||||
[0x10000595u] = 0x06006BEFu,
|
||||
[0x10000596u] = 0x06006BF0u,
|
||||
[0x10000597u] = 0x06006BF1u,
|
||||
[0x100005ABu] = 0x06006D7Fu,
|
||||
[0x100005ACu] = 0x06006D7Bu,
|
||||
[0x100005ADu] = 0x06006D79u,
|
||||
|
|
|
|||
29
tests/AcDream.Core.Tests/Items/AetheriaUnlockStateTests.cs
Normal file
29
tests/AcDream.Core.Tests/Items/AetheriaUnlockStateTests.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue