1 unstable release
| 0.2.0 | Apr 2, 2026 |
|---|
#954 in Games
Used in savetracker
12KB
288 lines
savetracker
Track your game saves while you play. Annotate them while you play, or come back to that later.
savetracker watches game save directories (local or remote), snapshots every change, diffs versions, and optionally uses an LLM to describe what changed in plain language. Supports multiple LLM providers: Ollama, OpenAI, Claude, and Gemini.
Features
- Watch save files over local filesystem, SSH, FTP, or HTTP
- Auto-detect file formats (JSON, YAML, TOML, XML, INI, binary)
- Configurable format definitions for game-specific decoding pipelines
- Incremental snapshots with binary patching (saves disk space)
- Decoded sidecars saved alongside raw files (e.g.
2.sav+2.sav.yaml) - Git backend for version storage with descriptions in commit messages
- Interactive TUI with version history, diffs, and note-taking
- Connection status for remote backends (connected/degraded/lost)
- Multi-provider LLM integration (Ollama, OpenAI, Claude, Gemini)
- Concurrent LLM requests with configurable limits
- Review tracking via git notes (tracks which models reviewed each version)
Install
From crates.io:
cargo install savetracker
From source:
cargo install --path savetracker
Crates
| Crate | Description |
|---|---|
savetracker |
CLI application and desktop library |
savetracker-core |
no_std core: Storage trait and binary patching |
Usage
Watch saves interactively
savetracker watch ./saves -i
Watch with AI descriptions
savetracker watch ./saves -i --live --model gemma3:4b
Watch with a different LLM provider
savetracker watch ./saves -i --live --model-provider claude --model claude-sonnet-4-20250514
Watch remote saves over SSH
savetracker watch ssh://user@host/path/to/saves -i --key-path ~/.ssh/id_rsa
Analyze existing snapshots
savetracker analyze --snapshot-dir .savetracker/snapshots
Review existing descriptions with a different model
savetracker analyze --snapshot-dir ./snapshots --review --model-provider openai --model gpt-4o-mini
Inspect a single save file
savetracker inspect path/to/save.sav
Compare descriptions side-by-side
savetracker compare --snapshot-dir ./snapshots
Use git backend for storage
savetracker watch ./saves -i --git --snapshot-dir ./save-repo
Supported protocols
| Protocol | Example |
|---|---|
| Local | ./saves or file:///path/to/saves |
| SSH | ssh://user@host:22/path |
| SFTP | sftp://user@host/path |
| FTP | ftp://host/path |
| HTTP | https://example.com/saves |
Commands
savetracker [OPTIONS] <COMMAND>
Commands:
watch Monitor a save directory for changes
analyze Analyze previously captured snapshots
inspect Decode and display a single save file
compare Compare existing descriptions with fresh analysis
Common options
--ollama-url <URL> Ollama API URL [default: http://localhost:11434]
--model <NAME> LLM model name (default varies by provider)
--model-provider <PROVIDER> LLM provider: ollama, openai, claude, gemini [default: ollama]
--model-provider-url <URL> API base URL for openai-compatible providers
--model-provider-key-env <VAR> Environment variable for API key
--snapshot-dir <PATH> Custom snapshot storage directory
--debounce-ms <MS> Debounce time in milliseconds [default: 2000]
--max-snapshots <N> Maximum snapshots to keep [default: 50]
--format <NAME> Force a specific save format by name
--git Use git backend for snapshot storage
-j, --concurrent <N> Max concurrent LLM requests [default: 1]
-l, --limit <N> Max total items to analyze before exiting
watch
savetracker watch <URL> [OPTIONS]
Options:
-i, --interactive Interactive TUI mode
--live Analyze changes with LLM in real-time
--max-versions <N> Max versions to display in TUI
--idle-timeout <SECS> Auto-jump to latest after idle [default: 15]
--poll-interval <SECS> Remote polling interval [default: 5]
--loss-timeout <SECS> Connection loss threshold [default: 30]
--key-path <PATH> SSH key file
--password <STRING> Remote authentication password
analyze
savetracker analyze [OPTIONS]
Options:
--file <NAME> Filter to a specific file
--review Re-analyze versions that already have descriptions
--since <DURATION> Time filter: "forever", "2h", "30m", "1d" [default: forever]
Dynamic parameters
Format definitions can declare parameters (e.g., Steam ID for encryption key
derivation). Pass them with --d::
savetracker watch ./saves -i --d:steam-id=76561198012345678
Parameters are resolved in order: path extraction (automatic), --d: flags
(manual), error if required and missing.
Save format definitions
Format definitions are TOML files that describe how to decode game-specific save
files. They live in etc/formats/ (built-in, shipped with the binary) and
~/.config/savetracker/formats/ (user-added).
When a save file is detected, savetracker scores each definition by extension match, path pattern match, and magic byte prefix match. The highest-scoring definition's pipeline runs to decode the file. If nothing matches, generic auto-detection kicks in.
Schema
[format]
name = "my_game"
display_name = "My Game"
[detect]
extensions = [".sav"]
magic_bytes = "deadbeef" # optional, hex prefix
[detect.platform.windows]
path_patterns = ["**/My Game/Saves/**/*.sav"]
[detect.platform.linux]
path_patterns = ["**/compatdata/*/pfx/**/My Game/**/*.sav"]
# Ordered decode pipeline — runs top to bottom
[[pipeline]]
type = "aes_ecb_decrypt"
key_hex = "00112233..."
key_transform = "xor_prefix"
key_transform_param = "steam_id"
key_transform_bytes = 8
[[pipeline]]
type = "pkcs7_unpad"
[[pipeline]]
type = "zlib_decompress"
# What the decoded bytes are (auto-detected if omitted)
[output]
format = "yaml"
# Parameters — extracted from path or provided via --d: flags
[params.steam_id]
flag = "steam-id"
description = "Steam ID (64-bit decimal)"
required = true
extract_from_path = "**/Saves/{}/Profiles/**"
Pipeline layers
| Layer | Fields | Description |
|---|---|---|
gzip_decompress |
Gzip decompression | |
zlib_decompress |
Zlib decompression | |
zstd_decompress |
Zstandard decompression | |
lz4_decompress |
LZ4 decompression | |
aes_ecb_decrypt |
key_hex, key_transform?, key_transform_param?, key_transform_bytes? |
AES-256 ECB |
aes_cbc_decrypt |
key_hex, iv_hex, same transform fields |
AES-256 CBC |
pkcs7_unpad |
Remove PKCS#7 padding | |
xor |
key_hex |
XOR with repeating key |
skip_bytes |
count |
Strip leading bytes |
take_bytes |
offset, length |
Extract byte range |
Key transforms
The key_transform field names an operation applied to the key before
decryption:
xor_prefix— XOR the firstkey_transform_bytesbytes of the key with a parameter value interpreted as a little-endian u64. The parameter is named bykey_transform_param.
Path extraction
Parameters can be auto-extracted from the file path using extract_from_path.
The pattern uses ** for any path segments and {} to capture the value:
**/SaveGames/{}/Profiles/**
Given path .../SaveGames/76561198012345678/Profiles/client/1.sav, this
captures 76561198012345678 as the parameter value.
Detection scoring
| Match type | Score |
|---|---|
| File extension | 1 |
| Path pattern (glob) | 5 |
| Magic bytes (hex prefix) | 10 |
Contributing format definitions
Add a .toml file to etc/formats/ following the schema above and open a PR.
Test your definition with cargo test — the format module tests parse all
embedded definitions.
TUI keybindings
| Key | Action |
|---|---|
| Tab / Shift+Tab | Navigate between versions |
| Alt+D | Toggle detail diff overlay |
| Alt+E | Open notes in external editor ($EDITOR) |
| PageUp / PageDown | Scroll diff |
| Ctrl+C | Quit |
License
BSD-2-Clause
Dependencies
~1–2MB
~35K SLoC