feat(inventory): port retail item giving (#216)

Preserve SmartBox drag-release coordinates, route the picked 3-D target through the retail AttemptPlaceIn3D policy, and send authoritative GiveObject requests with the selected stack quantity. Honor PlayerDescription's player secure-trade option and document the remaining trade-system boundary.

Co-Authored-By: Codex <noreply@openai.com>
This commit is contained in:
Erik 2026-07-13 20:28:38 +02:00
parent a944e49b9c
commit 30d294506c
13 changed files with 421 additions and 10 deletions

View file

@ -0,0 +1,135 @@
# Retail item giving — pseudocode and implementation plan (2026-07-13)
## Scope
Port the retail inventory-drag path needed to hand a starter-dungeon exit
token to its NPC. The same path also covers direct gifts to players when the
retail `DragItemOnPlayerOpensSecureTrade` character option is disabled.
This is **not** targeted item use. Retail has no `TARGET_MODE_GIVE`: the player
drags the physical item icon out of an item list and releases it over the 3-D
target.
## Retail sources
- `ItemHolder::AttemptPlaceIn3D @ 0x00588600`
- `ACCWeenieObject::UIAttemptGive @ 0x0058D620`
- `CM_Inventory::Event_GiveObjectRequest @ 0x006ABB00`
- `ItemHolder::GetObjectSplitSize @ 0x00586F00`
- `PlayerModule::DragItemOnPlayerOpensSecureTrade @ 0x005D31B0`
- SmartBox drag release call site at `0x004E5C32`
- `InventoryRequest::IR_GIVE = 0x9` in `named-retail/acclient.h`
The earlier complete placement translation remains in
`docs/research/2026-07-10-retail-toolbar-interaction-pseudocode.md` §8. This
note pins the live integration seam that was still missing.
## Cross-reference
ACE's `GameActionGiveObjectRequest.Handle` reads `(targetGuid, objectGuid,
int32 amount)` and calls `Player.HandleActionGiveObjectRequest`. The server
validates ownership/amount/trade state, walks the player into range, then
routes to `GiveObjectToNPC` or `GiveObjectToPlayer`. Accepted full-stack NPC
gifts send `InventoryRemoveObject`; accepted partial gifts send an
authoritative stack-size update. Rejected gifts send
`InventoryServerSaveFailed`. Therefore the client must not remove or resize
the source optimistically.
The inventory wire catalog independently cross-checked ACE and holtburger in
`docs/research/2026-06-16-inventory-deep-dive.md` §4: action `0x00CD`, payload
`u32 target, u32 item, i32 amount`. The existing acdream builder already
matches that layout byte-for-byte.
## Retail pseudocode
```text
SmartBox left-button release while dragging an inventory item:
target = object under the release point, or 0 when none
success = AttemptPlaceIn3D(draggedItem, target, allowGroundFallback=true)
if not success:
clear the item's waiting state
AttemptPlaceIn3D(itemId, targetId, allowGroundFallback):
if player cannot make an inventory request: return false
item = lookup(itemId)
if item missing: return false
if targetId == playerId:
return PlaceInBackpack(itemId)
if item is not owned by player:
show "You must first pick up ..."
return false
if item.tradeState != 0:
show traded-item error
return false
if targetId == 0:
goto DROP_ON_GROUND when fallback is allowed
return false
if AttemptMerge(itemId, targetId): return true
target = lookup(targetId)
if target missing:
goto DROP_ON_GROUND when fallback is allowed
return false
if target has BF_VENDOR:
sell only a full stack; otherwise reject
return false
if DragItemOnPlayerOpensSecureTrade and target.IsPlayer:
AttemptToTradeItem(targetId, itemId)
return false
if target.InqType() == ItemType.Creature:
item.UIAttemptGive(targetId, GetObjectSplitSize(item))
return true
if target.IsContainer:
require unlocked + currently open, then place in it
if fallback is allowed: goto DROP_ON_GROUND
show "Cannot give ..."
return false
DROP_ON_GROUND:
require player on ground
if selected amount < full stack:
send StackableSplitTo3D(itemId, amount)
else:
send DropItem(itemId)
return true
UIAttemptGive(targetId, amount):
if player cannot make an inventory request: return
Event_GiveObjectRequest(targetId, this.id, amount)
prevRequestObjectID = this.id
prevRequest = IR_GIVE
prevRequestTime = now
Event_GiveObjectRequest(targetId, itemId, amount):
send GameAction 0x00CD(targetId, itemId, amount)
```
`GetObjectSplitSize` uses the global selected quantity only when the dragged
object is the selected object; any other stack uses its full current size.
The retail default character-options mask `0x50C4A54A` includes
`DragItemOnPlayerOpensSecureTrade` (`0x04000000`). Consequently a normal
drag onto another player opens secure trade by default; a direct player gift
uses `0x00CD` only when that option is off. `AllowGive` (`0x40`) is the
recipient-side option and ACE validates it server-side.
## Implementation plan
1. Keep `ItemInteractionPolicy.DecidePlacement` as the single pure policy.
2. Pass the exact drag-release coordinates through the existing world picker
and call the controller with its target GUID (or zero).
3. Execute `GiveToTarget` through a new `WorldSession.SendGiveObject` wrapper;
leave the object table unchanged until ACE's response arrives.
4. Feed `PlayerDescription.Options1` into the controller so the player branch
uses the real retail secure-trade preference rather than a constant.
5. Add policy/controller/session/wiring tests for NPCs, stack quantities,
non-optimistic state, the player option, and exact packet bytes.
6. Build, run the full test suite, commit, then visually gate the starter
token turn-in and teleport.