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.
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.
Custom Save Directory takes an absolute path and expands three tokens:
| Token | Expands to | Example result |
|---|---|---|
{SavedDir} | The project's Saved/ directory | .../MyGame/Saved/ |
{UserDir} | The user's documents directory | C:/Users/Name/Documents/ |
{Project} | The project name | MyGame |
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.
.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.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
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.
// 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);};
// 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;}
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:
; Config/DefaultGame.ini[/Script/CrimsonSaveSystem.CrimsonSaveGameManagerDeveloperSettings]StorageProviderClass=/Script/MyGame.MyCloudSaveStorageProvider
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