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.

You probably do not need to change this
The default provider routes through the engine's 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

LayerOwnsExamples
UCrimsonSaveGameManagerSubsystemWhat to save: slots, fragments, world slabs, versioning, player scoping, serialization to bytes.RequestSaveProgress, RegisterSaveableSystem, fragment upgrades.
UCrimsonSaveStorageProviderWhere 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

ProviderBacking storeUse it for
UCrimsonPlatformSaveStorageProvider (default)Engine ISaveGameSystemEverything, 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.
UCrimsonFileSaveStorageProviderPlain files under a directory you configurePutting 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:

FieldTypeMeaning
SlotFCrimsonSaveSlotIdSlot 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.
BlobKindECrimsonSaveBlobKindHeader, Fragment, WorldSlab, or UserSettings.
NameFStringFragment name for Fragment blobs, level name for WorldSlab blobs. Empty otherwise.
Scope / ProfileIdFStringSet only for player-scoped fragments. Empty means the blob is shared by all players.
Why enumeration matters
The manager discovers what a slot contains by calling 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

text
Settings.sav user plugin settings
LocalProfiles.sav local user index -> profile id, this machine
Profiles/<ProfileId>/<Fragment>.sav profile-scoped data; travels between servers
Slots/Slot0/Header.sav slot metadata
Slots/Slot0/Shared/<Fragment>.sav world-scoped data, shared by the session
Slots/Slot0/World/<Level>/<AB>/<ActorGuid>.sav one saveable actor's captured state
Slots/Slot0/Players/<ProfileId>/<Fragment>.sav one player's data inside this slot
One file per actor, not per level
World state is stored a row at a time so a save costs what changed rather than what exists. A level's state used to be one blob, which meant moving a single crate re-serialized every actor in that level - game-thread CPU as much as disk. Rows make both halves proportional to the number of actors that actually changed.

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

text
Slot0_H header for manual slot 0
Auto1_H header for auto-save slot 1
Slot0_F_Inventory world-scoped fragment "Inventory" in manual slot 0
Slot0_F_Inventory__<ProfileId> the same fragment scoped to one player
Slot0_W_MainLevel world state for level "MainLevel" in manual slot 0
Global_S user plugin settings
Global_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.

Reserved sequence: double underscore (flat provider only)
A fragment or level name must not contain a double underscore (__) - 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.

PlatformHow it worksNeeds a custom provider?
PlayStation / Switch / XboxThe 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 APIReads and writes through ISteamRemoteStorage rather than the filesystem.Yes
Epic Online ServicesReads and writes through EOS Player Data Storage.Yes
Custom serverYour 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