fix(plugin-ui): plugin did icons use retail's keyed-white recolor, not a raw blit

Owner report 2026-09-06: MossTank's shelf icon (0x06002C41) drew with a white
ring. DAT icon art reserves pure-white-opaque pixels as the recolor key that
retail IconData::RenderIcons (0x0058d180) replaces per pixel through
SurfaceWindow::ReplaceColor (0x004415b0) from the effect tile — the solid-black
0x21 tile when there are no effects. The inventory already does this through
IconComposer; the plugin did sink (markup <icon did>, <button icon>, <list icons>
and the shelf button) blitted the art raw.

RetailMarkupIconResolver.ResolveDid now hands out IconComposer.GetKeyedIcon —
the drag-icon composite (base art + effects==0 recolor, no overlay, no
underlay), sharing that cache — so did icons look like a mundane inventory item
does. The resolver no longer needs a TextureCache. KeyedIconInstalledDatTests
pins both halves against the real DAT: the raw art carries the key, the
composite carries none, and ResolveDid returns exactly the keyed composite.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
Erik 2026-09-06 17:40:39 +02:00
parent f2d7562c86
commit e61edd946b
8 changed files with 137 additions and 19 deletions

View file

@ -66,7 +66,6 @@ public interface IMarkupIconResolver
public sealed class RetailMarkupIconResolver : IMarkupIconResolver
{
private readonly IDatReaderWriter _dats;
private readonly TextureCache _textureCache;
private readonly IconComposer _icons;
private readonly ClientObjectTable _objects;
@ -114,12 +113,10 @@ public sealed class RetailMarkupIconResolver : IMarkupIconResolver
public RetailMarkupIconResolver(
IDatReaderWriter dats,
TextureCache textureCache,
IconComposer icons,
ClientObjectTable objects)
{
_dats = dats ?? throw new ArgumentNullException(nameof(dats));
_textureCache = textureCache ?? throw new ArgumentNullException(nameof(textureCache));
_icons = icons ?? throw new ArgumentNullException(nameof(icons));
_objects = objects ?? throw new ArgumentNullException(nameof(objects));
}
@ -137,12 +134,14 @@ public sealed class RetailMarkupIconResolver : IMarkupIconResolver
/// (<c>DatDatabaseWrapper.TryGet</c>'s <c>_databaseLock</c>) — the same
/// synchronization <see cref="IconComposer.TryDecode"/> relies on for
/// every one of its own DAT reads — so no additional lock is taken here.
/// Requests the NEAREST sampler (Slice B finding 6): plugin icon art is
/// pixel-exact 32x32 DAT art, the same convention every other icon in the
/// client draws with (dat-font glyphs, composited spell/item icons), and
/// <see cref="TextureCache"/> keys its render-surface cache by
/// <c>(id, nearest)</c> so this never collides with a chrome sprite's
/// linear sampling of the same id.
/// A resolvable id is NOT blitted raw: it goes through
/// <see cref="IconComposer.GetKeyedIcon"/> — retail's <c>RenderIcons</c>
/// composite with no overlay and no effects, which replaces the art's
/// pure-white keyed pixels with the effects==0 solid-black tile. Item,
/// spell and component art reserve pure white as that key, so a raw blit
/// shows a white ring (owner report 2026-09-06, the MossTank shelf icon);
/// the composed texture is uploaded nearest-sampled by the composer, the
/// same convention every other icon in the client draws with.
/// </summary>
public (uint tex, int w, int h) ResolveDid(uint did)
{
@ -161,9 +160,8 @@ public sealed class RetailMarkupIconResolver : IMarkupIconResolver
}
else
{
uint tex = _textureCache.GetOrUploadRenderSurface(did, out int w, out int h, nearest: true);
result = (tex, w, h);
isMiss = false;
result = _icons.GetKeyedIcon(did);
isMiss = result.tex == 0u;
}
_resolvedDidCache[did] = result;

View file

@ -264,6 +264,46 @@ public sealed class IconComposer
return iconId == 0 ? 0u : GetOrCreateDragIcon(iconId, overlayId, effects)?.Texture ?? 0u;
}
/// <summary>
/// A bare RenderSurface icon drawn the way retail draws EVERY icon it composites
/// (<c>IconData::RenderIcons</c> 0x0058d180 with no custom overlay and no effects):
/// the art, then <c>SurfaceWindow::ReplaceColor</c> of its pure-white keyed pixels
/// from the effects==0 fallback tile (the solid-black 0x21 tile 0x060011C5). Item,
/// spell and component art in the DAT reserve pure white as the "recolor me" key —
/// blitting such art raw shows a white ring around the icon (owner report
/// 2026-09-06 on the plugin shelf's MossTank icon; the same defect the inventory
/// had before the effect recolor landed). This is exactly the drag-icon composite
/// (<see cref="GetDragIcon"/>) with neither overlay nor effects, so it shares that
/// cache. Used by the plugin markup <c>did</c> sink and the plugin shelf.
/// Returns <c>(0, 0, 0)</c> when the surface does not exist.
/// </summary>
public (uint tex, int w, int h) GetKeyedIcon(uint iconId)
{
if (iconId == 0u) return (0u, 0, 0);
ComposedIcon? icon = GetOrCreateDragIcon(iconId, overlayId: 0u, effects: 0u);
return icon is null ? (0u, 0, 0) : (icon.Texture, icon.Width, icon.Height);
}
/// <summary>Test seam for <see cref="GetKeyedIcon"/>: the composed RGBA8 pixels.</summary>
internal bool TryGetKeyedIconRgba(uint iconId, out byte[] rgba, out int w, out int h)
{
rgba = Array.Empty<byte>(); w = 0; h = 0;
if (iconId == 0u) return false;
ComposedIcon? icon = GetOrCreateDragIcon(iconId, overlayId: 0u, effects: 0u);
if (icon is null) return false;
rgba = icon.Rgba; w = icon.Width; h = icon.Height;
return true;
}
/// <summary>Test seam: the raw decoded RGBA8 of one RenderSurface, no compositing.</summary>
internal bool TryDecodeRaw(uint renderSurfaceId, out byte[] rgba, out int w, out int h)
{
rgba = Array.Empty<byte>(); w = 0; h = 0;
if (!TryDecode(renderSurfaceId, out DecodedTexture decoded)) return false;
rgba = decoded.Rgba8; w = decoded.Width; h = decoded.Height;
return true;
}
private ComposedIcon? GetOrCreateDragIcon(uint iconId, uint overlayId, uint effects)
{
var key = (iconId, overlayId, effects);

View file

@ -4726,7 +4726,6 @@ public sealed class RetailUiRuntime : IDisposable
// existing bindings field supplies the object table.
IMarkupIconResolver iconResolver = new RetailMarkupIconResolver(
_bindings.Assets.Dats,
_bindings.Assets.TextureCache,
_bindings.Assets.Icons,
_bindings.Toolbar.Objects);