1
0

PuddleSystem.cs 29 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768
  1. using System.Linq;
  2. using Content.Server.Administration.Logs;
  3. using Content.Server.Chemistry.TileReactions;
  4. using Content.Server.DoAfter;
  5. using Content.Server.Fluids.Components;
  6. using Content.Server.Spreader;
  7. using Content.Shared.Chemistry;
  8. using Content.Shared.Chemistry.Components;
  9. using Content.Shared.Chemistry.Components.SolutionManager;
  10. using Content.Shared.Chemistry.EntitySystems;
  11. using Content.Shared.Chemistry.Reaction;
  12. using Content.Shared.Chemistry.Reagent;
  13. using Content.Shared.Database;
  14. using Content.Shared.Effects;
  15. using Content.Shared.FixedPoint;
  16. using Content.Shared.Fluids;
  17. using Content.Shared.Fluids.Components;
  18. using Content.Shared.Friction;
  19. using Content.Shared.IdentityManagement;
  20. using Content.Shared.Maps;
  21. using Content.Shared.Movement.Components;
  22. using Content.Shared.Movement.Systems;
  23. using Content.Shared.Popups;
  24. using Content.Shared.Slippery;
  25. using Content.Shared.StepTrigger.Components;
  26. using Content.Shared.StepTrigger.Systems;
  27. using Robust.Server.Audio;
  28. using Robust.Shared.Collections;
  29. using Robust.Shared.Map;
  30. using Robust.Shared.Map.Components;
  31. using Robust.Shared.Player;
  32. using Robust.Shared.Prototypes;
  33. using Robust.Shared.Random;
  34. using Robust.Shared.Timing;
  35. namespace Content.Server.Fluids.EntitySystems;
  36. /// <summary>
  37. /// Handles solutions on floors. Also handles the spreader logic for where the solution overflows a specified volume.
  38. /// </summary>
  39. public sealed partial class PuddleSystem : SharedPuddleSystem
  40. {
  41. [Dependency] private readonly IAdminLogManager _adminLogger = default!;
  42. [Dependency] private readonly IGameTiming _timing = default!;
  43. [Dependency] private readonly SharedMapSystem _map = default!;
  44. [Dependency] private readonly IPrototypeManager _prototypeManager = default!;
  45. [Dependency] private readonly IRobustRandom _random = default!;
  46. [Dependency] private readonly ITileDefinitionManager _tileDefMan = default!;
  47. [Dependency] private readonly AudioSystem _audio = default!;
  48. [Dependency] private readonly EntityLookupSystem _lookup = default!;
  49. [Dependency] private readonly ReactiveSystem _reactive = default!;
  50. [Dependency] private readonly SharedAppearanceSystem _appearance = default!;
  51. [Dependency] private readonly SharedColorFlashEffectSystem _color = default!;
  52. [Dependency] private readonly SharedPopupSystem _popups = default!;
  53. [Dependency] private readonly SharedSolutionContainerSystem _solutionContainerSystem = default!;
  54. [Dependency] private readonly StepTriggerSystem _stepTrigger = default!;
  55. [Dependency] private readonly SpeedModifierContactsSystem _speedModContacts = default!;
  56. [Dependency] private readonly TileFrictionController _tile = default!;
  57. [ValidatePrototypeId<ReagentPrototype>]
  58. private const string Blood = "Blood";
  59. [ValidatePrototypeId<ReagentPrototype>]
  60. private const string Slime = "Slime";
  61. [ValidatePrototypeId<ReagentPrototype>]
  62. private const string CopperBlood = "CopperBlood";
  63. private static string[] _standoutReagents = [Blood, Slime, CopperBlood];
  64. public static readonly float PuddleVolume = 1000;
  65. // Using local deletion queue instead of the standard queue so that we can easily "undelete" if a puddle
  66. // loses & then gains reagents in a single tick.
  67. private HashSet<EntityUid> _deletionQueue = [];
  68. private EntityQuery<PuddleComponent> _puddleQuery;
  69. /*
  70. * TODO: Need some sort of way to do blood slash / vomit solution spill on its own
  71. * This would then evaporate into the puddle tile below
  72. */
  73. /// <inheritdoc/>
  74. public override void Initialize()
  75. {
  76. base.Initialize();
  77. _puddleQuery = GetEntityQuery<PuddleComponent>();
  78. // Shouldn't need re-anchoring.
  79. SubscribeLocalEvent<PuddleComponent, AnchorStateChangedEvent>(OnAnchorChanged);
  80. SubscribeLocalEvent<PuddleComponent, SolutionContainerChangedEvent>(OnSolutionUpdate);
  81. SubscribeLocalEvent<PuddleComponent, ComponentInit>(OnPuddleInit);
  82. SubscribeLocalEvent<PuddleComponent, SpreadNeighborsEvent>(OnPuddleSpread);
  83. SubscribeLocalEvent<PuddleComponent, SlipEvent>(OnPuddleSlip);
  84. SubscribeLocalEvent<EvaporationComponent, MapInitEvent>(OnEvaporationMapInit);
  85. InitializeTransfers();
  86. }
  87. private void OnPuddleSpread(Entity<PuddleComponent> entity, ref SpreadNeighborsEvent args)
  88. {
  89. // Overflow is the source of the overflowing liquid. This contains the excess fluid above overflow limit (20u)
  90. var overflow = GetOverflowSolution(entity.Owner, entity.Comp);
  91. if (overflow.Volume == FixedPoint2.Zero)
  92. {
  93. RemCompDeferred<ActiveEdgeSpreaderComponent>(entity);
  94. return;
  95. }
  96. // For overflows, we never go to a fully evaporative tile just to avoid continuously having to mop it.
  97. // First we go to free tiles.
  98. // Need to go even if we have a little remainder to avoid solution sploshing around internally
  99. // for ages.
  100. if (args.NeighborFreeTiles.Count > 0 && args.Updates > 0)
  101. {
  102. _random.Shuffle(args.NeighborFreeTiles);
  103. var spillAmount = overflow.Volume / args.NeighborFreeTiles.Count;
  104. foreach (var neighbor in args.NeighborFreeTiles)
  105. {
  106. var split = overflow.SplitSolution(spillAmount);
  107. TrySpillAt(_map.GridTileToLocal(neighbor.Tile.GridUid, neighbor.Grid, neighbor.Tile.GridIndices), split, out _, false);
  108. args.Updates--;
  109. if (args.Updates <= 0)
  110. break;
  111. }
  112. RemCompDeferred<ActiveEdgeSpreaderComponent>(entity);
  113. return;
  114. }
  115. // Then we overflow to neighbors with overflow capacity
  116. if (args.Neighbors.Count > 0)
  117. {
  118. var resolvedNeighbourSolutions = new ValueList<(Solution neighborSolution, PuddleComponent puddle, EntityUid neighbor)>();
  119. // Resolve all our neighbours first, so we can use their properties to decide who to operate on first.
  120. foreach (var neighbor in args.Neighbors)
  121. {
  122. if (!_puddleQuery.TryGetComponent(neighbor, out var puddle) ||
  123. !_solutionContainerSystem.ResolveSolution(neighbor, puddle.SolutionName, ref puddle.Solution,
  124. out var neighborSolution) ||
  125. CanFullyEvaporate(neighborSolution))
  126. {
  127. continue;
  128. }
  129. resolvedNeighbourSolutions.Add(
  130. (neighborSolution, puddle, neighbor)
  131. );
  132. }
  133. // We want to deal with our neighbours by lowest current volume to highest, as this allows us to fill up our low points quickly.
  134. resolvedNeighbourSolutions.Sort(
  135. (x, y) =>
  136. x.neighborSolution.Volume.CompareTo(y.neighborSolution.Volume));
  137. // Overflow to neighbors with remaining space.
  138. foreach (var (neighborSolution, puddle, neighbor) in resolvedNeighbourSolutions)
  139. {
  140. // Water doesn't flow uphill
  141. if (neighborSolution.Volume >= (overflow.Volume + puddle.OverflowVolume))
  142. {
  143. continue;
  144. }
  145. // Work out how much we could send into this neighbour without overflowing it, and send up to that much
  146. var remaining = puddle.OverflowVolume - neighborSolution.Volume;
  147. // If we can't send anything, then skip this neighbour
  148. if (remaining <= FixedPoint2.Zero)
  149. continue;
  150. // We don't want to spill over to make high points either.
  151. if (neighborSolution.Volume + remaining >= (overflow.Volume + puddle.OverflowVolume))
  152. {
  153. continue;
  154. }
  155. var split = overflow.SplitSolution(remaining);
  156. if (puddle.Solution != null && !_solutionContainerSystem.TryAddSolution(puddle.Solution.Value, split))
  157. continue;
  158. args.Updates--;
  159. EnsureComp<ActiveEdgeSpreaderComponent>(neighbor);
  160. if (args.Updates <= 0)
  161. break;
  162. }
  163. // If there is nothing left to overflow from our tile, then we'll stop this tile being a active spreader
  164. if (overflow.Volume == FixedPoint2.Zero)
  165. {
  166. RemCompDeferred<ActiveEdgeSpreaderComponent>(entity);
  167. return;
  168. }
  169. }
  170. // Then we go to anything else.
  171. if (overflow.Volume > FixedPoint2.Zero && args.Neighbors.Count > 0 && args.Updates > 0)
  172. {
  173. var resolvedNeighbourSolutions =
  174. new ValueList<(Solution neighborSolution, PuddleComponent puddle, EntityUid neighbor)>();
  175. // Keep track of the total volume in the area
  176. FixedPoint2 totalVolume = 0;
  177. // Resolve all our neighbours so that we can use their properties to decide who to act on first
  178. foreach (var neighbor in args.Neighbors)
  179. {
  180. if (!_puddleQuery.TryGetComponent(neighbor, out var puddle) ||
  181. !_solutionContainerSystem.ResolveSolution(neighbor, puddle.SolutionName, ref puddle.Solution,
  182. out var neighborSolution) ||
  183. CanFullyEvaporate(neighborSolution))
  184. {
  185. continue;
  186. }
  187. resolvedNeighbourSolutions.Add((neighborSolution, puddle, neighbor));
  188. totalVolume += neighborSolution.Volume;
  189. }
  190. // We should act on neighbours by their total volume.
  191. resolvedNeighbourSolutions.Sort(
  192. (x, y) =>
  193. x.neighborSolution.Volume.CompareTo(y.neighborSolution.Volume)
  194. );
  195. // Overflow to neighbors with remaining total allowed space (1000u) above the overflow volume (20u).
  196. foreach (var (neighborSolution, puddle, neighbor) in resolvedNeighbourSolutions)
  197. {
  198. // What the source tiles current volume is.
  199. var sourceCurrentVolume = overflow.Volume + puddle.OverflowVolume;
  200. // Water doesn't flow uphill
  201. if (neighborSolution.Volume >= sourceCurrentVolume)
  202. {
  203. continue;
  204. }
  205. // We're in the low point in this area, let the neighbour tiles have a chance to spread to us first.
  206. var idealAverageVolume =
  207. (totalVolume + overflow.Volume + puddle.OverflowVolume) / (args.Neighbors.Count + 1);
  208. if (idealAverageVolume > sourceCurrentVolume)
  209. {
  210. continue;
  211. }
  212. // Work our how far off the ideal average this neighbour is.
  213. var spillThisNeighbor = idealAverageVolume - neighborSolution.Volume;
  214. // Skip if we want to spill negative amounts of fluid to this neighbour
  215. if (spillThisNeighbor < FixedPoint2.Zero)
  216. {
  217. continue;
  218. }
  219. // Try to send them as much towards the average ideal as we can
  220. var split = overflow.SplitSolution(spillThisNeighbor);
  221. // If we can't do it, move on.
  222. if (puddle.Solution != null && !_solutionContainerSystem.TryAddSolution(puddle.Solution.Value, split))
  223. continue;
  224. // If we succeed, then ensure that this neighbour is also able to spread it's overflow onwards
  225. EnsureComp<ActiveEdgeSpreaderComponent>(neighbor);
  226. args.Updates--;
  227. if (args.Updates <= 0)
  228. break;
  229. }
  230. }
  231. // Add the remainder back
  232. if (_solutionContainerSystem.ResolveSolution(entity.Owner, entity.Comp.SolutionName, ref entity.Comp.Solution))
  233. {
  234. _solutionContainerSystem.TryAddSolution(entity.Comp.Solution.Value, overflow);
  235. }
  236. }
  237. private void OnPuddleSlip(Entity<PuddleComponent> entity, ref SlipEvent args)
  238. {
  239. // Reactive entities have a chance to get a touch reaction from slipping on a puddle
  240. // (i.e. it is implied they fell face first onto it or something)
  241. if (!HasComp<ReactiveComponent>(args.Slipped) || HasComp<SlidingComponent>(args.Slipped))
  242. return;
  243. // Eventually probably have some system of 'body coverage' to tweak the probability but for now just 0.5
  244. // (implying that spacemen have a 50% chance to either land on their ass or their face)
  245. if (!_random.Prob(0.5f))
  246. return;
  247. if (!_solutionContainerSystem.ResolveSolution(entity.Owner, entity.Comp.SolutionName, ref entity.Comp.Solution,
  248. out var solution))
  249. return;
  250. _popups.PopupEntity(Loc.GetString("puddle-component-slipped-touch-reaction", ("puddle", entity.Owner)),
  251. args.Slipped, args.Slipped, PopupType.SmallCaution);
  252. // Take 15% of the puddle solution
  253. var splitSol = _solutionContainerSystem.SplitSolution(entity.Comp.Solution.Value, solution.Volume * 0.15f);
  254. _reactive.DoEntityReaction(args.Slipped, splitSol, ReactionMethod.Touch);
  255. }
  256. /// <inheritdoc/>
  257. public override void Update(float frameTime)
  258. {
  259. base.Update(frameTime);
  260. foreach (var ent in _deletionQueue)
  261. {
  262. Del(ent);
  263. }
  264. _deletionQueue.Clear();
  265. TickEvaporation();
  266. }
  267. private void OnPuddleInit(Entity<PuddleComponent> entity, ref ComponentInit args)
  268. {
  269. _solutionContainerSystem.EnsureSolution(entity.Owner, entity.Comp.SolutionName, out _, FixedPoint2.New(PuddleVolume));
  270. }
  271. private void OnSolutionUpdate(Entity<PuddleComponent> entity, ref SolutionContainerChangedEvent args)
  272. {
  273. if (args.SolutionId != entity.Comp.SolutionName)
  274. return;
  275. if (args.Solution.Volume <= 0)
  276. {
  277. _deletionQueue.Add(entity);
  278. return;
  279. }
  280. _deletionQueue.Remove(entity);
  281. UpdateSlip(entity, entity.Comp, args.Solution);
  282. UpdateSlow(entity, args.Solution);
  283. UpdateEvaporation(entity, args.Solution);
  284. UpdateAppearance(entity, entity.Comp);
  285. }
  286. private void UpdateAppearance(EntityUid uid, PuddleComponent? puddleComponent = null,
  287. AppearanceComponent? appearance = null)
  288. {
  289. if (!Resolve(uid, ref puddleComponent, ref appearance, false))
  290. {
  291. return;
  292. }
  293. var volume = FixedPoint2.Zero;
  294. Color color = Color.White;
  295. if (_solutionContainerSystem.ResolveSolution(uid, puddleComponent.SolutionName, ref puddleComponent.Solution,
  296. out var solution))
  297. {
  298. volume = solution.Volume / puddleComponent.OverflowVolume;
  299. // Make blood stand out more
  300. // Kinda EH
  301. // Could potentially do alpha per-solution but future problem.
  302. color = solution.GetColorWithout(_prototypeManager, _standoutReagents);
  303. color = color.WithAlpha(0.7f);
  304. foreach (var standout in _standoutReagents)
  305. {
  306. var quantity = solution.GetTotalPrototypeQuantity(standout);
  307. if (quantity <= FixedPoint2.Zero)
  308. continue;
  309. var interpolateValue = quantity.Float() / solution.Volume.Float();
  310. color = Color.InterpolateBetween(color,
  311. _prototypeManager.Index<ReagentPrototype>(standout).SubstanceColor, interpolateValue);
  312. }
  313. }
  314. _appearance.SetData(uid, PuddleVisuals.CurrentVolume, volume.Float(), appearance);
  315. _appearance.SetData(uid, PuddleVisuals.SolutionColor, color, appearance);
  316. }
  317. private void UpdateSlip(EntityUid entityUid, PuddleComponent component, Solution solution)
  318. {
  319. var isSlippery = false;
  320. var isSuperSlippery = false;
  321. // The base sprite is currently at 0.3 so we require at least 2nd tier to be slippery or else it's too hard to see.
  322. var amountRequired = FixedPoint2.New(component.OverflowVolume.Float() * LowThreshold);
  323. var slipperyAmount = FixedPoint2.Zero;
  324. // Utilize the defaults from their relevant systems... this sucks, and is a bandaid
  325. var launchForwardsMultiplier = SlipperyComponent.DefaultLaunchForwardsMultiplier;
  326. var paralyzeTime = SlipperyComponent.DefaultParalyzeTime;
  327. var requiredSlipSpeed = StepTriggerComponent.DefaultRequiredTriggeredSpeed;
  328. foreach (var (reagent, quantity) in solution.Contents)
  329. {
  330. var reagentProto = _prototypeManager.Index<ReagentPrototype>(reagent.Prototype);
  331. if (!reagentProto.Slippery)
  332. continue;
  333. slipperyAmount += quantity;
  334. if (slipperyAmount <= amountRequired)
  335. continue;
  336. isSlippery = true;
  337. foreach (var tileReaction in reagentProto.TileReactions)
  338. {
  339. if (tileReaction is not SpillTileReaction spillTileReaction)
  340. continue;
  341. isSuperSlippery = spillTileReaction.SuperSlippery;
  342. launchForwardsMultiplier = launchForwardsMultiplier < spillTileReaction.LaunchForwardsMultiplier ? spillTileReaction.LaunchForwardsMultiplier : launchForwardsMultiplier;
  343. requiredSlipSpeed = requiredSlipSpeed > spillTileReaction.RequiredSlipSpeed ? spillTileReaction.RequiredSlipSpeed : requiredSlipSpeed;
  344. paralyzeTime = paralyzeTime < spillTileReaction.ParalyzeTime ? spillTileReaction.ParalyzeTime : paralyzeTime;
  345. }
  346. }
  347. if (isSlippery)
  348. {
  349. var comp = EnsureComp<StepTriggerComponent>(entityUid);
  350. _stepTrigger.SetActive(entityUid, true, comp);
  351. var friction = EnsureComp<TileFrictionModifierComponent>(entityUid);
  352. _tile.SetModifier(entityUid, TileFrictionController.DefaultFriction * 0.5f, friction);
  353. if (!TryComp<SlipperyComponent>(entityUid, out var slipperyComponent))
  354. return;
  355. slipperyComponent.SuperSlippery = isSuperSlippery;
  356. _stepTrigger.SetRequiredTriggerSpeed(entityUid, requiredSlipSpeed);
  357. slipperyComponent.LaunchForwardsMultiplier = launchForwardsMultiplier;
  358. slipperyComponent.ParalyzeTime = paralyzeTime;
  359. }
  360. else if (TryComp<StepTriggerComponent>(entityUid, out var comp))
  361. {
  362. _stepTrigger.SetActive(entityUid, false, comp);
  363. RemCompDeferred<TileFrictionModifierComponent>(entityUid);
  364. }
  365. }
  366. private void UpdateSlow(EntityUid uid, Solution solution)
  367. {
  368. var maxViscosity = 0f;
  369. foreach (var (reagent, _) in solution.Contents)
  370. {
  371. var reagentProto = _prototypeManager.Index<ReagentPrototype>(reagent.Prototype);
  372. maxViscosity = Math.Max(maxViscosity, reagentProto.Viscosity);
  373. }
  374. if (maxViscosity > 0)
  375. {
  376. var comp = EnsureComp<SpeedModifierContactsComponent>(uid);
  377. var speed = 1 - maxViscosity;
  378. _speedModContacts.ChangeModifiers(uid, speed, comp);
  379. }
  380. else
  381. {
  382. RemComp<SpeedModifierContactsComponent>(uid);
  383. }
  384. }
  385. private void OnAnchorChanged(Entity<PuddleComponent> entity, ref AnchorStateChangedEvent args)
  386. {
  387. if (!args.Anchored)
  388. QueueDel(entity);
  389. }
  390. /// <summary>
  391. /// Gets the current volume of the given puddle, which may not necessarily be PuddleVolume.
  392. /// </summary>
  393. public FixedPoint2 CurrentVolume(EntityUid uid, PuddleComponent? puddleComponent = null)
  394. {
  395. if (!Resolve(uid, ref puddleComponent))
  396. return FixedPoint2.Zero;
  397. return _solutionContainerSystem.ResolveSolution(uid, puddleComponent.SolutionName, ref puddleComponent.Solution,
  398. out var solution)
  399. ? solution.Volume
  400. : FixedPoint2.Zero;
  401. }
  402. /// <summary>
  403. /// Try to add solution to <paramref name="puddleUid"/>.
  404. /// </summary>
  405. /// <param name="puddleUid">Puddle to which we add</param>
  406. /// <param name="addedSolution">Solution that is added to puddleComponent</param>
  407. /// <param name="sound">Play sound on overflow</param>
  408. /// <param name="checkForOverflow">Overflow on encountered values</param>
  409. /// <param name="puddleComponent">Optional resolved PuddleComponent</param>
  410. /// <returns></returns>
  411. public bool TryAddSolution(EntityUid puddleUid,
  412. Solution addedSolution,
  413. bool sound = true,
  414. bool checkForOverflow = true,
  415. PuddleComponent? puddleComponent = null,
  416. SolutionContainerManagerComponent? sol = null)
  417. {
  418. if (!Resolve(puddleUid, ref puddleComponent, ref sol))
  419. return false;
  420. _solutionContainerSystem.EnsureAllSolutions((puddleUid, sol));
  421. if (addedSolution.Volume == 0 ||
  422. !_solutionContainerSystem.ResolveSolution(puddleUid, puddleComponent.SolutionName,
  423. ref puddleComponent.Solution))
  424. {
  425. return false;
  426. }
  427. _solutionContainerSystem.AddSolution(puddleComponent.Solution.Value, addedSolution);
  428. if (checkForOverflow && IsOverflowing(puddleUid, puddleComponent))
  429. {
  430. EnsureComp<ActiveEdgeSpreaderComponent>(puddleUid);
  431. }
  432. if (!sound)
  433. {
  434. return true;
  435. }
  436. _audio.PlayPvs(puddleComponent.SpillSound, puddleUid);
  437. return true;
  438. }
  439. /// <summary>
  440. /// Whether adding this solution to this puddle would overflow.
  441. /// </summary>
  442. public bool WouldOverflow(EntityUid uid, Solution solution, PuddleComponent? puddle = null)
  443. {
  444. if (!Resolve(uid, ref puddle))
  445. return false;
  446. return CurrentVolume(uid, puddle) + solution.Volume > puddle.OverflowVolume;
  447. }
  448. /// <summary>
  449. /// Whether adding this solution to this puddle would overflow.
  450. /// </summary>
  451. private bool IsOverflowing(EntityUid uid, PuddleComponent? puddle = null)
  452. {
  453. if (!Resolve(uid, ref puddle))
  454. return false;
  455. return CurrentVolume(uid, puddle) > puddle.OverflowVolume;
  456. }
  457. /// <summary>
  458. /// Gets the solution amount above the overflow threshold for the puddle.
  459. /// </summary>
  460. public Solution GetOverflowSolution(EntityUid uid, PuddleComponent? puddle = null)
  461. {
  462. if (!Resolve(uid, ref puddle) ||
  463. !_solutionContainerSystem.ResolveSolution(uid, puddle.SolutionName, ref puddle.Solution))
  464. {
  465. return new Solution(0);
  466. }
  467. // TODO: This is going to fail with struct solutions.
  468. var remaining = puddle.OverflowVolume;
  469. var split = _solutionContainerSystem.SplitSolution(puddle.Solution.Value,
  470. CurrentVolume(uid, puddle) - remaining);
  471. return split;
  472. }
  473. #region Spill
  474. /// <inheritdoc/>
  475. public override bool TrySplashSpillAt(EntityUid uid,
  476. EntityCoordinates coordinates,
  477. Solution solution,
  478. out EntityUid puddleUid,
  479. bool sound = true,
  480. EntityUid? user = null)
  481. {
  482. puddleUid = EntityUid.Invalid;
  483. if (solution.Volume == 0)
  484. return false;
  485. var targets = new List<EntityUid>();
  486. var reactive = new HashSet<Entity<ReactiveComponent>>();
  487. _lookup.GetEntitiesInRange(coordinates, 1.0f, reactive);
  488. // Get reactive entities nearby--if there are some, it'll spill a bit on them instead.
  489. foreach (var ent in reactive)
  490. {
  491. // sorry! no overload for returning uid, so .owner must be used
  492. var owner = ent.Owner;
  493. // between 5 and 30%
  494. var splitAmount = solution.Volume * _random.NextFloat(0.05f, 0.30f);
  495. var splitSolution = solution.SplitSolution(splitAmount);
  496. if (user != null)
  497. {
  498. _adminLogger.Add(LogType.Landed,
  499. $"{ToPrettyString(user.Value):user} threw {ToPrettyString(uid):entity} which splashed a solution {SharedSolutionContainerSystem.ToPrettyString(solution):solution} onto {ToPrettyString(owner):target}");
  500. }
  501. targets.Add(owner);
  502. _reactive.DoEntityReaction(owner, splitSolution, ReactionMethod.Touch);
  503. _popups.PopupEntity(
  504. Loc.GetString("spill-land-spilled-on-other", ("spillable", uid),
  505. ("target", Identity.Entity(owner, EntityManager))), owner, PopupType.SmallCaution);
  506. }
  507. _color.RaiseEffect(solution.GetColor(_prototypeManager), targets,
  508. Filter.Pvs(uid, entityManager: EntityManager));
  509. return TrySpillAt(coordinates, solution, out puddleUid, sound);
  510. }
  511. /// <inheritdoc/>
  512. public override bool TrySpillAt(EntityCoordinates coordinates, Solution solution, out EntityUid puddleUid, bool sound = true)
  513. {
  514. if (solution.Volume == 0)
  515. {
  516. puddleUid = EntityUid.Invalid;
  517. return false;
  518. }
  519. var gridUid = coordinates.GetGridUid(EntityManager);
  520. if (!TryComp<MapGridComponent>(gridUid, out var mapGrid))
  521. {
  522. puddleUid = EntityUid.Invalid;
  523. return false;
  524. }
  525. return TrySpillAt(_map.GetTileRef(gridUid.Value, mapGrid, coordinates), solution, out puddleUid, sound);
  526. }
  527. /// <inheritdoc/>
  528. public override bool TrySpillAt(EntityUid uid, Solution solution, out EntityUid puddleUid, bool sound = true,
  529. TransformComponent? transformComponent = null)
  530. {
  531. if (!Resolve(uid, ref transformComponent, false))
  532. {
  533. puddleUid = EntityUid.Invalid;
  534. return false;
  535. }
  536. return TrySpillAt(transformComponent.Coordinates, solution, out puddleUid, sound: sound);
  537. }
  538. /// <inheritdoc/>
  539. public override bool TrySpillAt(TileRef tileRef, Solution solution, out EntityUid puddleUid, bool sound = true,
  540. bool tileReact = true)
  541. {
  542. if (solution.Volume <= 0)
  543. {
  544. puddleUid = EntityUid.Invalid;
  545. return false;
  546. }
  547. // If space return early, let that spill go out into the void
  548. if (tileRef.Tile.IsEmpty || tileRef.IsSpace(_tileDefMan))
  549. {
  550. puddleUid = EntityUid.Invalid;
  551. return false;
  552. }
  553. // Let's not spill to invalid grids.
  554. var gridId = tileRef.GridUid;
  555. if (!TryComp<MapGridComponent>(gridId, out var mapGrid))
  556. {
  557. puddleUid = EntityUid.Invalid;
  558. return false;
  559. }
  560. if (tileReact)
  561. {
  562. // First, do all tile reactions
  563. DoTileReactions(tileRef, solution);
  564. }
  565. // Tile reactions used up everything.
  566. if (solution.Volume == FixedPoint2.Zero)
  567. {
  568. puddleUid = EntityUid.Invalid;
  569. return false;
  570. }
  571. // Get normalized co-ordinate for spill location and spill it in the centre
  572. // TODO: Does SnapGrid or something else already do this?
  573. var anchored = _map.GetAnchoredEntitiesEnumerator(gridId, mapGrid, tileRef.GridIndices);
  574. var puddleQuery = GetEntityQuery<PuddleComponent>();
  575. var sparklesQuery = GetEntityQuery<EvaporationSparkleComponent>();
  576. while (anchored.MoveNext(out var ent))
  577. {
  578. // If there's existing sparkles then delete it
  579. if (sparklesQuery.TryGetComponent(ent, out var sparkles))
  580. {
  581. QueueDel(ent.Value);
  582. continue;
  583. }
  584. if (!puddleQuery.TryGetComponent(ent, out var puddle))
  585. continue;
  586. if (TryAddSolution(ent.Value, solution, sound, puddleComponent: puddle))
  587. {
  588. EnsureComp<ActiveEdgeSpreaderComponent>(ent.Value);
  589. }
  590. puddleUid = ent.Value;
  591. return true;
  592. }
  593. var coords = _map.GridTileToLocal(gridId, mapGrid, tileRef.GridIndices);
  594. puddleUid = EntityManager.SpawnEntity("Puddle", coords);
  595. EnsureComp<PuddleComponent>(puddleUid);
  596. if (TryAddSolution(puddleUid, solution, sound))
  597. {
  598. EnsureComp<ActiveEdgeSpreaderComponent>(puddleUid);
  599. }
  600. return true;
  601. }
  602. #endregion
  603. public void DoTileReactions(TileRef tileRef, Solution solution)
  604. {
  605. for (var i = solution.Contents.Count - 1; i >= 0; i--)
  606. {
  607. var (reagent, quantity) = solution.Contents[i];
  608. var proto = _prototypeManager.Index<ReagentPrototype>(reagent.Prototype);
  609. var removed = proto.ReactionTile(tileRef, quantity, EntityManager, reagent.Data);
  610. if (removed <= FixedPoint2.Zero)
  611. continue;
  612. solution.RemoveReagent(reagent, removed);
  613. }
  614. }
  615. /// <summary>
  616. /// Tries to get the relevant puddle entity for a tile.
  617. /// </summary>
  618. public bool TryGetPuddle(TileRef tile, out EntityUid puddleUid)
  619. {
  620. puddleUid = EntityUid.Invalid;
  621. if (!TryComp<MapGridComponent>(tile.GridUid, out var grid))
  622. return false;
  623. var anc = _map.GetAnchoredEntitiesEnumerator(tile.GridUid, grid, tile.GridIndices);
  624. var puddleQuery = GetEntityQuery<PuddleComponent>();
  625. while (anc.MoveNext(out var ent))
  626. {
  627. if (!puddleQuery.HasComponent(ent.Value))
  628. continue;
  629. puddleUid = ent.Value;
  630. return true;
  631. }
  632. return false;
  633. }
  634. }