acdream/docs/research/2026-07-13-retail-give-item-pseudocode.md
Erik 30d294506c 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>
2026-07-13 20:28:48 +02:00

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 @ 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

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.