Skip to main content
summer-player-data is a skill from the Summer skill library: a guide your AI agent reads when the task matches, so it builds the good version on the first try. Save a player’s progress so it follows them to every match: authority load and commit, autosave, safe retries.

When your agent uses it

  • Saving player progress, coins, unlocks, levels or settings in a multiplayer Summer game so they persist between matches
  • A player’s progress resets every match, or saves fail with save_id_conflict, superseded or operation_in_progress
  • Showing a player’s saved progress in the menu before they join a match

Install

summer setup installs every skill for your agent. To add only this one:
Swap claude-code for your agent, such as codex or cursor. See Skills for every option.

The skill

Overview

Player data is what Summer remembers about one player in one game: coins, unlocks, level, settings. It belongs to the player, not to a match. A match World ends and its state is gone. The player’s data comes back in the next match, on any server and any device.
  • Only the authority writes it. Clients never write player data.
  • Two slots per player, written separately:
    • public: the authority reads and writes it. The player’s own client can read it too, for example to show coins in the menu.
    • secret: only the authority reads or writes it, for hidden values such as anti-cheat counters or hidden ratings. It never reaches a client.
  • Nobody else can read either slot.
  • Each slot is one JSON object of at most 64 KiB.
    • Allowed values: bool, int, finite float, String, Array and Dictionary with String keys, nested at most 16 deep.
    • Vector3, Color and null fail before anything is sent.
    • Whole numbers load back as int.
Where it works: Local Play’s store keeps the latest value plus five earlier revisions, with the same validation and save_id rules as hosted. Delete a persona’s folder to start that player fresh. Only hosted play proves durability. This skill extends the game from multiplayer-project and multiplayer-state: the authority’s _join Command handler, the private mine State group, and the wallets it keeps.

Steps

1. Decide what to save

Save only what must outlive the match, and save it as a plain document: Keep values that must change together in one slot: there is no transaction across the two slots.

2. Load when the player arrives

The authority starts loading on session_joined, before the client’s join Command arrives. Add to authority/main.gd:
In _ready(), next to the other world signals, before Summer.initialize (connect session_joined only once if summer-match-results already did):
If _on_session_joined already exists (summer-match-results adds one), put this body at its end instead of defining it twice.
loaded.secret_record.data holds the secret slot. A slot that was never written has exists == false and empty data.

3. Make join wait for the load

In _join, right after the seat check:
Start the player from their saved values instead of zero, in the block that creates their private group:
On the client, in client/main.gd, retry join while the answer is loading:
profile_unavailable means the load failed. Show “Couldn’t load your progress”, and do not let the player play on a blank profile that would overwrite their real one.

4. Mark changes, autosave, flush on leave

Wherever progress changes (here, after the wallet changes in _collect):
At the top of _on_session_left:
The saving code:
Why it is shaped this way:
  • Autosave, not only save-on-leave. A crash delivers no signal. With a 5 s autosave a crash loses at most 5 s.
  • One commit per player and slot at a time. A second concurrent commit fails with operation_in_progress.
  • A new save_id for each new save, the same save_id for a retry.
    • Resending a save_id with the same data returns the original revision (commit.replayed).
    • The same save_id with different data fails with player_data_save_id_conflict.
    • Build the id from the time the Session joined plus a counter. Session ids are not enough: Local Play reuses them on every run.
  • player_data_superseded is final. The player joined a newer World, and only their newest Session may write.
  • draining fires before the World shuts down. Commits are still accepted until the stop deadline, so flush everyone then.
  • Under Local Play a player who quits only disconnects. Their seat is held and session_left arrives when the World ends, so the autosave is what saves them.
Use commit_secret in the same way for the secret slot. Each slot has its own revisions and save_id space.

5. Show saved progress in the menu (optional)

The client reads its own public slot, even before joining a match:

6. Keep the publish checks passing

The source-graph check in multiplayer-publish (step 4b) flags every call ending in load( without a string literal inside, so Summer.authority.player_data.load(session) and Summer.client.player_data.load() report SUMMER_COMPOSITION_DYNAMIC_PATH_UNDECLARED. They load no file. Declare one dependency edge per file that makes such a call, in source-domains.json:

7. Test persistence: run Local Play twice

Run the same session twice. Local Play personas (player-1, player-2) keep their files between runs, so run 2 must start where run 1 ended.
From a terminal: <summer> --path . --summer-local-play 2 --summer-local-play-headless --summer-local-play-timeout 20 -- --bot={client}, twice (see multiplayer-testing for bot mode). Print markers such as print("PROFILE_LOADED coins=", ...) and print("SAVED ", save_id, " revision=", commit.revision) while testing, then check them. Files: ~/Library/Application Support/Godot/app_userdata/<project name>/summer_local_player_data/<game id>/<persona>/public.json on macOS (the project’s user:// folder elsewhere). Delete a persona’s folder to start over.

Checklist

  • Run 1: a player earns something, and the authority logs a successful save within the autosave interval.
  • Run 2: the authority loads the saved value at join; the client menu shows it.
  • A load failure refuses join with profile_unavailable; nothing is written over the old data.
  • No player_data_save_id_conflict in the logs, even on the second run.
  • The saved document stays well under 64 KiB at its largest. Test the maximum.

Common mistakes

See also

  • multiplayer-state — the wallet and private State group this skill saves
  • summer-world-saves — saving a whole persistent World instead of one player
  • multiplayer-testing — bots and repeated Local Play runs
  • summer-leaderboards — scores other players can see

From a shell

No Summer MCP tools in this session? Run each one as a command; when summer --version is missing or below 4.0, use npx -y summer-engine@latest in place of summer. Fields are flags in kebab-case (--scene-path res://main.tscn) or one --input '<json>', and summer ops describe <op> prints the exact input. The summer-cli skill has the rest.
  • multiplayer: Start here for Summer multiplayer: how authority, clients, Worlds and Sessions fit, and what comes next.
  • multiplayer-state: Authority-owned state in Summer multiplayer: shared and private State groups, Commands, Events.
  • multiplayer-testing: Test a Summer multiplayer game with Local Play: several players, bad network, bots, a passing run.
  • summer-world-saves: Keep a persistent Summer World between restarts: checkpoint saves, restore before join, the final save.
  • summer-leaderboards: Leaderboards in a Summer game: queue ratings from matches, and Game rankings your authority scores.
  • summer-store: Sell Summer items for Sparks in a multiplayer game: shop, checkout, owned items, authority entitlement checks.

Source: library/skills/summer-player-data/SKILL.md, rendered from summer-engine@4.3.0 on npm.