feat: v4.0.0 -- radiator switch picks the mode, ammo gate removed
ON vents constantly (vanilla). OFF banks heat and vents only when the network is nearly full. The gate is now PowerToggle OR MustVent. The launcher's thermal-ammo broadcast is gone: it did not follow the ammo selection live, and the switch states the player's intent directly. Only the radiator is patched now. New test_gate_is_power_or_must_vent pins the semantics. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011oLWY1G6S8U35bi2FxVWni
This commit is contained in:
@@ -65,23 +65,23 @@ overflow continuously and no threshold value saves you.
|
||||
|
||||
## What the mod does
|
||||
|
||||
A radiator vents when **either**:
|
||||
The radiator's own on/off switch picks a mode:
|
||||
|
||||
1. **The heat network is running out of room.** The radiator measures this
|
||||
itself, with a bare `NetworkResourceInput` — the same component an
|
||||
overclocked part pushes its heat through (`base_part_overclock.rules`:
|
||||
`NetworkHeatInput`), so it sees exactly what a producer sees. Vent while
|
||||
less than **5%** of the network's heat storage is free, hold again once
|
||||
**10%** is free — a deadband so radiators do not chatter. Both numbers are
|
||||
named constants at the top of `components/radiator_auto.rules`.
|
||||
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.
|
||||
- **ON** — vanilla. The radiator vents constantly. You want the heat gone.
|
||||
- **OFF** — bank heat. The radiator idles while the heat network has room and
|
||||
vents only when it is nearly full: vent while less than **5%** of the
|
||||
network's heat storage is free, hold again once **10%** is free (a deadband
|
||||
so radiators do not chatter). The ship never overflows heat onto its own
|
||||
parts, and thermal batteries stay topped up as a deep missile reserve. An
|
||||
OFF radiator also releases its crew.
|
||||
|
||||
Your manual radiator on/off switch still overrides both. The automation can only
|
||||
idle a radiator you have left switched on.
|
||||
The radiator measures the network itself, with a bare `NetworkResourceInput`
|
||||
— the same component an overclocked part pushes its heat through
|
||||
(`base_part_overclock.rules`: `NetworkHeatInput`), so it sees exactly what a
|
||||
producer sees. Both thresholds are named constants in
|
||||
`components/radiator_auto.rules`.
|
||||
|
||||
The design is **fail-safe**: holding heat requires positive evidence that the
|
||||
The design is **fail-safe**: idling requires positive evidence that the
|
||||
network has room to spare. Anything missing, disconnected or unmeasurable ends
|
||||
up venting, which is vanilla behaviour.
|
||||
|
||||
@@ -97,8 +97,7 @@ No stat or number is changed. Only *when* radiators run.
|
||||
|
||||
```
|
||||
mod.rules manifest + patch actions
|
||||
components/radiator_auto.rules measures network headroom, decides, acts
|
||||
components/launcher_ammo_signal.rules reports thermal ammo in use
|
||||
components/radiator_auto.rules measures network headroom, gates the radiator
|
||||
tests/ offline validation suite
|
||||
tools/dotnet_enum.py reads component schemas out of Cosmoteer.dll
|
||||
tools/ildump.py disassembles component methods to readable CIL
|
||||
@@ -134,25 +133,25 @@ 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 a radiator **OFF** with heat storage low. It shows the "powered off"
|
||||
indicator and no steam plume while the network fills. As the heat gauge nears
|
||||
full the plume starts and the gauge stalls around 90–95%, with no overheat
|
||||
warning. Fire missiles, the gauge drops, the radiator goes quiet again.
|
||||
|
||||
- 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.
|
||||
Switch it **ON** and it runs constantly, exactly as in vanilla.
|
||||
|
||||
## Status
|
||||
|
||||
**Unconfirmed.** Against game data 0.30.4c, as of 2026-08-29:
|
||||
**Release path confirmed, switch semantics not yet.** Against game 0.30.4c on
|
||||
2026-08-29, v3.0.0 in a real fight: radiators idled while the network filled,
|
||||
vented as it neared full, the ship never overheated, and they went quiet again
|
||||
once missiles drained the reserve.
|
||||
|
||||
- Confirmed in game: the gate closes and idles a radiator when thermal ammo is
|
||||
in use, and with no launcher on thermal ammo behaviour is vanilla.
|
||||
- **Not yet confirmed: the release path** — that radiators start venting as the
|
||||
network fills. v3.0.0 is the first build whose threshold is on the right
|
||||
scale, and it has not been played yet.
|
||||
v4.0.0 changed only the gate: earlier builds required a launcher to broadcast
|
||||
"thermal ammo in use" over the heat network, which worked at load but did not
|
||||
follow the ammo selection live. The player's switch expresses the intent more
|
||||
directly, so the broadcast was removed and OFF now means "bank heat". That
|
||||
switch behaviour is the one thing v4.0.0 has not been played with yet.
|
||||
|
||||
### What the assembly says
|
||||
|
||||
@@ -223,6 +222,7 @@ 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`.
|
||||
- A third switch position (ON / BANK / OFF) so a radiator can be fully
|
||||
disabled, which needs a new `ChoiceID` wired into
|
||||
`Data/gui/game/parts/part_toggles.rules`.
|
||||
- Exposing the 5% / 10% band in the game UI rather than as file constants.
|
||||
|
||||
@@ -1,55 +0,0 @@
|
||||
// ABOUT ME
|
||||
// Components merged into the Missile Launcher. They broadcast, over the heat
|
||||
// network, whether this launcher is currently set to Thermal Canister Missiles.
|
||||
//
|
||||
// Why it exists: banking heat is only worth anything if something on the ship
|
||||
// actually spends heat as ammunition. On a ship with no thermal launchers,
|
||||
// holding the heat network near full buys nothing, so the radiators should just
|
||||
// behave as they do in vanilla. Radiators treat a received 0 as permission to
|
||||
// vent freely.
|
||||
//
|
||||
// ThermalMissilesToggle is the launcher's own vanilla mode toggle
|
||||
// (ModeToggle, Mode = MissileType, OnMode = 4), so this signal follows the
|
||||
// player's ammunition selection live, including mid-battle switches.
|
||||
//
|
||||
// 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_One { Type = StaticValue; Value = 1 }
|
||||
AutoRad_Zero { Type = StaticValue; Value = 0 }
|
||||
|
||||
AutoRad_ThermalAmmoValue
|
||||
{
|
||||
Type = ToggleValueSelector
|
||||
Toggle = ThermalMissilesToggle
|
||||
ToggleOnValue = AutoRad_One
|
||||
ToggleOffValue = AutoRad_Zero
|
||||
}
|
||||
|
||||
AutoRad_NetworkedThermalAmmo
|
||||
{
|
||||
Type = NetworkValue
|
||||
ValueSource = AutoRad_ThermalAmmoValue
|
||||
Categories = [ auto_radiator_ammo_user ]
|
||||
IncludeInBlueprints = false
|
||||
}
|
||||
|
||||
// Routes the broadcast onto the heat network. NetworkHeatOutputProxy is what
|
||||
// the launcher's thermal ports anchor on. Those ports are only operational in
|
||||
// thermal missile mode, so a launcher loaded with anything else is simply not
|
||||
// on the network -- which is the behaviour we want anyway.
|
||||
AutoRad_Router
|
||||
{
|
||||
Type = NetworkRouter
|
||||
RouteGenerators
|
||||
[
|
||||
{
|
||||
Type = Bidirectional
|
||||
Routes
|
||||
[
|
||||
[NetworkHeatOutputProxy, AutoRad_NetworkedThermalAmmo, 0]
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,20 +1,18 @@
|
||||
// ABOUT ME
|
||||
// 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.
|
||||
// Components merged into the Radiator. They decide whether this radiator must
|
||||
// vent right now, and combine that with the player's switch into a single gate,
|
||||
// AutoPowerToggle, which mod.rules substitutes for the 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.
|
||||
// The player's on/off switch (PowerToggle) chooses the mode:
|
||||
// ON -> vanilla. Vent constantly; the player wants the heat gone.
|
||||
// OFF -> bank heat for Thermal Canister Missiles. The radiator idles while
|
||||
// the heat network has room, and vents only when it is nearly full,
|
||||
// so the ship never overflows heat onto its own parts.
|
||||
//
|
||||
// A radiator vents when EITHER:
|
||||
// - the heat network is running out of room, or
|
||||
// - no launcher on the network is using thermal ammunition
|
||||
//
|
||||
// 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.
|
||||
// The gate is therefore PowerToggle OR MustVent. Fail-safe: idling requires
|
||||
// positive evidence that the network has room to spare. Anything missing,
|
||||
// disconnected or unmeasurable results in venting.
|
||||
|
||||
// --- Is the heat network running out of room? -------------------------------
|
||||
//
|
||||
@@ -88,48 +86,14 @@ AutoRad_MustVent
|
||||
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
|
||||
Categories = [ auto_radiator_ammo_user ]
|
||||
Mode = Max
|
||||
AllowUncategorized = false
|
||||
}
|
||||
|
||||
AutoRad_ThermalAmmoInUse
|
||||
{
|
||||
Type = ThresholdToggle
|
||||
ValueFrom = AutoRad_ThermalAmmoSignal
|
||||
MinAmount = 0
|
||||
MaxAmount = 0
|
||||
Invert = true
|
||||
}
|
||||
|
||||
// --- The gate ---------------------------------------------------------------
|
||||
|
||||
AutoRad_VentAllowed
|
||||
{
|
||||
Type = MultiToggle
|
||||
Toggles
|
||||
[
|
||||
AutoRad_MustVent
|
||||
{ Toggle = AutoRad_ThermalAmmoInUse; Invert = true }
|
||||
]
|
||||
Mode = Any
|
||||
}
|
||||
|
||||
// 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.
|
||||
//
|
||||
// ON always vents (vanilla). OFF vents only while MustVent says the network is
|
||||
// nearly full. Indicators and the steam plume follow this gate, so a radiator
|
||||
// that is OFF but venting looks like a working radiator.
|
||||
AutoPowerToggle
|
||||
{
|
||||
Type = MultiToggle
|
||||
Toggles = [ PowerToggle, AutoRad_VentAllowed ]
|
||||
Mode = All
|
||||
Toggles = [ PowerToggle, AutoRad_MustVent ]
|
||||
Mode = Any
|
||||
}
|
||||
|
||||
@@ -7,49 +7,39 @@
|
||||
// pull from the same pool, with the same Mode, in the same category, and
|
||||
// Data/ships/priorities.rules defines no ordering between them.
|
||||
//
|
||||
// The patches below add components to two vanilla parts -- the radiator, which
|
||||
// decides and acts, and the missile launcher, which reports whether thermal
|
||||
// ammunition is in use -- and then repoint the radiator's heat dissipation,
|
||||
// steam plume, dissipation readout and power indicator from the raw PowerToggle
|
||||
// to the AutoPowerToggle gate defined in components/radiator_auto.rules.
|
||||
// The patches below add components to one vanilla part, the radiator, and
|
||||
// repoint its heat dissipation, steam plume, dissipation readout and power
|
||||
// indicator from the raw PowerToggle to the AutoPowerToggle gate defined in
|
||||
// components/radiator_auto.rules. The radiator's own on/off switch picks the
|
||||
// mode: ON vents constantly (vanilla), OFF banks heat and vents only when the
|
||||
// network is nearly full.
|
||||
//
|
||||
// The radiator measures the heat network's remaining room itself, so nothing
|
||||
// patches the thermal battery or the overclock base. Holding requires positive
|
||||
// evidence of headroom; anything missing or unmeasurable results in venting.
|
||||
// The radiator measures the heat network's remaining room itself, so no other
|
||||
// part is patched. Idling requires positive evidence of headroom; anything
|
||||
// missing or unmeasurable results in venting.
|
||||
//
|
||||
// Nothing here changes a stat or a number. It only changes when radiators run.
|
||||
|
||||
ID = ganter.auto_radiator
|
||||
Name = "Auto Radiator"
|
||||
Version = 3.0.0
|
||||
Version = 4.0.0
|
||||
CompatibleGameVersions = ["0.30.4c", "0.30.4"]
|
||||
ModifiesGameplay = true
|
||||
Author = "Tobias Ganter"
|
||||
|
||||
Description = "Radiators only vent surplus heat instead of running constantly.\n\n"\
|
||||
Description = "Radiators switched OFF bank heat and vent only when the ship's heat storage is nearly full.\n\n"\
|
||||
"Thermal Canister Missiles are built from your ship's own heat, and in vanilla "\
|
||||
"your radiators compete with your launchers for it -- the heat network is drained "\
|
||||
"to keep radiators busy, and your missiles reload at whatever rate your reactors "\
|
||||
"happen to produce.\n\n"\
|
||||
"With this mod, radiators hold off while the heat network still has room, keeping "\
|
||||
"your thermal batteries topped up as a deep missile reserve, and vent as the "\
|
||||
"network runs out of room.\n\n"\
|
||||
"- Only applies when a launcher is actually set to Thermal Canister Missiles. "\
|
||||
"Otherwise radiators behave exactly as in vanilla.\n\n"\
|
||||
"- Your manual radiator on/off switch still overrides everything.\n\n"\
|
||||
"- Fail-safe: holding heat requires measured headroom on the network. If that "\
|
||||
"headroom runs low, or cannot be measured at all, radiators vent."
|
||||
"your radiators compete with your launchers for it. With this mod the radiator's "\
|
||||
"on/off switch picks a mode:\n\n"\
|
||||
"- ON: vanilla. Vents constantly, the heat is gone.\n\n"\
|
||||
"- OFF: banks heat as a deep missile reserve. The radiator idles while the heat "\
|
||||
"network has room and vents as it approaches full, so the ship never overflows "\
|
||||
"heat onto its own parts. An OFF radiator also frees its crew.\n\n"\
|
||||
"- Fail-safe: idling requires measured headroom on the network. If that runs low, "\
|
||||
"or cannot be measured at all, radiators vent."
|
||||
|
||||
Actions
|
||||
[
|
||||
// The missile launcher reports whether it is set to thermal ammunition.
|
||||
{
|
||||
Action = AddBase
|
||||
AddBaseTo = "<ships/terran/missile_launcher/missile_launcher.rules>/Part/Components"
|
||||
BaseToAdd = &<components/launcher_ammo_signal.rules>
|
||||
}
|
||||
|
||||
|
||||
// --- Receiver ---------------------------------------------------------
|
||||
|
||||
{
|
||||
@@ -58,21 +48,20 @@ Actions
|
||||
BaseToAdd = &<components/radiator_auto.rules>
|
||||
}
|
||||
|
||||
// --- Attach the radiator's receivers to the network -------------------
|
||||
// --- Attach the radiator's network reader ----------------------------
|
||||
//
|
||||
// A network component only reaches the network if a route leads to it. The
|
||||
// radiator's ports anchor on NetworkedHeatBuffer, and its existing Router
|
||||
// bridges that anchor to the other network components -- which is how
|
||||
// vanilla's own NetworkedThermalDissipationLoadValue gets connected.
|
||||
// Without these three entries the receivers are inert and read nothing,
|
||||
// which makes the vent gate permanently open.
|
||||
// Without this entry the reader is inert, reads 0, and the radiator vents
|
||||
// permanently.
|
||||
{
|
||||
Action = AddMany
|
||||
AddTo = "<ships/terran/radiator/radiator.rules>/Part/Components/Router/RouteGenerators/0/Routes"
|
||||
ManyToAdd
|
||||
[
|
||||
[NetworkedHeatBuffer, AutoRad_NetworkSpace, 0]
|
||||
[NetworkedHeatBuffer, AutoRad_ThermalAmmoSignal, 0]
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
@@ -698,6 +698,41 @@ def test_threshold_amounts_match_value_scale():
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# The gate's semantics: radiator button ON = vanilla, always vent; button OFF
|
||||
# = bank heat, and the mod vents only when the network is nearly full. So the
|
||||
# gate is PowerToggle OR MustVent, and nothing else -- no ammo criterion, no
|
||||
# launcher patch, no cross-part signal.
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def test_gate_is_power_or_must_vent():
|
||||
mod = rules_parser.parse_file(os.path.join(MOD_ROOT, "mod.rules"))
|
||||
for action in iter_actions(mod):
|
||||
for field in ("AddBaseTo", "OverrideIn", "AddTo"):
|
||||
node = action.get(field)
|
||||
check(
|
||||
node is None or "missile_launcher" not in node.value,
|
||||
f"mod.rules still patches the missile launcher via {field}",
|
||||
)
|
||||
check(
|
||||
not os.path.exists(os.path.join(MOD_ROOT, "components", "launcher_ammo_signal.rules")),
|
||||
"components/launcher_ammo_signal.rules should be gone",
|
||||
)
|
||||
|
||||
auto = rules_parser.parse_file(os.path.join(MOD_ROOT, "components", "radiator_auto.rules"))
|
||||
gate = auto.get("AutoPowerToggle")
|
||||
check(gate is not None, "AutoPowerToggle missing")
|
||||
if gate is None:
|
||||
return
|
||||
check(gate.get("Mode") is not None and gate.get("Mode").value == "Any", "AutoPowerToggle must be Mode = Any (OR)")
|
||||
toggles = gate.get("Toggles")
|
||||
names = [c.value for c in toggles.children.values()] if toggles is not None and toggles.kind == "list" else []
|
||||
check(sorted(names) == ["AutoRad_MustVent", "PowerToggle"], f"AutoPowerToggle toggles should be exactly PowerToggle + AutoRad_MustVent, got {names}")
|
||||
for name in auto.order:
|
||||
check("Ammo" not in name, f"ammo-signal component still present: {name}")
|
||||
|
||||
|
||||
def test_manifest_is_well_formed():
|
||||
mod = rules_parser.parse_file(os.path.join(MOD_ROOT, "mod.rules"))
|
||||
for field in ("ID", "Name", "Version", "CompatibleGameVersions", "ModifiesGameplay"):
|
||||
|
||||
Reference in New Issue
Block a user