| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428 |
- using System.Linq;
- using Content.Server.Administration.Logs;
- using Content.Server.Atmos.EntitySystems;
- using Content.Server.Body.Components;
- using Content.Server.Temperature.Components;
- using Content.Shared.Alert;
- using Content.Shared.Atmos;
- using Content.Shared.Damage;
- using Content.Shared.Database;
- using Content.Shared.Inventory;
- using Content.Shared.Rejuvenate;
- using Content.Shared.Temperature;
- using Robust.Shared.Physics.Components;
- using Robust.Shared.Prototypes;
- using Robust.Shared.Physics.Events;
- using Content.Shared.Projectiles;
- namespace Content.Server.Temperature.Systems;
- public sealed class TemperatureSystem : EntitySystem
- {
- [Dependency] private readonly AlertsSystem _alerts = default!;
- [Dependency] private readonly AtmosphereSystem _atmosphere = default!;
- [Dependency] private readonly DamageableSystem _damageable = default!;
- [Dependency] private readonly IAdminLogManager _adminLogger = default!;
- [Dependency] private readonly TemperatureSystem _temperature = default!;
- /// <summary>
- /// All the components that will have their damage updated at the end of the tick.
- /// This is done because both AtmosExposed and Flammable call ChangeHeat in the same tick, meaning
- /// that we need some mechanism to ensure it doesn't double dip on damage for both calls.
- /// </summary>
- public HashSet<Entity<TemperatureComponent>> ShouldUpdateDamage = new();
- public float UpdateInterval = 1.0f;
- private float _accumulatedFrametime;
- [ValidatePrototypeId<AlertCategoryPrototype>]
- public const string TemperatureAlertCategory = "Temperature";
- public override void Initialize()
- {
- SubscribeLocalEvent<TemperatureComponent, OnTemperatureChangeEvent>(EnqueueDamage);
- SubscribeLocalEvent<TemperatureComponent, AtmosExposedUpdateEvent>(OnAtmosExposedUpdate);
- SubscribeLocalEvent<TemperatureComponent, RejuvenateEvent>(OnRejuvenate);
- SubscribeLocalEvent<AlertsComponent, OnTemperatureChangeEvent>(ServerAlert);
- SubscribeLocalEvent<TemperatureProtectionComponent, InventoryRelayedEvent<ModifyChangedTemperatureEvent>>(
- OnTemperatureChangeAttempt);
- SubscribeLocalEvent<InternalTemperatureComponent, MapInitEvent>(OnInit);
- SubscribeLocalEvent<ChangeTemperatureOnCollideComponent, ProjectileHitEvent>(ChangeTemperatureOnCollide);
- // Allows overriding thresholds based on the parent's thresholds.
- SubscribeLocalEvent<TemperatureComponent, EntParentChangedMessage>(OnParentChange);
- SubscribeLocalEvent<ContainerTemperatureDamageThresholdsComponent, ComponentStartup>(
- OnParentThresholdStartup);
- SubscribeLocalEvent<ContainerTemperatureDamageThresholdsComponent, ComponentShutdown>(
- OnParentThresholdShutdown);
- }
- public override void Update(float frameTime)
- {
- base.Update(frameTime);
- // conduct heat from the surface to the inside of entities with internal temperatures
- var query = EntityQueryEnumerator<InternalTemperatureComponent, TemperatureComponent>();
- while (query.MoveNext(out var uid, out var comp, out var temp))
- {
- // don't do anything if they equalised
- var diff = Math.Abs(temp.CurrentTemperature - comp.Temperature);
- if (diff < 0.1f)
- continue;
- // heat flow in W/m^2 as per fourier's law in 1D.
- var q = comp.Conductivity * diff / comp.Thickness;
- // convert to J then K
- var joules = q * comp.Area * frameTime;
- var degrees = joules / GetHeatCapacity(uid, temp);
- if (temp.CurrentTemperature < comp.Temperature)
- degrees *= -1;
- // exchange heat between inside and surface
- comp.Temperature += degrees;
- ForceChangeTemperature(uid, temp.CurrentTemperature - degrees, temp);
- }
- UpdateDamage(frameTime);
- }
- private void UpdateDamage(float frameTime)
- {
- _accumulatedFrametime += frameTime;
- if (_accumulatedFrametime < UpdateInterval)
- return;
- _accumulatedFrametime -= UpdateInterval;
- if (!ShouldUpdateDamage.Any())
- return;
- foreach (var comp in ShouldUpdateDamage)
- {
- MetaDataComponent? metaData = null;
- var uid = comp.Owner;
- if (Deleted(uid, metaData) || Paused(uid, metaData))
- continue;
- ChangeDamage(uid, comp);
- }
- ShouldUpdateDamage.Clear();
- }
- public void ForceChangeTemperature(EntityUid uid, float temp, TemperatureComponent? temperature = null)
- {
- if (!Resolve(uid, ref temperature))
- return;
- float lastTemp = temperature.CurrentTemperature;
- float delta = temperature.CurrentTemperature - temp;
- temperature.CurrentTemperature = temp;
- RaiseLocalEvent(uid, new OnTemperatureChangeEvent(temperature.CurrentTemperature, lastTemp, delta),
- true);
- }
- public void ChangeHeat(EntityUid uid, float heatAmount, bool ignoreHeatResistance = false,
- TemperatureComponent? temperature = null)
- {
- if (!Resolve(uid, ref temperature, false))
- return;
- if (!ignoreHeatResistance)
- {
- var ev = new ModifyChangedTemperatureEvent(heatAmount);
- RaiseLocalEvent(uid, ev);
- heatAmount = ev.TemperatureDelta;
- }
- float lastTemp = temperature.CurrentTemperature;
- temperature.CurrentTemperature += heatAmount / GetHeatCapacity(uid, temperature);
- float delta = temperature.CurrentTemperature - lastTemp;
- RaiseLocalEvent(uid, new OnTemperatureChangeEvent(temperature.CurrentTemperature, lastTemp, delta), true);
- }
- private void OnAtmosExposedUpdate(EntityUid uid, TemperatureComponent temperature,
- ref AtmosExposedUpdateEvent args)
- {
- var transform = args.Transform;
- if (transform.MapUid == null)
- return;
- var temperatureDelta = args.GasMixture.Temperature - temperature.CurrentTemperature;
- var airHeatCapacity = _atmosphere.GetHeatCapacity(args.GasMixture, false);
- var heatCapacity = GetHeatCapacity(uid, temperature);
- var heat = temperatureDelta * (airHeatCapacity * heatCapacity /
- (airHeatCapacity + heatCapacity));
- ChangeHeat(uid, heat * temperature.AtmosTemperatureTransferEfficiency, temperature: temperature);
- }
- public float GetHeatCapacity(EntityUid uid, TemperatureComponent? comp = null, PhysicsComponent? physics = null)
- {
- if (!Resolve(uid, ref comp) || !Resolve(uid, ref physics, false) || physics.FixturesMass <= 0)
- {
- return Atmospherics.MinimumHeatCapacity;
- }
- return comp.SpecificHeat * physics.FixturesMass;
- }
- private void OnInit(EntityUid uid, InternalTemperatureComponent comp, MapInitEvent args)
- {
- if (!TryComp<TemperatureComponent>(uid, out var temp))
- return;
- comp.Temperature = temp.CurrentTemperature;
- }
- private void OnRejuvenate(EntityUid uid, TemperatureComponent comp, RejuvenateEvent args)
- {
- ForceChangeTemperature(uid, Atmospherics.T20C, comp);
- }
- private void ServerAlert(EntityUid uid, AlertsComponent status, OnTemperatureChangeEvent args)
- {
- ProtoId<AlertPrototype> type;
- float threshold;
- float idealTemp;
- if (!TryComp<TemperatureComponent>(uid, out var temperature))
- {
- _alerts.ClearAlertCategory(uid, TemperatureAlertCategory);
- return;
- }
- if (TryComp<ThermalRegulatorComponent>(uid, out var regulator) &&
- regulator.NormalBodyTemperature > temperature.ColdDamageThreshold &&
- regulator.NormalBodyTemperature < temperature.HeatDamageThreshold)
- {
- idealTemp = regulator.NormalBodyTemperature;
- }
- else
- {
- idealTemp = (temperature.ColdDamageThreshold + temperature.HeatDamageThreshold) / 2;
- }
- if (args.CurrentTemperature <= idealTemp)
- {
- type = temperature.ColdAlert;
- threshold = temperature.ColdDamageThreshold;
- }
- else
- {
- type = temperature.HotAlert;
- threshold = temperature.HeatDamageThreshold;
- }
- // Calculates a scale where 1.0 is the ideal temperature and 0.0 is where temperature damage begins
- // The cold and hot scales will differ in their range if the ideal temperature is not exactly halfway between the thresholds
- var tempScale = (args.CurrentTemperature - threshold) / (idealTemp - threshold);
- switch (tempScale)
- {
- case <= 0f:
- _alerts.ShowAlert(uid, type, 3);
- break;
- case <= 0.4f:
- _alerts.ShowAlert(uid, type, 2);
- break;
- case <= 0.66f:
- _alerts.ShowAlert(uid, type, 1);
- break;
- case > 0.66f:
- _alerts.ClearAlertCategory(uid, TemperatureAlertCategory);
- break;
- }
- }
- private void EnqueueDamage(Entity<TemperatureComponent> temperature, ref OnTemperatureChangeEvent args)
- {
- ShouldUpdateDamage.Add(temperature);
- }
- private void ChangeDamage(EntityUid uid, TemperatureComponent temperature)
- {
- if (!HasComp<DamageableComponent>(uid))
- return;
- // See this link for where the scaling func comes from:
- // https://www.desmos.com/calculator/0vknqtdvq9
- // Based on a logistic curve, which caps out at MaxDamage
- var heatK = 0.005;
- var a = 1;
- var y = temperature.DamageCap;
- var c = y * 2;
- var heatDamageThreshold = temperature.ParentHeatDamageThreshold ?? temperature.HeatDamageThreshold;
- var coldDamageThreshold = temperature.ParentColdDamageThreshold ?? temperature.ColdDamageThreshold;
- if (temperature.CurrentTemperature >= heatDamageThreshold)
- {
- if (!temperature.TakingDamage)
- {
- _adminLogger.Add(LogType.Temperature, $"{ToPrettyString(uid):entity} started taking high temperature damage");
- temperature.TakingDamage = true;
- }
- var diff = Math.Abs(temperature.CurrentTemperature - heatDamageThreshold);
- var tempDamage = c / (1 + a * Math.Pow(Math.E, -heatK * diff)) - y;
- _damageable.TryChangeDamage(uid, temperature.HeatDamage * tempDamage, ignoreResistances: true, interruptsDoAfters: false);
- }
- else if (temperature.CurrentTemperature <= coldDamageThreshold)
- {
- if (!temperature.TakingDamage)
- {
- _adminLogger.Add(LogType.Temperature, $"{ToPrettyString(uid):entity} started taking low temperature damage");
- temperature.TakingDamage = true;
- }
- var diff = Math.Abs(temperature.CurrentTemperature - coldDamageThreshold);
- var tempDamage =
- Math.Sqrt(diff * (Math.Pow(temperature.DamageCap.Double(), 2) / coldDamageThreshold));
- _damageable.TryChangeDamage(uid, temperature.ColdDamage * tempDamage, ignoreResistances: true, interruptsDoAfters: false);
- }
- else if (temperature.TakingDamage)
- {
- _adminLogger.Add(LogType.Temperature, $"{ToPrettyString(uid):entity} stopped taking temperature damage");
- temperature.TakingDamage = false;
- }
- }
- private void OnTemperatureChangeAttempt(EntityUid uid, TemperatureProtectionComponent component,
- InventoryRelayedEvent<ModifyChangedTemperatureEvent> args)
- {
- var coefficient = args.Args.TemperatureDelta < 0
- ? component.CoolingCoefficient
- : component.HeatingCoefficient;
- var ev = new GetTemperatureProtectionEvent(coefficient);
- RaiseLocalEvent(uid, ref ev);
- args.Args.TemperatureDelta *= ev.Coefficient;
- }
- private void ChangeTemperatureOnCollide(Entity<ChangeTemperatureOnCollideComponent> ent, ref ProjectileHitEvent args)
- {
- _temperature.ChangeHeat(args.Target, ent.Comp.Heat, ent.Comp.IgnoreHeatResistance);// adjust the temperature
- }
- private void OnParentChange(EntityUid uid, TemperatureComponent component,
- ref EntParentChangedMessage args)
- {
- var temperatureQuery = GetEntityQuery<TemperatureComponent>();
- var transformQuery = GetEntityQuery<TransformComponent>();
- var thresholdsQuery = GetEntityQuery<ContainerTemperatureDamageThresholdsComponent>();
- // We only need to update thresholds if the thresholds changed for the entity's ancestors.
- var oldThresholds = args.OldParent != null
- ? RecalculateParentThresholds(args.OldParent.Value, transformQuery, thresholdsQuery)
- : (null, null);
- var newThresholds = RecalculateParentThresholds(transformQuery.GetComponent(uid).ParentUid, transformQuery, thresholdsQuery);
- if (oldThresholds != newThresholds)
- {
- RecursiveThresholdUpdate(uid, temperatureQuery, transformQuery, thresholdsQuery);
- }
- }
- private void OnParentThresholdStartup(EntityUid uid, ContainerTemperatureDamageThresholdsComponent component,
- ComponentStartup args)
- {
- RecursiveThresholdUpdate(uid, GetEntityQuery<TemperatureComponent>(), GetEntityQuery<TransformComponent>(),
- GetEntityQuery<ContainerTemperatureDamageThresholdsComponent>());
- }
- private void OnParentThresholdShutdown(EntityUid uid, ContainerTemperatureDamageThresholdsComponent component,
- ComponentShutdown args)
- {
- RecursiveThresholdUpdate(uid, GetEntityQuery<TemperatureComponent>(), GetEntityQuery<TransformComponent>(),
- GetEntityQuery<ContainerTemperatureDamageThresholdsComponent>());
- }
- /// <summary>
- /// Recalculate and apply parent thresholds for the root entity and all its descendant.
- /// </summary>
- /// <param name="root"></param>
- /// <param name="temperatureQuery"></param>
- /// <param name="transformQuery"></param>
- /// <param name="tempThresholdsQuery"></param>
- private void RecursiveThresholdUpdate(EntityUid root, EntityQuery<TemperatureComponent> temperatureQuery,
- EntityQuery<TransformComponent> transformQuery,
- EntityQuery<ContainerTemperatureDamageThresholdsComponent> tempThresholdsQuery)
- {
- RecalculateAndApplyParentThresholds(root, temperatureQuery, transformQuery, tempThresholdsQuery);
- var enumerator = Transform(root).ChildEnumerator;
- while (enumerator.MoveNext(out var child))
- {
- RecursiveThresholdUpdate(child, temperatureQuery, transformQuery, tempThresholdsQuery);
- }
- }
- /// <summary>
- /// Recalculate parent thresholds and apply them on the uid temperature component.
- /// </summary>
- /// <param name="uid"></param>
- /// <param name="temperatureQuery"></param>
- /// <param name="transformQuery"></param>
- /// <param name="tempThresholdsQuery"></param>
- private void RecalculateAndApplyParentThresholds(EntityUid uid,
- EntityQuery<TemperatureComponent> temperatureQuery, EntityQuery<TransformComponent> transformQuery,
- EntityQuery<ContainerTemperatureDamageThresholdsComponent> tempThresholdsQuery)
- {
- if (!temperatureQuery.TryGetComponent(uid, out var temperature))
- {
- return;
- }
- var newThresholds = RecalculateParentThresholds(transformQuery.GetComponent(uid).ParentUid, transformQuery, tempThresholdsQuery);
- temperature.ParentHeatDamageThreshold = newThresholds.Item1;
- temperature.ParentColdDamageThreshold = newThresholds.Item2;
- }
- /// <summary>
- /// Recalculate Parent Heat/Cold DamageThreshold by recursively checking each ancestor and fetching the
- /// maximum HeatDamageThreshold and the minimum ColdDamageThreshold if any exists (aka the best value for each).
- /// </summary>
- /// <param name="initialParentUid"></param>
- /// <param name="transformQuery"></param>
- /// <param name="tempThresholdsQuery"></param>
- private (float?, float?) RecalculateParentThresholds(
- EntityUid initialParentUid,
- EntityQuery<TransformComponent> transformQuery,
- EntityQuery<ContainerTemperatureDamageThresholdsComponent> tempThresholdsQuery)
- {
- // Recursively check parents for the best threshold available
- var parentUid = initialParentUid;
- float? newHeatThreshold = null;
- float? newColdThreshold = null;
- while (parentUid.IsValid())
- {
- if (tempThresholdsQuery.TryGetComponent(parentUid, out var newThresholds))
- {
- if (newThresholds.HeatDamageThreshold != null)
- {
- newHeatThreshold = Math.Max(newThresholds.HeatDamageThreshold.Value,
- newHeatThreshold ?? 0);
- }
- if (newThresholds.ColdDamageThreshold != null)
- {
- newColdThreshold = Math.Min(newThresholds.ColdDamageThreshold.Value,
- newColdThreshold ?? float.MaxValue);
- }
- }
- parentUid = transformQuery.GetComponent(parentUid).ParentUid;
- }
- return (newHeatThreshold, newColdThreshold);
- }
- }
|