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. |
Scope / ProfileId | 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, Scope and ProfileId all round-trip exactly.Storage naming
The two shipped providers deliberately store the same blobs differently, because they answer to different constraints. This is the whole reason the provider abstraction exists - and it is why the two are not file-compatible with each other.
File provider (desktop default): a directory tree
Settings.sav user plugin settingsLocalProfiles.sav local user index -> profile id, this machineProfiles/<ProfileId>/<Fragment>.sav profile-scoped data; travels between serversSlots/Slot0/Header.sav slot metadataSlots/Slot0/Shared/<Fragment>.sav world-scoped data, shared by the sessionSlots/Slot0/World/<Level>/<AB>/<ActorGuid>.sav one saveable actor's captured stateSlots/Slot0/Players/<ProfileId>/<Fragment>.sav one player's data inside this slot
The two-character directory between the level and the file shards rows by their id, for the same reason One File Per Actor shards external actor packages: a World Partition world resolves every one of its actors to a single owning level, so without it they would all land in one directory.
Auto and quick saves use Auto<N> and Quick<N> in place of Slot<N>. The structure is the point: it is browsable, one player's data can be copied or deleted on its own, a whole slot is one recursive delete, and no character sequence is reserved in fragment or level names.
Platform provider (console): flat container keys
Slot0_H header for manual slot 0Auto1_H header for auto-save slot 1Slot0_F_Inventory world-scoped fragment "Inventory" in manual slot 0Slot0_F_Inventory__<ProfileId> the same fragment scoped to one playerSlot0_W_MainLevel world state for level "MainLevel" in manual slot 0Global_S user plugin settingsGlobal_P local profile store
Flat is not a stylistic choice here. ISaveGameSystem treats a save name as an opaque container key rather than a path and rejects separators outright, and FGenericSaveGameSystem::GetSaveGameNames does not recurse into subdirectories - so a nested layout cannot be enumerated through it even on PC.
__) - it delimits the profile id in the flat storage name. The manager logs an error and skips the blob rather than writing something it cannot read back. The file provider has no such restriction, because the profile is a directory rather than a name suffix.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 & Save Scopes. 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 & Save Scopes
- API Reference -> Storage providers