chore(plugins): remove the Smoke gate plugin; MossTank shelf icon 0x06002C41

Owner direction 2026-09-06: the owner visually accepted all three connected
gates from the plugin-shelf/DAT-icon work — the movable plugin shelf
(Slice A), the DAT icon markup (Slice B), and the retained-UI outline-order
fix (761a7519f). With that gate passed, the Smoke plugin's job as the gate
artifact is done, so it is deleted outright rather than merely hidden:
src/AcDream.Plugins.Smoke/ (SmokePlugin.cs, SmokeIconPanel.cs, csproj, lock
files).

Reference sites cleaned:
- AcDream.slnx: removed the project entry.
- src/AcDream.App/AcDream.App.csproj: removed the Smoke ProjectReference and
  the CopySmokePluginToBuildOutput/CopySmokePluginToPublishOutput targets;
  the MossTank equivalents are untouched.
- .github/workflows/headless-portability.yml: the Linux package-contract
  step no longer asserts a Smoke plugin.dll/plugin.json pair — repointed at
  MossTank's, since the step's job was to prove the plugin-copy packaging
  mechanism works end to end, not specifically to prove Smoke.
- tests/AcDream.Core.Tests/Plugins/PluginManifestTests.cs: the inline JSON
  fixture used Smoke's manifest values as arbitrary test data; swapped for
  MossTank's so the parser test still proves the same thing.
- tests/AcDream.App.Tests/Rendering/LinuxPlatformBoundaryTests.cs: the
  shipped-plugin-copy shape test counted 4 GetTargetPath targets (Smoke +
  MossTank, build + publish); now 2 (MossTank only).
- tests/AcDream.App.Tests/Plugins/AppAutomationSurfaceIconInstalledDatTests.cs:
  reworded a doc comment that named the now-deleted SmokeIconPanel.
- README.md, docs/plugin-ui-markup.md: dropped Smoke-specific mentions,
  kept the icon markup example/grammar (now citing MossTank's own real
  IconSurfaceId).
- docs/plans/2026-09-06-plugin-shelf-and-dat-icons.md: recorded the owner's
  2026-09-06 acceptance and the Smoke removal in the review ledger; deleted
  the now-moot "before shipment" Smoke-in-release-zip warning.
- docs/reviews/coverage-ledger.md, docs/reviews/findings-ledger.md: left
  untouched — both are frozen audit snapshots ("complete for baseline
  <hash>"), so their Smoke rows are historical record, not live claims.
- docs/ISSUES.md: left untouched — its Smoke mentions are inside closed
  issue #193's historical write-up of a past investigation.

MossTank plugin shelf icon: MossTankPlugin.cs's PluginPanelDescriptor now
sets IconSurfaceId = 0x06002C41 (IconText = "MT" remains the fallback).
Verified against the installed retail DAT with a new InstalledDat-lane test,
tests/AcDream.App.Tests/UI/MossTankIconInstalledDatTests.cs, mirroring
RetailMarkupIconResolverInstalledDatTests's convention: confirms the id is a
real Portal/HighRes RenderSurface and that RetailMarkupIconResolver.ResolveDid
returns a non-zero texture for it.

Verified: dotnet build AcDream.slnx -c Release green; a stale
plugins/AcDream.Plugins.Smoke output folder from a prior build was deleted
and a fresh build does not recreate it. Full App suite: 7,363 passed / 97
skipped / 36 failed (was 7,362/97/36) — the failing set is unchanged and
none are plugin-related; the one new pass is the MossTank DAT-icon test.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
Erik 2026-09-06 16:49:17 +02:00
parent ce05c4fb03
commit ece2104189
17 changed files with 102 additions and 359 deletions

View file

@ -100,62 +100,7 @@
<EmbeddedResource Include="..\..\assets\icons\acdream-client-256.png"
LogicalName="AcDream.App.Rendering.Icons.acdream-client-256.png" />
</ItemGroup>
<ItemGroup>
<!-- Build the smoke plugin first and copy it into plugins/AcDream.Plugins.Smoke/ -->
<ProjectReference Include="..\AcDream.Plugins.Smoke\AcDream.Plugins.Smoke.csproj">
<ReferenceOutputAssembly>false</ReferenceOutputAssembly>
<SkipGetTargetFrameworkProperties>true</SkipGetTargetFrameworkProperties>
</ProjectReference>
</ItemGroup>
<Target
Name="CopySmokePluginToBuildOutput"
AfterTargets="Build"
Condition="'$(IsCrossTargetingBuild)' != 'true'">
<PropertyGroup>
<_SmokePluginDestDir>$(OutputPath)plugins/AcDream.Plugins.Smoke</_SmokePluginDestDir>
</PropertyGroup>
<MSBuild
Projects="$(MSBuildProjectDirectory)/../AcDream.Plugins.Smoke/AcDream.Plugins.Smoke.csproj"
Targets="GetTargetPath"
Properties="Configuration=$(Configuration);TargetFramework=$(TargetFramework);RuntimeIdentifier=$(RuntimeIdentifier);OutputPath=$(OutputPath)">
<Output TaskParameter="TargetOutputs" ItemName="_SmokePluginBuildTarget" />
</MSBuild>
<MakeDir Directories="$(_SmokePluginDestDir)" />
<Copy
SourceFiles="@(_SmokePluginBuildTarget)"
DestinationFolder="$(_SmokePluginDestDir)"
SkipUnchangedFiles="true" />
<WriteLinesToFile
File="$(_SmokePluginDestDir)/plugin.json"
Overwrite="true"
Lines="{ &quot;id&quot;: &quot;acdream.smoke&quot;, &quot;displayName&quot;: &quot;Smoke Plugin&quot;, &quot;version&quot;: &quot;0.1.0&quot;, &quot;entryDll&quot;: &quot;AcDream.Plugins.Smoke.dll&quot;, &quot;apiVersion&quot;: 1 }" />
</Target>
<Target
Name="CopySmokePluginToPublishOutput"
AfterTargets="Publish"
Condition="'$(IsCrossTargetingBuild)' != 'true'">
<PropertyGroup>
<_SmokePluginPublishDestDir>$(PublishDir)plugins/AcDream.Plugins.Smoke</_SmokePluginPublishDestDir>
</PropertyGroup>
<MSBuild
Projects="$(MSBuildProjectDirectory)/../AcDream.Plugins.Smoke/AcDream.Plugins.Smoke.csproj"
Targets="GetTargetPath"
Properties="Configuration=$(Configuration);TargetFramework=$(TargetFramework);RuntimeIdentifier=$(RuntimeIdentifier);OutputPath=$(OutputPath)">
<Output TaskParameter="TargetOutputs" ItemName="_SmokePluginPublishTarget" />
</MSBuild>
<MakeDir Directories="$(_SmokePluginPublishDestDir)" />
<Copy
SourceFiles="@(_SmokePluginPublishTarget)"
DestinationFolder="$(_SmokePluginPublishDestDir)"
SkipUnchangedFiles="true" />
<WriteLinesToFile
File="$(_SmokePluginPublishDestDir)/plugin.json"
Overwrite="true"
Lines="{ &quot;id&quot;: &quot;acdream.smoke&quot;, &quot;displayName&quot;: &quot;Smoke Plugin&quot;, &quot;version&quot;: &quot;0.1.0&quot;, &quot;entryDll&quot;: &quot;AcDream.Plugins.Smoke.dll&quot;, &quot;apiVersion&quot;: 1 }" />
</Target>
<!-- MossTank ships the same way as the smoke plugin, plus its panel markup:
<!-- MossTank ships as a copy-only plugin, plus its panel markup:
MossTankPlugin resolves mosstank.xml relative to its own assembly, so the
two files must land in the same plugin directory. -->
<ItemGroup>

View file

@ -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,
},

View file

@ -1,17 +0,0 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<LangVersion>latest</LangVersion>
<!-- Plugin DLLs are copied to plugins/<id>/ at build of AcDream.App.
They must NOT bring AcDream.Plugin.Abstractions.dll with them;
the host already owns it. -->
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\AcDream.Plugin.Abstractions\AcDream.Plugin.Abstractions.csproj">
<Private>false</Private>
<ExcludeAssets>runtime</ExcludeAssets>
</ProjectReference>
</ItemGroup>
</Project>

View file

@ -1,168 +0,0 @@
using AcDream.Plugin.Abstractions;
namespace AcDream.Plugins.Smoke;
/// <summary>
/// Slice B proof panel (<c>docs/plans/2026-09-06-plugin-shelf-and-dat-icons.md</c>
/// item 9): exercises every new plugin-markup icon surface in one place —
/// registered from in-memory KSML via <see cref="IUiRegistry.RegisterPanelContent"/>
/// (no plugin-side XML file), with the descriptor's own
/// <see cref="PluginPanelDescriptor.IconSurfaceId"/> also set to the bare
/// Decal-style index used below, so the shelf button and the panel's own
/// <c>&lt;icon did="0x165"&gt;</c> prove
/// <see cref="PluginIcons.Normalize"/> the same way at both sinks.
/// </summary>
/// <remarks>
/// Review fix round (2026-09-06): <see cref="BareIndexIconId"/> was
/// <c>7735</c> (an unverified literal) and is now retail's Melee Defense
/// skill icon index <c>0x165</c> — <c>SampleData.cs:69</c> attests
/// <c>0x06000165</c> is a real installed-DAT RenderSurface, so the
/// descriptor and the bare-index <c>&lt;icon&gt;</c> both draw ART a tester
/// can actually verify against retail, not a guess. The list's
/// <c>iconkind="spell"</c> column also had a wiring bug: it fed
/// <see cref="PluginSpellInfo.IconId"/> (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
/// <see cref="Binding.SpellIds"/>.
/// </remarks>
internal static class SmokeIconPanel
{
/// <summary>
/// A Decal/VirindiViewService-style bare portal.dat index for retail's
/// Melee Defense skill icon (<c>0x06000165</c> — attested in
/// <c>src/AcDream.App/UI/Layout/SampleData.cs:69</c>) rather than an
/// unverified literal. Deliberately used on BOTH the descriptor and the
/// first <c>&lt;icon&gt;</c> below to prove the host normalizes it
/// identically at each sink.
/// </summary>
public const uint BareIndexIconId = 0x165u;
/// <summary>A literal, already-normalized RenderSurface DID (a portal icon).</summary>
private const uint LiteralDidIconId = 0x06002D14u;
/// <summary>
/// Retail's Strength Other I — the plan's named fallback when the local
/// character has not learned any self-castable buff yet.
/// </summary>
private const uint FallbackSpellId = 1u;
public static readonly PluginPanelDescriptor Descriptor = new("icons", "Icon Smoke")
{
IconSurfaceId = BareIndexIconId,
StartVisible = true,
ShowInSidePanel = true,
};
public const string Markup = """
<panel x="60" y="60" w="280" h="220" title="Icon Smoke">
<icon x="12" y="28" w="32" h="32" did="0x165" tooltip="Melee Defense skill icon (bare index 0x165)"/>
<icon x="52" y="28" w="32" h="32" did="0x06002D14" tooltip="Portal icon 0x06002D14 (literal RenderSurface DID)"/>
<icon x="92" y="28" w="32" h="32" spell="{SpellId}" tooltip="composited spell icon (first known self-buff, or Strength Other I)"/>
<button x="12" y="68" w="120" h="24" text="Report" icon="0x06002D14" iconkind="did" onclick="{Report}"/>
<list x="12" y="100" w="256" h="108" items="{SpellRows}" icons="{SpellIds}"
iconkind="spell" selected="{SelectedIndex}"/>
</panel>
""";
/// <summary>Binding object for <see cref="Markup"/>. Reads live host state
/// on every frame the same way any BCL-only plugin binding would.</summary>
internal sealed class Binding
{
private readonly IPluginHost _host;
public Binding(IPluginHost host) => _host = host;
public int SelectedIndex { get; set; } = -1;
/// <summary>The first spell a plugin markup <c>&lt;icon spell=...&gt;</c>
/// draws — the character's first known self-buff, falling back to
/// <see cref="FallbackSpellId"/> (Strength Other I) when nothing is
/// learned yet (fresh character, or no live session).</summary>
public uint SpellId
{
get
{
IReadOnlyList<PluginSpellInfo> known = _host.Automation.Spells.KnownSelfBuffs;
return known.Count > 0 ? known[0].SpellId : FallbackSpellId;
}
}
/// <summary>
/// The first five known self-buffs, or — when the character has
/// learned nothing yet — a single-entry fallback to
/// <see cref="FallbackSpellId"/>'s REAL catalog entry (never
/// fabricated art: if the host cannot resolve spell 1 either, the
/// fallback is empty and <see cref="SpellRows"/>/<see cref="SpellIds"/>
/// show the honest "no spells known" row instead).
/// </summary>
private IReadOnlyList<PluginSpellInfo> KnownSelfBuffsOrFallback()
{
IReadOnlyList<PluginSpellInfo> known = _host.Automation.Spells.KnownSelfBuffs;
if (known.Count > 0)
return known;
return _host.Automation.Spells.TryGet(FallbackSpellId, out PluginSpellInfo info)
? new[] { info }
: Array.Empty<PluginSpellInfo>();
}
/// <summary>
/// Parallel spell-id column for <see cref="SpellRows"/>, feeding the
/// list's <c>iconkind="spell"</c> composited badge. Review fix round
/// finding 2: this must be the spell id
/// (<see cref="PluginSpellInfo.SpellId"/>) that
/// <see cref="IMarkupIconResolver.ResolveSpell"/> composites a badge
/// from — NOT <see cref="PluginSpellInfo.IconId"/> (the spell's raw
/// art DID), which is what this property used to yield. The raw
/// <see cref="PluginSpellInfo.IconId"/> is still visible, printed
/// alongside the name in <see cref="SpellRows"/>, for anyone
/// comparing the composited badge against the plain art tile.
/// </summary>
public IEnumerable<uint> SpellIds
{
get
{
IReadOnlyList<PluginSpellInfo> spells = KnownSelfBuffsOrFallback();
if (spells.Count == 0)
return new uint[] { 0u };
var ids = new List<uint>();
foreach (PluginSpellInfo spell in spells)
{
ids.Add(spell.SpellId);
if (ids.Count == 5)
break;
}
return ids;
}
}
/// <summary>Row text for the first five known self-buffs (or the
/// single-entry fallback), with each spell's
/// <see cref="PluginSpellInfo.IconId"/> 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.</summary>
public IEnumerable<string> SpellRows
{
get
{
IReadOnlyList<PluginSpellInfo> spells = KnownSelfBuffsOrFallback();
if (spells.Count == 0)
return new[] { "no spells known" };
var rows = new List<string>();
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");
}
}

View file

@ -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++;
}

View file

@ -1,11 +0,0 @@
{
"version": 2,
"dependencies": {
"net10.0": {
"acdream.plugin.abstractions": {
"type": "Project"
}
},
"net10.0/linux-x64": {}
}
}

View file

@ -1,10 +0,0 @@
{
"version": 2,
"dependencies": {
"net10.0": {
"acdream.plugin.abstractions": {
"type": "Project"
}
}
}
}

View file

@ -1,11 +0,0 @@
{
"version": 2,
"dependencies": {
"net10.0": {
"acdream.plugin.abstractions": {
"type": "Project"
}
},
"net10.0/win-x64": {}
}
}