Extending
The framework is designed to be extended without editing it. Every feature module — Weight, Crafting, Trading, Equipment — is built from the extension points below and changes no Core signature.
What needs C++
This is the one place where the two languages are not equals, so it is worth being blunt about the line and where it comes from.
Using the framework needs no C++ at all. Every runtime operation is a node: creating containers, adding, removing, moving, splitting, equipping, crafting, trading, rolling loot, saving, and reading any fragment on any item. See the Blueprint reference.
Inventing a new kind of thing needs C++:
| Extension | Blueprint | Why |
|---|---|---|
| New fragment, rule, layout config or transaction type | ✗ | They are USTRUCTs, and a Blueprint struct cannot inherit from a native one. An engine rule, not a choice this plugin made. |
| New rule evaluator, transaction executor or layout | ✗ | They implement native interfaces held by value, so there is no UObject for Blueprint to subclass. |
| Stranded-item handler | ✓ | UEDStrandedItemHandler is a Blueprintable UObject. |
| Equipment attach finder | ✓ | UEDEquipmentAttachFinder, likewise. |
| Per-item behaviour | ✓ | UEDItemInstance is Blueprintable. |
| Widgets — cells, grids, bars, tooltips | ✓ | Every view base is Blueprintable. |
| Items, containers, recipes, loot tables | ✓ | They are data assets. No code in either language. |
Before assuming you are on the wrong side of that line, check whether you actually need a new type. The plugin ships 16 fragments, 4 rules, 3 layouts and 22 transactions, and they compose: a container is described by picking rules and a layout in a data asset, an item by picking fragments. A Blueprint project reaches all of it, including building a layout config with Make Grid Layout and sending any shipped transaction with Request Transaction.
What a Blueprint project genuinely cannot do is describe a property no shipped fragment describes — fuel, radiation, spell charges — or gate a container on a condition no shipped rule expresses. That is a header and a few lines, and the rest of your game stays in Blueprint: a native fragment shows up in the item editor's picker and is readable from Blueprint with Get Fragment the moment it exists.
Going the other way there is no line at all. A C++-only project can do everything, including the UI — the view classes are C++ bases with Blueprint subclasses shipped for convenience, not the reverse.
A fragment
The most common extension, and the cheapest. A fragment is a plain struct:
USTRUCT(BlueprintType, meta = (DisplayName = "Fuel Fragment"))
struct MYGAME_API FMyFuelFragment : public FEDItemFragment
{
GENERATED_BODY()
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Fuel", meta = (ClampMin = "0"))
float BurnSeconds = 30.0f;
};
That is the whole thing. It appears in the definition editor's fragment picker immediately, and it serialises and replicates with the definition.
if (const FMyFuelFragment* Fuel = Definition->FindFragment<FMyFuelFragment>())
{
Campfire->AddFuel(Fuel->BurnSeconds);
}
A vtable inside an FInstancedStruct corrupts serialization. If you want behaviour, put it in a
system that reads the fragment — that is what every shipped module does.
A rule
Rules gate what a container accepts. Two pieces: the data, and an evaluator registered at startup.
USTRUCT(BlueprintType, meta = (DisplayName = "Owner Only Rule"))
struct FMyOwnerRule : public FEDContainerRule
{
GENERATED_BODY()
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Rule")
TObjectPtr<AActor> AllowedOwner = nullptr;
};
static EEDTransactionError EvaluateOwnerRule(const FInstancedStruct& Rule, const FEDRuleContext& Context)
{
const FMyOwnerRule* Typed = Rule.GetPtr<FMyOwnerRule>();
if (!Typed) { return EEDTransactionError::None; }
return Context.Instigator == Typed->AllowedOwner
? EEDTransactionError::None
: EEDTransactionError::RuleRejected;
}
// In your module's StartupModule:
FEDRuleEvaluatorRegistry::Get().Register(FMyOwnerRule::StaticStruct(), &EvaluateOwnerRule);
FEDRuleContext carries the container, the candidate item and the instigator, and the instigator
reaches both the validate and the execute stage — so a rule about who is asking holds when the
change is actually applied, not only when it is checked.
Rules that depend on who is asking are deliberately not shipped: ownership, guild rank and moderator tiers differ enough per game that writing thirty lines beats bending someone else's model.
A transaction
For an operation that must be atomic across containers.
- A
USTRUCTderiving fromFEDTransaction— data only, since it travels inside anFInstancedStructover RPC. - An executor implementing
IEDTransactionExecutor:Validate— may not mutate.Execute— the only thing that does.GetAffectedContainers— how the processor knows what to snapshot. Get this wrong and rollback misses a container.
- Register it in
FEDTransactionExecutorRegistryat startup.
Core dispatches yours exactly as it dispatches its own.
A layout
For a topology that is not a list, a paperdoll or a grid — a hex board, a ring, a tetris well with gravity.
- An
FEDLayoutConfig-derived struct with the shape's parameters. - An
IEDLayoutimplementation:GetCapacity,IsValidAddress,CanPlaceAt,FindPlacement. - Register it in
FEDLayoutRegistry.
Containers, transactions and views then treat it like any other topology.
A stranded-item handler
What happens to items in a container that is going away. UEDStrandedItemHandler is Blueprintable,
so this one can be done without C++:
- Create a Blueprint class deriving from ED Stranded Item Handler.
- Override Handle Stranded Item; return true if you took the item.
- Add it to the inventory's Stranded Item Handler Classes, after the rehome handler so it only sees what would not fit.
An observer component
For a system that reacts to what an actor is carrying — visuals, grants, counters. Derive from
UEDContainerObserverComponent and you inherit the plumbing: finding the owner's containers however
they are held, binding their events, noticing containers that appear later, and undoing your work on
EndPlay.
You implement three things:
| Override | Does |
|---|---|
ReevaluateItem(Handle) | React to one item. |
GetTrackedHandles(Out) | Name the items you hold state for. |
StopTrackingItem(Handle) | Undo one item. |
RefreshAllItems then works for free — it re-evaluates the union of tracked and present items, so
items that vanished are torn down along with the ones that appeared.
A per-item UObject
When an item needs real per-instance behaviour, subclass UEDItemInstance and set it as the
definition's Instance Class. Entries from that definition are promoted and replicated as
subobjects; everything else stays a struct.
Use it sparingly. The struct fast path is why a thousand arrows cost nothing.
Where next
- Transactions
- Weight and durability — two modules built exactly this way