acron.persist

Files a game keeps between sessions: JSON read defensively, written atomically, and brought forward through a schema-version chain.

Three mechanisms every save system re-invents, grown out of REGOLITH’s run file and ECHO’s prints (GAMEDEMO_MINING.md §3 is the mined rule set):

  • Defensive reads. read_json() answers the fallback for a file that is missing, unreadable or not JSON — a game must start whatever is on disk — and read_object() narrows that to a JSON object, since a file holding [1, 2] is valid JSON and once crashed a boot on .items(). A file that is on disk but will not parse is evidence, not garbage: it is moved aside as <name>.corrupt (quarantine()) so the loss can be looked at, and the read falls back.

  • Atomic writes. write_json() goes through a temporary file, an fsync and a rename, so a power cut between the write and the rename leaves the previous file rather than a zero-length one. It answers whether the write landed — a save that silently fails is a player’s evening lost, and the caller should say so once.

  • Versioned migration. migrate() walks a dictionary forward through {from_version: fn} steps to the version the build writes (v missing or malformed reads as version 1, the first shipped shape); a newer file than the build knows is handed back as it is, for the defensive reader to take what it recognises. The steps themselves are the game’s.

Paths are pathlib.Path; where the files live and what they are called is policy and stays with the game.

Global Functions

migrate(d: dict, version: int, upgrades: dict[int, Callable[[dict], dict]]) → dict

d brought forward to version through upgrades ({from_version: fn(dict) -> dict}), one step at a time. A step the table lacks ends the walk; a version past version is handed back untouched.

quarantine(path: Path) → None

Move a file that failed to parse aside, out of the read path.

read_json(path, fallback=None)

The JSON in path, or fallback if the file is missing, unreadable, or not JSON (in which case it is quarantined).

read_object(path) → dict | None

read_json(), narrowed to a JSON object: anything else — a list, a string, a number — reads as None.

remove(path) → bool

Remove a file. True if it is gone (or never was); False only when it is there and could not be removed — which the caller should say.

schema_version(d: dict) → int

The v a dictionary carries, or 1 when it has none or a malformed one (the first shipped shape had no version field).

write_json(path, data) → bool

Write data as JSON to path atomically. True if it landed; False when the directory could not be made, the file could not be written, or data is not JSON.