Skip to main content

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++:

ExtensionBlueprintWhy
New fragment, rule, layout config or transaction typeThey 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 layoutThey implement native interfaces held by value, so there is no UObject for Blueprint to subclass.
Stranded-item handlerUEDStrandedItemHandler is a Blueprintable UObject.
Equipment attach finderUEDEquipmentAttachFinder, likewise.
Per-item behaviourUEDItemInstance is Blueprintable.
Widgets — cells, grids, bars, tooltipsEvery view base is Blueprintable.
Items, containers, recipes, loot tablesThey 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);
}
No virtuals on a fragment

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.

  1. A USTRUCT deriving from FEDTransaction — data only, since it travels inside an FInstancedStruct over RPC.
  2. 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.
  3. Register it in FEDTransactionExecutorRegistry at 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.

  1. An FEDLayoutConfig-derived struct with the shape's parameters.
  2. An IEDLayout implementation: GetCapacity, IsValidAddress, CanPlaceAt, FindPlacement.
  3. 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++:

  1. Create a Blueprint class deriving from ED Stranded Item Handler.
  2. Override Handle Stranded Item; return true if you took the item.
  3. 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:

OverrideDoes
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