Summary
Four repos now run the Factorio game headless to ask it questions. Each one wrote
the same plumbing from scratch. The plan is to pull that plumbing into one shared
Rust CLI, factorio-oracle, in its own repo.
This issue records what that means for FactorioTools. Nothing here changes
existing code. The agreed migration rule is "new probes only": the tool gets used
by the next capture anyone writes, and tools/ stays as it is until someone has a
reason to touch it.
The four repos are FactorioTools, factorio-blueprint-editor, FactorioMapWebUI and
FactorioWikiDamageThresholds. Each is getting its own issue.
Why
Measured, not estimated:
| Repo |
Oracle tooling |
Lines |
Install discovery |
| FactorioTools |
tools/capture-factorio-oracle.sh + trim-factorio-oracle.py |
421 |
own copy |
| factorio-blueprint-editor |
tools/oracle/, 18 probes |
7,900 |
own copy x18 |
| FactorioMapWebUI |
test/oracle/ |
9,504 |
own copy |
A fourth copy turned up in an unrelated WordPress theme repo, in a Factorio
benchmark script that had been committed there by accident. It has since been
removed.
A real bug this work found
runtime-api.json does not contain the values of defines. Across all 1,554
define entries the only keys are name, order and description. There is no
value field at all.
tools/trim-factorio-oracle.py:150 does this:
return {v["name"]: v["order"] for v in define.get("values", [])}
So it uses declaration order as if it were the runtime value. It is correct today
only because Factorio declares directions clockwise from north = 0, which makes
order and value the same for a sequential enum. The committed fixture matches the
real numbers, so this is not a live defect in output.
But the docstring above it says defines.direction is "the authoritative answer to
'what number means east'". That claim is now false. It is an inference, and nothing
documents order as the runtime value.
The irony is worth stating plainly: direction encoding is the exact constant that
silently broke in 2.0, and it is the one the oracle infers rather than reads. Only
the running game knows that defines.direction.east == 4. Reading it properly
needs a probe mod, which is capability this repo has used and does not have - see
below.
Worth fixing whether or not the shared tool happens.
A documentation error
CLAUDE.md says the capture pulls four sources, and lists data/changelog.txt as
one of them. It pulls three. There is no changelog handling anywhere in tools/.
The changelog is a research source a human reads; it was written into the table as
if it were automated.
What this repo would hand over
Roughly 80% of the 421 lines. All of it is generic:
- Install discovery, including the macOS
.app versus Linux directory layouts
(capture-factorio-oracle.sh:71-107)
- Version derivation from
--version, plus deriving the major.minor a mod's
info.json needs
- The empty mod directory and its
mod-list.json (:121-122)
- Finding the user data directory that
script-output/ lands in (:140-154)
- Reading
data/*/migrations/*.json into a rename table
(trim-factorio-oracle.py:154-175). Nothing in it knows about oil fields.
- Pulling a named
defines table out of runtime-api.json, generalised so the
shared tool is not hardcoded to direction
find_prototype's type search and its collision-box disambiguation (:95-114).
This is the most reusable piece here. Most of these names exist twice, once as
the placeable entity and once as the carried item, and
data.raw["item"]["pumpjack"] has none of the geometry.
- Deterministic output and the
--check drift mode
Two guards here are better than what the other repos have, and become shared
defaults:
- The dump mtime check (
:124-126 and :156-168). A dump written before this
run is left over from an older capture. Trimming it anyway would stamp stale
prototype data with the version of the game that just ran, which is the exact
silent pass the tool exists to prevent.
- The loaded-mods report (
:136-138). Mods rewrite prototypes freely, so a
capture that loads them describes one person's game. In the shared tool this
should be a hard assertion against an expected set, not an echo a human has to
read.
What stays here
WANTED_ENTITIES (trim-factorio-oracle.py:30-41). These ten names are exactly
EntityNames.Vanilla, one for one. A blueprint editor wants hundreds; a map tool
wants none of them. The allowlist becomes caller-supplied config.
WANTED_FIELDS and WANTED_CONNECTION_FIELDS. supply_area_distance,
maximum_wire_distance and max_underground_distance are what OilFieldOptions
encodes.
- Every assertion in
FactorioOracleTest.cs.
Two rules the shared tool must not break
Raw values only, never derived. The docstring at trim-factorio-oracle.py:11-21
is what makes this fixture trustworthy. Factorio's rule for turning
supply_area_distance into covered tiles is not one formula - poles come out as
2*distance, a beacon as 2*distance plus its own footprint, and substation fits
neither. A guessed formula in a fixture would be confidently wrong and drift
invisibly. Note the tests already do derive things, but they do it where a wrong
derivation fails loudly against a hardcoded value, not in the fixture where it
would sit silently.
Byte-for-byte determinism. --check is a diff -u against a committed file.
Rust's serde_json does not sort maps, and HashMap order is randomised per
process. The shared tool needs BTreeMap or explicit sorting, indent=2, and a
trailing newline. Float formatting is the trap: 0.29, 2.5, 1.5 and 0.2 all
appear in the fixture, and any printer difference from Python turns every future
--check into permanent false drift. Test the Rust output against the current
committed fixture byte for byte before adopting it.
What this repo would get
A probe-mod mode it has used and does not have. The mod that established the
2.1 direction table, the mirror field and the Version bit layout was written,
run, and thrown away. The resulting blueprint string was pasted into
ParseBlueprintTest.cs and the generator discarded. Regenerating that fixture
today means rebuilding the mod from a paragraph of prose in CLAUDE.md. That is the
strongest single argument for the tool in this repo, and it is also what would fix
the defines problem above.
Three run modes rather than one. --dump-data here needs no mod at all - the
mod directory exists only to be empty. That is a contamination control, not a
scaffold. Two of the three sources need no game at all: migrations and
runtime-api.json are read straight off disk, and wube/factorio-data has
byte-identical migrations, so renames can be checked with no install.
Reference syncing, which this repo has never had. ~/GitHub/factorio-data is
used by hand here and named only in docs.
Prior art checked
Nothing existing covers this. Four projects re-implement Factorio's data stage in
embedded Lua (YAFC, factorio-draftsman, KirkMcDonald/factorio-tools,
factorio-scanner). They are fast and CI-friendly, and they are all approximations.
YAFC ships the warning itself: "YAFC loads mods in environment that is not
completely compatible with Factorio."
factorio-rust-tools is the closest match. Its CI downloads a pinned headless
Factorio from factorio.com/get-download/<version>/headless/linux64 and diffs
against a committed 37 MB golden file. That contradicts the line in CLAUDE.md
saying CI machines "have no Factorio install and never will", and it is a real
option for closing the gap the design doc already names: nothing automatically
notices a new Factorio release.
Also confirmed: --dump-data-raw is not a real flag, so --dump-data here is
correct. And capturing real exported blueprint strings by running the game has no
prior art anywhere, on two independent search passes.
Status
Design is agreed in outline: a Rust CLI, JSON in and JSON out, in a new public
repo. The write-up is not finished, and this issue will be updated with a link.
Separately, moving these repos under the FactoryGameFan org is proposed and not
decided. Nothing should be changed on that basis yet. One caution specific to
this repo: joelverhagen/FactorioTools#10 is open, cross-repository, with head
wormeyman:main and 466 files. What happens to a cross-repo pull request when the
head repo is transferred is not covered anywhere in GitHub's documentation. The
docs cover deleting a fork (pull requests permanently deleted) and detaching one
(pull requests lost), but not transfer. So this repo should move last, if at all,
and only after that question is settled.
Related issues
One per affected repo, all describing the same shared tool:
Summary
Four repos now run the Factorio game headless to ask it questions. Each one wrote
the same plumbing from scratch. The plan is to pull that plumbing into one shared
Rust CLI,
factorio-oracle, in its own repo.This issue records what that means for FactorioTools. Nothing here changes
existing code. The agreed migration rule is "new probes only": the tool gets used
by the next capture anyone writes, and
tools/stays as it is until someone has areason to touch it.
The four repos are FactorioTools, factorio-blueprint-editor, FactorioMapWebUI and
FactorioWikiDamageThresholds. Each is getting its own issue.
Why
Measured, not estimated:
tools/capture-factorio-oracle.sh+trim-factorio-oracle.pytools/oracle/, 18 probestest/oracle/A fourth copy turned up in an unrelated WordPress theme repo, in a Factorio
benchmark script that had been committed there by accident. It has since been
removed.
A real bug this work found
runtime-api.jsondoes not contain the values ofdefines. Across all 1,554define entries the only keys are
name,orderanddescription. There is novalue field at all.
tools/trim-factorio-oracle.py:150does this:So it uses declaration order as if it were the runtime value. It is correct today
only because Factorio declares directions clockwise from
north = 0, which makesorder and value the same for a sequential enum. The committed fixture matches the
real numbers, so this is not a live defect in output.
But the docstring above it says
defines.directionis "the authoritative answer to'what number means east'". That claim is now false. It is an inference, and nothing
documents
orderas the runtime value.The irony is worth stating plainly: direction encoding is the exact constant that
silently broke in 2.0, and it is the one the oracle infers rather than reads. Only
the running game knows that
defines.direction.east == 4. Reading it properlyneeds a probe mod, which is capability this repo has used and does not have - see
below.
Worth fixing whether or not the shared tool happens.
A documentation error
CLAUDE.md says the capture pulls four sources, and lists
data/changelog.txtasone of them. It pulls three. There is no changelog handling anywhere in
tools/.The changelog is a research source a human reads; it was written into the table as
if it were automated.
What this repo would hand over
Roughly 80% of the 421 lines. All of it is generic:
.appversus Linux directory layouts(
capture-factorio-oracle.sh:71-107)--version, plus deriving themajor.minora mod'sinfo.jsonneedsmod-list.json(:121-122)script-output/lands in (:140-154)data/*/migrations/*.jsoninto a rename table(
trim-factorio-oracle.py:154-175). Nothing in it knows about oil fields.definestable out ofruntime-api.json, generalised so theshared tool is not hardcoded to
directionfind_prototype's type search and its collision-box disambiguation (:95-114).This is the most reusable piece here. Most of these names exist twice, once as
the placeable entity and once as the carried item, and
data.raw["item"]["pumpjack"]has none of the geometry.--checkdrift modeTwo guards here are better than what the other repos have, and become shared
defaults:
:124-126and:156-168). A dump written before thisrun is left over from an older capture. Trimming it anyway would stamp stale
prototype data with the version of the game that just ran, which is the exact
silent pass the tool exists to prevent.
:136-138). Mods rewrite prototypes freely, so acapture that loads them describes one person's game. In the shared tool this
should be a hard assertion against an expected set, not an echo a human has to
read.
What stays here
WANTED_ENTITIES(trim-factorio-oracle.py:30-41). These ten names are exactlyEntityNames.Vanilla, one for one. A blueprint editor wants hundreds; a map toolwants none of them. The allowlist becomes caller-supplied config.
WANTED_FIELDSandWANTED_CONNECTION_FIELDS.supply_area_distance,maximum_wire_distanceandmax_underground_distanceare whatOilFieldOptionsencodes.
FactorioOracleTest.cs.Two rules the shared tool must not break
Raw values only, never derived. The docstring at
trim-factorio-oracle.py:11-21is what makes this fixture trustworthy. Factorio's rule for turning
supply_area_distanceinto covered tiles is not one formula - poles come out as2*distance, a beacon as2*distanceplus its own footprint, and substation fitsneither. A guessed formula in a fixture would be confidently wrong and drift
invisibly. Note the tests already do derive things, but they do it where a wrong
derivation fails loudly against a hardcoded value, not in the fixture where it
would sit silently.
Byte-for-byte determinism.
--checkis adiff -uagainst a committed file.Rust's
serde_jsondoes not sort maps, andHashMaporder is randomised perprocess. The shared tool needs
BTreeMapor explicit sorting,indent=2, and atrailing newline. Float formatting is the trap:
0.29,2.5,1.5and0.2allappear in the fixture, and any printer difference from Python turns every future
--checkinto permanent false drift. Test the Rust output against the currentcommitted fixture byte for byte before adopting it.
What this repo would get
A probe-mod mode it has used and does not have. The mod that established the
2.1 direction table, the
mirrorfield and theVersionbit layout was written,run, and thrown away. The resulting blueprint string was pasted into
ParseBlueprintTest.csand the generator discarded. Regenerating that fixturetoday means rebuilding the mod from a paragraph of prose in CLAUDE.md. That is the
strongest single argument for the tool in this repo, and it is also what would fix
the
definesproblem above.Three run modes rather than one.
--dump-datahere needs no mod at all - themod directory exists only to be empty. That is a contamination control, not a
scaffold. Two of the three sources need no game at all: migrations and
runtime-api.jsonare read straight off disk, andwube/factorio-datahasbyte-identical migrations, so renames can be checked with no install.
Reference syncing, which this repo has never had.
~/GitHub/factorio-dataisused by hand here and named only in docs.
Prior art checked
Nothing existing covers this. Four projects re-implement Factorio's data stage in
embedded Lua (YAFC, factorio-draftsman, KirkMcDonald/factorio-tools,
factorio-scanner). They are fast and CI-friendly, and they are all approximations.
YAFC ships the warning itself: "YAFC loads mods in environment that is not
completely compatible with Factorio."
factorio-rust-toolsis the closest match. Its CI downloads a pinned headlessFactorio from
factorio.com/get-download/<version>/headless/linux64and diffsagainst a committed 37 MB golden file. That contradicts the line in CLAUDE.md
saying CI machines "have no Factorio install and never will", and it is a real
option for closing the gap the design doc already names: nothing automatically
notices a new Factorio release.
Also confirmed:
--dump-data-rawis not a real flag, so--dump-datahere iscorrect. And capturing real exported blueprint strings by running the game has no
prior art anywhere, on two independent search passes.
Status
Design is agreed in outline: a Rust CLI, JSON in and JSON out, in a new public
repo. The write-up is not finished, and this issue will be updated with a link.
Separately, moving these repos under the
FactoryGameFanorg is proposed and notdecided. Nothing should be changed on that basis yet. One caution specific to
this repo:
joelverhagen/FactorioTools#10is open, cross-repository, with headwormeyman:mainand 466 files. What happens to a cross-repo pull request when thehead repo is transferred is not covered anywhere in GitHub's documentation. The
docs cover deleting a fork (pull requests permanently deleted) and detaching one
(pull requests lost), but not transfer. So this repo should move last, if at all,
and only after that question is settled.
Related issues
One per affected repo, all describing the same shared tool:
factorio-datatag contention