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

@ -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 05) ──────────────────────────

View file

@ -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)

View file

@ -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>

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;
}
}