diff --git a/docs/plans/2026-04-11-roadmap.md b/docs/plans/2026-04-11-roadmap.md index a3aaa37e..3a6322a2 100644 --- a/docs/plans/2026-04-11-roadmap.md +++ b/docs/plans/2026-04-11-roadmap.md @@ -481,7 +481,7 @@ behavior. Estimated 17–26 days focused work, 3–5 weeks calendar. - **D.5 — Core panels.** Attributes (`chunk_00470000.c:FUN_0047ba70`), Skills (same), Paperdoll (`chunk_004A0000.c:FUN_004A5200`), Inventory, Spellbook (`chunk_004C0000.c`), Fellowship, Allegiance. Each uses the port sketches in slice 05. **(Targets `AcDream.UI.Abstractions` — ships with D.2a using ImGui-rendered widgets; reskinned by D.2b.)** The *chat* panel originally listed under D.5 shipped early in Phase I (I.4 input + I.7 combat translator superseded the chat-panel design here); this entry now covers Attributes / Skills / Paperdoll / Inventory / Spellbook / Fellowship / Allegiance only. - **✓ SHIPPED — D.5.1 — Toolbar (action bar).** Shipped 2026-06-16/17 (`30b28c2`→`0e7a083`, branch claude/hopeful-maxwell-214a12). First data-driven *game* panel: `gmToolbarUI` (`LayoutDesc 0x21000016`) — 18 shortcut slots from the persisted `PlayerDescription` SHORTCUT block, real **composited** item icons (opaque type-default underlay via the `EnumIDMap 0x10000004` resolve), **occupancy-gated slot numbers 1–9** (occupied = dark-box peace/war `0x10000042/43`; empty = background `0x1000005e` from cell composite `0x10000341`), **click-to-use** (`ItemHolder::UseObject` → `0x0036`), **peace/war stance** indicator live-wired to `CombatState`, **movable**, and a **chrome frame** (UiNineSlicePanel drawn over content via the new `UiElement.OnDrawAfterChildren` hook). New shared widgets `UiItemSlot` (`UIElement_UIItem` 0x10000032, procedural leaf) + `UiItemList` (`UIElement_ItemList` 0x10000031, factory branch) + `IconComposer` (CPU layered composite). `CreateObject.TryParse` extended to the full ACE-order weenie-header tail to capture `IconId`/`IconOverlay`/`IconUnderlay` → `ItemRepository.EnrichItem` → re-render. Spec/plan `docs/superpowers/{specs,plans}/2026-06-16-d2b-toolbar-phase1*.md`; research drop `docs/research/2026-06-16-*deep-dive.md` + synthesis. Divergence IA-16/IA-17 added. **User-confirmed** (numbers, icons, frame). Per-task spec+code-review throughout. - **✓ SHIPPED — D.5.2 — Stateful item-icon system.** Shipped 2026-06-17/18 (`419c3ac`..`fb288ad`, branch claude/hopeful-maxwell-214a12; **visually verified on a live Coldeve server**). Faithful retail icon composite (`IconData::RenderIcons` @0x0058d180): (1) `UiEffects` bitfield captured from the `CreateObject` weenie header (was discarded) → `ItemInstance.Effects`; (2) `IconComposer.GetIcon` rewritten as a 2-stage composite — Stage 1 = drag icon (base + custom overlay) + the effect treatment, Stage 2 = type-default underlay + custom underlay + drag. The effect treatment ports the **surface overload** of `SurfaceWindow::ReplaceColor` (`0x004415b0`): the textured effect tile (`EnumIDMap 0x10000005` by `LowestSetBit(effects)+1`, fallback `0x21` solid-black) is copied **per-pixel** into the icon's pure-white pixels — magical items take the tile's GRADIENT hue, mundane items go black; (3) `PublicUpdatePropertyInt (0x02CE)` parser + `WorldSession.ObjectIntPropertyUpdated` event + `GameWindow` subscription → `ItemRepository.UpdateIntProperty` → icon re-composites live. **Appraise (`0x00C9`) carries NO icon data** (ACE proof: `Icon`/`IconOverlay`/`IconUnderlay`/`UiEffects` all lack `[AssessmentProperty]`) — dropped as a no-op. **Two visual-verification fixes landed after the subagent build:** the `effects==0` recolor MUST run (mundane white edges → black, `40c97a5`) and the tint is a per-pixel GRADIENT not a flat color (the surface overload, `fb288ad`) — both confirmed via clean Ghidra + named decomp. Divergence: IA-16 retired; IA-18 (per-pixel surface-copy anti-regression) + AP-45 (0x02CE sequence) added; **AP-43/AP-44 retired by the visual fixes**. Spec/plan/research: `docs/superpowers/{specs,plans}/2026-06-17-d2b-stateful-icon*.md`, `docs/research/2026-06-17-stateful-icon-RESOLVED.md`. -- **D.5 remaining — sub-phase ledger.** D.5.1 (toolbar + the `UiItemSlot`/`UiItemList`/`IconComposer` spine) ✅, D.5.2 (stateful icons) ✅, D.5.4 (client object/item data model) ✅, D.2b-B window manager (`abbd97b`) ✅, D.2b-B B-Grid (inventory sub-window mount + `UiItemList` grid) ✅, D.2b-B B-Controller (inventory population + burden meter + captions, `03fbf44`; **visually confirmed 2026-06-21** — two render bugs fixed at the gate `417b137`: backdrop wash-out [a #145 continuation — mounted sub-window slots must keep their own frame ZLevel, not inherit the base root's 1000] + captions [drive the host UiText directly]) ✅, D.2b-B B-Wire (`EncumbranceVal`/PropertyInt 5 wire delivery, AP-48/49 retired) ✅, inventory window finish Stage 1 (scroll/frame/vertical-resize/102-slot grid) ✅, empty-slot art ✅, container-switching + open/selected indicators + main-pack icon + per-bag capacity bar ✅, B-Drag (inventory drag-drop / item moving) ✅, **Sub-phase C (paperdoll): Slice 1 equip slots ✅ + Slice 2 3-D doll `UiViewport` (Type 0xD via the `IUiViewportRenderer` RTT seam) + Slots toggle ✅ (visually confirmed 2026-06-25, `8fa66c2` — pose/camera/heading all retail-verbatim)** ✅, **UI Studio** (`AcDream.App ui-studio`) ✅, **importer dat-fidelity (Fix A/B/C)** ✅, **Character window** (`LayoutDesc 0x2100002E`, `CharacterStatController`) ✅, **Wave 3.1 unified selection** (Core `SelectionState`; AP-58 retired) ✅, and **Wave 3.2 pure item-use/drop policy** (`ItemInteractionPolicy`; AP-99 retired) ✅ are shipped. Build order from here: **finish Wave 3 activation router/cursors, then D.5.3 selected-object mana + stack entry/slider and the separate favorite-spell bars; ISSUES #146/#147/#197.** Each ☐ below gets its own focused oracle/spec/plan/gate. +- **D.5 remaining — sub-phase ledger.** D.5.1 (toolbar + the `UiItemSlot`/`UiItemList`/`IconComposer` spine) ✅, D.5.2 (stateful icons) ✅, D.5.4 (client object/item data model) ✅, D.2b-B window manager (`abbd97b`) ✅, D.2b-B B-Grid (inventory sub-window mount + `UiItemList` grid) ✅, D.2b-B B-Controller (inventory population + burden meter + captions, `03fbf44`; **visually confirmed 2026-06-21** — two render bugs fixed at the gate `417b137`: backdrop wash-out [a #145 continuation — mounted sub-window slots must keep their own frame ZLevel, not inherit the base root's 1000] + captions [drive the host UiText directly]) ✅, D.2b-B B-Wire (`EncumbranceVal`/PropertyInt 5 wire delivery, AP-48/49 retired) ✅, inventory window finish Stage 1 (scroll/frame/vertical-resize/102-slot grid) ✅, empty-slot art ✅, container-switching + open/selected indicators + main-pack icon + per-bag capacity bar ✅, B-Drag (inventory drag-drop / item moving, including retail select-before-waiting direct-drag order) ✅, **Sub-phase C (paperdoll): Slice 1 equip slots ✅ + Slice 2 3-D doll `UiViewport` (Type 0xD via the `IUiViewportRenderer` RTT seam) + Slots toggle ✅ (visually confirmed 2026-06-25, `8fa66c2` — pose/camera/heading all retail-verbatim)** ✅, **UI Studio** (`AcDream.App ui-studio`) ✅, **importer dat-fidelity (Fix A/B/C)** ✅, **Character window** (`LayoutDesc 0x2100002E`, `CharacterStatController`) ✅, **Wave 3.1 unified selection** (Core `SelectionState`; AP-58 retired) ✅, and **Wave 3.2 pure item-use/drop policy** (`ItemInteractionPolicy`; AP-99 retired) ✅ are shipped. Build order from here: **finish Wave 3 activation router/cursors, then D.5.3 selected-object mana + stack entry/slider and the separate favorite-spell bars; ISSUES #146/#147/#197.** Each ☐ below gets its own focused oracle/spec/plan/gate. - **✓ SHIPPED — UI Studio** (2026-06-26, branch `claude/hopeful-maxwell-214a12`, ~33693c6→HEAD). Standalone `AcDream.App ui-studio [--layout 0xNNNN | --dump ] [--screenshot ]` Silk tool that previews any panel through the **production renderer** (`RenderBootstrap.cs` + GameWindow untouched). Sources: 26-window retail dump (`docs/research/2026-06-25-retail-ui-layout-dump.json`) via `--dump`, or live dat import + fixtures via `--layout`. Interactive canvas (click-routing to `UiHost`, Interact/Inspect ImGui toggle), headless `--screenshot`. Architecture: `src/AcDream.App/Studio/` (`StudioWindow`, `PanelFbo`, `FixtureProvider`, `LayoutSource`) + `src/AcDream.App/Rendering/RenderBootstrap.cs`. ISSUES #156 (inventory all-black in studio) and #157 (GameWindow font resolver) filed. - **✓ SHIPPED — Importer dat-fidelity** (Fix A/B/C, 2026-06-26, same branch). **Boundary established: look = importer (font/justification/color from dat); state/behavior = runtime controller logic.** Fix A: justification property `0x14`/`0x15` → `UiText` Centered/RightAligned/VerticalJustify. Fix B: FontColor property `0x1B` → `UiText.DefaultColor`; character colors proved RUNTIME (no importer fix). Fix C: FontDid → per-element dat font via a resolver in `DatWidgetFactory` — **STUDIO path only**; GameWindow passes `null` (Issue #157). Fix 4 (UIState-group activation): INVESTIGATED — no importer fix exists; the dat has no Visible encoding; runtime `gm*UI` is correct. - **✓ SHIPPED — Wave 3.3 unified primary-click router.** `ItemInteractionController.OfferPrimaryClick` returns `NotActive` / `ConsumedSuccess` / `ConsumedRejected`; inventory, bags, main pack, paperdoll, toolbar, radar, and world clicks all offer target mode before local fallback. Rejected targets cannot drift selection or open/use the clicked object. Issue #197 and AP-107 retired; retained matrix and live ACE-provisioned healing gate user-confirmed. The 3-D doll routes through authored hit mask `0x100001D6` (resolved `UiButton` above viewport `0x100001D5`), and local health prefers authoritative private-vital state. diff --git a/docs/research/2026-07-13-retail-item-drag-visuals-pseudocode.md b/docs/research/2026-07-13-retail-item-drag-visuals-pseudocode.md index b3474783..d1ca97ad 100644 --- a/docs/research/2026-07-13-retail-item-drag-visuals-pseudocode.md +++ b/docs/research/2026-07-13-retail-item-drag-visuals-pseudocode.md @@ -51,6 +51,8 @@ ItemList.PrepareDragIcon(cell): ItemList.ItemList_BeginDrag(cell): if PrepareDragIcon(cell) failed: return + if cell is not selected: + SetSelectedObject(cell.itemID, false) if list is not vendor, salvage, or shortcut list: cell.SetWaitingState(true) StartDragAndDrop(cell.m_dragIcon, hotspot = 16,16) @@ -98,8 +100,11 @@ keeps drag-accept feedback topmost. `GetDragGhost` returns the latter, while `OnDraw` keeps the former in the source cell. - `UiRoot` owns the generic source-active lifecycle, including release outside - UI and source-subtree removal. `UiItemSlot` maps that lifecycle to the retail - waiting visual for physical lists; shortcut aliases remain unghosted. + UI and source-subtree removal. It dispatches `DragBegin` before enabling the + source-active mesh, matching retail's select-then-waiting order. The inventory, + paperdoll, and toolbar handlers select an unselected dragged item synchronously. + `UiItemSlot` maps the lifecycle to the retail waiting visual for physical lists; + shortcut aliases remain unghosted. - `UiItemSlot.WaitingSprite` is the authored `0x0600109A` mesh, not a procedural tint. @@ -107,6 +112,8 @@ keeps drag-accept feedback topmost. - `DragDropSpineTests.GetDragGhost_prefersDedicatedUnderlayFreeTexture` - `DragDropSpineTests.InventoryDrag_ghostsSourceUntilRelease` +- `InventoryControllerTests.OnDragLift_selectsItem_butKeepsItUntilServerConfirms` +- `PaperdollControllerTests.DragLift_selectsEquippedItem_beforeWaitingVisual` - `DragDropSpineTests.ShortcutDrag_doesNotGhostSource` - `UiItemSlotTests.DefaultWaitingSprite_isRetailGhostMesh` - `InventoryControllerTests.Empty_sprites_and_drop_feedback_are_applied_per_list_role` diff --git a/src/AcDream.App/UI/Layout/InventoryController.cs b/src/AcDream.App/UI/Layout/InventoryController.cs index 5db48f7a..f0307a09 100644 --- a/src/AcDream.App/UI/Layout/InventoryController.cs +++ b/src/AcDream.App/UI/Layout/InventoryController.cs @@ -422,9 +422,15 @@ public sealed class InventoryController : IItemListDragHandler, IRetainedPanelCo } // ── IItemListDragHandler (B-Drag) — drop an item to move it (optimistic + wire) ────────────── - /// Inventory items do NOT lift-remove (unlike the toolbar): the item stays in its slot - /// while reveals retail's waiting/ghosted mesh until release. - public void OnDragLift(UiItemList sourceList, UiItemSlot sourceCell, ItemDragPayload payload) { } + /// Retail ItemList_BeginDrag selects an unselected item before enabling its waiting + /// mesh. Inventory items do not lift-remove (unlike the toolbar): the item stays in its slot + /// until the server confirms the eventual drop. + public void OnDragLift(UiItemList sourceList, UiItemSlot sourceCell, ItemDragPayload payload) + { + // UIElement_ItemList::ItemList_BeginDrag @ 0x004E32D0. + if (payload.ObjId != 0 && _selection.SelectedObjectId != payload.ObjId) + _selection.Select(payload.ObjId, SelectionChangeSource.Inventory); + } /// Advisory neutral/accept/reject overlay. Shortcut aliases stay neutral; physical grid /// drops accept; a side-bag/main-pack drop rejects only when that container is known full. diff --git a/src/AcDream.App/UI/Layout/PaperdollController.cs b/src/AcDream.App/UI/Layout/PaperdollController.cs index f5b7ea0a..afb6c31a 100644 --- a/src/AcDream.App/UI/Layout/PaperdollController.cs +++ b/src/AcDream.App/UI/Layout/PaperdollController.cs @@ -283,9 +283,15 @@ public sealed class PaperdollController : IItemListDragHandler, IRetainedPanelCo } // ── IItemListDragHandler ────────────────────────────────────────────────────────────────────── - /// No-op: a wielded item stays put until the server confirms (same as the inventory grid, - /// unlike the toolbar's remove-on-lift). Unwield happens on DROP onto the pack grid — InventoryController. - public void OnDragLift(UiItemList sourceList, UiItemSlot sourceCell, ItemDragPayload payload) { } + /// Selects the wielded item before the waiting mesh appears. The item itself stays put + /// until the server confirms, like the inventory grid and unlike the toolbar's remove-on-lift. + /// Unwield happens on DROP onto the pack grid — InventoryController. + public void OnDragLift(UiItemList sourceList, UiItemSlot sourceCell, ItemDragPayload payload) + { + // UIElement_ItemList::ItemList_BeginDrag @ 0x004E32D0. + if (payload.ObjId != 0 && _selection.SelectedObjectId != payload.ObjId) + _selection.Select(payload.ObjId, SelectionChangeSource.Paperdoll); + } /// Accept iff the dragged item can be worn here (its ValidLocations intersects the slot mask). /// Retail: gmPaperDollUI::OnItemListDragOver (decomp 174302). diff --git a/src/AcDream.App/UI/Layout/ToolbarController.cs b/src/AcDream.App/UI/Layout/ToolbarController.cs index c1d6073f..8d36f5b9 100644 --- a/src/AcDream.App/UI/Layout/ToolbarController.cs +++ b/src/AcDream.App/UI/Layout/ToolbarController.cs @@ -615,6 +615,11 @@ public sealed class ToolbarController : IItemListDragHandler, IRetainedPanelCont /// public void OnDragLift(UiItemList sourceList, UiItemSlot sourceCell, ItemDragPayload payload) { + // UIElement_ItemList::ItemList_BeginDrag @ 0x004E32D0 selects first; the toolbar's + // RecvNotice_ItemListBeginDrag handler then removes the shortcut alias. + if (payload.ObjId != 0 && _selectedObjectId() != payload.ObjId) + _selectItem?.Invoke(payload.ObjId); + // Retail RecvNotice_ItemListBeginDrag → RemoveShortcut (0x004bd930/0x004bd450): the lifted // shortcut leaves the bar (+ wire) the instant the drag starts; it's re-placed only on a // drop onto a slot. Off-bar release leaves it removed. diff --git a/src/AcDream.App/UI/UiRoot.cs b/src/AcDream.App/UI/UiRoot.cs index a706dc2b..0dc075a3 100644 --- a/src/AcDream.App/UI/UiRoot.cs +++ b/src/AcDream.App/UI/UiRoot.cs @@ -757,9 +757,12 @@ public sealed class UiRoot : UiElement DragSource = source; DragPayload = payload; _dragGhost = source.GetDragGhost(); // snapshot NOW — the DragBegin handler may empty the source cell - source.SetDragSourceActive(true, payload); var e = new UiEvent(source.EventId, source, UiEventType.DragBegin, Payload: payload); source.OnEvent(in e); + // Retail UIElement_ItemList::ItemList_BeginDrag @ 0x004E32D0 selects an + // unselected item before enabling its waiting mesh. Keep that order so a + // press-drag shows the selection indicator on its very first frame. + source.SetDragSourceActive(true, payload); } private void UpdateDragHover(int x, int y) diff --git a/tests/AcDream.App.Tests/UI/DragDropSpineTests.cs b/tests/AcDream.App.Tests/UI/DragDropSpineTests.cs index 45374536..2b24b375 100644 --- a/tests/AcDream.App.Tests/UI/DragDropSpineTests.cs +++ b/tests/AcDream.App.Tests/UI/DragDropSpineTests.cs @@ -12,8 +12,12 @@ public class DragDropSpineTests public (UiItemList list, UiItemSlot cell, ItemDragPayload payload)? LastOver; public (UiItemList list, UiItemSlot cell, ItemDragPayload payload)? LastDrop; public (UiItemList list, UiItemSlot cell, ItemDragPayload payload)? LastLift; + public bool? WaitingAtLift; public void OnDragLift(UiItemList list, UiItemSlot cell, ItemDragPayload p) - { LastLift = (list, cell, p); } + { + WaitingAtLift = cell.WaitingVisual; + LastLift = (list, cell, p); + } public ItemDragAcceptance OnDragOver(UiItemList list, UiItemSlot cell, ItemDragPayload p) { LastOver = (list, cell, p); return Acceptance; } @@ -191,6 +195,8 @@ public class DragDropSpineTests root.OnMouseDown(UiMouseButton.Left, 10, 10); root.OnMouseMove(20, 10); // BeginDrag → OnDragLift + Assert.False(h.WaitingAtLift); // retail selects/lifts before enabling the mesh + Assert.True(list.Cell.WaitingVisual); root.OnMouseUp(UiMouseButton.Left, 600, 500); // release over empty space Assert.NotNull(h.LastLift); // lift happened Assert.Null(h.LastDrop); // no drop dispatched (off-bar) diff --git a/tests/AcDream.App.Tests/UI/Layout/InventoryControllerTests.cs b/tests/AcDream.App.Tests/UI/Layout/InventoryControllerTests.cs index f771017f..977c3b82 100644 --- a/tests/AcDream.App.Tests/UI/Layout/InventoryControllerTests.cs +++ b/tests/AcDream.App.Tests/UI/Layout/InventoryControllerTests.cs @@ -772,13 +772,17 @@ public class InventoryControllerTests } [Fact] - public void OnDragLift_isNoOp_itemStaysUntilServerConfirms() + public void OnDragLift_selectsItem_butKeepsItUntilServerConfirms() { var (layout, grid, _, _, _, _, _, _) = BuildLayout(); var objects = new ClientObjectTable(); SeedContained(objects, 0xA, Player, slot: 0); - var ctrl = (IItemListDragHandler)Bind(layout, objects); + var selection = new SelectionState(); + var ctrl = (IItemListDragHandler)Bind(layout, objects, selection: selection); ((IItemListDragHandler)ctrl).OnDragLift(grid, grid.GetItem(0)!, Payload(0xAu)); + + Assert.Equal(0xAu, selection.SelectedObjectId); + Assert.True(grid.GetItem(0)!.Selected); Assert.Equal(Player, objects.Get(0xAu)!.ContainerId); // NOT removed on lift (unlike the toolbar) } diff --git a/tests/AcDream.App.Tests/UI/Layout/PaperdollControllerTests.cs b/tests/AcDream.App.Tests/UI/Layout/PaperdollControllerTests.cs index 1c16e2d5..950e740f 100644 --- a/tests/AcDream.App.Tests/UI/Layout/PaperdollControllerTests.cs +++ b/tests/AcDream.App.Tests/UI/Layout/PaperdollControllerTests.cs @@ -81,6 +81,23 @@ public class PaperdollControllerTests Assert.True(lists[HeadSlot].Cell.Selected); } + [Fact] + public void DragLift_selectsEquippedItem_beforeWaitingVisual() + { + var (layout, lists) = BuildLayout(); + var objects = new ClientObjectTable(); + SeedEquipped(objects, 0xA01u, EquipMask.HeadWear); + var selection = new SelectionState(); + var ctrl = Bind(layout, objects, selection: selection); + var cell = lists[HeadSlot].Cell; + var payload = new ItemDragPayload(0xA01u, ItemDragSource.Equipment, 0, cell); + + ctrl.OnDragLift(lists[HeadSlot], cell, payload); + + Assert.Equal(0xA01u, selection.SelectedObjectId); + Assert.True(cell.Selected); + } + [Fact] public void Populate_matches_a_weapon_into_the_composite_slot() { diff --git a/tests/AcDream.App.Tests/UI/Layout/ToolbarControllerTests.cs b/tests/AcDream.App.Tests/UI/Layout/ToolbarControllerTests.cs index 30669dfc..6378af55 100644 --- a/tests/AcDream.App.Tests/UI/Layout/ToolbarControllerTests.cs +++ b/tests/AcDream.App.Tests/UI/Layout/ToolbarControllerTests.cs @@ -1010,15 +1010,18 @@ public class ToolbarControllerTests var shortcuts = new System.Collections.Generic.List { new(Index: 3, ObjectId: 0x5001u, SpellId: 0) }; var (adds, removes) = NewSpies(out var add, out var rem); + uint selected = 0; var ctrl = ToolbarController.Bind(layout, repo, () => shortcuts, iconIds: (_,_,_,_,_) => 0x77u, useItem: _ => { }, - sendAddShortcut: add, sendRemoveShortcut: rem); + sendAddShortcut: add, sendRemoveShortcut: rem, + selectItem: item => selected = item, selectedObjectId: () => selected); Assert.Equal(0x5001u, slots[Row1[3]].Cell.ItemId); var payload = new ItemDragPayload(0x5001u, ItemDragSource.ShortcutBar, 3, slots[Row1[3]].Cell); ctrl.OnDragLift(slots[Row1[3]], slots[Row1[3]].Cell, payload); + Assert.Equal(0x5001u, selected); Assert.Contains(3u, removes); Assert.Equal(0u, slots[Row1[3]].Cell.ItemId); }