Skip to main content

Transactions

Nothing mutates a container directly. Every change — add, move, split, craft, buy — is a transaction: a plain data struct, executed by a registered executor, with a snapshot taken first so a failure leaves nothing half-applied.

Why

A move between two containers touches two of them. A craft consumes several stacks and inserts one. A purchase takes coins from a buyer and an item from a vendor, on two different actors. Any of those can fail on the last step, and "half done" is not an acceptable outcome for a player's things.

Request ──▶ Validate ──▶ Snapshot ──▶ Execute ──┬─ success ─▶ commit + broadcast
└─ failure ─▶ restore snapshot

Validate never mutates. Execute is the only thing that does. Because both consult the same acceptance gate, a dry run and the real call cannot disagree.

Using them

Most of the time you do not build a transaction. The ergonomic calls do it for you:

Container->TryAddItem(Definition, 3);                    // FEDAddItemTransaction
Container->TryTransferItem(Handle, Target, 2); // FEDTransferTransaction
Container->SortInventory(EEDSortMode::ByCategory); // FEDSortTransaction

Build one by hand when you need a field the wrapper does not expose — a specific target cell, for instance:

FEDMoveTransaction Move;
Move.SourceContainer = From;
Move.TargetContainer = To;
Move.Handle = Handle;
Move.TargetAddress = FEDSlotAddress::AtCell(FIntPoint(2, 1));

Container->RequestTransaction(FInstancedStruct::Make(Move));

Try or Request

Two families, and the difference is who is allowed to call them.

Try*Request*
WhereAuthority onlyAnywhere
ReturnsWhether it happened, nowNothing; the result arrives later
On a clientReturns false, does nothingTravels to the server
ResultThe return valueOnTransactionResult on the requesting component

Use Try* inside server code — a Server RPC, an authority-gated branch. Use Request* from UI and anything else that might be running on a client.

The shipped transactions

TransactionWhat it does
FEDInsertTransactionAdds an existing entry, optionally at an address.
FEDExtractTransactionRemoves one entry.
FEDMoveTransactionMoves an entry between containers, preserving handle, state and instance.
FEDSplitTransactionSplits N units into a new stack.
FEDMergeTransactionPours one stack into another.
FEDDestroyTransactionRemoves an entry outright.
FEDAddItemTransactionStack-aware give: tops up existing stacks first, spills into new ones. All-or-nothing.
FEDConsumeTransactionRemoves N units by definition, smallest piles first.
FEDTransferTransactionMoves N units between containers with stack-aware merging. Atomic across both.
FEDSortTransactionConsolidates partial stacks, then re-packs.
FEDSwapTransactionTrades two entries atomically.
FEDUseItemTransactionUses a usable item (Usable module).
FEDCraftTransactionConsumes ingredients and inserts the output, or neither (Crafting module).
FEDPurchaseTransaction / FEDSaleTransactionBuy and sell against a currency item (Trading module).
FEDRotateTransactionRotates an item in a grid (Spatial module).

Sorting

FEDSortTransaction does the half of "sort" players actually mean first: partial stacks of the same item are poured together, so eight loose piles of arrows become one. Then the container re-packs — grids compact row-major with auto-rotation, lists from slot 0.

Only plain, stackable entries with identical state merge. Two half-worn swords and any promoted instance keep their identity.

EEDSortMode covers ByName, ByCategory, ByCount, ByDefinition and ByRarity. A view model remembers a mode and CycleSortMode advances it, which is the one-button "change ordering" control inventory mods have trained players to expect.

Failure is a reason, not a boolean

EEDTransactionError names why: NoAuthority, TargetFull, RuleRejected, InvalidPlacement, InvalidItem, InvalidParameters, and so on. EDTransactionReasons::ToText turns one into player-readable text.

That is what EvaluateAddItem returns, so a vendor's Buy button can be greyed out with "There is no room for that" instead of failing silently when clicked:

const FEDPlacementCheckResult Check = Container->EvaluateAddItem(Definition, Count);
BuyButton->SetIsEnabled(Check.bCanAdd);
Tooltip->SetText(Check.Reason);
Enum order is frozen

EEDTransactionError replicates, so the order of its values is part of the network contract. New reasons are appended, never inserted.

Adding your own

Three pieces:

  1. A USTRUCT deriving from FEDTransaction — data only, no virtuals, because it travels inside an FInstancedStruct over RPC.
  2. An executor implementing IEDTransactionExecutor: Validate, Execute, GetAffectedContainers. The last one is how the processor knows what to snapshot.
  3. A registration, at module startup, in FEDTransactionExecutorRegistry.

Core dispatches it without knowing your type. That registry is how the feature modules add crafting and trading without touching Core — see Extending.

Client prediction

Opt-in, and only on UEDContainerComponent:

const FEDPredictionKey Key = Component->RequestPredictedTransaction(FInstancedStruct::Make(Move));

The client applies the change to a local predicted view, sends the request, and reconciles when the server confirms or rejects. Read GetPredictedContainer() from latency-sensitive UI; on the authority it simply mirrors the real one.

Most inventories do not need this. Reach for it when a player is dragging items during combat and the round trip is visible.

Where next