using System.Collections.Generic; using DatReaderWriter; using DatReaderWriter.DBObjs; using DatReaderWriter.Types; namespace AcDream.App.UI.Layout; /// /// 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. /// public static class ItemListCellTemplate { /// 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). 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 /// /// Resolve the empty-slot sprite (a 0x06xxxxxx RenderSurface id) for the cells of item-list /// element in LayoutDesc . /// Returns 0 when the layout/element/attribute/prototype/media is absent (the caller keeps /// the default). /// public static uint ResolveEmptySprite(DatCollection dats, uint listLayoutId, uint listElementId) { var listLd = dats.Get(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(CatalogLayoutId); if (catalog is null) return 0; var proto = FindDesc(catalog, protoId); if (proto is null) return 0; // The empty-slot BACKGROUND is the m_elem_Icon's ItemSlot_Empty media, resolved THROUGH // BaseElement inheritance: the 36x36 container prototype (0x1000033F) nests its icon behind // an inner wrapper (0x10000340 -> base 0x1000033E -> 0x1000033B -> 0x06000F6E), so a raw // child search alone returns nothing for containers. We deliberately do NOT use the // prototype's DirectState child overlay: on the container prototype that child is the // open/selected-container TRIANGLE indicator (0x06005D9C), which retail draws ONLY on the // selected container (a deferred container-selection feature) — never as empty-cell art. // (Live visual gate 2026-06-22: frame-first stamped the triangle onto every empty cell.) return FindIconEmpty(catalog, proto, new HashSet()); } // ── 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: the m_elem_Icon (0x1000033B) ItemSlot_Empty, resolved through inheritance ── // Find the icon child's empty media within `element`'s subtree, following BaseElement edges // within the same catalog (cycle-guarded by `baseSeen`). The 32x32 contents prototype carries // 0x1000033B as a direct child; the 36x36 container prototype reaches it only via an inherited // base (0x10000340 -> 0x1000033E), so a raw child search alone returns nothing for containers. private static uint FindIconEmpty(LayoutDesc catalog, ElementDesc element, HashSet baseSeen) { if (element.ElementId == IconChildId) { uint e = IconEmptyState(element); if (e != 0) return e; } foreach (var kv in element.Children) { uint e = FindIconEmpty(catalog, kv.Value, baseSeen); if (e != 0) return e; } if (element.BaseElement != 0 && element.BaseLayoutId == CatalogLayoutId && baseSeen.Add(element.BaseElement)) { var baseDesc = FindDesc(catalog, element.BaseElement); if (baseDesc is not null) { uint e = FindIconEmpty(catalog, baseDesc, baseSeen); if (e != 0) return e; } } return 0; } private static uint IconEmptyState(ElementDesc icon) { 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 icons 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; } }