# Custom storage

A **Storage** implementation reads and writes serialized save text by key. Visual Script includes player-preference and persistent-file implementations; a custom subclass adds another backend to [Save settings](../../../manual/save-load/settings.md).

## The interface

Derive from `StorageBase` and implement reading, writing, existence, and deletion by key. This outline omits the backend operations:

```cs
[Title("My Cloud Storage")]
[Serializable]
public class StorageMyCloud : StorageBase
{
    public override string Get(string key) { /* … */ }
    public override void Set(string key, string data) { /* … */ }
    public override bool Exists(string key) { /* … */ }
    public override void Delete(string key) { /* … */ }
}
```

`Get` returns the stored text or `null` when no valid entry exists. Keys are opaque strings produced by the save system, so treat them as identifiers rather than parsing their contents.

## When to write one

Use custom storage when the save destination has requirements the built-in implementations don't cover:

| Reason | Use when |
| :--- | :--- |
| **Cloud saves** | Steam, console platforms, a service of your own. |
| **A specific directory** | Alongside the executable, or in a location a launcher expects. |
| **Platform requirements** | Consoles typically mandate a platform save API. |
| **Multiple destinations** | Write locally *and* to the cloud, read from whichever is newer. |

## Handle storage failures

A file backend can fail when the disk is full, the path is read-only, or a file is locked. A network backend also needs to handle disconnections, timeouts, rate limits, and expired authentication.

!!! warning "Never let a storage failure throw into the save system"
    An exception escaping `Set` aborts the save. Handle failure inside the storage and report it through the project's own status path.

    A failed `Get` should return `null` so the caller treats the entry as missing or invalid.

For a networked backend, keep a local copy and report upload status separately:

1. Write locally first, so the game retains a copy if the upload fails.
2. Attempt the remote write.
3. On failure, log it and queue a retry.
4. Report status through your own event so the UI can show *saving…* or *offline*.

## Storage calls are synchronous

Every `StorageBase` method completes synchronously on the calling thread. A blocking network request therefore freezes the game during save or load.

For cloud saves, synchronize remote data into a local cache before calling `Load`. Let `Set` update that cache immediately, then queue the remote upload through a separate asynchronous service.

## Testing

Test the backend's persistence and failure paths:

- First save on a fresh install, where nothing exists.
- Save, quit the process entirely, relaunch, and load.
- Delete, then check that `Exists` reports `false`.
- Corrupt the stored data by hand and confirm loading rejects it without preventing launch.
- For networked synchronization, test offline, mid-write disconnection, and slow connections.

A truncated file simulates an interrupted write. The game must still start when the storage contains one.

!!! tip "Write atomically"
    Where the backend allows it, write to a temporary location and move it into place on success. A move is atomic on most filesystems, so a crash mid-write leaves the previous save intact rather than a half-written one.

## Where to go next

- **[Custom serializers](serializers.md)** — control the format written to storage.
- **[Custom encryption](encryption.md)** — transform the text before storage.
- **[Save settings](../../../manual/save-load/settings.md)** — select a storage implementation.
