From b41ccc87a09628f6d67e55fbfc0b5c4fb1c0a3ba Mon Sep 17 00:00:00 2001 From: Erik Date: Sun, 6 Sep 2026 13:13:32 +0200 Subject: [PATCH] docs(plugin-ui): plan the movable plugin shelf and DAT-icon markup slices Owner pivot 2026-09-06: before MossTank feature work resumes, the plugin shelf must be movable and easy to hide, and plugin markup must embed DAT icons the way Decal/VirindiViewService plugins (MosswartMassacre) do. This plan is the contract for Slice A (shelf) and Slice B (icons): verified current-state facts, behavior, files, tests, review lenses, and scope. Co-Authored-By: Claude Fable 5.1 --- .../2026-09-06-plugin-shelf-and-dat-icons.md | 254 ++++++++++++++++++ 1 file changed, 254 insertions(+) create mode 100644 docs/plans/2026-09-06-plugin-shelf-and-dat-icons.md 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 new file mode 100644 index 000000000..2c94c004e --- /dev/null +++ b/docs/plans/2026-09-06-plugin-shelf-and-dat-icons.md @@ -0,0 +1,254 @@ +# Plugin shelf mobility + DAT icons in plugin markup + +Date: 2026-09-06 +Status: ACTIVE — prerequisite for the MossTank plugin work that follows +Owner direction: plan (Fable) → implement (Sonnet) → review (Opus, two lenses) + +## Why + +Two owner requests before MossTank feature work resumes: + +1. The plugin shelf (`PluginSidePanel`, the right-edge strip of plugin-window + buttons) must be **movable** and **easy to hide**. Today it is pinned to the + right screen edge every tick, is not draggable, has no hide affordance, and + its position is never persisted. +2. Plugin panels must be able to **embed icons from the game's DAT files**, the + way Decal/VirindiViewService plugins do. The owner's own Decal plugin, + MosswartMassacre (`C:\Users\erikn\source\repos\MosswartMassacre`), is the + reference usage: `DecalControls.IconColumn` list columns fed by + `HudPictureBox.Image = iconId`, icon ids resolved from Decal's + `FileService.SpellTable` (spell art) and `SkillTable` (skill art), raw + portal.dat indices normalized by adding `0x06000000`, and a `` + window icon. See `MosswartMassacre/Views/FlagTrackerView.cs` + (`SafeSetListImage`) and `MosswartMassacre/FlagTrackerData.cs` + (`GetSpellIcon`, `GetSkillIconId`). + +## Non-negotiables + +- Plugins stay **BCL-only**. Nothing in `AcDream.Plugin.Abstractions` may + reference App/UI types. Plugins hand the host DAT ids; the host owns every + texture. +- API-v1 source/binary compatibility: every abstraction change is **additive** + (new `init` properties with defaults, new default-interface members). +- No new default-on behavior, no env-var probes. Persisted state goes through + the existing `SettingsStore` / `RetailWindowLayoutPersistence` path only. +- The shelf must never disable a plugin or dispose a window. Hiding is + presentation only (existing invariant, keep the existing tests green). +- Retail's UI lock (`UiRoot.UiLocked`, retail `@lockui`) must also lock the + shelf's drag, exactly like every other retained window. + +## Current state (verified 2026-09-06, HEAD `0275b4ca5`) + +| Thing | Where | Fact | +|---|---|---| +| Shelf | `src/AcDream.App/UI/PluginSidePanel.cs` | `UiPanel` subclass; `Draggable=false`; `OnTick` forces `Left = parent.Width - Width - 4`; `Top = 116`; `Visible = EntryCount > 0`; not registered with `RetailWindowManager`; disposed at `RetailUiRuntime.cs:5583`. | +| Shelf construction | `RetailUiRuntime.MountPlugins` (`RetailUiRuntime.cs:4637-4710`) | Built lazily on the first `ShowInSidePanel` plugin window; `Host.Root.AddChild(_pluginSidePanel)`. | +| Plugin windows | same method | `MarkupDocument.Build(xml, binding, Assets.ResolveSprite, Assets.Controls, Assets.DefaultFont)`; registered as `plugin:{pluginId}:{windowId}` with `Host.WindowManager.Register`; geometry/visibility persist via `RetailWindowLayoutPersistence` (attaches on `WindowRegistered`). | +| Window layout schema | `src/AcDream.UI.Abstractions/Panels/Settings/UiWindowLayout.cs` | `X, Y, Width, Height, Visible, Collapsed, Maximized, AuthoredGeometryRevision` — `Collapsed` already exists and is captured/restored through `IRetainedWindowStateController` (`RetailWindowLayoutPersistence.cs:249,296`). | +| Retail plugin-manager chord | `InputAction.TogglePluginManager` (Shift+Ctrl+F1, retail id `0x7C`) | Handler at `RetailUiRuntime.cs:1044` only prints "The retail plugin manager is not available in acdream." | +| Markup vocabulary | `src/AcDream.App/UI/MarkupDocument.cs` | `panel` root; `group, meter, label, button, tab, toggle, slider, field, menu, list`. Bindings are `{PropertyName}` reflection reads per frame. `resolve: Func` is `TextureCache.GetOrUploadRenderSurface` (any `0x06xxxxxx` RenderSurface DID). | +| List widget | `src/AcDream.App/UI/UiMarkupList.cs` | text rows + optional per-row colors; no icon column. | +| Button widget | `UiSimpleButton` in `src/AcDream.App/UI/UiPanel.cs:161` | rect + text; no icon. | +| Descriptor icon | `PluginPanelDescriptor.IconSurfaceId` | already drawn on the shelf button (`PluginShelfButton.OnDraw`); zero → initials. No normalization: a Decal-style bare index (`7735`) resolves to nothing. | +| Composited icons | `src/AcDream.App/UI/IconComposer.cs` | `GetSpellIcon(spellId)` = retail `CompositeSpellIcon` (power backing + art + tint + self/fellow overlay); `GetIcon(itemType, iconId, underlay, overlay, effects)` = retail item icon. Available to the UI runtime as `RetailUiAssets.Icons`. | +| Plugin-visible icon data | `src/AcDream.Plugin.Abstractions/Automation.cs` | `PluginSpellComponentInfo.IconId` exists. `PluginSpellInfo`, `PluginSkillInfo`, `PluginInventoryItem`, `PluginWorldObject` carry **no** icon id. `ClientObject` has `IconId/IconUnderlayId/IconOverlayId` (`src/AcDream.Core/Items/ClientObject.cs:201`). `AppAutomationSurface` implements `ISpellCatalog` and reads the DAT SpellTable. | +| Tests | `tests/AcDream.App.Tests/UI/PluginSidePanelTests.cs`, `MarkupDocumentTests.cs`, `MarkupPanelClickTests.cs` | harness pattern: `new UiRoot{Width,Height}`, `root.WindowManager.Register`, fake `resolve` (`_ => (1u,32,32)`), `root.Tick(...)`, `OnEvent(new UiEvent{...})`. | +| Smoke plugin | `src/AcDream.Plugins.Smoke/SmokePlugin.cs` | no UI; copied into `plugins/AcDream.Plugins.Smoke/` by `AcDream.App.csproj`. User plugins load from `{ConfigDirectory}/plugins` (`Program.cs:186`). | + +## Slice A — movable, hideable plugin shelf + +### Behavior + +1. **The shelf is a retained window.** Register it with + `Host.WindowManager.Register("plugin-shelf", shelf, shelf, controller)` the + moment it is created in `MountPlugins`. That gives it drag, z-order, + `RetailWindowLayoutPersistence` (position + visibility + collapsed, per + character and resolution) and the UI lock for free. Name it exactly + `plugin-shelf` (stable persisted key; document it). +2. **Default dock is unchanged.** With no saved layout, the shelf sits where it + sits today (right edge, top 116). The edge dock is computed once when the + parent has a real size (first tick with `parent.Width > 0`) and **only while + the user has never moved it** — after a drag, or after a restored layout, + the shelf stays where it was put. Reflow on entry add/remove keeps the + shelf's **top-right corner** fixed when it is docked and its **top-left** + fixed otherwise, so growth never pushes it off screen. Keep the existing + `KeepWindowReachable` clamp for the shelf itself on display resize. +3. **Drag grip.** A grip strip across the top of the shelf (≈10 px tall, full + width, drawn with a subtle three-line glyph or dotted texture in the + existing border color) is the drag handle. Set `Draggable = true` on the + shelf; verify in `UiRoot` that the shelf buttons (which handle clicks) + do not promote to a window drag and that the grip does. Respect + `ConstrainDragToParent = true`. +4. **Collapse toggle.** The grip carries a small toggle at its right end + (`»` when expanded, `«` when collapsed; use the DAT font). Collapsed = + only the grip (with the toggle) remains, buttons hidden, width shrinks to + the grip's minimum. Collapsed state persists through `UiWindowLayout.Collapsed` + by implementing `IRetainedWindowStateController` on the shelf (capture/ + restore). A collapsed shelf still reflows its hidden button set so + expanding is instant. +5. **Hide/show.** Re-purpose `InputAction.TogglePluginManager` + (Shift+Ctrl+F1, retail's plugin-manager chord — acdream has no other + plugin manager, so this is the honest home). Handler in `RetailUiRuntime`: + - shelf exists and visible → `Hide()`; + - shelf exists and hidden → `Show()` (and expand if collapsed); + - no shelf (no plugin windows registered) → keep today's system message + but reword to "No plugin windows are registered." Update the + Configure Keyboard caption for the action if a caption table names it + ("Plugin Manager" → "Plugin Shelf"); check `RetailActionIdentityTable` + and the keymap caption source before renaming anything retail-derived + and cite what you found in the commit message. + - Hidden state persists (window layout `Visible`). New plugin windows + registering while the shelf is hidden do **not** show it. + - Also add a `/plugins` chat verb? **No.** Out of scope. +6. **Minimize buttons on plugin windows** stay as they are. + +### Files + +- `src/AcDream.App/UI/PluginSidePanel.cs` — grip, toggle, docking rule, + `IRetainedWindowStateController`, remove the per-tick right-edge pin. +- `src/AcDream.App/UI/RetailUiRuntime.cs` — register the shelf window in + `MountPlugins`; `TogglePluginManager` handler; shutdown path unchanged + (unregister happens through the window manager's own disposal — verify). +- Tests in `tests/AcDream.App.Tests/UI/PluginSidePanelTests.cs` (extend; keep + every existing test green): + - default dock when no layout, top-right anchored growth; + - drag via grip moves the shelf and the position survives a reflow; + - drag refused while `UiLocked`; + - collapse hides buttons, shrinks width, `CaptureWindowState().Collapsed` + round-trips through `RestoreWindowState`; + - toggle action: visible→hidden→visible; hidden shelf stays hidden when a + new plugin window registers; + - persistence: `RetailWindowLayoutPersistence` round-trips X/Y/Visible/ + Collapsed for `plugin-shelf` (use the existing persistence test harness + if one exists; otherwise a focused test with a temp `SettingsStore`). + +### Acceptance + +`dotnet build` green; `dotnet test tests/AcDream.App.Tests --filter +"FullyQualifiedName~PluginSidePanel|FullyQualifiedName~RetailWindowLayout"` +green; then the full App suite green. Connected visual gate by the owner +(after Slice B): drag the shelf, collapse, hide with Shift+Ctrl+F1, relog, +confirm it comes back where it was. + +## Slice B — DAT icons in plugin markup + +### Plugin-facing contract (BCL-only, additive) + +1. **Icon id grammar (`PluginIcons`).** New static class + `AcDream.Plugin.Abstractions.PluginIcons` with + `static uint Normalize(uint idOrIndex)`: `0 → 0`; values below + `0x01000000` are Decal/VVS-style bare portal indices and become + `0x06000000 + value`; anything else is returned unchanged. The host applies + `Normalize` at **every** icon sink (descriptor `IconSurfaceId`, ``, `