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:
@@ -1,66 +0,0 @@
|
||||
// ABOUT ME
|
||||
// Components merged into base_part_overclock, the shared base for every
|
||||
// overclockable part. They form the mod's safety net.
|
||||
//
|
||||
// Why it exists: the threshold band on the thermal battery is a prediction. It
|
||||
// can be wrong -- if the ship generates heat faster than its radiators can
|
||||
// clear it, or if heat pipe topology cannot deliver to the batteries quickly
|
||||
// enough, heat overflows regardless of what percentage the band is set to.
|
||||
//
|
||||
// OverflowHeatStorage is the engine's own ground truth for that: heat only ever
|
||||
// reaches it when the network has already refused the heat, which is the exact
|
||||
// moment the part starts cooking itself. Any part in that state forces every
|
||||
// radiator on the network to vent, ignoring the band entirely.
|
||||
//
|
||||
// The timer holds the signal for a few seconds so a stream of per-tick overflow
|
||||
// events becomes one steady "vent now" rather than a flicker.
|
||||
//
|
||||
// Carried as a NetworkValue rather than a NetworkToggle: vanilla marks every
|
||||
// NetworkToggle IncludeInPhysical = false, so they do not carry a signal on a
|
||||
// real ship.
|
||||
|
||||
AutoRad_OverflowTimer
|
||||
{
|
||||
Type = Timer
|
||||
Duration = 3
|
||||
StartExpired = true
|
||||
StartTrigger = OverflowHeatStorage
|
||||
ResetTrigger = OverflowHeatStorage
|
||||
}
|
||||
|
||||
AutoRad_One { Type = StaticValue; Value = 1 }
|
||||
AutoRad_Zero { Type = StaticValue; Value = 0 }
|
||||
|
||||
AutoRad_OverflowValue
|
||||
{
|
||||
Type = ToggleValueSelector
|
||||
Toggle = AutoRad_OverflowTimer
|
||||
ToggleOnValue = AutoRad_One
|
||||
ToggleOffValue = AutoRad_Zero
|
||||
}
|
||||
|
||||
AutoRad_NetworkedForceVent
|
||||
{
|
||||
Type = NetworkValue
|
||||
ValueSource = AutoRad_OverflowValue
|
||||
Categories = [ auto_radiator_force_vent ]
|
||||
IncludeInBlueprints = false
|
||||
}
|
||||
|
||||
// Routes the broadcast onto the heat network. NetworkHeatInput is what this
|
||||
// base's thermal ports anchor on (BASE_THERMAL_PORT: RouteSource =
|
||||
// &../HEAT_NETWORK_INPUT).
|
||||
AutoRad_Router
|
||||
{
|
||||
Type = NetworkRouter
|
||||
RouteGenerators
|
||||
[
|
||||
{
|
||||
Type = Bidirectional
|
||||
Routes
|
||||
[
|
||||
[NetworkHeatInput, AutoRad_NetworkedForceVent, 0]
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -1,113 +0,0 @@
|
||||
// ABOUT ME
|
||||
// Components merged into the Thermal Battery. The battery is the ship's heat
|
||||
// gauge, so it is what decides whether the heat network has surplus worth
|
||||
// venting, and broadcasts that decision to every radiator on the network.
|
||||
//
|
||||
// The broadcast is a NetworkValue carrying 1 (hold) or 0 (vent), not a
|
||||
// NetworkToggle. Every NetworkToggle in vanilla sets IncludeInPhysical = false
|
||||
// -- they exist only for blueprint stat display and do not carry a signal on a
|
||||
// real ship. NetworkValue has no such restriction and is what the thermal
|
||||
// amplification pump uses to drive actual physical behaviour.
|
||||
//
|
||||
// Polarity is deliberate: the battery broadcasts "HOLD", not "vent". A ship
|
||||
// with no thermal battery has no broadcaster, every radiator reads 0, and
|
||||
// radiators behave exactly as in vanilla.
|
||||
//
|
||||
// Heat storage fullness causes no damage on its own -- damage only occurs when
|
||||
// the network refuses heat and it overflows onto the producing part
|
||||
// (base_part_overclock.rules: HeatDistributionStorage adds InOrder, network
|
||||
// first, OverflowHeatStorage second). So the band sits high on purpose, keeping
|
||||
// the network full as a deep ammunition reserve for Thermal Canister Missiles.
|
||||
//
|
||||
// All names are prefixed to avoid colliding with vanilla component names.
|
||||
|
||||
// Vent when the battery is essentially full.
|
||||
AutoRad_VentCeiling
|
||||
{
|
||||
Type = ThresholdToggle
|
||||
ValueFrom = HeatStorage
|
||||
MinAmount = 98%
|
||||
}
|
||||
|
||||
// Stop venting once there is real headroom again.
|
||||
AutoRad_VentFloor
|
||||
{
|
||||
Type = ThresholdToggle
|
||||
ValueFrom = HeatStorage
|
||||
MaxAmount = 94%
|
||||
}
|
||||
|
||||
AutoRad_CeilingTrigger
|
||||
{
|
||||
Type = ToggleTrigger
|
||||
Toggle = AutoRad_VentCeiling
|
||||
TriggerWhenOn = true
|
||||
TriggerWhenOff = false
|
||||
}
|
||||
|
||||
AutoRad_FloorTrigger
|
||||
{
|
||||
Type = ToggleTrigger
|
||||
Toggle = AutoRad_VentFloor
|
||||
TriggerWhenOn = true
|
||||
TriggerWhenOff = false
|
||||
}
|
||||
|
||||
// The deadband: latches on at the ceiling, stays on until the floor is reached.
|
||||
// Without this the radiators would chatter on and off at the threshold.
|
||||
AutoRad_VentLatch
|
||||
{
|
||||
Type = TriggeredToggle
|
||||
OnTrigger = AutoRad_CeilingTrigger
|
||||
OffTrigger = AutoRad_FloorTrigger
|
||||
}
|
||||
|
||||
// OR the raw ceiling back in, so a ship that loads already above the ceiling
|
||||
// vents immediately instead of waiting for a threshold crossing that never comes.
|
||||
AutoRad_VentActive
|
||||
{
|
||||
Type = MultiToggle
|
||||
Toggles = [ AutoRad_VentLatch, AutoRad_VentCeiling ]
|
||||
Mode = Any
|
||||
}
|
||||
|
||||
AutoRad_One { Type = StaticValue; Value = 1 }
|
||||
AutoRad_Zero { Type = StaticValue; Value = 0 }
|
||||
|
||||
// 1 while the network should hold its heat, 0 while it should vent.
|
||||
AutoRad_HoldValue
|
||||
{
|
||||
Type = ToggleValueSelector
|
||||
Toggle = AutoRad_VentActive
|
||||
ToggleOnValue = AutoRad_Zero
|
||||
ToggleOffValue = AutoRad_One
|
||||
}
|
||||
|
||||
AutoRad_NetworkedHold
|
||||
{
|
||||
Type = NetworkValue
|
||||
ValueSource = AutoRad_HoldValue
|
||||
Categories = [ auto_radiator_hold ]
|
||||
IncludeInBlueprints = false
|
||||
}
|
||||
|
||||
// A network component is only actually attached to the network if a route
|
||||
// reaches it. The part's NetworkPort anchors on one component -- here
|
||||
// NetworkedHeatStorage -- and a NetworkRouter bridges that anchor to every
|
||||
// other network component on the part. The thermal battery ships no router,
|
||||
// because vanilla has nothing else to reach, so the broadcast above needs one
|
||||
// or it is never connected to anything and reads as absent.
|
||||
AutoRad_Router
|
||||
{
|
||||
Type = NetworkRouter
|
||||
RouteGenerators
|
||||
[
|
||||
{
|
||||
Type = Bidirectional
|
||||
Routes
|
||||
[
|
||||
[NetworkedHeatStorage, AutoRad_NetworkedHold, 0]
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user