diff --git a/Content.Server/Atmos/Components/HeatExchangerComponent.cs b/Content.Server/Atmos/Components/HeatExchangerComponent.cs
new file mode 100644
index 0000000000..10819387cf
--- /dev/null
+++ b/Content.Server/Atmos/Components/HeatExchangerComponent.cs
@@ -0,0 +1,36 @@
+namespace Content.Server.Atmos.Components;
+
+[RegisterComponent]
+public sealed class HeatExchangerComponent : Component
+{
+ [ViewVariables(VVAccess.ReadWrite)]
+ [DataField("inlet")]
+ public string InletName { get; set; } = "inlet";
+
+ [ViewVariables(VVAccess.ReadWrite)]
+ [DataField("outlet")]
+ public string OutletName { get; set; } = "outlet";
+
+ ///
+ /// Pipe conductivity (mols/kPa/sec).
+ ///
+ [ViewVariables(VVAccess.ReadWrite)]
+ [DataField("conductivity")]
+ public float G { get; set; } = 1f;
+
+ ///
+ /// Thermal convection coefficient (J/degK/sec).
+ ///
+ [ViewVariables(VVAccess.ReadWrite)]
+ [DataField("convectionCoefficient")]
+ public float K { get; set; } = 8000f;
+
+ ///
+ /// Thermal radiation coefficient. Number of "effective" tiles this
+ /// radiator radiates compared to superconductivity tile losses.
+ ///
+ [ViewVariables(VVAccess.ReadWrite)]
+ [DataField("radiationCoefficient")]
+ public float alpha { get; set; } = 400f;
+}
+
diff --git a/Content.Server/Atmos/EntitySystems/AtmosphereSystem.Gases.cs b/Content.Server/Atmos/EntitySystems/AtmosphereSystem.Gases.cs
index f08262ef0c..0726606ae0 100644
--- a/Content.Server/Atmos/EntitySystems/AtmosphereSystem.Gases.cs
+++ b/Content.Server/Atmos/EntitySystems/AtmosphereSystem.Gases.cs
@@ -78,6 +78,16 @@ namespace Content.Server.Atmos.EntitySystems
return mixture.Temperature * cachedHeatCapacity;
}
+ ///
+ /// Add 'dQ' Joules of energy into 'mixture'.
+ ///
+ public void AddHeat(GasMixture mixture, float dQ)
+ {
+ var c = GetHeatCapacity(mixture);
+ float dT = dQ / c;
+ mixture.Temperature += dT;
+ }
+
///
/// Merges the gas mixture into the gas mixture.
/// The gas mixture is not modified by this method.
diff --git a/Content.Server/Atmos/EntitySystems/HeatExchangerSystem.cs b/Content.Server/Atmos/EntitySystems/HeatExchangerSystem.cs
new file mode 100644
index 0000000000..6ef617b0cd
--- /dev/null
+++ b/Content.Server/Atmos/EntitySystems/HeatExchangerSystem.cs
@@ -0,0 +1,97 @@
+using Content.Server.Atmos.EntitySystems;
+using Content.Server.Atmos.Piping.Components;
+using Content.Server.Atmos.Piping.Unary.Components;
+using Content.Server.Atmos;
+using Content.Server.Atmos.Components;
+using Content.Server.NodeContainer.EntitySystems;
+using Content.Server.NodeContainer.Nodes;
+using Content.Server.NodeContainer;
+using Content.Shared.Atmos.Piping;
+using Content.Shared.Atmos;
+using Content.Shared.CCVar;
+using Content.Shared.Interaction;
+using JetBrains.Annotations;
+using Robust.Shared.Configuration;
+using Robust.Shared.Timing;
+
+namespace Content.Server.Atmos.EntitySystems;
+
+public sealed class HeatExchangerSystem : EntitySystem
+{
+ [Dependency] private readonly AtmosphereSystem _atmosphereSystem = default!;
+ [Dependency] private readonly IConfigurationManager _cfg = default!;
+ [Dependency] private readonly NodeContainerSystem _nodeContainer = default!;
+
+ float tileLoss;
+
+ public override void Initialize()
+ {
+ base.Initialize();
+ SubscribeLocalEvent(OnAtmosUpdate);
+
+ // Getting CVars is expensive, don't do it every tick
+ _cfg.OnValueChanged(CCVars.SuperconductionTileLoss, CacheTileLoss, true);
+ }
+
+ public override void Shutdown()
+ {
+ base.Shutdown();
+ _cfg.UnsubValueChanged(CCVars.SuperconductionTileLoss, CacheTileLoss);
+ }
+
+ private void CacheTileLoss(float val)
+ {
+ tileLoss = val;
+ }
+
+ private void OnAtmosUpdate(EntityUid uid, HeatExchangerComponent comp, AtmosDeviceUpdateEvent args)
+ {
+ if (!EntityManager.TryGetComponent(uid, out NodeContainerComponent? nodeContainer)
+ || !_nodeContainer.TryGetNode(nodeContainer, comp.InletName, out PipeNode? inlet)
+ || !_nodeContainer.TryGetNode(nodeContainer, comp.OutletName, out PipeNode? outlet))
+ {
+ return;
+ }
+
+ // Positive dN flows from inlet to outlet
+ var dt = 1/_atmosphereSystem.AtmosTickRate;
+ var dP = inlet.Air.Pressure - outlet.Air.Pressure;
+ var dN = comp.G*dP*dt;
+
+ GasMixture xfer;
+ if (dN > 0)
+ xfer = inlet.Air.Remove(dN);
+ else
+ xfer = outlet.Air.Remove(-dN);
+
+ var radTemp = Atmospherics.TCMB;
+
+ // Convection
+ var environment = _atmosphereSystem.GetContainingMixture(uid, true, true);
+ if (environment != null)
+ {
+ radTemp = environment.Temperature;
+
+ // Positive dT is from pipe to surroundings
+ var dT = xfer.Temperature - environment.Temperature;
+ var dE = comp.K * dT * dt;
+ var envLim = Math.Abs(_atmosphereSystem.GetHeatCapacity(environment) * dT * dt);
+ var xferLim = Math.Abs(_atmosphereSystem.GetHeatCapacity(xfer) * dT * dt);
+ var dEactual = Math.Sign(dE) * Math.Min(Math.Abs(dE), Math.Min(envLim, xferLim));
+ _atmosphereSystem.AddHeat(xfer, -dEactual);
+ _atmosphereSystem.AddHeat(environment, dEactual);
+ }
+
+ // Radiation
+ float dTR = xfer.Temperature - radTemp;
+ float a0 = tileLoss / MathF.Pow(Atmospherics.T20C, 4);
+ float dER = comp.alpha * a0 * MathF.Pow(dTR, 4) * dt;
+ _atmosphereSystem.AddHeat(xfer, -dER);
+
+ if (dN > 0)
+ _atmosphereSystem.Merge(outlet.Air, xfer);
+ else
+ _atmosphereSystem.Merge(inlet.Air, xfer);
+
+ }
+}
diff --git a/Content.Shared/CCVar/CCVars.cs b/Content.Shared/CCVar/CCVars.cs
index 3c4162106d..493bf14c1a 100644
--- a/Content.Shared/CCVar/CCVars.cs
+++ b/Content.Shared/CCVar/CCVars.cs
@@ -937,6 +937,12 @@ namespace Content.Shared.CCVar
public static readonly CVarDef Superconduction =
CVarDef.Create("atmos.superconduction", false, CVar.SERVERONLY);
+ ///
+ /// Heat loss per tile due to radiation at 20 degC, in W.
+ ///
+ public static readonly CVarDef SuperconductionTileLoss =
+ CVarDef.Create("atmos.superconduction_tile_loss", 30f, CVar.SERVERONLY);
+
///
/// Whether excited groups will be processed and created.
///
diff --git a/Resources/Prototypes/Entities/Structures/Piping/Atmospherics/binary.yml b/Resources/Prototypes/Entities/Structures/Piping/Atmospherics/binary.yml
index 758f4dbc00..13116f19ef 100644
--- a/Resources/Prototypes/Entities/Structures/Piping/Atmospherics/binary.yml
+++ b/Resources/Prototypes/Entities/Structures/Piping/Atmospherics/binary.yml
@@ -379,3 +379,45 @@
acts: ["Destruction"]
- type: Machine
board: GasRecyclerMachineCircuitboard
+
+- type: entity
+ parent: GasBinaryBase
+ id: HeatExchanger
+ name: radiator
+ description: Transfers heat between the pipe and its surroundings.
+ placement:
+ mode: SnapgridCenter
+ components:
+ - type: Rotatable
+ - type: Transform
+ noRot: false
+ - type: Sprite
+ sprite: Structures/Piping/Atmospherics/heatexchanger.rsi
+ layers:
+ - sprite: Structures/Piping/Atmospherics/pipe.rsi
+ state: pipeStraight
+ map: [ "enum.PipeVisualLayers.Pipe" ]
+ - state: heStraight
+ map: [ "enum.SubfloorLayers.FirstLayer" ]
+ - type: SubFloorHide
+ visibleLayers:
+ - enum.SubfloorLayers.FirstLayer
+ - type: Appearance
+ - type: PipeColorVisuals
+ - type: AtmosDevice
+ - type: HeatExchanger
+ - type: NodeContainer
+ nodes:
+ inlet:
+ !type:PipeNode
+ nodeGroupID: Pipe
+ pipeDirection: North
+ outlet:
+ !type:PipeNode
+ nodeGroupID: Pipe
+ pipeDirection: South
+ - type: Construction
+ graph: GasBinary
+ node: radiator
+ - type: StaticPrice
+ price: 50
diff --git a/Resources/Prototypes/Recipes/Construction/Graphs/utilities/atmos_binary.yml b/Resources/Prototypes/Recipes/Construction/Graphs/utilities/atmos_binary.yml
index 26db6533bf..eee5c6fbc4 100644
--- a/Resources/Prototypes/Recipes/Construction/Graphs/utilities/atmos_binary.yml
+++ b/Resources/Prototypes/Recipes/Construction/Graphs/utilities/atmos_binary.yml
@@ -46,6 +46,12 @@
amount: 2
doAfter: 1
+ - to: radiator
+ steps:
+ - material: Steel
+ amount: 8
+ doAfter: 1
+
- node: pressurepump
entity: GasPressurePump
edges:
@@ -159,3 +165,21 @@
doAfter: 1
- tool: Welding
doAfter: 1
+
+ - node: radiator
+ entity: HeatExchanger
+ edges:
+ - to: start
+ conditions:
+ - !type:EntityAnchored
+ anchored: false
+ completed:
+ - !type:SpawnPrototype
+ prototype: SheetSteel1
+ amount: 8
+ - !type:DeleteEntity
+ steps:
+ - tool: Screwing
+ doAfter: 1
+ - tool: Welding
+ doAfter: 1
diff --git a/Resources/Prototypes/Recipes/Construction/utilities.yml b/Resources/Prototypes/Recipes/Construction/utilities.yml
index 6eba83b82f..40fe9b6883 100644
--- a/Resources/Prototypes/Recipes/Construction/utilities.yml
+++ b/Resources/Prototypes/Recipes/Construction/utilities.yml
@@ -663,6 +663,22 @@
conditions:
- !type:TileNotBlocked {}
+- type: construction
+ id: HeatExchanger
+ name: radiator
+ description: Transfers heat between the pipe and its surroundings.
+ graph: GasBinary
+ startNode: start
+ targetNode: radiator
+ category: construction-category-utilities
+ placementMode: SnapgridCenter
+ canBuildInImpassable: false
+ icon:
+ sprite: Structures/Piping/Atmospherics/heatexchanger.rsi
+ state: heStraight
+ conditions:
+ - !type:TileNotBlocked {}
+
# ATMOS TRINARY
- type: construction
id: GasFilter
diff --git a/Resources/Textures/Structures/Piping/Atmospherics/heatexchanger.rsi/heBend.png b/Resources/Textures/Structures/Piping/Atmospherics/heatexchanger.rsi/heBend.png
new file mode 100644
index 0000000000..180be218cb
Binary files /dev/null and b/Resources/Textures/Structures/Piping/Atmospherics/heatexchanger.rsi/heBend.png differ
diff --git a/Resources/Textures/Structures/Piping/Atmospherics/heatexchanger.rsi/heStraight.png b/Resources/Textures/Structures/Piping/Atmospherics/heatexchanger.rsi/heStraight.png
new file mode 100644
index 0000000000..dfdf1f405b
Binary files /dev/null and b/Resources/Textures/Structures/Piping/Atmospherics/heatexchanger.rsi/heStraight.png differ
diff --git a/Resources/Textures/Structures/Piping/Atmospherics/heatexchanger.rsi/meta.json b/Resources/Textures/Structures/Piping/Atmospherics/heatexchanger.rsi/meta.json
new file mode 100644
index 0000000000..d5b5c3beda
--- /dev/null
+++ b/Resources/Textures/Structures/Piping/Atmospherics/heatexchanger.rsi/meta.json
@@ -0,0 +1,19 @@
+{
+ "version":1,
+ "size":{
+ "x":32,
+ "y":32
+ },
+ "license":"CC-BY-SA-3.0",
+ "copyright":"Taken from https://github.com/tgstation/tgstation at commit 57cd1d59ca019dd0e7811ac451f295f818e573da and modified by BasedUser",
+ "states":[
+ {
+ "name":"heStraight",
+ "directions":4
+ },
+ {
+ "name":"heBend",
+ "directions":4
+ }
+ ]
+}