feat(ui): complete retail item drop branches

Give retained buttons a reusable item-drop seam, wire the toolbar backpack target, and port retail stack-merge legality, capacity clamping, wire dispatch, destination selection, and immediate shortcut rekey notice. Record the live ACE merge gate and keep split quantity under AP-101.

Co-Authored-By: Codex <codex@openai.com>
This commit is contained in:
Erik 2026-07-11 09:46:32 +02:00
parent 3281e0acb4
commit dc1649c493
19 changed files with 479 additions and 28 deletions

View file

@ -489,6 +489,7 @@ behavior. Estimated 1726 days focused work, 35 weeks calendar.
- **✓ SHIPPED — Wave 4.2 exact shortcut records.** One Core-owned `ShortcutEntry` preserves retail's signed `index_`, unsigned `objectID_`, and raw 32-bit `spellID_` through PlayerDescription parse, the nullable 18-slot session store, drag snapshots/reindexing, WorldSession, and exact 12-byte AddShortcut packing. The visible `gmToolbarUI` remains object-only; `favorite_spells_[8]` remains a separate future spell-bar source. AP-103 retired. Full automated conformance and live mutate→clean-relog persistence gate passed 2026-07-11.
- **✓ SHIPPED — Wave 4.3a shortcut-alias boundary.** Drag feedback now preserves retail's neutral/accept/reject distinction. Inventory and paperdoll apply the `(DropItemFlags & 0xE) == 0` physical-item gate, so a toolbar alias can reorder within the toolbar but can never move, unwield, wield, or drop the object it references. Live equipped-helmet gate passed 2026-07-11: inventory drop removed only the shortcut.
- **✓ SHIPPED — Wave 4.3b source-aware toolbar transactions.** Pure Core `ShortcutDropPlanner` ports `gmToolbarUI::HandleDropRelease @ 0x004BE7C0`: fresh inventory drops displace cyclically right (with wrap), shortcut aliases restore to their vacated source, duplicate objects are removed before placement, full bars intentionally lose the displaced entry when retail finds no empty slot, and every transaction is planned before local mutation then emitted in exact Remove/Add order. Full-stack rekey planning preserves the raw spell word. AP-102 retired; 4,646 automated tests pass; live inventory-to-occupied displacement and toolbar reorder gate passed 2026-07-11.
- **✓ SHIPPED — Wave 4.3c remaining drop branches + stack merge.** `UiButton` now has a reusable retained item-drop-target seam; gmToolbarUI's inventory/backpack button accepts only fresh physical items, draws the exact authored green-arrow overlay `0x060011F7`, and sends them to the player container while shortcut aliases remain neutral. Pure Core `StackMergePlanner` ports `IsMergeAttemptLegal @ 0x00586F30` and `AttemptMerge @ 0x005878F0`; inventory drops try legal same-WCID stack merges before normal insertion, clamp `0x0054` amount to destination capacity, immediately broadcast retail's misleadingly named `FullMergingItem` notice to rekey the toolbar, and select the destination. Existing destination shortcuts are removed before rekey. Warning-free Release build and 4,660 tests pass; live stack-merge gate passed 2026-07-11. Backpack-button drop gate remains pending.
- **✓ SHIPPED — Character window** (`LayoutDesc 0x2100002E`, `CharacterStatController`, 2026-06-26, same branch). **Visually user-confirmed 2026-06-26 — Attributes tab reads as retail.** Three tabs, header (name/heritage/PK), large-gold level number (dat font, `largeDatFont` 18px), "Total Experience (XP):" + "XP for next level:" captions, 9-row attribute list (icons + right-aligned values + Health/Stamina/Mana vitals), click-to-select (top/bottom selection bars + footer State-B "{Attr}: {value}" / "Experience To Raise: Infinity!" + affordability-gated raise triangles), centered footer. User noted "still needs some polish for later" — deferred to Issue #158.
- **✓ SHIPPED — D.5.4 — Client object/item data model (foundation).** Shipped 2026-06-18 (`b506f53`..`a33e897`, 11 commits). Renamed `ItemRepository``ClientObjectTable` / `ItemInstance``ClientObject`; broadened the table to hold EVERY server object (retail `weenie_object_table` shape). `CreateObject` is now the canonical merge-upsert (`ClientObjectTable.Ingest`, retail `SetWeenieDesc` semantics) via a new Core.Net `ObjectTableWiring` (off GameWindow); `DeleteObject` evicts; `PlayerDescription` is a membership manifest (`RecordMembership`); live container-membership index (`GetContents`, retail `object_inventory_table`). `_liveEntityInfoByGuid` retired (selection/describe resolve from the one table). Root fix: the old enrich-existing-only `EnrichItem` dropped `CreateObject`s for items with no `PlayerDescription` stub — live-Coldeve 4/6 hotbar slots blank; items are now created, not dropped. **Crux resolved:** retail is TWO tables (`object_table` + `weenie_object_table`), NOT one — acdream's `WorldEntity` (3D system) + `ClientObjectTable` (data/UI) split was already architecturally faithful; the fix was the ingestion path, not a table unification. 2671 tests green.
- **Roadmap correction (2026-07-10):** the completion order is now the architecture-first campaign in `docs/superpowers/plans/2026-07-10-retail-ui-fidelity-completion.md`. Retail `gmToolbarUI` is object-only: preserve `ShortCutData.index_`, `objectID_`, and `spellID_`, but do not invent spell glyphs on this bar. `PlayerModule::favorite_spells_[8]` feeds separate spell bars.

View file

@ -401,7 +401,7 @@ All rows are `outbound` direction (GameActions are client→server only).
| 0x0048 | outbound | CastUntargetedSpell | `CM_Magic::Event_CastUntargetedSpell` | W | H | B | B+W | CastSpellRequest builder dead [^a-1] |
| 0x004A | outbound | CastTargetedSpell | `CM_Magic::Event_CastTargetedSpell` | W | H | B | B+W | CastSpellRequest builder dead [^a-1] |
| 0x0053 | outbound | ChangeCombatMode | `CM_Combat::Event_ChangeCombatMode` | W | H | W | B+W | Wired in WorldSession.SendChangeCombatMode |
| 0x0054 | outbound | StackableMerge | `CM_Inventory::Event_StackableMerge` | W | H | B | B+W | InventoryActions builder dead [^a-1] |
| 0x0054 | outbound | StackableMerge | `CM_Inventory::Event_StackableMerge` | W | H | W | B+W | Wired through `StackMergePlanner` → inventory drop → `WorldSession.SendStackableMerge` |
| 0x0055 | outbound | StackableSplitToContainer | `CM_Inventory::Event_StackableSplitToContainer` | W | H | B | B+W | InventoryActions builder dead [^a-1] |
| 0x0056 | outbound | StackableSplitTo3D | `CM_Inventory::Event_StackableSplitTo3D` | | H | B | B+W | InventoryActions builder dead [^a-1] |
| 0x0058 | outbound | ModifyCharacterSquelch | `CM_Communication::Event_ModifyCharacterSquelch` | | H | | B | Mute one player |

View file

@ -212,7 +212,7 @@ where I cite holtburger it is `inventory/actions.rs` or `inventory/events.rs`
| `0x001B` | DropItem | C→S | drop an item on the ground | `GameActionDropItem.Handle` | `u32 itemGuid` (holtburger `actions.rs:140`) | **MISSING** (no builder; acdream reuses 0x0019 for moves only) |
| `0x0035` | UseWithTarget | C→S | use src item on target (key→door) | (Interact) | `u32 sourceGuid, u32 targetGuid` | **parsed**`InteractRequests.BuildUseWithTarget` |
| `0x0036` | UseItem | C→S | use/equip-by-doubleclick a single item | `GameActionUseItem` | `u32 targetGuid` | **parsed**`InteractRequests.BuildUse` |
| `0x0054` | StackableMerge | C→S | drop stack A onto compatible stack B | `GameActionStackableMerge.Handle` | `u32 mergeFromGuid, u32 mergeToGuid, i32 amount` | **parsed** — `InventoryActions.BuildStackableMerge` |
| `0x0054` | StackableMerge | C→S | drop stack A onto compatible stack B | `GameActionStackableMerge.Handle` | `u32 mergeFromGuid, u32 mergeToGuid, i32 amount` | **wired** — `StackMergePlanner``InventoryController``WorldSession.SendStackableMerge` |
| `0x0055` | StackableSplitToContainer | C→S | split N off a stack into a pack slot | `GameActionStackableSplitToContainer.Handle` | `u32 stackGuid, u32 containerGuid, i32 place, i32 amount` | **parsed**`InventoryActions.BuildStackableSplitToContainer` |
| `0x0056` | StackableSplitTo3D | C→S | split N off a stack onto the ground | `GameActionStackableSplitTo3D.Handle` | `u32 stackGuid, i32 amount` | **parsed**`InventoryActions.BuildStackableSplitTo3D` |
| `0x019B` | StackableSplitToWield | C→S | split N off a stack into an equip slot (e.g. arrows) | `GameActionStackableSplitToWield` | `u32 stackGuid, u32 equipMask, i32 amount` | **parsed**`InventoryActions.BuildStackableSplitToWield` |

View file

@ -422,7 +422,7 @@ an occupied slot sends the displaced item to the first empty slot cyclically to
the right of the target. Only a dragged shortcut alias attempts to restore the
displaced item to `m_lastShortcutNumDragged`.
### 4.6 Full-stack merge rekey
### 4.6 Stack-merge attempt rekey
```text
RecvNotice_FullMergingItem(oldId, newId): // 0x004BE9B0
@ -435,6 +435,39 @@ This body updates the first matching shortcut returned by `RemoveShortcut`.
Whether duplicate object shortcuts can exist through external player-description
data is unresolved; `CreateShortcutToItem` normally prevents duplicates.
The notice name is misleading: `ItemHolder::AttemptMerge @ 0x005878F0` broadcasts
`SendNotice_FullMergingItem(oldId, newId)` immediately after `UIAttemptMerge`, for
every legal merge request. It is not an ACE confirmation and is not conditional on
the source stack being exhausted. `CreateShortcutToItem` also removes any existing
`newId` shortcut before adding it at the old source shortcut's slot.
```text
IsMergeAttemptLegal(sourceId, targetId, quiet): // 0x00586F30
if player not ready for inventory request: return false
if sourceId == targetId: return false
source = lookup sourceId; target = lookup targetId
if either missing: return false
if source.maxStackSize <= 1 or target.maxStackSize <= 1: return false
if source.tradeState == 1 or target.tradeState == 1: return false
if source.wcid != target.wcid: return false
return target.stackSize < target.maxStackSize
AttemptMerge(sourceId, targetId, quiet): // 0x005878F0
if not IsMergeAttemptLegal(...): return false
requested = sourceId == selectedId ? splitSize : max(1, source.stackSize)
available = target.maxStackSize - max(1, target.stackSize)
amount = min(requested, available)
source.UIAttemptMerge(targetId, amount) // sends 0x0054
SendNotice_FullMergingItem(sourceId, targetId) // immediate local notice
Select(targetId)
return true
```
acdream's selected-object stack entry/slider is still absent under AP-101, so the
current inventory merge path requests the whole source stack and lets the same
retail capacity clamp limit the transfer. Once the shared split-quantity owner
lands, it supplies `requested` without changing legality or wire ordering.
---
## 5. Selected-object health, mana, and stack behavior

View file

@ -471,7 +471,7 @@ same slot after a clean reconnect.
### 4.3 Port every drag/drop branch
- [ ] Add a pure `ShortcutDropPlanner` keyed by source kind: toolbar reorder,
- [x] Add a pure `ShortcutDropPlanner` keyed by source kind: toolbar reorder,
inventory fresh add, occupied target, inventory-button target,
off-bar removal, and full-stack merge replacement.
- [x] For an inventory-to-occupied-slot drop, place the displaced shortcut at
@ -479,7 +479,7 @@ same slot after a clean reconnect.
- [x] Validate the complete mutation before changing state, apply it atomically, and
emit exact Remove/Add wire ordering. Use pending/confirm/rollback only where the
protocol actually exposes rejection; never leave a partially mutated bar.
- [ ] On full-stack merge, rekey every matching object shortcut and preserve the
- [x] On full-stack merge, rekey the matching object shortcut and preserve the
non-object fields of its raw `ShortCutData` entry.
**Wave 4.3 partial implementation (2026-07-11):** drag-over feedback is now an
@ -499,9 +499,18 @@ records, exact wire order, and raw full-stack rekeying have conformance fixtures
`ToolbarController` applies the validated local transaction atomically, then emits
its ordered Remove/Add events. AP-102 is retired; 4,646 tests pass. The live
inventory-to-occupied displacement plus toolbar-reorder gate passed 2026-07-11.
Inventory-button placement and
the upstream full-stack-merge notice owner remain part of the unchecked umbrella
items above rather than being implied complete by this slice.
Wave 4.3c completes the umbrella: retained buttons can own item-drop callbacks;
the toolbar inventory button uses retail's fresh-item gate and exact green-arrow
overlay, while aliases stay neutral. Inventory now tries the pure named-retail
stack-merge legality/amount planner before ordinary insertion, sends `0x0054`,
broadcasts `FullMergingItem` immediately from the attempt owner, rekeys the toolbar
with existing-destination removal, and selects the target. The notice is not a
server confirmation despite its name. AP-101 still covers the absent shared split
entry/slider; until it lands, merge requests use the whole source stack before the
retail destination-capacity clamp. Release build is warning-free and 4,660 tests
pass. The compatible-stack live gate passed 2026-07-11; it exposed the expected
Wave 4.4/AP-101 gap that the selected strip still displays one instead of the
stack count and has no split slider. The inventory-button live gate remains pending.
### 4.4 Finish toolbar controls