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>
144 lines
6.1 KiB
C#
144 lines
6.1 KiB
C#
using System.Numerics;
|
|
using AcDream.App.Rendering;
|
|
using AcDream.App.Rendering.Gpu;
|
|
using AcDream.App.Tests.Rendering;
|
|
using AcDream.App.Tests.Rendering.Gpu;
|
|
using AcDream.App.UI;
|
|
using AcDream.Content;
|
|
using AcDream.Core.Items;
|
|
using AcDream.Plugin.Abstractions;
|
|
using DatReaderWriter;
|
|
using DatReaderWriter.Options;
|
|
using Xunit;
|
|
|
|
namespace AcDream.App.Tests.UI;
|
|
|
|
/// <summary>
|
|
/// Review fix round finding 1 (<c>docs/plans/2026-09-06-plugin-shelf-and-dat-icons.md</c>
|
|
/// Slice B): <see cref="RetailMarkupIconResolver.ResolveDid"/> must return
|
|
/// <c>(0, 0, 0)</c> for an id that does not resolve to a real installed
|
|
/// RenderSurface — NEVER fall through to
|
|
/// <see cref="TextureCache.GetOrUploadRenderSurface"/>'s 1x1 magenta
|
|
/// placeholder, which is load-bearing for authored chrome and would
|
|
/// otherwise get scaled up to a full magenta square by
|
|
/// <see cref="UiMarkupIcon"/>/<see cref="UiMarkupList"/>/<see cref="UiSimpleButton"/>
|
|
/// (see <c>claude-memory/feedback_ui_resolve_zero_magenta.md</c>: guard on
|
|
/// the id, never on the resolved handle).
|
|
/// </summary>
|
|
[Trait("Lane", "InstalledDat")]
|
|
public sealed class RetailMarkupIconResolverInstalledDatTests
|
|
{
|
|
/// <summary>
|
|
/// A Decal-habit "add the block prefix again" mistake applied to an
|
|
/// already-full DID: <c>0x06000165</c> (Melee Defense's real installed
|
|
/// icon, <c>SampleData.cs:69</c>) plus another <c>0x06000000</c> lands at
|
|
/// <c>0x0C000165</c> — a value almost certainly absent from both Portal
|
|
/// and HighRes.
|
|
/// </summary>
|
|
private const uint DecalHabitDoubleNormalizedId = 0x0C000165u;
|
|
|
|
/// <summary>Retail's Melee Defense skill icon — a known-real installed DID.</summary>
|
|
private const uint KnownRealDid = 0x06000165u;
|
|
|
|
[Fact]
|
|
public void ResolveDid_UnresolvableId_ReturnsNothing_AndKnownRealId_ReturnsATexture()
|
|
{
|
|
string? datDir = InstalledDatTestPath.Resolve();
|
|
if (datDir is null)
|
|
{
|
|
Assert.Fail(
|
|
"Lane=InstalledDat requires an installed retail DAT directory; see docs/release-gate.md.");
|
|
return;
|
|
}
|
|
|
|
using var dats = new DatCollection(datDir, DatAccessType.Read);
|
|
using var adapter = new DatCollectionAdapter(dats);
|
|
var device = new RecordingGpuDevice();
|
|
using var cache = new TextureCache(device, adapter);
|
|
var icons = new IconComposer(adapter, cache);
|
|
var objects = new ClientObjectTable();
|
|
var resolver = new RetailMarkupIconResolver(adapter, icons, objects);
|
|
|
|
(uint missTex, int missW, int missH) = resolver.ResolveDid(DecalHabitDoubleNormalizedId);
|
|
Assert.Equal((0u, 0, 0), (missTex, missW, missH));
|
|
|
|
(uint realTex, int realW, int realH) = resolver.ResolveDid(KnownRealDid);
|
|
Assert.NotEqual(0u, realTex);
|
|
Assert.True(realW > 0 && realH > 0);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Residual round finding N1: proves the production wiring fix end to
|
|
/// end against the REAL resolver — <see cref="RetailUiRuntime.MountPlugins"/>
|
|
/// now passes <c>iconResolver.ResolveDid</c> (this class) to
|
|
/// <see cref="PluginSidePanel"/>, not
|
|
/// <c>_bindings.Assets.ResolveSprite</c> (= ResolveChrome =
|
|
/// <see cref="TextureCache.GetOrUploadRenderSurface"/>, which returns a
|
|
/// non-zero 1x1 magenta placeholder for a missing id and would have kept
|
|
/// <see cref="PluginSidePanel.PluginShelfButton"/>'s initials fallback
|
|
/// from ever firing). A bogus descriptor icon id run through the real
|
|
/// resolver must resolve to nothing, and the shelf button must fall
|
|
/// back to its initials text — exactly the shape that would have caught
|
|
/// the pre-fix wiring bug (ResolveSprite would have returned a non-zero
|
|
/// magenta handle here instead of (0,0,0)).
|
|
/// </summary>
|
|
[Fact]
|
|
public void ShelfButton_BogusDescriptorId_OnTheRealResolver_FallsBackToInitials()
|
|
{
|
|
string? datDir = InstalledDatTestPath.Resolve();
|
|
if (datDir is null)
|
|
{
|
|
Assert.Fail(
|
|
"Lane=InstalledDat requires an installed retail DAT directory; see docs/release-gate.md.");
|
|
return;
|
|
}
|
|
|
|
using var dats = new DatCollection(datDir, DatAccessType.Read);
|
|
using var adapter = new DatCollectionAdapter(dats);
|
|
var device = new RecordingGpuDevice();
|
|
using var cache = new TextureCache(device, adapter);
|
|
var icons = new IconComposer(adapter, cache);
|
|
var objects = new ClientObjectTable();
|
|
var resolver = new RetailMarkupIconResolver(adapter, icons, objects);
|
|
|
|
var root = new UiRoot { Width = 800f, Height = 600f };
|
|
using var shelf = new PluginSidePanel(
|
|
root.WindowManager,
|
|
resolver.ResolveDid,
|
|
font: null);
|
|
root.AddChild(shelf);
|
|
|
|
var frame = new UiPanel { Width = 200f, Height = 100f };
|
|
root.AddChild(frame);
|
|
RetailWindowHandle handle = root.WindowManager.Register(
|
|
"plugin:acdream.test:main", frame);
|
|
shelf.Add(
|
|
new PluginUiOwner("acdream.test", "Test Plugin"),
|
|
new PluginPanelDescriptor("main", "Test Plugin")
|
|
{
|
|
IconText = "TP",
|
|
// Already >= the Normalize boundary (0x01000000), so
|
|
// PluginShelfButton's ctor leaves it unchanged — a real DID
|
|
// shape that is nonetheless absent from both Portal and
|
|
// HighRes (see DecalHabitDoubleNormalizedId's own doc above).
|
|
IconSurfaceId = DecalHabitDoubleNormalizedId,
|
|
},
|
|
handle);
|
|
|
|
PluginSidePanel.PluginShelfButton button = Assert.Single(
|
|
shelf.Children.OfType<PluginSidePanel.PluginShelfButton>());
|
|
Assert.Equal(string.Empty, button.Text);
|
|
|
|
var textRenderer = new TextRenderer(device, new NullGpuFrameSource(), "unused");
|
|
textRenderer.Begin(new Vector2(200f, 200f));
|
|
var ctx = new UiRenderContext(textRenderer, new Vector2(200f, 200f));
|
|
button.DrawSelfAndChildren(ctx);
|
|
|
|
Assert.Equal("TP", button.Text);
|
|
}
|
|
|
|
private sealed class NullGpuFrameSource : ICurrentGpuFrameSource
|
|
{
|
|
public IGpuFrame? CurrentFrame => null;
|
|
}
|
|
}
|