diff --git a/.github/workflows/headless-portability.yml b/.github/workflows/headless-portability.yml
index 7d498278..f1389c9f 100644
--- a/.github/workflows/headless-portability.yml
+++ b/.github/workflows/headless-portability.yml
@@ -211,8 +211,8 @@ jobs:
test -f "$root/libglfw.so.3"
test -f "$root/libopenal.so"
test -f "$root/Rendering/Shaders/mesh_modern.vert"
- test -f "$root/plugins/AcDream.Plugins.Smoke/AcDream.Plugins.Smoke.dll"
- test -f "$root/plugins/AcDream.Plugins.Smoke/plugin.json"
+ test -f "$root/plugins/AcDream.Plugins.MossTank/AcDream.Plugins.MossTank.dll"
+ test -f "$root/plugins/AcDream.Plugins.MossTank/plugin.json"
test "$(grep -RIl --include='*.cs' 'LibraryImport(\"kernel32.dll\"' \
src/AcDream.App | wc -l)" -eq 1
diff --git a/AcDream.slnx b/AcDream.slnx
index 892b0ee3..2699c534 100644
--- a/AcDream.slnx
+++ b/AcDream.slnx
@@ -12,7 +12,6 @@
-
diff --git a/README.md b/README.md
index 616035ea..02bacc48 100644
--- a/README.md
+++ b/README.md
@@ -235,7 +235,7 @@ src/
AcDream.UI.Abstractions/ shared UI/input models and contracts
AcDream.UI.ImGui/ developer-tool presentation
AcDream.Plugin.Abstractions/ BCL-only plugin contracts
- AcDream.Plugins.Smoke/ example plugin
+ AcDream.Plugins.MossTank/ example plugin
tests/
AcDream.*.Tests/ layer-matched xUnit projects
diff --git a/docs/plans/2026-09-06-plugin-shelf-and-dat-icons.md b/docs/plans/2026-09-06-plugin-shelf-and-dat-icons.md
index a7076079..b83e748d 100644
--- a/docs/plans/2026-09-06-plugin-shelf-and-dat-icons.md
+++ b/docs/plans/2026-09-06-plugin-shelf-and-dat-icons.md
@@ -290,22 +290,16 @@ all four places, list icon column aligned with rows.
now throws at `Build` instead of silently ignoring the attribute; stale
`SampleData.cs:64` citations corrected to `:69` (Melee Defense's real
line).
-- **Before shipment:** the Smoke plugin's "Icon Smoke" panel
- (`src/AcDream.Plugins.Smoke/SmokeIconPanel.cs`) keeps `StartVisible=true`
- on purpose, for the owner's connected gate — flip it to
- `StartVisible=false` (or drop `AcDream.Plugins.Smoke` from the release
- payload entirely) once that gate is done. Checked whether the LAUNCHER
- release payload actually ships it: `tools/publish-bin.ps1`'s
- `New-PayloadZip` zips App's ENTIRE publish output directory
- (`[IO.Compression.ZipFile]::CreateFromDirectory`, no file filtering
- beyond checking `$RequiredFiles` exist) into `client-.zip`, and
- `AcDream.App.csproj`'s `CopySmokePluginToPublishOutput` target
- (`AfterTargets="Publish"`, gated only on `IsCrossTargetingBuild`, not on
- `Configuration`) copies `plugins/AcDream.Plugins.Smoke/` straight into
- that same publish directory during `dotnet publish`. **The Smoke plugin
- — auto-open panel included — ships in today's release zip.** (The
- MossTank plugin has the equivalent `CopyMossTankPluginToPublishOutput`
- target and ships the same way; a quick grep shows several of its own
- panels also set `StartVisible = true` — worth the same "before shipment"
- look, but out of scope for this residual round, which only covers the
- Smoke plugin per N9.)
+- **Owner acceptance (2026-09-06):** the owner visually accepted all three
+ connected gates — the plugin shelf (Slice A), the DAT icon markup
+ (Slice B), and the retained-UI outline-order fix (`761a7519f`, landed
+ alongside this plan's residual round). With the visual gate passed, the
+ Smoke plugin's job as the gate artifact is done. Per owner direction,
+ `src/AcDream.Plugins.Smoke/` (including its "Icon Smoke" proof panel,
+ `SmokeIconPanel.cs`) was deleted outright rather than merely hidden —
+ the shipped-in-the-release-zip risk this ledger flagged above is now
+ moot because the plugin no longer exists to ship. Every build/CI/test/doc
+ reference to it was removed or repointed at the MossTank plugin in the
+ same commit; see `chore(plugins): remove the Smoke gate plugin; MossTank
+ shelf icon 0x06002C41`. MossTank's own panels defaulting to
+ `StartVisible = true` remains open — same look, still out of scope here.
diff --git a/docs/plugin-ui-markup.md b/docs/plugin-ui-markup.md
index 57fb5edb..055d048e 100644
--- a/docs/plugin-ui-markup.md
+++ b/docs/plugin-ui-markup.md
@@ -21,8 +21,8 @@ icon, and the parser that turns markup into a live `UiElement` tree
host.Ui.AddPanel(
new PluginPanelDescriptor("main", "MossTank")
{
- IconText = "MT", // fallback initials if IconSurfaceId is 0
- IconSurfaceId = 7735, // Decal-style bare index OR a full DID — both normalize
+ IconText = "MT", // fallback initials if IconSurfaceId is 0
+ IconSurfaceId = 0x06002C41, // Decal-style bare index OR a full DID — both normalize
StartVisible = true,
ShowInSidePanel = true,
},
@@ -32,9 +32,8 @@ host.Ui.AddPanel(
`RegisterPanel` (same signature, returns `IDisposable`) removes the window
independently of the plugin's own lifetime. `RegisterPanelContent` takes an
-in-memory KSML string instead of a file path — the route `AcDream.Plugins.Smoke`
-uses for its icon-surface proof panel (`SmokeIconPanel.cs`), when a panel is
-small enough not to need its own shipped `.xml` asset.
+in-memory KSML string instead of a file path — the route to reach for when a
+panel is small enough not to need its own shipped `.xml` asset.
Every registered window gets a stable persisted key
(`plugin:{pluginId}:{windowId}`), drag, resize (where the markup opts in),
@@ -263,9 +262,7 @@ There is no way to mix kinds within a single list. Two consequences:
that wants the badge look has no `did`-space escape hatch.
MosswartMassacre-style example — a list column composited from spell ids,
-with the spell's own raw art DID printed alongside the name for comparison
-(`AcDream.Plugins.Smoke`'s own proof panel, `SmokeIconPanel.cs`, is exactly
-this pattern):
+with the spell's own raw art DID printed alongside the name for comparison:
```csharp
// iconkind="spell": the values MUST be spell ids (what ResolveSpell composites
diff --git a/src/AcDream.App/AcDream.App.csproj b/src/AcDream.App/AcDream.App.csproj
index 3ccc68c8..b4a7c436 100644
--- a/src/AcDream.App/AcDream.App.csproj
+++ b/src/AcDream.App/AcDream.App.csproj
@@ -100,62 +100,7 @@
-
-
-
- false
- true
-
-
-
-
- <_SmokePluginDestDir>$(OutputPath)plugins/AcDream.Plugins.Smoke
-
-
-
-
-
-
-
-
-
-
-
- <_SmokePluginPublishDestDir>$(PublishDir)plugins/AcDream.Plugins.Smoke
-
-
-
-
-
-
-
-
-
-
diff --git a/src/AcDream.Plugins.MossTank/MossTankPlugin.cs b/src/AcDream.Plugins.MossTank/MossTankPlugin.cs
index dbcd3974..40f5fa46 100644
--- a/src/AcDream.Plugins.MossTank/MossTankPlugin.cs
+++ b/src/AcDream.Plugins.MossTank/MossTankPlugin.cs
@@ -41,6 +41,11 @@ public sealed class MossTankPlugin : IAcDreamPlugin
new PluginPanelDescriptor("main", "MossTank")
{
IconText = "MT",
+ // A real installed portal.dat RenderSurface — see
+ // MossTankIconInstalledDatTests for the DAT-presence proof.
+ // IconText remains the fallback if this id is ever absent
+ // from an install.
+ IconSurfaceId = 0x06002C41u,
StartVisible = true,
ShowInSidePanel = true,
},
diff --git a/src/AcDream.Plugins.Smoke/AcDream.Plugins.Smoke.csproj b/src/AcDream.Plugins.Smoke/AcDream.Plugins.Smoke.csproj
deleted file mode 100644
index 0f5b0c92..00000000
--- a/src/AcDream.Plugins.Smoke/AcDream.Plugins.Smoke.csproj
+++ /dev/null
@@ -1,17 +0,0 @@
-
-
- net10.0
- enable
- enable
- latest
-
-
-
-
- false
- runtime
-
-
-
diff --git a/src/AcDream.Plugins.Smoke/SmokeIconPanel.cs b/src/AcDream.Plugins.Smoke/SmokeIconPanel.cs
deleted file mode 100644
index 76d1dc46..00000000
--- a/src/AcDream.Plugins.Smoke/SmokeIconPanel.cs
+++ /dev/null
@@ -1,168 +0,0 @@
-using AcDream.Plugin.Abstractions;
-
-namespace AcDream.Plugins.Smoke;
-
-///
-/// Slice B proof panel (docs/plans/2026-09-06-plugin-shelf-and-dat-icons.md
-/// item 9): exercises every new plugin-markup icon surface in one place —
-/// registered from in-memory KSML via
-/// (no plugin-side XML file), with the descriptor's own
-/// also set to the bare
-/// Decal-style index used below, so the shelf button and the panel's own
-/// <icon did="0x165"> prove
-/// the same way at both sinks.
-///
-///
-/// Review fix round (2026-09-06): was
-/// 7735 (an unverified literal) and is now retail's Melee Defense
-/// skill icon index 0x165 — SampleData.cs:69 attests
-/// 0x06000165 is a real installed-DAT RenderSurface, so the
-/// descriptor and the bare-index <icon> both draw ART a tester
-/// can actually verify against retail, not a guess. The list's
-/// iconkind="spell" column also had a wiring bug: it fed
-/// (a raw RenderSurface DID) to the
-/// SPELL-id resolver, which composites a badge from a SpellTable entry
-/// looked up by SPELL id — the two numbers are unrelated, so every row
-/// silently resolved the wrong (or no) composited badge. See
-/// .
-///
-internal static class SmokeIconPanel
-{
- ///
- /// A Decal/VirindiViewService-style bare portal.dat index for retail's
- /// Melee Defense skill icon (0x06000165 — attested in
- /// src/AcDream.App/UI/Layout/SampleData.cs:69) rather than an
- /// unverified literal. Deliberately used on BOTH the descriptor and the
- /// first <icon> below to prove the host normalizes it
- /// identically at each sink.
- ///
- public const uint BareIndexIconId = 0x165u;
-
- /// A literal, already-normalized RenderSurface DID (a portal icon).
- private const uint LiteralDidIconId = 0x06002D14u;
-
- ///
- /// Retail's Strength Other I — the plan's named fallback when the local
- /// character has not learned any self-castable buff yet.
- ///
- private const uint FallbackSpellId = 1u;
-
- public static readonly PluginPanelDescriptor Descriptor = new("icons", "Icon Smoke")
- {
- IconSurfaceId = BareIndexIconId,
- StartVisible = true,
- ShowInSidePanel = true,
- };
-
- public const string Markup = """
-
-
-
-
-
-
-
- """;
-
- /// Binding object for . Reads live host state
- /// on every frame the same way any BCL-only plugin binding would.
- internal sealed class Binding
- {
- private readonly IPluginHost _host;
-
- public Binding(IPluginHost host) => _host = host;
-
- public int SelectedIndex { get; set; } = -1;
-
- /// The first spell a plugin markup <icon spell=...>
- /// draws — the character's first known self-buff, falling back to
- /// (Strength Other I) when nothing is
- /// learned yet (fresh character, or no live session).
- public uint SpellId
- {
- get
- {
- IReadOnlyList known = _host.Automation.Spells.KnownSelfBuffs;
- return known.Count > 0 ? known[0].SpellId : FallbackSpellId;
- }
- }
-
- ///
- /// The first five known self-buffs, or — when the character has
- /// learned nothing yet — a single-entry fallback to
- /// 's REAL catalog entry (never
- /// fabricated art: if the host cannot resolve spell 1 either, the
- /// fallback is empty and /
- /// show the honest "no spells known" row instead).
- ///
- private IReadOnlyList KnownSelfBuffsOrFallback()
- {
- IReadOnlyList known = _host.Automation.Spells.KnownSelfBuffs;
- if (known.Count > 0)
- return known;
- return _host.Automation.Spells.TryGet(FallbackSpellId, out PluginSpellInfo info)
- ? new[] { info }
- : Array.Empty();
- }
-
- ///
- /// Parallel spell-id column for , feeding the
- /// list's iconkind="spell" composited badge. Review fix round
- /// finding 2: this must be the spell id
- /// () that
- /// composites a badge
- /// from — NOT (the spell's raw
- /// art DID), which is what this property used to yield. The raw
- /// is still visible, printed
- /// alongside the name in , for anyone
- /// comparing the composited badge against the plain art tile.
- ///
- public IEnumerable SpellIds
- {
- get
- {
- IReadOnlyList spells = KnownSelfBuffsOrFallback();
- if (spells.Count == 0)
- return new uint[] { 0u };
- var ids = new List();
- foreach (PluginSpellInfo spell in spells)
- {
- ids.Add(spell.SpellId);
- if (ids.Count == 5)
- break;
- }
- return ids;
- }
- }
-
- /// Row text for the first five known self-buffs (or the
- /// single-entry fallback), with each spell's
- /// printed alongside its name so
- /// the composited icon column and the raw art id are both visible in
- /// one look. A character with no self-buffs known AND no resolvable
- /// fallback shows one honest "no spells known" row rather than a
- /// blank list.
- public IEnumerable SpellRows
- {
- get
- {
- IReadOnlyList spells = KnownSelfBuffsOrFallback();
- if (spells.Count == 0)
- return new[] { "no spells known" };
- var rows = new List();
- foreach (PluginSpellInfo spell in spells)
- {
- rows.Add($"{spell.Name} (icon 0x{spell.IconId:X8})");
- if (rows.Count == 5)
- break;
- }
- return rows;
- }
- }
-
- public Action Report =>
- () => _host.Log.Info(
- $"smoke icon panel: SpellId={SpellId:X8}, {SpellRows.Count()} spellbook rows");
- }
-}
diff --git a/src/AcDream.Plugins.Smoke/SmokePlugin.cs b/src/AcDream.Plugins.Smoke/SmokePlugin.cs
deleted file mode 100644
index aec4db41..00000000
--- a/src/AcDream.Plugins.Smoke/SmokePlugin.cs
+++ /dev/null
@@ -1,45 +0,0 @@
-using AcDream.Plugin.Abstractions;
-
-namespace AcDream.Plugins.Smoke;
-
-public sealed class SmokePlugin : IAcDreamPlugin
-{
- private IPluginHost? _host;
- private int _entitiesSeen;
- private IDisposable? _iconPanel;
-
- public void Initialize(IPluginHost host)
- {
- _host = host;
- _host.Log.Info("smoke plugin initialized");
- }
-
- public void Enable()
- {
- _host?.Log.Info("smoke plugin enabled");
- if (_host is not null)
- {
- _host.Events.EntitySpawned += OnEntitySpawned;
- _host.Log.Info($"smoke plugin sees {_entitiesSeen} entities (replay count at subscribe)");
-
- // Slice B proof panel (docs/plans/2026-09-06-plugin-shelf-and-dat-icons.md
- // item 9): in-memory KSML, no plugin-side XML file, exercising
- // every new icon markup surface.
- _iconPanel = _host.Ui.RegisterPanelContent(
- SmokeIconPanel.Descriptor,
- SmokeIconPanel.Markup,
- new SmokeIconPanel.Binding(_host));
- }
- }
-
- public void Disable()
- {
- if (_host is not null)
- _host.Events.EntitySpawned -= OnEntitySpawned;
- _iconPanel?.Dispose();
- _iconPanel = null;
- _host?.Log.Info($"smoke plugin disabled (saw {_entitiesSeen} entities total)");
- }
-
- private void OnEntitySpawned(WorldEntitySnapshot snapshot) => _entitiesSeen++;
-}
diff --git a/src/AcDream.Plugins.Smoke/packages.linux-x64.lock.json b/src/AcDream.Plugins.Smoke/packages.linux-x64.lock.json
deleted file mode 100644
index fe625889..00000000
--- a/src/AcDream.Plugins.Smoke/packages.linux-x64.lock.json
+++ /dev/null
@@ -1,11 +0,0 @@
-{
- "version": 2,
- "dependencies": {
- "net10.0": {
- "acdream.plugin.abstractions": {
- "type": "Project"
- }
- },
- "net10.0/linux-x64": {}
- }
-}
\ No newline at end of file
diff --git a/src/AcDream.Plugins.Smoke/packages.neutral.lock.json b/src/AcDream.Plugins.Smoke/packages.neutral.lock.json
deleted file mode 100644
index 3924e2e2..00000000
--- a/src/AcDream.Plugins.Smoke/packages.neutral.lock.json
+++ /dev/null
@@ -1,10 +0,0 @@
-{
- "version": 2,
- "dependencies": {
- "net10.0": {
- "acdream.plugin.abstractions": {
- "type": "Project"
- }
- }
- }
-}
\ No newline at end of file
diff --git a/src/AcDream.Plugins.Smoke/packages.win-x64.lock.json b/src/AcDream.Plugins.Smoke/packages.win-x64.lock.json
deleted file mode 100644
index b12a77e4..00000000
--- a/src/AcDream.Plugins.Smoke/packages.win-x64.lock.json
+++ /dev/null
@@ -1,11 +0,0 @@
-{
- "version": 2,
- "dependencies": {
- "net10.0": {
- "acdream.plugin.abstractions": {
- "type": "Project"
- }
- },
- "net10.0/win-x64": {}
- }
-}
\ No newline at end of file
diff --git a/tests/AcDream.App.Tests/Plugins/AppAutomationSurfaceIconInstalledDatTests.cs b/tests/AcDream.App.Tests/Plugins/AppAutomationSurfaceIconInstalledDatTests.cs
index 6e23dd3d..eacc72cf 100644
--- a/tests/AcDream.App.Tests/Plugins/AppAutomationSurfaceIconInstalledDatTests.cs
+++ b/tests/AcDream.App.Tests/Plugins/AppAutomationSurfaceIconInstalledDatTests.cs
@@ -23,9 +23,9 @@ namespace AcDream.App.Tests.Plugins;
[Trait("Lane", "InstalledDat")]
public sealed class AppAutomationSurfaceIconInstalledDatTests
{
- /// Retail's Strength Other I — the same well-known spell id
- /// SmokeIconPanel falls back to when the local character has learned
- /// nothing yet.
+ /// Retail's Strength Other I — a well-known always-present spell
+ /// id, usable as a fallback when the local character has learned nothing
+ /// yet.
private const uint KnownSpellId = 1u;
[Fact]
diff --git a/tests/AcDream.App.Tests/Rendering/LinuxPlatformBoundaryTests.cs b/tests/AcDream.App.Tests/Rendering/LinuxPlatformBoundaryTests.cs
index f6f7cb74..1fa4218a 100644
--- a/tests/AcDream.App.Tests/Rendering/LinuxPlatformBoundaryTests.cs
+++ b/tests/AcDream.App.Tests/Rendering/LinuxPlatformBoundaryTests.cs
@@ -120,7 +120,7 @@ public sealed class LinuxPlatformBoundaryTests
Assert.Contains("$(OutputPath)plugins/", project, StringComparison.Ordinal);
Assert.Contains("$(PublishDir)plugins/", project, StringComparison.Ordinal);
Assert.Equal(
- 4,
+ 2,
project.Split("Targets=\"GetTargetPath\"", StringSplitOptions.None)
.Length - 1);
Assert.Contains(
diff --git a/tests/AcDream.App.Tests/UI/MossTankIconInstalledDatTests.cs b/tests/AcDream.App.Tests/UI/MossTankIconInstalledDatTests.cs
new file mode 100644
index 00000000..fa6e6fdb
--- /dev/null
+++ b/tests/AcDream.App.Tests/UI/MossTankIconInstalledDatTests.cs
@@ -0,0 +1,65 @@
+using AcDream.App.Rendering;
+using AcDream.App.Tests.Rendering;
+using AcDream.App.Tests.Rendering.Gpu;
+using AcDream.App.UI;
+using AcDream.Content;
+using AcDream.Core.Items;
+using DatReaderWriter;
+using DatReaderWriter.DBObjs;
+using DatReaderWriter.Options;
+using Xunit;
+
+namespace AcDream.App.Tests.UI;
+
+///
+/// Owner direction 2026-09-06 (Smoke-plugin removal +
+/// docs/plans/2026-09-06-plugin-shelf-and-dat-icons.md follow-up):
+/// MossTankPlugin's shelf descriptor now sets a real
+/// IconSurfaceId instead of relying solely on the "MT" text
+/// fallback. Mirrors the
+///
+/// installed-DAT gate convention: +
+/// [Trait("Lane", "InstalledDat")], Assert.Fail with the
+/// standard message when no DAT is configured (see docs/release-gate.md).
+///
+[Trait("Lane", "InstalledDat")]
+public sealed class MossTankIconInstalledDatTests
+{
+ /// The id MossTankPlugin.cs sets as its shelf descriptor's
+ /// IconSurfaceId.
+ private const uint MossTankShelfIconId = 0x06002C41u;
+
+ [Fact]
+ public void MossTankShelfIconId_IsARealInstalledRenderSurface()
+ {
+ 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);
+
+ // The RenderSurface must exist in Portal or HighRes — the same two
+ // databases RetailMarkupIconResolver.ResolveDid probes before ever
+ // touching TextureCache.
+ bool existsInPortal = adapter.Portal.TryGet(MossTankShelfIconId, out _);
+ bool existsInHighRes = adapter.HighRes.TryGet(MossTankShelfIconId, out _);
+ Assert.True(
+ existsInPortal || existsInHighRes,
+ $"expected 0x{MossTankShelfIconId:X8} to be a real installed RenderSurface (Portal or HighRes)");
+
+ 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, cache, icons, objects);
+
+ (uint tex, int w, int h) = resolver.ResolveDid(MossTankShelfIconId);
+ Assert.NotEqual(0u, tex);
+ Assert.True(w > 0 && h > 0);
+ }
+}
diff --git a/tests/AcDream.Core.Tests/Plugins/PluginManifestTests.cs b/tests/AcDream.Core.Tests/Plugins/PluginManifestTests.cs
index b9ff6a51..0dc195ef 100644
--- a/tests/AcDream.Core.Tests/Plugins/PluginManifestTests.cs
+++ b/tests/AcDream.Core.Tests/Plugins/PluginManifestTests.cs
@@ -9,20 +9,20 @@ public class PluginManifestTests
{
const string json = """
{
- "id": "acdream.smoke",
- "displayName": "Smoke Test",
+ "id": "acdream.mosstank",
+ "displayName": "MossTank",
"version": "0.1.0",
- "entryDll": "AcDream.Plugins.Smoke.dll",
+ "entryDll": "AcDream.Plugins.MossTank.dll",
"apiVersion": 1
}
""";
var manifest = PluginManifest.Parse(json);
- Assert.Equal("acdream.smoke", manifest.Id);
- Assert.Equal("Smoke Test", manifest.DisplayName);
+ Assert.Equal("acdream.mosstank", manifest.Id);
+ Assert.Equal("MossTank", manifest.DisplayName);
Assert.Equal("0.1.0", manifest.Version);
- Assert.Equal("AcDream.Plugins.Smoke.dll", manifest.EntryDll);
+ Assert.Equal("AcDream.Plugins.MossTank.dll", manifest.EntryDll);
Assert.Equal(1, manifest.ApiVersion);
Assert.Equal([PluginKind.Gameplay], manifest.Kinds);
}