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>
135 lines
5.1 KiB
Markdown
135 lines
5.1 KiB
Markdown
# 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.
|