feat(ui): D.2b empty-slot art — ItemListCellTemplate resolver (0x1000000e -> 0x21000037)
Ports UIElement_ItemList::InternalCreateItem (0x004e3570) attribute lookup to a testable static helper. The 0x1000000e property is an EnumBaseProperty (not DataIdBaseProperty as the spec anticipated) — discovered by runtime reflection and confirmed against the live dat. Pinned sprite ids from real-dat test: contents (0x100001C6): 0x06004D20 side-bag (0x100001CA): 0x06005D9C main-pack (0x100001C9): 0x06005D9C Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
69ad2cf12d
commit
b8626401ad
2 changed files with 197 additions and 0 deletions
138
src/AcDream.App/UI/Layout/ItemListCellTemplate.cs
Normal file
138
src/AcDream.App/UI/Layout/ItemListCellTemplate.cs
Normal file
|
|
@ -0,0 +1,138 @@
|
|||
using DatReaderWriter;
|
||||
using DatReaderWriter.DBObjs;
|
||||
using DatReaderWriter.Types;
|
||||
|
||||
namespace AcDream.App.UI.Layout;
|
||||
|
||||
/// <summary>
|
||||
/// Resolves the empty-slot background sprite for a UIElement_ItemList's cells the way retail
|
||||
/// does. Retail ref: UIElement_ItemList::InternalCreateItem (acclient_2013_pseudo_c.txt:231486 /
|
||||
/// 0x004e3570): GetAttribute_Enum(this, 0x1000000e, &id) -> GetByEnum(0x10000038,5,0x23) [the
|
||||
/// shared UIItem catalog LayoutDesc, = 0x21000037] -> CreateChildElement(catalog, id); the cloned
|
||||
/// prototype's ItemSlot_Empty (state 0x1000001c) media is the empty-cell background.
|
||||
/// </summary>
|
||||
public static class ItemListCellTemplate
|
||||
{
|
||||
/// <summary>The shared UIItem cell-template catalog. Hardcoded: retail resolves it via
|
||||
/// GetByEnum(0x10000038,5,0x23) through a master enum-map DAT object (no code literal);
|
||||
/// 0x21000037 is the only LayoutDesc that is a catalog of standalone UIItem (0x10000032)
|
||||
/// prototypes. Validated by the real-dat test (the resolved sprite must be a real 0x06 surface).</summary>
|
||||
public const uint CatalogLayoutId = 0x21000037u;
|
||||
|
||||
private const uint CellTemplateAttr = 0x1000000eu; // UIElement attribute: cell-template element id
|
||||
private const uint IconChildId = 0x1000033Bu; // m_elem_Icon sub-element (carries ItemSlot_Empty)
|
||||
private const string ItemSlotEmpty = "ItemSlot_Empty"; // UIStateId.ToString() for state 0x1000001c
|
||||
|
||||
/// <summary>
|
||||
/// Resolve the empty-slot sprite (a 0x06xxxxxx RenderSurface id) for the cells of item-list
|
||||
/// element <paramref name="listElementId"/> in LayoutDesc <paramref name="listLayoutId"/>.
|
||||
/// Returns 0 when the layout/element/attribute/prototype/media is absent (the caller keeps
|
||||
/// the <see cref="UiItemSlot"/> default).
|
||||
/// </summary>
|
||||
public static uint ResolveEmptySprite(DatCollection dats, uint listLayoutId, uint listElementId)
|
||||
{
|
||||
var listLd = dats.Get<LayoutDesc>(listLayoutId);
|
||||
if (listLd is null) return 0;
|
||||
var listElem = FindDesc(listLd, listElementId);
|
||||
if (listElem is null) return 0;
|
||||
|
||||
uint protoId = ReadCellTemplateId(listElem); // attr 0x1000000e
|
||||
if (protoId == 0) return 0;
|
||||
|
||||
var catalog = dats.Get<LayoutDesc>(CatalogLayoutId);
|
||||
if (catalog is null) return 0;
|
||||
var proto = FindDesc(catalog, protoId);
|
||||
if (proto is null) return 0;
|
||||
|
||||
// Priority: a child's DirectState background frame (the 36x36 backpack cell's recessed
|
||||
// border, e.g. 0x06005D9C); else the m_elem_Icon child's ItemSlot_Empty media (the 32x32
|
||||
// contents cell, e.g. 0x06004D20). See the spec's explicit single-sprite contract.
|
||||
uint frame = FirstChildDirectStateImage(proto);
|
||||
return frame != 0 ? frame : IconChildEmptyImage(proto);
|
||||
}
|
||||
|
||||
// ── attribute 0x1000000e: stored as EnumBaseProperty in the dat (Value is the prototype id) ──
|
||||
// Note: the spec anticipated DataIdBaseProperty/ArrayBaseProperty based on the font-DID pattern,
|
||||
// but the live dat uses EnumBaseProperty.Value (uint) — confirmed by runtime reflection.
|
||||
private static uint ReadCellTemplateId(ElementDesc elem)
|
||||
{
|
||||
uint id = ReadIdFromState(elem.StateDesc);
|
||||
if (id != 0) return id;
|
||||
foreach (var s in elem.States)
|
||||
{
|
||||
id = ReadIdFromState(s.Value);
|
||||
if (id != 0) return id;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
private static uint ReadIdFromState(StateDesc? sd)
|
||||
{
|
||||
if (sd?.Properties is null) return 0;
|
||||
return sd.Properties.TryGetValue(CellTemplateAttr, out var raw) ? ReadId(raw) : 0;
|
||||
}
|
||||
|
||||
private static uint ReadId(BaseProperty raw)
|
||||
{
|
||||
if (raw is ArrayBaseProperty arr && arr.Value.Count > 0) return ReadId(arr.Value[0]);
|
||||
if (raw is DataIdBaseProperty did) return did.Value;
|
||||
if (raw is EnumBaseProperty ep) return ep.Value; // 0x1000000e is EnumBaseProperty in the live dat
|
||||
return 0;
|
||||
}
|
||||
|
||||
// ── prototype media ──
|
||||
private static uint FirstChildDirectStateImage(ElementDesc proto)
|
||||
{
|
||||
foreach (var kv in proto.Children)
|
||||
{
|
||||
uint f = DirectStateImage(kv.Value);
|
||||
if (f != 0) return f;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
private static uint IconChildEmptyImage(ElementDesc proto)
|
||||
{
|
||||
var icon = FindDescIn(proto, IconChildId); // recursive: handles inner wrappers (e.g. 0x10000340)
|
||||
if (icon is null) return 0;
|
||||
foreach (var s in icon.States)
|
||||
if (s.Key.ToString() == ItemSlotEmpty)
|
||||
{
|
||||
uint f = StateImage(s.Value);
|
||||
if (f != 0) return f;
|
||||
}
|
||||
return DirectStateImage(icon); // some prototypes carry empty media on the DirectState
|
||||
}
|
||||
|
||||
private static uint DirectStateImage(ElementDesc d)
|
||||
=> d.StateDesc is null ? 0u : StateImage(d.StateDesc);
|
||||
|
||||
private static uint StateImage(StateDesc sd)
|
||||
{
|
||||
foreach (var m in sd.Media)
|
||||
if (m is MediaDescImage img && img.File != 0) return img.File;
|
||||
return 0;
|
||||
}
|
||||
|
||||
// ── depth-first element search by id (LayoutImporter.FindDesc is private there) ──
|
||||
private static ElementDesc? FindDesc(LayoutDesc ld, uint id)
|
||||
{
|
||||
foreach (var kv in ld.Elements)
|
||||
{
|
||||
var f = FindDescIn(kv.Value, id);
|
||||
if (f is not null) return f;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static ElementDesc? FindDescIn(ElementDesc d, uint id)
|
||||
{
|
||||
if (d.ElementId == id) return d;
|
||||
foreach (var kv in d.Children)
|
||||
{
|
||||
var f = FindDescIn(kv.Value, id);
|
||||
if (f is not null) return f;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue