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:
2026-08-29 00:49:53 +02:00
co-authored by Claude Opus 5
parent d82b7e43a4
commit da331e955c
6 changed files with 159 additions and 301 deletions
+90 -50
View File
@@ -1,31 +1,99 @@
// ABOUT ME
// Components merged into the Radiator. They receive the three network values
// this mod broadcasts and combine them into a single gate, AutoPowerToggle,
// which mod.rules then substitutes for the radiator's vanilla PowerToggle on
// the parts of the radiator that actually dissipate heat.
//
// Each broadcast arrives as a number, 1 or 0, summarised across the network by
// a NetworkValueReceiver and turned back into a toggle by a threshold at 0.5.
// A network with no broadcaster at all yields 0, which is why a ship without
// thermal batteries or thermal launchers keeps vanilla behaviour.
// Components merged into the Radiator. They decide whether this radiator should
// vent or hold, and combine that into a single gate, AutoPowerToggle, which
// mod.rules substitutes for the radiator's vanilla PowerToggle on the parts of
// the radiator that actually dissipate heat.
//
// The player's manual on/off switch (PowerToggle) is preserved and still wins:
// a radiator the player has switched off stays off. The automation can only
// idle a radiator the player has left on.
//
// A radiator vents when ANY of these hold:
// - the network is not asking it to hold (no battery, or battery full)
// A radiator vents when EITHER:
// - the heat network is running out of room, or
// - no launcher on the network is using thermal ammunition
// - some part is overflowing heat right now (safety net, overrides all)
//
// The design is fail-safe: holding requires positive evidence that the network
// has room to spare. Anything missing, disconnected or unmeasurable results in
// venting, which is vanilla behaviour.
AutoRad_HoldSignal
// --- Is the heat network running out of room? -------------------------------
//
// Measured locally, exactly the way the heat exchanger decides whether it can
// still push heat into the network (heat_exchanger.rules: IsNetworkHeatNonFull
// reads a bare NetworkResourceInput). A NetworkResourceInput's value is how much
// the network can still accept.
//
// Reading a thermal battery's own HeatStorage does NOT work for this. That
// storage is wrapped by a NetworkResourceStore, and v2.0.0 held heat forever --
// batteries visibly full, parts cooking -- because a local percentage threshold
// on it never crossed. This measures the network itself instead, from the part
// that acts on the answer, so no cross-part broadcast is involved at all.
//
// Amounts are resource units: 1 status heat = 1000 resource units
// (statuses/heat/heat.rules: STATUS_TO_RESOURCE_RATIO).
AutoRad_NetworkSpace
{
Type = NetworkValueReceiver
Categories = [ auto_radiator_hold ]
Mode = Max
AllowUncategorized = false
Type = NetworkResourceInput
ResourceType = heat
Mode = DistributeProportionallyByAvailable
}
// Under ~2000 status heat of headroom left: vent.
AutoRad_SpaceLow
{
Type = ThresholdToggle
ValueFrom = AutoRad_NetworkSpace
MaxAmount = 2000000
}
// Back above ~6000 status heat of headroom: safe to bank heat again.
AutoRad_SpaceAmple
{
Type = ThresholdToggle
ValueFrom = AutoRad_NetworkSpace
MinAmount = 6000000
}
AutoRad_LowTrigger
{
Type = ToggleTrigger
Toggle = AutoRad_SpaceLow
TriggerWhenOn = true
TriggerWhenOff = false
}
AutoRad_AmpleTrigger
{
Type = ToggleTrigger
Toggle = AutoRad_SpaceAmple
TriggerWhenOn = true
TriggerWhenOff = false
}
// The deadband, so radiators do not chatter at the threshold.
AutoRad_VentLatch
{
Type = TriggeredToggle
OnTrigger = AutoRad_LowTrigger
OffTrigger = AutoRad_AmpleTrigger
}
// OR the raw low reading back in, so a ship that loads already out of room
// vents at once instead of waiting for a crossing that never comes.
AutoRad_MustVent
{
Type = MultiToggle
Toggles = [ AutoRad_VentLatch, AutoRad_SpaceLow ]
Mode = Any
}
// --- Is anything actually spending heat as ammunition? ----------------------
//
// Broadcast by the missile launcher over the heat network. Confirmed working in
// game: with a launcher on Thermal Canister ammo this reads 1, and with the
// launcher on anything else it reads 0 and radiators behave exactly as vanilla.
AutoRad_ThermalAmmoSignal
{
Type = NetworkValueReceiver
@@ -34,26 +102,6 @@ AutoRad_ThermalAmmoSignal
AllowUncategorized = false
}
AutoRad_ForceVentSignal
{
Type = NetworkValueReceiver
Categories = [ auto_radiator_force_vent ]
Mode = Max
AllowUncategorized = false
}
// "value is not zero", written the way vanilla writes it (the thermal pump's
// UtilizationToggle). The signals are only ever 0 or 1, so this is equivalent
// to a 0.5 threshold while sticking to a form the game demonstrably honours.
AutoRad_IsHolding
{
Type = ThresholdToggle
ValueFrom = AutoRad_HoldSignal
MinAmount = 0
MaxAmount = 0
Invert = true
}
AutoRad_ThermalAmmoInUse
{
Type = ThresholdToggle
@@ -63,30 +111,22 @@ AutoRad_ThermalAmmoInUse
Invert = true
}
AutoRad_ForceVent
{
Type = ThresholdToggle
ValueFrom = AutoRad_ForceVentSignal
MinAmount = 0
MaxAmount = 0
Invert = true
}
// --- The gate ---------------------------------------------------------------
AutoRad_VentAllowed
{
Type = MultiToggle
Toggles
[
{ Toggle = AutoRad_IsHolding; Invert = true }
AutoRad_MustVent
{ Toggle = AutoRad_ThermalAmmoInUse; Invert = true }
AutoRad_ForceVent
]
Mode = Any
}
// The replacement gate. PowerToggle is the radiator's vanilla UIToggle, so the
// player's switch remains the master control: it can turn a radiator off, and
// the automation can only idle one the player has left on.
// PowerToggle is the radiator's vanilla UIToggle, so the player's switch remains
// the master control: it can turn a radiator off, and the automation can only
// idle one the player has left on.
AutoPowerToggle
{
Type = MultiToggle