fix(D.5.2): effect tint = per-pixel tile copy (surface ReplaceColor overload)
Visual verification (Coldeve, Energy Crystal) showed acdream's Magical blue as a flat tint vs retail's gradient. Root cause: RenderIcons calls the SURFACE overload of SurfaceWindow::ReplaceColor (0x004415b0), which copies the textured effect tile pixel-by-pixel into the icon's pure-white pixels — not the flat color->color overload (0x00441530) I'd approximated with the tile's mean color. Port the surface overload exactly (dst[x,y]=src[x,y] where dst==white); confirmed via clean Ghidra decompile + named decomp. Retires AP-43 (mean-color approximation); IA-18 updated to the surface op. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
40c97a53ac
commit
fb288ad852
3 changed files with 79 additions and 59 deletions
|
|
@ -51,7 +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();
|
||||
private readonly Dictionary<uint, DecodedTexture> _effectTileByDid = new();
|
||||
|
||||
public IconComposer(DatCollection dats, TextureCache cache)
|
||||
{
|
||||
|
|
@ -127,46 +127,45 @@ public sealed class IconComposer
|
|||
}
|
||||
|
||||
/// <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.
|
||||
/// Retail <c>SurfaceWindow::ReplaceColor</c> SURFACE overload (0x004415b0): for every
|
||||
/// pixel in <paramref name="dst"/> 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 <paramref name="dst"/> in place.
|
||||
/// </summary>
|
||||
internal static void ReplaceColorWhite(byte[] rgba, int w, int h, (byte r, byte g, byte b, byte a) dest)
|
||||
internal static void ReplaceWhiteFromSurface(byte[] dst, int dw, int dh, byte[] src, int sw, int sh)
|
||||
{
|
||||
for (int i = 0; i < w * h; i++)
|
||||
for (int y = 0; y < dh; y++)
|
||||
for (int x = 0; x < dw; x++)
|
||||
{
|
||||
if (rgba[i * 4] == 255 && rgba[i * 4 + 1] == 255 &&
|
||||
rgba[i * 4 + 2] == 255 && rgba[i * 4 + 3] == 255)
|
||||
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)
|
||||
{
|
||||
rgba[i * 4] = dest.r; rgba[i * 4 + 1] = dest.g;
|
||||
rgba[i * 4 + 2] = dest.b; rgba[i * 4 + 3] = dest.a;
|
||||
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];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <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.
|
||||
/// The decoded effect tile for <paramref name="effects"/> (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.
|
||||
/// </summary>
|
||||
internal bool TryGetEffectColor(uint effects, out (byte r, byte g, byte b, byte a) color)
|
||||
internal bool TryGetEffectTile(uint effects, out DecodedTexture tile)
|
||||
{
|
||||
color = default;
|
||||
tile = null!;
|
||||
uint did = ResolveEffectDid(effects);
|
||||
if (did == 0) return false;
|
||||
if (_effectColorByDid.TryGetValue(did, out var cached)) { color = cached; return true; }
|
||||
if (_effectTileByDid.TryGetValue(did, out var cached)) { tile = 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;
|
||||
_effectTileByDid[did] = d;
|
||||
tile = d;
|
||||
return true;
|
||||
}
|
||||
|
||||
|
|
@ -235,12 +234,15 @@ public sealed class IconComposer
|
|||
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), so retail recolors
|
||||
// pure-white pixels to BLACK on mundane items and to the effect hue on magical
|
||||
// ones. Visually confirmed against retail 2026-06-17: the no-mana scroll's edges
|
||||
// are BLACK, not white — the earlier `effects != 0` gate (AP-44) was wrong.
|
||||
if (TryGetEffectColor(effects, out var ec))
|
||||
ReplaceColorWhite(composed.rgba, composed.w, composed.h, ec);
|
||||
// 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;
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue