using System; using System.Collections.Generic; using System.Numerics; using AcDream.App.Rendering; using AcDream.Core.Items; using AcDream.Core.Textures; using DatReaderWriter; using DatReaderWriter.DBObjs; namespace AcDream.App.UI; /// /// Builds an item icon by alpha-compositing its RenderSurface layers into one 32x32 /// texture, mirroring retail IconData::RenderIcons (decomp 407524) and /// DBCache::GetDIDFromEnum (0x413940). Each layer is a 0x06 RenderSurface decoded /// DIRECTLY (the D.2b RenderSurface-vs-Surface rule). /// /// Layer order (bottom → top), matching retail: /// 1. type-default underlay (OPAQUE backing; resolved via EnumIDMap 0x10000004 from /// the portal MasterMap) — /// 2. item custom underlay (e.g. "magic" tint strip) /// 3. base icon /// 4. item custom overlay (e.g. "enchanted" sparkle) /// /// The type-default underlay is the key to non-transparent filled slots: because it /// is fully opaque and is layer 0, sizes the output to it and /// the alpha-over pass fills every pixel. The overlay ReplaceColor tint and the effect /// overlay (RenderIcons 407546) remain out of scope (paperdoll phase). /// /// Composited textures are cached by their (typeUnderlay, underlay, base, overlay) tuple. /// public sealed class IconComposer { private readonly DatCollection _dats; private readonly TextureCache _cache; private readonly Dictionary<(uint, uint, uint, uint, uint), uint> _byTuple = new(); // ── type-default underlay resolve (EnumIDMap 0x10000004) ───────────────── // Portal MasterMap (0x25000000) maps enum 0x10000004 → submap DID (0x25000008). // Submap maps index → 0x06 RenderSurface DID. index = LSB(itemType)+1, or 0x21. // Refs: IconData::RenderIcons 0058d214–0058d22c; DBCache::GetDIDFromEnum 0x413940. private EnumIDMap? _underlaySubMap; private bool _underlayResolveTried; private readonly Dictionary _underlayDidByIndex = new(); // ── effect overlay resolve (EnumIDMap 0x10000005) ──────────────────────── // Portal MasterMap (0x25000000) maps enum 0x10000005 → submap DID (0x25000009). // Submap maps index → 0x06 RenderSurface DID. index = LSB(effects)+1, fallback 0x21. // Refs: IconData::RenderIcons 0x0058d180 (effect path); the effect tile is a // ReplaceColor tint SOURCE, not a blit layer (see RESOLVED doc, divergence DR-1). private EnumIDMap? _effectSubMap; private bool _effectResolveTried; private readonly Dictionary _effectDidByIndex = new(); private readonly Dictionary _effectTileByDid = new(); public IconComposer(DatCollection dats, TextureCache cache) { _dats = dats; _cache = cache; } /// /// Resolve the type-default underlay DID for via the /// two-level EnumIDMap chain (retail: IconData::RenderIcons 0058d214–0058d22c + /// DBCache::GetDIDFromEnum 0x413940). /// /// index = LowestSetBit(itemType) + 1, or 0x21 when itemType has no bits set. /// /// NOTE: retail RenderIcons (407546) has a special paperdoll IsThePlayer case /// that uses GetDIDByEnum(0x10000004, 7) + TYPE_CONTAINER for the player doll — that /// path is out of scope here (paperdoll phase). /// internal uint ResolveUnderlayDid(ItemType itemType) { uint raw = (uint)itemType; int lsb = raw == 0 ? -1 : BitOperations.TrailingZeroCount(raw); uint index = lsb < 0 ? 0x21u : (uint)(lsb + 1); if (_underlayDidByIndex.TryGetValue(index, out var cached)) return cached; EnsureUnderlaySubMap(); uint did = 0; if (_underlaySubMap is { } sub && sub.ClientEnumToID.TryGetValue(index, out var d)) did = d; _underlayDidByIndex[index] = did; return did; } private void EnsureUnderlaySubMap() { if (_underlayResolveTried) return; _underlayResolveTried = true; uint masterDid = (uint)_dats.Portal.Header.MasterMapId; // = 0x25000000 if (masterDid == 0) return; if (!_dats.Portal.TryGet(masterDid, out var master)) return; if (!master.ClientEnumToID.TryGetValue(0x10000004u, out var subDid)) return; // → 0x25000008 if (_dats.Portal.TryGet(subDid, out var sub)) _underlaySubMap = sub; } /// /// Resolve the effect-overlay DID for via the EnumIDMap /// 0x10000005 chain. index = LowestSetBit(effects)+1; if the entry is missing/zero, /// retail falls back to index 0x21 (the solid-black tile). NOTE: the effect path has /// NO lsb==-1 pre-check (unlike the type underlay), so effects==0 → index 0 → miss → /// fallback. (Retail IconData::RenderIcons 0x0058d180.) /// internal uint ResolveEffectDid(uint effects) { int lsb = effects == 0 ? -1 : BitOperations.TrailingZeroCount(effects); uint index = (uint)(lsb + 1); if (_effectDidByIndex.TryGetValue(index, out var cached)) return cached; EnsureEffectSubMap(); uint did = 0; if (_effectSubMap is { } sub && sub.ClientEnumToID.TryGetValue(index, out var d)) did = d; if (did == 0 && _effectSubMap is { } sub2 && sub2.ClientEnumToID.TryGetValue(0x21u, out var fb)) did = fb; _effectDidByIndex[index] = did; return did; } private void EnsureEffectSubMap() { if (_effectResolveTried) return; _effectResolveTried = true; uint masterDid = (uint)_dats.Portal.Header.MasterMapId; // = 0x25000000 if (masterDid == 0) return; if (!_dats.Portal.TryGet(masterDid, out var master)) return; if (!master.ClientEnumToID.TryGetValue(0x10000005u, out var subDid)) return; // → 0x25000009 if (_dats.Portal.TryGet(subDid, out var sub)) _effectSubMap = sub; } /// /// Retail SurfaceWindow::ReplaceColor SURFACE overload (0x004415b0): for every /// pixel in that equals pure-white-opaque (RGBAColor(1,1,1,1) → /// 0xFFFFFFFF), copy the SAME (x,y) pixel from the source effect tile. This preserves /// the effect tile's texture/gradient (NOT a flat color). Retail requires the source to /// cover the dest (it does — both are 32x32); out-of-range pixels are left unchanged. /// Mutates in place. /// internal static void ReplaceWhiteFromSurface(byte[] dst, int dw, int dh, byte[] src, int sw, int sh) { for (int y = 0; y < dh; y++) for (int x = 0; x < dw; x++) { int di = (y * dw + x) * 4; if (dst[di] == 255 && dst[di + 1] == 255 && dst[di + 2] == 255 && dst[di + 3] == 255 && x < sw && y < sh) { int si = (y * sw + x) * 4; dst[di] = src[si]; dst[di + 1] = src[si + 1]; dst[di + 2] = src[si + 2]; dst[di + 3] = src[si + 3]; } } } /// /// The decoded effect tile for (enum 0x10000005). The tile is /// a 32x32 textured RenderSurface whose pixels ARE the per-effect coloring (blue=Magical, /// green=Poisoned, …; the 0x21 fallback is solid black). Retail copies it per-pixel into /// the icon's white pixels (gradient), so we need the whole tile, not a representative /// color. Cached per DID. /// internal bool TryGetEffectTile(uint effects, out DecodedTexture tile) { tile = null!; uint did = ResolveEffectDid(effects); if (did == 0) return false; if (_effectTileByDid.TryGetValue(did, out var cached)) { tile = cached; return true; } if (!TryDecode(did, out var d)) return false; _effectTileByDid[did] = d; tile = d; return true; } private bool TryDecode(uint renderSurfaceId, out DecodedTexture decoded) { decoded = null!; if (renderSurfaceId == 0) return false; if (!_dats.Portal.TryGet(renderSurfaceId, out var rs) && !_dats.HighRes.TryGet(renderSurfaceId, out rs)) return false; decoded = SurfaceDecoder.DecodeRenderSurface(rs, palette: null); return true; } /// Pure alpha-over composite, bottom->top. Layers may differ in size; /// the result is sized to the FIRST (bottom) layer and upper layers are sampled /// top-left aligned (all icon layers are 32x32 in practice). public static (byte[] rgba, int w, int h) Compose(IReadOnlyList<(byte[] rgba, int w, int h)> layers) { if (layers.Count == 0) return (Array.Empty(), 0, 0); var (baseRgba, w, h) = layers[0]; var outp = (byte[])baseRgba.Clone(); for (int li = 1; li < layers.Count; li++) { var (src, sw, sh) = layers[li]; int cw = Math.Min(w, sw), ch = Math.Min(h, sh); for (int y = 0; y < ch; y++) for (int x = 0; x < cw; x++) { int di = (y * w + x) * 4, si = (y * sw + x) * 4; float sa = src[si + 3] / 255f; if (sa <= 0f) continue; float da = 1f - sa; outp[di] = (byte)(src[si] * sa + outp[di] * da); outp[di + 1] = (byte)(src[si + 1] * sa + outp[di + 1] * da); outp[di + 2] = (byte)(src[si + 2] * sa + outp[di + 2] * da); outp[di + 3] = (byte)Math.Min(255f, src[si + 3] + outp[di + 3] * da); } } return (outp, w, h); } /// /// Resolve (and cache) the composited GL texture for an item's icon state. /// Returns 0 if no base icon. Mirrors retail IconData::RenderIcons (0x0058d180): /// a DRAG composite (base + custom overlay + effect recolor) blitted over the /// type-default underlay + custom underlay. The effect tile (enum 0x10000005) is a /// ReplaceColor tint SOURCE, not a blit layer (DR-1). The recolor runs for ALL items: /// effects==0 resolves to the 0x21 solid-black fallback tile, so pure-white pixels become /// black (matching retail); magical items take the per-effect hue instead. /// public uint GetIcon(ItemType itemType, uint iconId, uint underlayId, uint overlayId, uint effects) { if (iconId == 0) return 0; uint typeUnderlayDid = ResolveUnderlayDid(itemType); var key = (typeUnderlayDid, iconId, underlayId, overlayId, effects); if (_byTuple.TryGetValue(key, out var tex)) return tex; // Stage 1 — retail m_pDragIcon: base + custom overlay, then the effect recolor. var dragLayers = new List<(byte[] rgba, int w, int h)>(); AddLayer(dragLayers, iconId); AddLayer(dragLayers, overlayId); (byte[] rgba, int w, int h)? drag = null; if (dragLayers.Count > 0) { var composed = Compose(dragLayers); // Effect recolor — ALWAYS, matching retail IconData::RenderIcons (0x0058d180): // the effect tile (enum 0x10000005, lsb(effects)+1, fallback 0x21) is non-null // even for effects==0 (the 0x21 SOLID-BLACK tile 0x060011C5). Retail's RenderIcons // calls the SURFACE overload of SurfaceWindow::ReplaceColor (0x004415b0), copying // the textured effect tile per-pixel into the icon's pure-white pixels — so // magical items take the tile's GRADIENT hue and mundane items go solid black. // (Visually confirmed against retail 2026-06-17: the Energy Crystal's blue is a // gradient, not a flat tint, and the no-mana scroll's edges are black.) if (TryGetEffectTile(effects, out var tile)) ReplaceWhiteFromSurface(composed.rgba, composed.w, composed.h, tile.Rgba8, tile.Width, tile.Height); drag = composed; } // Stage 2 — retail m_pIcon: type-default underlay (opaque) + custom underlay + drag. var layers = new List<(byte[] rgba, int w, int h)>(); AddLayer(layers, typeUnderlayDid); AddLayer(layers, underlayId); if (drag is { } d) layers.Add(d); if (layers.Count == 0) return 0; var (rgba, w, h) = Compose(layers); uint handle = _cache.UploadRgba8(rgba, w, h, nearest: true); _byTuple[key] = handle; return handle; } private void AddLayer(List<(byte[], int, int)> layers, uint renderSurfaceId) { if (renderSurfaceId == 0) return; if (!_dats.Portal.TryGet(renderSurfaceId, out var rs) && !_dats.HighRes.TryGet(renderSurfaceId, out rs)) return; var decoded = SurfaceDecoder.DecodeRenderSurface(rs, palette: null); layers.Add((decoded.Rgba8, decoded.Width, decoded.Height)); } }