MetabolizerSystem.cs 9.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259
  1. using Content.Server.Body.Components;
  2. using Content.Shared.Chemistry.EntitySystems;
  3. using Content.Shared.Administration.Logs;
  4. using Content.Shared.Body.Organ;
  5. using Content.Shared.Chemistry.Components;
  6. using Content.Shared.Chemistry.Components.SolutionManager;
  7. using Content.Shared.Chemistry.Reagent;
  8. using Content.Shared.Database;
  9. using Content.Shared.EntityEffects;
  10. using Content.Shared.FixedPoint;
  11. using Content.Shared.Mobs.Components;
  12. using Content.Shared.Mobs.Systems;
  13. using Robust.Shared.Collections;
  14. using Robust.Shared.Prototypes;
  15. using Robust.Shared.Random;
  16. using Robust.Shared.Timing;
  17. namespace Content.Server.Body.Systems
  18. {
  19. public sealed class MetabolizerSystem : EntitySystem
  20. {
  21. [Dependency] private readonly IGameTiming _gameTiming = default!;
  22. [Dependency] private readonly IPrototypeManager _prototypeManager = default!;
  23. [Dependency] private readonly IRobustRandom _random = default!;
  24. [Dependency] private readonly ISharedAdminLogManager _adminLogger = default!;
  25. [Dependency] private readonly MobStateSystem _mobStateSystem = default!;
  26. [Dependency] private readonly SharedSolutionContainerSystem _solutionContainerSystem = default!;
  27. private EntityQuery<OrganComponent> _organQuery;
  28. private EntityQuery<SolutionContainerManagerComponent> _solutionQuery;
  29. public override void Initialize()
  30. {
  31. base.Initialize();
  32. _organQuery = GetEntityQuery<OrganComponent>();
  33. _solutionQuery = GetEntityQuery<SolutionContainerManagerComponent>();
  34. SubscribeLocalEvent<MetabolizerComponent, ComponentInit>(OnMetabolizerInit);
  35. SubscribeLocalEvent<MetabolizerComponent, MapInitEvent>(OnMapInit);
  36. SubscribeLocalEvent<MetabolizerComponent, EntityUnpausedEvent>(OnUnpaused);
  37. SubscribeLocalEvent<MetabolizerComponent, ApplyMetabolicMultiplierEvent>(OnApplyMetabolicMultiplier);
  38. }
  39. private void OnMapInit(Entity<MetabolizerComponent> ent, ref MapInitEvent args)
  40. {
  41. ent.Comp.NextUpdate = _gameTiming.CurTime + ent.Comp.UpdateInterval;
  42. }
  43. private void OnUnpaused(Entity<MetabolizerComponent> ent, ref EntityUnpausedEvent args)
  44. {
  45. ent.Comp.NextUpdate += args.PausedTime;
  46. }
  47. private void OnMetabolizerInit(Entity<MetabolizerComponent> entity, ref ComponentInit args)
  48. {
  49. if (!entity.Comp.SolutionOnBody)
  50. {
  51. _solutionContainerSystem.EnsureSolution(entity.Owner, entity.Comp.SolutionName, out _);
  52. }
  53. else if (_organQuery.CompOrNull(entity)?.Body is { } body)
  54. {
  55. _solutionContainerSystem.EnsureSolution(body, entity.Comp.SolutionName, out _);
  56. }
  57. }
  58. private void OnApplyMetabolicMultiplier(
  59. Entity<MetabolizerComponent> ent,
  60. ref ApplyMetabolicMultiplierEvent args)
  61. {
  62. // TODO REFACTOR THIS
  63. // This will slowly drift over time due to floating point errors.
  64. // Instead, raise an event with the base rates and allow modifiers to get applied to it.
  65. if (args.Apply)
  66. {
  67. ent.Comp.UpdateInterval *= args.Multiplier;
  68. return;
  69. }
  70. ent.Comp.UpdateInterval /= args.Multiplier;
  71. }
  72. public override void Update(float frameTime)
  73. {
  74. base.Update(frameTime);
  75. var metabolizers = new ValueList<(EntityUid Uid, MetabolizerComponent Component)>(Count<MetabolizerComponent>());
  76. var query = EntityQueryEnumerator<MetabolizerComponent>();
  77. while (query.MoveNext(out var uid, out var comp))
  78. {
  79. metabolizers.Add((uid, comp));
  80. }
  81. foreach (var (uid, metab) in metabolizers)
  82. {
  83. // Only update as frequently as it should
  84. if (_gameTiming.CurTime < metab.NextUpdate)
  85. continue;
  86. metab.NextUpdate += metab.UpdateInterval;
  87. TryMetabolize((uid, metab));
  88. }
  89. }
  90. private void TryMetabolize(Entity<MetabolizerComponent, OrganComponent?, SolutionContainerManagerComponent?> ent)
  91. {
  92. _organQuery.Resolve(ent, ref ent.Comp2, logMissing: false);
  93. // First step is get the solution we actually care about
  94. var solutionName = ent.Comp1.SolutionName;
  95. Solution? solution = null;
  96. Entity<SolutionComponent>? soln = default!;
  97. EntityUid? solutionEntityUid = null;
  98. if (ent.Comp1.SolutionOnBody)
  99. {
  100. if (ent.Comp2?.Body is { } body)
  101. {
  102. if (!_solutionQuery.Resolve(body, ref ent.Comp3, logMissing: false))
  103. return;
  104. _solutionContainerSystem.TryGetSolution((body, ent.Comp3), solutionName, out soln, out solution);
  105. solutionEntityUid = body;
  106. }
  107. }
  108. else
  109. {
  110. if (!_solutionQuery.Resolve(ent, ref ent.Comp3, logMissing: false))
  111. return;
  112. _solutionContainerSystem.TryGetSolution((ent, ent), solutionName, out soln, out solution);
  113. solutionEntityUid = ent;
  114. }
  115. if (solutionEntityUid is null
  116. || soln is null
  117. || solution is null
  118. || solution.Contents.Count == 0)
  119. {
  120. return;
  121. }
  122. // randomize the reagent list so we don't have any weird quirks
  123. // like alphabetical order or insertion order mattering for processing
  124. var list = solution.Contents.ToArray();
  125. _random.Shuffle(list);
  126. int reagents = 0;
  127. foreach (var (reagent, quantity) in list)
  128. {
  129. if (!_prototypeManager.TryIndex<ReagentPrototype>(reagent.Prototype, out var proto))
  130. continue;
  131. var mostToRemove = FixedPoint2.Zero;
  132. if (proto.Metabolisms is null)
  133. {
  134. if (ent.Comp1.RemoveEmpty)
  135. {
  136. solution.RemoveReagent(reagent, FixedPoint2.New(1));
  137. }
  138. continue;
  139. }
  140. // we're done here entirely if this is true
  141. if (reagents >= ent.Comp1.MaxReagentsProcessable)
  142. return;
  143. // loop over all our groups and see which ones apply
  144. if (ent.Comp1.MetabolismGroups is null)
  145. continue;
  146. foreach (var group in ent.Comp1.MetabolismGroups)
  147. {
  148. if (!proto.Metabolisms.TryGetValue(group.Id, out var entry))
  149. continue;
  150. var rate = entry.MetabolismRate * group.MetabolismRateModifier;
  151. // Remove $rate, as long as there's enough reagent there to actually remove that much
  152. mostToRemove = FixedPoint2.Clamp(rate, 0, quantity);
  153. float scale = (float) mostToRemove / (float) rate;
  154. // if it's possible for them to be dead, and they are,
  155. // then we shouldn't process any effects, but should probably
  156. // still remove reagents
  157. if (TryComp<MobStateComponent>(solutionEntityUid.Value, out var state))
  158. {
  159. if (!proto.WorksOnTheDead && _mobStateSystem.IsDead(solutionEntityUid.Value, state))
  160. continue;
  161. }
  162. var actualEntity = ent.Comp2?.Body ?? solutionEntityUid.Value;
  163. var args = new EntityEffectReagentArgs(actualEntity, EntityManager, ent, solution, mostToRemove, proto, null, scale);
  164. // do all effects, if conditions apply
  165. foreach (var effect in entry.Effects)
  166. {
  167. if (!effect.ShouldApply(args, _random))
  168. continue;
  169. if (effect.ShouldLog)
  170. {
  171. _adminLogger.Add(
  172. LogType.ReagentEffect,
  173. effect.LogImpact,
  174. $"Metabolism effect {effect.GetType().Name:effect}"
  175. + $" of reagent {proto.LocalizedName:reagent}"
  176. + $" applied on entity {actualEntity:entity}"
  177. + $" at {Transform(actualEntity).Coordinates:coordinates}"
  178. );
  179. }
  180. effect.Effect(args);
  181. }
  182. }
  183. // remove a certain amount of reagent
  184. if (mostToRemove > FixedPoint2.Zero)
  185. {
  186. solution.RemoveReagent(reagent, mostToRemove);
  187. // We have processed a reagant, so count it towards the cap
  188. reagents += 1;
  189. }
  190. }
  191. _solutionContainerSystem.UpdateChemicals(soln.Value);
  192. }
  193. }
  194. // TODO REFACTOR THIS
  195. // This will cause rates to slowly drift over time due to floating point errors.
  196. // Instead, the system that raised this should trigger an update and subscribe to get-modifier events.
  197. [ByRefEvent]
  198. public readonly record struct ApplyMetabolicMultiplierEvent(
  199. EntityUid Uid,
  200. float Multiplier,
  201. bool Apply)
  202. {
  203. /// <summary>
  204. /// The entity whose metabolism is being modified.
  205. /// </summary>
  206. public readonly EntityUid Uid = Uid;
  207. /// <summary>
  208. /// What the metabolism's update rate will be multiplied by.
  209. /// </summary>
  210. public readonly float Multiplier = Multiplier;
  211. /// <summary>
  212. /// If true, apply the multiplier. If false, revert it.
  213. /// </summary>
  214. public readonly bool Apply = Apply;
  215. }
  216. }