diff --git a/README.md b/README.md index 55e506d..347ba22 100644 --- a/README.md +++ b/README.md @@ -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. diff --git a/components/launcher_ammo_signal.rules b/components/launcher_ammo_signal.rules deleted file mode 100644 index 7e569c4..0000000 --- a/components/launcher_ammo_signal.rules +++ /dev/null @@ -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] - ] - } - ] -} diff --git a/components/radiator_auto.rules b/components/radiator_auto.rules index 10a8189..d178c31 100644 --- a/components/radiator_auto.rules +++ b/components/radiator_auto.rules @@ -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 } diff --git a/mod.rules b/mod.rules index c1c0e35..a147b6d 100644 --- a/mod.rules +++ b/mod.rules @@ -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 = "/Part/Components" - BaseToAdd = & - } - - // --- Receiver --------------------------------------------------------- { @@ -58,21 +48,20 @@ Actions BaseToAdd = & } - // --- 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 = "/Part/Components/Router/RouteGenerators/0/Routes" ManyToAdd [ [NetworkedHeatBuffer, AutoRad_NetworkSpace, 0] - [NetworkedHeatBuffer, AutoRad_ThermalAmmoSignal, 0] ] } diff --git a/tests/test_mod.py b/tests/test_mod.py index 559f043..bcaccc5 100644 --- a/tests/test_mod.py +++ b/tests/test_mod.py @@ -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"):