fix(ui): port retail partial stack transfers
Route the selected stack quantity through retail GetObjectSplitSize semantics, send exact split-to-container and split-to-ground actions, and keep the original object in place until the server publishes the newly guided stack. Cover selection scoping, wire bytes, container placement, and world drops with conformance tests. Co-Authored-By: Codex <codex@openai.com>
This commit is contained in:
parent
6005c51c4d
commit
ea72c395c9
14 changed files with 280 additions and 19 deletions
|
|
@ -2050,7 +2050,7 @@ See divergence register **AP-59**.
|
|||
|
||||
## #141 — Toolbar interactivity — selected-object display
|
||||
|
||||
**Status:** IN PROGRESS (health/name/flash DONE and visually confirmed 2026-06-20; formatted stack count + entry/slider implemented 2026-07-11, visual gate pending; mana still deferred). Renumbered from #140 on the 2026-06-20 main merge — A7 Fix D held #140 on main; this branch's commits/spec still reference #140.
|
||||
**Status:** IN PROGRESS (health/name/flash DONE and visually confirmed 2026-06-20; formatted stack count + entry/slider visually confirmed 2026-07-11; partial stack-transfer live gate pending; mana still deferred). Renumbered from #140 on the 2026-06-20 main merge — A7 Fix D held #140 on main; this branch's commits/spec still reference #140.
|
||||
**Severity:** MEDIUM
|
||||
**Filed:** 2026-06-17
|
||||
**Component:** ui — D.5 toolbar / selection
|
||||
|
|
@ -2060,7 +2060,7 @@ See divergence register **AP-59**.
|
|||
**Root cause / status:** The selection-state wire was deferred out of D.5.1 scope; the meter/slider elements are present in LayoutDesc 0x21000016 but hidden (no backing data). D.5.3 is the planned port.
|
||||
- **D.5.3a (2026-06-18):** the Health meter (0x100001A1) + the object-name line (0x1000019F) + the overlay state (0x100001A0) are wired via `SelectedObjectController` (port of `gmToolbarUI::HandleSelectionChanged`); `SelectionChanged` event on `GameWindow`; `QueryHealth (0x01BF)` sent on select. Spec/plan: `docs/superpowers/specs|plans/2026-06-18-d53a-*`. **Still deferred:** the Mana meter (0x100001A2 — owned-item-only; no remote-target mana path yet). Divergence row AP-46.
|
||||
- **D.5.3a visual gate PASSED (2026-06-20):** name top-aligned in the bar sprite's black band, friendly NPCs/Doors name-only, players/monsters get the bar (gated on PWD BF_ATTACKABLE/BF_PLAYER), bar appears on assess/damage (UpdateHealth-driven, AP-47 retired), brief green selection flash. Fixed during the gate: the two magenta end-lines (UiMeter.DrawHBar resolved slice id 0 → 1x1 magenta placeholder → 1px caps), the stack-entry black box (hid 0x100001A3), and the flash being eaten by a framebuffer-dump diagnostic. Commits `8f627cc` (fixes), `0796585` (CLI apparatus). **Remaining for #141:** Mana meter (0x100001A2).
|
||||
- **D.5.3b implementation (2026-07-11):** stacked selection formats the retail `"%d %hs"` count/name, now preserving the wire `PluralName` selected by `NAME_APPROPRIATE`; reveals authored entry `0x100001A3` and horizontal DAT slider `0x100001A4`; initializes to the full stack; clamps entry edits; honors the entry's DAT right alignment; and uses the exact 1000-step slider conversion. The thumb retains its raw pointer position so minimum/maximum reach both endpoints. One Core `StackSplitQuantityState` feeds both controls and the selected-source merge amount. Warning-free App Release + 4,691 tests; live visual gate pending.
|
||||
- **D.5.3b implementation (2026-07-11):** stacked selection formats the retail `"%d %hs"` count/name, now preserving the wire `PluralName` selected by `NAME_APPROPRIATE`; reveals authored entry `0x100001A3` and horizontal DAT slider `0x100001A4`; initializes to the full stack; clamps entry edits; honors the entry's DAT right alignment; and uses the exact 1000-step slider conversion. The thumb retains its raw pointer position so minimum/maximum reach both endpoints. One Core `StackSplitQuantityState` feeds the controls plus selected-source merge, container split (`0x0055`), and ground split (`0x0056`). `GetObjectSplitSize @ 0x00586F00` ensures unselected objects still move their full stack. Partial transfers do not optimistically move the original because ACE creates the destination object with a new guid. Warning-free App Release + 4,697 tests; controls/polish visually confirmed, partial-transfer live action gate pending.
|
||||
|
||||
**Files:** `src/AcDream.App/UI/Layout/ToolbarController.cs` + the selection/WorldPicker state (see `claude-memory/project_interaction_pipeline.md`).
|
||||
|
||||
|
|
|
|||
|
|
@ -490,7 +490,7 @@ behavior. Estimated 17–26 days focused work, 3–5 weeks calendar.
|
|||
- **✓ 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.
|
||||
- **✓ IMPLEMENTED — Wave 4.4a selected-stack controls.** `SelectedObjectController` formats retail's count-first `NAME_APPROPRIATE` stack name from the wire plural, reveals the authored right-aligned numeric entry and horizontal DAT slider, initializes both to the full stack, refreshes them on selected-object stack changes, and shares one Core split quantity with inventory merge. The thumb owns its raw pointer position so quantity 1 and maximum reach the exact endpoints; entry writes retain retail's `split/max` attribute behavior. Exact cdb-pinned 1000-step rounding, plural fallback, real-DAT media/alignment, and pointer drag have conformance tests. Warning-free App Release build and 4,691 tests pass; live visual gate pending 2026-07-11. Selected-item mana and remaining toolbar buttons stay under AP-101.
|
||||
- **✓ IMPLEMENTED — Wave 4.4a selected-stack controls and transfers.** `SelectedObjectController` formats retail's count-first `NAME_APPROPRIATE` stack name from the wire plural, reveals the authored right-aligned numeric entry and horizontal DAT slider, initializes both to the full stack, refreshes them on selected-object stack changes, and shares one Core split quantity with every item-holder transfer path. The thumb owns its raw pointer position so quantity 1 and maximum reach the exact endpoints; entry writes retain retail's `split/max` attribute behavior. Exact `ItemHolder::GetObjectSplitSize` selection scoping now drives merge, split-to-container `0x0055`, and split-to-ground `0x0056`; partial splits wait for the server-created object instead of moving the source optimistically. Exact cdb-pinned 1000-step rounding, plural fallback, real-DAT media/alignment, pointer drag, and wire bytes have conformance tests. Warning-free App Release build and 4,697 tests pass; controls/polish are live-confirmed and partial-transfer live gate is pending 2026-07-11. Selected-item mana and remaining toolbar buttons stay under AP-101.
|
||||
- **✓ 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.
|
||||
|
|
|
|||
|
|
@ -794,6 +794,44 @@ UseWithTarget (0x0035): u32 sourceObjectId, u32 targetObjectId
|
|||
|
||||
## 8. `AttemptPlaceIn3D` pseudocode
|
||||
|
||||
### 8.1 Shared selected-stack amount
|
||||
|
||||
```text
|
||||
GetObjectSplitSize(item): // 0x00586F00
|
||||
if item.id == selectedID:
|
||||
return GenItemHolder.splitSize
|
||||
if item.stackSize != 0:
|
||||
return item.stackSize
|
||||
return 1
|
||||
```
|
||||
|
||||
The quantity field is therefore global UI state, but it applies only to the
|
||||
currently selected object. Dragging any other stack uses that object's full
|
||||
quantity.
|
||||
|
||||
### 8.2 Container move versus container split
|
||||
|
||||
After the legality, capacity, and auto-merge branches choose a destination,
|
||||
retail makes this exact decision:
|
||||
|
||||
```text
|
||||
AttemptToPlaceInContainer(itemId, containerId, ..., placement): // 0x00588140
|
||||
...legality and destination selection...
|
||||
if splitSize >= maxSplitSize:
|
||||
item.UIAttemptPutInContainer(containerId, placement)
|
||||
return true
|
||||
item.UIAttemptSplitToContainer(containerId, placement, splitSize)
|
||||
return true
|
||||
|
||||
UIAttemptSplitToContainer(item, containerId, placement, amount): // 0x0058D7D0
|
||||
Event_StackableSplitToContainer(item.id, containerId, placement, amount)
|
||||
```
|
||||
|
||||
The partial path sends GameAction `0x0055` with
|
||||
`(stackGuid, containerGuid, placement, amount)`. It does not move the original
|
||||
client object into the destination: the server reduces that source stack and
|
||||
creates a distinct destination object with a new guid.
|
||||
|
||||
```text
|
||||
AttemptPlaceIn3D(itemId, targetId, allowGroundFallback): // 0x00588600
|
||||
if player not ready for inventory request: return false
|
||||
|
|
@ -873,6 +911,11 @@ The vendor-drop test is now pinned. Matching-executable disassembly at
|
|||
`0x00000200 = BF_VENDOR`. It is an object-trait test, distinct from
|
||||
`ClientUISystem.vendorID`.
|
||||
|
||||
`UIAttemptSplitTo3D @ 0x0058D850` dispatches
|
||||
`CM_Inventory::Event_StackableSplitTo3D @ 0x006ABFC0`, GameAction `0x0056`
|
||||
with `(stackGuid, amount)`. Like the container split, the source object remains
|
||||
in place until authoritative server updates arrive.
|
||||
|
||||
---
|
||||
|
||||
## 9. Target-mode click ordering
|
||||
|
|
|
|||
|
|
@ -509,8 +509,9 @@ server confirmation despite its name. Release build is warning-free and 4,660 te
|
|||
pass. The compatible-stack live gate passed 2026-07-11 and exposed the expected
|
||||
Wave 4.4/AP-101 selected-stack gap. Wave 4.4a now implements that gap: exact count-first
|
||||
name formatting, authored entry/slider media, one Core split-quantity owner, live
|
||||
stack-size refresh, and selected-source merge amount. The inventory-button and
|
||||
selected-stack visual gates remain pending.
|
||||
stack-size refresh, and selected-source item-holder amounts. Controls/polish passed
|
||||
the live gate. Partial container/ground transfers now send retail `0x0055/0x0056`;
|
||||
their live action gate remains pending. The inventory-button gate remains pending.
|
||||
|
||||
### 4.4 Finish toolbar controls
|
||||
|
||||
|
|
@ -526,7 +527,7 @@ selected-stack visual gates remain pending.
|
|||
- [ ] Replace the PWD-only health gate with the exact attackable/player/pet policy.
|
||||
- [ ] Add item-mana query/cancel state and health query cancellation.
|
||||
- [x] Add one shared split-quantity owner for the entry, slider, and selected-source
|
||||
item-holder merge payload.
|
||||
item-holder merge, container-split, and ground-split payloads.
|
||||
|
||||
**Wave 4.4a implementation (2026-07-11):** static cdb against the matching retail
|
||||
binary pinned stack-name `"%d %hs"`, numeric entry `"%d"`, and the slider's exact
|
||||
|
|
@ -535,11 +536,16 @@ binary pinned stack-name `"%d %hs"`, numeric entry `"%d"`, and the slider's exac
|
|||
`InventoryController` consumes the value only when the dragged source is selected.
|
||||
The horizontal scrollbar remains the generic retained `UiScrollbar` in scalar mode,
|
||||
using the DAT track/thumb rather than panel-local drawing. The warning-free App Release
|
||||
build and 4,691-test suite are green. Gate polish preserves CreateObject
|
||||
build and 4,697-test suite are green. Gate polish preserves CreateObject
|
||||
`PluralName` through Core.Net into `ClientObject.GetAppropriateName`, honors the
|
||||
entry's authored right alignment, and lets the pointer-owned thumb remain at exact
|
||||
minimum/maximum endpoints instead of snapping back from the quantized amount. The
|
||||
live visual gate follows.
|
||||
controls/polish live gate passed. `ItemHolder::GetObjectSplitSize @ 0x00586F00`
|
||||
now scopes that quantity to the selected object for all transfers. Partial inventory
|
||||
moves send `StackableSplitToContainer 0x0055`; partial drag-out drops send
|
||||
`StackableSplitTo3D 0x0056`; neither path moves the original object optimistically
|
||||
because the authoritative destination stack receives a server-assigned guid. The
|
||||
partial-transfer live action gate follows.
|
||||
|
||||
**Wave 4.1 implementation (2026-07-11):** bare `1..9` now use slots 0..8,
|
||||
Ctrl+`1..9` selects them, Alt+`5..9` uses slots 13..17, and `0` creates a
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue