Rolly Docs

Keep old saves loading after an update

Change what your game saves after release without breaking players' saves: number each change with Game Data Version, describe it with migration steps that update older saves as they load, and check the saves of every released build in your CI.

Sheet
17 of 64
Rev
1.0.0
Updated
On this sheet

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

  1. In the Content Browser, Add > Miscellaneous > Data Asset > Rolly Save Migrations, such as DA_Migrations.
  2. 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.
  3. Open it, add an entry to Steps and pick a step type in its dropdown.
  4. 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:

  1. Add > Blueprint Class > All Classes > Rolly Save Migration Step, such as BP_GoldToInt.
  2. 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.
  3. For each record, Get Saved Value with Name Gold and a Float variable connected to Value, then Set Saved Value with Name Gold and an Integer connected. The Value pin takes the type of what you connect.
  4. In the Migrations asset, add an entry, pick BP_GoldToInt and 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 -nullrhi
Text

Without -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
Text
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;
};
C++