RespiratorSystem.cs 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354
  1. using Content.Server.Administration.Logs;
  2. using Content.Server.Atmos.EntitySystems;
  3. using Content.Server.Body.Components;
  4. using Content.Server.Chat.Systems;
  5. using Content.Server.EntityEffects.EffectConditions;
  6. using Content.Server.EntityEffects.Effects;
  7. using Content.Shared.Chemistry.EntitySystems;
  8. using Content.Shared.Alert;
  9. using Content.Shared.Atmos;
  10. using Content.Shared.Body.Components;
  11. using Content.Shared.Body.Prototypes;
  12. using Content.Shared.Chemistry.Components;
  13. using Content.Shared.Chemistry.Reagent;
  14. using Content.Shared.Damage;
  15. using Content.Shared.Database;
  16. using Content.Shared.EntityEffects;
  17. using Content.Shared.Mobs.Systems;
  18. using JetBrains.Annotations;
  19. using Robust.Shared.Prototypes;
  20. using Robust.Shared.Timing;
  21. namespace Content.Server.Body.Systems;
  22. [UsedImplicitly]
  23. public sealed class RespiratorSystem : EntitySystem
  24. {
  25. [Dependency] private readonly IAdminLogManager _adminLogger = default!;
  26. [Dependency] private readonly IGameTiming _gameTiming = default!;
  27. [Dependency] private readonly AlertsSystem _alertsSystem = default!;
  28. [Dependency] private readonly AtmosphereSystem _atmosSys = default!;
  29. [Dependency] private readonly BodySystem _bodySystem = default!;
  30. [Dependency] private readonly DamageableSystem _damageableSys = default!;
  31. [Dependency] private readonly LungSystem _lungSystem = default!;
  32. [Dependency] private readonly MobStateSystem _mobState = default!;
  33. [Dependency] private readonly IPrototypeManager _protoMan = default!;
  34. [Dependency] private readonly SharedSolutionContainerSystem _solutionContainerSystem = default!;
  35. [Dependency] private readonly ChatSystem _chat = default!;
  36. private static readonly ProtoId<MetabolismGroupPrototype> GasId = new("Gas");
  37. public override void Initialize()
  38. {
  39. base.Initialize();
  40. // We want to process lung reagents before we inhale new reagents.
  41. UpdatesAfter.Add(typeof(MetabolizerSystem));
  42. SubscribeLocalEvent<RespiratorComponent, MapInitEvent>(OnMapInit);
  43. SubscribeLocalEvent<RespiratorComponent, EntityUnpausedEvent>(OnUnpaused);
  44. SubscribeLocalEvent<RespiratorComponent, ApplyMetabolicMultiplierEvent>(OnApplyMetabolicMultiplier);
  45. }
  46. private void OnMapInit(Entity<RespiratorComponent> ent, ref MapInitEvent args)
  47. {
  48. ent.Comp.NextUpdate = _gameTiming.CurTime + ent.Comp.UpdateInterval;
  49. }
  50. private void OnUnpaused(Entity<RespiratorComponent> ent, ref EntityUnpausedEvent args)
  51. {
  52. ent.Comp.NextUpdate += args.PausedTime;
  53. }
  54. public override void Update(float frameTime)
  55. {
  56. base.Update(frameTime);
  57. var query = EntityQueryEnumerator<RespiratorComponent, BodyComponent>();
  58. while (query.MoveNext(out var uid, out var respirator, out var body))
  59. {
  60. if (_gameTiming.CurTime < respirator.NextUpdate)
  61. continue;
  62. respirator.NextUpdate += respirator.UpdateInterval;
  63. if (_mobState.IsDead(uid))
  64. continue;
  65. UpdateSaturation(uid, -(float) respirator.UpdateInterval.TotalSeconds, respirator);
  66. if (!_mobState.IsIncapacitated(uid)) // cannot breathe in crit.
  67. {
  68. switch (respirator.Status)
  69. {
  70. case RespiratorStatus.Inhaling:
  71. Inhale(uid, body);
  72. respirator.Status = RespiratorStatus.Exhaling;
  73. break;
  74. case RespiratorStatus.Exhaling:
  75. Exhale(uid, body);
  76. respirator.Status = RespiratorStatus.Inhaling;
  77. break;
  78. }
  79. }
  80. if (respirator.Saturation < respirator.SuffocationThreshold)
  81. {
  82. if (_gameTiming.CurTime >= respirator.LastGaspEmoteTime + respirator.GaspEmoteCooldown)
  83. {
  84. respirator.LastGaspEmoteTime = _gameTiming.CurTime;
  85. _chat.TryEmoteWithChat(uid, respirator.GaspEmote, ChatTransmitRange.HideChat, ignoreActionBlocker: true);
  86. }
  87. TakeSuffocationDamage((uid, respirator));
  88. respirator.SuffocationCycles += 1;
  89. continue;
  90. }
  91. StopSuffocation((uid, respirator));
  92. respirator.SuffocationCycles = 0;
  93. }
  94. }
  95. public void Inhale(EntityUid uid, BodyComponent? body = null)
  96. {
  97. if (!Resolve(uid, ref body, logMissing: false))
  98. return;
  99. var organs = _bodySystem.GetBodyOrganEntityComps<LungComponent>((uid, body));
  100. // Inhale gas
  101. var ev = new InhaleLocationEvent();
  102. RaiseLocalEvent(uid, ref ev);
  103. ev.Gas ??= _atmosSys.GetContainingMixture(uid, excite: true);
  104. if (ev.Gas is null)
  105. {
  106. return;
  107. }
  108. var actualGas = ev.Gas.RemoveVolume(Atmospherics.BreathVolume);
  109. var lungRatio = 1.0f / organs.Count;
  110. var gas = organs.Count == 1 ? actualGas : actualGas.RemoveRatio(lungRatio);
  111. foreach (var (organUid, lung, _) in organs)
  112. {
  113. // Merge doesn't remove gas from the giver.
  114. _atmosSys.Merge(lung.Air, gas);
  115. _lungSystem.GasToReagent(organUid, lung);
  116. }
  117. }
  118. public void Exhale(EntityUid uid, BodyComponent? body = null)
  119. {
  120. if (!Resolve(uid, ref body, logMissing: false))
  121. return;
  122. var organs = _bodySystem.GetBodyOrganEntityComps<LungComponent>((uid, body));
  123. // exhale gas
  124. var ev = new ExhaleLocationEvent();
  125. RaiseLocalEvent(uid, ref ev, broadcast: false);
  126. if (ev.Gas is null)
  127. {
  128. ev.Gas = _atmosSys.GetContainingMixture(uid, excite: true);
  129. // Walls and grids without atmos comp return null. I guess it makes sense to not be able to exhale in walls,
  130. // but this also means you cannot exhale on some grids.
  131. ev.Gas ??= GasMixture.SpaceGas;
  132. }
  133. var outGas = new GasMixture(ev.Gas.Volume);
  134. foreach (var (organUid, lung, _) in organs)
  135. {
  136. _atmosSys.Merge(outGas, lung.Air);
  137. lung.Air.Clear();
  138. if (_solutionContainerSystem.ResolveSolution(organUid, lung.SolutionName, ref lung.Solution))
  139. _solutionContainerSystem.RemoveAllSolution(lung.Solution.Value);
  140. }
  141. _atmosSys.Merge(ev.Gas, outGas);
  142. }
  143. /// <summary>
  144. /// Check whether or not an entity can metabolize inhaled air without suffocating or taking damage (i.e., no toxic
  145. /// gasses).
  146. /// </summary>
  147. public bool CanMetabolizeInhaledAir(Entity<RespiratorComponent?> ent)
  148. {
  149. if (!Resolve(ent, ref ent.Comp))
  150. return false;
  151. var ev = new InhaleLocationEvent();
  152. RaiseLocalEvent(ent, ref ev);
  153. var gas = ev.Gas ?? _atmosSys.GetContainingMixture(ent.Owner);
  154. if (gas == null)
  155. return false;
  156. return CanMetabolizeGas(ent, gas);
  157. }
  158. /// <summary>
  159. /// Check whether or not an entity can metabolize the given gas mixture without suffocating or taking damage
  160. /// (i.e., no toxic gasses).
  161. /// </summary>
  162. public bool CanMetabolizeGas(Entity<RespiratorComponent?> ent, GasMixture gas)
  163. {
  164. if (!Resolve(ent, ref ent.Comp))
  165. return false;
  166. var organs = _bodySystem.GetBodyOrganEntityComps<LungComponent>((ent, null));
  167. if (organs.Count == 0)
  168. return false;
  169. gas = new GasMixture(gas);
  170. var lungRatio = 1.0f / organs.Count;
  171. gas.Multiply(MathF.Min(lungRatio * gas.Volume/Atmospherics.BreathVolume, lungRatio));
  172. var solution = _lungSystem.GasToReagent(gas);
  173. float saturation = 0;
  174. foreach (var organ in organs)
  175. {
  176. saturation += GetSaturation(solution, organ.Owner, out var toxic);
  177. if (toxic)
  178. return false;
  179. }
  180. return saturation > ent.Comp.UpdateInterval.TotalSeconds;
  181. }
  182. /// <summary>
  183. /// Get the amount of saturation that would be generated if the lung were to metabolize the given solution.
  184. /// </summary>
  185. /// <remarks>
  186. /// This assumes the metabolism rate is unbounded, which generally should be the case for lungs, otherwise we get
  187. /// back to the old pulmonary edema bug.
  188. /// </remarks>
  189. /// <param name="solution">The reagents to metabolize</param>
  190. /// <param name="lung">The entity doing the metabolizing</param>
  191. /// <param name="toxic">Whether or not any of the reagents would deal damage to the entity</param>
  192. private float GetSaturation(Solution solution, Entity<MetabolizerComponent?> lung, out bool toxic)
  193. {
  194. toxic = false;
  195. if (!Resolve(lung, ref lung.Comp))
  196. return 0;
  197. if (lung.Comp.MetabolismGroups == null)
  198. return 0;
  199. float saturation = 0;
  200. foreach (var (id, quantity) in solution.Contents)
  201. {
  202. var reagent = _protoMan.Index<ReagentPrototype>(id.Prototype);
  203. if (reagent.Metabolisms == null)
  204. continue;
  205. if (!reagent.Metabolisms.TryGetValue(GasId, out var entry))
  206. continue;
  207. foreach (var effect in entry.Effects)
  208. {
  209. if (effect is HealthChange health)
  210. toxic |= CanMetabolize(health) && health.Damage.AnyPositive();
  211. else if (effect is Oxygenate oxy && CanMetabolize(oxy))
  212. saturation += oxy.Factor * quantity.Float();
  213. }
  214. }
  215. // TODO generalize condition checks
  216. // this is pretty janky, but I just want to bodge a method that checks if an entity can breathe a gas mixture
  217. // Applying actual reaction effects require a full ReagentEffectArgs struct.
  218. bool CanMetabolize(EntityEffect effect)
  219. {
  220. if (effect.Conditions == null)
  221. return true;
  222. foreach (var cond in effect.Conditions)
  223. {
  224. if (cond is OrganType organ && !organ.Condition(lung, EntityManager))
  225. return false;
  226. }
  227. return true;
  228. }
  229. return saturation;
  230. }
  231. private void TakeSuffocationDamage(Entity<RespiratorComponent> ent)
  232. {
  233. if (ent.Comp.SuffocationCycles == 2)
  234. _adminLogger.Add(LogType.Asphyxiation, $"{ToPrettyString(ent):entity} started suffocating");
  235. if (ent.Comp.SuffocationCycles >= ent.Comp.SuffocationCycleThreshold)
  236. {
  237. // TODO: This is not going work with multiple different lungs, if that ever becomes a possibility
  238. var organs = _bodySystem.GetBodyOrganEntityComps<LungComponent>((ent, null));
  239. foreach (var entity in organs)
  240. {
  241. _alertsSystem.ShowAlert(ent, entity.Comp1.Alert);
  242. }
  243. }
  244. _damageableSys.TryChangeDamage(ent, ent.Comp.Damage, interruptsDoAfters: false);
  245. }
  246. private void StopSuffocation(Entity<RespiratorComponent> ent)
  247. {
  248. if (ent.Comp.SuffocationCycles >= 2)
  249. _adminLogger.Add(LogType.Asphyxiation, $"{ToPrettyString(ent):entity} stopped suffocating");
  250. // TODO: This is not going work with multiple different lungs, if that ever becomes a possibility
  251. var organs = _bodySystem.GetBodyOrganEntityComps<LungComponent>((ent, null));
  252. foreach (var entity in organs)
  253. {
  254. _alertsSystem.ClearAlert(ent, entity.Comp1.Alert);
  255. }
  256. _damageableSys.TryChangeDamage(ent, ent.Comp.DamageRecovery);
  257. }
  258. public void UpdateSaturation(EntityUid uid, float amount,
  259. RespiratorComponent? respirator = null)
  260. {
  261. if (!Resolve(uid, ref respirator, false))
  262. return;
  263. respirator.Saturation += amount;
  264. respirator.Saturation =
  265. Math.Clamp(respirator.Saturation, respirator.MinSaturation, respirator.MaxSaturation);
  266. }
  267. private void OnApplyMetabolicMultiplier(
  268. Entity<RespiratorComponent> ent,
  269. ref ApplyMetabolicMultiplierEvent args)
  270. {
  271. // TODO REFACTOR THIS
  272. // This will slowly drift over time due to floating point errors.
  273. // Instead, raise an event with the base rates and allow modifiers to get applied to it.
  274. if (args.Apply)
  275. {
  276. ent.Comp.UpdateInterval *= args.Multiplier;
  277. ent.Comp.Saturation *= args.Multiplier;
  278. ent.Comp.MaxSaturation *= args.Multiplier;
  279. ent.Comp.MinSaturation *= args.Multiplier;
  280. return;
  281. }
  282. // This way we don't have to worry about it breaking if the stasis bed component is destroyed
  283. ent.Comp.UpdateInterval /= args.Multiplier;
  284. ent.Comp.Saturation /= args.Multiplier;
  285. ent.Comp.MaxSaturation /= args.Multiplier;
  286. ent.Comp.MinSaturation /= args.Multiplier;
  287. }
  288. }
  289. [ByRefEvent]
  290. public record struct InhaleLocationEvent(GasMixture? Gas);
  291. [ByRefEvent]
  292. public record struct ExhaleLocationEvent(GasMixture? Gas);