using System.Linq; using Content.Shared.Movement.Components; using Content.Shared.Movement.Systems; using Content.Shared.Temperature.Components; using Content.Shared.Temperature.HeatContainer; using Robust.Shared.Physics.Components; using Robust.Shared.Physics.Events; using Robust.Shared.Timing; namespace Content.Shared.Temperature.Systems; /// /// This handles predicting temperature based speedup. /// public abstract partial class SharedTemperatureSystem : EntitySystem { [Dependency] private IGameTiming _timing = default!; [Dependency] private MovementSpeedModifierSystem _movementSpeedModifier = default!; [Dependency] protected EntityQuery TemperatureQuery = default!; /// /// Band-aid for unpredicted atmos. Delays the application for a short period so that laggy clients can get the replicated temperature. /// private static readonly TimeSpan SlowdownApplicationDelay = TimeSpan.FromSeconds(1f); public override void Initialize() { base.Initialize(); SubscribeLocalEvent(OnMapInit); SubscribeLocalEvent(OnMassDataChanged); SubscribeLocalEvent(OnTemperatureChanged); SubscribeLocalEvent(OnRefreshMovementSpeedModifiers); } /// /// Calculates the HeatCapacity for this entity based on its SpecificHeat and mass. /// This is a carryover from the old TemperatureSystem. /// If this entity does not have a , then it assumes it's 1kg. /// protected virtual void OnMapInit(Entity entity, ref MapInitEvent args) { // We calculate the heat capacity for our entity by multiplying its mass by its specific heat. // If the entity has no mass, we assume it's 1 kilogram. var mass = CompOrNull(entity)?.FixturesMass ?? 1f; // TODO: This assumes this is temperature for the whole body, but ideally we want it split into surface and internal temperature! entity.Comp.HeatCapacity = mass * entity.Comp.SpecificHeat; } private void OnMassDataChanged(Entity entity, ref MassDataChangedEvent args) { entity.Comp.HeatCapacity = args.NewMass * entity.Comp.SpecificHeat; } private void OnTemperatureChanged(Entity ent, ref TemperatureChangedEvent args) { foreach (var (threshold, modifier) in ent.Comp.Thresholds) { if (args.CurrentTemperature < threshold && args.LastTemperature > threshold || args.CurrentTemperature > threshold && args.LastTemperature < threshold) { ent.Comp.NextSlowdownUpdate = _timing.CurTime + SlowdownApplicationDelay; ent.Comp.CurrentSpeedModifier = modifier; Dirty(ent); break; } } var maxThreshold = ent.Comp.Thresholds.Max(p => p.Key); if (args.CurrentTemperature > maxThreshold && args.LastTemperature < maxThreshold) { ent.Comp.NextSlowdownUpdate = _timing.CurTime + SlowdownApplicationDelay; ent.Comp.CurrentSpeedModifier = null; Dirty(ent); } } private void OnRefreshMovementSpeedModifiers(Entity ent, ref RefreshMovementSpeedModifiersEvent args) { // Don't update speed and mispredict while we're compensating for lag. if (ent.Comp.NextSlowdownUpdate != null || ent.Comp.CurrentSpeedModifier == null) return; args.ModifySpeed(ent.Comp.CurrentSpeedModifier.Value, ent.Comp.CurrentSpeedModifier.Value); } public override void Update(float frameTime) { base.Update(frameTime); var query = EntityQueryEnumerator(); while (query.MoveNext(out var uid, out var temp, out var movement)) { if (temp.NextSlowdownUpdate == null) continue; if (_timing.CurTime < temp.NextSlowdownUpdate) continue; temp.NextSlowdownUpdate = null; _movementSpeedModifier.RefreshMovementSpeedModifiers((uid, movement)); Dirty(uid, temp); } } /// /// Conducts heat between an entity with TemperatureComponent and another /// /// Entity we're conducting heat with /// Heat container which is conducting heat with our entity /// The amount of time that the heat is allowed to conduct, in seconds. This value should be small. /// An optional heat transfer modifier for this exchange /// Whether we should avoid raising an event which checks for conduction modifiers on our entity. /// Returns the amount of heat exchanged, in Joules. A positive value means the entity lost heat energy. public float ConductHeat(Entity entity, ref T heatContainer, float deltaT, float heatTransferMod = 1f, bool ignoreHeatResistance = false) where T : IHeatContainer { if (!TemperatureQuery.Resolve(entity, ref entity.Comp, false) || MathHelper.CloseTo(entity.Comp.Temperature, heatContainer.Temperature)) return 0f; var conductance = entity.Comp.ThermalConductivity * heatTransferMod; if (!ignoreHeatResistance) { var ev = new BeforeHeatExchangeEvent(); RaiseLocalEvent(entity, ref ev); conductance *= ev.HeatTransferModifier; } var lastTemp = entity.Comp.Temperature; var heatEx = HeatContainerHelpers.ConductHeat(ref entity.Comp, ref heatContainer, deltaT, conductance); var changeEv = new TemperatureChangedEvent(entity.Comp.Temperature, lastTemp); RaiseLocalEvent(entity, ref changeEv, broadcast: true); return heatEx; } /// /// Conducts heat for an entity with a TemperatureComponent to a source at a fixed temperature. /// /// Entity we're conducting heat with /// Temperature this entity is being exposed to /// The amount of time that the heat is allowed to conduct, in seconds. This value should be small. /// An optional heat transfer modifier for this exchange /// Whether we should avoid raising an event which checks for conduction modifiers on our entity. /// Returns the amount of heat exchanged, in Joules. A positive value means the entity lost heat energy. public float ConductHeat(Entity entity, float temperature, float deltaT, float heatTransferMod = 1f, bool ignoreHeatResistance = false) { if (!TemperatureQuery.Resolve(entity, ref entity.Comp, false) || MathHelper.CloseTo(entity.Comp.Temperature, temperature)) return 0f; var conductance = entity.Comp.ThermalConductivity * heatTransferMod; if (!ignoreHeatResistance) { var ev = new BeforeHeatExchangeEvent(); RaiseLocalEvent(entity, ref ev); conductance *= ev.HeatTransferModifier; } var lastTemp = entity.Comp.Temperature; var heatEx = HeatContainerHelpers.ConductHeat(ref entity.Comp, temperature, deltaT, conductance); var changeEv = new TemperatureChangedEvent(entity.Comp.Temperature, lastTemp); RaiseLocalEvent(entity, ref changeEv, broadcast: true); return heatEx; } /// /// Adds or removes the specified amount of heat from an entity. /// /// Entity whose temperature we're modifying. /// The change in heat. /// Whether we should avoid raising an event to modify the conductance of our entity. /// Returns the amount of heat that was actually added, after applying heat resistances. public float ChangeHeat(Entity entity, float heatAmount, bool ignoreHeatResistance = false) { if (!TemperatureQuery.Resolve(entity, ref entity.Comp, false) || heatAmount == 0f) return 0f; if (!ignoreHeatResistance) { var ev = new BeforeHeatExchangeEvent(); RaiseLocalEvent(entity, ref ev ); heatAmount *= ev.HeatTransferModifier; } var lastTemp = entity.Comp.Temperature; HeatContainerHelpers.AddHeat(ref entity.Comp, heatAmount); var changeEv = new TemperatureChangedEvent(entity.Comp.Temperature, lastTemp); RaiseLocalEvent(entity, ref changeEv, broadcast: true); return heatAmount; } /// /// Protected because you should not be calling this. Sets heat to a new value. /// protected void SetTemperature(Entity entity, float temp) { if (!TemperatureQuery.Resolve(entity, ref entity.Comp)) return; var lastTemp = entity.Comp.Temperature; entity.Comp.Temperature = temp; var changeEv = new TemperatureChangedEvent(entity.Comp.Temperature, lastTemp); RaiseLocalEvent(entity, ref changeEv, broadcast: true); } }