# Saves

One blob per player per game, a version the game increments, how conflicts resolve, and how to keep an old save loading.

The SDK stores one save per player per game: a JSON value up to 64 kB. It writes `localStorage` first and
pushes to the host after, so a save never waits on the network. The reference is [save](/docs/sdk/save/);
this page is about how to design yours.

## One blob

Keep everything in one object: progress, settings, unlocks. Several keys were a habit of portal data
modules; the SDK has one slot and resolves conflicts on it as a whole. Put a schema field inside the blob
(`schema: 3`) and fill defaults for every field an older blob lacks when you load. That field has nothing to
do with the SDK's version.

<CodePanel title="Load with defaults">
  <Fragment slot="javascript">
    ```js
    const defaults = () => ({ schema: 3, level: 1, stars: [], sound: true });

    async function loadSave() {
      const read = await sdk.save.get();
      version = read.version;
      return { ...defaults(), ...(read.data ?? {}) };
    }
    ```

  </Fragment>
  <Fragment slot="typescript">
    ```ts
    interface Save { schema: 3; level: number; stars: number[]; sound: boolean }
    const defaults = (): Save => ({ schema: 3, level: 1, stars: [], sound: true });

    async function loadSave(): Promise<Save> {
      const read = await sdk.save.get<Partial<Save>>();
      version = read.version;
      return { ...defaults(), ...(read.data ?? {}) };
    }
    ```

  </Fragment>
</CodePanel>

## The version

The SDK's `version` is a counter the game increments on every write: `set(data, { version: last + 1 })`
where `last` is the version of the latest `get()` or `saved` result. Keep the writes in order: one at a
time, each with the version the previous one returned. A debounce (write at most every second) keeps the
count low and the API quiet.

## Conflicts

Two devices, one player. The device with the higher version wins. When the server is ahead, `set()` returns
`conflict` with the server copy: take it, merge it if your game can (keep the higher star count per level),
and write again with `server.version + 1`. When a newer copy arrives on its own, `replaced` fires: reload
your state from it. Equal versions with different content: the server wins, because another device has
seen it.

<CodePanel title="Merge on conflict">
  <Fragment slot="javascript">
    ```js
    function merge(mine, theirs) {
      return {
        ...theirs,
        level: Math.max(mine.level, theirs.level),
        stars: mine.stars.map((s, i) => Math.max(s, theirs.stars[i] ?? 0)),
      };
    }

    const result = await sdk.save.set(state, { version: version + 1 });
    if (result.status === 'conflict') {
      state = merge(state, result.server.data);
      version = result.server.version;
      await sdk.save.set(state, { version: version + 1 });
    }
    ```

  </Fragment>
  <Fragment slot="typescript">
    ```ts
    function merge(mine: Save, theirs: Save): Save {
      return {
        ...theirs,
        level: Math.max(mine.level, theirs.level),
        stars: mine.stars.map((s, i) => Math.max(s, theirs.stars[i] ?? 0)),
      };
    }

    const result = await sdk.save.set(state, { version: version + 1 });
    if (result.status === 'conflict') {
      state = merge(state, result.server.data as Save);
      version = result.server.version;
      await sdk.save.set(state, { version: version + 1 });
    }
    ```

  </Fragment>
</CodePanel>

## Migrating an old save

A game that moves to the SDK has players with a save under the old key: the portal's data module, or
`localStorage`. Read it when the SDK has nothing, write it through the SDK, and leave the old key where it
is: a rollback to the previous build must still find it.

<CodePanel title="Read the old key once">
  <Fragment slot="javascript">
    ```js
    async function loadSave() {
      const read = await sdk.save.get();
      version = read.version;
      if (read.data !== null) return { ...defaults(), ...read.data };
      const legacy = readLegacySave();          // the old key, exactly as the old code read it
      if (legacy === null) return defaults();
      await sdk.save.set(legacy, { version: version + 1 });
      version += 1;
      return { ...defaults(), ...legacy };
    }

    function readLegacySave() {
      try {
        const data = window.CrazyGames?.SDK?.data;
        const raw = data?.getItem('gloop-save') ?? localStorage.getItem('gloop-save');
        return raw ? JSON.parse(raw) : null;
      } catch {
        return null;
      }
    }
    ```

  </Fragment>
  <Fragment slot="typescript">
    ```ts
    async function loadSave(): Promise<Save> {
      const read = await sdk.save.get<Partial<Save>>();
      version = read.version;
      if (read.data !== null) return { ...defaults(), ...read.data };
      const legacy = readLegacySave();          // the old key, exactly as the old code read it
      if (legacy === null) return defaults();
      await sdk.save.set(legacy, { version: version + 1 });
      version += 1;
      return { ...defaults(), ...legacy };
    }

    function readLegacySave(): Partial<Save> | null {
      try {
        const data = (window as { CrazyGames?: { SDK?: { data?: { getItem(k: string): string | null } } } })
          .CrazyGames?.SDK?.data;
        const raw = data?.getItem('gloop-save') ?? localStorage.getItem('gloop-save');
        return raw ? (JSON.parse(raw) as Partial<Save>) : null;
      } catch {
        return null;
      }
    }
    ```

  </Fragment>
</CodePanel>

## What the hub shows

Add a `_summary` object at the top level of the blob and the game's page on filudi.com draws your progress
without knowing your game: `line` ("World 3 of 5", under 60 characters), `progress` (0 to 1), `streak` (days,
for a daily game, counted by you) and `share` (today's result as a text grid, under 400 characters, the one
place emoji are welcome). Update it on every write.

## Per host

| Host       | Where the push goes                                                                              | `features.saves`    |
| ---------- | ------------------------------------------------------------------------------------------------ | ------------------- |
| filudi.com | The API, `PUT /v1/sdk/saves/:gameId` with `If-Match`                                             | `remote`            |
| CrazyGames | The data module, one key `filudi:save` holding `{ v, d }`; `localStorage` when the module is off | `remote` or `local` |
| Playgama   | `bridge.storage`, same key and shape                                                             | `remote`            |
| Poki, mock | `localStorage` only                                                                              | `local`             |
