本文へ移動
cccskills
無料GitHub で公開

game-save-and-persistence

Use when adding or changing save games, settings or progression storage - a versioned save format with a tested migration chain, atomic writes with backup and checksum, per-engine storage locations, safe serializers, and the round-trip/migration/corruption tests.

インストール方法を見る

含まれるファイル(1)

  • SKILL.md5.7 KB

SKILL.md(原文)

インストールする前に、エージェントに与えられる指示の中身を確認できます。

Game Save and Persistence

Overview

A save file outlives the code that wrote it. Players carry saves across updates, crashes happen mid-write, and a lost 40-hour save is the bug players remember. Every save has a version, every older version has a tested migration, and a write never destroys the last good save.

Core principle: Versioned, migrated, written atomically, checked on load — and never deserialized with a format that can execute code.

The format

{
  "version": 3,
  "savedAtUtc": "2026-10-08T12:00:00Z",
  "player": { "levelId": "forest_02", "position": [12.5, 0, -4.0], "hp": 72 },
  "inventory": [{ "itemId": "sword_iron", "count": 1 }],
  "flags": ["met_blacksmith", "boss_forest_defeated"]
}
  • A plain DTO, separate from engine objects: no GameObject, Node or AActor references, no array indices into data tables — stable IDs only (data-driven-gameplay-design).
  • Settings (volume, bindings, accessibility) in a separate file from progress, so resetting one never touches the other.
  • JSON is readable and diff-able; a compact binary format (MessagePack, protobuf, Godot's store_var without objects) is fine when size matters — the version field stays first.

Migrations

type Migration = (save: any) => any;
const migrations: Record<number, Migration> = {
  1: (s) => ({ ...s, version: 2, inventory: s.items.map((id: string) => ({ itemId: id, count: 1 })) }),
  2: (s) => ({ ...s, version: 3, flags: Object.keys(s.questState ?? {}).filter((k) => s.questState[k] === "done") }),
};

export function upgrade(raw: any): SaveV3 {
  if (typeof raw?.version !== "number") throw new SaveError("unversioned");
  if (raw.version > CURRENT) throw new SaveError("from a newer game version");  // refuse, never guess
  let s = raw;
  while (s.version < CURRENT) s = migrations[s.version](s);
  return SaveV3Schema.parse(s);
}
  • One migration per version step, never edited after release; each tested with a fixture file captured from that version (tests/fixtures/saves/v1.json, v2.json).
  • A save from a newer version is refused with a clear message, not loaded and overwritten.
  • Changing a saved field's meaning without bumping the version is the bug this exists to prevent.

Writing safely

  1. Serialize to bytes in memory.
  2. Write to save.json.tmp, flush to disk.
  3. Keep the previous file as save.json.bak.
  4. Rename the temp file over save.json (atomic on the same volume).
  5. Store a checksum (CRC32 or SHA-256 of the payload) in a header or a sidecar.

On load: verify the checksum and parse; on failure fall back to .bak, tell the player, and never crash or silently start a new game over the old one.

Serialize on the main thread if the engine requires it, but do file I/O off it (or between frames at a checkpoint) — a save must not hitch gameplay. Autosave at checkpoints, not every frame.

Where saves live

EngineLocation / API
UnityApplication.persistentDataPath + System.IO; PlayerPrefs only for tiny settings
Godotuser:// with FileAccess; ConfigFile for settings
UnrealUSaveGame subclass + UGameplayStatics::AsyncSaveGameToSlot / LoadGameFromSlot; keep a version UPROPERTY in it
WebIndexedDB for saves (async, larger; e.g. idb-keyval), localStorage only for small settings — it is synchronous, ~5 MB and can throw in private mode or when full
Bevy / Rustserde to the platform data dir (directories crate)
pygame / MonoGamethe OS user data directory, never the install folder

Consoles and some stores impose their own save APIs and size limits — follow the platform layer the repository already has.

Serializers that execute code — never for save data

  • .NET BinaryFormatter: obsolete and insecure; do not use it in Unity, MonoGame or Godot C#.
  • Godot: bytes_to_var_with_objects / str_to_var on untrusted data, and loading a user-supplied .tres/.res, can instantiate scripts — use JSON, ConfigFile, or FileAccess.get_var(false) (objects disallowed).
  • Python pickle for pygame saves: a shared or downloaded save becomes code execution — use JSON.
  • Treat every save file as untrusted input: players edit them, and mods share them.

Tests

  • Round trip: state → save → load → equal state.
  • Migration: each fixture vN.json → upgrade → matches the expected current-version object.
  • Corruption: truncated file, bad checksum, garbage bytes → falls back to .bak and reports; no exception escapes.
  • Newer version: refused, original file untouched.
  • Atomicity: the write path is tested with a fake file system that fails between steps — the previous save is still loadable.

Common Mistakes

  • Serializing engine objects or private implementation classes directly, so every refactor breaks old saves.
  • No version field; "we'll add one when we need it".
  • Writing in place over the only copy.
  • Saving inventory by data-table index; reordering the table corrupts every save.
  • Blocking the main thread with file I/O mid-gameplay.

Red Flags

  • A change to a saved type with no version bump and no migration test.
  • BinaryFormatter, pickle, or object-enabled Godot variant decoding in a save path.
  • A load path with no fallback when parsing fails.

レビュー

まだレビューはありません。使ってみた感想をお寄せください。

同じリポジトリのスキル

概要と使いどころ

Use when writing acceptance criteria for a task - express each as an observable Given/When/Then that QA can execute, including negative cases

日本語の概要は準備中です。原文の説明を表示しています。

makifbaysal/tasktrooper1122026年10月11日 更新

Use when the diff adds or changes an endpoint, resolver, RPC, job or query that takes an object id, a role check, a request binding or a tenant filter - BOLA/IDOR, function-level authorization, mass assignment and tenant scoping

日本語の概要は準備中です。原文の説明を表示しています。

makifbaysal/tasktrooper1122026年10月11日 更新

Use on every UI change - semantic HTML, labels for controls, keyboard-navigable dialogs/menus, visible focus, and never color as the only signal

日本語の概要は準備中です。原文の説明を表示しています。

makifbaysal/tasktrooper1122026年10月11日 更新

Use when a task changes any screen, form, dialog, menu or control - Lighthouse/axe scan of the changed screens, a keyboard walk, and the thresholds that fail a task

日本語の概要は準備中です。原文の説明を表示しています。

makifbaysal/tasktrooper1122026年10月11日 更新

How to work a task returned with review, QA or UAT findings. Use when a task is in need_revision or PR review comments are in your context.

日本語の概要は準備中です。原文の説明を表示しています。

makifbaysal/tasktrooper1122026年10月11日 更新

Use when deciding whether a request needs an analiz task before implementation - the conditions that require the architect's analysis versus going straight to implementation

日本語の概要は準備中です。原文の説明を表示しています。

makifbaysal/tasktrooper1122026年10月11日 更新

makifbaysal のスキルをすべて見る

このスキルの問題を報告する