27. Extending the plugin
In short: most extensions need no C++. Conditions, effects, ability effects, AI scorers, interactable objects, menu entries, screens and command handlers can all be written in Blueprint and show up in the editor's lists. When you do write C++, follow the plugin's rules: logic in a module with no presentation, content in data assets, rules in settings, state that can be saved and replicated.
27.1 Basics
27.1.1 Pick the lightest tool
Work down this list and stop at the first one that does the job.
- Data. A new ability, item, enemy, quest or dialog is a data asset, a spreadsheet row or a Studio wizard. See Editor tools.
- A setting or a rules profile. A different number or rule for one zone or boss is a setting override in a rules profile, not code.
- A Blueprint extension class. A condition, effect, scorer, reaction, interactable, menu entry, screen or command handler written as a Blueprint class. It appears in the same dropdowns as the built-in ones.
- A C++ subclass of the same extension classes, when you need speed or access the Blueprint graph cannot give.
- A new system in your own module, built the plugin way (below).
- A change to the plugin's source, only for things like a new battle mode. It is a bigger commitment, because you then own the merge with every update.
27.1.2 The Blueprint-only workflow
This is the step list for a condition. The other extension classes work the same way.
A condition: "it is night".
- In the Content Browser: Add → Blueprint Class → All Classes, search for
RSECondition_Custom(its display name is Custom (Blueprint), which every custom base shares, so search by the class name), and name itBP_Condition_IsNight. - Open it. In Class Defaults, set Label to
Is night(it shows in logs and debug tools). - In My Blueprint → Functions → Override, choose Is Condition Met. The
Contextinput is a handle to the game state. - Build the graph: Context → Is Flag Set (Context) (the flag, for example your own
Narrative.Flag.Night) → Return Node. The Context nodes live under RPG Saga|Narrative|Context. - Compile and save. The condition does not change the game: it must only read.
- In any conditions list (a dialog branch, a quest transition, a puzzle, an interaction, a shop) click +, and pick Custom (Blueprint), then your class. It now behaves like a built-in condition.
An effect: "heal the party and play a sound".
- Add → Blueprint Class → All Classes, search for
RSEWorldEffect_Custom, and name itBP_Effect_HealParty. - Add editable variables (Instance Editable) for the numbers, so designers tune them on the instance.
- Override On Execute. Use the Context nodes (for example Modify Member Resource (Context)) so the change is reported to quests and the HUD, like built-in effects.
- List it in any effects list. It runs in order with the others, and the Only If gate works.
A command handler (co-op ready). See Co-op and online: a child of RSE Command Handler, a Command Type, and the class listed in Project Settings → RPG Saga Engine → Commands → Handler Classes.
An interactable: a lever. A Blueprint child of RSE Interactable Actor with On Interact is interactable even without data. In it, flip a story flag, play a sound and refresh the door it controls.
A menu entry. A Blueprint child of RSE Menu Extension: set Id, Point, Label, Order, implement Is Visible, Is Enabled and Execute (which returns what the menu does next), and list the class in Game Flow → Menu Extensions.
A screen. A Widget Blueprint derived from RSE Screen Widget that reads the screen's view model. Set it in the settings of that screen. See User interface.
27.2 How it works
27.2.1 The module layers
The plugin separates logic from presentation with modules, and the compiler enforces it.
| Module | Type | Holds | May depend on |
|---|---|---|---|
RSECore | Runtime | Data, tags, settings, state, and the rules and simulation: battle (all modes), grid, pathfinding, abilities, items, AI, conditions and effects, narrative state, save structures. No actors, visual components, rendering, UI or input. | engine base modules, GameplayTags |
RSE | Runtime | Presentation and world: actors, pawns, camera, input, debug drawing, UI, exploration, save and load orchestration | RSECore |
RSEOnline | Runtime | Sessions and the lobby | RSECore, RSE |
RSENet | Runtime | The co-op transport | RSECore, RSE, RSEOnline |
RSEEditor | Editor | Tools, wizards, importers, visual editors, extra validation | RSECore, RSE |
RSEGAS, RSEGASEditor | Runtime, Editor | Optional bridge: Gameplay Ability System characters in battles | the above |
RSEStateTree, RSEStateTreeEditor | Runtime, Editor | Optional bridge: StateTree nodes for the utility AI | the above |
RSECommonUI | Runtime | Optional Common UI host for the screens | the above |
The rules that follow from this:
RSECorenever includes anything fromRSE,RSEOnline,RSENetorRSEEditor. It talks to presentation through events (delegates).- Any new logic goes into the logic layer by default. Only what needs the world, actors or rendering goes in the presentation layer.
- The plugin never depends on your game module or on
/Gamecontent. Your game depends on the plugin. - Battle logic can run headless, with no rendering, which is how the simulator, Balance My Campaign and the tests work. Presentation listens to events and reads state through
IRSEBattleView. It never changes a battle directly.
In your own project, add RSECore and RSE to your game module's PublicDependencyModuleNames. Add RSEEditor only for Target.bBuildEditor code, and RSENet or RSEOnline only if you use their types.
27.2.2 The extension points
| Base class | Module | Override | Where it shows up |
|---|---|---|---|
URSECondition_Custom | RSECore | Is Condition Met | every Conditions list: dialog, quests, puzzles, interactions, tutorials, shops |
URSEWorldEffect_Custom | RSECore | On Execute | every Effects list, in order with the built-in effects |
URSEEffect_Custom | RSECore | On Apply, Preview Damage | effects of abilities. Preview Damage is read by the forecast and the AI |
URSEBattleCue_Custom | RSE | On Cue Start / Update / Stop | presentation timelines |
URSEAICondition | RSECore | Is Met | rules and phases of AI profiles |
URSEAIScorer | RSECore | Score | added to the score of every ability option, weighted |
URSEAbilityReaction, URSEPassiveModifier | RSECore | Can Trigger, Is Active For | reactions and passives |
ARSEInteractableActor | RSE | On Interact, Can Interact (Custom) | custom interactions in the world |
URSECommandHandler | RSECore | Validate, Apply | the command bus |
URSEMenuExtension | RSE | Is Visible, Is Enabled, Execute | main and in-game menus |
URSEScreenWidget + URSEScreenViewModel | RSE | the screen | any framework screen |
URSESessionProvider | RSEOnline | host, find, join, leave | online services |
URSEPlayerProvider, URSEAuthorityPolicy | RSE | players, authority | who plays, and when co-op rules apply |
IRSESaveBackend | RSECore | slot read and write | where saves are stored |
URSEDeveloperSettings | RSECore | your settings class | Project Settings → RPG Saga Engine and the Configurator |
The base classes are Abstract, Blueprintable, EditInlineNew, so a Blueprint or C++ subclass appears by itself in the dropdowns of instanced objects. Effects and conditions you write are deterministic and read-only where documented: use the battle's Roll Int and Roll Percent nodes instead of random nodes, so a battle replays the same from its seed.
A policy class, such as the leash, the group travel rule or the co-op encounter conversion, is the same idea: the setting holds a class, and you replace it with your own.
27.2.3 The engineering standards
These are the rules every piece of the plugin follows. Follow them in your own systems and your project stays easy to extend, test and ship.
| Rule | In practice |
|---|---|
| No hardcoded tunables | Content goes in data assets and tables. Global rules go in a settings class (Project Settings → RPG Saga Engine). Debug switches are console variables named RSE.*. Only mathematical or logical invariants stay in code. |
| Data assets are Primary Data Assets | Derive from URSEPrimaryDataAsset. Its PrimaryAssetId is the stable id that saves and runtime state refer to. |
| Validate in the editor | Override IsDataValid (call Super), so mistakes show on save, not in play. Settings classes validate too. |
| Soft references for heavy assets | Meshes, animations, sounds and UI are TSoftObjectPtr or TSoftClassPtr, loaded when needed. |
| Ids, not pointers, in state | Runtime state refers to content by tag or FPrimaryAssetId. |
| No Tick by default | Actors and components start with Tick off. Use events, timers or a subsystem that processes a batch. |
| Flat hot data | Grids and pathfinding use contiguous arrays, not an actor per tile. Repeated visuals use instanced meshes. |
| Serializable state | Runtime state that must be saved is a plain USTRUCT with SaveGame properties, separate from actors. |
| Control from the editor | Settings are editable in Project Settings. Test actors have CallInEditor buttons and live properties. |
| Logs and CVars per domain | A log category per domain (LogRSEGrid, LogRSEBattle) and RSE.<Domain>.* CVars. |
| Rules profiles | A gameplay rule lives in a profile-able settings class and is read with RSERules::Get<T>(Profile) or URSEBattle::GetRules<T>(), never with GetDefault<T>() in rule code. |
| Tests | Automation tests named RSE.<Domain>.<Test> for logic, runnable from the Session Frontend and the command line. |
| Blueprint access | Every public API is exposed (BlueprintCallable, BlueprintPure, events, BlueprintType structs). Extension points are Blueprintable with BlueprintNativeEvent. A system is not done until it can be used and extended without C++. |
| No copyright or trademark problems | Every source file has the copyright header. Nothing names other games or companies. Every shipped asset has a licence that allows redistribution. |
27.3 Key classes and assets
| Class / asset | Module | Role |
|---|---|---|
URSEPrimaryDataAsset | RSECore | Base of all content definitions: display name, dev notes, balance lock, archetype link, validation |
URSEDeveloperSettings | RSECore | Base of every settings class. IsProfileSection() makes a class overridable by a rules profile |
URSERulesProfile | RSECore | A rules profile asset |
RSERules::Get<T>(Profile) | RSECore | Read a profile-able setting |
URSEFeatureSettings, RSEFeatures::IsEnabled, RSE_FEATURE_GATE(...) | RSECore | Feature toggles and the macro that stops a subsystem from being created while its feature is off |
URSESaveSubsystem, RegisterStruct<T> | RSECore | Register your system's state as a save section |
URSECommandSubsystem, FRSECommand, URSECommandHandler | RSECore | The command bus |
URSESeedSource | RSECore | Deterministic seeds by named stream |
RSEGameplayTags | RSECore | Native tags, for tags that C++ reads directly |
URSEBattle, IRSEBattleView | RSECore | The headless battle and the read-only view presentation uses |
ARSETacticsBattleDirector, ARSEATBBattleDirector, ARSEFreeTacticsDirector, ARSEActionBattleDirector | RSE | The actor that runs one battle in each mode (presentation, camera, input) |
27.4 Settings
The settings that matter when extending:
| Where | What |
|---|---|
| Project Settings → RPG Saga Engine → Features | Switch whole systems off. A disabled subsystem is not created. The command line -RSEDisableFeatures=Quests,Cards does it for one run |
| Project Settings → RPG Saga Engine → Commands | Handler Classes (your Blueprint handlers), Max Nested Depth, Log Refused Commands |
| Project Settings → RPG Saga Engine → Game Flow | Menu Extensions |
| Project Settings → Asset Manager | The folders scanned for your own Primary Data Asset types |
| Project Settings → Editor → RPG Saga Engine Configurator → Groups | Which group your settings class appears in |
Defaults the plugin ships live in Plugins/RPGSagaEngine/Config/Defaults/{Game,Editor}.ini and apply under your project's config, so anything you change wins and a plugin update never overwrites it. See Appendix B.
27.5 Blueprint usage
A short list of nodes that help when you extend:
- Is Feature Enabled (RPG Saga|Features): adapt your UI to the project's feature set.
- Submit Command, Submit Command As, Can Submit, Submit Command (Game), Has Game Authority (RPG Saga|Commands): the command bus and the authority check.
- Get Subsystem nodes for the framework's subsystems, each with its events (On Quest Started, On Encounter Ended, On Saved, and so on).
- The Context nodes (RPG Saga|Narrative|Context): read and change the game from a custom condition or effect, with changes reported once, like built-in effects.
- In battle code: Get Unit, Is Unit Active, Unit Has Status, Deal Damage, Heal Unit, Apply Status, Defeat Unit, Roll Int, Roll Percent.
The full list is generated: Appendix A. The Blueprint API is frozen: a node or event that ships is never removed or renamed. That is checked by the test RSE.Editor.BlueprintAPI.Manifest, and RSE.BlueprintAPI.Export regenerates the manifest after a deliberate change.
27.6 C++ usage
27.6.1 A new system, the plugin way
A system has a data asset, a settings class, a subsystem that owns the state, a save section, commands, and events. The snippets below show the shape of each piece.
The content type. Derive from URSEPrimaryDataAsset, keep references soft, validate:
UCLASS(BlueprintType)
class UMyBountyData : public URSEPrimaryDataAsset
{
GENERATED_BODY()
public:
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Bounty", meta = (ClampMin = "0"))
int32 Reward = 100;
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Bounty")
FGameplayTag TargetEnemyTag;
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Bounty")
TSoftObjectPtr<UTexture2D> Poster; // soft: loaded when shown
#if WITH_EDITOR
virtual EDataValidationResult IsDataValid(FDataValidationContext& Context) const override
{
EDataValidationResult Result = Super::IsDataValid(Context);
if (!TargetEnemyTag.IsValid())
{
Context.AddError(INVTEXT("Target Enemy Tag is empty."));
Result = EDataValidationResult::Invalid;
}
return Result;
}
#endif
};
Put the folder in Project Settings → Asset Manager → Primary Asset Types to Scan, so saves and tools can find it.
The settings class. Global rules, with limits and a description:
UCLASS(Config = Game, DefaultConfig, meta = (DisplayName = "Bounties"))
class UMyBountySettings : public URSEDeveloperSettings
{
GENERATED_BODY()
public:
/** Gold multiplier for bounties on bosses. */
UPROPERTY(Config, EditAnywhere, meta = (ClampMin = "0.1", ClampMax = "10"))
float BossRewardMultiplier = 2.f;
};
If the rule should differ per zone or boss, override IsProfileSection() to return true, add EditInlineNew, and read it with RSERules::Get<UMyBountySettings>(Profile).
The subsystem and its state. The state is a plain struct, saved by id, replicated by section:
USTRUCT()
struct FMyBountyState
{
GENERATED_BODY()
UPROPERTY(SaveGame) TArray<FPrimaryAssetId> Claimed; // ids, never pointers
};
UCLASS()
class UMyBountySubsystem : public UGameInstanceSubsystem
{
GENERATED_BODY()
public:
RSE_FEATURE_GATE(ERSEFeature::Quests) // not created while the feature is off
virtual void Initialize(FSubsystemCollectionBase& Collection) override
{
Super::Initialize(Collection);
Collection.InitializeDependency<URSESaveSubsystem>()
->RegisterStruct<FMyBountyState>(TEXT("Bounties"), 1, this, [this]() { return &State; });
Collection.InitializeDependency<URSECommandSubsystem>()
->RegisterHandler<FMyCmd_ClaimBounty>(
[this](const FRSECommandContext& C, const FMyCmd_ClaimBounty& Cmd, FText& Why) { return CanClaim(Cmd.Bounty, Why); },
[this](const FRSECommandContext& C, const FMyCmd_ClaimBounty& Cmd) { Claim(Cmd.Bounty); },
ERSECommandPermission::AnyPlayer, this);
}
FSimpleMulticastDelegate OnBountyClaimed; // presentation listens to this
private:
bool CanClaim(FPrimaryAssetId Id, FText& Why) const;
void Claim(FPrimaryAssetId Id)
{
State.Claimed.Add(Id);
RSEStateSections::MarkChanged(this, TEXT("Bounties"));
OnBountyClaimed.Broadcast();
}
FMyBountyState State;
};
The picture to keep in mind: the data asset says what a bounty is, the settings say how the rules work, the subsystem owns the one copy of the state, the section makes it save and replicate, the command is the only way to change it, and the event tells the screen.
The test. Logic that lives in RSECore-style code is testable without a world:
IMPLEMENT_SIMPLE_AUTOMATION_TEST(FMyBountyClaimTest, "MyGame.Bounties.Claim",
EAutomationTestFlags::EditorContext | EAutomationTestFlags::EngineFilter)
bool FMyBountyClaimTest::RunTest(const FString& Parameters)
{
// build the state, run the pure function, TestEqual(...)
return true;
}
27.6.2 Subclassing directors and other actors
A battle director is the actor that runs one battle in its mode: it owns the camera, the input and the presentation, and holds a headless battle driven by a driver. ARSEActionBattleDirector is Blueprintable, so you can make a Blueprint child to change its defaults and hook its events. The grid and ATB directors are placed in a level and configured through their properties, their zone or stage, and the encounter asset. For anything the properties do not cover, subclass in C++ and override the virtual functions the director documents, and keep your logic out of the director: put it in a policy class or a handler that the director calls.
27.6.3 Adding another battle mode (what it takes)
A new battle mode is not a data-only extension. The existing modes (grid Tactics, Free Tactics, ATB, turn-based and Action) follow one pattern, and a new one needs the same pieces:
- A simulation in the logic layer that runs headless, built on the common battle pipeline, so the simulator and the balance tool can play it.
- A driver and a director actor in
RSEfor the camera, input and presentation, which read the battle throughIRSEBattleViewand listen to its events. - A command struct derived from
FRSECommandfor every player action, with a handler on the command bus. That is what makes it co-op ready. - A native gameplay tag for the mode (
Battle.Mode.*), the encounter data to select it, and a case in the encounter starter. - A feature toggle if the mode should be switchable. The feature list is an enum in the plugin, so adding to it is a source change.
- Settings, validation, tests, and a Blueprint surface.
Before you start, check whether an existing mode with a rules profile, an AI profile and your own conditions and effects already gives you what you want. Most "new modes" turn out to be a configuration of Free Tactics, ATB or Action.
27.7 Advanced
27.7.1 Keeping changes to the plugin mergeable
If you must edit the plugin's source:
- Keep your changes small, in your own files where you can, and note each one.
- Never edit a shipped asset in place. Use Create a copy in the project.
- Run
RSE.Plugin.PlatformApis, the content tests and Validate All Data after the change. - Before you package, build the game target and the editor target with strict includes (every
.cppincludes what it uses, and editor-only APIs sit under#if WITH_EDITOR). The plugin is also compiled in the game configuration, which the editor target never covers.
27.7.2 Stable ids and tags
Name a content tag for what it is, not for where you use it. Tags the code reads directly are declared as native tags with UE_DECLARE_GAMEPLAY_TAG_EXTERN, never built from a string. Content tags live in .ini files; a new domain gets its own Config/Tags/<Prefix>_<Domain>.ini, and the plugin registers its tag folder at load. Your own tags go in your project's Config/Tags.
27.7.3 Mechanical renames
The product name and prefix are provisional. Keep them out of dynamically built names so a future rename stays mechanical: use the prefix in class names, never in strings you assemble at run time.
27.7.4 Replication readiness
Systems you add should be ready for co-op from the start, even if you never ship it. See Rules for network-ready systems in Co-op and online: actions as commands, one owner per piece of state, presentation fed by events, swappable control, deterministic seeds, authority-guarded effects, and no references to "player 0" in logic.
27.8 Troubleshooting
| Symptom | Cause and fix |
|---|---|
| My Blueprint condition or effect is not in the list | The class must derive from the Custom (Blueprint) base, be saved, and compile. Reopen the list. A class that does not derive from the base is not offered. |
| A condition changes the game | Conditions must only read. Move the change to an effect. |
| A custom effect's change is not reported to quests or the HUD | It used a plain variable node. Use the Context nodes so changes are reported and deferred like built-in effects. |
| An ability effect replays differently each time | It uses random nodes. Use Roll Int or Roll Percent, which come from the battle's seed. |
| A new settings class sits under Other in the Configurator | Add it to a group (Project Settings → Editor → RPG Saga Engine Configurator → Groups). The test RSE.Editor.Configurator.EveryClassHasGroup fails until you do. |
| My data asset type is not found by the tools | Its folder is not in the Asset Manager's scan list. Add it in Project Settings → Asset Manager. |
Link errors using RSENet or RSEEditor types | Add the module to your Build.cs. Editor modules only under Target.bBuildEditor. |
| The plugin fails to compile in the game configuration | A .cpp relied on an include from a neighbour, or an editor API sits outside #if WITH_EDITOR. Build the game target with strict includes to find it. |
| My subsystem exists although I switched the feature off | It lacks RSE_FEATURE_GATE(...) (or an equivalent ShouldCreateSubsystem). |
| A saved game from before my change fails to load | You changed a section's struct without raising its version number in RegisterStruct. Bump the version and handle the old one. |
27.9 See also
- Core concepts: data-driven content, validation, settings and CVars.
- Abilities and statuses: custom ability effects and logic.
- Enemy AI and adaptive difficulty: AI scorers and conditions, and the StateTree bridge.
- Save and load: save sections.
- Co-op and online: commands, state sections and authority.
- Editor tools: the Configurator and the importers.
- Appendix A, Appendix B, Appendix D.
- Design record:
docs/engineering-standards.md.