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

# Save load skill for AI agents

> Save and load game state: user:// paths, JSON progress vs ConfigFile settings, versioned saves, autosave. A Summer Engine skill for AI agents.

`save-load` is a skill from the [Summer skill library](/mcp/skills): a guide your AI agent reads when the task matches, so it builds the good version on the first try. Save and load game state: user:// paths, JSON progress vs ConfigFile settings, versioned saves, autosave.

| | |
| - | - |
| Status | Preview: not yet exercised in-engine by the Summer team |
| Domains | Scripting, Gameplay |
| Version | `1.0.0` |
| Engine version | `>=4.6` |
| Recommended set | No, installed by `--all` or by name |

## When your agent uses it

* Persisting progress, settings, high scores, checkpoints, or save slots
* Deciding between JSON and ConfigFile for a save or settings file
* Making saves survive game updates with a schema version and migration

## Install

`summer setup` installs every skill for your agent. To add only this one:

```bash theme={null}
npx -y summer-engine@latest skills install save-load --agent claude-code
```

Swap `claude-code` for your agent, such as `codex` or `cursor`. See [Skills](/mcp/skills#quick-start) for every option.

## The skill

Trigger phrases: "save progress", "settings menu should remember", "high scores",
"checkpoints", "save slots", "JSON or ConfigFile?", "saves broke after the update".

Save what the player would be angry to lose, and nothing else. Persist DATA
(numbers, ids, flags), never live nodes or scene references.

## Where saves go

Always `user://` - a writable per-user dir (`res://` is read-only in an export).

* `user://save_0.json` - progress
* `user://settings.cfg` - options

Never write gameplay saves to `res://`; it fails in the shipped game.

## JSON vs ConfigFile

| Use JSON when | Use ConfigFile when |
| - | - |
| Nested/structured game state (inventory, quests, world) | Flat key/value settings |
| One document you load whole | Sectioned options (audio, video, controls) |
| You want a schema + version field | You want human-editable ini-style |

Default to JSON for game progress, ConfigFile for settings.

## Save schema (version it from day one)

A top-level `version` field is the difference between "old saves still load after
an update" and "everyone loses their progress on patch day".

```gdscript theme={null}
const SAVE_PATH := "user://save_0.json"
const SAVE_VERSION := 1

func build_save() -> Dictionary:
	return {
		"version": SAVE_VERSION,
		"level": GameManager.current_level,
		"player": {
			"hp": GameManager.player_health,
			"gold": GameManager.player_gold,
			"pos": var_to_str(player.global_position),  # Vector serialized as string
		},
		"inventory": GameManager.inventory,   # array of item ids, not item nodes
		"flags": GameManager.story_flags,
	}
```

Serialize Vectors/Colors with `var_to_str()` and restore with `str_to_var()` -
JSON has no native Vector2/3.

## Write and read

```gdscript theme={null}
func save_game() -> void:
	var f := FileAccess.open(SAVE_PATH, FileAccess.WRITE)
	if f == null:
		push_error("save failed: %s" % FileAccess.get_open_error())
		return
	f.store_string(JSON.stringify(build_save(), "\t"))   # tab-indented, readable
	f.close()

func load_game() -> bool:
	if not FileAccess.file_exists(SAVE_PATH):
		return false
	var f := FileAccess.open(SAVE_PATH, FileAccess.READ)
	var data: Variant = JSON.parse_string(f.get_as_text())
	f.close()
	if typeof(data) != TYPE_DICTIONARY:
		return false                       # corrupt/partial file
	data = _migrate(data)                  # bring old versions up to current
	_apply_save(data)
	return true
```

## Migration (do not skip)

When the schema changes, bump `SAVE_VERSION` and upgrade old saves instead of
rejecting them.

```gdscript theme={null}
func _migrate(data: Dictionary) -> Dictionary:
	var v: int = data.get("version", 0)
	if v < 1:
		data["flags"] = data.get("flags", {})   # field added in v1
		data["version"] = 1
	return data
```

## Settings with ConfigFile

```gdscript theme={null}
func save_settings() -> void:
	var cfg := ConfigFile.new()
	cfg.set_value("audio", "master", master_volume)
	cfg.set_value("video", "fullscreen", is_fullscreen)
	cfg.save("user://settings.cfg")

func load_settings() -> void:
	var cfg := ConfigFile.new()
	if cfg.load("user://settings.cfg") != OK:
		return                              # first run, use defaults
	master_volume = cfg.get_value("audio", "master", 1.0)
```

Always pass a default to `get_value` so a missing key never crashes.

## Autosave hooks

* Save on meaningful checkpoints (level end, area transition, shop close), not
  every frame. Disk writes stutter.
* Save on quit via `_notification`:

```gdscript theme={null}
func _notification(what: int) -> void:
	if what == NOTIFICATION_WM_CLOSE_REQUEST:
		save_game()
		get_tree().quit()
```

Set `get_tree().set_auto_accept_quit(false)` so you control the quit.

## Traps

* Storing a node/`PackedScene`/callable in the save = unserializable or huge.
  Store ids and rebuild.
* Writing to `res://` - read-only in exports; silently fails or errors.
* No `version` field - the first schema change wipes every player's save.
* `JSON.parse_string` returns `null` on bad input; type-check before using it.
* Floats round-trip fine; ints may load back as floats from JSON - cast on read
  (`int(data["gold"])`).

## Applying it in Summer Engine

* The save code is a script, usually an autoload (`res://scripts/autoloads/save_manager.gd`):
  create it with `summer_write_file(path=..., content=..., create_only=true)`,
  then register it with `summer_project_setting(key="autoload/SaveManager", value="*res://scripts/autoloads/save_manager.gd")`
  (the leading `*` makes it a singleton). Check it with `summer_get_script_errors`.
* `user://` is the per-user data dir, outside the project, so do not expect
  `summer_read_file` (a project-file reader) to show a save file. Prove a round trip from inside the game
  instead: a `RunVerification` probe (see `playtesting-a-feature`) that calls
  `save_game()`, changes a value, calls `load_game()`, and `report()`s the
  restored value.

## 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.

| In this skill | Command |
| - | - |
| `summer_get_diagnostics` | `summer engine get-diagnostics` |
| `summer_get_script_errors` | `summer engine get-script-errors` |
| `summer_project_setting` | `summer engine project-setting` |
| `summer_read_file` | `summer engine read-file` |
| `summer_replace_text` | `summer engine replace-text` |
| `summer_write_file` | `summer engine write-file` |

## Related skills

* [`make-game`](/skills/make-game): Orchestrate 'make me a game': brainstorm, plan, scaffold, mechanics, art, audio, polish, verify, ship.
* [`gdscript-patterns`](/skills/gdscript-patterns): GDScript conventions: type hints, signals, exports, onready, lifecycle methods, node paths, naming.
* [`ui-basics`](/skills/ui-basics): Build UI: HUDs, menus, health bars, dialogue boxes, Control trees, anchors, containers and themes.

***

Source: [`library/skills/save-load/SKILL.md`](https://github.com/SummerEngine/summer/blob/main/library/skills/save-load/SKILL.md), rendered from `summer-engine@4.3.0` on npm.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.