Guard against enabling beacons and heat pipes together (temporary) - #10
Merged
Merged
Conversation
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
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>
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.
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.csAddHeatPipesand thePlanBeacons.*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
Implementation
OilFieldStore.ts) via watchers - rather than duplicating logic acrossBeaconForm.vueandHeatPipeForm.vue.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 correctlyfalse.Verification
In-browser (Playwright) walkthrough of all paths:
npm run build(vue-tsc + vite) stays green.🤖 Generated with Claude Code