feat(D.5.2): IconComposer effect-color + ReplaceColorWhite helpers
ReplaceColorWhite (retail SurfaceWindow::ReplaceColor 0x00441530): replaces only pure-white-opaque (RGBA 255,255,255,255) pixels in place. TryGetEffectColor: resolves the effect tile DID via ResolveEffectDid, decodes the RenderSurface, and returns the mean-opaque RGB as the tint color (divergence DR-2: exact retail color byte is decompiler-ambiguous). TryDecode: shared RenderSurface decode helper for the effect path. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
75ac51ac23
commit
3e019e408a
2 changed files with 74 additions and 0 deletions
|
|
@ -51,6 +51,7 @@ public sealed class IconComposer
|
|||
private EnumIDMap? _effectSubMap;
|
||||
private bool _effectResolveTried;
|
||||
private readonly Dictionary<uint, uint> _effectDidByIndex = new();
|
||||
private readonly Dictionary<uint, (byte r, byte g, byte b, byte a)> _effectColorByDid = new();
|
||||
|
||||
public IconComposer(DatCollection dats, TextureCache cache)
|
||||
{
|
||||
|
|
@ -125,6 +126,61 @@ public sealed class IconComposer
|
|||
if (_dats.Portal.TryGet<EnumIDMap>(subDid, out var sub)) _effectSubMap = sub;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Retail <c>SurfaceWindow::ReplaceColor</c> (0x00441530) with the icon-composite's
|
||||
/// fixed source color: replace pixels exactly equal to pure-white-opaque
|
||||
/// (RGBAColor(1,1,1,1) → 0xFFFFFFFF) with <paramref name="dest"/>. Mutates in place.
|
||||
/// </summary>
|
||||
internal static void ReplaceColorWhite(byte[] rgba, int w, int h, (byte r, byte g, byte b, byte a) dest)
|
||||
{
|
||||
for (int i = 0; i < w * h; i++)
|
||||
{
|
||||
if (rgba[i * 4] == 255 && rgba[i * 4 + 1] == 255 &&
|
||||
rgba[i * 4 + 2] == 255 && rgba[i * 4 + 3] == 255)
|
||||
{
|
||||
rgba[i * 4] = dest.r; rgba[i * 4 + 1] = dest.g;
|
||||
rgba[i * 4 + 2] = dest.b; rgba[i * 4 + 3] = dest.a;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The effect tint color for <paramref name="effects"/>: the effect tile's mean-opaque
|
||||
/// color (blue=Magical, green=Poisoned, …). The exact retail color byte is a
|
||||
/// decompiler-ambiguous SurfaceWindow-header read; the tile IS the per-effect color, so
|
||||
/// its representative color is the faithful equivalent (divergence DR-2). Cached per DID.
|
||||
/// </summary>
|
||||
private bool TryGetEffectColor(uint effects, out (byte r, byte g, byte b, byte a) color)
|
||||
{
|
||||
color = default;
|
||||
uint did = ResolveEffectDid(effects);
|
||||
if (did == 0) return false;
|
||||
if (_effectColorByDid.TryGetValue(did, out var cached)) { color = cached; return true; }
|
||||
if (!TryDecode(did, out var d)) return false;
|
||||
long sr = 0, sg = 0, sb = 0; int n = 0;
|
||||
for (int i = 0; i < d.Width * d.Height; i++)
|
||||
{
|
||||
if (d.Rgba8[i * 4 + 3] == 0) continue;
|
||||
sr += d.Rgba8[i * 4]; sg += d.Rgba8[i * 4 + 1]; sb += d.Rgba8[i * 4 + 2]; n++;
|
||||
}
|
||||
if (n == 0) return false;
|
||||
var rep = ((byte)(sr / n), (byte)(sg / n), (byte)(sb / n), (byte)255);
|
||||
_effectColorByDid[did] = rep;
|
||||
color = rep;
|
||||
return true;
|
||||
}
|
||||
|
||||
private bool TryDecode(uint renderSurfaceId, out DecodedTexture decoded)
|
||||
{
|
||||
decoded = null!;
|
||||
if (renderSurfaceId == 0) return false;
|
||||
if (!_dats.Portal.TryGet<RenderSurface>(renderSurfaceId, out var rs) &&
|
||||
!_dats.HighRes.TryGet<RenderSurface>(renderSurfaceId, out rs))
|
||||
return false;
|
||||
decoded = SurfaceDecoder.DecodeRenderSurface(rs, palette: null);
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <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
|
||||
/// top-left aligned (all icon layers are 32x32 in practice).</summary>
|
||||
|
|
|
|||
|
|
@ -126,4 +126,22 @@ public class IconComposerTests
|
|||
Assert.Equal(0x060011C5u, composer.ResolveEffectDid(0x1000u));
|
||||
Assert.Equal(0x060011C5u, composer.ResolveEffectDid(0x0000u));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ReplaceColorWhite_replacesOnlyPureWhiteOpaque()
|
||||
{
|
||||
// 2x2: [white-opaque, red-opaque, white-transparent, white-opaque]
|
||||
var px = new byte[]
|
||||
{
|
||||
255,255,255,255, // pure white opaque → replaced
|
||||
255, 0, 0,255, // red → untouched
|
||||
255,255,255, 0, // white but alpha 0 → untouched (not 0xFFFFFFFF)
|
||||
255,255,255,255, // pure white opaque → replaced
|
||||
};
|
||||
IconComposer.ReplaceColorWhite(px, 2, 2, (10, 20, 30, 255));
|
||||
Assert.Equal(new byte[] { 10, 20, 30, 255 }, px[0..4]); // replaced
|
||||
Assert.Equal(new byte[] { 255, 0, 0, 255 }, px[4..8]); // untouched
|
||||
Assert.Equal(new byte[] { 255, 255, 255, 0 }, px[8..12]); // untouched
|
||||
Assert.Equal(new byte[] { 10, 20, 30, 255 }, px[12..16]); // replaced
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue