fs-sav
Foxhole save file parser - extracts stockpile data from .sav files.
Features
- Parse Foxhole save files (
.sav) using the uesave library - Extract stockpile information: items, locations, types, quantities
- Find the game's save files (Windows, WSL, Steam Proton and Flatpak on Linux)
- Watch files for changes with diff modes (NDJSON output)
- Filter by hex, stockpile type, public/reserve status
- Output compatible with foxhole-stockpiles
- Python bindings via PyO3
Installation
Python (from PyPI)
pip install fs-sav
Rust CLI (from source)
cargo install --path .
Python (from source)
pip install maturin
maturin develop --features python
CLI Usage
Parse command
# Parse a save file
fs-sav parse path/to/War.sav
# Parse with compact output (no pretty printing)
fs-sav parse path/to/War.sav --compact
# Read from stdin
cat War.sav | fs-sav parse
Filter options
# Only public stockpiles (non-reserve)
fs-sav parse War.sav --public
# Only reserve stockpiles
fs-sav parse War.sav --reserves
# Filter by hex
fs-sav parse War.sav --hex TerminusHex
# Filter by stockpile type
fs-sav parse War.sav --type Seaport
# Only stockpiles with items
fs-sav parse War.sav --with-items
# Combine filters
fs-sav parse War.sav --hex TerminusHex --public --with-items
Watch command
# Watch for changes (outputs all stockpiles on each change)
fs-sav watch path/to/War.sav
# Watch with custom poll interval (seconds)
fs-sav watch path/to/War.sav --poll 2.0
# Only output stockpiles that changed (any field)
fs-sav watch path/to/War.sav --diff
# Only output stockpiles where items changed
fs-sav watch path/to/War.sav --diff-items
# Watch with filters
fs-sav watch path/to/War.sav --hex TerminusHex --public
Saves command
# List the map save files found on this machine (JSON, newest first)
fs-sav saves
[
{
"path": "C:\\Users\\me\\AppData\\Local\\Foxhole\\Saved\\SaveGames\\76561197960293553_MapData.sav",
"steam_id": "76561197960293553",
"modified": "2026-08-01T16:25:36Z",
"size_bytes": 143466
}
]
The game keeps one <SteamID64>_MapData.sav per Steam account. They are
looked for in:
- Windows:
%LOCALAPPDATA%\Foxhole\Saved\SaveGames - WSL: the same folder of every Windows user, under
/mnt/c/Users - Linux: the Proton prefix (
steamapps/compatdata/505460/...) of every Steam library, for native Steam (~/.steam/steam,~/.steam/root,~/.steam/debian-installation,~/.local/share/Steam) and Flatpak Steam (~/.var/app/com.valvesoftware.Steam/.local/share/Steam), including the extra libraries listed inlibraryfolders.vdf
A save elsewhere is read by passing its path to parse or watch.
Version
fs-sav version
Python Usage
import fs_sav
# Parse a save file
stockpiles = fs_sav.parse_save("path/to/War.sav")
print(f"Found {len(stockpiles)} stockpiles")
for stockpile in stockpiles:
print(f" {stockpile['name']} ({stockpile['type']}): {len(stockpile['items'])} items")
# Parse with filters
seaports = fs_sav.parse_save("War.sav", stockpile_type="Seaport")
reserves = fs_sav.parse_save("War.sav", reserves=True)
terminus_public = fs_sav.parse_save("War.sav", hex="TerminusHex", public=True)
# Parse from bytes
with open("War.sav", "rb") as f:
data = f.read()
stockpiles = fs_sav.parse_save_bytes(data)
# Track stockpiles across parses by their key
by_key = {fs_sav.stockpile_key(s): s for s in stockpiles}
# Find the save files on this machine (newest first)
saves = fs_sav.find_save_files()
if saves:
stockpiles = fs_sav.parse_save(saves[0]["path"])
# Get parser info
info = fs_sav.info()
print(f"Implementation: {info['implementation']}, Version: {info['version']}")
Pinned tooltips that yield no stockpiles (see Skipped tooltips)
are reported as UserWarnings:
import warnings
with warnings.catch_warnings(record=True) as caught:
warnings.simplefilter("always")
stockpiles = fs_sav.parse_save("War.sav")
skipped = [str(w.message) for w in caught]
Filter parameters
| Parameter | Type | Description |
|---|---|---|
public |
bool |
Only public stockpiles (non-reserve) |
reserves |
bool |
Only reserve stockpiles |
hex |
str |
Filter by hex name (e.g., "TerminusHex") |
stockpile_type |
str |
Filter by type (e.g., "Seaport") |
with_items |
bool |
Only stockpiles with items (drops empty and newly pinned ones) |
Rust Library Usage
use fs_sav::{parse_save, ParseResult};
fn main() -> fs_sav::Result<()> {
let result = parse_save("path/to/War.sav")?;
println!("Found {} stockpiles", result.stockpiles.len());
for stockpile in &result.stockpiles {
println!(" {} ({:?}): {} items",
stockpile.name,
stockpile.stockpile_type,
stockpile.items.len()
);
}
for warning in &result.warnings {
eprintln!("skipped: {warning}");
}
// From bytes: a `ParsedStockpiles` with the same stockpiles and warnings
let parsed = fs_sav::parse_save_bytes(&std::fs::read("path/to/War.sav")?)?;
println!("Found {} stockpiles", parsed.stockpiles.len());
// Find the save files on this machine (newest first)
if let Some(save) = fs_sav::find_save_files().first() {
let result = parse_save(&save.path)?;
println!("{}: {} stockpiles", save.path.display(), result.stockpiles.len());
}
Ok(())
}
Output Format
The output is a JSON array of stockpiles (Python: a list of dictionaries):
[
{
"name": "",
"type": "Seaport",
"faction": "Colonial",
"hex": "TerminusHex",
"coords": { "x": 0.457745, "y": 0.664469 },
"is_reserve": false,
"items": [
{ "code": "Rifle", "quantity": 100, "crated": false },
{ "code": "RifleAmmo", "quantity": 50, "crated": true }
],
"timestamp": "2024-01-15T10:29:00Z"
}
]
Stockpile fields
| Field | Type | Presence | Description |
|---|---|---|---|
name |
str |
always | "" for a structure's main stockpile, the reserve's name for a reserve stockpile |
type |
str |
always | Raw in-game CodeName (e.g. Seaport). Unknown or new types are passed through as is |
faction |
str |
always | Warden or Colonial: the faction whose pinned tooltips the stockpile was read from |
hex |
str |
when known | Hex name, e.g. TerminusHex |
coords |
{x, y} |
when known | Normalized map coordinates (0.0 to 1.0) |
is_reserve |
bool |
always | Whether this is a reserve stockpile |
items |
list | always | Items in the stockpile; empty for an empty stockpile (see below) |
items[].code |
str |
always | Item CodeName |
items[].quantity |
int |
always | Quantity |
items[].crated |
bool |
always | Whether the item is crated |
tech |
object | bases only | Garrison/upgrade build progress (0-100 per tech component) |
refinery |
object | refineries only | See Refinery Data |
factory_orders |
list | factories only | See Factory Orders |
timestamp |
str or null |
always | When the tooltip was last updated (UTC, ISO 8601); null if missing in the save, with an entry in errors |
errors |
list of str |
when any | Problems found reading the stockpile |
Fields that are not present are omitted rather than set to null (except
timestamp). Items never carry confidence, and shard, ingame_timestamp
and resolution are never set: those only exist in screenshot (OCR) output.
Empty stockpiles
Empty stockpiles are returned like any other, with items: []. This includes
newly pinned stockpiles, which start empty. The with_items filter drops them,
so leave it off when tracking stockpiles over time.
Skipped tooltips
Only the latest details of each pinned tooltip are read. A tooltip whose
latest details carry no data is skipped: the game sends none for structures
the player is not on the side of, and stores empty details when it has none
for the structure (e.g. it no longer exists, or the tooltip could not be
matched to it). Skipped (and unreadable) tooltips are reported as warnings: in
warnings of the Rust results, as Python UserWarnings, and on stderr by the
CLI.
Stockpile key
Each stockpile has a key that identifies it across parses, used by
fs-sav watch to track changes:
type:hex:x,y:name
Coordinates are rounded to 4 decimals (0,0 when absent), an empty type is
written as Undefined and a missing hex as an empty string, e.g.
Seaport:TerminusHex:0.4577,0.6645:. Build it with fs_sav.stockpile_key(stockpile)
in Python or Stockpile::to_key() / fs_sav::stockpile_key() in Rust rather
than reimplementing it, so it stays consistent across versions.
Stability
The fields above, the key format and the input of parse_save_bytes (the raw
bytes of a .sav file) are the public contract. New optional fields may be
added in any release, so ignore fields you don't use. Removing or renaming a
field, changing its type or meaning, or changing the key format is a breaking
change: it bumps the minor version while fs-sav is 0.x, and the major version
from 1.0.
Changes between versions, breaking ones first, are listed in the CHANGELOG.
Refinery Data
The main stockpile of a refinery also carries a refinery object with its
production state, as seen by the player who last opened the tooltip:
{
"type": "Refinery",
"items": [{ "code": "Diesel", "quantity": 2340, "crated": false }],
"refinery": {
"orders_access": { "level": "public" },
"orders": [
{ "slot": 0, "code": "Cloth", "source": 0, "refined": 50, "duration": 0 }
],
"storages": [
{
"access": { "level": "squad", "squad_id": 1 },
"slot": 1,
"code": "Diesel",
"refined": 5309
}
]
}
}
orders_access: the queue the player had selected (levelispersonal,publicorsquad;squad_idonly for squad queues).orders: one entry per recipe of that queue.sourceis the material still waiting to be refined,refinedthe output ready to collect (for a squad queue, the squad's stored output) anddurationthe seconds left to refine the player's remaining source.storages: output held per owner and recipe: the player's other squads and, while viewing a squad queue, the player's own personal output.slotis the recipe's position in the refinery's recipe list (255 if not in the list);codeis the item it produces, when known.- Counts are capped by the game at 65000 and
durationat 64800 seconds.
Factory Orders
The main stockpile of a Factory or Mass Production Factory also carries the orders of the player who last opened the tooltip, one per production queue:
{
"type": "Factory",
"factory_orders": [
{
"queue": 0,
"queue_type": "SmallArms",
"duration": 0,
"items": [{ "slot": 16, "code": "RifleC" }]
},
{
"queue": 6,
"queue_type": "Uniforms",
"duration": 90,
"items": [{ "slot": 13, "code": "ScoutUniformC" }]
}
]
}
queueis the queue's position in the factory's queue list;queue_typenames it when known. A Mass Production Factory's vehicle and structure queues are not reported in tooltips.durationis the seconds until the order is done, counting the orders ahead of it in the queue (capped by the game at 64800); it is 0 for an order that is done and waiting to be collected, which is reported before pending ones.itemsare the order's items:slotis the position in the queue's production list (255 if not in the list) andcodethe item, when known.
Stockpile Types
type is the structure's in-game CodeName, passed through as is, so any
structure that can be pinned is supported, including ones added by game
updates. Some common types:
| Category | Types |
|---|---|
| Bases | GarrisonStation, Keep, ForwardBase1, RelicBase (1-3), FortBase (T1-T3), BorderBase, TownBase (1-3), FortGarrisonStation |
| Storage | StorageFacility, Seaport, AircraftDepot, ConstructionYard |
| Production | Refinery, Factory, MassProduction |
| Facilities | Hospital, MaintenanceTunnel, FacilityFactorySmallArms, FacilityModificationCenter, FacilityTransfer (Liquid/Material/Resource), FacilityVehicleFactory (1-3) |
License
MIT
Metadata
Release files for fs-sav 0.4.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | |
|---|---|---|---|---|
| fs_sav-0.4.0.tar.gz | 67.4 kB | Without provenance | Details |
Built distributions (wheels)
| File | Reset | |||
|---|---|---|---|---|
| fs_sav-0.4.0-cp310-abi3-win_amd64.whl | CPython 3.10 | abi3 | Windows x86-64 | Details |
| fs_sav-0.4.0-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl | CPython 3.10 | abi3 | Linux glibc 2.17+ x86-64 | Details |
| fs_sav-0.4.0-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl | CPython 3.10 | abi3 | Linux glibc 2.17+ ARM64 | Details |
| fs_sav-0.4.0-cp310-abi3-macosx_11_0_arm64.whl | CPython 3.10 | abi3 | macOS 11.0+ ARM64 | Details |
| fs_sav-0.4.0-cp310-abi3-macosx_10_12_x86_64.whl | CPython 3.10 | abi3 | macOS 10.12+ x86-64 | Details |
Total release size: 3.4 MB
Release files / fs_sav-0.4.0.tar.gz
| Download URL | fs_sav-0.4.0.tar.gz |
|---|---|
| Size | 67.4 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
b0b17dd53304f491b07f42f4df7dcda4a4f17d2d4f2d6c87ffa5102bbd9ef526
|
|
BLAKE2b-256 checksum How to use checksums |
2f9378ae2009ab9da4171086fce4b62e6cd8cb7bf9cee1fecd466e0c31c00b8f
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
maturin/1.15.0
|
Release files / fs_sav-0.4.0-cp310-abi3-win_amd64.whl
| Download URL | fs_sav-0.4.0-cp310-abi3-win_amd64.whl |
|---|---|
| Size | 663.2 kB |
| Tags | CPython 3.10 Windows x86-64 abi3 |
|
SHA-256 checksum How to use checksums |
e83e74eae992aa11514a655201b365a476e7c5e93198a9ee3396f2022c63206a
|
|
BLAKE2b-256 checksum How to use checksums |
af45953f78a315ec4b75dfc88dcb34791c2ea057fb9e57ac8d9dd15f2850da88
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
maturin/1.15.0
|
Release files / fs_sav-0.4.0-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
| Download URL | fs_sav-0.4.0-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl |
|---|---|
| Size | 720.7 kB |
| Tags | CPython 3.10 Linux glibc 2.17+ x86-64 abi3 |
|
SHA-256 checksum How to use checksums |
ac2f5e163b711848099dccaecb705b8901630067bc26f7c5eb32b0ddee052806
|
|
BLAKE2b-256 checksum How to use checksums |
5f414041531c47586b480cbb39823ebd456ec9b8237c7b42680764ea77320b08
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
maturin/1.15.0
|
Release files / fs_sav-0.4.0-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl
| Download URL | fs_sav-0.4.0-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl |
|---|---|
| Size | 684.1 kB |
| Tags | CPython 3.10 Linux glibc 2.17+ ARM64 abi3 |
|
SHA-256 checksum How to use checksums |
010d5a3f02fa51a6bf505c618a033154770d2ec1860785d5835a552ef211893e
|
|
BLAKE2b-256 checksum How to use checksums |
0dc51e3fd47c3d81d5f7c6cd77d4f20fdcf84cd99ed1a1c45ee12ab2194d285b
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
maturin/1.15.0
|
Release files / fs_sav-0.4.0-cp310-abi3-macosx_11_0_arm64.whl
| Download URL | fs_sav-0.4.0-cp310-abi3-macosx_11_0_arm64.whl |
|---|---|
| Size | 624.0 kB |
| Tags | CPython 3.10 abi3 macOS 11.0+ ARM64 |
|
SHA-256 checksum How to use checksums |
c9c418be2d86200978a2972ecb817217034e1b6689de7803741b30258bb59a7e
|
|
BLAKE2b-256 checksum How to use checksums |
1fefede1fd1af68bca287df4b089f86adfe64e92ced6c660cbaa3d4f6332e32c
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
maturin/1.15.0
|
Release files / fs_sav-0.4.0-cp310-abi3-macosx_10_12_x86_64.whl
| Download URL | fs_sav-0.4.0-cp310-abi3-macosx_10_12_x86_64.whl |
|---|---|
| Size | 649.0 kB |
| Tags | CPython 3.10 abi3 macOS 10.12+ x86-64 |
|
SHA-256 checksum How to use checksums |
f33014b7b16106ad643817246c67d4199f533a7a7e898dce25a57534ccb37c04
|
|
BLAKE2b-256 checksum How to use checksums |
7687757744431f06f0b0c21f2479c6f21086c5c2dfa97cd1693e95b6fd65b341
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
maturin/1.15.0
|
Security for fs-sav 0.4.0
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
No provenance found
None of the files in this release were uploaded with provenance metadata.
A note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.