diff --git a/Content.Client/Atmos/UI/GasThermomachineBoundUserInterface.cs b/Content.Client/Atmos/UI/GasThermomachineBoundUserInterface.cs index cda7f6836c..23850cb25a 100644 --- a/Content.Client/Atmos/UI/GasThermomachineBoundUserInterface.cs +++ b/Content.Client/Atmos/UI/GasThermomachineBoundUserInterface.cs @@ -1,4 +1,5 @@ -using Content.Shared.Atmos.Piping.Unary.Components; +using Content.Shared.Atmos; +using Content.Shared.Atmos.Piping.Unary.Components; using JetBrains.Annotations; using Robust.Client.GameObjects; @@ -18,6 +19,9 @@ namespace Content.Client.Atmos.UI [ViewVariables] private float _maxTemp = 0.0f; + + [ViewVariables] + private bool _isHeater = true; public GasThermomachineBoundUserInterface(EntityUid owner, Enum uiKey) : base(owner, uiKey) { @@ -50,7 +54,12 @@ namespace Content.Client.Atmos.UI private void OnTemperatureChanged(float value) { - var actual = Math.Clamp(value, _minTemp, _maxTemp); + var actual = 0f; + if (_isHeater) + actual = Math.Min(value, _maxTemp); + else + actual = Math.Max(value, _minTemp); + actual = Math.Max(actual, Atmospherics.TCMB); if (!MathHelper.CloseTo(actual, value, 0.09)) { _window?.SetTemperature(actual); @@ -72,14 +81,14 @@ namespace Content.Client.Atmos.UI _minTemp = cast.MinTemperature; _maxTemp = cast.MaxTemperature; + _isHeater = cast.IsHeater; _window.SetTemperature(cast.Temperature); _window.SetActive(cast.Enabled); - _window.Title = cast.Mode switch + _window.Title = _isHeater switch { - ThermoMachineMode.Freezer => Loc.GetString("comp-gas-thermomachine-ui-title-freezer"), - ThermoMachineMode.Heater => Loc.GetString("comp-gas-thermomachine-ui-title-heater"), - _ => string.Empty + false => Loc.GetString("comp-gas-thermomachine-ui-title-freezer"), + true => Loc.GetString("comp-gas-thermomachine-ui-title-heater") }; } diff --git a/Content.Server/Atmos/Piping/Unary/Components/GasThermoMachineComponent.cs b/Content.Server/Atmos/Piping/Unary/Components/GasThermoMachineComponent.cs index 903ad64c95..8cfc3044a6 100644 --- a/Content.Server/Atmos/Piping/Unary/Components/GasThermoMachineComponent.cs +++ b/Content.Server/Atmos/Piping/Unary/Components/GasThermoMachineComponent.cs @@ -12,9 +12,8 @@ namespace Content.Server.Atmos.Piping.Unary.Components public string InletName = "pipe"; /// - /// Current maximum temperature, calculated from and the quality of matter - /// bins. The heat capacity effectively determines the rate at which the thermo machine can add or remove - /// heat from a pipenet. + /// Current electrical power consumption, in watts. Increasing power increases the ability of the + /// thermomachine to heat or cool air. /// [ViewVariables(VVAccess.ReadWrite)] public float HeatCapacity = 10000; @@ -30,12 +29,31 @@ namespace Content.Server.Atmos.Piping.Unary.Components [ViewVariables(VVAccess.ReadWrite)] public float TargetTemperature = Atmospherics.T20C; - [DataField("mode")] - public ThermoMachineMode Mode = ThermoMachineMode.Freezer; + /// + /// Tolerance for temperature setpoint hysteresis. + /// + [ViewVariables(VVAccess.ReadOnly)] + public float TemperatureTolerance = 2f; + + /// + /// Implements setpoint hysteresis to prevent heater from rapidly cycling on and off at setpoint. + /// If true, add Sign(Cp)*TemperatureTolerance to the temperature setpoint. + /// + [ViewVariables(VVAccess.ReadOnly)] + public bool HysteresisState = false; + + /// + /// Coefficient of performance. Output power / input power. + /// Positive for heaters, negative for freezers. + /// + [DataField("coefficientOfPerformance")] + [ViewVariables(VVAccess.ReadWrite)] + public float Cp = 0.9f; // output power / input power, positive is heat /// /// Current minimum temperature, calculated from and . + /// Ignored if heater. /// [ViewVariables(VVAccess.ReadWrite)] public float MinTemperature; @@ -43,6 +61,7 @@ namespace Content.Server.Atmos.Piping.Unary.Components /// /// Current maximum temperature, calculated from and . + /// Ignored if freezer. /// [ViewVariables(VVAccess.ReadWrite)] public float MaxTemperature; diff --git a/Content.Server/Atmos/Piping/Unary/EntitySystems/GasThermoMachineSystem.cs b/Content.Server/Atmos/Piping/Unary/EntitySystems/GasThermoMachineSystem.cs index 756942ecca..c9855c5885 100644 --- a/Content.Server/Atmos/Piping/Unary/EntitySystems/GasThermoMachineSystem.cs +++ b/Content.Server/Atmos/Piping/Unary/EntitySystems/GasThermoMachineSystem.cs @@ -49,48 +49,74 @@ namespace Content.Server.Atmos.Piping.Unary.EntitySystems private void OnThermoMachineUpdated(EntityUid uid, GasThermoMachineComponent thermoMachine, AtmosDeviceUpdateEvent args) { - - if (!(_power.IsPowered(uid)) - || !TryComp(uid, out NodeContainerComponent? nodeContainer) + if (!(_power.IsPowered(uid) && TryComp(uid, out var receiver)) + || !TryComp(uid, out var nodeContainer) || !_nodeContainer.TryGetNode(nodeContainer, thermoMachine.InletName, out PipeNode? inlet)) { return; } - var airHeatCapacity = _atmosphereSystem.GetHeatCapacity(inlet.Air); - var combinedHeatCapacity = airHeatCapacity + thermoMachine.HeatCapacity; + float sign = Math.Sign(thermoMachine.Cp); // 1 if heater, -1 if freezer + float targetTemp = thermoMachine.TargetTemperature; + float highTemp = targetTemp + sign * thermoMachine.TemperatureTolerance; + float temp = inlet.Air.Temperature; - var startEnergy = inlet.Air.Temperature * airHeatCapacity; + if (sign * temp >= sign * highTemp) // upper bound + thermoMachine.HysteresisState = false; // turn off + else if (sign * temp < sign * targetTemp) // lower bound + thermoMachine.HysteresisState = true; // turn on - if (!MathHelper.CloseTo(combinedHeatCapacity, 0, 0.001f)) + if (thermoMachine.HysteresisState) + targetTemp = highTemp; // when on, target upper hysteresis bound + + if (!thermoMachine.HysteresisState) // Hysteresis is the same as "Should this be on?" { - var combinedEnergy = thermoMachine.HeatCapacity * thermoMachine.TargetTemperature + airHeatCapacity * inlet.Air.Temperature; - inlet.Air.Temperature = combinedEnergy / combinedHeatCapacity; - - thermoMachine.LastEnergyDelta = inlet.Air.Temperature * airHeatCapacity - startEnergy; + // Turn dynamic load back on when power has been adjusted to not cause lights to + // blink every time this heater comes on. + //receiver.Load = 0f; + return; } + + // Multiply power in by coefficient of performance, add that heat to gas + float dQ = thermoMachine.HeatCapacity * thermoMachine.Cp * args.dt; + + // Clamps the heat transferred to not overshoot + float Cin = _atmosphereSystem.GetHeatCapacity(inlet.Air); + float dT = targetTemp - temp; + float dQLim = dT * Cin; + float scale = 1f; + if (Math.Abs(dQ) > Math.Abs(dQLim)) + { + scale = dQLim / dQ; // reduce power consumption + thermoMachine.HysteresisState = false; // turn off + } + float dQActual = dQ * scale; + _atmosphereSystem.AddHeat(inlet.Air, dQActual); + receiver.Load = thermoMachine.HeatCapacity;// * scale; // we're not ready for dynamic load yet, see note above + } + + private bool IsHeater(GasThermoMachineComponent comp) + { + return comp.Cp >= 0; } private void OnGasThermoRefreshParts(EntityUid uid, GasThermoMachineComponent thermoMachine, RefreshPartsEvent args) { var heatCapacityPartRating = args.PartRatings[thermoMachine.MachinePartHeatCapacity]; - var temperatureRangePartRating = args.PartRatings[thermoMachine.MachinePartTemperature]; - thermoMachine.HeatCapacity = thermoMachine.BaseHeatCapacity * MathF.Pow(heatCapacityPartRating, 2); - switch (thermoMachine.Mode) + var temperatureRangePartRating = args.PartRatings[thermoMachine.MachinePartTemperature]; + if (IsHeater(thermoMachine)) { // 593.15K with stock parts. - case ThermoMachineMode.Heater: - thermoMachine.MaxTemperature = thermoMachine.BaseMaxTemperature + thermoMachine.MaxTemperatureDelta * temperatureRangePartRating; - thermoMachine.MinTemperature = Atmospherics.T20C; - break; + thermoMachine.MaxTemperature = thermoMachine.BaseMaxTemperature + thermoMachine.MaxTemperatureDelta * temperatureRangePartRating; + thermoMachine.MinTemperature = Atmospherics.T20C; + } + else { // 73.15K with stock parts. - case ThermoMachineMode.Freezer: - thermoMachine.MinTemperature = MathF.Max( - thermoMachine.BaseMinTemperature - thermoMachine.MinTemperatureDelta * temperatureRangePartRating, Atmospherics.TCMB); - thermoMachine.MaxTemperature = Atmospherics.T20C; - break; + thermoMachine.MinTemperature = MathF.Max( + thermoMachine.BaseMinTemperature - thermoMachine.MinTemperatureDelta * temperatureRangePartRating, Atmospherics.TCMB); + thermoMachine.MaxTemperature = Atmospherics.T20C; } DirtyUI(uid, thermoMachine); @@ -98,14 +124,13 @@ namespace Content.Server.Atmos.Piping.Unary.EntitySystems private void OnGasThermoUpgradeExamine(EntityUid uid, GasThermoMachineComponent thermoMachine, UpgradeExamineEvent args) { - switch (thermoMachine.Mode) + if (IsHeater(thermoMachine)) { - case ThermoMachineMode.Heater: - args.AddPercentageUpgrade("gas-thermo-component-upgrade-heating", thermoMachine.MaxTemperature / (thermoMachine.BaseMaxTemperature + thermoMachine.MaxTemperatureDelta)); - break; - case ThermoMachineMode.Freezer: - args.AddPercentageUpgrade("gas-thermo-component-upgrade-cooling", thermoMachine.MinTemperature / (thermoMachine.BaseMinTemperature - thermoMachine.MinTemperatureDelta)); - break; + args.AddPercentageUpgrade("gas-thermo-component-upgrade-heating", thermoMachine.MaxTemperature / (thermoMachine.BaseMaxTemperature + thermoMachine.MaxTemperatureDelta)); + } + else + { + args.AddPercentageUpgrade("gas-thermo-component-upgrade-cooling", thermoMachine.MinTemperature / (thermoMachine.BaseMinTemperature - thermoMachine.MinTemperatureDelta)); } args.AddPercentageUpgrade("gas-thermo-component-upgrade-heat-capacity", thermoMachine.HeatCapacity / thermoMachine.BaseHeatCapacity); } @@ -118,9 +143,11 @@ namespace Content.Server.Atmos.Piping.Unary.EntitySystems private void OnChangeTemperature(EntityUid uid, GasThermoMachineComponent thermoMachine, GasThermomachineChangeTemperatureMessage args) { - thermoMachine.TargetTemperature = - Math.Clamp(args.Temperature, thermoMachine.MinTemperature, thermoMachine.MaxTemperature); - + if (IsHeater(thermoMachine)) + thermoMachine.TargetTemperature = MathF.Min(args.Temperature, thermoMachine.MaxTemperature); + else + thermoMachine.TargetTemperature = MathF.Max(args.Temperature, thermoMachine.MinTemperature); + thermoMachine.TargetTemperature = MathF.Max(thermoMachine.TargetTemperature, Atmospherics.TCMB); DirtyUI(uid, thermoMachine); } @@ -134,7 +161,7 @@ namespace Content.Server.Atmos.Piping.Unary.EntitySystems return; _userInterfaceSystem.TrySetUiState(uid, ThermomachineUiKey.Key, - new GasThermomachineBoundUserInterfaceState(thermoMachine.MinTemperature, thermoMachine.MaxTemperature, thermoMachine.TargetTemperature, !powerReceiver.PowerDisabled, thermoMachine.Mode), null, ui); + new GasThermomachineBoundUserInterfaceState(thermoMachine.MinTemperature, thermoMachine.MaxTemperature, thermoMachine.TargetTemperature, !powerReceiver.PowerDisabled, IsHeater(thermoMachine)), null, ui); } private void OnExamined(EntityUid uid, GasThermoMachineComponent thermoMachine, ExaminedEvent args) @@ -143,8 +170,8 @@ namespace Content.Server.Atmos.Piping.Unary.EntitySystems return; if (Loc.TryGetString("gas-thermomachine-system-examined", out var str, - ("machineName", thermoMachine.Mode == ThermoMachineMode.Freezer ? "freezer" : "heater"), - ("tempColor", thermoMachine.Mode == ThermoMachineMode.Freezer ? "deepskyblue" : "red"), + ("machineName", !IsHeater(thermoMachine) ? "freezer" : "heater"), + ("tempColor", !IsHeater(thermoMachine) ? "deepskyblue" : "red"), ("temp", Math.Round(thermoMachine.TargetTemperature,2)) )) diff --git a/Content.Shared/Atmos/Piping/Unary/Components/SharedGasThermomachineComponent.cs b/Content.Shared/Atmos/Piping/Unary/Components/SharedGasThermomachineComponent.cs index 4bdd6a0e5f..259ebf4eea 100644 --- a/Content.Shared/Atmos/Piping/Unary/Components/SharedGasThermomachineComponent.cs +++ b/Content.Shared/Atmos/Piping/Unary/Components/SharedGasThermomachineComponent.cs @@ -12,14 +12,6 @@ public enum ThermomachineUiKey Key } -[Serializable] -[NetSerializable] -public enum ThermoMachineMode : byte -{ - Freezer = 0, - Heater = 1, -} - [Serializable] [NetSerializable] public sealed class GasThermomachineToggleMessage : BoundUserInterfaceMessage @@ -46,14 +38,14 @@ public sealed class GasThermomachineBoundUserInterfaceState : BoundUserInterface public float MaxTemperature { get; } public float Temperature { get; } public bool Enabled { get; } - public ThermoMachineMode Mode { get; } + public bool IsHeater { get; } - public GasThermomachineBoundUserInterfaceState(float minTemperature, float maxTemperature, float temperature, bool enabled, ThermoMachineMode mode) + public GasThermomachineBoundUserInterfaceState(float minTemperature, float maxTemperature, float temperature, bool enabled, bool isHeater) { MinTemperature = minTemperature; MaxTemperature = maxTemperature; Temperature = temperature; Enabled = enabled; - Mode = mode; + IsHeater = isHeater; } } diff --git a/Resources/Prototypes/Entities/Structures/Piping/Atmospherics/unary.yml b/Resources/Prototypes/Entities/Structures/Piping/Atmospherics/unary.yml index e50e256e9c..fa628b2932 100644 --- a/Resources/Prototypes/Entities/Structures/Piping/Atmospherics/unary.yml +++ b/Resources/Prototypes/Entities/Structures/Piping/Atmospherics/unary.yml @@ -284,7 +284,7 @@ True: { state: freezerOn } False: { state: freezerOff } - type: GasThermoMachine - mode: Freezer + coefficientOfPerformance: -3.9 - type: ApcPowerReceiver powerDisabled: true #starts off - type: Machine @@ -327,7 +327,7 @@ True: { state: heaterOn } False: { state: heaterOff } - type: GasThermoMachine - mode: Heater + coefficientOfPerformance: 0.95 - type: ApcPowerReceiver powerDisabled: true #starts off - type: Machine