SmokeSystem.cs 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350
  1. using Content.Server.Administration.Logs;
  2. using Content.Server.Body.Components;
  3. using Content.Server.Body.Systems;
  4. using Content.Server.EntityEffects.Effects;
  5. using Content.Server.Spreader;
  6. using Content.Shared.Chemistry;
  7. using Content.Shared.Chemistry.Components;
  8. using Content.Shared.Chemistry.EntitySystems;
  9. using Content.Shared.Chemistry.Reaction;
  10. using Content.Shared.Chemistry.Reagent;
  11. using Content.Shared.Database;
  12. using Content.Shared.FixedPoint;
  13. using Content.Shared.Smoking;
  14. using Robust.Server.GameObjects;
  15. using Robust.Shared.Map.Components;
  16. using Robust.Shared.Physics;
  17. using Robust.Shared.Physics.Components;
  18. using Robust.Shared.Physics.Events;
  19. using Robust.Shared.Physics.Systems;
  20. using Robust.Shared.Prototypes;
  21. using Robust.Shared.Random;
  22. using Robust.Shared.Timing;
  23. using System.Linq;
  24. using TimedDespawnComponent = Robust.Shared.Spawners.TimedDespawnComponent;
  25. namespace Content.Server.Fluids.EntitySystems;
  26. /// <summary>
  27. /// Handles non-atmos solution entities similar to puddles.
  28. /// </summary>
  29. public sealed class SmokeSystem : EntitySystem
  30. {
  31. // If I could do it all again this could probably use a lot more of puddles.
  32. [Dependency] private readonly IAdminLogManager _logger = default!;
  33. [Dependency] private readonly IGameTiming _timing = default!;
  34. [Dependency] private readonly SharedMapSystem _map = default!;
  35. [Dependency] private readonly IPrototypeManager _prototype = default!;
  36. [Dependency] private readonly IRobustRandom _random = default!;
  37. [Dependency] private readonly AppearanceSystem _appearance = default!;
  38. [Dependency] private readonly BloodstreamSystem _blood = default!;
  39. [Dependency] private readonly InternalsSystem _internals = default!;
  40. [Dependency] private readonly ReactiveSystem _reactive = default!;
  41. [Dependency] private readonly SharedBroadphaseSystem _broadphase = default!;
  42. [Dependency] private readonly SharedPhysicsSystem _physics = default!;
  43. [Dependency] private readonly SharedSolutionContainerSystem _solutionContainerSystem = default!;
  44. private EntityQuery<SmokeComponent> _smokeQuery;
  45. private EntityQuery<SmokeAffectedComponent> _smokeAffectedQuery;
  46. /// <inheritdoc/>
  47. public override void Initialize()
  48. {
  49. base.Initialize();
  50. _smokeQuery = GetEntityQuery<SmokeComponent>();
  51. _smokeAffectedQuery = GetEntityQuery<SmokeAffectedComponent>();
  52. SubscribeLocalEvent<SmokeComponent, StartCollideEvent>(OnStartCollide);
  53. SubscribeLocalEvent<SmokeComponent, EndCollideEvent>(OnEndCollide);
  54. SubscribeLocalEvent<SmokeComponent, ReactionAttemptEvent>(OnReactionAttempt);
  55. SubscribeLocalEvent<SmokeComponent, SolutionRelayEvent<ReactionAttemptEvent>>(OnReactionAttempt);
  56. SubscribeLocalEvent<SmokeComponent, SpreadNeighborsEvent>(OnSmokeSpread);
  57. }
  58. /// <inheritdoc/>
  59. public override void Update(float frameTime)
  60. {
  61. base.Update(frameTime);
  62. var query = EntityQueryEnumerator<SmokeAffectedComponent>();
  63. var curTime = _timing.CurTime;
  64. while (query.MoveNext(out var uid, out var smoke))
  65. {
  66. if (curTime < smoke.NextSecond)
  67. continue;
  68. smoke.NextSecond += TimeSpan.FromSeconds(1);
  69. SmokeReact(uid, smoke.SmokeEntity);
  70. }
  71. }
  72. private void OnStartCollide(Entity<SmokeComponent> entity, ref StartCollideEvent args)
  73. {
  74. if (_smokeAffectedQuery.HasComponent(args.OtherEntity))
  75. return;
  76. var smokeAffected = AddComp<SmokeAffectedComponent>(args.OtherEntity);
  77. smokeAffected.SmokeEntity = entity;
  78. smokeAffected.NextSecond = _timing.CurTime + TimeSpan.FromSeconds(1);
  79. }
  80. private void OnEndCollide(Entity<SmokeComponent> entity, ref EndCollideEvent args)
  81. {
  82. // if we are already in smoke, make sure the thing we are exiting is the current smoke we are in.
  83. if (_smokeAffectedQuery.TryGetComponent(args.OtherEntity, out var smokeAffectedComponent))
  84. {
  85. if (smokeAffectedComponent.SmokeEntity != entity.Owner)
  86. return;
  87. }
  88. var exists = Exists(entity);
  89. if (!TryComp<PhysicsComponent>(args.OtherEntity, out var body))
  90. return;
  91. foreach (var ent in _physics.GetContactingEntities(args.OtherEntity, body))
  92. {
  93. if (exists && ent == entity.Owner)
  94. continue;
  95. if (!_smokeQuery.HasComponent(ent))
  96. continue;
  97. smokeAffectedComponent ??= EnsureComp<SmokeAffectedComponent>(args.OtherEntity);
  98. smokeAffectedComponent.SmokeEntity = ent;
  99. return; // exit the function so we don't remove the component.
  100. }
  101. if (smokeAffectedComponent != null)
  102. RemComp(args.OtherEntity, smokeAffectedComponent);
  103. }
  104. private void OnSmokeSpread(Entity<SmokeComponent> entity, ref SpreadNeighborsEvent args)
  105. {
  106. if (entity.Comp.SpreadAmount == 0 || !_solutionContainerSystem.ResolveSolution(entity.Owner, SmokeComponent.SolutionName, ref entity.Comp.Solution, out var solution))
  107. {
  108. RemCompDeferred<ActiveEdgeSpreaderComponent>(entity);
  109. return;
  110. }
  111. if (Prototype(entity) is not { } prototype)
  112. {
  113. RemCompDeferred<ActiveEdgeSpreaderComponent>(entity);
  114. return;
  115. }
  116. if (args.NeighborFreeTiles.Count == 0)
  117. return;
  118. TryComp<TimedDespawnComponent>(entity, out var timer);
  119. // wtf is the logic behind any of this.
  120. var smokePerSpread = entity.Comp.SpreadAmount / Math.Max(1, args.NeighborFreeTiles.Count);
  121. foreach (var neighbor in args.NeighborFreeTiles)
  122. {
  123. var coords = _map.GridTileToLocal(neighbor.Tile.GridUid, neighbor.Grid, neighbor.Tile.GridIndices);
  124. var ent = Spawn(prototype.ID, coords);
  125. var spreadAmount = Math.Max(0, smokePerSpread);
  126. entity.Comp.SpreadAmount -= args.NeighborFreeTiles.Count;
  127. StartSmoke(ent, solution.Clone(), timer?.Lifetime ?? entity.Comp.Duration, spreadAmount);
  128. if (entity.Comp.SpreadAmount == 0)
  129. {
  130. RemCompDeferred<ActiveEdgeSpreaderComponent>(entity);
  131. break;
  132. }
  133. }
  134. args.Updates--;
  135. if (args.NeighborFreeTiles.Count > 0 || args.Neighbors.Count == 0 || entity.Comp.SpreadAmount < 1)
  136. return;
  137. // We have no more neighbours to spread to. So instead we will randomly distribute our volume to neighbouring smoke tiles.
  138. var smokeQuery = GetEntityQuery<SmokeComponent>();
  139. _random.Shuffle(args.Neighbors);
  140. foreach (var neighbor in args.Neighbors)
  141. {
  142. if (!smokeQuery.TryGetComponent(neighbor, out var smoke))
  143. continue;
  144. smoke.SpreadAmount++;
  145. entity.Comp.SpreadAmount--;
  146. EnsureComp<ActiveEdgeSpreaderComponent>(neighbor);
  147. if (entity.Comp.SpreadAmount == 0)
  148. {
  149. RemCompDeferred<ActiveEdgeSpreaderComponent>(entity);
  150. break;
  151. }
  152. }
  153. }
  154. private void OnReactionAttempt(Entity<SmokeComponent> entity, ref ReactionAttemptEvent args)
  155. {
  156. if (args.Cancelled)
  157. return;
  158. // Prevent smoke/foam fork bombs (smoke creating more smoke).
  159. foreach (var effect in args.Reaction.Effects)
  160. {
  161. if (effect is AreaReactionEffect)
  162. {
  163. args.Cancelled = true;
  164. return;
  165. }
  166. }
  167. }
  168. private void OnReactionAttempt(Entity<SmokeComponent> entity, ref SolutionRelayEvent<ReactionAttemptEvent> args)
  169. {
  170. if (args.Name == SmokeComponent.SolutionName)
  171. OnReactionAttempt(entity, ref args.Event);
  172. }
  173. /// <summary>
  174. /// Sets up a smoke component for spreading.
  175. /// </summary>
  176. public void StartSmoke(EntityUid uid, Solution solution, float duration, int spreadAmount, SmokeComponent? component = null)
  177. {
  178. if (!Resolve(uid, ref component))
  179. return;
  180. component.SpreadAmount = spreadAmount;
  181. component.Duration = duration;
  182. component.TransferRate = solution.Volume / duration;
  183. TryAddSolution(uid, solution);
  184. Dirty(uid, component);
  185. EnsureComp<ActiveEdgeSpreaderComponent>(uid);
  186. if (TryComp<PhysicsComponent>(uid, out var body) && TryComp<FixturesComponent>(uid, out var fixtures))
  187. {
  188. var xform = Transform(uid);
  189. _physics.SetBodyType(uid, BodyType.Dynamic, fixtures, body, xform);
  190. _physics.SetCanCollide(uid, true, manager: fixtures, body: body);
  191. _broadphase.RegenerateContacts((uid, body, fixtures, xform));
  192. }
  193. var timer = EnsureComp<TimedDespawnComponent>(uid);
  194. timer.Lifetime = duration;
  195. // The tile reaction happens here because it only occurs once.
  196. ReactOnTile(uid, component);
  197. }
  198. /// <summary>
  199. /// Does the relevant smoke reactions for an entity.
  200. /// </summary>
  201. public void SmokeReact(EntityUid entity, EntityUid smokeUid, SmokeComponent? component = null)
  202. {
  203. if (!Resolve(smokeUid, ref component))
  204. return;
  205. if (!_solutionContainerSystem.ResolveSolution(smokeUid, SmokeComponent.SolutionName, ref component.Solution, out var solution) ||
  206. solution.Contents.Count == 0)
  207. {
  208. return;
  209. }
  210. ReactWithEntity(entity, smokeUid, solution, component);
  211. UpdateVisuals((smokeUid, component));
  212. }
  213. private void ReactWithEntity(EntityUid entity, EntityUid smokeUid, Solution solution, SmokeComponent? component = null)
  214. {
  215. if (!Resolve(smokeUid, ref component))
  216. return;
  217. if (!TryComp<BloodstreamComponent>(entity, out var bloodstream))
  218. return;
  219. if (!_solutionContainerSystem.ResolveSolution(entity, bloodstream.ChemicalSolutionName, ref bloodstream.ChemicalSolution, out var chemSolution) || chemSolution.AvailableVolume <= 0)
  220. return;
  221. var blockIngestion = _internals.AreInternalsWorking(entity);
  222. var cloneSolution = solution.Clone();
  223. var availableTransfer = FixedPoint2.Min(cloneSolution.Volume, component.TransferRate);
  224. var transferAmount = FixedPoint2.Min(availableTransfer, chemSolution.AvailableVolume);
  225. var transferSolution = cloneSolution.SplitSolution(transferAmount);
  226. foreach (var reagentQuantity in transferSolution.Contents.ToArray())
  227. {
  228. if (reagentQuantity.Quantity == FixedPoint2.Zero)
  229. continue;
  230. var reagentProto = _prototype.Index<ReagentPrototype>(reagentQuantity.Reagent.Prototype);
  231. _reactive.ReactionEntity(entity, ReactionMethod.Touch, reagentProto, reagentQuantity, transferSolution);
  232. if (!blockIngestion)
  233. _reactive.ReactionEntity(entity, ReactionMethod.Ingestion, reagentProto, reagentQuantity, transferSolution);
  234. }
  235. if (blockIngestion)
  236. return;
  237. if (_blood.TryAddToChemicals(entity, transferSolution, bloodstream))
  238. {
  239. // Log solution addition by smoke
  240. _logger.Add(LogType.ForceFeed, LogImpact.Medium, $"{ToPrettyString(entity):target} ingested smoke {SharedSolutionContainerSystem.ToPrettyString(transferSolution)}");
  241. }
  242. }
  243. private void ReactOnTile(EntityUid uid, SmokeComponent? component = null, TransformComponent? xform = null)
  244. {
  245. if (!Resolve(uid, ref component, ref xform))
  246. return;
  247. if (!_solutionContainerSystem.ResolveSolution(uid, SmokeComponent.SolutionName, ref component.Solution, out var solution) || !solution.Any())
  248. return;
  249. if (!TryComp<MapGridComponent>(xform.GridUid, out var mapGrid))
  250. return;
  251. var tile = _map.GetTileRef(xform.GridUid.Value, mapGrid, xform.Coordinates);
  252. foreach (var reagentQuantity in solution.Contents.ToArray())
  253. {
  254. if (reagentQuantity.Quantity == FixedPoint2.Zero)
  255. continue;
  256. var reagent = _prototype.Index<ReagentPrototype>(reagentQuantity.Reagent.Prototype);
  257. reagent.ReactionTile(tile, reagentQuantity.Quantity, EntityManager, reagentQuantity.Reagent.Data);
  258. }
  259. }
  260. /// <summary>
  261. /// Adds the specified solution to the relevant smoke solution.
  262. /// </summary>
  263. private void TryAddSolution(Entity<SmokeComponent?> smoke, Solution solution)
  264. {
  265. if (solution.Volume == FixedPoint2.Zero)
  266. return;
  267. if (!Resolve(smoke, ref smoke.Comp))
  268. return;
  269. if (!_solutionContainerSystem.ResolveSolution(smoke.Owner, SmokeComponent.SolutionName, ref smoke.Comp.Solution, out var solutionArea))
  270. return;
  271. var addSolution = solution.SplitSolution(FixedPoint2.Min(solution.Volume, solutionArea.AvailableVolume));
  272. _solutionContainerSystem.TryAddSolution(smoke.Comp.Solution.Value, addSolution);
  273. UpdateVisuals(smoke);
  274. }
  275. private void UpdateVisuals(Entity<SmokeComponent?, AppearanceComponent?> smoke)
  276. {
  277. if (!Resolve(smoke, ref smoke.Comp1, ref smoke.Comp2) ||
  278. !_solutionContainerSystem.ResolveSolution(smoke.Owner, SmokeComponent.SolutionName, ref smoke.Comp1.Solution, out var solution))
  279. return;
  280. var color = solution.GetColor(_prototype);
  281. _appearance.SetData(smoke.Owner, SmokeVisuals.Color, color, smoke.Comp2);
  282. }
  283. }