---
title: "Save player data"
description: "Write, read, and reconcile a player's saved data."
seo:
  noindex: true
---

{/* Copyright (c) 2026 Betide Studio. All Rights Reserved. */}

Configure **Storage** providers in **Betide → Crossplay**, then [sign in](/crossplay/identity). The write examples store UTF-8 text. Serialize Unreal save objects yourself before uploading.

## Read a save

**Blueprint**

<BlueprintViewer title="Read a save" src="/blueprints/crossplay-read-user-file.txt" />

**C++**

Game module dependencies: `UCIKCore`, `BetideCore`.

```cpp
#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

**Blueprint**

<BlueprintViewer title="Write without overwriting a changed save" src="/blueprints/crossplay-guarded-user-file.txt" />

**C++**

Game module dependencies: `UCIKCore`, `UCIKNodes`, `BetideCore`.

```cpp
#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

**Blueprint**

<Accordion>
  <AccordionItem title="List cloud saves">
<BlueprintViewer title="List cloud saves" src="/blueprints/crossplay-enumerate-user-files.txt" />
  </AccordionItem>
  <AccordionItem title="Read storage quota">
<BlueprintViewer title="Read storage quota" src="/blueprints/crossplay-storage-quota.txt" />
  </AccordionItem>
  <AccordionItem title="Delete a cloud save">
<BlueprintViewer title="Delete a cloud save" src="/blueprints/crossplay-delete-user-file.txt" />
  </AccordionItem>
</Accordion>
<BlueprintViewer title="Overwrite a cloud save" src="/blueprints/crossplay-overwrite-user-file.txt" />

**C++**

Game module dependencies: `UCIKCore`, `UCIKNodes`, `BetideCore`.

```cpp
#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.

<Accordion>
  <AccordionItem title="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](/integrations/apple/game-services) saved games and [Google Play](/integrations/google/game-services) snapshots through their own APIs.

  </AccordionItem>
</Accordion>
