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>
5.1 KiB
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 @ 0x00588600ACCWeenieObject::UIAttemptGive @ 0x0058D620CM_Inventory::Event_GiveObjectRequest @ 0x006ABB00ItemHolder::GetObjectSplitSize @ 0x00586F00PlayerModule::DragItemOnPlayerOpensSecureTrade @ 0x005D31B0- SmartBox drag release call site at
0x004E5C32 InventoryRequest::IR_GIVE = 0x9innamed-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
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
- Keep
ItemInteractionPolicy.DecidePlacementas the single pure policy. - Pass the exact drag-release coordinates through the existing world picker and call the controller with its target GUID (or zero).
- Execute
GiveToTargetthrough a newWorldSession.SendGiveObjectwrapper; leave the object table unchanged until ACE's response arrives. - Feed
PlayerDescription.Options1into the controller so the player branch uses the real retail secure-trade preference rather than a constant. - Add policy/controller/session/wiring tests for NPCs, stack quantities, non-optimistic state, the player option, and exact packet bytes.
- Build, run the full test suite, commit, then visually gate the starter token turn-in and teleport.