How-To: Change Where Saves Are Stored

Goal: put save data somewhere other than the engine's default location - a specific folder, or a cloud service such as Steam Cloud, EOS, or your own server.

Prerequisites
Saving already works (see Quick Start: Saveable Systems). Read Concept: Where Saves Are Stored first if you have not - it explains the provider model this page configures.
Do you actually need this?
The default provider is already correct on PC and console, and platform-managed cloud sync (PlayStation / Switch / Xbox / Steam Auto-Cloud) works with no change. You need this page only for a custom directory or a service-API backend.

1. Point saves at a specific directory

Switch Storage Provider Class to UCrimsonFileSaveStorageProvider and set Custom Save Directory. This writes plain .sav files to a directory you choose instead of going through the platform save system.

**Project Settings -> Crimson -> Crimson Save Game Manager -> Storage**. (1) Set **Storage Provider Class** to `CrimsonFileSaveStorageProvider`. (2) Type a path into **Custom Save Directory**, e.g. `{UserDir}/{Project}/Saves`.

Custom Save Directory takes an absolute path and expands three tokens:

TokenExpands toExample result
{SavedDir}The project's Saved/ directory.../MyGame/Saved/
{UserDir}The user's documents directoryC:/Users/Name/Documents/
{Project}The project nameMyGame

So {UserDir}/{Project}/Saves resolves to C:/Users/Name/Documents/MyGame/Saves. Leaving the setting empty means {SavedDir}SaveGames, which matches where the engine's default save system writes. The directory is created if it does not exist.

Verify
Play, trigger a save, and confirm .sav blobs appear in the directory you configured (not in Saved/SaveGames/). With bVerboseLogging on, the log line FileSaveStorageProvider: saving to '<path>' reports the resolved directory at startup.
This bypasses the platform save system
UCrimsonFileSaveStorageProvider talks straight to the filesystem, so it is a PC-oriented choice. On console, keep the default UCrimsonPlatformSaveStorageProvider - only it writes into the platform's save container, and shipping a console title that writes saves to a raw path will fail certification.

2. Write a provider for a cloud or custom backend

C++ only - and why
Storage providers are a C++ surface. Their functions may be called from a background thread, which Blueprint cannot safely execute on, and a real backend needs an SDK (ISteamRemoteStorage, EOS, HTTP) that has no Blueprint equivalent. Choosing a provider is fully Blueprint-friendly (step 1); writing one is not.

Subclass UCrimsonSaveStorageProvider and implement five synchronous functions. Async comes for free: the base WriteBlobAsync / ReadBlobAsync run your synchronous implementation on a background thread and marshal the completion delegate back to the game thread, which is exactly what you want for anything that blocks on I/O.

cpp
// MyCloudSaveStorageProvider.h
#pragma once
#include "CoreMinimal.h"
#include "Core/Storage/CrimsonSaveStorageProvider.h"
#include "MyCloudSaveStorageProvider.generated.h"
UCLASS()
class MYGAME_API UMyCloudSaveStorageProvider : public UCrimsonSaveStorageProvider
{
GENERATED_BODY()
public:
// Optional: open the connection / validate config. Return false to reject the provider;
// the manager then logs an error and falls back to the platform provider.
virtual bool InitializeStorage() override;
virtual void ShutdownStorage() override;
// Required. NOTE: these may run on a background thread.
virtual bool WriteBlob(const FCrimsonSaveBlobId& BlobId, const TArray<uint8>& Data) override;
virtual bool ReadBlob(const FCrimsonSaveBlobId& BlobId, TArray<uint8>& OutData) override;
virtual bool DeleteBlob(const FCrimsonSaveBlobId& BlobId) override;
virtual bool BlobExists(const FCrimsonSaveBlobId& BlobId) const override;
virtual bool EnumerateBlobs(const FCrimsonSaveSlotId& SlotId,
TArray<FCrimsonSaveBlobId>& OutBlobs) const override;
private:
// Map a blob id to whatever key your backend uses, and back again.
// EnumerateBlobs depends on this round-tripping exactly.
static FString MakeRemoteKey(const FCrimsonSaveBlobId& BlobId);
static bool ParseRemoteKey(const FString& Key, FCrimsonSaveBlobId& OutBlobId);
};
cpp
// MyCloudSaveStorageProvider.cpp
#include "MyCloudSaveStorageProvider.h"
FString UMyCloudSaveStorageProvider::MakeRemoteKey(const FCrimsonSaveBlobId& BlobId)
{
// Any stable, reversible encoding works. The shipped providers' scheme is available
// for reuse via UCrimsonPlatformSaveStorageProvider::BlobIdToStorageName().
return FString::Printf(TEXT("%s/%d/%d/%s/%s"),
*BlobId.Slot.ToString(),
(int32)BlobId.Slot.Kind,
(int32)BlobId.BlobKind,
*BlobId.Name,
*BlobId.PlayerKey);
}
bool UMyCloudSaveStorageProvider::WriteBlob(const FCrimsonSaveBlobId& BlobId, const TArray<uint8>& Data)
{
// Blocking upload. Safe: the manager calls this from a worker thread via WriteBlobAsync.
return MyBackend::Put(MakeRemoteKey(BlobId), Data);
}
bool UMyCloudSaveStorageProvider::ReadBlob(const FCrimsonSaveBlobId& BlobId, TArray<uint8>& OutData)
{
return MyBackend::Get(MakeRemoteKey(BlobId), OutData);
}
bool UMyCloudSaveStorageProvider::EnumerateBlobs(const FCrimsonSaveSlotId& SlotId,
TArray<FCrimsonSaveBlobId>& OutBlobs) const
{
// Must return ids whose Name and PlayerKey round-trip - this is how the manager
// discovers what a slot contains.
TArray<FString> Keys;
if (!MyBackend::List(SlotId.ToString(), Keys))
{
return false;
}
for (const FString& Key : Keys)
{
FCrimsonSaveBlobId BlobId;
if (ParseRemoteKey(Key, BlobId) && BlobId.Slot == SlotId)
{
OutBlobs.Add(BlobId);
}
}
return true;
}
Threading contract
The synchronous functions may be called from a background thread. Do not touch UObjects, the world, or any non-thread-safe engine subsystem inside them. Keep them to your backend SDK and the byte buffer you were handed. If your backend has a native async API worth using, override WriteBlobAsync / ReadBlobAsync instead - but you must still fire the completion delegate on the game thread.

Then select your class in Storage Provider Class, exactly as in step 1:

ini
; Config/DefaultGame.ini
[/Script/CrimsonSaveSystem.CrimsonSaveGameManagerDeveloperSettings]
StorageProviderClass=/Script/MyGame.MyCloudSaveStorageProvider
Verify
At startup the log reports Save storage provider: MyCloudSaveStorageProvider. Trigger a save and confirm the blobs land in your backend; then delete the local Saved/SaveGames/ folder entirely and load the slot - it must still load, proving nothing fell back to disk. Check Play As Client too: saves are server-authoritative, so only the server writes.

3. Optional: speed up slot listing

The base EnumerateSlots finds slots by probing each slot number for a header, one BlobExists call at a time. That is portable but chatty - it is what the save menu calls through GetAllSaveSlotHeaders. If your backend can list its contents in one request, override EnumerateSlots and answer from a single listing. Both shipped providers do exactly this.

Likewise, override DeleteSlot if your backend can drop a whole container atomically; the default enumerates and deletes blob by blob.

See also

  • Concept: Where Saves Are Stored
  • Concept: Multiplayer & Player Scoping
  • API Reference -> Storage providers
  • API Reference -> Developer Settings -> Storage