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
197 lines
8.9 KiB
Markdown
197 lines
8.9 KiB
Markdown
# Auto Radiator
|
||
|
||
A Cosmoteer mod. Radiators stop dumping heat the ship still wants.
|
||
|
||
## The problem
|
||
|
||
Thermal Canister Missiles are built from the ship's own heat. The game says so
|
||
itself, in `Data/strings/en.rules`:
|
||
|
||
> Manufactures guided Thermal Canister Missiles that take thermal energy from
|
||
> their own ship's heat pipe network and inflict it upon the target.
|
||
|
||
The launcher charges each missile from the heat network — `HEAT_PER_MISSILE =
|
||
8500`, two missiles buffered per launcher. A radiator pulls from that same
|
||
network, through a component that is behaviourally identical to the launcher's:
|
||
|
||
| | Radiator | Thermal launcher |
|
||
|---|---|---|
|
||
| Type | `NetworkResourceOutput` | `NetworkResourceOutput` |
|
||
| Mode | `DistributeProportionallyByAvailable` | `DistributeProportionallyByAvailable` |
|
||
| Categories | `[ thermal_storage ]` | `[ thermal_storage ]` |
|
||
|
||
`Data/ships/priorities.rules` defines priorities for crew and supply only —
|
||
nothing for heat. So the two compete flat, proportionally, and a radiator
|
||
running constantly is spending your ammunition to make steam.
|
||
|
||
## Why holding heat is safe
|
||
|
||
Heat storage being full does not hurt you. Damage comes only from *overflow*.
|
||
Every overclocked part routes its heat through this
|
||
(`Data/ships/base_part_overclock.rules`):
|
||
|
||
```
|
||
HeatDistributionStorage
|
||
{
|
||
Type = MultiResourceStorage
|
||
ResourceStorages = [NetworkHeatInput, OverflowHeatStorage]
|
||
AddMode = InOrder
|
||
}
|
||
```
|
||
|
||
`InOrder` means the network is tried first, and `OverflowHeatStorage` only
|
||
receives what the network refuses. Overflow then applies the `cosmoteer.heat`
|
||
status to the producing part's own tiles.
|
||
|
||
From `Data/statuses/heat/heat.rules`, that status does nothing whatsoever below
|
||
**350** per tile (`RequireValueRangeForTick = [350, Infinity]`). Fire chance
|
||
ramps from 350 to 2500; thermal damage begins at 700.
|
||
|
||
An overclocked large reactor produces 2750 heat/sec over its 4×4 footprint —
|
||
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 }`).
|
||
|
||
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.
|
||
|
||
## What the mod does
|
||
|
||
A radiator vents when **either**:
|
||
|
||
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.
|
||
|
||
Your manual radiator on/off switch still overrides both. The automation can only
|
||
idle a radiator you have left switched on.
|
||
|
||
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/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
|
||
|
||
```sh
|
||
./install.sh
|
||
```
|
||
|
||
Then enable **Auto Radiator** in the game's Mods menu. `./install.sh --uninstall`
|
||
removes it. If the script cannot find your Mods folder, set `COSMOTEER_MODS_DIR`.
|
||
|
||
## Tests
|
||
|
||
```sh
|
||
python3 tests/test_mod.py
|
||
```
|
||
|
||
Cosmoteer applies mod actions to vanilla data at load time. Some mistakes crash
|
||
the game on startup — an unresolvable path, a misspelled component `Type` — and
|
||
the suite catches those by parsing the installed game's `.rules` tree offline.
|
||
|
||
The more dangerous mistakes do not crash anything. They load perfectly and
|
||
silently do nothing; see **Status** below for the four this project hit and the
|
||
guard for each. The suite also self-checks its own parser against known vanilla
|
||
facts first, so a broken parser fails loudly instead of passing everything.
|
||
|
||
Set `COSMOTEER_DATA` to point at a different install, and `COSMOTEER_ASSEMBLY`
|
||
for the schema checks.
|
||
|
||
## How to tell it is working
|
||
|
||
There is no separate "auto" mode and nothing to reset. The radiator's on/off
|
||
switch is an AND gate:
|
||
|
||
- switch **off** -> radiator is off, and stays off. The mod never turns it back on.
|
||
- switch **on** -> the automation decides, moment to moment.
|
||
|
||
An auto-idled radiator shows the "powered off" indicator icon and stops emitting
|
||
its steam plume. That icon on a radiator you did *not* switch off is the mod
|
||
holding heat back for your launchers.
|
||
|
||
## Status
|
||
|
||
**Partly confirmed.** Against game data 0.30.4c, as of 2026-08-29:
|
||
|
||
- 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 |
|
||
|---|---|
|
||
| `NetworkToggle` / `NetworkToggleReceiver` carry no signal on a physical ship (every vanilla use sets `IncludeInPhysical = false`) | `test_no_blueprint_only_signal_types`, plus `test_vanilla_marks_those_types_blueprint_only` so the premise cannot go stale |
|
||
| 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 |
|
||
|
||
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.
|
||
|
||
`tools/dotnet_enum.py` reads component schemas and enum members straight out of
|
||
`Cosmoteer.dll`, which is how the field and type questions got settled. It is
|
||
useful on its own for any Cosmoteer modding:
|
||
|
||
```sh
|
||
python3 tools/dotnet_enum.py /path/to/Cosmoteer.dll PartThresholdToggleRules
|
||
python3 tools/dotnet_enum.py /path/to/Cosmoteer.dll MultiToggleMode
|
||
```
|
||
|
||
Version 1.4.0 in the git history carries a probe build that makes a radiator
|
||
report its own internal state through indicator icons, if this ever needs
|
||
debugging again.
|
||
|
||
## Possible additions
|
||
|
||
- A per-radiator UI toggle for auto/manual, which needs a new `ChoiceID` wired
|
||
into `Data/gui/game/parts/part_toggles.rules`.
|
||
- Making the 98% / 94% band configurable.
|