Skip to content

Make a system save

Designer · 30 min · No code

Result: your own state is written into XenoCore's save file and comes back on Continue or Load, with old save files still loading. The worked example is a lever that stays pulled; the same steps save a bestiary, a crafting bench or anything else, without editing XenoCore.

Before you start

How it works

XenoCore saves its own systems (party, levels, bag, quests, chests) itself. Anything else takes part as a save participant: an actor, component or object that stores its own record in the file under its own key. XenoCore never needs to know what the record holds.

Piece What it is
SaveGame flag on a variable Marks what to save. Only flagged variables are written
XenoCore Save Participant component (UXenoCoreSaveParticipantComponent) Add it to an actor and that actor's flagged variables are saved. No graph needed
XenoCore Save Participant interface (IXenoCoreSaveParticipant) Optional. Gives an object a key, a data version and two hooks
Register Save Participant / Unregister Save Participant On the save subsystem, for anything the component does not cover

What is found without any registration: the player controller, the player state, the game state, and every component on those three, if it implements the interface. So a party-wide system on a controller component only needs the interface.

Order. On load, participants are restored after all of XenoCore's own systems, so a participant that reads the party or the bag while restoring sees the loaded state.

Records that nobody claims are kept. If a participant is missing this session (a streamed-out actor, a system removed for one build), its record is carried into the next save instead of being lost.

Steps

1. Flag what to save

  1. Open your Blueprint (here BP_MyGame_Lever). Select the variable that holds the state, such as a boolean bPulled.
  2. In its Details, open the advanced section under Variable and tick SaveGame.
  3. Repeat for every variable that must survive a load.

Fails silently: a struct saves empty

Only flagged properties are written, and that includes the fields inside a struct variable. Flag a struct variable but not the struct's own fields, and it saves as all defaults. For a Blueprint struct, open the struct asset, expand each field with its arrow and tick Save Game there too.

2. Add the component

  1. In Components, add XenoCore Save Participant.
  2. Leave Save Key empty for an actor placed in a level. Its key is then made from the map and the actor's name, which is unique.
  3. For an actor spawned at runtime, set Save Key to a unique name, such as MyGame.Forge.

That is all the lever needs. The component registers the actor at Begin Play and unregisters it at End Play. Unregistering records the actor's current state first, so an actor that streams out and back in, or a level left and re-entered, gets its state back.

Fails silently: two actors with one key

A spawned actor with an empty Save Key is keyed by its class, so a second copy has the same key. Only the first one registered is saved; the other logs an error (XenoCoreSave: ... registered with save key '...', already used by ... and, at save time, duplicate save participant key). Give every copy of a spawned actor its own key.

A key is forever

Records are found by key. Renaming a key, or renaming a placed actor whose key is derived from its name, leaves every existing save holding a record nobody claims, so the actor loads with its defaults. Settle keys and placed-actor names before release.

3. React to a load (optional)

The component restores the variables and nothing else. If the lever must also look pulled, use the interface's hooks:

  1. Class Settings › Interfaces › Add: XenoCore Save Participant.
  2. Implement On Save Data Restored. It runs after this actor's record has been applied, and is given Saved Version. Here, snap the lever mesh to the pulled pose if bPulled is true, and call Set Interact Enabled with the opposite.
  3. Implement On Pre Save Capture only if some state lives outside flagged variables: copy it into them there. It runs just before this actor is written.

On Save Data Restored runs only when there was a record

An actor the save has never heard of keeps its defaults and hears nothing. To react to every load, bind the save subsystem's On Save Loaded instead; it fires once the whole load is applied, participants included, and passes the slot index.

4. Version your data (optional)

Implement Get Save Data Version and return 1. When you later change what you save (rename a variable, change what a number means), return 2, and in On Save Data Restored convert data that arrived with Saved Version 1.

Adding a new flagged variable does not need a new version: an older record simply has no value for it, and the variable keeps its default.

Your version is your own. XenoCore's save file has its own version, which you never change. A save made with a XenoCore release older than save participants loads as "no participant has saved anything yet".

5. Other kinds of participant

You have Do this
A component on the player controller, player state or game state (a party-wide system) Implement the interface on the component. It is found automatically; no save participant component, no registration
An actor that is not placed and not spawned by you, or a plain object Call Register Save Participant on the save subsystem with it (and a Save Key if it can exist twice), and Unregister Save Participant from its End Play
A record you want gone (a quest line reset) Forget Save Record with its key; Has Save Record checks for one

A registered object does not have to implement the interface; its flagged variables are saved either way. The interface only adds the key, the version and the hooks.

If a record already exists when something registers (it was loaded, or it streamed out earlier), it is applied at once, unless a load is still being applied, in which case it waits and is applied with everything else.

For programmers: C++ participants

A C++ class implementing IXenoCoreSaveParticipant may override CaptureSaveData(FArchive&) and RestoreSaveData(FArchive&, int32 SavedVersion) to write whatever it likes instead of its UPROPERTY(SaveGame) fields. The archive stores object pointers as paths. The defaults (GetSaveParticipantKey, GetSaveDataVersion, OnPreSaveCapture, OnSaveDataRestored) are BlueprintNativeEvents: override the _Implementation versions. All of it is exported, so this works from your own module; see Add a mechanic by subclassing. XenoCore's own file version is EXenoCoreSaveVersion (currently Gems = 5; participant records arrived in ParticipantData = 3).

Rules that keep old saves loading

These are the rules XenoCore's own saved data follows. Follow them in your records too.

  • Keep state on something that outlives the world for anything party-wide: a controller component, not a pawn. A benched party member has no pawn.
  • Store asset references as soft references or paths, never as object pointers to live actors. An asset that has since been deleted is then skipped instead of breaking the load.
  • Store enums and table rows by name, never by number, if the list may grow.
  • Recompute what you can. Save only what nothing else remembers.
  • Guard starting grants. If your system gives something at the start of a game (a first entry, a free item), check Was World Loaded From Save on the save subsystem first, or every load gives it again.

Test it

  1. Place the lever in Maps/L_XenoCore_Gym, press Play, and pull it.
  2. Open the console and enter XenoSave 1. The Output Log reports XenoCoreSave: captured 1 save participant(s).
  3. XenoSaveDump prints each slot; slot 1 now lists 1 participant records.
  4. Stop, press Play again, and enter XenoLoad 1. After the map reloads the lever is still pulled and offers no prompt. The log line restored ... 1/1 participant record(s) confirms it.
  5. Load a save made before you added the component: it loads, and the lever starts unpulled.

If it doesn't work

Loaded, but the value is back to its default

The variable is not flagged SaveGame, or it is a struct whose fields are not flagged. Or the actor was renamed since the save was written, which changed its key.

Only one of several spawned copies keeps its state

They share a key. Set Save Key per copy. The Output Log names the clash.

The value loads, but the actor does not look right

The component restores variables only. Apply them in On Save Data Restored.

Going further