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:
Erik 2026-07-13 09:43:21 +02:00
parent 6ba4148b24
commit 1be18cc4db
11 changed files with 335 additions and 52 deletions

View file

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

View file

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

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