feat(recipes): parse Eco's AutoGen C# into the vendored recipe graph #245

Merged
coilysiren merged 4 commits from feat/autogen-recipe-index into main 2026-08-13 00:55:32 +00:00
Member

What

Parses Eco's own generated C# into the vendored recipe graph, replacing the Eco Gnome export as the primary source. Implements the decision on #242.

Eco's dedicated server ships Mods/__core__/AutoGen — one plain C# file per recipe, item, skill, and tag, emitted from RecipeTemplate.tt. It is downloadable with steamcmd +login anonymous +app_update 739590: an anonymous-login depot, so no Steam account and no game licence, confirmed by SLG on the official Steam discussions. That makes it the only vanilla source carrying every DTO field at once — including a crafting station and a real craft time for every recipe, which the Gnome export does not have.

What's here

  • src/eco_mcp_app/autogen.py — the parser. Regex over the generated shapes, no .NET anywhere in the pipeline.
  • scripts/autogen_refresh.py + ward exec autogen-refresh — the maintenance verb that regenerates the vendored index. Downloads the server, or takes --root for a tree already on disk.
  • data/eco_autogen_data.json — the vendored index, plus .SOURCE.txt recording provenance and the SLG copyright.
  • recipes.load_recipe_index() prefers it, falling back to the Gnome seed.
  • tests/mcp/test_autogen.py — 12 tests over miniature fixtures.

Verification

Parsed from Steam build 24618181 (Eco 0.13.0), 2,284 source files:

1,487 recipes / 44 skills / 112 tags / 1,323 products / 68 stations, zero warnings.

Every recipe has a station, a non-zero labor cost, and a non-zero craft time. Every tag ingredient resolves to a known tag. Independent cross-check: Eco.ReferenceAssemblies at the same version declares 1,088 direct RecipeFamily subclasses and the tree has 1,122; the Gnome seed has 1,453 recipes and 43 skills against our 1,487 and 44.

ward exec lint clean (ruff + format + mypy, 110 files). ward exec smoke passes. 630 tests pass — see the caveat below.

Three things worth reviewing

Tag-product variants inherit family cost. A class X : Recipe registered via AddTagProduct declares only its own ingredients; Eco applies the owning family's labor and craft time at craft time. Left unresolved, 365 of 1,487 recipes would report a free, instant craft — a plausible-looking answer that is wrong, the class of defect #240 catalogued. The parser pushes the family's values down, and warns rather than silently zeroing when a family is missing.

Two parse bugs that only surfaced under test. Eco emits three CreateCraftTimeValue shapes and the 0.13 named form nests typeof(...) inside the call, so a regex stopping at the first ) reads nothing — hence the paren-balanced argument scan. Separately, generated attributes combine on one line ([RequiresSkill(...), Tag("..."), Tier(3)]), so anchoring a pattern on [ drops every attribute after the first; that one was caught by a failing assertion, not by reading the source.

Vendored, not fetched at build time. Per the #105 finding. It does mean ~1.6 MB of SLG-derived game data in the repo. eco_gnome_data.json set the precedent and /preview/recipes.json already serves this class of data publicly, so the exposure category is unchanged — but it is SLG's proprietary content with no licence grant, unlike the MIT Gnome file, so flagging it explicitly rather than burying it. .SOURCE.txt states this.

Caveat

tests/discord/test_worker.py::test_worker_uses_only_non_privileged_intents fails, and it fails on clean main too — verified in a detached worktree at origin/main. It passes in isolation and fails in a full-suite run, so it is a test-ordering/isolation issue, pre-existing and unrelated. Happy to file it separately.

Not in this PR

The MCP tools themselves (get_recipes, price_recipe, get_skills via DualRouteRegistry, step 4 of the #242 plan). This lands the data layer, which already upgrades the existing /preview/recipes.json plane. The tools are a focused follow-up on top.

Also unchanged: data/eco_gnome_data.json stays as the fallback. Removing it is a separate call once the AutoGen graph has run for a while.

Refs #242

Co-Authored-By: Claude Opus 5 (1M context) noreply@anthropic.com

## What Parses Eco's own generated C# into the vendored recipe graph, replacing the Eco Gnome export as the primary source. Implements the decision on #242. Eco's dedicated server ships `Mods/__core__/AutoGen` — one plain C# file per recipe, item, skill, and tag, emitted from `RecipeTemplate.tt`. It is downloadable with `steamcmd +login anonymous +app_update 739590`: an anonymous-login depot, so no Steam account and no game licence, confirmed by SLG on the official Steam discussions. That makes it the only vanilla source carrying every DTO field at once — including a crafting station and a real craft time for every recipe, which the Gnome export does not have. ## What's here - `src/eco_mcp_app/autogen.py` — the parser. Regex over the generated shapes, no .NET anywhere in the pipeline. - `scripts/autogen_refresh.py` + `ward exec autogen-refresh` — the maintenance verb that regenerates the vendored index. Downloads the server, or takes `--root` for a tree already on disk. - `data/eco_autogen_data.json` — the vendored index, plus `.SOURCE.txt` recording provenance and the SLG copyright. - `recipes.load_recipe_index()` prefers it, falling back to the Gnome seed. - `tests/mcp/test_autogen.py` — 12 tests over miniature fixtures. ## Verification Parsed from Steam build 24618181 (Eco 0.13.0), 2,284 source files: **1,487 recipes / 44 skills / 112 tags / 1,323 products / 68 stations, zero warnings.** Every recipe has a station, a non-zero labor cost, and a non-zero craft time. Every tag ingredient resolves to a known tag. Independent cross-check: `Eco.ReferenceAssemblies` at the same version declares 1,088 direct `RecipeFamily` subclasses and the tree has 1,122; the Gnome seed has 1,453 recipes and 43 skills against our 1,487 and 44. `ward exec lint` clean (ruff + format + mypy, 110 files). `ward exec smoke` passes. 630 tests pass — see the caveat below. ## Three things worth reviewing **Tag-product variants inherit family cost.** A `class X : Recipe` registered via `AddTagProduct` declares only its own ingredients; Eco applies the owning family's labor and craft time at craft time. Left unresolved, **365 of 1,487 recipes would report a free, instant craft** — a plausible-looking answer that is wrong, the class of defect #240 catalogued. The parser pushes the family's values down, and warns rather than silently zeroing when a family is missing. **Two parse bugs that only surfaced under test.** Eco emits three `CreateCraftTimeValue` shapes and the 0.13 named form nests `typeof(...)` inside the call, so a regex stopping at the first `)` reads nothing — hence the paren-balanced argument scan. Separately, generated attributes combine on one line (`[RequiresSkill(...), Tag("..."), Tier(3)]`), so anchoring a pattern on `[` drops every attribute after the first; that one was caught by a failing assertion, not by reading the source. **Vendored, not fetched at build time.** Per the #105 finding. It does mean ~1.6 MB of SLG-derived game data in the repo. `eco_gnome_data.json` set the precedent and `/preview/recipes.json` already serves this class of data publicly, so the exposure category is unchanged — but it is SLG's proprietary content with no licence grant, unlike the MIT Gnome file, so flagging it explicitly rather than burying it. `.SOURCE.txt` states this. ## Caveat `tests/discord/test_worker.py::test_worker_uses_only_non_privileged_intents` fails, and **it fails on clean `main` too** — verified in a detached worktree at `origin/main`. It passes in isolation and fails in a full-suite run, so it is a test-ordering/isolation issue, pre-existing and unrelated. Happy to file it separately. ## Not in this PR The MCP tools themselves (`get_recipes`, `price_recipe`, `get_skills` via `DualRouteRegistry`, step 4 of the #242 plan). This lands the data layer, which already upgrades the existing `/preview/recipes.json` plane. The tools are a focused follow-up on top. Also unchanged: `data/eco_gnome_data.json` stays as the fallback. Removing it is a separate call once the AutoGen graph has run for a while. Refs #242 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The bill-of-materials layer was seeded from Eco Gnome's export because that
was the only machine-readable recipe graph available (#98, #105). It is not
the best one: it carries no crafting-table tier, it is pinned to whatever
vanilla snapshot upstream bundled, and it cannot see a modded server.

Eco ships the real graph itself. The dedicated server's Mods/__core__/AutoGen
tree is plain generated C# — one file per recipe, item, skill, and tag — and
it is downloadable with `steamcmd +login anonymous +app_update 739590`. No
Steam account and no game licence: SLG publishes the server as an anonymous
depot. That makes it the only vanilla source carrying every DTO field at once,
including the crafting station and a real craft time.

Adds a parser for that tree, a maintenance verb that regenerates the vendored
index, and prefers the result at load time with the Gnome seed as fallback.

Parsed from build 24618181 (Eco 0.13.0): 1,487 recipes, 44 skills, 112 tags,
1,323 products, 68 stations, zero warnings. Every tag ingredient resolves to a
known tag, and no recipe reports a missing station, zero labor, or zero craft
time.

Three details worth knowing:

- Tag-product variants (`: Recipe` registered via AddTagProduct) declare only
  their own ingredients; Eco applies the owning family's labor and craft time.
  Left unresolved, 365 of 1,487 recipes would report a free, instant craft —
  the class of plausible-but-wrong answer #240 catalogued. They now inherit.
- Eco emits three CreateCraftTimeValue shapes, and the 0.13 named form nests
  `typeof(...)` inside the call, so a regex that stops at the first `)` reads
  nothing. The argument list is scanned with parens balanced instead.
- Generated attributes combine on one line, so anchoring a pattern on `[`
  drops every attribute after the first. Caught by a test, not by reading.

The graph is vendored rather than fetched during the image build, per #105.
data/eco_autogen_data.SOURCE.txt records the provenance and the SLG copyright.

Refs #242

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-authored-by: Kai Siren <coilysiren@gmail.com>
Co-authored-by: Claude <noreply@anthropic.com>
Refs #242

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-authored-by: Kai Siren <coilysiren@gmail.com>
Co-authored-by: Claude <noreply@anthropic.com>
Refs #242

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-authored-by: Kai Siren <coilysiren@gmail.com>
Co-authored-by: Claude <noreply@anthropic.com>
1.6 MB of generated JSON in the tree meant 73k lines in every `git diff` and a
hit on every `rg` for an item or recipe name. Nobody reads this file by hand and
`ward exec autogen-refresh` regenerates it, so it ships compressed: 1.6 MB ->
125 KB, a 13x cut.

The loader gunzips in place, and `mtime=0` keeps the gzip header from adding
nondeterminism on top of the payload's own `fetchedAtISO`.

Also drops a module-level mutable that tracked why the AutoGen load failed; it is
a local now, and the Gnome-fallback branch it feeds finally has a test.

Note this changes nothing about the licensing position recorded in
data/eco_autogen_data.SOURCE.txt. The data is still vendored and still served;
compression is an ergonomics change, not a rights one.

Refs #242

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-authored-by: Kai Siren <coilysiren@gmail.com>
Co-authored-by: Claude <noreply@anthropic.com>
Author
Member

Pushed two changes since the first review pass.

The graph now ships gzipped

data/eco_autogen_data.json.gz, 1.6 MB → 125 KB. It was putting 73k lines into every git diff and hitting every rg for an item or recipe name. The reviewable diff on this PR drops from 73,937 insertions to 1,163 as a result.

The loader gunzips in place. mtime=0 keeps the gzip header from adding nondeterminism on top of the payload's own fetchedAtISO. data/eco_autogen_data.SOURCE.txt carries the two one-liners for reading it back (gzcat … | jq, or gzip.open from Python).

Two things fell out while making the change:

  • A module-level mutable that tracked why the AutoGen load failed is now a local. It was accumulating across calls and would have duplicated warnings.
  • The Gnome-fallback branch now has a test: a corrupt AutoGen bundle must serve the 1,453-recipe seed and say in warnings that it fell back. Silently serving the older graph would be worse than failing.

631 tests pass, ward exec lint and ward exec smoke clean.

Correction on the licensing note

The PR body says the exposure category is unchanged because eco_gnome_data.json set the precedent. That was too comfortable, and I checked properly. The Eco EULA says:

You may not otherwise copy, display, distribute, perform, publish, modify, or use the Software or any component of it.

and

Except to the extent permitted under applicable law, you may not decompile, disassemble, reverse engineer or create derivative works of the Software by any means whatsoever.

A parsed index of the recipe definitions is a derivative work of a component of the Software, redistributed. There is also no LICENSE file in the server root, despite every AutoGen header saying "See LICENSE file in the project root."

Against that: SLG ships the server free on an anonymous depot, supports modding with free API and tooling, and their own Mods/UserCode/README.md tells you to "just copy the original file" when overriding a recipe. The ecosystem — Eco Gnome's DataExporter, the official wiki — publishes derived recipe data openly and unchallenged. That is tolerance in practice, not a grant. And Eco Gnome licensing their file MIT gives neither them nor us rights over SLG's underlying data, so the precedent is thinner than the PR body implies.

Nothing here resolves that, and the gzip does not either — it is an ergonomics change, not a rights one, and it is trivially reversible with the parser and provenance file sitting next to it. Recorded so the decision to merge is a visible one. The cleanest fix remains generating the index at build or first boot from a server copy the operator already has, which keeps the parser (entirely our code) in the repo and the derived data out; that stays available as a follow-up if the licensing question ever gets sharp.

Pushed two changes since the first review pass. ## The graph now ships gzipped `data/eco_autogen_data.json.gz`, 1.6 MB → **125 KB**. It was putting 73k lines into every `git diff` and hitting every `rg` for an item or recipe name. The reviewable diff on this PR drops from 73,937 insertions to **1,163** as a result. The loader gunzips in place. `mtime=0` keeps the gzip header from adding nondeterminism on top of the payload's own `fetchedAtISO`. `data/eco_autogen_data.SOURCE.txt` carries the two one-liners for reading it back (`gzcat … | jq`, or `gzip.open` from Python). Two things fell out while making the change: - A module-level mutable that tracked *why* the AutoGen load failed is now a local. It was accumulating across calls and would have duplicated warnings. - The Gnome-fallback branch now has a test: a corrupt AutoGen bundle must serve the 1,453-recipe seed **and** say in `warnings` that it fell back. Silently serving the older graph would be worse than failing. 631 tests pass, `ward exec lint` and `ward exec smoke` clean. ## Correction on the licensing note The PR body says the exposure category is unchanged because `eco_gnome_data.json` set the precedent. That was too comfortable, and I checked properly. The [Eco EULA](https://accounts.strangeloopgames.com/terms/eula) says: > You may not otherwise copy, display, distribute, perform, publish, modify, or use the Software or any component of it. and > Except to the extent permitted under applicable law, you may not decompile, disassemble, reverse engineer or create derivative works of the Software by any means whatsoever. A parsed index of the recipe definitions is a derivative work of a component of the Software, redistributed. There is also **no LICENSE file in the server root**, despite every AutoGen header saying "See LICENSE file in the project root." Against that: SLG ships the server free on an anonymous depot, [supports modding](https://wiki.play.eco/en/Modding) with free API and tooling, and their own `Mods/UserCode/README.md` tells you to "just copy the original file" when overriding a recipe. The ecosystem — Eco Gnome's DataExporter, the official wiki — publishes derived recipe data openly and unchallenged. That is tolerance in practice, not a grant. And Eco Gnome licensing *their* file MIT gives neither them nor us rights over SLG's underlying data, so the precedent is thinner than the PR body implies. **Nothing here resolves that, and the gzip does not either** — it is an ergonomics change, not a rights one, and it is trivially reversible with the parser and provenance file sitting next to it. Recorded so the decision to merge is a visible one. The cleanest fix remains generating the index at build or first boot from a server copy the operator already has, which keeps the parser (entirely our code) in the repo and the derived data out; that stays available as a follow-up if the licensing question ever gets sharp.
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
coilyco-gaming/eco-app!245
No description provided.