Skip to content

mufanq/sts2-save-rebuild

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

2 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

sts2-save-rebuild

Python 3.10+ License: MIT Platform: Windows

Toolkit to reconstruct a loadable current_run.save / current_run_mp.save for Slay the Spire 2 from the game's completed-run history files, plus the Steam Cloud tricks needed to actually deploy the result.

This started as a rescue mission after a multiplayer death wiped an in-progress run. The game keeps a .run history entry for every completed run but deletes the live save on death. Turns out that history entry is rich enough — paired with a known-good save used as a schema template — to rebuild an approximate pre-death state the game will load.


⚠️ READ FIRST — before you touch anything

These are the traps that will cost you hours if you skip them. Every point here came from an actual failure mode I hit building this.

Steam Cloud is not your friend here

  1. The game's in-process cloud sync will wipe any local save that isn't already in Steam's cloud manifest. CloudSaveStore.SyncCloudToLocalInternal runs on every launch; the "not on remote → delete local" branch runs before the menu renders. A naive "drop rebuilt file in saves folder" approach fails silently.

  2. Steam Offline Mode alone does NOT stop this. Steam's in-process ISteamRemoteStorage APIs use the local cloud cache — they return the same answers online or offline. You need either (a) real app- level cloud disable or (b) to trick Steam's manifest. Both are documented in docs/steam-cloud-bypass.md.

  3. Disabling cloud in game properties requires a FULL Steam restart. Many people think they disabled cloud but Steam still reports app: True to the game because the setting wasn't reloaded. Fully quit Steam from the tray icon (not just close the window) and check Task Manager confirms steam.exe is gone before relaunching.

  4. The death-clear propagates to Steam Cloud. When you die, the game calls DeleteCurrentMultiplayerRun(), which deletes both local AND cloud copies in the same call. Cloud is not a backup against death. Only history/*.run files survive.

SSD + TRIM realities

  1. On a modern NVMe SSD with TRIM enabled (default), files deleted more than a couple of minutes ago are essentially unrecoverable from hardware. Samsung 990 PRO's garbage collection zeros TRIMed blocks aggressively. Don't waste time with Recuva/PhotoRec if the delete was hours ago — go straight to the history-rebuild path below.

Don't break your working save

  1. Always snapshot before running anything here. The toolkit does this automatically in recover_mp_run.py, but if you use the lower-level scripts manually, run scripts/snapshot.py first. The --apply flag on rebuild_run.py will overwrite files; back up.

  2. If game's Report Bug button appears, do NOT click it. Click Ignore / 无视. The bug report may include your rebuilt save, which the devs can correctly identify as hand-edited.

Scope

  1. Rebuilt runs have a fresh RNG state. Upcoming shops, card rewards, random events will differ from what the original run would have shown. Ascension, seed, deck, relics, HP, gold, map position are preserved — everything downstream of "next random roll" is a parallel universe. For byte-for-byte replay you'd need a headless STS2 simulator; that's out of scope here.

  2. Multiplayer clients can't recover anything. Only the host writes current_run_mp.save. If you were a joiner in the lost game, the save only existed on the host's machine.

  3. For ongoing protection, install Rewind. This toolkit is a post-mortem rescue tool. Rewind mod maintains its own checkpoint store that bypasses the death-clear flow entirely — much better for daily protection.


What you get back

Field Recovered?
Character, deck, relics, potions yes
HP, gold, max_hp yes (from last pre-death map-point stats)
Ascension, modifiers, game mode, seed yes
Map position, path, events seen yes (via map_point_history)
RNG state for upcoming shops / draws no — parallel universe
Shared relic pool progress no
Map drawings (doodles) no
Mid-combat / mid-event state no

The rebuilt run resumes at a map-point boundary (just before the death encounter by default). Luck-line fields are reset; you won't get the same shop offers or card draws as the original run, but the loadout, progression and position are preserved.

Layout

sts2_save/                  library
  paths.py                  locate saves / cloud mirror / vdf on disk
  rebuild.py                rebuild SerializableRun from RunHistory
  validate.py               schema match against a known-good template
  steam_vdf.py              inject entries into remotecache.vdf
  snapshot.py               cold backup helper
  cli/                      thin wrappers wired to `sts2-*` entry points
docs/                       technical write-ups
examples/                   scenario walkthroughs
.claude/skills/             Claude Code skill definition
pyproject.toml              console_scripts + build metadata

Requirements

  • Windows (Steam save paths are Windows-specific)
  • Python 3.10+
  • No Python dependencies for the core toolkit

Optional:

  • pyautogui + pygetwindow for the UI-launch test harness (not required for rebuild)

Install

git clone https://github.com/mufanq/sts2-save-rebuild
cd sts2-save-rebuild
pip install .

This installs five console commands:

Command Purpose
sts2-recover-mp end-to-end pipeline (recommended)
sts2-rebuild single save rebuild
sts2-validate schema check against template
sts2-snapshot cold backup
sts2-inject-vdf inject entries into Steam's cloud manifest

Quick start — recover tonight's MP death

# 1. Fully quit Steam (tray icon -> Exit — NOT just close the window)

# 2. Run the end-to-end pipeline
sts2-recover-mp --backup-dir "C:/my-sts2-backups"

# 3. Launch Steam in Offline Mode, start the game, open Multiplayer.
#    The 'Continue' button should appear and load your pre-death state.

What it does, in order:

  1. Cold-backs-up your whole saves tree to --backup-dir.
  2. Picks the most recent history/*.run as the source.
  3. Picks a MP schema template (live current_run_mp.save, the most recent *.VAL.corrupt variant, or the SP save as fallback).
  4. Calls rebuild with --snapshot-offset 1 (pre-death).
  5. Static-validates the rebuilt JSON shape against the template.
  6. Mirrors the rebuilt file into Steam's userdata/.../remote/ directory with matching timestamps.
  7. Injects entries into remotecache.vdf so Steam's cloud-vs-local sync reports the file as "in cloud" and skips the delete branch.

Why the Steam Cloud juggling?

The game has its own in-process cloud sync on startup (CloudSaveStore.SyncCloudToLocalInternal). If it sees a local file whose key isn't in Steam's local cloud manifest, it deletes the local file and its .backup on every launch. Just dropping a rebuilt file into the saves folder isn't enough — the next launch wipes it.

Workarounds the community has tried:

Approach Works?
App-level "Keep saves in Steam Cloud" unchecked + full Steam restart yes — cleanest if you remember to fully exit Steam first
Steam client "Go Offline" no for this game — the in-process sync still runs
Firewall block on Steam fragile; Steam may refuse to launch the game
Edit remotecache.vdf so Steam reports the file as present in cloud yes, and what this toolkit does. Must be combined with Offline Mode so Steam doesn't reconcile our injected entry with Valve's servers

See docs/steam-cloud-bypass.md for the full mechanism.

Manual usage (skip the one-shot pipeline)

# Rebuild only, write next to template
sts2-rebuild `
    --history  "$env:APPDATA\SlayTheSpire2\steam\<id>\modded\profile1\saves\history\1776390784.run" `
    --template "$env:APPDATA\SlayTheSpire2\steam\<id>\modded\profile1\saves\current_run_mp.save" `
    --snapshot-offset 1 `
    --output "./rebuilt.save"

# Validate shape matches template
sts2-validate `
    --template "$env:APPDATA\SlayTheSpire2\...\current_run_mp.save" `
    --rebuilt "./rebuilt.save"

# Inject into Steam's cloud manifest (Steam must be fully quit)
sts2-inject-vdf `
    --vdf "C:\Program Files (x86)\Steam\userdata\<steam3>\2868840\remotecache.vdf" `
    --file-local "$env:APPDATA\SlayTheSpire2\...\current_run_mp.save" `
    --relative-key "modded/profile1/saves/current_run_mp.save" `
    --with-backup

Not installing? You can run the modules directly from a clone:

python -m sts2_save.cli.rebuild --history ... --template ...

Safety

Nothing here is reversible-by-magic, but the toolkit tries hard:

  • sts2-recover-mp always cold-backs-up before touching anything.
  • sts2-inject-vdf writes a timestamped .bak_<ts> alongside the vdf.
  • sts2-rebuild writes to a fresh file by default; --apply in-place mode moves the original to .pre_rebuild_<ts>.

You can always drop the backup back on top of the live folder. If the game refuses a rebuilt save, it will rename it to *.MIG.corrupt or *.VAL.corrupt — both are valid JSON you can diff against the template to diagnose.

Important caveats

  • Will not work after TRIM on NVMe SSD. If your save was deleted hours ago, recovery from unlinked sectors is near-impossible on modern SSDs. This toolkit rebuilds from history/*.run files, which the game keeps intact.
  • Schema drift. The game's SerializableRun schema has migrated from v9 to v16 over patches. rebuild uses a template to sidestep migrations and pass strict JSON deserialization. If your template is from a much older build than current game, try a newer one.
  • Multiplayer-specific. Rebuilt MP saves pass CanonicalizeSave because player net_ids are preserved from the history. Your co-op partners need to rejoin a lobby you host — the save only carries the final party state.
  • This is a reverse-engineered workaround, not a supported path. Do not use it to grief friends in competitive play, and expect that future game updates can break the schema assumptions.

Using with Claude / Claude Code

A skill definition is included at .claude/skills/sts2-save-rebuild.md. If you use Claude Code, copy that file into your project's .claude/skills/ directory (or symlink the whole repo in) and the assistant will know when and how to invoke the toolkit based on user requests like "I lost my run" or "how do I recover a MP save".

The skill file encodes the full decision tree: single-player vs multiplayer vs client, SSD-TRIM considerations, Steam Cloud preconditions (Steam must be fully quit), and post-deployment instructions (Offline Mode launch). It also lists the exact log patterns and rename suffixes to look for when a rebuilt save fails to load, mapped to their root causes.

Licensing and attribution

MIT for this repository's own code.

Slay the Spire 2 © Mega Crit Games. This toolkit does not redistribute any game assets, game code, or decompilation output. It only reads player-owned save files that the game produces and writes replacement save files conforming to the same JSON schema. If you don't own the game, don't use this toolkit.

Links

About

Rebuild a loadable current_run[_mp].save for Slay the Spire 2 from history files. Handles Steam Cloud bypass.

Topics

Resources

License

Stars

Watchers

Forks

Packages

 
 
 

Contributors

Languages