fix(paperdoll): restore authored slot backgrounds
Port the retail UIItem catalog path so every supported equipment location resolves its exact empty-state silhouette from live DAT. Share one definition table between equip behavior and presentation, wire runtime plus UI Studio, pin all 21 prototypes and surfaces, and retire AP-66. Co-Authored-By: Codex <codex@openai.com>
This commit is contained in:
parent
6ba4148b24
commit
1be18cc4db
11 changed files with 335 additions and 52 deletions
|
|
@ -136,6 +136,7 @@ public static class FixtureProvider
|
|||
uint contentsEmpty = ItemListCellTemplate.ResolveEmptySprite(dats, 0x21000021u, 0x100001C6u);
|
||||
uint sideBagEmpty = ItemListCellTemplate.ResolveEmptySprite(dats, 0x21000022u, 0x100001CAu);
|
||||
uint mainPackEmpty = ItemListCellTemplate.ResolveEmptySprite(dats, 0x21000022u, 0x100001C9u);
|
||||
var paperdollEmpty = PaperdollSlotBackgrounds.ResolveEmptySprites(dats);
|
||||
|
||||
var iconIds = MakeIconIds(stack);
|
||||
var selection = new AcDream.Core.Selection.SelectionState();
|
||||
|
|
@ -152,9 +153,7 @@ public static class FixtureProvider
|
|||
sideBagEmptySprite: sideBagEmpty,
|
||||
mainPackEmptySprite: mainPackEmpty);
|
||||
|
||||
// Bind the paperdoll equip slots (same imported subtree as the inventory).
|
||||
// Mirrors GameWindow:2257-2265: PaperdollController.Bind with contentsEmpty as
|
||||
// emptySlotSprite (each slot shows the same square-frame placeholder as the grid).
|
||||
// Bind the paperdoll equip slots with their authored per-location UIItem prototypes.
|
||||
PaperdollController.Bind(
|
||||
layout,
|
||||
objects,
|
||||
|
|
@ -163,7 +162,8 @@ public static class FixtureProvider
|
|||
selection: selection,
|
||||
sendWield: null, // no live session in the studio
|
||||
emptySlotSprite: contentsEmpty,
|
||||
datFont: stack.VitalsDatFont);
|
||||
datFont: stack.VitalsDatFont,
|
||||
emptySlotSprites: paperdollEmpty);
|
||||
break;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -40,9 +40,21 @@ public static class ItemListCellTemplate
|
|||
uint protoId = ReadCellTemplateId(listElem); // attr 0x1000000e
|
||||
if (protoId == 0) return 0;
|
||||
|
||||
return ResolvePrototypeEmptySprite(dats, protoId);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Resolves <c>ItemSlot_Empty</c> directly from an authored UIItem prototype in
|
||||
/// <see cref="CatalogLayoutId"/>. Paperdoll lists select distinct prototypes for their
|
||||
/// jewelry, weapon, clothing, and armor locations even though the lists share one base.
|
||||
/// </summary>
|
||||
public static uint ResolvePrototypeEmptySprite(DatCollection dats, uint prototypeElementId)
|
||||
{
|
||||
if (prototypeElementId == 0) return 0;
|
||||
|
||||
var catalog = dats.Get<LayoutDesc>(CatalogLayoutId);
|
||||
if (catalog is null) return 0;
|
||||
var proto = FindDesc(catalog, protoId);
|
||||
var proto = FindDesc(catalog, prototypeElementId);
|
||||
if (proto is null) return 0;
|
||||
|
||||
// The empty-slot BACKGROUND is the m_elem_Icon's ItemSlot_Empty media, resolved THROUGH
|
||||
|
|
|
|||
|
|
@ -44,37 +44,6 @@ public sealed class PaperdollController : IItemListDragHandler, IRetainedPanelCo
|
|||
public void Toggle() => SlotView = !SlotView;
|
||||
}
|
||||
|
||||
// WEAPON_READY_SLOT_LOC (acclient.h:3235): the weapon-hand doll slot accepts any wieldable weapon.
|
||||
private const EquipMask WeaponSlotMask =
|
||||
EquipMask.MeleeWeapon | EquipMask.MissileWeapon | EquipMask.Held | EquipMask.TwoHanded;
|
||||
|
||||
// element-id → EquipMask (verified: dump paperdoll-0x21000024.txt ↔ deep-dive §3a ↔ acclient.h:3193).
|
||||
// The 3 Aetheria sigil slots (0x10000595/96/97) are SetVisible(0) in retail — skipped (Slice 1 scope).
|
||||
private static readonly (uint Element, EquipMask Mask)[] SlotMap =
|
||||
{
|
||||
(0x100005ABu, EquipMask.HeadWear), // 0x1
|
||||
(0x100001E2u, EquipMask.ChestWear), // 0x2
|
||||
(0x100001E3u, EquipMask.UpperLegWear), // 0x40
|
||||
(0x100005B0u, EquipMask.HandWear), // 0x20
|
||||
(0x100005B3u, EquipMask.FootWear), // 0x100
|
||||
(0x100005ACu, EquipMask.ChestArmor), // 0x200
|
||||
(0x100005ADu, EquipMask.AbdomenArmor), // 0x400
|
||||
(0x100005AEu, EquipMask.UpperArmArmor), // 0x800
|
||||
(0x100005AFu, EquipMask.LowerArmArmor), // 0x1000
|
||||
(0x100005B1u, EquipMask.UpperLegArmor), // 0x2000
|
||||
(0x100005B2u, EquipMask.LowerLegArmor), // 0x4000
|
||||
(0x100001DAu, EquipMask.NeckWear), // 0x8000
|
||||
(0x100001DBu, EquipMask.WristWearLeft), // 0x10000
|
||||
(0x100001DDu, EquipMask.WristWearRight), // 0x20000
|
||||
(0x100001DCu, EquipMask.FingerWearLeft), // 0x40000
|
||||
(0x100001DEu, EquipMask.FingerWearRight), // 0x80000
|
||||
(0x100001E1u, EquipMask.Shield), // 0x200000
|
||||
(0x100001E0u, EquipMask.MissileAmmo), // 0x800000 (LIKELY — gate-verify, AP row)
|
||||
(0x100001DFu, WeaponSlotMask), // 0x3500000 composite
|
||||
(0x1000058Eu, EquipMask.TrinketOne), // 0x4000000
|
||||
(0x100005E9u, EquipMask.Cloak), // 0x8000000
|
||||
};
|
||||
|
||||
private readonly ClientObjectTable _objects;
|
||||
private readonly Func<uint> _playerGuid;
|
||||
private readonly Func<ItemType, uint, uint, uint, uint, uint> _iconIds;
|
||||
|
|
@ -98,7 +67,8 @@ public sealed class PaperdollController : IItemListDragHandler, IRetainedPanelCo
|
|||
Action<uint, uint>? sendWield,
|
||||
uint emptySlotSprite, UiDatFont? datFont, ItemInteractionController? itemInteraction,
|
||||
PaperdollClickMap? clickMap,
|
||||
Func<ItemType, uint, uint, uint, uint, uint>? dragIconIds)
|
||||
Func<ItemType, uint, uint, uint, uint, uint>? dragIconIds,
|
||||
IReadOnlyDictionary<uint, uint>? emptySlotSprites)
|
||||
{
|
||||
_objects = objects; _playerGuid = playerGuid; _iconIds = iconIds; _sendWield = sendWield;
|
||||
_dragIconIds = dragIconIds;
|
||||
|
|
@ -106,14 +76,17 @@ public sealed class PaperdollController : IItemListDragHandler, IRetainedPanelCo
|
|||
_selection = selection ?? throw new ArgumentNullException(nameof(selection));
|
||||
_clickMap = clickMap;
|
||||
|
||||
for (int i = 0; i < SlotMap.Length; i++)
|
||||
for (int i = 0; i < PaperdollSlotBackgrounds.Definitions.Length; i++)
|
||||
{
|
||||
var (element, mask) = SlotMap[i];
|
||||
var (element, mask, _) = PaperdollSlotBackgrounds.Definitions[i];
|
||||
if (layout.FindElement(element) is not UiItemList list) continue;
|
||||
list.RegisterDragHandler(this);
|
||||
list.Cell.SourceKind = ItemDragSource.Equipment;
|
||||
list.Cell.SlotIndex = i; // SlotMap position = this equipped item's drag-payload SourceSlot when dragged out to unwield
|
||||
list.Cell.EmptySprite = emptySlotSprite; // visible empty-slot frame so every position is seen + usable. The live
|
||||
list.Cell.SlotIndex = i; // definition position = equipped drag-payload SourceSlot
|
||||
list.Cell.EmptySprite = emptySlotSprites is not null
|
||||
&& emptySlotSprites.TryGetValue(element, out uint authoredSprite)
|
||||
? authoredSprite
|
||||
: emptySlotSprite;
|
||||
list.Cell.Clicked = () =>
|
||||
{
|
||||
uint itemId = list.Cell.ItemId;
|
||||
|
|
@ -199,10 +172,11 @@ public sealed class PaperdollController : IItemListDragHandler, IRetainedPanelCo
|
|||
uint emptySlotSprite = 0u, UiDatFont? datFont = null,
|
||||
ItemInteractionController? itemInteraction = null,
|
||||
PaperdollClickMap? clickMap = null,
|
||||
Func<ItemType, uint, uint, uint, uint, uint>? dragIconIds = null)
|
||||
Func<ItemType, uint, uint, uint, uint, uint>? dragIconIds = null,
|
||||
IReadOnlyDictionary<uint, uint>? emptySlotSprites = null)
|
||||
=> new PaperdollController(
|
||||
layout, objects, playerGuid, iconIds, selection, sendWield, emptySlotSprite,
|
||||
datFont, itemInteraction, clickMap, dragIconIds);
|
||||
datFont, itemInteraction, clickMap, dragIconIds, emptySlotSprites);
|
||||
|
||||
private void HandleDollClick(int x, int y)
|
||||
{
|
||||
|
|
|
|||
86
src/AcDream.App/UI/Layout/PaperdollSlotBackgrounds.cs
Normal file
86
src/AcDream.App/UI/Layout/PaperdollSlotBackgrounds.cs
Normal file
|
|
@ -0,0 +1,86 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using AcDream.Core.Items;
|
||||
using DatReaderWriter;
|
||||
|
||||
namespace AcDream.App.UI.Layout;
|
||||
|
||||
/// <summary>
|
||||
/// Retail paperdoll slot definitions: one source of truth for the paperdoll element,
|
||||
/// accepted <see cref="EquipMask"/>, and authored UIItem prototype whose
|
||||
/// <c>ItemSlot_Empty</c> state supplies that location's background.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Element-to-location: <c>gmPaperDollUI::GetLocationInfoFromElementID @ 0x004A37F0</c>.
|
||||
/// Cell creation: <c>UIElement_ItemList::InternalCreateItem @ 0x004E3570</c>.
|
||||
/// Prototype media: live DAT catalog <c>LayoutDesc 0x21000037</c>.
|
||||
/// </remarks>
|
||||
public static class PaperdollSlotBackgrounds
|
||||
{
|
||||
internal readonly record struct Definition(
|
||||
uint Element,
|
||||
EquipMask Mask,
|
||||
uint EmptyPrototype);
|
||||
|
||||
// WEAPON_READY_SLOT_LOC (acclient.h:3235): any wieldable weapon in the hand slot.
|
||||
private const EquipMask WeaponSlotMask =
|
||||
EquipMask.MeleeWeapon | EquipMask.MissileWeapon | EquipMask.Held | EquipMask.TwoHanded;
|
||||
|
||||
internal static readonly Definition[] Definitions =
|
||||
{
|
||||
new(0x100005ABu, EquipMask.HeadWear, 0x100005B4u),
|
||||
new(0x100001E2u, EquipMask.ChestWear, 0x1000044Eu),
|
||||
new(0x100001E3u, EquipMask.UpperLegWear, 0x1000044Fu),
|
||||
new(0x100005B0u, EquipMask.HandWear, 0x100005B9u),
|
||||
new(0x100005B3u, EquipMask.FootWear, 0x100005BDu),
|
||||
new(0x100005ACu, EquipMask.ChestArmor, 0x100005B5u),
|
||||
new(0x100005ADu, EquipMask.AbdomenArmor, 0x100005B6u),
|
||||
new(0x100005AEu, EquipMask.UpperArmArmor, 0x100005B7u),
|
||||
new(0x100005AFu, EquipMask.LowerArmArmor, 0x100005B8u),
|
||||
new(0x100005B1u, EquipMask.UpperLegArmor, 0x100005BAu),
|
||||
new(0x100005B2u, EquipMask.LowerLegArmor, 0x100005BBu),
|
||||
new(0x100001DAu, EquipMask.NeckWear, 0x10000446u),
|
||||
new(0x100001DBu, EquipMask.WristWearLeft, 0x10000447u),
|
||||
new(0x100001DDu, EquipMask.WristWearRight, 0x10000449u),
|
||||
new(0x100001DCu, EquipMask.FingerWearLeft, 0x10000448u),
|
||||
new(0x100001DEu, EquipMask.FingerWearRight, 0x1000044Au),
|
||||
new(0x100001E1u, EquipMask.Shield, 0x1000044Du),
|
||||
new(0x100001E0u, EquipMask.MissileAmmo, 0x1000044Cu),
|
||||
new(0x100001DFu, WeaponSlotMask, 0x1000044Bu),
|
||||
new(0x1000058Eu, EquipMask.TrinketOne, 0x1000058Fu),
|
||||
new(0x100005E9u, EquipMask.Cloak, 0x100005EAu),
|
||||
};
|
||||
|
||||
/// <summary>Resolves every supported slot's exact empty RenderSurface from the live DAT.</summary>
|
||||
public static IReadOnlyDictionary<uint, uint> ResolveEmptySprites(DatCollection dats)
|
||||
{
|
||||
var result = new Dictionary<uint, uint>(Definitions.Length);
|
||||
foreach (Definition definition in Definitions)
|
||||
{
|
||||
uint sprite = ItemListCellTemplate.ResolvePrototypeEmptySprite(
|
||||
dats,
|
||||
definition.EmptyPrototype);
|
||||
if (sprite == 0)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"Retail paperdoll UIItem prototype 0x{definition.EmptyPrototype:X8} " +
|
||||
$"for slot 0x{definition.Element:X8} has no ItemSlot_Empty surface.");
|
||||
}
|
||||
result.Add(definition.Element, sprite);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>Exposes the authored prototype identity for dat-free conformance tests.</summary>
|
||||
public static bool TryGetPrototype(uint slotElementId, out uint prototypeElementId)
|
||||
{
|
||||
foreach (Definition definition in Definitions)
|
||||
{
|
||||
if (definition.Element != slotElementId) continue;
|
||||
prototypeElementId = definition.EmptyPrototype;
|
||||
return true;
|
||||
}
|
||||
prototypeElementId = 0;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
|
@ -691,12 +691,14 @@ public sealed class RetailUiRuntime : IDisposable
|
|||
});
|
||||
|
||||
uint contents, sideBag, mainPack;
|
||||
IReadOnlyDictionary<uint, uint> paperdollEmptySprites;
|
||||
PaperdollClickMap? paperdollClickMap;
|
||||
lock (_bindings.Assets.DatLock)
|
||||
{
|
||||
contents = ItemListCellTemplate.ResolveEmptySprite(_bindings.Assets.Dats, 0x21000021u, 0x100001C6u);
|
||||
sideBag = ItemListCellTemplate.ResolveEmptySprite(_bindings.Assets.Dats, 0x21000022u, 0x100001CAu);
|
||||
mainPack = ItemListCellTemplate.ResolveEmptySprite(_bindings.Assets.Dats, 0x21000022u, 0x100001C9u);
|
||||
paperdollEmptySprites = PaperdollSlotBackgrounds.ResolveEmptySprites(_bindings.Assets.Dats);
|
||||
paperdollClickMap = PaperdollClickMap.Load(_bindings.Assets.Dats);
|
||||
}
|
||||
|
||||
|
|
@ -716,7 +718,7 @@ public sealed class RetailUiRuntime : IDisposable
|
|||
PaperdollController paperdoll = PaperdollController.Bind(
|
||||
layout, b.Objects, b.PlayerGuid, b.ResolveIcon, b.Selection, b.SendWield,
|
||||
contents, _bindings.Assets.DefaultFont, b.ItemInteraction, paperdollClickMap,
|
||||
b.ResolveDragIcon);
|
||||
b.ResolveDragIcon, paperdollEmptySprites);
|
||||
Host.WindowManager.AttachController(
|
||||
WindowNames.Inventory,
|
||||
new RetainedPanelControllerGroup(inventory, paperdoll));
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue