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* | |
|---|---|---|
| Where | Authority only | Anywhere |
| Returns | Whether it happened, now | Nothing; the result arrives later |
| On a client | Returns false, does nothing | Travels to the server |
| Result | The return value | OnTransactionResult 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
| Transaction | What it does |
|---|---|
FEDInsertTransaction | Adds an existing entry, optionally at an address. |
FEDExtractTransaction | Removes one entry. |
FEDMoveTransaction | Moves an entry between containers, preserving handle, state and instance. |
FEDSplitTransaction | Splits N units into a new stack. |
FEDMergeTransaction | Pours one stack into another. |
FEDDestroyTransaction | Removes an entry outright. |
FEDAddItemTransaction | Stack-aware give: tops up existing stacks first, spills into new ones. All-or-nothing. |
FEDConsumeTransaction | Removes N units by definition, smallest piles first. |
FEDTransferTransaction | Moves N units between containers with stack-aware merging. Atomic across both. |
FEDSortTransaction | Consolidates partial stacks, then re-packs. |
FEDSwapTransaction | Trades two entries atomically. |
FEDUseItemTransaction | Uses a usable item (Usable module). |
FEDCraftTransaction | Consumes ingredients and inserts the output, or neither (Crafting module). |
FEDPurchaseTransaction / FEDSaleTransaction | Buy and sell against a currency item (Trading module). |
FEDRotateTransaction | Rotates 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);
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:
- A
USTRUCTderiving fromFEDTransaction— data only, no virtuals, because it travels inside anFInstancedStructover RPC. - An executor implementing
IEDTransactionExecutor:Validate,Execute,GetAffectedContainers. The last one is how the processor knows what to snapshot. - 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
- Replication — what a client may believe
- Extending — your own transaction, rule or layout