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:
parent
a944e49b9c
commit
30d294506c
13 changed files with 421 additions and 10 deletions
135
docs/research/2026-07-13-retail-give-item-pseudocode.md
Normal file
135
docs/research/2026-07-13-retail-give-item-pseudocode.md
Normal 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.
|
||||
Loading…
Add table
Add a link
Reference in a new issue