An ERP-style production planner for factory games — starting with
Satisfactory. It plans factories from the
inputs you have to the outputs you need, and ingests your live .sav file so it can
plan around what's already placed in your world. The planner, persistence, and UI are
game-agnostic; per-game adapters (catalogue parsing, save parsing) live in their own
module so additional factory games can slot in next to Satisfactory.
🌐 Web home: erp-for-factory.games (each supported
game gets a subdomain — satisfactory.erp-for-factory.games is the first). See
ADR-0020 for the rebrand decision
and what's deliberately out of scope (UI rebrand, namespace refactor).
📖 Deep-dive docs live in the GitHub Wiki. Start with Getting Started, Architecture, Save File Parsing, or LP Planner.
This project is OSS and runs on out-of-pocket spend. If you find it useful, sponsor via GitHub Sponsors — every contribution goes back into keeping it running.
See where the money goes for the running list of project costs.
v1.0 is the shipped product: a hosted planner at
satisfactory.erp-for-factory.games,
a winget-installable Windows agent that uploads your catalogue and save files
in the background, and an LP planner that picks recipes, miners, generators, and
pipe tiers from what you have to what you need.
Planner
- LP-driven recipe selection — OR-Tools picks recipes, allocates miners to resource nodes, sizes fluid pipes, and emits shadow prices + reduced costs so you can see why a plan is shaped the way it is (#129).
- Generator-aware planning — pass a
PowerTargetMwand the LP picks generator kinds + fuels freely; missing fuel surfaces as aMissingInputrather than infeasibility (#91 / #137). - Variance warnings for plans that touch miners or fluid extractors — base-power × count under-reports peak draw by ~50%, so the plan flags it (#91).
- Fluid throughput constraints — per-item pipe requirements with recommended tier on the resulting plan (#90).
Live factory state
- Save-file ingestion end-to-end — items, buildings, recipes, conveyor
polylines, and pipe polylines (
mSplineDatadeep-parse) all land on the map as GeoJSON (#12 / #138). - Auto-ingest — TickerQ background scheduler picks up newer
.savfiles without manual reload (#115). /dashboardpage — glance-able snapshot, auto-refresh, in-game-browser friendly (#131).
Agent + catalogue handover (ADR-0025)
- Windows agent —
winget install ErpForFactoryGames.Agentinstalls a background service that watchesSaved/SaveGames/and uploads new.savfiles plus yourDocs.jsonto your planner account (#201, #205, #210, #228). - Per-player catalogue store — planner endpoints resolve the catalogue from
the agent's upload instead of a server-local file, with parsed-state cached
in memory by
DocsHash(#238, #251). erp-agent://deep-link pairing — mint a token on the My Agents page, click "Open in agent", and the installed agent picks it up via the URL protocol handler (#237, #246).erp-agent --setupis the CLI fallback for headless installs.- Re-ingest on demand — "Re-ingest catalogue" button on My Agents flips a sticky flag the agent honours on its next log-tail poll (~60 s), forcing a re-upload regardless of the agent's cached hash (#239, #252).
Hosted deploy
satisfactory.erp-for-factory.gamesruns the planner as Docker containers on the homelab behind a Cloudflare Tunnel (ADR-0023).- Tag-to-deploy — pushing a
vX.Y.Ztag builds and publishes Docker images; the homelab pulls them on the next compose-up.
Foundations — Onion + CQRS + Wolverine on .NET 10 / Blazor Server / Aspire; MudBlazor 9 UI; PostgreSQL via Npgsql + EF Core with migration-drift CI guard.
Map editing (drag-to-plan, belt reroute, plan-vs-actual diff) is the v2 epic tracked under milestone 14.
See the full backlog at milestones or the wiki Roadmap.
Requires the .NET SDK pinned in global.json (currently .NET 10
preview). Get it from dot.net or
winget install Microsoft.DotNet.SDK.Preview.
You also need the GitHub CLI — the build restores
SatisfactorySaveNet from the fork's GitHub Packages feed, which always
requires auth (even for public packages). One-time:
gh auth refresh -h github.com -s read:packagesThen:
git clone https://github.com/ChrisonSimtian/ErpForFactoryGames.git
cd ErpForFactoryGames
export GITHUB_TOKEN=$(gh auth token)
dotnet run --project src/AppHostThe Aspire dashboard URL prints in the console — open it and click webfrontend.
For more detail, see the wiki's Getting Started page.
Everything runs through NUKE — the same commands work locally and in CI:
./build.sh Compile # restore + build
./build.sh Test # build + run xUnit suite (TRX → artifacts/test-results/)
./build.sh Format # dotnet format --verify-no-changes (vendor/ excluded)
./build.sh ComputeVersion # print the NB.GV-computed version for HEADWindows: ./build.ps1 <Target> or build.cmd <Target>. Mac/Linux: ./build.sh <Target> (executable bit is tracked in git). Targets live in
build/Build.cs.
UI tests (test/Web/Web.UiTests) drive a real browser via Playwright. ./build.sh Test installs the required chromium build automatically — the
InstallPlaywrightBrowsers target runs before Test. If you'd rather pre-install
manually:
pwsh test/Web/Web.UiTests/bin/Debug/net10.0/playwright.ps1 install chromiumThe planner reads items, buildings, and recipes from the catalogue JSON shipped
with your Satisfactory install. Modern installs use per-locale files (en-US.json,
de-DE.json, …); legacy installs had a single Docs.json. Either shape works —
point us at the directory and we'll pick en-US.json automatically, or point us
at a specific file.
On first run, the app tries to find the catalogue in this order:
ERP_SATISFACTORY_DOCS_PATHenvironment variable.- A user-saved path (set via the in-app Settings page).
Catalogue:Satisfactory:DocsPathinappsettings.json.- Steam library auto-detect on Windows.
The typical Steam Windows location is:
C:\Program Files (x86)\Steam\steamapps\common\Satisfactory\CommunityResources\Docs
Per ADR-0016, per-item icons
and the wiki map-backdrop source files live in a gitignored .assets/ folder
at the repo root, served at /assets/* by the Web project at runtime. A
fresh clone has no icons — the Planner picker degrades to text-only until
they're downloaded.
To populate .assets/:
# 1. Start the app (the script reads /catalog/items from the running ApiService).
dotnet run --project src/AppHost
# 2. In another shell:
pwsh tools/Update-Assets.ps1The script pulls icons from satisfactory.wiki.gg
with a polite 1 req/s rate-limit. Existing files are skipped — pass -Force
to re-download (e.g. after a game patch changed icon art).
The Factory state page (/factory/ingest) ingests a Satisfactory .sav file
and surfaces what's actually placed in your world — miners by tier, buildings by
type, belts, generators, resource node counts. The save path is resolved via the
same chain as the catalogue (ERP_SATISFACTORY_SAVE_PATH env var → app config
→ auto-detect under %LocalAppData%\FactoryGame\Saved\SaveGames\).
The .sav parser is a forked, v1.2-patched copy of
R3dByt3/SatisfactorySaveNet
— see the Save File Parsing
wiki page or ADR-0014
for the lineage and rationale.
- Onion with CQRS handlers dispatched via Wolverine.
- Two bounded contexts:
ERP(the planner) andSatisfactory(game-specific adapters). - Aspire orchestrates local dev across
ApiService,Web, andServiceDefaults.
flowchart TB
subgraph CR["Composition root"]
Aspire["AppHost — Aspire"]
Api["ApiService — Minimal API"]
Web["Web — Blazor + MudBlazor"]
end
subgraph Infra["ERP.Infrastructure"]
ORTools["OrToolsRecipePlanner"]
EF["EF Core<br/>SQLite · Postgres"]
Ticker["TickerQ jobs"]
SaveAdapter["SatisfactorySaveNet adapter"]
end
subgraph App["ERP.Application"]
Ports["IRecipePlanner · IFactoryStateProvider · IPlanRepository"]
Wolverine["Wolverine handlers"]
end
subgraph Dom["ERP.Domain"]
Domain["ProductionPlan · ProductionTarget · ..."]
end
Aspire --> Api & Web
Api --> Wolverine
Web --> Api
Wolverine --> Ports
Ports -.->|adapters| ORTools & SaveAdapter & EF
Ticker --> Wolverine
App --> Dom
Infra --> Dom
And here's what happens when you ingest a save and ask for a plan:
flowchart LR
Sav[".sav file"] --> Reader["SaveFileReader"]
Reader --> State["IFactoryStateProvider"]
Docs["Docs.json"] --> Catalog["Catalog parser"]
User["User input<br/>Targets · Available · PowerMW"] --> Query["PlanProductionQuery"]
State --> Query
Catalog --> Query
Query --> Bus["Wolverine bus"]
Bus --> LP["OrToolsRecipePlanner<br/>GLOP solver"]
LP --> Plan["ProductionPlan"]
Plan --> ApiOut["GET /plan"]
ApiOut --> UI["Blazor — /planner · /dashboard"]
AutoIngest["TickerQ auto-ingest"] -.watches.-> Sav
All architecturally significant decisions live in docs/adr/.
Notable ones:
| ID | Title |
|---|---|
| 0004 | Onion architecture |
| 0005 | CQRS in the Application layer |
| 0006 | Wolverine as mediator |
| 0009 | Runtime catalogue ingestion |
| 0010 | Game-agnostic contract |
| 0014 | Pure-C# save ingestion |
The headline libraries powering ERP for Factory Games. The wiki pages go deeper into how each one is wired.
| Library | Role |
|---|---|
| .NET Aspire | Local-dev orchestrator — dotnet run --project src/AppHost |
| Blazor + MudBlazor | Server-side UI (ADR-0002, ADR-0017) |
| Wolverine | In-process CQRS mediator (ADR-0006) |
| Google OR-Tools (GLOP) | The LP planner under OrToolsRecipePlanner |
| TickerQ | Background scheduler — auto-ingest, plan re-optimisation (ADR-0019) |
| EF Core + Npgsql | Dual-provider persistence (SQLite default, Postgres opt-in) (ADR-0018) |
| Nerdbank.GitVersioning | Version stamping from git height + version.json |
| Playwright | UI tests against a real Chromium build (ADR-0008) |
| xUnit + FluentAssertions | Unit + integration testing |
| OpenTelemetry | Traces / metrics / logs from Aspire defaults |
| NUKE | Build automation — same targets locally + in CI |
We rely on one library that needed game-format work upstream couldn't take on the same cadence as the Satisfactory team's releases — so we maintain a patched fork on GitHub Packages:
| Library | Upstream | Our fork | What we added |
|---|---|---|---|
SatisfactorySaveNet |
R3dByt3/SatisfactorySaveNet | ChrisonSimtian/SatisfactorySaveNet (currently 4.1.3 on GitHub Packages) |
Save format v1.2 (SaveVersion 60) TOC + Data Blob structure; deep-parse for ObjectProperty, ArrayProperty<ObjectProperty>, ArrayProperty<StructProperty> (incl. pipe mSplineData), StrProperty; chain-actor v1.2 fallback; continuous publish workflow. See ADR-0014 and the Save File Parsing wiki page. |
The fork is consumed as a PackageReference from the fork's GitHub Packages
feed (see nuget.config). GitHub Packages NuGet always
requires auth, even for public packages — set GITHUB_TOKEN before
restoring:
export GITHUB_TOKEN=$(gh auth token) # token needs read:packages
dotnet build ErpForFactoryGames.slnxThe submodule at vendor/SatisfactorySaveNet/ is an optional local drop-in
for fork iteration; it's marked update = none + ignore = all and not
required for the main build path.
src/
AppHost/ # Aspire orchestrator — `dotnet run` entry point
ApiService/ # Minimal-API backend
Web/ # Blazor Server UI (FICSIT-themed)
ServiceDefaults/ # Aspire defaults
ERP/
Domain/ # Pure entities
Application/ # Ports + CQRS handlers
Infrastructure/ # Adapters + DI wiring
Satisfactory/
Catalog/ # Docs.json parser
Save/ # .sav parser (wraps the SatisfactorySaveNet fork)
test/ # xUnit projects per layer
build/ # NUKE C# build scripts
vendor/SatisfactorySaveNet # Forked .sav parser submodule
deploy/Homelab.Stacks.ErpForFactoryGames # Compose stack submodule (off the build graph)
docs/adr/ # Architecture decisions
.claude/ # Claude Code conventions for this repo
Every push to main and every PR targeting main runs:
- Lint —
dotnet format --verify-no-changes(Ubuntu) - Build & Test — restore + build + xUnit (Ubuntu). OS-specific regressions surface locally on Windows/Mac before reaching CI.
- Publish test results — TRX files surface as a commit/PR check
Pushes to main additionally trigger:
- Release — auto-creates a GitHub release tagged with the
Nerdbank.GitVersioning-computed
version (
v0.1.N). Release notes auto-generated from PRs since the previous tag.
Bump the major/minor by editing version.json — the next
commit becomes vX.Y.0. Patch increments per commit automatically.
main is protected: PRs require all four matrix checks to pass before merging.
The Blazor apps run on Chris's homelab as Docker containers behind a
Cloudflare Tunnel — see ADR-0023
for the why and docs/operations/deploy.md for
the tag-to-deploy runbook.
The compose stack lives in a sibling repo,
Homelab.Stacks.ErpForFactoryGames,
attached here as a submodule at deploy/Homelab.Stacks.ErpForFactoryGames/.
Like the vendor/* submodules it's off the build graph (update = none,
ignore = all); fetch it explicitly only when you want to operate the
homelab:
git submodule update --init --checkout deploy/Homelab.Stacks.ErpForFactoryGames- Epics → milestones
- Stories / bugs → issues
- In-game state + TODOs →
.satisfactory/— not the project backlog
Repo-level Claude conventions live in CLAUDE.md and
.claude/ — repo layout, onion rules, the ADA in-game
assistant agent. Contributor workflow (branch names, commit style, CI gates)
lives on the wiki: Contributing.