Commit graph

1242 commits

Author SHA1 Message Date
Erik
1be7e65fad feat(ui): D.2b inventory finish — contents grid shows full main-pack capacity
The contents grid now pads empty slot frames up to the main-pack capacity
(player ItemsCapacity, default 102 per retail "up to 102 items"), so the pack
reads like retail's fixed 102-slot grid you scroll through — not just the loose
items. Mirrors the side-bag column padding. Three existing grid tests updated
for the now-padded count; new test covers the 102-pad.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-21 21:21:20 +02:00
Erik
a1e7041ba8 feat(ui): D.2b inventory finish — vertical-only window resize
Drag the bottom edge to expand the inventory and see more of the pack
(ResizeX=false + ResizableEdges=Bottom blocks horizontal resize). The contents
grid + its gm3DItemsUI sub-window + scrollbar + the backdrop stretch vertically
(Left|Top|Bottom); the paperdoll + side-bag column stay pinned at the top
(Left|Top). The grid's ViewHeight grows → more rows + the scrollbar thumb ratio
(view/content) reflects how much is left to scroll. Min = dat default (expand
only for now); max = 560. Visually confirmed: scroll + resize work.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-21 21:17:28 +02:00
Erik
06c4288282 feat(ui): D.2b inventory finish — window chrome frame (UiNineSlicePanel)
Wrap the gmInventoryUI content in the universal 8-piece beveled chrome (same
UiNineSlicePanel as vitals/chat/toolbar), so the inventory has a proper window
frame like every other window. The frame is the draggable window + the F12-
toggled registered window; the dat content sits inside it offset by the border.
Reused as the base for the vertical-resize step next.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-21 20:55:50 +02:00
Erik
14ea938c7f fix(ui): D.2b inventory finish — grid cells exempt from the anchor pass
Root cause of the "grid escapes the window when scrolled" bug: UiElement.
DrawSelfAndChildren runs ApplyAnchor on every child AFTER OnDraw, which captured
each cell's scroll-0 position and reset Top to it every frame, fighting
LayoutCells' scroll offset. Cells are laid out procedurally by the list, so they
must be exempt — AddItem now sets cell.Anchors = None, making LayoutCells the
sole authority. Regression test reproduces the anchor-pass interaction (the unit
tests missed it by calling LayoutCells in isolation, never the draw traversal).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-21 20:51:08 +02:00
Erik
4112a53683 feat(ui): D.2b inventory finish — side-bag column slots (36px pitch + empties)
The side-bag column (0x100001CA, 36x252 = 7 slots) pads empty slot frames up to
the player's ContainersCapacity (clamped to 7), at the correct 36px pitch
(split from the contents grid's 32px). Reads like retail's bag column. Two
existing tests updated for the now-padded count (divergence AP-52: 7-slot
fallback when capacity is absent — register row added in the wrap-up commit).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-21 20:33:03 +02:00
Erik
fad807587d feat(ui): D.2b inventory finish — bind contents-grid gutter scrollbar
InventoryController binds 0x100001C7 (factory Type-11 UiScrollbar) to the
contents grid's UiScrollable + the shared 0x2100003E scrollbar sprites,
mirroring ChatWindowController. The grid now scrolls instead of overflowing.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-21 20:29:53 +02:00
Erik
0d3117596c feat(ui): D.2b inventory finish — UiItemList mouse-wheel scroll
OnEvent handles the Scroll wheel in grid mode (mirrors UiText's sign), driving
the shared UiScrollable. The bound gutter scrollbar (next task) is the primary
scroll affordance; the wheel is the convenience path.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-21 20:26:57 +02:00
Erik
366af0c34f feat(ui): D.2b inventory finish — UiItemList clip+scroll via UiScrollable
Grid mode now drives a shared UiScrollable from its content + clips whole rows
to the panel (cells offset by -ScrollY, Visible toggled by whole-row clip,
mirroring UiText). Fill mode (toolbar single cell) unchanged. LayoutCells made
internal for tests; RowCount helper added.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-21 20:25:11 +02:00
Erik
7badecf387 test(app): D.2b-B B-Wire — burden wire-first + live-refresh tests + review notes
Final-review response. Adds the spec §5 / Task-16 burden end-to-end coverage
the per-task plan missed: (1) burden reads wire EncumbranceVal over the carried
sum (asserts 50% from wire 7500, not 20% from sum 3000 — retires AP-48's
fallback as the primary), (2) a live player-int update repaints the bar (60%),
which only happens via the C1d Concerns `o.ObjectId == p` branch. Both are
discriminating (fail if the respective branch is reverted).

Plus two clarifying comments from the review: the 0x02CD player route depends on
the login PD upsert having created the player object (no-ops, no phantom, if
not); and a TODO that the container-open phase must treat ViewContents as a full
replace, not the additive merge used here.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-21 19:19:13 +02:00
Erik
aa0ecaeb4d fix(app): D.2b-B B-Wire — burden bar refreshes on player-object property update
InventoryController.Concerns() now returns true when the updated object IS the
player object itself (o.ObjectId == playerGuid). Previously the method only
triggered a repopulate for objects that the player *contains* or *wields*, but
the player's own ClientObject is the carrier of EncumbranceVal — so a live
PrivateUpdatePropertyInt for burden would be silently ignored. Task 15 of the
B-Wire plan.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-21 19:08:26 +02:00
Erik
102c46c8e3 feat(app): D.2b-B B-Wire — wire player guid into ObjectTableWiring + GameEventWiring
Pass () => _playerServerGuid to ObjectTableWiring.Wire and GameEventWiring.WireAll
so the new Batch 5/6 playerGuid parameters are populated. These were previously
left as null (default), meaning PD property upserts and PrivateUpdatePropertyInt
delivery would not route to the correct player object. Task 14 of the B-Wire plan.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-21 19:08:07 +02:00
Erik
275319461e feat(net): D.2b-B B-Wire — ObjectTableWiring applies all ints + player int + stack/remove
- Wire signature gains optional 3rd param `Func<uint>? playerGuid` (existing
  GameWindow caller `Wire(session, Objects)` still compiles — default null).
- ObjectIntPropertyUpdated gate loosened: was UiEffects-only, now applies ALL
  PropertyInt updates on visible objects (server is the authority on object props).
- PlayerIntPropertyUpdated → UpdateIntProperty(playerGuid(), ...) so live
  EncumbranceVal (0x02CD) updates the player's burden bar.
- StackSizeUpdated → UpdateStackSize(guid, stackSize, value).
- InventoryObjectRemoved → Remove(guid).
- Updated class doc-comment to list all 5 wired opcodes.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-21 19:05:00 +02:00
Erik
672df23010 feat(net): D.2b-B B-Wire — WorldSession dispatch for 0x02CD/SetStackSize/InventoryRemoveObject
Add 3 new events (PlayerIntPropertyUpdated, StackSizeUpdated, InventoryObjectRemoved)
and their payload record types near the existing ObjectIntPropertyUpdated event.
Add 3 switch cases in the GameMessage dispatcher immediately after the
PublicUpdatePropertyInt (0x02CE) branch for:
  - PrivateUpdatePropertyInt (0x02CD) — no-guid player-own property
  - SetStackSize (0x0197) — stack count + value update
  - InventoryRemoveObject (0x0024) — remove from inventory view

No test seam on the switch; verified by build + parser tests (Tasks 3/4/5)
+ live run per codebase convention (see ObjectTableWiringTests comment).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-21 19:04:22 +02:00
Erik
3a9c188233 feat(net): D.2b-B B-Wire — register ViewContents + InventoryPutObjectIn3D/SaveFailed/CloseGroundContainer
Register four previously-unwired GameEvent handlers after InventoryPutObjInContainer:

- ViewContents (0x0196): iterates ParseViewContents entries and calls
  items.RecordMembership(entry.Guid, containerId) for each — so the
  object table is correct before the container-open UI mounts.
- InventoryPutObjectIn3D (0x019A): calls items.MoveItem(guid, 0u) to
  unparent a dropped item from its container (it's now a ground object).
- InventoryServerSaveFailed (0x00A0): parse + log; rollback behavior
  deferred to B-Drag (acdream has no speculative moves yet).
- CloseGroundContainer (0x0052): parse + log; no table change needed
  (the container-open view is UI-only).

Two dispatcher tests cover ViewContents membership and Put3D unparenting.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-21 19:01:53 +02:00
Erik
d97d84d7ca feat(net): D.2b-B B-Wire — PlayerDescription delivers player properties to ClientObject
Add optional Func<uint>? playerGuid parameter (last in WireAll signature so all
existing callers compile unchanged). When provided, the PD handler calls
items.UpsertProperties(playerGuid(), p.Value.Properties) immediately after the
null-guard, landing EncumbranceVal (PropertyInt 5) and other player stats into the
player ClientObject. Upsert (create-if-absent) handles PD arriving before the
player's CreateObject. Retires AP-48/AP-49 divergence rows (wired in Task 16).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-21 19:00:59 +02:00
Erik
de8baa1aa1 feat(net): D.2b-B B-Wire — DropItem/GetAndWieldItem/NoLongerViewingContents builders + Send wrappers
Task 9 of the B-Wire inventory-wire plan. Adds three outbound C→S GameAction
builders to InventoryActions.cs (DropItem 0x001B, GetAndWieldItem 0x001A,
NoLongerViewingContents 0x0195) plus matching opcode constants after TeleToPoiOpcode.
Adds SendDropItem/SendGetAndWieldItem/SendNoLongerViewingContents wrappers to
WorldSession.cs (after SendRemoveShortcut, ~line 1157), following the exact same
NextGameActionSequence() + SendGameAction() pattern as the existing Send* family.
Three byte-layout tests added to InventoryActionsTests.cs (12 total, all green).
Core.Net build clean. Unblocks B-Drag (drop) and container-open (NoLongerViewingContents
on close) call sites in the inventory controller.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-21 18:58:30 +02:00
Erik
7013651a90 fix(net): D.2b-B B-Wire — ParseInventoryServerSaveFailed reads weenieError
0x00A0 InventoryServerSaveFailed carries (itemGuid, weenieError) per ACE
GameEventInventoryServerSaveFailed.cs and holtburger events.rs:147. The
old parser returned only the itemGuid as uint?, silently dropping the
error code. Replaced with a typed InventoryServerSaveFailed record that
reads both u32s (8-byte guard). Parser was unwired (no callers in
GameEvents.cs or GameEventWiring.cs) so the signature change is safe.
1 new test in GameEventsInventoryTests.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-21 18:55:56 +02:00
Erik
01910e3fab fix(net): D.2b-B B-Wire — ParsePutObjInContainer reads containerType (4th field)
0x0022 InventoryPutObjInContainer carries 4 u32s per ACE
GameEventItemServerSaysContainId.cs: itemGuid, containerGuid, placement,
containerType. The parser was reading only 3 (12 bytes) and silently
dropping containerType. Fixed the record struct to add ContainerType and
raised the length guard to 16. GameEventWiring caller uses only
.ItemGuid/.ContainerGuid/.Placement — adding a positional field is
source-compatible. 1 new test in GameEventsInventoryTests.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-21 18:55:14 +02:00
Erik
43afcc2adb feat(net): D.2b-B B-Wire — ParseViewContents (0x0196)
GameEvents.ParseViewContents parses the ViewContents GameEvent payload:
containerGuid + count + [guid, containerType]×count. Records
ViewContentsEntry and ViewContents added. 3 unit tests added in
GameEventsInventoryTests.cs (two-entry, zero-count, truncated).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-21 18:54:25 +02:00
Erik
79c0374d53 feat(net): D.2b-B B-Wire — InventoryRemoveObject (0x0024) parser
Top-level GameMessage (UIQueue), not a GameEvent. Server sends when an
object leaves the client's inventory view (sold / dropped / destroyed).
Layout: opcode(4) + guid(4) = 8 bytes — the smallest inventory wire.
3 tests: happy path, wrong opcode, truncated.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-21 18:52:21 +02:00
Erik
87e354f583 feat(net): D.2b-B B-Wire — SetStackSize (0x0197) parser
Top-level GameMessage (UIQueue), not a GameEvent. Server sends after a
stack merge / split to update count + value. Layout: opcode(4) + seq(1)
+ guid(4) + stackSize(4) + value(4) = 17 bytes. 3 tests: happy path,
wrong opcode, truncated. Client consumer: ACCWeenieObject::ServerSaysSetStackSize.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-21 18:51:42 +02:00
Erik
7b25d78506 feat(net): D.2b-B B-Wire — PrivateUpdatePropertyInt (0x02CD) parser
Mirror of PublicUpdatePropertyInt (0x02CE) but with no guid field —
targets the local player's own object. Burden (EncumbranceVal, PropertyInt 5)
changes ride this opcode on every pick-up / drop. Layout: opcode(4) + seq(1)
+ property(4) + value(4) = 13 bytes. 3 tests: happy path, wrong opcode, truncated.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-21 18:50:56 +02:00
Erik
3f190811be feat(core): D.2b-B B-Wire — ClientObjectTable.UpdateStackSize
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-21 18:48:54 +02:00
Erik
b56087b498 feat(core): D.2b-B B-Wire — ClientObjectTable.UpsertProperties (create-if-absent)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-21 18:48:04 +02:00
Erik
417b1375fd fix(ui): D.2b-B — inventory panels render over the backdrop (#145 continuation) + captions
Visual verification surfaced two render bugs (controller LOGIC was already correct):

1. BACKDROP WASH-OUT (the big one — #145 continuation). The mounted backpack/
   3D-items panels inherited their sub-window root's ZLevel 1000 via the merge's
   zero-wins-base rule. The #145 ZOrder fold (ReadOrder − ZLevel·10000) turned 1000
   into ZOrder ≈ −10,000,000 — sinking the panels BEHIND the frame's Alphablend
   backdrop (ZLevel 100 → ≈ −1,000,000). The backdrop then overpainted the panels'
   captions/burden-meter/cells (the paperdoll root is ZLevel 0 so it escaped, which
   is why the previous session thought #145 was done). Fix: the sub-window mount now
   keeps each slot's OWN frame ZLevel, so panels sit in front of the backdrop.
   Root-caused via a one-shot sprite-segment-order dump (backdrop was painting after
   the panel content) + a live ZLevel probe.

2. CAPTIONS. The caption elements resolve to UiText; driving a nested child UiText
   didn't paint. AttachCaption now drives the host UiText directly.

Locked by InventoryFrameImportProbe (real-dat smoke: asserts each mounted panel's
ZOrder > the backdrop's). Visually confirmed by the user: dark backdrop behind,
Burden 17% + vertical bar + Contents-of-Backpack + full item grid all visible.
Build + App(532)/Core(1526) tests green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-21 17:58:44 +02:00
Erik
1ccf07b705 fix(ui): D.2b-B — address phase-boundary review (burden % saturation + equipped filter)
Opus phase-boundary review findings:
- I1 (faithfulness): LoadToPercent now computes from the CLAMPED fill
  (floor(LoadToFill(load)*300)), so the burden % SATURATES at 300% like retail
  (decomp 176544-176576 clamps arg2 to [0,1] BEFORE the *300). The old
  floor(load*100) over-read to 400% at 4x capacity. Golden test corrected.
- I2 (partition): exclude equipped items (CurrentlyEquippedLocation != None) from
  the contents grid + selector — a mid-session self-wield routes them through
  MoveItem(item, WielderGuid=player) into GetContents(player); retail's gm3DItemsUI
  shows pack contents only. New conformance test.
- N1: drop dead 'using System.Collections.Generic' (left after the 383e8b7 cleanup).
- N3: AP-48 risk wording (drift can be low OR high vs server EncumbranceVal).

Build green; BurdenMath 17, InventoryController/UiMeter 10 tests pass.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-21 09:28:29 +02:00
Erik
03fbf4464d feat(ui): D.2b-B — wire InventoryController into the inventory-init block
Adds _inventoryController field (after _selectedObjectController) and calls
InventoryController.Bind in the existing inventory-init block, between
RegisterWindow and the Console.WriteLine. Uses Objects/iconComposer/
vitalsDatFont already in scope; strength lambda pattern-matches AttributeSnapshot
to int? (uint? has no implicit cast to int?).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-21 09:12:34 +02:00
Erik
383e8b7b55 refactor(ui): D.2b-B — Populate uses GetContents only (drop test-driven fallback)
The implementer added a full-scan fallback in Populate() to accommodate a test
that seeded items via AddOrUpdate (which deliberately does NOT touch the container
index — only Ingest/MoveItem do). That was a production workaround for a faulty
test, and inconsistent (it only triggered when the index was wholly empty).

Root-fix: Populate reads GetContents(player) only — the index IS retail's
per-container item list. The test now seeds via the faithful indexed path
(AddOrUpdate + MoveItem → Reindex) through a SeedContained helper. 530 App tests green.
2026-06-21 09:09:15 +02:00
Erik
89c640a54d feat(ui): D.2b-B — InventoryController bind + grid population (loose/side-bag partition)
Task 4: InventoryController.Bind + Populate: find-by-id bind for the 7 inventory
element ids, configure grid (6 cols x 32px) + container list, partition
GetContents(player) into loose items (contentsGrid) vs side bags (containerList),
populate main-pack cell in topContainer, subscribe ObjectAdded/Moved/Removed/Updated
for live rebuilds. Handles both Ingest-indexed (production) and AddOrUpdate-direct
(test) paths via fallback scan.

Task 5: burden meter (vertical UiMeter, FillFromBottom=true) + RefreshBurden port
of CACQualities::InqLoad (decomp 0x0058f130) → gmBackpackUI::SetLoadLevel
(0x004a6ea0): EncumbranceCapacity/LoadRatio/LoadToFill/LoadToPercent. Three
AttachCaption overlays (Burden, Contents of Backpack, %text). 5 tests all green.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-21 09:04:48 +02:00
Erik
5e75d2ac76 feat(ui): D.2b-B — UiMeter vertical fill (burden bar, retail m_eDirection 2/4)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-21 08:58:37 +02:00
Erik
fb050aed4d feat(core): D.2b-B — ClientObjectTable.SumCarriedBurden (carried-Burden fallback)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-21 08:55:35 +02:00
Erik
5875ac857d feat(core): D.2b-B — port EncumbranceSystem capacity/load + SetLoadLevel fill/percent
EncumbranceCapacity (decomp 0x004fcc00), Load (0x004fcc40), SetLoadLevel
fill=load/3 clamped + percent=floor(load*100) (0x004a6ea0). Golden tests.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-21 08:54:16 +02:00
Erik
45a5cc5bb6 fix(ui): #145 — importer honors ZLevel so the backdrop sits behind panels
The factory mapped ZOrder from ReadOrder only, so the gmInventoryUI
full-window backdrop (0x100001D0, ReadOrder 4) painted over the nested
panels (ReadOrder 1-3). Wire ElementDesc.ZLevel through ElementInfo /
ToInfo / Merge and fold it into ZOrder = ReadOrder - ZLevel*10000 (higher
ZLevel = further back, ReadOrder the within-layer tiebreaker). Vitals
(all ZLevel 0) are unchanged; chat (ZLevel 900) + toolbar (1,2) shift to
their dat layering — verify visually.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-21 07:22:26 +02:00
Erik
7d971a2b3e feat(ui): D.2b-B — F12 shows the real gmInventoryUI (0x21000023)
Swap the Sub-phase A placeholder UiNineSlicePanel for LayoutImporter.Import
(0x21000023). The sub-window mount nests the paperdoll/backpack/3D-items
panels; the window starts hidden, registered under WindowNames.Inventory,
toggled by F12 via the window manager (unchanged).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-20 22:37:01 +02:00
Erik
85098f535d feat(ui): D.2b-B — sub-window mount (inheritor attaches base subtree)
LayoutImporter.Resolve now captures the resolved base element's children
and, for a pure-container leaf (no own children + no own media) inheriting
from a base WITH content, attaches that subtree. This pulls the nested
gmInventoryUI panels' content (paperdoll/backpack/3D-items) in through the
existing BaseElement+BaseLayoutId path. ShouldMountBaseChildren is a pure,
unit-tested predicate; it's inert for media-bearing inheritors and childless
style prototypes, so vitals/chat/toolbar are unaffected (existing importer
tests still green).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-20 22:36:20 +02:00
Erik
4fd4b09f3f feat(ui): D.2b-B — UiItemList N-cell grid mode
Columns + CellWidth/CellHeight + a row-major CellOffset/LayoutCells pass.
CellWidth<=0 keeps the single-cell fill mode (toolbar slot sizes to the
list — unchanged); CellWidth>0 tiles cells in a grid. Layout runs on
AddItem and per-frame in OnDraw so a list resize reflows.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-20 22:34:30 +02:00
Erik
882b4dd5d3 feat(ui): D.2b-A — F12 toggles placeholder inventory window
UiHost RegisterWindow/ToggleWindow forwarders to UiRoot. A default-hidden
placeholder inventory window mounts in the RetailUi block + registers as
WindowNames.Inventory. OnInputAction handles the existing F12-bound
ToggleInventoryPanel action -> _uiHost.ToggleWindow (no-op when retail UI
off; gated by WantsKeyboard so it can't fire while typing in chat).

Placeholder is throwaway scaffolding; Sub-phase B swaps in the real
gmInventoryUI (0x21000023) under the same registry name.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-20 21:32:17 +02:00
Erik
036db8b8ab feat(ui): D.2b-A — BringToFront + raise on show/click
BringToFront sets a window's ZOrder one past the max among its peers.
ShowWindow now raises on open; OnMouseDown raises any pressed top-level
window (retail-faithful stacking). Existing drag/resize tests unaffected
(raise only touches ZOrder, not geometry).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-20 21:31:29 +02:00
Erik
6409038576 feat(ui): D.2b-A — UiRoot named-window registry (Show/Hide/Toggle)
A Dictionary<string,UiElement> registry on UiRoot with RegisterWindow +
Show/Hide/Toggle. Show/Hide flip UiElement.Visible (already gates
Draw/Tick/HitTest); Toggle returns the new visibility; unknown names are
no-ops. WindowNames.Inventory const shared by mount/registry/toggle.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-20 21:30:22 +02:00
Erik
14443e5c27 fix(ui): D.2b — collapsed toolbar left black pillars + draggable below the bar
Two bugs from the visual gate, same dump-confirmed cause: row 2 is an 11-element
band (left edge-piece 0x100006B6 + 9 slots 0x100006B7..BF + right edge-piece
0x100006C0, all at content-y 90), but SecondRow hid only the 9 slots — so the two
edge-pieces kept drawing as black pillars below the collapsed bar. And toolbarRoot
(ClickThrough=false, full height) still caught clicks below the collapsed frame →
walked up to the Draggable frame → phantom window-move.

Fix: SecondRow now hides the full band (B6..C0); toolbarRoot.ClickThrough=true so
its children (slots/indicators) still get hits children-first but its empty/below
regions fall through (no phantom drag) — same pattern as the chat content panel.
The slots are edge-flag 0 = Left|Top fixed, so nothing reflows. Build green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-20 18:52:49 +02:00
Erik
f74e017509 feat(ui): D.2b — toolbar collapse-to-one-row (bottom-edge snap resize hides/shows row 2)
UiElement.MaxHeight + ResizableEdges mask; UiCollapsibleFrame snaps height to the nearer
of {collapsed,expanded} and toggles row-2 visibility; GameWindow computes the two heights
from the layout + top-anchors the content. Amends IA-17. UiNineSlicePanel unsealed to
allow subclassing.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-20 18:33:35 +02:00
Erik
ff3592ec25 refactor(core): D.5.3/B.2 — move ShortcutStore to Core.Items (decouple Load from the wire type)
Matches the ClientObjectTable model-placement convention; Load now takes (slot,objGuid) pairs so
the store has no Core.Net dependency. + self-drop wire-count assert + comment fix.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-20 15:50:33 +02:00
Erik
250f7827be feat(ui): D.5.3/B.2 — toolbar reorder/remove via ShortcutStore + wire + green-cross overlay
ToolbarController is now the LIVE drag handler:
- OnDragLift: removes the source slot from ShortcutStore, sends
  RemoveShortcut (0x019D) wire immediately (retail remove-on-lift model).
- HandleDropRelease: evicts occupant from target (RemoveShortcut),
  places dragged item (AddShortcut 0x019C), bumps evicted item into
  the vacated source slot if empty (swap path); off-bar release leaves
  the lift's removal standing.
- Populate() now lazy-loads ShortcutStore from the PD shortcut list on
  first call; store is authoritative thereafter.
- IsShortcutGuid() uses the store (O(18)) when loaded, falls back to
  scanning _shortcuts() in the pre-PD window.
- All 18 slot cells now get DragAcceptSprite=0x060011FA (green cross,
  distinct from the inventory ring 0x060011F9).
- GameWindow.Bind wired: sendAddShortcut + sendRemoveShortcut lambdas
  forwarded to _liveSession?.Send*().
- Divergence register: AP-47 Divergence+Risk cells updated (opacity
  corrected, underlay-backing framing improved). TS-33 row deleted
  (stopgap retired). Section header 32→31 rows.
- Tests: HandleDropRelease_isInertStub removed; 5 new B.2 tests added
  (lift removes + sends, swap sequence, empty-target place, self-drop
  re-adds, green-cross sprite). 24/24 ToolbarController tests pass.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-20 15:33:23 +02:00
Erik
41bb70c11a feat(ui): D.5.3/B.2 — spine: drag-lift hook + ghost snapshot (full opacity) + drop-on-hit-only
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-20 15:22:27 +02:00
Erik
a497cc631e test(core): D.5.3/B.2 — move ShortcutStoreTests to Core.Net.Tests (Rule 6) + cosmetic cleanups
- Move ShortcutStoreTests from AcDream.Core.Tests to AcDream.Core.Net.Tests (Rule 6:
  tests live in the project matching the layer under test; ShortcutStore is Core.Net)
- Replace fully-qualified System.Buffers.Binary.BinaryPrimitives. with BinaryPrimitives.
  in the two new BuildAddShortcut tests (file already has `using System.Buffers.Binary`)
- Add `using System;` to ShortcutStore.cs; change System.Array.Clear → Array.Clear
  (matches sibling file style, no behavior change)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-20 15:18:35 +02:00
Erik
745a92bbae feat(core): D.5.3/B.2 — ShortcutStore + AddShortcut/RemoveShortcut wire + senders
ShortcutStore lands in AcDream.Core.Net.Items (not AcDream.Core.Items) because
it depends on PlayerDescriptionParser.ShortcutEntry; placing it in AcDream.Core
would create a circular dependency (Core.Net already references Core). The test
lives in AcDream.Core.Tests which gets Core.Net transitively via App.

BuildAddShortcut signature corrected from the old (seq, slotIndex, objectType,
targetId) 4×u32 layout to the retail ShortCutData wire format confirmed in the
action-bar deep-dive: Index(u32), ObjectId(u32), SpellId(u16), Layer(u16).
The old BuildAddShortcut_ThreeFields test is replaced by two new tests that
verify both item and spell shortcut packing.

WorldSession gains SendAddShortcut / SendRemoveShortcut following the
SendChangeCombatMode sender pattern.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-20 15:09:58 +02:00
Erik
acdefc2e21 fix(ui): D.5.3/B.1 — dragging a toolbar item moved the window instead of the item
Found at visual verification: an occupied UiItemSlot sits inside the Draggable
toolbar frame (UiNineSlicePanel.Draggable=true), so UiRoot.OnMouseDown's FindWindow
returned the frame and the window-move branch won — press+drag on a slot moved the
whole bar instead of picking up the item. The slot wasn't CapturesPointerDrag (that
path is for self-driven text-selection and suppresses the BeginDrag promotion), and
UiRoot had no path for "a drag-source inside a draggable window."

Fix: add UiElement.IsDragSource (virtual, default false); UiItemSlot overrides it to
`ItemId != 0` (occupancy-gated). UiRoot.OnMouseDown now prioritizes IsDragSource over
window-move — an OCCUPIED slot starts the item drag (promotes to BeginDrag on >3px),
an EMPTY slot falls through to the IA-12 whole-window-drag so the bar stays movable
by its empty cells / chrome. UiRoot stays item-agnostic (reads only the bool). This
REDUCES divergence (occupied cells now drag like retail) within IA-12's umbrella — no
new register row.

Regression tests reproduce the LIVE topology (slot inside a Draggable frame); the
earlier RootWithBoundSlot tests put the slot directly under the root, so they could
not catch it. Full suite 493 pass / 0 fail / 2 skip.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-20 14:26:53 +02:00
Erik
3d04f61451 feat(ui): D.5.3/B.1 — toolbar drag handler stub + spine wiring (TS-33)
Implements Task 4 of the drag-drop spine: ToolbarController now satisfies
IItemListDragHandler. The ctor loop wires RegisterDragHandler(this) on every
slot list and stamps Cell.SlotIndex + Cell.SourceKind=ShortcutBar, matching
retail's gmToolbarUI::PostInit/RegisterItemListDragHandler pattern. OnDragOver
accepts any non-empty payload (TS-33 stub; eligibility gate is Stream B.2).
HandleDropRelease logs and returns (no AddShortcut 0x019C / RemoveShortcut
0x019D wire yet). Three new B.1 xUnit tests cover handler registration, accept,
and inert-stub semantics. All 490 app tests pass.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-20 13:00:20 +02:00
Erik
595c4bdac4 feat(ui): D.5.3/B.1 — UiRoot payload injection + cursor drag ghost (AP-47)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-20 12:51:01 +02:00
Erik
1672eaa620 feat(ui): D.5.3/B.1 — UiItemSlot drag source + drop target + accept/reject overlay
- UiElement: two new virtuals GetDragPayload()/GetDragGhost() (default null)
  keep UiRoot item-agnostic; any leaf can opt into drag by overriding these.
- UiItemSlot: SlotIndex + SourceKind properties for payload identity; two
  overrides return ItemDragPayload / icon ghost when the slot is occupied.
  FindList() walks the parent chain to locate the owning UiItemList and its
  registered IItemListDragHandler.
- UiItemSlot.OnEvent: MouseDown now just consumes the press; use-item fires
  on Click (mouse-up) so a drag doesn't also trigger the use-item callback.
  DragEnter → ask handler, set Accept/Reject overlay. DragOver → reset to
  None (fires on leave). DropReleased → clear overlay + dispatch to handler
  when Data0 == 1 (accepted). DragBegin consumed (source).
- OnDraw: accept/reject sprite overlay drawn last, guarded on id != 0 to
  avoid the resolve(0)-→-magenta footgun.
- ToolbarControllerTests: Click_emitsUseForBoundItem changed from MouseDown
  to Click to match the new dispatch.
- 12 new DragDropSpineTests pass; full suite 481/483 (2 pre-existing skips).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-20 12:40:53 +02:00