Transfer Pipeline
This page explains what happens when the player drops an item over a slot, an inventory area, or a drop zone.
The transfer pipeline checks whether the action is allowed, chooses the behavior for the target, and either moves the item or leaves the inventories in a valid state.
Big Picture
flowchart TD
DROP["Player drops an item"] --> TARGET{"Where was it dropped?"}
TARGET --> SLOT["On a slot"]
TARGET --> AREA["On an inventory area"]
SLOT --> CHECK{"Can the slot accept the item?"}
AREA --> AUTO["Find a suitable slot"]
AUTO --> CHECK
CHECK -->|yes| MOVE["Move the item or merge the stack"]
CHECK -->|no| POLICY{"What does policy allow?"}
POLICY -->|Reject| REJECT["Reject the transfer and return the item"]
POLICY -->|Swap| SWAP["Try swap"]
POLICY -->|Find alternative| ALTERNATIVE["Try another slot"]
ALTERNATIVE --> CHECK
MOVE --> SYNC["Update UI and game data through DataBinding"]
SWAP --> SYNC
What Happens During Transfer
A typical transfer works like this:
- The system resolves the source, target, and active drop policy settings.
- If the inventories use different item models, the item is converted to the target inventory model.
- The system checks whether the selected target can accept the item.
- If the target is valid, the item is moved or merged into the stack in the target slot.
- If the target is not valid,
DropPolicySettingsdecides what happens next according to the selected options: reject the transfer, find another place, or try swap. - On success, events are sent and DataBinding is updated. On failure, the state is restored to the saved copy that was made before the transfer attempt.
Cases
Blocked Target
The selected slot cannot accept the item: it is occupied, incompatible, or full. What
happens next is controlled by BlockedTargetResolutionKind:
| Value | Behavior |
|---|---|
Reject |
The transfer entry fails. Nothing moves. |
FindAlternative |
The strategy provides available candidates, and the configured PlacementCandidateOrderer sorts them by priority. |
Swap |
The system attempts a single swap with the occupied target. |
AllowSameInventoryAlternativePlacement controls whether FindAlternative, when
moving an item inside the same inventory onto an occupied slot, may choose another
slot inside that same inventory. If it cannot, the item remains where it was before
the transfer attempt.
Area Drop / Auto Transfer
There is no concrete slot: the player dropped onto the inventory area, or
double-click / auto-transfer started AutoTransferService. The engine skips explicit
target handling and immediately asks the strategy for candidates, then orders them.
Large Stack Split Across Several Placements
A stack of 10 is still one transfer entry even if the target distributes it across several placements. The engine:
- creates a working stack of the requested size;
- asks the strategy for candidate slots with free capacity, such as one item per slot
for
UniqueItemStrategy; - takes exactly that amount and applies the candidate;
- processes candidates again against the changed state and repeats until the stack or the inventory slots run out;
- returns anything that did not fit to the source.
Example: 10 items into a Unique inventory with 6 free slots means 6 are transferred and 4 are returned.
Partial Or Require Full
The _allowPartial policy setting decides what happens when only part of a transfer
entry fits:
true=Allow— commit what fits and return the rest to the source.false=RequireFull— if the whole amount cannot be placed, roll the entry back.
Moving Inside One Inventory
When moving inside one inventory, the source is not blindly freed. A partial attempt keeps the source occupied and excludes it from the candidate list, so target placements cannot take the same cells that the remainder would need to return to. A full move may run a separate attempt with the source temporarily removed, and it must move the whole entry or roll back.
Occupied Target Handler
If an item is dropped onto an occupied slot, the target inventory DataBinding can take over that case through IOccupiedSlotDropHandler.
There are two timing variants:
| Interface | When it is checked | When to use |
|---|---|---|
IPreRuleOccupiedSlotDropHandler |
before target slot drop rules | dropping onto the occupied slot really means another destination, such as a container inside that slot |
IPostRuleOccupiedSlotDropHandler |
after target slot drop rules | custom behavior must still pass the target's normal rules first |
ExecuteOccupiedSlotDrop returns OccupiedSlotDropResult:
Handled— the handler performed the action itself; regular placement, alternative placement, and swap do not run.Rejected— the handler rejected the action; the transfer is cancelled and state is restored to the saved copy.Fallthrough— the handler does not take over this attempt; the regular occupied-slot pipeline continues.
Batch
flowchart TD
A["Validate request once<br/>CanStartTransfer / CanStartTransferAsync"] --> B["Entry commit 1"]
B --> C["Entry 2 sees the committed result of entry 1"]
C --> D["Entry 3 ..."]
D --> E["Transfer ends"]
- Transfer entries run in drag-context order; each sees the committed result of the previous one.
- A failed entry rolls back by itself; previous transfers remain.
- The selected target slot applies only to the first entry; the rest behave like an area drop.
Batch swap, meaning several dragged items onto one occupied target with Swap, is
rejected before any mutation.
Partial transfer is applied per entry. RequireFull never cancels previous entries
in the batch.
Validation
Several kinds of checks can allow or block a transfer, and they run at different times.
| Layer | Interface | Question | When it runs |
|---|---|---|---|
| Rules | IGlobalRule / IInventoryRule / ISlotRule |
Can this item mechanically go here? Type filter, locked slot, and similar checks. | Drag start and target validation |
| Transfer-wide veto | ITransferDomainHandler.CanStartTransfer |
Can this operation start at all? | Once, before the first mutation |
| Async transfer-wide veto | IAsyncTransferDomainHandler.CanStartTransferAsync |
Same, but the answer needs waiting for something such as a server or disk. | Once, async path only, before the first mutation |
| Commit check | ITransferDomainHandler.CanCommitTransfer |
Can this specific placement be committed? Enough gold, ownership, etc. | Right before each candidate mutation |
| Success side effects | ITransferDomainHandler.OnTransferSucceeded |
React after a placement has been committed. | After a successful commit |
Rules are attached directly to slot or inventory objects. DataBinding also exposes
similar hooks by default: CanStartDrag, CanDrop, and CanSwap. Bindings can
implement ITransferDomainHandler and IAsyncTransferDomainHandler for additional
checks and actions that apply to the whole transfer.
When Events Fire
Events and DataBinding notifications for an entry are sent only after that entry is committed and before the next one starts. This ensures that the next entry sees the same state as the live inventory.
Each add/remove event carries the exact sub-stack that actually moved, never the raw
DragEntry.Stack. An entry of 10 that lands as 6 + 4 sends events for 10 adapters
in total; an entry of 10 where 6 moved and 4 returned sends events for 6.
Extension Points
Everything intended for plugging in custom logic is listed here:
| Extension point | Type | Purpose |
|---|---|---|
| Placement strategy | IStrategy / InventoryStrategyBase |
Defines how items occupy slots: unique, stackable, separable, or custom merge/create/capacity logic. Read-only; returns candidates. |
| Topology | IInventoryTopology (SlotTopology, RectGridTopology, custom) |
Defines the cell space: cell count, shape projection, orientation steps, and visual angles. |
| Placement shape | IPlacementShape (RectPlacementShape, ComplexPlacementShape) |
Defines an item's footprint, including non-rectangular shapes and their rotations. |
| Candidate orderer | PlacementCandidateOrderer |
Controls which slot automatic placement prefers, such as merge-first or empty-first. It is not used for an explicit target. |
| Blocked target policy | BlockedTargetResolutionKind + DropPolicySettings |
Chooses reject / find-alternative / swap, plus partial-transfer and same-inventory options. Data-only, configured in the Inspector. |
| Transfer-wide veto | ITransferDomainHandler.CanStartTransfer |
Allow or deny the whole operation before anything changes. |
| Async transfer-wide veto | IAsyncTransferDomainHandler.CanStartTransferAsync |
Same, when the answer requires waiting for a server, file, or database. |
| Business commit check | ITransferDomainHandler.CanCommitTransfer |
Allow or deny one specific placement, such as checking gold or ownership. |
| Success hook | ITransferDomainHandler.OnTransferSucceeded |
Apply side effects after a committed placement. |
| Occupied-slot handler | IPreRuleOccupiedSlotDropHandler / IPostRuleOccupiedSlotDropHandler |
Custom behavior when dropping onto an occupied slot, such as equip or insert into container. Implemented on DataBinding. |
| Dynamic slot lifecycle | IDynamicSlotLifecycle |
Lets an inventory grow or shrink; the engine drives creation and removal. |
| Item converter | IItemAdapterConverter via CreateItemConverter() |
Converts items between inventories with different adapter models. |
| Rules | IGlobalRule / IInventoryRule / ISlotRule |
Declarative mechanical constraints on three levels. |
| Drop zones | DropAreaBase |
Custom drop targets, such as trash, sale, or spawn in world, without changing the transfer pipeline. |