Version 1.0, coming to Fab. Orrery Terrain 1.0 is not on sale yet; this guide can still change before it is.
6. Tutorial: save and load
The terrain saves its own ground state to a file beside your game's save slot. Your game keeps a small reference to that file in its own save. This chapter shows the Blueprint and C++ flow.
Save contents
Section titled “Save contents ”A terrain save holds the sculpt layer, the paint, the feature stack (plates and corridors) and the identity of the natural ground. It does not hold baked tiles or the natural ground itself. Load it over the same natural ground it was made over, or it is refused.
The file is <Slot>.<n>.tsterrain in <Project>/Saved/SaveGames. Each save writes a new file with a higher n, then deletes older files of that slot that nothing needs. The suffix tsterrain can be changed in Project Settings > Plugins > Orrery Terrain.
The save reference (Terrain Save Reference) names the file and carries its hashes, so the game can tell if the file is the one it recorded. Keep it in your own USaveGame. A reference goes stale when its slot is saved again, so always keep the newest one.
Part 1: Blueprint
Section titled “Part 1: Blueprint”- Make a
SaveGameBlueprint (for exampleBP_MySave) with a variableTerrainRefof type Terrain Save Reference. - When the player saves, call Save Terrain To Slot (Async). Set Slot Name to your slot (for example
Slot1), User Index to0, and leave Options unwired: the default options keep every feature. - Wire On Completed to: create or update your save object, set
TerrainReffrom the node’s Ref output, then call Save Game to Slot with the same slot name. - Wire On Failed to a message. The node’s Result, Lost Codes and Message outputs say why nothing was saved.
The graph as text
- SaveTerrain (event; Custom Event)
- Out Output Delegate (delegate): not connected
- Then: runs Save Terrain To Slot (Async)
- Save Terrain To Slot (Async) (latent function)
- In Slot Name (String): Slot1
- In User Index (Integer): 0
- In Options (OrreryTerrainSaveOptions): not connected
- Then: not connected
- On Completed: runs Create Save Game Object
- On Failed: runs Print String
- Out Ref (OrreryTerrainSaveRef): to Set TerrainRef › Terrain Ref
- Out Result (EOrreryTerrainSaveResult): not connected
- Out Lost Codes (Integer64): not connected
- Out Message (String): to Print String › In String
- Create Save Game Object (function call)
- In Save Game Class (SaveGame class): BP My Save
- Then: runs Set TerrainRef
- Out Return Value (BP_MySave_C object): to Set TerrainRef › Target, Save Game to Slot › Save Game Object
- Set TerrainRef (variable set)
- In Terrain Ref (OrreryTerrainSaveRef): from Save Terrain To Slot (Async) › Ref
- In Target (BP_MySave_C object): from Create Save Game Object › Return Value
- Then: runs Save Game to Slot
- Out Output Get (OrreryTerrainSaveRef): not connected
- Save Game to Slot (function call)
- In Save Game Object (SaveGame object): from Create Save Game Object › Return Value
- In Slot Name (String): Slot1
- In User Index (Integer): 0
- Out Return Value (Boolean): not connected
- Print String (function call; development only)
- In In String (String): from Save Terrain To Slot (Async) › Message
- In Print to Screen (Boolean): true
- In Print to Log (Boolean): true
- In Text Color (LinearColor): (R=0.0, G=0.66, B=1.0, A=1.0)
- In Duration (Float): 2.0
- In Key (Name): None
BP_TerrainSave in the samples project (/Game/DocExamples). To paste, the Blueprint needs the Blueprint BP_MySave.
- When the player loads, call Load Game from Slot and cast the result to
BP_MySave. - Call Does Terrain Save Exist with
TerrainRef. If false, tell the player the terrain file is missing. - Call Load Terrain From Slot (Async) with the reference. On Completed fires when the terrain has been replaced. On Failed leaves the terrain unchanged, and the node’s Result output says why (see the table below).
The graph as text
- LoadTerrain (event; Custom Event)
- Out Output Delegate (delegate): not connected
- Then: runs Load Game from Slot
- Load Game from Slot (function call)
- In Slot Name (String): Slot1
- In User Index (Integer): 0
- Then: runs Cast To BP_MySave
- Out Return Value (SaveGame object): to Cast To BP_MySave › Object
- Cast To BP_MySave (cast)
- In Object (Object object): from Load Game from Slot › Return Value
- Then: runs Branch
- Cast Failed: not connected
- Out As BP My Save (BP_MySave_C object): to Get TerrainRef › Target
- Get TerrainRef (variable)
- In Target (BP_MySave_C object): from Cast To BP_MySave › As BP My Save
- Out Terrain Ref (OrreryTerrainSaveRef): to Does Terrain Save Exist › Ref, Load Terrain From Slot (Async) › Ref
- Does Terrain Save Exist (pure function)
- In Ref (OrreryTerrainSaveRef): from Get TerrainRef › Terrain Ref
- Out Return Value (Boolean): to Branch › Condition
- Branch (flow control)
- In Condition (Boolean): from Does Terrain Save Exist › Return Value
- True: runs Load Terrain From Slot (Async)
- False: runs Print String 2
- Load Terrain From Slot (Async) (latent function)
- In Ref (OrreryTerrainSaveRef): from Get TerrainRef › Terrain Ref
- Then: not connected
- On Completed: not connected
- On Failed: runs Print String 1
- Out Result (EOrreryTerrainLoadResult): to Enum to String › Enumerator
- Enum to String (pure function)
- In Enumerator (EOrreryTerrainLoadResult): from Load Terrain From Slot (Async) › Result
- Out Return Value (String): to Print String 1 › In String
- Print String 1 (function call; development only)
- In In String (String): from Enum to String › Return Value
- In Print to Screen (Boolean): true
- In Print to Log (Boolean): true
- In Text Color (LinearColor): (R=0.0, G=0.66, B=1.0, A=1.0)
- In Duration (Float): 2.0
- In Key (Name): None
- Print String 2 (function call; development only)
- In In String (String): The terrain save file is missing
- In Print to Screen (Boolean): true
- In Print to Log (Boolean): true
- In Text Color (LinearColor): (R=0.0, G=0.66, B=1.0, A=1.0)
- In Duration (Float): 2.0
- In Key (Name): None
BP_TerrainLoad in the samples project (/Game/DocExamples). To paste, the Blueprint needs the Blueprint BP_MySave.
Results you may see
Section titled “Results you may see”| Result | Meaning |
|---|---|
Ok |
Done. |
NaturalMismatch |
The save was made over different natural ground (another source asset or another import). |
GridMismatch |
The grid differs: origin, cell size or subdivision. |
BadVersion |
The file was written by a newer or incompatible save format. A save from the previous format (version 4) loads and is migrated to version 5 (four paint layers per ground point); an older build cannot read a version 5 save. |
Corrupt |
The file is damaged, or it is not the file the reference recorded. |
NotFound |
The file does not exist. |
Refused |
The terrain refuses edits: an editor world, or you called it inside a ground-changed listener. |
NoTerrain |
The world has no terrain. |
Saving can answer Busy (another save is still being written), OpenFailed, WriteFailed, DiskFull, ReadFailed (see below), BadOptions or BadSlot (the name is empty, over 100 characters, or holds a character a file name cannot).
A damaged file is refused when it loads (Corrupt), and the terrain stays as it was. Sculpted tiles can lose their data, and the next save answers ReadFailed instead of writing a save with holes, only if the file was damaged after it was loaded. Sculpt over those tiles again, or reload an undamaged save.
Part 2: C++
Section titled “Part 2: C++”void AMyGameMode::SaveTerrain(){ UOrreryTerrainSubsystem* Terrain = GetWorld()->GetSubsystem<UOrreryTerrainSubsystem>(); if (!Terrain) return;
Terrain->SaveTerrainAsync(TEXT("Slot1"), 0, FOnTerrainSaved::CreateWeakLambda(this, [this](const FOrreryTerrainSlotSaveResult& Result) { if (Result.Result == EOrreryTerrainSaveResult::Ok) { MySave->TerrainRef = Result.Ref; UGameplayStatics::SaveGameToSlot(MySave, TEXT("Slot1"), 0); } }));}
void AMyGameMode::LoadTerrain(){ UOrreryTerrainSubsystem* Terrain = GetWorld()->GetSubsystem<UOrreryTerrainSubsystem>(); if (!Terrain || !MySave) return;
Terrain->LoadTerrainAsync(MySave->TerrainRef, FOnTerrainLoaded::CreateWeakLambda(this, [](EOrreryTerrainLoadResult Result) { /* handle */ }));}Other C++ helpers: GetSaveDirectory(), IsValidSaveSlotName() and ResolveSavePath() are static on the subsystem.
Save only some features
Section titled “Save only some features”Options takes a list of owner domains. A save with a non-empty list keeps only the features of those domains. The sculpt layer and the paint are always saved whole.
Use this when another system (for example your rail or road code) saves its own features and you do not want them twice.
Tidy up
Section titled “Tidy up”Delete Terrain Save removes the file a reference names. It returns false when the reference is invalid, or when the running terrain still pages sculpted tiles from that file.
Check it worked
Section titled “Check it worked”- Play and sculpt a hill. Call Get State Hash and note the number.
- Save. Quit Play. Start Play again (the sculpting is gone).
- Load from your slot. Get State Hash returns the number from step 1, and the hill is back.
- Change the terrain actor’s source to a different asset, keep the same slot, and load. The result is
NaturalMismatch.
Save compatibility between plugin versions is covered in the versioning policy.