Concept: Where Saves Are Stored
The save manager decides what to save and serializes it down to bytes. A storage provider decides where those bytes go. Nothing in the save manager knows about files, paths, or platforms - so retargeting storage never means touching save logic, and a cloud backend is a small class rather than a fork of the save system.
ISaveGameSystem, which is the platform's own save system. It is already correct on PC and on console, including per-user save containers and platform-managed cloud sync. Read this page when you want saves somewhere else - not to get them working.The two halves
| Layer | Owns | Examples |
|---|---|---|
UCrimsonSaveGameManagerSubsystem | What to save: slots, fragments, world slabs, versioning, player scoping, serialization to bytes. | RequestSaveProgress, RegisterSaveableSystem, fragment upgrades. |
UCrimsonSaveStorageProvider | Where the bytes live: write, read, delete, exists, enumerate. | The platform save system, a directory of files, an HTTP API. |
Choose the provider in Project Settings -> Crimson -> Crimson Save Game Manager -> Storage Provider Class. The manager instantiates it once at startup and calls InitializeStorage before any save access. If the configured class is unset or fails to initialize, the manager logs an error and falls back to the platform provider rather than silently discarding saves.
Shipped providers
| Provider | Backing store | Use it for |
|---|---|---|
UCrimsonPlatformSaveStorageProvider (default) | Engine ISaveGameSystem | Everything, unless you have a specific reason not to. On PC this resolves to the engine's generic file system (Saved/SaveGames/*.sav); on console it resolves to that platform's save system module and writes into the platform save container, honoring its user binding and native save UI. |
UCrimsonFileSaveStorageProvider | Plain files under a directory you configure | Putting saves somewhere specific on PC - a Documents subfolder, a Steam Auto-Cloud path, or separate directories per build. Bypasses ISaveGameSystem, so it is a PC-oriented choice. |
The blob model
A provider never sees a USaveGame, a UObject, or a path. It is a keyed byte store. The key is FCrimsonSaveBlobId, which carries everything needed to name, find, or delete one piece of save data:
| Field | Type | Meaning |
|---|---|---|
Slot | FCrimsonSaveSlotId | Slot kind (Manual / AutoSave / QuickSave / Global) plus a number. This is the decoded form of the public int32 slot index, so providers never deal with the index-offset encoding. |
BlobKind | ECrimsonSaveBlobKind | Header, Fragment, WorldSlab, or UserSettings. |
Name | FString | Fragment name for Fragment blobs, level name for WorldSlab blobs. Empty otherwise. |
PlayerKey | FString | Set only for player-scoped fragments. Empty means the blob is shared by all players. |
EnumerateBlobs - it never scans a directory. That is what lets a backend with no filesystem at all (a console container, an HTTP API) work unchanged. A provider's EnumerateBlobs must return ids whose Name and PlayerKey round-trip exactly.Storage naming
Both shipped providers store blobs flat - one blob per name, no subdirectories - with the identity encoded in the name:
Slot0_H header for manual slot 0Auto1_H header for auto-save slot 1Slot0_F_Inventory global fragment "Inventory" in manual slot 0Slot0_F_Inventory__Key the same fragment scoped to player "Key"Slot0_W_MainLevel world state for level "MainLevel" in manual slot 0Global_S user plugin settings
Flat is deliberate, and it is the reason the layout looks the way it does. Console save systems treat the name as an opaque container key and reject path separators outright. On top of that, the engine's own ISaveGameSystem::GetSaveGameNames does not recurse into subdirectories - so a nested layout could not be enumerated even on PC.
__) - it delimits the player key in the stored name. The manager validates this and logs an error rather than writing a blob it cannot read back. Single underscores are fine (My_Fragment, PIE_Server).Cloud saves
"Cloud save" means two very different things depending on the platform, and only one of them needs code from you.
| Platform | How it works | Needs a custom provider? |
|---|---|---|
| PlayStation / Switch / Xbox | The platform syncs its own save container. The default provider writes into that container, so sync is automatic. | No |
| Steam (Auto-Cloud) | Steam syncs files matching a pattern you register in the Steamworks partner site. Point it at the save directory; the plugin is not involved. | No |
| Steam Cloud API | Reads and writes through ISteamRemoteStorage rather than the filesystem. | Yes |
| Epic Online Services | Reads and writes through EOS Player Data Storage. | Yes |
| Custom server | Your own HTTP API. | Yes |
The pattern: if the platform syncs a container or directory for you, the default provider is enough. If the backend is a service API, it needs a provider - see How-To: Change Where Saves Are Stored.
Per-user storage
On console, save data is bound to a signed-in platform user. UCrimsonPlatformSaveStorageProvider::SetUserIndex selects which local user's storage to use; it defaults to 0, the primary user. This is distinct from the player save key, which scopes fragments within a slot for multiplayer - see Concept: Multiplayer & Player Scoping. One is which storage container to open; the other is which fragment inside it belongs to whom.
See also
- How-To: Change Where Saves Are Stored
- Concept: The Fragment Model
- Concept: Multiplayer & Player Scoping
- API Reference -> Storage providers