acdream/tests/AcDream.App.Tests
Erik 8217a349e0 feat(plugin-ui): Slice B — DAT icons in plugin markup (icon element, button/list icons, plugin icon ids)
Owner request: plugin panels (Decal/VirindiViewService-class, per the
MosswartMassacre reference usage) need to embed real DAT icons the way
FlagTrackerView.SafeSetListImage does — spell/skill art, raw portal
indices, and a window icon. This is Slice B of
docs/plans/2026-09-06-plugin-shelf-and-dat-icons.md (Slice A, the
movable/collapsible shelf, landed in 01b98ca30/4fada238e/718005b21).

What shipped:

- AcDream.Plugin.Abstractions.PluginIcons.Normalize: the one Decal-style
  bare-index -> 0x06xxxxxx RenderSurface DID grammar, applied at every
  icon SINK (descriptor IconSurfaceId in PluginShelfButton, and markup
  <icon did>/<button icon>/<list icons> did-kind ids) rather than on the
  plugin-facing records, which already carry real DIDs read straight
  from the client's tables.
- PluginSpellInfo.IconId / PluginSkillInfo.IconId /
  PluginInventoryItem.IconId / PluginWorldObject.IconId: additive init
  properties (default 0), filled in AppAutomationSurface from
  SpellMetadata.IconId (already projected from SpellBase.Icon by
  RetailSpellMetadataProjector — no gap there), a new BindSkillIcons
  parallel to BindSkillNames (GameWindow reads
  DatReaderWriter.Types.SkillBase.IconId — confirmed via reflection over
  the installed Chorizite.DatReaderWriter package, since its XML docs
  don't cover Pack/Unpack-generated public fields: Description, Name,
  IconId (uint), TrainedCost, SpecializedCost, Category, ChargenUse,
  MinLevel, Formula, UpperBound, LowerBound, LearnMod), and
  ClientObject.IconId in CaptureOwnedItems/ProjectWorldObject.
- IMarkupIconResolver (AcDream.App.UI): ResolveDid/ResolveSpell/
  ResolveItem. MarkupDocument.Build gains an optional parameter (null by
  default -> every icon sink resolves to nothing rather than throwing,
  so pre-Slice-B callers/tests are unaffected). RetailUiRuntime.
  MountPlugins builds ONE RetailMarkupIconResolver per pass from
  RetailUiAssets.ResolveSprite + RetailUiAssets.Icons (the shared
  IconComposer) + Toolbar.Objects (the SAME ClientObjectTable
  Magic/Toolbar bindings already borrow for their own icon resolution —
  no second object lookup introduced).
- New UiMarkupIcon widget (<icon x y w h did|spell|item tooltip>):
  exactly one source required (FormatException at Build otherwise,
  matching every other malformed-attribute rule), aspect-preserved,
  centered, click-through unless a tooltip makes it a real hit-test
  target.
- UiSimpleButton.IconSource and UiMarkupList.IconIdsSource/IconResolve:
  additive, default null/no-op, so every existing button/list caller
  (including the plugin shelf's own toggle/minimize buttons) is
  unaffected. Button icon draws flush left and shifts the caption's
  centering region right; list icons reserve a leading RowHeight-2
  column (Decal's IconColumn) and skip rows whose id is 0 or
  unresolvable.
- MarkupDocument centralizes the did/spell/item dispatch (including
  PluginIcons.Normalize for did) in two small helpers (BuildIconSource
  for <icon>/<button>, BuildRowIconResolve for <list>) so all three
  markup surfaces share one resolver call path.
- AcDream.Plugins.Smoke ships a RegisterPanelContent (in-memory KSML,
  no plugin-side .xml file) proof panel exercising every new surface:
  a bare-index <icon>, a literal-hex <icon>, a composited <icon
  spell=...>, a <button icon=...>, and a <list icons=... iconkind=
  spell> of the first five known self-buffs with their IconId printed
  alongside. Descriptor IconSurfaceId reuses the same bare index to
  prove the shelf button and the panel's own icon normalize identically.
- docs/plugin-ui-markup.md is the new SSOT for the full markup
  vocabulary + icon grammar + the Slice A shelf; linked from
  docs/README.md and docs/plans/2026-04-24-ui-framework.md.

Design decisions where the plan left room:
- Normalize runs inside the resolver dispatch (BuildIconSource/
  BuildRowIconResolve), not scattered at each markup call site, so
  every did-kind sink shares one choke point.
- did/spell/item all accept either a literal (decimal or 0x-hex) or a
  {Binding}, via one BindUintLiteralOrBinding helper, for symmetry —
  the plan only showed spell/item as bindings but didn't forbid a
  literal.
- <icon> requires exactly one source INCLUDING zero (not just two);
  an icon with no source is not a coherent element.
- The button/list icon draw math (icon column extent, padding) lives
  in the widgets themselves (UiSimpleButton/UiMarkupList), not in
  MarkupDocument, keeping the parser only responsible for wiring
  Func<(tex,w,h)> sources.

Tests: PluginIconsTests (Normalize table), MarkupIconTests (icon/button/
list resolver dispatch via a fake IMarkupIconResolver, plus draw-level
pins via the RecordingGpuDevice/TextRenderer apparatus already used by
UiAncestorClipTests/UiRenderContextDrawStringDatOutlineTests — "draws
nothing when unresolvable" and "button/list icon shifts the text"),
and AppAutomationSurfaceIconInstalledDatTests (Lane=InstalledDat: a
known spell's IconId matches the real installed SpellTable's own Icon
field exactly). Verified every new test fails to COMPILE without this
change (git-stashed the src/ changes, rebuilt the test project: CS0246
on IMarkupIconResolver) before restoring. Full App suite: 7331 passed /
97 skipped / 36 failed (identical pre-existing failure set/count to the
7306/97/36 baseline; the +25 passes are exactly the new tests).
AcDream.Plugins.MossTank.Tests (the main consumer of the touched
Plugin.Abstractions records) passes 337/337 unchanged, confirming
API-v1 binary/source compatibility. Full solution builds green.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-06 14:52:23 +02:00
..
Architecture test: replace gameplay owner source freezes 2026-08-18 16:49:38 +02:00
Audio feat(audio): Campaign A slice A2 — retail's 2D pan+gain mixer replaces AL 3D 2026-08-08 21:58:50 +02:00
Combat fix #436: combat no-target refusal reaches the SpewBox with retail's exact text 2026-08-24 12:53:11 +02:00
Composition Finish bounded lighting-observer cleanup and record verification 2026-09-05 15:15:59 +02:00
Configuration fix: complete retail parity stability pass 2026-08-28 20:01:39 +02:00
Credentials test: make prerequisite lanes fail honestly 2026-08-18 12:09:41 +02:00
Diagnostics feat(diagnostics): retain bounded local crash reports (#477) 2026-09-05 19:49:22 +02:00
Input fix(camera): port retail offset adjustment laws 2026-08-31 05:42:23 +02:00
Interaction fix(client): restore retail interaction parity 2026-08-26 20:45:11 +02:00
Net fix: complete retail parity stability pass 2026-08-28 20:01:39 +02:00
Physics fix #482: restore neighboring outdoor wall collision 2026-09-05 18:42:31 +02:00
Platform feat(mosstank): add VTank-style automation PoC 2026-08-27 18:57:21 +02:00
Plugins feat(plugin-ui): Slice B — DAT icons in plugin markup (icon element, button/list icons, plugin icon ids) 2026-09-06 14:52:23 +02:00
Rendering fix #485: isolate console capture and align Gitea portable test lanes 2026-09-06 11:41:27 +02:00
Runtime fix(client): restore retail interaction parity 2026-08-26 20:45:11 +02:00
Settings fix: complete retail parity stability pass 2026-08-28 20:01:39 +02:00
Spells feat(headless): share immutable gameplay content 2026-07-27 09:00:48 +02:00
Streaming fix #483: restore far terrain and close OVERHAUL with #484 deferred 2026-09-06 10:22:51 +02:00
UI feat(plugin-ui): Slice B — DAT icons in plugin markup (icon element, button/list icons, plugin icon ids) 2026-09-06 14:52:23 +02:00
World fix: complete retail parity stability pass 2026-08-28 20:01:39 +02:00
AcDream.App.Tests.csproj fix(rendering): split ordinary transform and sidecar indices 2026-09-05 03:19:55 +02:00
BoundedTestDatCollection.cs perf(content): remove exception-driven setup probes 2026-07-24 15:12:21 +02:00
FramePhaseTestDoubles.cs refactor(runtime): move session lifetime and ordered transport 2026-07-25 19:39:24 +02:00
FrameProfilerReportTests.cs perf(diag): complete trustworthy Slice A capture tooling 2026-07-24 12:46:51 +02:00
FrameStatsBufferTests.cs fix(pipeline): MP0 - profiler toggle-path hygiene + Max() seed (review follow-up) 2026-07-05 19:26:28 +02:00
GameRuntimeTestFactory.cs refactor(runtime): cut graphical host over to canonical root 2026-07-26 19:06:09 +02:00
GlobalUsings.cs feat(runtime): share chat commands and run login sequence 2026-08-14 20:27:45 +02:00
LiveEntityRuntimeFixture.cs test(runtime): restore the world-frame precondition across first-entry fixtures 2026-08-03 13:32:28 +02:00
LiveEntitySpawnFixture.cs feat(runtime): C3c - production placement cutover: both hosts on the residence conductors (routes 1+8) 2026-08-02 18:10:33 +02:00
packages.neutral.lock.json build: make release restore reproducible 2026-08-18 10:29:00 +02:00
RuntimeDatAccessArchitectureTests.cs feat(headless): share immutable process content 2026-07-27 08:37:24 +02:00
RuntimeDatCollectionFactoryTests.cs feat(headless): share immutable process content 2026-07-27 08:37:24 +02:00
RuntimeOptionsRetailUiTests.cs fix: complete retail parity stability pass 2026-08-28 20:01:39 +02:00
RuntimeOptionsTests.cs feat(render): S3 chunk 1 — print-only walk transcript, OH fixtures, offline signature diff 2026-09-03 11:49:42 +02:00
RuntimeWorldTransitTestDriver.cs refactor(runtime): own teleport destination correlation 2026-07-26 17:52:34 +02:00
ThreadSchedulingCollection.cs test(ci): harden scheduling and allocation gates 2026-08-27 19:28:10 +02:00
ZeroAllocationProbe.cs test(app): measure the warmed path, not the path being warmed (#250) 2026-07-29 03:42:08 +02:00
ZeroAllocationProbeTests.cs test(app): measure the warmed path, not the path being warmed (#250) 2026-07-29 03:42:08 +02:00