Storage

Microsoft::Xna::Framework::Storage — StorageDevice, StorageContainer, StorageDeviceNotConnectedException

Implementation status: 100% functionally complete. All three Storage types are fully implemented — real std::filesystem I/O, a hand-written glob matcher for the pattern-based directory queries, and correct <root>/<displayName>/Player{N} save namespacing. The async Begin*/End* pairs are implemented synchronously, which is the correct behaviour on desktop.

Storage remains lightly tested. Alpha.1 has one Storage test source with five GoogleTest-family definitions. That is enough to disprove the old zero-test claim, but not enough to call the whole persistence surface exhaustively verified. Treat implementation audit and those scoped tests as evidence, then test the target filesystem behavior your game depends on.

Overview

The Microsoft::Xna::Framework::Storage namespace provides a platform-independent abstraction for save-game and user-data persistence. On the original Xbox 360, StorageDevice represented a physical memory unit or hard drive, and device selection was presented to the player as a system UI. On PC, the selector returned immediately with the local user profile directory.

CNA preserves the full XNA 4.0 API shape, including the Begin/End asynchronous pattern, and backs it with genuine std::filesystem operations. StorageContainer maps to a real directory on the local filesystem and StorageDevice represents that location.

The async wrappers complete synchronously. This is a deliberate choice rather than a shortcut: the pattern exists in XNA because Xbox 360 storage selection could block on player interaction with a system UI, which has no desktop analogue. Local filesystem operations return fast enough that dispatching them to a thread would add latency and failure modes without adding responsiveness. Source-level XNA compatibility is preserved either way — your callback still fires and End* still returns the result.

On the web, saves do not persist. Under Emscripten, SDL_GetPrefPath() resolves into the in-memory MEMFS filesystem. CNA mounts no IDBFS and never calls FS.syncfs, so every StorageContainer write succeeds, reports success, and is then silently discarded when the page reloads. If your game targets the browser and needs persistence, save through your own localStorage or IndexedDB path rather than through StorageDevice.

Namespace members at a glance

Type Description Status
StorageDevice Represents a storage location (local disk). Obtained via the async selector. Implemented
StorageContainer Opened from a StorageDevice; provides file and directory access within a named save scope. Implemented
StorageDeviceNotConnectedException Thrown when a previously valid StorageDevice is no longer available. Implemented

StorageDevice

StorageDevice is the entry point into the storage system. On PC (and in CNA on all platforms) it represents the local filesystem. To obtain a device, use the static Begin/End async selector pattern. On a PC target the selector returns immediately with a device backed by the current user's profile directory.

Device selection (Begin/End pattern)

MethodDescription
StorageDevice::BeginShowSelector(callback, state) Starts the (synchronous on CNA) device-selection operation. callback is an AsyncCallback invoked when the result is ready; state is an arbitrary user object passed through to the callback.
StorageDevice::EndShowSelector(result) Retrieves the selected StorageDevice from the IAsyncResult returned by BeginShowSelector. Returns nullptr if the user cancelled selection (not applicable on CNA, where a device is always available).

Opening a container

MethodDescription
device.BeginOpenContainer(name, callback, state) Begins opening a StorageContainer with the given display name. The container maps to a subdirectory within the device root.
device.EndOpenContainer(result) Retrieves the opened StorageContainer from the IAsyncResult. The caller is responsible for closing the container when done.

On-disk layout

Saves are namespaced on disk as <root>/<displayName>/Player{N}, matching XNA's own scheme:

  • <root> — the storage root the StorageDevice represents.
  • <displayName> — the container name you pass to BeginOpenContainer.
  • Player{N} — the per-player subdirectory, derived from the PlayerIndex the device was selected for.

The consequence is the one XNA intended: two players on the same machine writing to the same container name do not collide, and a container opened without a player index is shared across the title.

StorageContainer

StorageContainer provides file and directory access scoped to a named save context within a device. It implements the XNA 4.0 API almost entirely. All paths passed to its methods are relative to the container root directory.

File operations

MethodReturnsDescription
OpenFile(path, mode) Stream Opens or creates a file at path using the given FileMode (Create, Open, OpenOrCreate, Append, Truncate). Returns a Stream for reading or writing.
FileExists(path) bool Returns true if the file exists within the container.
DeleteFile(path) void Deletes the file at path. Throws if the file does not exist.
GetFileNames() std::vector<std::string> Returns the names of all files in the container root (non-recursive).
GetFileNames(pattern) std::vector<std::string> Returns file names matching a wildcard pattern (e.g. "*.sav").

Directory operations

MethodReturnsDescription
CreateDirectory(path) void Creates a subdirectory within the container (creates intermediate directories as needed).
DirectoryExists(path) bool Returns true if the directory exists within the container.
GetDirectoryNames() std::vector<std::string> Returns the names of all subdirectories in the container root (non-recursive).
GetDirectoryNames(pattern) std::vector<std::string> Returns directory names matching a wildcard pattern.

Pattern matching

The pattern-taking overloads are backed by a hand-written glob matcher rather than a delegation to the platform shell or a regex engine. That keeps wildcard semantics identical across every platform CNA builds for, instead of inheriting whatever the host's directory-enumeration API happens to do.

StorageDeviceNotConnectedException

StorageDeviceNotConnectedException is thrown when a StorageDevice that was previously valid is no longer accessible — for example, if a memory unit was ejected on Xbox 360. In CNA, because storage always maps to the local filesystem, this exception is raised only in exceptional circumstances such as the backing directory being removed by an external process after the device was opened.

It derives from ExternalException and carries a human-readable message. Catch it wherever you call BeginOpenContainer or perform file operations if your application needs to handle a missing device gracefully.

try {
    auto result = device.BeginOpenContainer("SaveData", nullptr, nullptr);
    auto container = device.EndOpenContainer(result);
    // ... use container ...
    container->Dispose();
} catch (const StorageDeviceNotConnectedException& ex) {
    // Storage unavailable — prompt the user to reconnect or select another device
    ShowErrorDialog(ex.what());
}

IAsyncResult pattern

XNA 4.0 used the standard .NET IAsyncResult Begin/End async pattern so that storage operations could show system UI (device selector, container namer) without blocking the game loop. The pattern works as follows:

  1. Call the Begin* method, supplying an optional AsyncCallback and state object. It returns an IAsyncResult.
  2. In the callback (or by polling IAsyncResult::IsCompleted), call the matching End* method to retrieve the result.

In CNA, all storage operations complete synchronously: the Begin* call performs the operation immediately and the returned IAsyncResult has IsCompleted == true before the callback is even invoked. The pattern is preserved verbatim so that XNA save-game code ports without changes, and synchronous completion is the correct semantics for a desktop filesystem — there is no system UI to wait on and no removable media to negotiate.

Code examples

Saving a file

// Obtain a StorageDevice
StorageDevice* device = nullptr;
StorageDevice::BeginShowSelector(
    [](IAsyncResult* result) {
        device = StorageDevice::EndShowSelector(result);
    }, nullptr);

// Open a container named after your save slot
auto openResult = device->BeginOpenContainer("Slot1", nullptr, nullptr);
auto container  = device->EndOpenContainer(openResult);

// Write a save file
if (!container->DirectoryExists("profile"))
    container->CreateDirectory("profile");

auto stream = container->OpenFile("profile/save.dat", FileMode::Create);
stream->Write(saveData.data(), 0, static_cast<int>(saveData.size()));
stream->Close();

container->Dispose();

Loading a file

auto openResult = device->BeginOpenContainer("Slot1", nullptr, nullptr);
auto container  = device->EndOpenContainer(openResult);

auto stream = container->OpenFile("profile/save.dat", FileMode::Open);

std::vector<uint8_t> buffer(stream->Length());
stream->Read(buffer.data(), 0, static_cast<int>(buffer.size()));
stream->Close();

container->Dispose();

// Deserialise from buffer ...
LoadGameState(buffer);

Checking file existence before loading

auto openResult = device->BeginOpenContainer("Slot1", nullptr, nullptr);
auto container  = device->EndOpenContainer(openResult);

if (container->FileExists("profile/save.dat")) {
    auto stream = container->OpenFile("profile/save.dat", FileMode::Open);
    // ... read and deserialise ...
    stream->Close();
} else {
    // No save file found — start a new game
    StartNewGame();
}

container->Dispose();

Always call container->Dispose() when you are finished with a StorageContainer. On CNA this releases the directory handle; on Xbox 360 it was required to flush and unlock the device. Keeping containers open longer than necessary is discouraged.