1
0

RespiratorSystem.cs 13 KB

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