feat(D.5.2): IconComposer.ResolveEffectDid (effect submap 0x10000005)
Add effect-overlay submap resolve: EnsureEffectSubMap walks the portal MasterMap (0x25000000) → EnumIDMap 0x10000005 → submap 0x25000009; ResolveEffectDid(effects) maps LowestSetBit(effects)+1 → RenderSurface DID with fallback to index 0x21. Golden test validates all 6 cases (Magical/Poisoned/BoostHealth/BoostStamina/Nether/zero) against the live dat. Retail ref: IconData::RenderIcons 0x0058d180. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
e7b6e83cf8
commit
75ac51ac23
2 changed files with 66 additions and 0 deletions
|
|
@ -43,6 +43,15 @@ public sealed class IconComposer
|
||||||
private bool _underlayResolveTried;
|
private bool _underlayResolveTried;
|
||||||
private readonly Dictionary<uint, uint> _underlayDidByIndex = new();
|
private readonly Dictionary<uint, uint> _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<uint, uint> _effectDidByIndex = new();
|
||||||
|
|
||||||
public IconComposer(DatCollection dats, TextureCache cache)
|
public IconComposer(DatCollection dats, TextureCache cache)
|
||||||
{
|
{
|
||||||
_dats = dats;
|
_dats = dats;
|
||||||
|
|
@ -84,6 +93,38 @@ public sealed class IconComposer
|
||||||
if (_dats.Portal.TryGet<EnumIDMap>(subDid, out var sub)) _underlaySubMap = sub;
|
if (_dats.Portal.TryGet<EnumIDMap>(subDid, out var sub)) _underlaySubMap = sub;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Resolve the effect-overlay DID for <paramref name="effects"/> 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.)
|
||||||
|
/// </summary>
|
||||||
|
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<EnumIDMap>(masterDid, out var master)) return;
|
||||||
|
if (!master.ClientEnumToID.TryGetValue(0x10000005u, out var subDid)) return; // → 0x25000009
|
||||||
|
if (_dats.Portal.TryGet<EnumIDMap>(subDid, out var sub)) _effectSubMap = sub;
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>Pure alpha-over composite, bottom->top. Layers may differ in size;
|
/// <summary>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
|
/// the result is sized to the FIRST (bottom) layer and upper layers are sampled
|
||||||
/// top-left aligned (all icon layers are 32x32 in practice).</summary>
|
/// top-left aligned (all icon layers are 32x32 in practice).</summary>
|
||||||
|
|
|
||||||
|
|
@ -101,4 +101,29 @@ public class IconComposerTests
|
||||||
Assert.Equal(0x060011D5u, composer.ResolveUnderlayDid(ItemType.Jewelry));
|
Assert.Equal(0x060011D5u, composer.ResolveUnderlayDid(ItemType.Jewelry));
|
||||||
Assert.Equal(0x060011D4u, composer.ResolveUnderlayDid(ItemType.None));
|
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));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue