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

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