Save System

In short

Saves are JsonUtility JSON in Application.persistentDataPath. Every write goes to a .tmp file that is swapped in with File.Replace, keeping the old file as .bak; an unreadable file falls back to its .bak. save.json holds meta progression; run.json is the checkpoint of the run in progress.

How it works

The files

FileHoldsWritten by
save.jsonSaveData: points, unlocks, maps, XP, settings, cosmetics, leaderboardSaveSystem.Save
run.jsonRunSaveData: the solo run checkpointSaveSystem.SaveRun
fit-overrides.jsonFit Mode values saved from a buildFitOverrides.SaveToPersistent (Fit Mode)
swings.csvOne line per pitch you see, for tuning the batting (not a save: never read back by the game)SwingLog (Swing Log)

Crash-safe writes

  1. Write the JSON to path.tmp.
  2. If path exists, File.Replace(tmp, path, path.bak): one step, the old file becomes the backup. Otherwise File.Move(tmp, path).
  3. Reading tries path, then path.bak (logging “was unreadable; restored from backup”).
  4. A failed write only logs an error; the old file is untouched.

SaveSystem.Delete() removes save.json, its .bak and .tmp, and the run files too. FitOverrides writes the same way but keeps no .bak.

SaveData (save.json)

FieldMeaning
version1
playerName”You” by default (used on the leaderboard and in the co-op lobby)
metaPoints, bestScore, runsPlayed, runsWonMeta totals (Meta Progression)
selectedBatterId”rookie” by default
unlockedIdsIds of bought batters, shop items and cosmetics
leaderboardLeaderboardEntry list (board, playerName, score, batterId, date); 20 kept per board
settingsGameSettings (below)
cosmeticsCosmeticLoadout: hatId, glassesId, shoesId, batId ("" = nothing)
mapsMapProgress per park: parkId, beaten (0 none, 1 Normal, 2 Medium, 3 Hard), best[] per difficulty
accountXpAccount XP (#37). Only XP is stored; levels are computed (XP and Levels)
batterProgressBatterProgress list: batterId, xp (#37)
callSignIdThe worn call sign (Call Signs)

GameSettings

FieldDefaultSetting label
showTimingRingfalseTIMING RING
skipCutscenesfalse (cutscenes play)CUTSCENES (shows ON when false). Also the dev menu’s CUTSCENES tab (Cutscenes).
noReplaysfalse (replays play)REPLAYS (shows ON when false) (Hit Show)
musicVolume0.6MUSIC
sfxVolume1SOUND
aimSensitivity1 (0.5 to 2.5, steps of 0.05)AIM SPEED
showFpsfalseFPS COUNTER
displayMode0 (as launched; 1 fullscreen, 2 windowed)FULLSCREEN / WINDOWED
resolutionWidth, resolutionHeight0 (the screen’s own)resolution
vSynctrueV-SYNC
fpsLimit0 (unlimited; only with V-Sync off)FPS CAP

A field missing from an old save just gets its default, so new settings need no migration (Add a Setting).

RunSaveData (run.json)

Fields: version (current 2), seed, isDaily, dailyDate, batterId, parkId, difficulty, currentIndex, inShop, coins, totalScore, homeRuns, longestFt, inningScores, upgradeIds.

  • Checkpoints: RunManager.ShowMap (between innings) and RunManager.PrepareShop (on entering the shop; called by OpenShop, and before the BOSS DOWN cutscene so quitting mid-scene resumes in the shop). Nothing is saved mid-inning; quitting mid-inning resumes before it with the same pitches (Seeds and Determinism).
  • Resume: RunManager.ContinueRun rebuilds the RunState from the seed, restores totals, marks earlier innings cleared with their scores, replaces the starting kit with upgradeIds, then opens the shop (inShop) or the map.
  • Dropped when (CanResume false): version is not 2, the batter or park no longer exists, the difficulty is out of range, currentIndex is past the last inning, or a daily from another date.
  • Deleted when the run ends (EndRun) or by SaveSystem.Delete.
  • Co-op runs are never checkpointed (CanSaveAndQuit is false).

When save.json is written

  • Every MetaProgression change that calls Persist(): unlocks, equip / unequip, call sign, map records, every dev-menu action.
  • Run end (RunManager.EndRun, CoopRun.EndRun), once for points, XP and leaderboard. This happens before the MAP BEATEN / GAME OVER cutscene, so quitting mid-cutscene loses nothing.
  • Settings: on toggles (including CUTSCENES and REPLAYS), and when a slider is released. The dev menu’s CUTSCENES: ON / OFF button saves at once too.
  • GameRoot.OnApplicationQuit.

Numbers

WhatValueWhere to tune
SaveData.version1SaveData.cs
RunSaveData.CurrentVersion2 (a run is 9 innings at one park, with a difficulty)RunSaveData.cs
Leaderboard entries kept per board20LocalLeaderboardService.MaxEntriesPerBoard
Board namesall-time, daily-yyyy-MM-ddBoards

In the code

FileWhat it does
Scripts/Progression/SaveSystem.csLoad, Save, Delete, HasRun, LoadRun, SaveRun, DeleteRun, Read, Write.
Scripts/Progression/SaveData.csSaveData, BatterProgress, MapProgress, CosmeticLoadout, GameSettings, LeaderboardEntry.
Scripts/Progression/RunSaveData.csThe checkpoint shape.
Scripts/Run/RunManager.csCheckpoint, PrepareShop, SavedRun, CanResume, ContinueRun.
Editor/ContentExporter.csTools > Home Run Derby > Delete Save File.

Gotchas

No migration code exists

SaveData’s comment says to bump version and migrate in SaveSystem if the shape changes, but SaveSystem has no migration step. Today, adding fields with defaults is the only safe change. Renaming or retyping a field silently drops that data.

  • JsonUtility can’t serialize Dictionary; use lists.
  • Changing the selected batter (MetaProgression.SelectedBatter) does not save on its own; it is written with the next save or on quit.
  • Tests build MetaProgression with persist: false, so they never touch the real save.json.
  • In the Editor, persistentDataPath is shared by every Play session of the project; use Delete Save File to start over.
  • Where that is on Windows: %USERPROFILE%\AppData\LocalLow\DefaultCompany\RoguelikeHomeRunDerby\ (company and product names from ProjectSettings/ProjectSettings.asset; renaming either moves the saves and players lose progress).