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
|
## 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
|
- **ON** — vanilla. The radiator vents constantly. You want the heat gone.
|
||||||
itself, with a bare `NetworkResourceInput` — the same component an
|
- **OFF** — bank heat. The radiator idles while the heat network has room and
|
||||||
overclocked part pushes its heat through (`base_part_overclock.rules`:
|
vents only when it is nearly full: vent while less than **5%** of the
|
||||||
`NetworkHeatInput`), so it sees exactly what a producer sees. Vent while
|
network's heat storage is free, hold again once **10%** is free (a deadband
|
||||||
less than **5%** of the network's heat storage is free, hold again once
|
so radiators do not chatter). The ship never overflows heat onto its own
|
||||||
**10%** is free — a deadband so radiators do not chatter. Both numbers are
|
parts, and thermal batteries stay topped up as a deep missile reserve. An
|
||||||
named constants at the top of `components/radiator_auto.rules`.
|
OFF radiator also releases its crew.
|
||||||
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
|
The radiator measures the network itself, with a bare `NetworkResourceInput`
|
||||||
idle a radiator you have left switched on.
|
— 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
|
network has room to spare. Anything missing, disconnected or unmeasurable ends
|
||||||
up venting, which is vanilla behaviour.
|
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
|
mod.rules manifest + patch actions
|
||||||
components/radiator_auto.rules measures network headroom, decides, acts
|
components/radiator_auto.rules measures network headroom, gates the radiator
|
||||||
components/launcher_ammo_signal.rules reports thermal ammo in use
|
|
||||||
tests/ offline validation suite
|
tests/ offline validation suite
|
||||||
tools/dotnet_enum.py reads component schemas out of Cosmoteer.dll
|
tools/dotnet_enum.py reads component schemas out of Cosmoteer.dll
|
||||||
tools/ildump.py disassembles component methods to readable CIL
|
tools/ildump.py disassembles component methods to readable CIL
|
||||||
@@ -134,25 +133,25 @@ for the schema checks.
|
|||||||
|
|
||||||
## How to tell it is working
|
## How to tell it is working
|
||||||
|
|
||||||
There is no separate "auto" mode and nothing to reset. The radiator's on/off
|
Switch a radiator **OFF** with heat storage low. It shows the "powered off"
|
||||||
switch is an AND gate:
|
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 it **ON** and it runs constantly, exactly as in vanilla.
|
||||||
- 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
|
## 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
|
v4.0.0 changed only the gate: earlier builds required a launcher to broadcast
|
||||||
in use, and with no launcher on thermal ammo behaviour is vanilla.
|
"thermal ammo in use" over the heat network, which worked at load but did not
|
||||||
- **Not yet confirmed: the release path** — that radiators start venting as the
|
follow the ammo selection live. The player's switch expresses the intent more
|
||||||
network fills. v3.0.0 is the first build whose threshold is on the right
|
directly, so the broadcast was removed and OFF now means "bank heat". That
|
||||||
scale, and it has not been played yet.
|
switch behaviour is the one thing v4.0.0 has not been played with yet.
|
||||||
|
|
||||||
### What the assembly says
|
### What the assembly says
|
||||||
|
|
||||||
@@ -223,6 +222,7 @@ debugging again.
|
|||||||
|
|
||||||
## Possible additions
|
## Possible additions
|
||||||
|
|
||||||
- A per-radiator UI toggle for auto/manual, which needs a new `ChoiceID` wired
|
- A third switch position (ON / BANK / OFF) so a radiator can be fully
|
||||||
into `Data/gui/game/parts/part_toggles.rules`.
|
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.
|
- 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
|
// ABOUT ME
|
||||||
// Components merged into the Radiator. They decide whether this radiator should
|
// Components merged into the Radiator. They decide whether this radiator must
|
||||||
// vent or hold, and combine that into a single gate, AutoPowerToggle, which
|
// vent right now, and combine that with the player's switch into a single gate,
|
||||||
// mod.rules substitutes for the radiator's vanilla PowerToggle on the parts of
|
// AutoPowerToggle, which mod.rules substitutes for the vanilla PowerToggle on
|
||||||
// the radiator that actually dissipate heat.
|
// the parts of the radiator that actually dissipate heat.
|
||||||
//
|
//
|
||||||
// The player's manual on/off switch (PowerToggle) is preserved and still wins:
|
// The player's on/off switch (PowerToggle) chooses the mode:
|
||||||
// a radiator the player has switched off stays off. The automation can only
|
// ON -> vanilla. Vent constantly; the player wants the heat gone.
|
||||||
// idle a radiator the player has left on.
|
// 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 gate is therefore PowerToggle OR MustVent. Fail-safe: idling requires
|
||||||
// - the heat network is running out of room, or
|
// positive evidence that the network has room to spare. Anything missing,
|
||||||
// - no launcher on the network is using thermal ammunition
|
// disconnected or unmeasurable results in venting.
|
||||||
//
|
|
||||||
// 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.
|
|
||||||
|
|
||||||
// --- Is the heat network running out of room? -------------------------------
|
// --- Is the heat network running out of room? -------------------------------
|
||||||
//
|
//
|
||||||
@@ -88,48 +86,14 @@ AutoRad_MustVent
|
|||||||
Mode = Any
|
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 ---------------------------------------------------------------
|
// --- The gate ---------------------------------------------------------------
|
||||||
|
//
|
||||||
AutoRad_VentAllowed
|
// 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
|
||||||
Type = MultiToggle
|
// that is OFF but venting looks like a working radiator.
|
||||||
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.
|
|
||||||
AutoPowerToggle
|
AutoPowerToggle
|
||||||
{
|
{
|
||||||
Type = MultiToggle
|
Type = MultiToggle
|
||||||
Toggles = [ PowerToggle, AutoRad_VentAllowed ]
|
Toggles = [ PowerToggle, AutoRad_MustVent ]
|
||||||
Mode = All
|
Mode = Any
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,49 +7,39 @@
|
|||||||
// pull from the same pool, with the same Mode, in the same category, and
|
// pull from the same pool, with the same Mode, in the same category, and
|
||||||
// Data/ships/priorities.rules defines no ordering between them.
|
// Data/ships/priorities.rules defines no ordering between them.
|
||||||
//
|
//
|
||||||
// The patches below add components to two vanilla parts -- the radiator, which
|
// The patches below add components to one vanilla part, the radiator, and
|
||||||
// decides and acts, and the missile launcher, which reports whether thermal
|
// repoint its heat dissipation, steam plume, dissipation readout and power
|
||||||
// ammunition is in use -- and then repoint the radiator's heat dissipation,
|
// indicator from the raw PowerToggle to the AutoPowerToggle gate defined in
|
||||||
// steam plume, dissipation readout and power indicator from the raw PowerToggle
|
// components/radiator_auto.rules. The radiator's own on/off switch picks the
|
||||||
// to the AutoPowerToggle gate defined in components/radiator_auto.rules.
|
// 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
|
// The radiator measures the heat network's remaining room itself, so no other
|
||||||
// patches the thermal battery or the overclock base. Holding requires positive
|
// part is patched. Idling requires positive evidence of headroom; anything
|
||||||
// evidence of headroom; anything missing or unmeasurable results in venting.
|
// missing or unmeasurable results in venting.
|
||||||
//
|
//
|
||||||
// Nothing here changes a stat or a number. It only changes when radiators run.
|
// Nothing here changes a stat or a number. It only changes when radiators run.
|
||||||
|
|
||||||
ID = ganter.auto_radiator
|
ID = ganter.auto_radiator
|
||||||
Name = "Auto Radiator"
|
Name = "Auto Radiator"
|
||||||
Version = 3.0.0
|
Version = 4.0.0
|
||||||
CompatibleGameVersions = ["0.30.4c", "0.30.4"]
|
CompatibleGameVersions = ["0.30.4c", "0.30.4"]
|
||||||
ModifiesGameplay = true
|
ModifiesGameplay = true
|
||||||
Author = "Tobias Ganter"
|
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 "\
|
"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 "\
|
"your radiators compete with your launchers for it. With this mod the radiator's "\
|
||||||
"to keep radiators busy, and your missiles reload at whatever rate your reactors "\
|
"on/off switch picks a mode:\n\n"\
|
||||||
"happen to produce.\n\n"\
|
"- ON: vanilla. Vents constantly, the heat is gone.\n\n"\
|
||||||
"With this mod, radiators hold off while the heat network still has room, keeping "\
|
"- OFF: banks heat as a deep missile reserve. The radiator idles while the heat "\
|
||||||
"your thermal batteries topped up as a deep missile reserve, and vent as the "\
|
"network has room and vents as it approaches full, so the ship never overflows "\
|
||||||
"network runs out of room.\n\n"\
|
"heat onto its own parts. An OFF radiator also frees its crew.\n\n"\
|
||||||
"- Only applies when a launcher is actually set to Thermal Canister Missiles. "\
|
"- Fail-safe: idling requires measured headroom on the network. If that runs low, "\
|
||||||
"Otherwise radiators behave exactly as in vanilla.\n\n"\
|
"or cannot be measured at all, radiators vent."
|
||||||
"- 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."
|
|
||||||
|
|
||||||
Actions
|
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 ---------------------------------------------------------
|
// --- Receiver ---------------------------------------------------------
|
||||||
|
|
||||||
{
|
{
|
||||||
@@ -58,21 +48,20 @@ Actions
|
|||||||
BaseToAdd = &<components/radiator_auto.rules>
|
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
|
// A network component only reaches the network if a route leads to it. The
|
||||||
// radiator's ports anchor on NetworkedHeatBuffer, and its existing Router
|
// radiator's ports anchor on NetworkedHeatBuffer, and its existing Router
|
||||||
// bridges that anchor to the other network components -- which is how
|
// bridges that anchor to the other network components -- which is how
|
||||||
// vanilla's own NetworkedThermalDissipationLoadValue gets connected.
|
// vanilla's own NetworkedThermalDissipationLoadValue gets connected.
|
||||||
// Without these three entries the receivers are inert and read nothing,
|
// Without this entry the reader is inert, reads 0, and the radiator vents
|
||||||
// which makes the vent gate permanently open.
|
// permanently.
|
||||||
{
|
{
|
||||||
Action = AddMany
|
Action = AddMany
|
||||||
AddTo = "<ships/terran/radiator/radiator.rules>/Part/Components/Router/RouteGenerators/0/Routes"
|
AddTo = "<ships/terran/radiator/radiator.rules>/Part/Components/Router/RouteGenerators/0/Routes"
|
||||||
ManyToAdd
|
ManyToAdd
|
||||||
[
|
[
|
||||||
[NetworkedHeatBuffer, AutoRad_NetworkSpace, 0]
|
[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():
|
def test_manifest_is_well_formed():
|
||||||
mod = rules_parser.parse_file(os.path.join(MOD_ROOT, "mod.rules"))
|
mod = rules_parser.parse_file(os.path.join(MOD_ROOT, "mod.rules"))
|
||||||
for field in ("ID", "Name", "Version", "CompatibleGameVersions", "ModifiesGameplay"):
|
for field in ("ID", "Name", "Version", "CompatibleGameVersions", "ModifiesGameplay"):
|
||||||
|
|||||||
Reference in New Issue
Block a user