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:
Erik 2026-07-11 10:34:13 +02:00
parent 6005c51c4d
commit ea72c395c9
14 changed files with 280 additions and 19 deletions

View file

@ -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