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);
}
}