Skip to content

Guard against enabling beacons and heat pipes together (temporary) - #10

Merged
wormeyman merged 1 commit into
mainfrom
guard/beacons-heatpipes-mutual-exclusion
Jun 22, 2026
Merged

Guard against enabling beacons and heat pipes together (temporary)#10
wormeyman merged 1 commit into
mainfrom
guard/beacons-heatpipes-mutual-exclusion

Conversation

@wormeyman

Copy link
Copy Markdown
Collaborator

What

A temporary front-end guard that makes "Add beacons" and "Add heatpipes for Aquilo ❄️" mutually exclusive in the Vue app.

Why

The core planner can't do beacons and heat pipes at the same time yet. They compete for the tiles next to pipes, so when both are on the heat pipes get dropped and machines would freeze on Aquilo (see src/FactorioTools/OilField/OilFieldOptions.cs AddHeatPipes and the PlanBeacons.* steps). A real fix (making them coexist in the planner) is out of scope - this is just a guard until that's solved. No C#/WASM changes.

Behavior

  • Enabling heat pipes silently unchecks beacons.
  • Trying to enable beacons while heat pipes is on reverts the beacon checkbox and shows an inline Bootstrap warning near the toggle. Heat pipes win - keeping the field warm matters more than the beacon bonus.
  • A returning user with both enabled in localStorage gets beacons forced off on load (silently, heat pipes win).

Implementation

  • Coordination lives in one place - the Pinia store (OilFieldStore.ts) via watchers - rather than duplicating logic across BeaconForm.vue and HeatPipeForm.vue.
  • The revert watcher uses flush: "post" so the false-after-true transition is a real render diff that patches the checkbox back to unchecked. A "pre"/"sync" revert is invisible to the render and leaves the box stuck visually checked even though the store value is correctly false.
  • The warning flag is intentionally not part of the persisted/query-string store state.

Verification

In-browser (Playwright) walkthrough of all paths:

  • check heat → beacons unchecks, no warning ✅
  • re-check beacons (heat on) → reverts + warning shown ✅
  • uncheck heat → warning clears, beacons enable-able ✅
  • both-true localStorage load → beacons forced off ✅

npm run build (vue-tsc + vite) stays green.

🤖 Generated with Claude Code

The core planner can't do beacons and heat pipes at the same time yet:
they compete for the tiles next to pipes, so when both are on the heat
pipes get dropped and machines would freeze on Aquilo (see
OilFieldOptions.cs AddHeatPipes and the PlanBeacons.* steps). Until the
planner can make them coexist, make the two options mutually exclusive in
the Vue front-end.

Coordination lives in one place - the Pinia store (OilFieldStore.ts) -
via watchers, rather than duplicating logic across BeaconForm.vue and
HeatPipeForm.vue:

- Enabling heat pipes silently unchecks beacons.
- Trying to enable beacons while heat pipes is on reverts the beacon
  checkbox and shows an inline Bootstrap warning near the toggle. Heat
  pipes win - keeping the field warm matters more than the beacon bonus.
- A returning user with both enabled in localStorage gets beacons forced
  off on load (silently, heat pipes win).

The revert watcher uses flush: "post" so the false-after-true transition
is a real render diff that patches the checkbox back to unchecked; a
"pre"/"sync" revert is invisible to the render and leaves the box stuck
visually checked even though the store value is correctly false.

The warning flag is intentionally not part of the persisted/query-string
store state.

Verified in-browser: checking heat unchecks beacons (no warning),
re-checking beacons warns and reverts, unchecking heat clears the warning
and re-enables beacons, and a both-true localStorage load forces beacons
off. npm run build (vue-tsc + vite) stays green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WobBhi3atoKDE1jr53pu8c
@wormeyman
wormeyman merged commit 20b8efd into main Jun 22, 2026
7 checks passed
@wormeyman
wormeyman deleted the guard/beacons-heatpipes-mutual-exclusion branch June 22, 2026 22:59
wormeyman added a commit that referenced this pull request Aug 17, 2026
* Spec: a shared Factorio oracle CLI across four repos

Four repos ask the real game questions, and each wrote the same plumbing.
Measured: about 1,320 lines of near-identical wiring across 23 copies, before
counting FactorioTools' own share. 14 of 18 blueprint-editor probes hardcode a
factorio_version that silently skips the mod when it is wrong, only 1 of 18
checks the binary exists, and none of the three repos has a timeout.

The design is a Rust CLI, JSON in and JSON out. It owns the plumbing and not
the questions, because a probe compares the game against the consumer's own
reimplementation - so analysis has to stay in the consumer's language. That
also rules out a shared probe framework in any single language.

Records what reading the repos changed: five run modes rather than one, a
per-mode success predicate, the runner returning a work directory instead of
a dump, Lua passed in opaque, and reading factorio-data at a tag rather than
checking one out in a clone three repos share.

Spec lives here because this repo has the specs convention and the oracle repo
does not exist yet. Move it there when it does.

Refs #82, #83.

* Plan: the factorio-oracle runner core (plan 1 of 4)

Eleven TDD tasks from an empty directory to a working 'run' subcommand:
crate skeleton with Renovate in the first commit, version parsing, install
layout resolution, discovery, the probe spec types, Lua literal injection,
mod scaffolding, per-mode argument vectors, the per-mode success predicate,
the spawn boundary with a timeout, and the wiring plus a fake game.

Records a conflict plan 2 has to handle: the spec wants a byte-for-byte
reproduction of the committed fixture, but #83 shows that fixture's
directions come from reading a documentation index as if it were a value.
The port has to reproduce the bug first, then fix it as a visible diff.

Refs #82, #83.

* Fold in three decisions and the MapWebUI f32 requirement

Contamination reporting is now on by default, and the mechanism changed:
script.on_init takes exactly one handler, so a prelude using it would be
silently replaced by the consumer's own - which 17 of 18 blueprint-editor
probes register. A self-cancelling on_nth_tick collides with nothing.

Adds the f32 round-trip rule the map repo asked for, with its evidence:
two candidate kernels shared an identical worst absolute error of 2.682e-7
and differed by 42 exact matches of 512, so an error bound could not tell
them apart. Recorded as a test in a new task rather than a comment, because
a capture that loses precision still looks fine.

--instrument-mod is confirmed real in the 2.1.14 binary and gets a spike
before plan 2. Records MapWebUI#234 as the first customer.

* Send the seed through both channels from one field

The map seed can reach the game twice: inside the map-gen settings JSON and
as --map-gen-seed. MapWebUI sets both from the same variable, so they have
never disagreed - and nothing establishes which one the game honours.

Collapsing to one channel would be the bad kind of hazard. If the flag beat
the file, a CLI writing only the file would generate a different map from
the same request, with nothing erroring. That repo already paid for a
seed-provenance mistake once: a correct field scored 0.5% overlap against
the wrong convention where the right one scored 99.9%.

Taking one seed field and writing both channels makes the design correct
whichever wins, and makes disagreement structurally impossible.

Also softens the --map-gen-settings claim from 'required' to 'always
passed', which is what is actually established. Nobody has run --create
without it.

* Replace two guesses with measurements from the game

FactorioMapWebUI ran the probe: a mod reading back
game.surfaces[1].map_gen_settings.seed, four argument vectors, file seed
111111 against flag seed 222222.

--map-gen-settings is NOT required. Two arms generated a map, loaded the
mod and produced a dump with no settings file at all. 'Always passed' was
habit in the consumer repos, not a rule of the game, so the CLI now passes
it only when a caller supplies one.

--map-gen-seed OVERRIDES the file's seed: file 111111 plus flag 222222
gives a surface seed of 222222, and a file-only arm rules out the file
being ignored in general. So writing both channels from one field is now
the only shape that cannot bite - a tool writing only the file would be
silently overridden by a caller's flag.

Also records what that measurement found in MapWebUI itself: its settings
file's seed field has never done anything, because the harness always
passes the flag too. No fixture is wrong, but mapGenOverrides silently
discards a seed passed through it. Tracked on that repo's #232.

* Measure --instrument-mod, and drop the prelude's event registration

Ran a four-file probe mod on 2.1.14 logging from every stage.

--instrument-mod is real and gives earlier hooks: instrument-data.lua at
0.045s against data.lua at 0.129s, and instrument-control.lua toplevel
before control.lua toplevel. Without the flag neither instrument file
loads at all.

But instrument-control.lua's script.on_init handler NEVER fired, because
control.lua registered one too and the later registration replaced it. So
Instrument Mode hands a probe an earlier hook whose event registration the
consumer then silently destroys. Not adopting it.

That also turns the single-handler rule from an inference into a
measurement, which matters because the whole contamination design rested
on it.

The better answer came out of the same run: helpers.write_file works at
plain control.lua toplevel with no event, and script.active_mods is
populated there and includes the probe's own mod. So the prelude is now
one line with no event registration and no collision surface, replacing
the self-cancelling on_nth_tick. A test asserts the absence of on_init,
on_nth_tick and on_event so it cannot regress.

Also points the new repo at FactoryGameFan, following today's move.

Refs #82.

* Narrow the toplevel finding, and sharpen the Instrument Mode warning

Two corrections from the FactorioMapWebUI session.

The toplevel write is for metadata, not sampling. game.surfaces[1] does
not exist at control-stage toplevel, so anything calling
calculate_tile_properties or get_tile still needs on_init. Those
registrations are load-bearing rather than habit, and the two
requirements compose: the prelude registers nothing, a sampling probe
registers exactly one on_init, and nothing contests the slot. The spec
now says not to read the finding as 'probes should stop using on_init',
because that is the wrong lesson the earlier wording invited.

Instrument Mode is actively dangerous rather than merely unhelpful: a
tool built on it works on every probe that does not register on_init and
fails silently on every probe that does. Keeping the entry despite not
adopting the feature, since a measured destruction of the handler is a
stronger claim than an inference from the docs.

* Close the org-move open question with what the move taught

The same document already said, under Repo setup, that all six repos moved
to FactoryGameFan. Open question 1 still called it undecided.

Record the answer rather than deleting the question. GitHub documents what
happens to a fork on deletion and on detachment, but never on transfer, and
that gap was the stated blocker. A transfer rewrites a cross-repo pull
request's head to the new owner and keeps the pull request - verified on
PR #10, which now reads head: FactoryGameFan:main and is still closed with
both comments intact. It was closed before the transfer, so this covers the
closed case only.

Also note the two things that bite afterwards: the transfer API is
asynchronous, and old wormeyman URLs 301 correctly, so stale references
survive without complaining.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Jij2RRpAh3qNARvhxM3PUP

* Correct three things the first real run disproved

Build-order steps 1 to 3 are built, and running them against a real 2.1.14
install contradicted this document three times.

The sentinel is on stdout. Factorio writes nothing to stderr at all: a
control-stage error("DUMPED-OK"), a data-stage error and an unknown command
line flag all printed to stdout with stderr at zero bytes. sentinelSeen was
false on every real run, which is worse than not reporting it, because a
create run keys success off the dump existing and the sentinel is the only
thing that catches a probe which wrote its dump and then died.

Omission in mod-list.json means enabled. The game rewrites the file and adds
back every bundled mod it does not mention. A file naming only base came
back naming five, and all five loaded. A control arm settled both halves at
once: three mods listed as disabled stayed out, while recycler, unmentioned
in the same file, was added and loaded. So an empty mod directory keeps out
user mods and nothing else.

Both defects were invisible to sixty unit tests, because the fake game
encoded the same wrong belief as the code. That is the part worth carrying
into the remaining plans, so the testing section now says it outright.

Two claims survived and are now measured: --dump-data does honour write-data
for script-output, which closes an open item, and --create still needs no
settings file.

The plan's task bodies are left alone, since they record what was planned. A
corrections section names the three tasks that carry a wrong premise, and
flags for plan 2 that the byte-for-byte fixture comparison only holds
against the default mod set.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Jij2RRpAh3qNARvhxM3PUP

* Plan: the trimmer and the byte-for-byte gate (plan 2 of 4)

Covers build-order step 4, which is the gate on whether the shared tool can
replace tools/capture-factorio-oracle.sh.

Written after spiking the risky parts rather than guessing at them, so four
things the plan would otherwise have had to hedge on are settled:

The capture half is already done. A dump-data run through the new tool
produced a 28 MB data-raw-dump.json in 2.9 seconds, and feeding that to the
existing Python trimmer reproduced the committed fixture byte for byte. Only
the trim half is left.

The port is achievable. A sixty-line Rust spike over the real dump produced
entities and modules blocks byte identical to the fixture. The float
formatting the spec called the trap is not one here: serde_json's printer
emits shortest round-trip, same as Python's repr.

But serde_json's number PARSER is one ULP out on Factorio's long decimal
expansions, in both directions, while Rust's own f64::from_str agrees with
CPython bit for bit. Round-tripping the whole 25 MB dump found 9,744
differing lines, none in a field the fixture keeps. So it is latent, and
cheap to fix now. Task 4 does.

captureInfo.loadedMods cannot come from the active-mods prelude, because the
fixture lists core and script.active_mods never reports it. Task 7 reads it
off the game's stdout instead.

The #83 conflict plan 1 flagged is handled by ordering: port the order bug
first, prove the port with the acceptance test, then fix it as its own
change. A create probe already showed the game agrees with order today, so
that fix provably changes no bytes - and an acceptance test asserts it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Jij2RRpAh3qNARvhxM3PUP

* Correct the capture's source count, and name its replacement

The table said four sources and listed data/changelog.txt. The capture reads
three, and there is no changelog handling anywhere in tools/. The changelog
is a research source a human reads; it had been written into the table as
though it were automated.

Records that FactoryGameFan/factorio-oracle now reproduces this fixture byte
for byte, with an acceptance test that proves it against a real 2.1.14
install. The script stays: the agreed migration rule is new probes only.

Two things that tool measured, written down where they matter. An empty mod
directory keeps out user mods and nothing else - Factorio rewrites
mod-list.json and re-enables every bundled mod the file omits, so the
base-only list this script writes still loads all six. And the fixture's
directions come from `order`, which is a documentation index rather than the
runtime value, so a note now sits beside the runtime-api.json row instead of
that row implying it publishes numbers. Issue #83.

--help printed a hardcoded line range, so growing the header truncated it
mid-sentence. It now prints the whole leading comment block.

Verified: --check still reports the fixture up to date against 2.1.14, and
--help reaches the Usage section again.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Jij2RRpAh3qNARvhxM3PUP

---------

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