Speak Factorio 2.1 instead of 1.1 - #77
Merged
Merged
Conversation
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
This was referenced Aug 17, 2026
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>
This was referenced Aug 17, 2026
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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
effectivity-module-3has 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 inlocalStorageand in shared URLs (module names travel in the query string, so a rename alone would leave old links broken forever).4) parsed asDirection.Down. That is the mis-rotation.mirrorflag was dropped. Factorio 2.1.7 added pumpjack flipping; a flipped entity carries"mirror": true. Now parsed, not emitted.GridToBlueprintString.Execute, and normalize callsSerializeBlueprintdirectly, bypassing it. SoPlanOrchestrator.Normalizewas emitting internal 1.1 values underversion: 0. Stamping and doubling now sit together inSerializeBlueprintas a single choke point.How this was found, and how it gets caught next time
Rather than trusting the wiki,
tools/capture-factorio-oracle.shpulls prototype facts out of the game itself and commits them as a fixture.FactorioOracleTestasserts 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
mirrorencodings were each confirmed by round-tripping blueprints through Factorio 2.1.14 rather than read from docs, and cross-checked againstwube/factorio-dataat tag2.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.txtis 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 inlinesconststrings, and nullable auto-properties are not emitted).Not done locally, needs CI
luac5.2syntax check andlua5.2 sample.lua- the localluacis 5.5.1, a poor proxy.wasm-toolsis not installed locally, soBrowserWasm/BlazorWebAppwere not built. Every change is purely additive with no altered signature, so risk is low.deploy-cloudflare.ymlrunsbuild-wasmand asserts the bundle shape on pull requests.Known and deliberately deferred
GridToBlueprintStringstill emitsneighboursfor electric poles, which Factorio 2.0 replaced with a top-levelwiresarray. Confirmed with a probe mod. Impact is near zero because poles auto-connect by wire reach, and emittingwiresproperly is a feature rather than a one-liner. Filed separately.🤖 Generated with Claude Code