diff --git a/src/AcDream.App/UI/IconComposer.cs b/src/AcDream.App/UI/IconComposer.cs index fc2c87aa..516b8e9b 100644 --- a/src/AcDream.App/UI/IconComposer.cs +++ b/src/AcDream.App/UI/IconComposer.cs @@ -43,6 +43,15 @@ public sealed class IconComposer 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(); + public IconComposer(DatCollection dats, TextureCache cache) { _dats = dats; @@ -84,6 +93,38 @@ public sealed class IconComposer 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; + } + /// 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). diff --git a/tests/AcDream.App.Tests/UI/IconComposerTests.cs b/tests/AcDream.App.Tests/UI/IconComposerTests.cs index 06a225e5..4a19ed83 100644 --- a/tests/AcDream.App.Tests/UI/IconComposerTests.cs +++ b/tests/AcDream.App.Tests/UI/IconComposerTests.cs @@ -101,4 +101,29 @@ public class IconComposerTests Assert.Equal(0x060011D5u, composer.ResolveUnderlayDid(ItemType.Jewelry)); Assert.Equal(0x060011D4u, composer.ResolveUnderlayDid(ItemType.None)); } + + [Fact] + public void ResolveEffectDid_goldenValues_matchDat() + { + var datDir = ResolveDatDir(); + if (datDir is null) return; // dats absent (CI) — skip cleanly + + using var dats = new DatCollection(datDir, DatAccessType.Read); + var composer = new IconComposer(dats, null!); + + // Golden values (live dat, MasterMap 0x25000000 → effect submap 0x25000009; + // index = LowestSetBit(UiEffects)+1, fallback 0x21): + // Magical (0x0001) → idx 1 → 0x060011CA + // Poisoned (0x0002) → idx 2 → 0x060011C6 + // BoostHealth (0x0004) → idx 3 → 0x06001B05 + // BoostStamina (0x0010) → idx 5 → 0x06001B06 + // Nether (0x1000) → idx 13 (absent) → fallback 0x21 → 0x060011C5 + // none (0x0000) → idx 0 (zero) → fallback 0x21 → 0x060011C5 + Assert.Equal(0x060011CAu, composer.ResolveEffectDid(0x0001u)); + Assert.Equal(0x060011C6u, composer.ResolveEffectDid(0x0002u)); + Assert.Equal(0x06001B05u, composer.ResolveEffectDid(0x0004u)); + Assert.Equal(0x06001B06u, composer.ResolveEffectDid(0x0010u)); + Assert.Equal(0x060011C5u, composer.ResolveEffectDid(0x1000u)); + Assert.Equal(0x060011C5u, composer.ResolveEffectDid(0x0000u)); + } }