> ## Documentation Index
> Fetch the complete documentation index at: https://devdocs.xbox.com/llms.txt
> Use this file to discover all available pages before exploring further.

# 将 Steam Cloud 存储移植到 XBOX GDK

> 使用 XBOX GDK 的 XGameSaves 封装替换 Steam Cloud 和 ISteamRemoteStorage，并提供读写与同步存档的并排代码示例。

Steam 通过两种方式之一启用云存储。你可以使用 `ISteamRemoteStorage` API 方法完成所有读写操作，这些方法会将文件写入本地硬盘上游戏的存储文件夹并同步到云端。也可以选择直接对计算机文件系统进行所有读写操作，然后使用 Steam Auto-Cloud 自动将包含游戏数据的本地文件夹同步到云端。

XBOX Game Development Kit（GDK）支持这两种方法：

* 对于基于代码的云存档，GDK 提供了一个更复杂的 `XGameSaves` API 的[简化封装](/reference/system/Wrappers/xgamesave_wrapper_members)，其功能与 `ISteamRemoteStorage` 的方法类似。
* 对于类似 Steam Auto-Cloud 的方式，GDK 支持[使用无代码云存档将旧游戏移植到 PC 游戏存档](/build/core-features/common/game-save/game-saves-walkthroughs-and-samples#porting-previous-titles-to-pc-game-saves-with-no-code-cloud-saves)，可将指定的本地文件夹同步到云端，而无需修改文件 I/O 代码。

<Warning>
  XBOX 主机和 XBOX Cloud Gaming 不支持无代码云存档。如果你的游戏在 Windows PC 之外还面向主机或云平台，请改用 `XGameSaves` 封装或完整的 `XGameSaves` API。
</Warning>

由于基于代码的封装是面向主机或云平台发行的游戏的推荐路径，并且它与本地写入文件或使用 Steam Remote Storage API 的方式高度接近，因此本主题重点介绍如何使用该简化封装。

<Note>
  完整（非封装）的 `XGameSaves` API 比这个简单封装提供了更多的功能和灵活性。如果你想使用该 API 的任何功能，就不应使用封装，因为游戏绝不应在两者之间切换或混合调用两者的 API。有关 `XGameSaves` API 的更多信息，请参阅 [Game saves](/build/core-features/common/game-save/game-saves-overview)。
</Note>

以下代码示例展示了 Steamworks SDK 和 XBOX Game Development Kit（GDK）中基本文件操作的实现方式，并附带说明两者 API 之间的一些细微差异。

## 文件操作对比

以下代码示例展示了如何在 Steamworks Remote Storage API 和 GDK 中完成等价的基本文件操作。它们假定 `provider` 变量持有一个已初始化的 `Microsoft::Xbox::Wrappers::GameSave::Provider` 对象的指针。

如果你没有使用 Steam Remote Storage API，而是选择使用 Steam Auto-Cloud，请将 Remote Storage API 调用替换为等价的文件系统 API 调用。

### 读取文件

#### Steamworks

```cpp theme={null}
int32 size = SteamRemoteStorage()->GetFileSize("MyFile.json");
if (size > 0) 
{
    char *buffer = new char[size];
    bool result = SteamRemoteStorage()->FileRead("MyFile.json", buffer, size);
}           
```

或

```cpp theme={null}
int32 size = GetFileSize("MyFile.json");
if (size > 0)
{
    m_readResult = SteamRemoteStorage()->FileReadAsync("MyFile.json", 0, size);
    // Check the value of m_readResult in callback for error handling.
    STEAM_CALLBACK(MyGameClass, OnFileReadCompleted, RemoteStorageFileReadAsyncComplete_t);
}
```

#### GDK

```cpp theme={null}
BlobData data = provider->Load("SaveSlot1", "MyData");
if(!data.empty())
{
    // Iterate over the data to read bytes from the file.
}
else
{
    // Couldn't find the container/blob name.
}
```

#### 参考文档

[Microsoft.Xbox.Wrappers.XGameSave.Provider.Load](/reference/system/Wrappers/xgamesave_wrapper_members)

### 写入文件

#### Steamworks

```cpp theme={null}
std::string saveData = "{progress: 25}";
SteamRemoteStorage()->FileWrite("MyFile.json", saveData.c_str(), saveData.size());
```

或

```cpp theme={null}
std::string saveData = "{progress: 25}";
m_writeResult = SteamRemoteStorage()->FileWriteAsync("MyFile.json", saveData.c_str(), saveData.size());
STEAM_CALLBACK(MyGameClass, OnFileWriteCompleted, RemoteStorageFileWriteAsyncComplete_t);
```

#### XBOX Game Development Kit（GDK）

```cpp theme={null}
std::vector<uint8_t> saveData; // Contains the player's data.
HRESULT hr = provider->Save("SaveSlot1", "MyData", saveData.size(), saveData.data());
if(FAILED(hr))
{
  if(hr == E_GS_QUOTA_EXCEEDED)
  {
     // Message that the user must clear out saves for this game.
  }
  else if(hr == E_GS_OUT_OF_LOCAL_STORAGE)
  {
     // Message to the user that they have run out of save space on the local device.
  }
  else if(hr == E_GS_UPDATE_TOO_BIG)
  {
     // Your save size was over 16 MB (GS_MAX_BLOB_SIZE).
  }
  else if(hr == E_GS_HANDLE_EXPIRED)
  {
     // Need to re-create the provider and try again.
     // This can happen if your game was suspended and, during that time, another
     // device initialized a provider for the same user.
  }
  else
  {
     // Log error.
  }
}
```

#### 参考文档

[Microsoft.Xbox.Wrappers.XGameSave.Provider.Save](/reference/system/Wrappers/xgamesave_wrapper_members)

### 删除文件

在 Steam 上，你可以只删除云端的文件而保留本地副本（`FileForget`），也可以同时从两处删除文件（`FileDelete`）。`XGameSave` 封装 API 没有与 `FileForget` 等价的功能。它的 `Delete` 函数的作用类似于 Steamworks 中的 `FileDelete`。

#### Steamworks

```cpp theme={null}
// Delete a file from the cloud but keep it locally.
bool result = SteamRemoteStorage()->FileForget("MyFile.json");
```

或

```cpp theme={null}
// Delete a file locally AND from the cloud.
bool result = SteamRemoteStorage()->FileDelete("MyFile.json");
```

#### XBOX Game Development Kit（GDK）

```cpp theme={null}
// Delete a specific file.
HRESULT hr = provider->Delete("MyContainer", "MyData");
```

或

```cpp theme={null}
// Delete a set of files in a container. 
std::vector<std::string> toDelete = { "blob1", "blob2", "blob3" };
HRESULT hr = provider->Delete("MyContainer", toDelete);
```

或

```cpp theme={null}
// Delete all the files in a container.
HRESULT hr = provider->Delete("MyContainer");
```

#### 参考文档

* [Microsoft.Xbox.Wrappers.XGameSave.Provider.Delete(std::string)](/reference/system/Wrappers/xgamesave_wrapper_members)
* [Microsoft.Xbox.Wrappers.XGameSave.Provider.Delete(std::string, std::string)](/reference/system/Wrappers/xgamesave_wrapper_members)
* [Microsoft.Xbox.Wrappers.XGameSave.Provider.Delete(std::string, BlobNames)](/reference/system/Wrappers/xgamesave_wrapper_members)

### 获取所有文件

#### Steamworks

```cpp theme={null}
int32 fileCount = SteamRemoteStorage()->GetFileCount();
for ( int i = 0; i < fileCount; ++i ) {
    int32 fileSize;
    const char *fileName = SteamRemoteStorage()->GetFileNameAndSize( i, &fileSize );
    // Do something with fileSize and fileName.
}
```

#### XBOX Game Development Kit（GDK）

```cpp theme={null}
// To get all the files across all containers for the game, make this = ""
// Otherwise, make it a prefix of whichever container's files you'd like.
std::string containerQuery = "";
std::vector<std::string> containers = provider->QueryContainers(containerQuery);

for (auto&& container : containers)
{
    BlobInfoSet blobs = provider->QueryContainerBlobs(container);
    for (auto&& blob : blobs)
    {
        uint32_t blobSize = blob.size;
        std::string blobName = blob.name;
        // Do something with blobSize and blobName.
    }
}
```

#### 参考文档

* [Microsoft.Xbox.Wrappers.XGameSave.Provider.QueryContainers](/reference/system/Wrappers/xgamesave_wrapper_members)
* [Microsoft.Xbox.Wrappers.XGameSave.Provider.QueryContainerBlobs](/reference/system/Wrappers/xgamesave_wrapper_members)

### 检查可用空间

在以下示例中，`totalBytes` 是你的游戏在云存储提供程序中获得的空间大小，而 `availableBytes` 是剩余的可用空间量（即 `availableBytes` = `totalBytes` – `bytesUsed`）。

#### Steamworks

```cpp theme={null}
uint64 totalBytes, availableBytes;
SteamRemoteStorage()->GetQuota(&totalBytes, &availableBytes);
```

#### XBOX Game Development Kit（GDK）

```cpp theme={null}
// totalBytes is always 256 MB.
int64_t availableBytes = provider->GetQuota();
```

#### 参考文档

[Microsoft.Xbox.Wrappers.XGameSave.Provider.GetQuota](/reference/system/Wrappers/xgamesave_wrapper_members)

## 术语差异

在 Steam 上，远程存储的数据以文件形式管理，其行为类似于本地硬盘上的文件。读写通过指定要读/写的文件、并获取/设置该文件中所含的字节来完成。

在 XBOX Game Development Kit（GDK）中，与 Steam 文件等价的概念是 *blob*（数据块），并且 blob 会被分组存放在一个称为 *container*（容器）的结构中。容器就是一个命名的 blob 分组。例如，可以使用容器为每个用户提供多个存档槽位，在每个槽位中包含同样的文件名。如果你不需要容器提供的这层额外组织结构，只需将所有 blob（文件）放在同一个容器中即可。

<Note>
  容器名称不能包含空格。尝试访问或创建
</Note>

包含空格的容器名会导致提供程序方法返回 HRESULT `0x80830001`：指定的卷不支持存储层。

## 存储限制

XBOX Game Development Kit（GDK）的单个 blob/文件写入大小和整体存储限制都低于 Steam。在 Steam 上，每次文件写入操作限制为 100 mebibytes（MiB）。每个文件不能超过 200 MiB，而 `XGameSave` API 及其封装则不允许单个 blob 超过 16 MB，并且每位用户每款游戏最多可存储 256 MB。

如果你需要在一个 blob 中存储超过 16 MB 的数据，就需要将数据拆分为多个 blob，并实现一个顺序读/写函数，每次读/写一个 blob。

## 封装函数是阻塞式的

`ISteamRemoteStorage` 接口提供两种版本的读写函数：`FileRead`/`FileWrite` 和 `FileReadAsync`/`FileWriteAsync`。后者为异步函数，会在文件读/写完成后进行回调。简化后的 `XGameSave` 封装函数并未为其等价于 `FileRead`/`FileWrite` 的函数提供异步版本。不过，`Provider::Load` 和 `Provider::Save` 都是阻塞式的，因此在你的游戏中使用它们时务必牢记这一点。

出于这个原因，*如果在 UI 线程中调用 `Provider::Initialize`，则会抛出异常*。

## 初始化

在做任何其他事情之前，你需要将封装的头文件包含到你的游戏解决方案中。它位于 *%GRDKLatest%\GameKit\Include\xgamesavewrappers.hpp*。

在使用 `XGameSaves` 封装的方法之前，你必须创建 `Provider` 类的实例（你应在游戏的整个生命周期内持有它的指针），并调用 `Provider::Initialize` 方法。再次强调，该方法应在与 UI 线程分开的线程中调用，如果从 UI 线程调用则会抛出异常。请注意，要初始化封装的提供程序，你需要有当前用户的 `XUserHandle` 以及你的游戏的服务配置标识符（SCID）。

```cpp theme={null}
using namespace Microsoft::Xbox::Wrappers::GameSave;

Provider provider = new Provider();
if(SUCCEEDED(provider->Initialize(userHandle, mySCID)) {
    // Start using the XGameSave wrapper...
```

#### 参考文档

[Microsoft.Xbox.Wrappers.XGameSave.Provider.Initialize](/reference/system/Wrappers/xgamesave_wrapper_members)


## Related topics

- [从 Steam 移植](/zh-CN/paths/porting/from-steam.md)
- [Steam 移植指南概览](/zh-CN/build/steam-porting-guide/overview.md)
- [Stats 与成就](/zh-CN/build/steam-porting-guide/features/stats-and-achievements.md)
- [将现有输入代码移植到 GameInput](/zh-CN/build/core-features/common/input/porting/index.md)
- [从 Windows.XBOX.Input 移植到 GameInput](/zh-CN/build/core-features/common/input/porting/input-porting-wxi.md)
