fix: measure the network, not a battery's storage
v2.0.0 held heat forever: thermal batteries visibly full, parts taking heat damage, radiators still idle. That is the dangerous direction. The ceiling read a thermal battery's own HeatStorage with a percentage threshold. That storage is wrapped by a NetworkResourceStore, and the local read never crossed 98% however full the network actually got, so the battery broadcast "hold" permanently. Measure the network's remaining room directly instead, from the radiator itself, with a bare NetworkResourceInput -- the same way the heat exchanger decides whether it can still push heat. Vent below ~2000 status heat of headroom, hold again above ~6000. Absolute headroom is also the better metric: what matters is room against the generation rate, not a percentage of whatever storage happens to be installed. Polarity is now fail-safe. Holding requires positive evidence of headroom, so anything missing or unmeasurable vents rather than holds -- the opposite of the failure above. This deletes both broadcasters. The thermal battery and base_part_overclock are no longer patched at all, and the mod touches two parts instead of four. The launcher ammo signal stays; it is confirmed working in both directions. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011oLWY1G6S8U35bi2FxVWni
This commit is contained in:
@@ -52,52 +52,50 @@ about **172 status/sec per tile** while fully overflowing, so roughly **2
|
||||
seconds of continuous, total overflow** before fire risk even arms. Radiators
|
||||
react within **0.5 s** (`HeatDissipationConverter { Interval = 0.5 }`).
|
||||
|
||||
At a 98% ceiling on a single 15000-capacity thermal battery, the overshoot
|
||||
during that reaction window is about 625 status heat spread over 16 tiles —
|
||||
roughly **39 per tile**, against a threshold of 350. Holding the network near
|
||||
full is safe with a wide margin, and every percent of ceiling given up is
|
||||
ammunition thrown away for nothing.
|
||||
With ~2000 status heat of headroom left when venting starts, one overclocked
|
||||
large reactor takes about 0.7 s to consume it, against a 0.5 s radiator reaction
|
||||
time — so the overshoot lands well under the 350-per-tile threshold at which
|
||||
anything begins to happen. Holding the network near full is safe with a wide
|
||||
margin, and headroom given up beyond that is ammunition thrown away for nothing.
|
||||
|
||||
The genuine constraint is not the threshold. It is total radiator throughput
|
||||
(1100/sec each) versus sustained generation. If you are short on radiators you
|
||||
overflow continuously and no threshold value saves you — which is what the
|
||||
safety net below is for.
|
||||
overflow continuously and no threshold value saves you.
|
||||
|
||||
## What the mod does
|
||||
|
||||
Radiators vent when **any** of these is true:
|
||||
A radiator vents when **either**:
|
||||
|
||||
1. The heat network is not asking them to hold — the thermal battery is at or
|
||||
above 98% full (dropping the hold again at 94%, a deadband so radiators do
|
||||
not chatter at the threshold).
|
||||
2. No launcher on the network is set to Thermal Canister Missiles. If you are
|
||||
not spending heat as ammunition there is nothing to hoard, so radiators
|
||||
1. **The heat network is running out of room.** The radiator measures this
|
||||
itself, with a bare `NetworkResourceInput` — the same way the heat exchanger
|
||||
decides whether it can still push heat (`heat_exchanger.rules`:
|
||||
`IsNetworkHeatNonFull`). Vent below ~2000 status heat of remaining headroom,
|
||||
hold again above ~6000, a deadband so radiators do not chatter.
|
||||
2. **No launcher on the network is set to Thermal Canister Missiles.** If you
|
||||
are not spending heat as ammunition there is nothing to hoard, so radiators
|
||||
behave exactly as in vanilla.
|
||||
3. **Safety net:** some part is actually overflowing heat right now. This reads
|
||||
the engine's own `OverflowHeatStorage`, so it fires the moment the network
|
||||
genuinely refuses heat — regardless of thresholds, pipe topology, or whether
|
||||
your radiator capacity was ever sufficient. Latched for 3 seconds so a
|
||||
stream of per-tick overflow events becomes one steady signal.
|
||||
|
||||
Your manual radiator on/off switch still overrides all of it. The automation can
|
||||
only idle a radiator you have left switched on.
|
||||
Your manual radiator on/off switch still overrides both. The automation can only
|
||||
idle a radiator you have left switched on.
|
||||
|
||||
Ships with no thermal battery have no broadcaster, so nothing changes for them.
|
||||
The signal polarity is deliberately "hold", not "vent", so that the absence of
|
||||
any part of this mod's machinery always falls back to vanilla behaviour.
|
||||
The design is **fail-safe**: holding heat requires positive evidence that the
|
||||
network has room to spare. Anything missing, disconnected or unmeasurable ends
|
||||
up venting, which is vanilla behaviour. Headroom is measured in absolute heat
|
||||
rather than as a percentage, because what actually matters is headroom against
|
||||
your generation rate — 2000 status heat is under a second of one overclocked
|
||||
large reactor.
|
||||
|
||||
No stat or number is changed. Only *when* radiators run.
|
||||
|
||||
## Layout
|
||||
|
||||
```
|
||||
mod.rules manifest + patch actions
|
||||
components/thermal_battery_vent.rules decides when there is surplus
|
||||
components/launcher_ammo_signal.rules reports thermal ammo in use
|
||||
components/overclock_overflow_signal.rules safety net, reports real overflow
|
||||
components/radiator_auto.rules combines the signals into one gate
|
||||
tests/ offline validation suite
|
||||
install.sh validate + copy into the Mods folder
|
||||
mod.rules manifest + patch actions
|
||||
components/radiator_auto.rules measures network headroom, decides, acts
|
||||
components/launcher_ammo_signal.rules reports thermal ammo in use
|
||||
tests/ offline validation suite
|
||||
tools/dotnet_enum.py reads component schemas out of Cosmoteer.dll
|
||||
install.sh validate + copy into the Mods folder
|
||||
```
|
||||
|
||||
## Install
|
||||
@@ -141,11 +139,24 @@ holding heat back for your launchers.
|
||||
|
||||
## Status
|
||||
|
||||
Working, confirmed in game on 2026-08-29 against game data version 0.30.4c.
|
||||
**Partly confirmed.** Against game data 0.30.4c, as of 2026-08-29:
|
||||
|
||||
Getting there took four builds that each loaded without a single error and did
|
||||
nothing, for four different reasons. Cosmoteer reports none of them, so each one
|
||||
now has a mutation-verified test:
|
||||
- Confirmed: the gate closes and idles a radiator when thermal ammo is in use.
|
||||
- Confirmed: with no launcher on thermal ammo, behaviour is vanilla.
|
||||
- **Not yet confirmed: the release path.** v2.0.0 held heat *forever* — thermal
|
||||
batteries visibly full, parts taking heat damage, radiators still idle.
|
||||
|
||||
That failure was the dangerous direction, and it came from reading the wrong
|
||||
thing. v2.0.0 put a percentage threshold on a thermal battery's own
|
||||
`HeatStorage`, which is wrapped by a `NetworkResourceStore`; that local read
|
||||
never crossed its ceiling however full the network got. v2.1.0 measures the
|
||||
network's remaining room directly instead, from the radiator, using the same
|
||||
bare `NetworkResourceInput` the heat exchanger uses — and the polarity is now
|
||||
fail-safe, so an unmeasurable signal vents rather than holds.
|
||||
|
||||
Getting this far took five builds that each loaded without a single error and
|
||||
misbehaved silently. Cosmoteer reports none of these, so each has a
|
||||
mutation-verified test:
|
||||
|
||||
| Silent failure | Guard |
|
||||
|---|---|
|
||||
@@ -153,16 +164,14 @@ now has a mutation-verified test:
|
||||
| A field name that does not exist in the component's real schema is ignored rather than reported | `test_component_fields_exist_in_schema`, read from the assembly's own metadata |
|
||||
| A network component no route reaches never attaches, and reads as absent forever | `test_network_components_are_routed` |
|
||||
| An `Overrides` value naming a component that is not there reverts the gate to nothing | `test_override_values_name_real_components` |
|
||||
| A percentage threshold on a network-wrapped `ResourceStorage` never crosses, however full the network is | no test — avoid the pattern; measure the network, not a part's storage |
|
||||
|
||||
The third was the actual cause. A network component is only attached if a
|
||||
**route** reaches it: the part's `NetworkPort` anchors on one component, and a
|
||||
`NetworkRouter` bridges that anchor to every other network component on the
|
||||
part. That is how vanilla connects the radiator's own
|
||||
`NetworkedThermalDissipationLoadValue`, and how the amplification pump reaches
|
||||
the resonance beam turret. Routes now run from each part's anchor —
|
||||
`NetworkedHeatBuffer` on the radiator through its existing `Router`, plus a new
|
||||
`NetworkRouter` on the thermal battery, the missile launcher and
|
||||
`base_part_overclock`, none of which ships one.
|
||||
Routing was the cause of the first three builds doing nothing. A network
|
||||
component is only attached if a **route** reaches it: the part's `NetworkPort`
|
||||
anchors on one component, and a `NetworkRouter` bridges that anchor to every
|
||||
other network component on the part. That is how vanilla connects the radiator's
|
||||
own `NetworkedThermalDissipationLoadValue`, and how the amplification pump
|
||||
reaches the resonance beam turret. Many parts ship no router at all.
|
||||
|
||||
None of this is documented: the wiki's modding pages cover no network components
|
||||
at all, so the game's own `.rules` files are the only reference.
|
||||
|
||||
Reference in New Issue
Block a user