Skip to content

Extract a shared Factorio oracle CLI #82

Description

@wormeyman

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:

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions