Skip to content
Brass Orrery

Version 1.0, coming to Fab. Orrery Terrain 1.0 is not on sale yet; this guide can still change before it is.

GuideOrrery Terrain 1.0Tutorials

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.

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.

  1. Make a SaveGame Blueprint (for example BP_MySave) with a variable TerrainRef of type Terrain Save Reference.
  2. When the player saves, call Save Terrain To Slot (Async). Set Slot Name to your slot (for example Slot1), User Index to 0, and leave Options unwired: the default options keep every feature.
  3. Wire On Completed to: create or update your save object, set TerrainRef from the node’s Ref output, then call Save Game to Slot with the same slot name.
  4. Wire On Failed to a message. The node’s Result, Lost Codes and Message outputs say why nothing was saved.
Blueprint BP_TerrainSave · Event Graph
The graph as text
  1. SaveTerrain (event; Custom Event)
    • Out Output Delegate (delegate): not connected
    • Then: runs Save Terrain To Slot (Async)
  2. 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
  3. 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
  4. 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
  5. 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
  6. 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.

  1. When the player loads, call Load Game from Slot and cast the result to BP_MySave.
  2. Call Does Terrain Save Exist with TerrainRef. If false, tell the player the terrain file is missing.
  3. 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).
Blueprint BP_TerrainLoad · Event Graph
The graph as text
  1. LoadTerrain (event; Custom Event)
    • Out Output Delegate (delegate): not connected
    • Then: runs Load Game from Slot
  2. 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
  3. 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
  4. 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
  5. Does Terrain Save Exist (pure function)
    • In Ref (OrreryTerrainSaveRef): from Get TerrainRef › Terrain Ref
    • Out Return Value (Boolean): to Branch › Condition
  6. 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
  7. 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
  8. 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
  9. 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
  10. 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.

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.

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.

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.

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.

  1. Play and sculpt a hill. Call Get State Hash and note the number.
  2. Save. Quit Play. Start Play again (the sculpting is gone).
  3. Load from your slot. Get State Hash returns the number from step 1, and the hill is back.
  4. 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.

Plugin 1.0 · Unreal Engine 5.8Found a mistake or something unclear? E-mail us