Number each change to saved data
Every save records Game Data Version from Project Settings > Plugins > Rolly Save. Raise it by 1 in each update that changes what saves hold: a variable renamed, removed or given another type, a class replaced, a registered name or a map renamed. Then add a step for the change to your Migrations asset, so saves of the lower version are updated before they load.
A save of a higher version than the build reading it, such as one synced from a device that has the update, does not load: the load ends with Newer Version. Saves and copies to that slot end with Newer Version too and write nothing, so the slot keeps the save; Delete Rolly Slot still deletes it. Never lower Game Data Version after a release.
Add a Migrations asset
- In the Content Browser, Add > Miscellaneous > Data Asset > Rolly Save Migrations, such as
DA_Migrations. - In Project Settings > Plugins > Rolly Save > Old Saves, pick it in Migrations. It is cooked into every packaged build, also when no map refers to it.
- Open it, add an entry to Steps and pick a step type in its dropdown.
- Set the step’s To Version to the Game Data Version of the update that makes the change, and fill in its fields.
A step runs on a save whose Game Data Version is below its To Version, once Game Data Version in Project Settings is at least its To Version. Steps run in order of To Version, then in list order, so a save two updates old goes through the steps of both. They run after the save’s file passed its checks and before anything of it loads, the classes and assets it names included; the next save writes the project’s Game Data Version.
Built-in steps
| Step | Fields | Change | Fails the load when |
|---|---|---|---|
| Rename Saved Value | Classes, Old Name, New Name | In saved objects of the classes listed, the value Old Name becomes New Name | an object holds both names |
| Remove Saved Value | Classes, Value Name | Removes the value from saved objects of those classes, so the old-save check stops reporting it as dropped | never |
| Replace Class | Old Class Path, New Class | Saved objects of Old Class Path load into objects of New Class, and pointers to the old class point at the new one | never |
| Remove Records of Class | Class Path | Removes every saved object of the class, such as one of a plugin your game no longer uses | never |
| Rename Registered Name | Old Name, New Name | The object saved under a Register Object for Saving name, and every saved pointer to it, takes the new name | the save already holds an object under New Name |
| Rename Map | Old Map, New Map | A save made in a renamed or moved map opens the new map, and the map’s Level Blueprint and everything saved inside the map take its new name | the save already holds the new map |
Steps change top-level values: a variable of the class, not a member of a struct it holds. Classes compare after Core Redirects. Type Old Class Path, Class Path and Old Map as rolly.save.dump <slot> -records and the old-save check show them, such as /Game/Chests/BP_Chest.BP_Chest_C or /Game/Maps/Tavern: the asset may no longer exist.
A rename that a redirect already covers needs no step: see Rename saved data without losing saves.
Write a Blueprint step
For a change no built-in step makes, such as gold stored as a float that becomes an integer:
- Add > Blueprint Class > All Classes > Rolly Save Migration Step, such as
BP_GoldToInt. - Add Event Migrate. Its Context finds the save’s objects: Find Records By Class (Class Path), Find Records By Subject (Global and
GameMode, or Registered Name and the name) or Find Records In Chunk. - For each record, Get Saved Value with Name
Goldand a Float variable connected to Value, then Set Saved Value with NameGoldand an Integer connected. The Value pin takes the type of what you connect. - In the Migrations asset, add an entry, pick
BP_GoldToIntand set its To Version.
Get Saved Value reads a value only when the connected variable has exactly the type it was saved as: an Integer read as a Float returns false, and a warning names both types. A Blueprint Float variable is a double (DoubleProperty); read a value a C++ float saved (FloatProperty) in a C++ step. Get Saved Value Type returns the saved type, such as IntProperty; rolly.save.dump <slot> -records lists every value with its type. To change one member of a saved struct, read the whole struct, change the member and write the struct back. A value whose type no longer exists can only be renamed or removed.
Steps run before the save’s objects and assets load. Get Saved Value returns false, with a warning that names the value and what it could not read, for a value, or a struct, that holds a pointer to another saved object, to an object the saved object owns, or to an asset or class that is not loaded, or a text from a string table that is not loaded: a step cannot rewrite such a value. Rename or remove it, or keep the members that change in a value of their own. A read that works in the editor, where the asset or string table is loaded, can return false in a packaged game. When Get Saved Value returns false, the variable keeps the value it had.
Values a Save Rule or Rolly Save component saves by path, such as a light’s intensity, have nodes of their own: Get Path Value, Set Path Value, Get Path Value Type and Remove Path Value. Name is the property, then each member, joined by dots, such as Intensity or BodyInstance.CollisionEnabled, and the record is the one of the component or actor that holds the property (Find Records By Class with the component’s class, such as /Script/Engine.PointLightComponent). Read the value as its old type and write it as the new one when an engine or plugin update changes the type, so the player’s value loads; a load applies it only while a rule or component picks it and the types match.
A record is valid only in the Migrate call whose Find node returned it. Fail Migration stops the load with Migration Failed and your reason. A Blueprint error inside Migrate, such as reading a property of nothing, fails the load too, and so does a value written past a limit of the save format. The load then applies nothing, and a setup error names the step’s index in Steps, its type and its To Version.
Check old saves in CI
Keep a copy of the saves of each released build in a folder outside Content, one subfolder per release, such as TestData/OldSaves/1.0 and TestData/OldSaves/1.1, each holding <prefix>_slot_<name>_a.sav and _b.sav as the game wrote them. Name the folder in Project Settings > Plugins > Rolly Save > Check Folder (TestData/OldSaves by default). Each slot is loaded the way the game loads it, Migrations steps included, without a world and without writing anything.
In the editor: Tools > Session Frontend > Automation, run Rolly.Save.OldSaves: one test per slot, named after its subfolder and slot, such as Rolly.Save.OldSaves.1_0.quick for 1.0/rollysave_slot_quick_a.sav.
In CI, with the editor closed:
UnrealEditor-Cmd.exe "<Game>.uproject" -run=RollySaveCheckSaves -Folder="<folder>" -unattended -nopause -nosplash -nullrhi
UnrealEditor-Cmd.exe "<Game>.uproject" -ExecCmds="Automation RunTests Rolly.Save.OldSaves;Quit" -TestExit="Automation Test Queue Empty" -ReportExportPath="<dir>" -unattended -nopause -nosplash -nullrhiWithout -Folder, the commandlet checks Check Folder; a relative path starts at the project’s folder. Its exit code is 0 when every save loads, 1 when one does not, and 2 when the folder is missing, holds no save of your File Prefix, or -Folder= names no folder. It prints:
Rolly Save old saves: <folder>
OK 1_0.quick: game data version 1 to 2, 1 steps, 0 warnings
WARNING 1_0.quick: DroppedValue: the registered name 'Inventory' holds Weight, which /Game/BP_Inventory.BP_Inventory_C no longer has, so a load drops it
FAILED 1_1.tavern: MissingClass: the registered name 'Chest_1' was saved as /Game/BP_OldChest.BP_OldChest_C, which this build does not have; a Remove Records of Class step declares it gone
Rolly Save old saves: 1 loaded, 1 failed, 1 warnings| Result | Kind | What to do |
|---|---|---|
| Fails | A status, such as NewerVersion, Damaged or MigrationFailed | Fix the step it names, or check the file |
| Fails | MissingClass | Add Replace Class, or Remove Records of Class when the objects are gone on purpose |
| Fails | DamagedRecord | A saved object no longer reads into its class: add a step that renames or removes the value |
| Warns | DroppedValue | A saved value the class no longer has: add Rename Saved Value, or Remove Saved Value when it is gone on purpose |
| Warns | MissingImport, RejectedImport | An asset the save names is gone, or is no longer of the class it was saved as: it loads as empty |
| Warns | EditorOnlyRename | A renamed Blueprint variable that the editor finds through its property ID, which the packaged game lacks: add Rename Saved Value, and set the Blueprint’s Class Settings > Should Cook Property Guids? to Yes |
| Warns | StructWithoutIds | A Blueprint struct saved without its members’ IDs: open it and save it, so a renamed member keeps loading in packaged games |
Pointers between saved objects need a world and are not checked. The editor finds some renames that a packaged game does not, so test a packaged build too before a release.
What the editor tells you
| Message | Where | Fix |
|---|---|---|
| Migrations names an asset that cannot be loaded | Project Settings’ Data Validation, and as the game starts | Pick the asset again, or clear the field. Until then saves of lower versions do not load |
| Step N is empty | The asset’s Data Validation | Pick a step type, or remove the entry |
| Step N has no Classes, Old Name, New Name or another field | The asset’s Data Validation | Fill it in |
| Step N renames a name to itself | The asset’s Data Validation | Set the new name |
| Step N has To Version above Game Data Version, so it does not run yet | The asset’s Data Validation | Raise Game Data Version in the build that ships the change |
| Step N does not implement Migrate | The asset’s Data Validation | Add Event Migrate to the Blueprint, or remove the entry |
| The asset is not the Migrations asset in Project Settings | The asset’s Data Validation | Pick it in Migrations, or delete it |
From C++
Subclass URollySaveMigrationStep (Migration/RollySaveMigrations.h) and override Migrate_Implementation. URollySaveMigrationContext (Migration/RollySaveMigrationContext.h) reads and writes a value with the type of a property you give (ReadSavedValue and WriteSavedValue; ReadPathValue and WritePathValue for values saved by path), and gives a step the records themselves (GetMutableRecord, AddRecord, RenameChunk, ReplaceSubject, ReplaceClassImports); each record a step changes must still pass the check a load runs, or the step fails.
#include "Migration/RollySaveMigrationContext.h"
#include "Migration/RollySaveMigrations.h"
UCLASS()
class UMyGoldToIntStep : public URollySaveMigrationStep
{
GENERATED_BODY()
protected:
virtual void Migrate_Implementation(URollySaveMigrationContext* Context) override
{
// A property of each type tells the context how to read and write the value.
const FProperty* AsFloat = StaticClass()->FindPropertyByName(GET_MEMBER_NAME_CHECKED(UMyGoldToIntStep, FloatGold));
const FProperty* AsInt = UMyInventory::StaticClass()->FindPropertyByName(GET_MEMBER_NAME_CHECKED(UMyInventory, Gold));
for (const FRollySaveRecordHandle& Record : Context->FindRecordsByClass(TEXT("/Script/MyGame.MyInventory")))
{
float Saved = 0.0f;
if (Context->ReadSavedValue(Record, TEXT("Gold"), AsFloat, &Saved))
{
const int32 Now = FMath::RoundToInt32(Saved);
Context->WriteSavedValue(Record, TEXT("Gold"), AsInt, &Now);
}
}
}
/** The type Gold was saved as before Game Data Version 2. */
UPROPERTY()
float FloatGold = 0.0f;
};