# 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.