Skip to content
UCIK Docs
Esc
↑↓navigate↵open⌘Jpreview
On this page

Save player data

Write, read, and reconcile a player's saved data.

Configure Storage providers in Betide → Crossplay, then sign in. The write examples store UTF-8 text. Serialize Unreal save objects yourself before uploading.

Read a save

Read a save
Preparing your graph…

Loading the interactive viewer.

Game module dependencies: UCIKCore, BetideCore.

#include "Engine/GameInstance.h"
#include "Storage/UCIKStorageSubsystem.h"
#include "UCIKCore.h"

void ReadCloudSave(UGameInstance &GameInstance)
{
    auto *Service = GameInstance.GetSubsystem<UUCIKStorageSubsystem>();
    if (!Service)
        return;

    Service->ReadUserFile(TEXT("save.bin"),
                          FBetideCallback<FUCIK_UserFile>::CreateWeakLambda(
                              &GameInstance, [](const TBetideResult<FUCIK_UserFile> &Result) {
                                  if (!Result.bOk)
                                  {
                                      UE_LOG(LogUCIK, Warning, TEXT("%s"),
                                             *Result.Error.GetLogString());
                                      return;
                                  }
                                  UE_LOG(LogUCIK, Log, TEXT("Save bytes: %d; checksum: %s"),
                                         Result.GetValue().Data.Num(), *Result.GetValue().Checksum);
                              }));
}

Keep the returned Checksum with the local save. A read uses the first configured provider; it does not merge different saves across stores.

Write without overwriting a changed save

Write without overwriting a changed save
Preparing your graph…

Loading the interactive viewer.

Game module dependencies: UCIKCore, UCIKNodes, BetideCore.

#include "Engine/GameInstance.h"
#include "Helpers/UCIK_HelperLibraries.h"
#include "Storage/UCIKStorageSubsystem.h"
#include "UCIKCore.h"

void WriteCloudSave(UGameInstance &GameInstance, const FString &SaveText,
                    const FString &PreviousChecksum)
{
    auto *Service = GameInstance.GetSubsystem<UUCIKStorageSubsystem>();
    if (!Service)
        return;

    const FUCIK_UserFile File =
        UUCIK_UserFileLibrary::MakeUserFileFromString(TEXT("save.bin"), SaveText);
    Service->WriteUserFileWithConflictCheck(
        File, PreviousChecksum,
        FBetideCallback<TMap<FBetideProviderId, bool>>::CreateWeakLambda(
            &GameInstance, [](const TBetideResult<TMap<FBetideProviderId, bool>> &Result) {
                if (!Result.bOk)
                {
                    UE_LOG(LogUCIK, Warning, TEXT("%s"), *Result.Error.GetLogString());
                    return;
                }
                for (const auto &Pair : Result.GetValue())
                {
                    UE_LOG(LogUCIK, Log, TEXT("%s: %s"), *Pair.Key.ToString(),
                           Pair.Value ? TEXT("written") : TEXT("not written"));
                }
            }));
}

An empty expected checksum means “create only if absent.” Bind On Save Conflict to offer a choice between the local and remote save. Do not silently retry a conflict with an unconditional write.

Every write provider must pass the read/check step before uploads begin. Another device can write between the check and upload. If an upload fails, providers can be left with different copies. Read back before the next edit.

Files and quota

List cloud saves
List cloud saves
Preparing your graph…

Loading the interactive viewer.

Read storage quota
Read storage quota
Preparing your graph…

Loading the interactive viewer.

Delete a cloud save
Delete a cloud save
Preparing your graph…

Loading the interactive viewer.

Overwrite a cloud save
Preparing your graph…

Loading the interactive viewer.

Game module dependencies: UCIKCore, UCIKNodes, BetideCore.

#include "Engine/GameInstance.h"
#include "Helpers/UCIK_HelperLibraries.h"
#include "Storage/UCIKStorageSubsystem.h"
#include "UCIKCore.h"

namespace CloudSaveExamples
{
UUCIKStorageSubsystem *Service(UGameInstance &Game)
{
    auto *Storage = Game.GetSubsystem<UUCIKStorageSubsystem>();
    if (!Storage)
        UE_LOG(LogUCIK, Warning, TEXT("Storage subsystem is unavailable"));
    return Storage;
}

void ListCloudSaves(UGameInstance &Game)
{
    auto *Storage = Service(Game);
    if (!Storage)
        return;
    Storage->EnumerateUserFiles(FBetideCallback<TArray<FUCIK_UserFileMetadata>>::CreateWeakLambda(
        &Game, [](const TBetideResult<TArray<FUCIK_UserFileMetadata>> &Result) {
            if (!Result.bOk)
            {
                UE_LOG(LogUCIK, Warning, TEXT("%s"), *Result.Error.GetLogString());
                return;
            }
            for (const FUCIK_UserFileMetadata &File : Result.GetValue())
                UE_LOG(LogUCIK, Log, TEXT("%s: %lld bytes"), *File.Name, File.SizeBytes);
        }));
}

void ReadStorageQuota(UGameInstance &Game)
{
    auto *Storage = Service(Game);
    if (!Storage)
        return;
    Storage->GetStorageQuota(FBetideCallback<TArray<FUCIK_StorageQuota>>::CreateWeakLambda(
        &Game, [](const TBetideResult<TArray<FUCIK_StorageQuota>> &Result) {
            if (!Result.bOk)
            {
                UE_LOG(LogUCIK, Warning, TEXT("%s"), *Result.Error.GetLogString());
                return;
            }
            for (const FUCIK_StorageQuota &Quota : Result.GetValue())
                UE_LOG(LogUCIK, Log,
                       TEXT("%s: available %lld; total %lld bytes (-1 means unknown)"),
                       *Quota.ProviderId.ToString(), Quota.AvailableBytes, Quota.TotalBytes);
        }));
}

FBetideCallback<TMap<FBetideProviderId, bool>> ReportProviders(UGameInstance &Game, FString Operation)
{
    return FBetideCallback<TMap<FBetideProviderId, bool>>::CreateWeakLambda(
        &Game, [Operation =
                    MoveTemp(Operation)](const TBetideResult<TMap<FBetideProviderId, bool>> &Result) {
            if (!Result.bOk)
            {
                UE_LOG(LogUCIK, Warning, TEXT("%s: %s"), *Operation, *Result.Error.GetLogString());
                return;
            }
            for (const auto &Pair : Result.GetValue())
                UE_LOG(LogUCIK, Log, TEXT("%s on %s: %s"), *Operation, *Pair.Key.ToString(),
                       Pair.Value ? TEXT("completed") : TEXT("not completed"));
        });
}

void DeleteConfirmedSave(UGameInstance &Game, const FString &FileName)
{
    if (auto *Storage = Service(Game))
        Storage->DeleteUserFile(FileName, ReportProviders(Game, TEXT("Delete save")));
}

void OverwriteConfirmedCloudSave(UGameInstance &Game, const FString &SaveText)
{
    auto *Storage = Service(Game);
    if (!Storage)
        return;
    const FUCIK_UserFile File =
        UUCIK_UserFileLibrary::MakeUserFileFromString(TEXT("save.bin"), SaveText);
    Storage->WriteUserFile(File, ReportProviders(Game, TEXT("Overwrite save")));
}
} // namespace CloudSaveExamples

Only call delete or overwrite after the player chooses it. Refresh the file list after completion. Listing uses the first provider that supports enumeration.

Multiple providers and conflicting devices

The conflict check reads every selected write provider before writing. A failed read, failed absence check, or changed checksum stops all writes. Checking and writing are not atomic: another device can still write between them, and a later upload failure can leave only some providers updated.

A successful write result does not guarantee every provider succeeded. C++ exposes per-provider results; the Blueprint write/delete nodes expose only overall success and error. Use C++ when you need per-provider write receipts. Quota -1 means unknown; EOS storage does not expose quota through this API. A zero modification time means unavailable, not an empty save.

Use provider-specific Game Center saved games and Google Play snapshots through their own APIs.

Was this page helpful?