TemperatureSystem.cs 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428
  1. using System.Linq;
  2. using Content.Server.Administration.Logs;
  3. using Content.Server.Atmos.EntitySystems;
  4. using Content.Server.Body.Components;
  5. using Content.Server.Temperature.Components;
  6. using Content.Shared.Alert;
  7. using Content.Shared.Atmos;
  8. using Content.Shared.Damage;
  9. using Content.Shared.Database;
  10. using Content.Shared.Inventory;
  11. using Content.Shared.Rejuvenate;
  12. using Content.Shared.Temperature;
  13. using Robust.Shared.Physics.Components;
  14. using Robust.Shared.Prototypes;
  15. using Robust.Shared.Physics.Events;
  16. using Content.Shared.Projectiles;
  17. namespace Content.Server.Temperature.Systems;
  18. public sealed class TemperatureSystem : EntitySystem
  19. {
  20. [Dependency] private readonly AlertsSystem _alerts = default!;
  21. [Dependency] private readonly AtmosphereSystem _atmosphere = default!;
  22. [Dependency] private readonly DamageableSystem _damageable = default!;
  23. [Dependency] private readonly IAdminLogManager _adminLogger = default!;
  24. [Dependency] private readonly TemperatureSystem _temperature = default!;
  25. /// <summary>
  26. /// All the components that will have their damage updated at the end of the tick.
  27. /// This is done because both AtmosExposed and Flammable call ChangeHeat in the same tick, meaning
  28. /// that we need some mechanism to ensure it doesn't double dip on damage for both calls.
  29. /// </summary>
  30. public HashSet<Entity<TemperatureComponent>> ShouldUpdateDamage = new();
  31. public float UpdateInterval = 1.0f;
  32. private float _accumulatedFrametime;
  33. [ValidatePrototypeId<AlertCategoryPrototype>]
  34. public const string TemperatureAlertCategory = "Temperature";
  35. public override void Initialize()
  36. {
  37. SubscribeLocalEvent<TemperatureComponent, OnTemperatureChangeEvent>(EnqueueDamage);
  38. SubscribeLocalEvent<TemperatureComponent, AtmosExposedUpdateEvent>(OnAtmosExposedUpdate);
  39. SubscribeLocalEvent<TemperatureComponent, RejuvenateEvent>(OnRejuvenate);
  40. SubscribeLocalEvent<AlertsComponent, OnTemperatureChangeEvent>(ServerAlert);
  41. SubscribeLocalEvent<TemperatureProtectionComponent, InventoryRelayedEvent<ModifyChangedTemperatureEvent>>(
  42. OnTemperatureChangeAttempt);
  43. SubscribeLocalEvent<InternalTemperatureComponent, MapInitEvent>(OnInit);
  44. SubscribeLocalEvent<ChangeTemperatureOnCollideComponent, ProjectileHitEvent>(ChangeTemperatureOnCollide);
  45. // Allows overriding thresholds based on the parent's thresholds.
  46. SubscribeLocalEvent<TemperatureComponent, EntParentChangedMessage>(OnParentChange);
  47. SubscribeLocalEvent<ContainerTemperatureDamageThresholdsComponent, ComponentStartup>(
  48. OnParentThresholdStartup);
  49. SubscribeLocalEvent<ContainerTemperatureDamageThresholdsComponent, ComponentShutdown>(
  50. OnParentThresholdShutdown);
  51. }
  52. public override void Update(float frameTime)
  53. {
  54. base.Update(frameTime);
  55. // conduct heat from the surface to the inside of entities with internal temperatures
  56. var query = EntityQueryEnumerator<InternalTemperatureComponent, TemperatureComponent>();
  57. while (query.MoveNext(out var uid, out var comp, out var temp))
  58. {
  59. // don't do anything if they equalised
  60. var diff = Math.Abs(temp.CurrentTemperature - comp.Temperature);
  61. if (diff < 0.1f)
  62. continue;
  63. // heat flow in W/m^2 as per fourier's law in 1D.
  64. var q = comp.Conductivity * diff / comp.Thickness;
  65. // convert to J then K
  66. var joules = q * comp.Area * frameTime;
  67. var degrees = joules / GetHeatCapacity(uid, temp);
  68. if (temp.CurrentTemperature < comp.Temperature)
  69. degrees *= -1;
  70. // exchange heat between inside and surface
  71. comp.Temperature += degrees;
  72. ForceChangeTemperature(uid, temp.CurrentTemperature - degrees, temp);
  73. }
  74. UpdateDamage(frameTime);
  75. }
  76. private void UpdateDamage(float frameTime)
  77. {
  78. _accumulatedFrametime += frameTime;
  79. if (_accumulatedFrametime < UpdateInterval)
  80. return;
  81. _accumulatedFrametime -= UpdateInterval;
  82. if (!ShouldUpdateDamage.Any())
  83. return;
  84. foreach (var comp in ShouldUpdateDamage)
  85. {
  86. MetaDataComponent? metaData = null;
  87. var uid = comp.Owner;
  88. if (Deleted(uid, metaData) || Paused(uid, metaData))
  89. continue;
  90. ChangeDamage(uid, comp);
  91. }
  92. ShouldUpdateDamage.Clear();
  93. }
  94. public void ForceChangeTemperature(EntityUid uid, float temp, TemperatureComponent? temperature = null)
  95. {
  96. if (!Resolve(uid, ref temperature))
  97. return;
  98. float lastTemp = temperature.CurrentTemperature;
  99. float delta = temperature.CurrentTemperature - temp;
  100. temperature.CurrentTemperature = temp;
  101. RaiseLocalEvent(uid, new OnTemperatureChangeEvent(temperature.CurrentTemperature, lastTemp, delta),
  102. true);
  103. }
  104. public void ChangeHeat(EntityUid uid, float heatAmount, bool ignoreHeatResistance = false,
  105. TemperatureComponent? temperature = null)
  106. {
  107. if (!Resolve(uid, ref temperature, false))
  108. return;
  109. if (!ignoreHeatResistance)
  110. {
  111. var ev = new ModifyChangedTemperatureEvent(heatAmount);
  112. RaiseLocalEvent(uid, ev);
  113. heatAmount = ev.TemperatureDelta;
  114. }
  115. float lastTemp = temperature.CurrentTemperature;
  116. temperature.CurrentTemperature += heatAmount / GetHeatCapacity(uid, temperature);
  117. float delta = temperature.CurrentTemperature - lastTemp;
  118. RaiseLocalEvent(uid, new OnTemperatureChangeEvent(temperature.CurrentTemperature, lastTemp, delta), true);
  119. }
  120. private void OnAtmosExposedUpdate(EntityUid uid, TemperatureComponent temperature,
  121. ref AtmosExposedUpdateEvent args)
  122. {
  123. var transform = args.Transform;
  124. if (transform.MapUid == null)
  125. return;
  126. var temperatureDelta = args.GasMixture.Temperature - temperature.CurrentTemperature;
  127. var airHeatCapacity = _atmosphere.GetHeatCapacity(args.GasMixture, false);
  128. var heatCapacity = GetHeatCapacity(uid, temperature);
  129. var heat = temperatureDelta * (airHeatCapacity * heatCapacity /
  130. (airHeatCapacity + heatCapacity));
  131. ChangeHeat(uid, heat * temperature.AtmosTemperatureTransferEfficiency, temperature: temperature);
  132. }
  133. public float GetHeatCapacity(EntityUid uid, TemperatureComponent? comp = null, PhysicsComponent? physics = null)
  134. {
  135. if (!Resolve(uid, ref comp) || !Resolve(uid, ref physics, false) || physics.FixturesMass <= 0)
  136. {
  137. return Atmospherics.MinimumHeatCapacity;
  138. }
  139. return comp.SpecificHeat * physics.FixturesMass;
  140. }
  141. private void OnInit(EntityUid uid, InternalTemperatureComponent comp, MapInitEvent args)
  142. {
  143. if (!TryComp<TemperatureComponent>(uid, out var temp))
  144. return;
  145. comp.Temperature = temp.CurrentTemperature;
  146. }
  147. private void OnRejuvenate(EntityUid uid, TemperatureComponent comp, RejuvenateEvent args)
  148. {
  149. ForceChangeTemperature(uid, Atmospherics.T20C, comp);
  150. }
  151. private void ServerAlert(EntityUid uid, AlertsComponent status, OnTemperatureChangeEvent args)
  152. {
  153. ProtoId<AlertPrototype> type;
  154. float threshold;
  155. float idealTemp;
  156. if (!TryComp<TemperatureComponent>(uid, out var temperature))
  157. {
  158. _alerts.ClearAlertCategory(uid, TemperatureAlertCategory);
  159. return;
  160. }
  161. if (TryComp<ThermalRegulatorComponent>(uid, out var regulator) &&
  162. regulator.NormalBodyTemperature > temperature.ColdDamageThreshold &&
  163. regulator.NormalBodyTemperature < temperature.HeatDamageThreshold)
  164. {
  165. idealTemp = regulator.NormalBodyTemperature;
  166. }
  167. else
  168. {
  169. idealTemp = (temperature.ColdDamageThreshold + temperature.HeatDamageThreshold) / 2;
  170. }
  171. if (args.CurrentTemperature <= idealTemp)
  172. {
  173. type = temperature.ColdAlert;
  174. threshold = temperature.ColdDamageThreshold;
  175. }
  176. else
  177. {
  178. type = temperature.HotAlert;
  179. threshold = temperature.HeatDamageThreshold;
  180. }
  181. // Calculates a scale where 1.0 is the ideal temperature and 0.0 is where temperature damage begins
  182. // The cold and hot scales will differ in their range if the ideal temperature is not exactly halfway between the thresholds
  183. var tempScale = (args.CurrentTemperature - threshold) / (idealTemp - threshold);
  184. switch (tempScale)
  185. {
  186. case <= 0f:
  187. _alerts.ShowAlert(uid, type, 3);
  188. break;
  189. case <= 0.4f:
  190. _alerts.ShowAlert(uid, type, 2);
  191. break;
  192. case <= 0.66f:
  193. _alerts.ShowAlert(uid, type, 1);
  194. break;
  195. case > 0.66f:
  196. _alerts.ClearAlertCategory(uid, TemperatureAlertCategory);
  197. break;
  198. }
  199. }
  200. private void EnqueueDamage(Entity<TemperatureComponent> temperature, ref OnTemperatureChangeEvent args)
  201. {
  202. ShouldUpdateDamage.Add(temperature);
  203. }
  204. private void ChangeDamage(EntityUid uid, TemperatureComponent temperature)
  205. {
  206. if (!HasComp<DamageableComponent>(uid))
  207. return;
  208. // See this link for where the scaling func comes from:
  209. // https://www.desmos.com/calculator/0vknqtdvq9
  210. // Based on a logistic curve, which caps out at MaxDamage
  211. var heatK = 0.005;
  212. var a = 1;
  213. var y = temperature.DamageCap;
  214. var c = y * 2;
  215. var heatDamageThreshold = temperature.ParentHeatDamageThreshold ?? temperature.HeatDamageThreshold;
  216. var coldDamageThreshold = temperature.ParentColdDamageThreshold ?? temperature.ColdDamageThreshold;
  217. if (temperature.CurrentTemperature >= heatDamageThreshold)
  218. {
  219. if (!temperature.TakingDamage)
  220. {
  221. _adminLogger.Add(LogType.Temperature, $"{ToPrettyString(uid):entity} started taking high temperature damage");
  222. temperature.TakingDamage = true;
  223. }
  224. var diff = Math.Abs(temperature.CurrentTemperature - heatDamageThreshold);
  225. var tempDamage = c / (1 + a * Math.Pow(Math.E, -heatK * diff)) - y;
  226. _damageable.TryChangeDamage(uid, temperature.HeatDamage * tempDamage, ignoreResistances: true, interruptsDoAfters: false);
  227. }
  228. else if (temperature.CurrentTemperature <= coldDamageThreshold)
  229. {
  230. if (!temperature.TakingDamage)
  231. {
  232. _adminLogger.Add(LogType.Temperature, $"{ToPrettyString(uid):entity} started taking low temperature damage");
  233. temperature.TakingDamage = true;
  234. }
  235. var diff = Math.Abs(temperature.CurrentTemperature - coldDamageThreshold);
  236. var tempDamage =
  237. Math.Sqrt(diff * (Math.Pow(temperature.DamageCap.Double(), 2) / coldDamageThreshold));
  238. _damageable.TryChangeDamage(uid, temperature.ColdDamage * tempDamage, ignoreResistances: true, interruptsDoAfters: false);
  239. }
  240. else if (temperature.TakingDamage)
  241. {
  242. _adminLogger.Add(LogType.Temperature, $"{ToPrettyString(uid):entity} stopped taking temperature damage");
  243. temperature.TakingDamage = false;
  244. }
  245. }
  246. private void OnTemperatureChangeAttempt(EntityUid uid, TemperatureProtectionComponent component,
  247. InventoryRelayedEvent<ModifyChangedTemperatureEvent> args)
  248. {
  249. var coefficient = args.Args.TemperatureDelta < 0
  250. ? component.CoolingCoefficient
  251. : component.HeatingCoefficient;
  252. var ev = new GetTemperatureProtectionEvent(coefficient);
  253. RaiseLocalEvent(uid, ref ev);
  254. args.Args.TemperatureDelta *= ev.Coefficient;
  255. }
  256. private void ChangeTemperatureOnCollide(Entity<ChangeTemperatureOnCollideComponent> ent, ref ProjectileHitEvent args)
  257. {
  258. _temperature.ChangeHeat(args.Target, ent.Comp.Heat, ent.Comp.IgnoreHeatResistance);// adjust the temperature
  259. }
  260. private void OnParentChange(EntityUid uid, TemperatureComponent component,
  261. ref EntParentChangedMessage args)
  262. {
  263. var temperatureQuery = GetEntityQuery<TemperatureComponent>();
  264. var transformQuery = GetEntityQuery<TransformComponent>();
  265. var thresholdsQuery = GetEntityQuery<ContainerTemperatureDamageThresholdsComponent>();
  266. // We only need to update thresholds if the thresholds changed for the entity's ancestors.
  267. var oldThresholds = args.OldParent != null
  268. ? RecalculateParentThresholds(args.OldParent.Value, transformQuery, thresholdsQuery)
  269. : (null, null);
  270. var newThresholds = RecalculateParentThresholds(transformQuery.GetComponent(uid).ParentUid, transformQuery, thresholdsQuery);
  271. if (oldThresholds != newThresholds)
  272. {
  273. RecursiveThresholdUpdate(uid, temperatureQuery, transformQuery, thresholdsQuery);
  274. }
  275. }
  276. private void OnParentThresholdStartup(EntityUid uid, ContainerTemperatureDamageThresholdsComponent component,
  277. ComponentStartup args)
  278. {
  279. RecursiveThresholdUpdate(uid, GetEntityQuery<TemperatureComponent>(), GetEntityQuery<TransformComponent>(),
  280. GetEntityQuery<ContainerTemperatureDamageThresholdsComponent>());
  281. }
  282. private void OnParentThresholdShutdown(EntityUid uid, ContainerTemperatureDamageThresholdsComponent component,
  283. ComponentShutdown args)
  284. {
  285. RecursiveThresholdUpdate(uid, GetEntityQuery<TemperatureComponent>(), GetEntityQuery<TransformComponent>(),
  286. GetEntityQuery<ContainerTemperatureDamageThresholdsComponent>());
  287. }
  288. /// <summary>
  289. /// Recalculate and apply parent thresholds for the root entity and all its descendant.
  290. /// </summary>
  291. /// <param name="root"></param>
  292. /// <param name="temperatureQuery"></param>
  293. /// <param name="transformQuery"></param>
  294. /// <param name="tempThresholdsQuery"></param>
  295. private void RecursiveThresholdUpdate(EntityUid root, EntityQuery<TemperatureComponent> temperatureQuery,
  296. EntityQuery<TransformComponent> transformQuery,
  297. EntityQuery<ContainerTemperatureDamageThresholdsComponent> tempThresholdsQuery)
  298. {
  299. RecalculateAndApplyParentThresholds(root, temperatureQuery, transformQuery, tempThresholdsQuery);
  300. var enumerator = Transform(root).ChildEnumerator;
  301. while (enumerator.MoveNext(out var child))
  302. {
  303. RecursiveThresholdUpdate(child, temperatureQuery, transformQuery, tempThresholdsQuery);
  304. }
  305. }
  306. /// <summary>
  307. /// Recalculate parent thresholds and apply them on the uid temperature component.
  308. /// </summary>
  309. /// <param name="uid"></param>
  310. /// <param name="temperatureQuery"></param>
  311. /// <param name="transformQuery"></param>
  312. /// <param name="tempThresholdsQuery"></param>
  313. private void RecalculateAndApplyParentThresholds(EntityUid uid,
  314. EntityQuery<TemperatureComponent> temperatureQuery, EntityQuery<TransformComponent> transformQuery,
  315. EntityQuery<ContainerTemperatureDamageThresholdsComponent> tempThresholdsQuery)
  316. {
  317. if (!temperatureQuery.TryGetComponent(uid, out var temperature))
  318. {
  319. return;
  320. }
  321. var newThresholds = RecalculateParentThresholds(transformQuery.GetComponent(uid).ParentUid, transformQuery, tempThresholdsQuery);
  322. temperature.ParentHeatDamageThreshold = newThresholds.Item1;
  323. temperature.ParentColdDamageThreshold = newThresholds.Item2;
  324. }
  325. /// <summary>
  326. /// Recalculate Parent Heat/Cold DamageThreshold by recursively checking each ancestor and fetching the
  327. /// maximum HeatDamageThreshold and the minimum ColdDamageThreshold if any exists (aka the best value for each).
  328. /// </summary>
  329. /// <param name="initialParentUid"></param>
  330. /// <param name="transformQuery"></param>
  331. /// <param name="tempThresholdsQuery"></param>
  332. private (float?, float?) RecalculateParentThresholds(
  333. EntityUid initialParentUid,
  334. EntityQuery<TransformComponent> transformQuery,
  335. EntityQuery<ContainerTemperatureDamageThresholdsComponent> tempThresholdsQuery)
  336. {
  337. // Recursively check parents for the best threshold available
  338. var parentUid = initialParentUid;
  339. float? newHeatThreshold = null;
  340. float? newColdThreshold = null;
  341. while (parentUid.IsValid())
  342. {
  343. if (tempThresholdsQuery.TryGetComponent(parentUid, out var newThresholds))
  344. {
  345. if (newThresholds.HeatDamageThreshold != null)
  346. {
  347. newHeatThreshold = Math.Max(newThresholds.HeatDamageThreshold.Value,
  348. newHeatThreshold ?? 0);
  349. }
  350. if (newThresholds.ColdDamageThreshold != null)
  351. {
  352. newColdThreshold = Math.Min(newThresholds.ColdDamageThreshold.Value,
  353. newColdThreshold ?? float.MaxValue);
  354. }
  355. }
  356. parentUid = transformQuery.GetComponent(parentUid).ParentUid;
  357. }
  358. return (newHeatThreshold, newColdThreshold);
  359. }
  360. }