Skip to content

Speak Factorio 2.1 instead of 1.1 - #77

Merged
wormeyman merged 17 commits into
mainfrom
fix/factorio-21-oracle
Aug 17, 2026
Merged

Speak Factorio 2.1 instead of 1.1#77
wormeyman merged 17 commits into
mainfrom
fix/factorio-21-oracle

Conversation

@wormeyman

Copy link
Copy Markdown
Collaborator

Fixes the mis-rotated pumpjacks and unrecognized item names reported since Factorio 2.1.

Both symptoms had one root cause: the planner still spoke Factorio 1.1.

The five defects

  1. effectivity-module-3 has not existed since 2.0. The game silently rejected it. Fixed in C# and in the Vue dropdown, plus a migration for the name already saved in localStorage and in shared URLs (module names travel in the query string, so a rename alone would leave old links broken forever).
  2. Directions were doubled on output but never halved on input. A 2.x east pumpjack (4) parsed as Direction.Down. That is the mis-rotation.
  3. The mirror flag was dropped. Factorio 2.1.7 added pumpjack flipping; a flipped entity carries "mirror": true. Now parsed, not emitted.
  4. The test corpus was 1.1-encoded, which is exactly why none of this ever failed a test. Both lists re-normalized to 2.x.
  5. The normalize path never doubled directions at all - found mid-work, not in the original design. The doubling lived in GridToBlueprintString.Execute, and normalize calls SerializeBlueprint directly, bypassing it. So PlanOrchestrator.Normalize was emitting internal 1.1 values under version: 0. Stamping and doubling now sit together in SerializeBlueprint as a single choke point.

How this was found, and how it gets caught next time

Rather than trusting the wiki, tools/capture-factorio-oracle.sh pulls prototype facts out of the game itself and commits them as a fixture. FactorioOracleTest asserts the planner's hardcoded constants against it on every build - no Factorio install needed on CI, because it reads the committed fixture. Re-capture after a game update and a changed fixture fails the test with a diff.

The version, direction and mirror encodings were each confirmed by round-tripping blueprints through Factorio 2.1.14 rather than read from docs, and cross-checked against wube/factorio-data at tag 2.1.14.

The oracle also ruled things out: every pole and beacon value, and MaxUnderground, still match the game. The damage was narrower than it looked.

The safety check

Re-normalizing the corpus and fixing the parser are inverse operations, so Score.HasExpectedScore.verified.txt is unchanged. A moved scoreboard would have meant the change was wrong. That turned the 61-blueprint scoreboard from churn into a free end-to-end check.

4299 tests pass under both default and UseLuaSettings=true. The Lua regeneration is a genuine no-op (CSharp.lua inlines const strings, and nullable auto-properties are not emitted).

Not done locally, needs CI

  • luac5.2 syntax check and lua5.2 sample.lua - the local luac is 5.5.1, a poor proxy.
  • Solution-wide build under Lua settings - wasm-tools is not installed locally, so BrowserWasm/BlazorWebApp were not built. Every change is purely additive with no altered signature, so risk is low.
  • The WASM bundle rebuild and a browser smoke test. deploy-cloudflare.yml runs build-wasm and asserts the bundle shape on pull requests.

Known and deliberately deferred

GridToBlueprintString still emits neighbours for electric poles, which Factorio 2.0 replaced with a top-level wires array. Confirmed with a probe mod. Impact is near zero because poles auto-connect by wire reach, and emitting wires properly is a feature rather than a one-liner. Filed separately.

🤖 Generated with Claude Code

wormeyman and others added 15 commits August 16, 2026 15:27
The planner hardcodes entity names, item names, direction values and entity
geometry. When Factorio changes any of those, nothing here notices: plans keep
generating, they are just wrong. Factorio 2.0 renamed effectivity-module-N to
efficiency-module-N and widened directions from 8-way to 16-way, and both went
unnoticed.

Rather than trust memory or the wiki, pull the facts out of the game. It ships
four machine-readable sources, and this merges them into one small fixture:

  factorio --dump-data        prototypes: names, boxes, pipe connections,
                              pole supply and wire reach, beacon stats
  data/*/migrations/*.json    every rename, as a table rather than a guess
  doc-html/runtime-api.json   defines.direction, stamped to the install
  data/changelog.txt          consulted by hand, not captured

The capture runs with user mods disabled. Mods rewrite prototypes freely, so a
capture that loads them describes one person's modded game rather than
Factorio. Only core, base and the bundled DLC load.

CI never runs the capture and needs no Factorio install: it reads the committed
fixture. That is why the fixture is committed rather than generated on demand.

The fixture stores raw prototype values, never values derived from them.
Factorio's rule for turning supply_area_distance into a covered tile area is
not one formula (poles come out as 2*distance, a beacon as 2*distance plus its
own footprint, and substation's collision box fits neither), so encoding a
guessed formula would produce a fixture that is confidently wrong and drifts
where no test can see it.

No source or planner behavior changes here. Pointing the fixture at the current
constants already reports what is stale, which is the next commit's job:

  ItemNames.EfficiencyModule3  "effectivity-module-3" -> efficiency-module-3
  ModuleSelect.vue             effectivity-module{,-2,-3} -> efficiency-module*
  Direction.Right=2            2 now means northeast; east is 4
  Direction.Down=4             4 now means east;      south is 8
  Direction.Left=6             6 now means southeast; west is 12

Verified: runs headless (exit 0), loads only core/base/DLC, and two independent
captures are byte-identical.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Design for the bugs reported since 2.1: mis-rotated pumpjacks and unrecognized
renamed items. Both are one root cause - the planner still speaks 1.1.

Covers the four defects the oracle from bfef7ba surfaced (module rename,
direction converted on output but not input, dropped mirror flag, 1.1-encoded
corpus), and records why the tests never caught any of it: the corpus is
1.1-encoded, so the parser is correct for the tests and wrong for users.

Two design points worth keeping:

The internal Direction enum does not change. It is the planner's logical
four-way concept and is transpiled to Lua, so version knowledge lives at the
serialization boundary instead.

Re-normalizing the corpus and fixing the parser are inverse operations, so
Score.HasExpectedScore.verified.txt should come out unchanged. That turns the
61-blueprint scoreboard from churn into a free end-to-end check.

The uint64 layout of Blueprint.Version is unconfirmed - no blueprint in this
repo carries a nonzero version - so confirming it against a real 2.1 export is
written up as the first implementation step rather than guessed at.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The spec left the uint64 layout of Blueprint.Version unconfirmed, because no
blueprint in this repo carries a nonzero version. It is now confirmed against a
real 2.1.14 export:

  raw 562954249306113 = 0x0002_0001_000e_0001 -> 2.1.14.1

So the layout is major<<48 | minor<<32 | patch<<16 | dev, and the 2.0 threshold
is 2<<48 = 562949953421312.

Direction and mirror were established the same way, by round-tripping a
blueprint through the game rather than reading docs. A throwaway mod builds a
blueprint with known entities and exports it headless via --create. What the
exporter actually writes:

  north  omitted entirely (not 0)
  east   4
  south  8
  west   12
  a flipped pumpjack carries "mirror": true

North being omitted rather than written as 0 matters: Entity.Direction is
already nullable and defaults to Up, so that path is correct today.

FFF #442 and changelog 2.1.7 announce pumpjack flipping but say nothing about
how a flip is represented, which is why it was tested rather than assumed.

Cross-checked the capture against wube/factorio-data at tag 2.1.14: the
migrations directories are byte-identical and item.lua defines
efficiency-module directly. Also worth recording so it is not re-raised -
"effectivity" still appears 14 times in 2.1.14, but only as a property name
(distribution_effectivity, vehicle effectivity), never an item name.

Adds --check to the capture script, which reports drift and exits 1 without
touching the fixture, following the convention in FactorioMapWebUI's
scripts/sync-factorio-refs.sh. Verified both paths: clean run reports up to
date, and a corrupted fixture prints the exact diff and exits 1.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Seven tasks over the four defects in the spec, in an order that keeps the suite
green at each commit except one deliberate exception.

Two structural findings that shaped it.

Task 1 lands the oracle assertion test BEFORE any fix, so the suite goes red on
effectivity-module-3. That is the reported bug reproduced as a test rather than
described in a commit message.

Task 5 is atomic and says so. Fixing the parser alone misreads the 1.1-encoded
corpus; re-normalizing the corpus alone produces 2.x values the unfixed parser
misreads. Either half on its own turns the suite red, so they land together,
with the unchanged score scoreboard as the check that they cancel.

Also corrects three spec claims found while reading the code: FormatVersion and
ParseVersion already exist and already match the confirmed layout, output is
already version-stamped at GridToBlueprintString.cs:222, and the fixture needs
no csproj entry because tests resolve paths through GetRepositoryRoot().

Self-review caught a bug in the plan's own first draft: it had CleanBlueprint
carry the source version through, which would emit 2.x directions under a 1.1
stamp and halve already-converted values on reparse. Stamping moved to
SerializeBlueprint, next to the code that doubles the directions, so the two
cannot disagree.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Both were REPLACE_ME placeholders the executor would have had to fill by
running Factorio. They are now literal strings exported from 2.1.14 and
verified to decode to exactly what their tests assert, so every task in the
plan is mechanical.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Per-plan ledgers, task briefs and review packages are working state for an
in-flight plan. Git history is the record, not this.
Reads the committed fixture, never the game, so CI needs no Factorio install.
EveryVanillaModuleNameExistsInFactorio fails on effectivity-module-3, which is
the reported bug reproduced as a test. Fixed in the next commit.

Also corrects three spec claims found while planning: FormatVersion/ParseVersion
already exist, output is already version-stamped, and the fixture needs no
csproj entry.
The doc comment explaining why EntityNames.AaiIndustry is not checked was
attached to EveryVanillaModuleNameExistsInFactorio, which reflects only
ItemNames.Vanilla and never touches EntityNames. Moved it to
EveryVanillaEntityNameExistsInFactorio, the method that actually excludes
AaiIndustry, and reworded it to name that method's own reflection target.

Per spec section C2, both carve-outs need a comment saying why; this restores
that for the entity carve-out.
effectivity-module-3 has not existed since 2.0, so the game silently rejected
it. The C# identifier was already EfficiencyModule3; only the string was stale.
Changing the dropdown alone would leave every existing user sending
effectivity-module-3, which Factorio has not accepted since 2.0. The name is
persisted in localStorage AND travels in shared query-string links
(storeToQuery maps pumpjackModule/beaconModule into the URL), and only the
localStorage path runs pinia-plugin-persistedstate's afterHydrate hook. So
migrateModuleNames is called from both load sites: the persist afterHydrate
hook in getStore(), and explicitly at the end of populateStoreFromQuery.
Factorio 2.1.7 added pumpjack flipping, and a flipped entity carries
mirror: true. Confirmed by round-tripping a blueprint through 2.1.14, since
FFF #442 describes the feature but not its blueprint representation.

Parsed only. The planner re-chooses every pumpjack orientation itself, so
honoring a flip is a separate question and is out of scope.
Output already multiplied directions by 2 for Factorio 2.0's 16-way encoding,
but input did nothing, so a 2.x east pumpjack (4) parsed as Direction.Down.
That is the reported mis-rotation.

Sniffing the values cannot fix it: a blueprint whose directions are all in
{0, 4} is valid under both encodings with different meanings. Version is the
only sound signal, so ToInternalDirection gates on it, treating an unstamped
blueprint as modern because that is what users paste.

Only the planner path doubled directions, at the two entity sites in
GridToBlueprintString.Execute. The normalize path serializes a CleanBlueprint
result, so it emitted internal 1.1-style directions under version 0 - and once
parsing became version-aware, re-reading its own output halved values that were
never doubled. So the doubling moves to SerializeBlueprint, which every
blueprint string goes through, and the version stamp sits right beside it. The
two can no longer disagree. This also fixes the api/v1/oil-field/normalize
response, which had the same defect. CleanBlueprint is untouched, so the core
library and its Lua transpilation are unaffected.

The corpus was 1.1-encoded, which is exactly why no test caught this. Both
lists are re-normalized to 2.x, and the seven inline test blueprints that this
repo's own normalizer had produced with directions but no version are converted
the same way. Blueprints carrying a real 1.1 version are genuine Factorio 1.1
captures and are left alone, so the legacy path stays covered end to end.

CleanBlueprintTest now compares the version separately, because CleanBlueprint
builds a fresh blueprint and the version is a serialization concern.

Score.HasExpectedScore.verified.txt is unchanged, as it must be: the corpus
rewrite and the parser fix are inverse operations. No Verify snapshot moved.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Two problems with the version-gated parsing added in the previous commit.

First, it converted and validated every entity's direction, but nothing
downstream reads a non-pumpjack direction - InitializeContext and CleanBlueprint
both keep the pumpjacks and discard the rest. So a blueprint holding a rail at a
2.x diagonal direction (2, 6, 10 or 14) was rejected outright, with a message
about the four directions a pumpjack can face, for an entity that is not a
pumpjack. That blueprint parsed fine before. Oil-field blueprints plausibly
contain rails, so this was a real regression. The loop now converts pumpjacks
only.

Second, halving a raw direction used integer division, so odd values quietly
rounded down to the cardinal below them: 1 became Up, 13 became Left. A
malformed pumpjack direction came out silently rotated, which is the same class
of bug this change set out to fix. Odd values are now left unhalved, so the
cardinal check rejects them exactly as it rejects the even non-cardinals.

Score.HasExpectedScore.verified.txt is still unchanged, and no Verify snapshot
moved. The corpus was not regenerated.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The corpus is 1.1-encoded, so it cannot cover a real 2.x blueprint. This
fixture came out of Factorio 2.1.14 with all four cardinals, which is the case
that was silently rotating.
Six review findings, all applied:

1. capture-factorio-oracle.sh could silently reuse a stale dump.json if
   Factorio exited 0 without writing one, or wrote it to an undiscovered
   directory - the trimmed fixture would then be stamped with the new
   version but describe the old game. Now records a timestamp before
   launching Factorio and fails loudly if the dump file it finds predates
   the run.

2. FactorioOracleTest.ReCaptureHint only pointed at re-running the capture
   script, but the more likely cause of a failure is a name added to
   EntityNames.Vanilla without a matching addition to WANTED_ENTITIES in
   trim-factorio-oracle.py. The hint now names both causes.

3. capture-factorio-oracle.sh --help printed one line past its header
   comment (echoing "set -euo pipefail"). Narrowed the sed range to stop
   at the header.

4. ReadsEveryCardinalFromARealFactorio21Blueprint asserted
   Direction.Up == e.Direction ?? Direction.Up, which cannot fail because
   the north entity in that fixture has no direction field at all.
   Replaced with Assert.Null(e.Direction), with a comment on why north is
   omitted rather than zero.

5. Added the oracle assertions for three constants verified by hand this
   round: GridToBlueprintString.EntityNameToSize (derived from
   collision_box as ceil(width)/ceil(height), verified against every
   hardcoded entry), PlanUndergroundPipes.MaxUnderground (== the fixture's
   max_underground_distance + 1, since one counts the ends and the other
   the gap), and the beacon's distribution_effectivity (1.5).

6. Doc corrections: the plan's Task 5 Step 5 said SerializeBlueprint only
   gained a version stamp, but the branch also moved the direction-doubling
   loop (ToOutputDirection) into it - corrected. Added D5 to the spec's
   "Defects in scope" list: the normalize path called SerializeBlueprint
   directly and never doubled directions at all, so it emitted internal
   1.1-style values under version: 0 - a real user-visible bug hiding
   inside what read as a refactor. Also recorded, as a deliberate decision
   rather than a gap, that OilFieldStore.ts hand-types the three module
   rename pairs instead of reading the fixture's renames table, per spec
   section C4.

dotnet test test/FactorioTools.Test/FactorioTools.Test.csproj:
4299 passed, 0 failed. Score.HasExpectedScore.verified.txt unchanged.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HxscuWbkHMYTt75eUgL8gp
The generated Lua targets 5.2 because Factorio runs a modified 5.2, but Homebrew
no longer ships 5.2 - a Mac checkout typically has luac 5.4 or newer. Parsing
5.2-targeted code with a 5.5 parser proves very little, so until now CI was the
only real check.

tools/check-lua.sh runs both CI steps in nickblah/lua:5.2-alpine, which is Lua
5.2.4 - the same version the README performance log was measured against, and
what CI installs via apt.

Both steps are needed, and the second is the one that matters. LINQ transpiles
cleanly and parses cleanly; it emits "local Linq = System.Linq.Enumerable",
which is nil because Collections.Linq is not in the CoreSystem load list. That
surfaces only when the module loads, so the check has to run the planner rather
than merely parse it.

Verified against the current tree: 127/127 files parse, and the planner runs to
completion in 0.37s.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The committed fixture is captured from 2.1.14, which is the experimental branch,
not stable 2.0.77. That is deliberate - the bug reports come from 2.1 - but it
was not written down anywhere.

Comparing the two, everything the planner reads is byte-identical except the
pumpjack's output fluid box, which went from 2 distinct corners to 4, one per
rotation (FFF #442). That may mean the hardcoded terminal offsets are wrong for
2.1; tracked in #81.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@wormeyman
wormeyman merged commit 484c499 into main Aug 17, 2026
9 checks passed
@wormeyman
wormeyman deleted the fix/factorio-21-oracle branch August 17, 2026 05:39
wormeyman added a commit that referenced this pull request Aug 17, 2026
Factorio 2.1 gave the pumpjack four distinct output corners where 2.0 had
two: east reused north's corner and west reused south's (FFF #442). The
planner still hardcoded the 2.0 pair, so every east-facing and west-facing
pumpjack got its pipe attached to the opposite corner from the one the game
outputs on. This is issue #81, and it is a second, independent cause of the
mis-rotated pumpjack reports, separate from the direction encoding fixed
in #77.

The offsets are measured, not derived. A probe mod placed pumpjacks in all
four rotations and read PipeConnection.target_position - the tile the
connecting pipe actually occupies - out of two running games. It reproduces
the numbers the planner already had on 2.0.77 before being believed about
2.1.14, which is what makes the new pair trustworthy.

FactorioOracleTest.TerminalOffsetsMatchTheFactorioOutputCorners pins the
four offsets to the committed oracle fixture, so a future corner move fails
a test naming Helpers.cs instead of silently shipping wrong plans.

Everything else here follows from the geometry:

- The corpus is re-normalized. CleanBlueprint gives each pumpjack the
  lowest-numbered direction whose terminal is unblocked, and 16 big-list
  blueprints answer that differently now. Counts are unchanged at 1147 and
  61, and the small list did not move.
- Four PlanUndergroundPipes fixtures place a pumpjack so its terminal lands
  on, beside, or at the end of a pipe run. Each was moved so the terminal
  lands where the scenario needs it again.
- One blueprint left BlueprintsWithIsolatedAreas: its isolated area is now
  reachable and the planner returns a valid plan for it, checked with
  ValidateSolution on.
- FbeOriginalFallsBackToFbeWhenLeftoverPumpsCannotConnect got a new
  blueprint. The old one stopped reaching that fallback; big-list index 827
  still does, found by making the branch throw and scanning both lists.
- CountsNoRotatedPumpjacks was re-stamped so its pumpjacks again face the
  way the planner picks, and YieldsAlternateSolutions took a field that
  still ties.

Heat routing pays for this. On the unchanged small list, fields needing zero
pumpjack drops go 35 -> 34 and total drops 51 -> 64, and one field now drops
a pumpjack with beacons on that heat-only keeps. That case is pinned rather
than skipped so fixing it fails the test. Tracked as #89.

4302 tests pass under both the default settings and UseLuaSettings=true. The
Lua is regenerated and checked with tools/check-lua.sh.

Fixes #81


Claude-Session: https://claude.ai/code/session_01UEteUvDzR5h4jEcowhm799

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant