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
|
|
@ -128,7 +128,7 @@ public class IconComposerTests
|
|||
}
|
||||
|
||||
[Fact]
|
||||
public void TryGetEffectColor_noEffect_resolvesToBlackFallback()
|
||||
public void TryGetEffectTile_noEffectBlack_magicalTextured()
|
||||
{
|
||||
var datDir = ResolveDatDir();
|
||||
if (datDir is null) return; // dats absent (CI) — skip cleanly
|
||||
|
|
@ -136,43 +136,62 @@ public class IconComposerTests
|
|||
using var dats = new DatCollection(datDir, DatAccessType.Read);
|
||||
var composer = new IconComposer(dats, null!);
|
||||
|
||||
// effects==0 resolves to the 0x21 solid-black fallback tile (0x060011C5), so the
|
||||
// ALWAYS-on recolor blackens an icon's pure-white edge pixels on mundane items —
|
||||
// retail-faithful (the no-mana scroll's edges are BLACK, not white). Confirmed
|
||||
// visually against retail 2026-06-17.
|
||||
Assert.True(composer.TryGetEffectColor(0u, out var c));
|
||||
Assert.True(c.r <= 8 && c.g <= 8 && c.b <= 8, $"expected ~black, got ({c.r},{c.g},{c.b})");
|
||||
Assert.Equal(255, c.a);
|
||||
// effects==0 → 0x21 fallback → 0x060011C5, a 32x32 SOLID-BLACK tile. Copying it
|
||||
// per-pixel blackens an icon's pure-white pixels (retail-faithful no-mana scroll edge).
|
||||
Assert.True(composer.TryGetEffectTile(0u, out var black));
|
||||
Assert.Equal(32, black.Width);
|
||||
Assert.Equal(32, black.Height);
|
||||
Assert.True(black.Rgba8[0] <= 8 && black.Rgba8[1] <= 8 && black.Rgba8[2] <= 8);
|
||||
|
||||
// Magical (0x1) → 0x060011CA, a TEXTURED blue tile (NOT a flat color) — this is the
|
||||
// gradient retail copies per-pixel into the icon's white pixels (the Energy Crystal
|
||||
// blue). Assert the tile is non-uniform so a future flat-color regression fails here.
|
||||
Assert.True(composer.TryGetEffectTile(0x1u, out var magic));
|
||||
bool uniform = true;
|
||||
for (int i = 4; i < magic.Width * magic.Height * 4 && uniform; i += 4)
|
||||
if (magic.Rgba8[i] != magic.Rgba8[0] || magic.Rgba8[i + 1] != magic.Rgba8[1] ||
|
||||
magic.Rgba8[i + 2] != magic.Rgba8[2])
|
||||
uniform = false;
|
||||
Assert.False(uniform); // textured → gradient, not flat
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ReplaceColorWhite_replacesOnlyPureWhiteOpaque()
|
||||
public void ReplaceWhiteFromSurface_copiesSourcePixelForPureWhiteOpaque()
|
||||
{
|
||||
// 2x2: [white-opaque, red-opaque, white-transparent, white-opaque]
|
||||
var px = new byte[]
|
||||
// 2x2 dest: [white-opaque, red-opaque, white-transparent, white-opaque]
|
||||
var dst = new byte[]
|
||||
{
|
||||
255,255,255,255, // pure white opaque → replaced
|
||||
255,255,255,255, // pure white opaque → takes src(0,0)
|
||||
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
|
||||
255,255,255,255, // pure white opaque → takes src(1,1)
|
||||
};
|
||||
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
|
||||
// 2x2 src — distinct per-pixel colors (a "gradient").
|
||||
var src = new byte[]
|
||||
{
|
||||
10, 20, 30,255, // (0,0)
|
||||
40, 50, 60,255, // (1,0)
|
||||
70, 80, 90,255, // (0,1)
|
||||
100,110,120,255, // (1,1)
|
||||
};
|
||||
IconComposer.ReplaceWhiteFromSurface(dst, 2, 2, src, 2, 2);
|
||||
Assert.Equal(new byte[] { 10, 20, 30, 255 }, dst[0..4]); // copied src(0,0)
|
||||
Assert.Equal(new byte[] { 255, 0, 0, 255 }, dst[4..8]); // untouched (not white)
|
||||
Assert.Equal(new byte[] { 255, 255, 255, 0 }, dst[8..12]); // untouched (transparent)
|
||||
Assert.Equal(new byte[] { 100, 110, 120, 255 }, dst[12..16]); // copied src(1,1) — per-pixel
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TwoStageWithEffect_recolorsWhiteBeforeUnderlay()
|
||||
public void TwoStageWithEffect_copiesTilePixelBeforeUnderlay()
|
||||
{
|
||||
// drag = base (white pixel) over overlay (none); recolor white→blue; then over
|
||||
// an opaque tawny underlay. The white pixel must become blue in the final.
|
||||
// drag = base (white pixel); copy the effect tile's pixel into the white; then over
|
||||
// an opaque tawny underlay. The white pixel must become the tile's pixel in the final.
|
||||
var baseIcon = (new byte[] { 255,255,255,255 }, 1, 1); // 1x1 white opaque
|
||||
var drag = IconComposer.Compose(new[] { baseIcon });
|
||||
IconComposer.ReplaceColorWhite(drag.rgba, drag.w, drag.h, (0, 0, 255, 255)); // blue
|
||||
var tile = new byte[] { 0, 0, 255, 255 }; // 1x1 blue tile pixel
|
||||
IconComposer.ReplaceWhiteFromSurface(drag.rgba, drag.w, drag.h, tile, 1, 1);
|
||||
var underlay = (new byte[] { 105, 70, 50, 255 }, 1, 1); // tawny opaque
|
||||
var final = IconComposer.Compose(new[] { underlay, (drag.rgba, drag.w, drag.h) });
|
||||
Assert.Equal(new byte[] { 0, 0, 255, 255 }, final.rgba); // blue on top
|
||||
Assert.Equal(new byte[] { 0, 0, 255, 255 }, final.rgba); // tile pixel on top
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue