]> git.smokeofanarchy.ru Git - space-station-14.git/commitdiff
Optimise the puddle system to reach an equilibrium quickly. (#23776)
authormr-bo-jangles <mr-bo-jangles@users.noreply.github.com>
Thu, 11 Jan 2024 13:22:56 +0000 (13:22 +0000)
committerGitHub <noreply@github.com>
Thu, 11 Jan 2024 13:22:56 +0000 (00:22 +1100)
* Optimise the puddle system to reach an equilibrium quickly.

* Remove use of Linq
Try to be more efficient with Tuples

* review

---------

Co-authored-by: metalgearsloth <comedian_vs_clown@hotmail.com>
Content.Server/Fluids/EntitySystems/PuddleSystem.cs

index 52948f2afb3763dbf1bf3559651aef5de2e8cb4a..c5eb42e05b55d5a13fab09ecf7c2e8dd39373311 100644 (file)
@@ -24,6 +24,7 @@ using Content.Shared.Slippery;
 using Content.Shared.StepTrigger.Components;
 using Content.Shared.StepTrigger.Systems;
 using Robust.Server.Audio;
+using Robust.Shared.Collections;
 using Robust.Shared.Map;
 using Robust.Shared.Map.Components;
 using Robust.Shared.Player;
@@ -38,7 +39,7 @@ namespace Content.Server.Fluids.EntitySystems;
 /// </summary>
 public sealed partial class PuddleSystem : SharedPuddleSystem
 {
-    [Dependency] private readonly IAdminLogManager _adminLogger= default!;
+    [Dependency] private readonly IAdminLogManager _adminLogger = default!;
     [Dependency] private readonly IGameTiming _timing = default!;
     [Dependency] private readonly IMapManager _mapManager = default!;
     [Dependency] private readonly IPrototypeManager _prototypeManager = default!;
@@ -73,6 +74,8 @@ public sealed partial class PuddleSystem : SharedPuddleSystem
     // loses & then gains reagents in a single tick.
     private HashSet<EntityUid> _deletionQueue = new();
 
+    private EntityQuery<PuddleComponent> _puddleQuery;
+
     /*
      * TODO: Need some sort of way to do blood slash / vomit solution spill on its own
      * This would then evaporate into the puddle tile below
@@ -83,6 +86,8 @@ public sealed partial class PuddleSystem : SharedPuddleSystem
     {
         base.Initialize();
 
+        _puddleQuery = GetEntityQuery<PuddleComponent>();
+
         // Shouldn't need re-anchoring.
         SubscribeLocalEvent<PuddleComponent, AnchorStateChangedEvent>(OnAnchorChanged);
         SubscribeLocalEvent<PuddleComponent, ExaminedEvent>(HandlePuddleExamined);
@@ -99,6 +104,7 @@ public sealed partial class PuddleSystem : SharedPuddleSystem
 
     private void OnPuddleSpread(Entity<PuddleComponent> entity, ref SpreadNeighborsEvent args)
     {
+        // Overflow is the source of the overflowing liquid. This contains the excess fluid above overflow limit (20u)
         var overflow = GetOverflowSolution(entity.Owner, entity.Comp);
 
         if (overflow.Volume == FixedPoint2.Zero)
@@ -107,33 +113,81 @@ public sealed partial class PuddleSystem : SharedPuddleSystem
             return;
         }
 
-        var puddleQuery = GetEntityQuery<PuddleComponent>();
-
         // For overflows, we never go to a fully evaporative tile just to avoid continuously having to mop it.
 
-        // First we overflow to neighbors with overflow capacity
+        // First we go to free tiles.
+        // Need to go even if we have a little remainder to avoid solution sploshing around internally
+        // for ages.
+        if (args.NeighborFreeTiles.Count > 0 && args.Updates > 0)
+        {
+            _random.Shuffle(args.NeighborFreeTiles);
+            var spillAmount = overflow.Volume / args.NeighborFreeTiles.Count;
+
+            foreach (var neighbor in args.NeighborFreeTiles)
+            {
+                var split = overflow.SplitSolution(spillAmount);
+                TrySpillAt(neighbor.Grid.GridTileToLocal(neighbor.Tile), split, out _, false);
+                args.Updates--;
+
+                if (args.Updates <= 0)
+                    break;
+            }
+
+            RemCompDeferred<ActiveEdgeSpreaderComponent>(entity);
+            return;
+        }
+
+        // Then we overflow to neighbors with overflow capacity
         if (args.Neighbors.Count > 0)
         {
-            _random.Shuffle(args.Neighbors);
+            var resolvedNeighbourSolutions = new ValueList<(Solution neighborSolution, PuddleComponent puddle, EntityUid neighbor)>();
 
-            // Overflow to neighbors with remaining space.
+            // Resolve all our neighbours first, so we can use their properties to decide who to operate on first.
             foreach (var neighbor in args.Neighbors)
             {
-                if (!puddleQuery.TryGetComponent(neighbor, out var puddle) ||
-                    !_solutionContainerSystem.ResolveSolution(neighbor, puddle.SolutionName, ref puddle.Solution, out var neighborSolution) ||
+                if (!_puddleQuery.TryGetComponent(neighbor, out var puddle) ||
+                    !_solutionContainerSystem.ResolveSolution(neighbor, puddle.SolutionName, ref puddle.Solution,
+                        out var neighborSolution) ||
                     CanFullyEvaporate(neighborSolution))
                 {
                     continue;
                 }
 
+                resolvedNeighbourSolutions.Add(
+                    (neighborSolution, puddle, neighbor)
+                );
+            }
+
+            // We want to deal with our neighbours by lowest current volume to highest, as this allows us to fill up our low points quickly.
+            resolvedNeighbourSolutions.Sort(
+                (x, y) =>
+                    x.neighborSolution.Volume.CompareTo(y.neighborSolution.Volume));
+
+            // Overflow to neighbors with remaining space.
+            foreach (var (neighborSolution, puddle, neighbor) in resolvedNeighbourSolutions)
+            {
+                // Water doesn't flow uphill
+                if (neighborSolution.Volume >= (overflow.Volume + puddle.OverflowVolume))
+                {
+                    continue;
+                }
+
+                // Work out how much we could send into this neighbour without overflowing it, and send up to that much
                 var remaining = puddle.OverflowVolume - neighborSolution.Volume;
 
+                // If we can't send anything, then skip this neighbour
                 if (remaining <= FixedPoint2.Zero)
                     continue;
 
+                // We don't want to spill over to make high points either.
+                if (neighborSolution.Volume + remaining >= (overflow.Volume + puddle.OverflowVolume))
+                {
+                    continue;
+                }
+
                 var split = overflow.SplitSolution(remaining);
 
-                if (!_solutionContainerSystem.TryAddSolution(puddle.Solution.Value, split))
+                if (puddle.Solution != null && !_solutionContainerSystem.TryAddSolution(puddle.Solution.Value, split))
                     continue;
 
                 args.Updates--;
@@ -143,6 +197,7 @@ public sealed partial class PuddleSystem : SharedPuddleSystem
                     break;
             }
 
+            // If there is nothing left to overflow from our tile, then we'll stop this tile being a active spreader
             if (overflow.Volume == FixedPoint2.Zero)
             {
                 RemCompDeferred<ActiveEdgeSpreaderComponent>(entity);
@@ -150,48 +205,74 @@ public sealed partial class PuddleSystem : SharedPuddleSystem
             }
         }
 
-        // Then we go to free tiles.
-        // Need to go even if we have a little remainder to avoid solution sploshing around internally
-        // for ages.
-        if (args.NeighborFreeTiles.Count > 0 && args.Updates > 0)
+        // Then we go to anything else.
+        if (overflow.Volume > FixedPoint2.Zero && args.Neighbors.Count > 0 && args.Updates > 0)
         {
-            _random.Shuffle(args.NeighborFreeTiles);
-            var spillAmount = overflow.Volume / args.NeighborFreeTiles.Count;
+            var resolvedNeighbourSolutions =
+                new ValueList<(Solution neighborSolution, PuddleComponent puddle, EntityUid neighbor)>();
 
-            foreach (var neighbor in args.NeighborFreeTiles)
+            // Keep track of the total volume in the area
+            FixedPoint2 totalVolume = 0;
+
+            // Resolve all our neighbours so that we can use their properties to decide who to act on first
+            foreach (var neighbor in args.Neighbors)
             {
-                var split = overflow.SplitSolution(spillAmount);
-                TrySpillAt(neighbor.Grid.GridTileToLocal(neighbor.Tile), split, out _, false);
-                args.Updates--;
+                if (!_puddleQuery.TryGetComponent(neighbor, out var puddle) ||
+                    !_solutionContainerSystem.ResolveSolution(neighbor, puddle.SolutionName, ref puddle.Solution,
+                        out var neighborSolution) ||
+                    CanFullyEvaporate(neighborSolution))
+                {
+                    continue;
+                }
 
-                if (args.Updates <= 0)
-                    break;
+                resolvedNeighbourSolutions.Add((neighborSolution, puddle, neighbor));
+                totalVolume += neighborSolution.Volume;
             }
 
-            RemCompDeferred<ActiveEdgeSpreaderComponent>(entity);
-            return;
-        }
+            // We should act on neighbours by their total volume.
+            resolvedNeighbourSolutions.Sort(
+                (x, y) =>
+                    x.neighborSolution.Volume.CompareTo(y.neighborSolution.Volume)
+            );
 
-        // Then we go to anything else.
-        if (overflow.Volume > FixedPoint2.Zero && args.Neighbors.Count > 0 && args.Updates > 0)
-        {
-            var spillPerNeighbor = overflow.Volume / args.Neighbors.Count;
-
-            foreach (var neighbor in args.Neighbors)
+            // Overflow to neighbors with remaining total allowed space (1000u) above the overflow volume (20u).
+            foreach (var (neighborSolution, puddle, neighbor) in resolvedNeighbourSolutions)
             {
-                // Overflow to neighbours (unless it's pure water)
-                if (!puddleQuery.TryGetComponent(neighbor, out var puddle) ||
-                    !_solutionContainerSystem.ResolveSolution(neighbor, puddle.SolutionName, ref puddle.Solution, out var neighborSolution) ||
-                    CanFullyEvaporate(neighborSolution))
+                // What the source tiles current volume is.
+                var sourceCurrentVolume = overflow.Volume + puddle.OverflowVolume;
+
+                // Water doesn't flow uphill
+                if (neighborSolution.Volume >= sourceCurrentVolume)
+                {
+                    continue;
+                }
+
+                // We're in the low point in this area, let the neighbour tiles have a chance to spread to us first.
+                var idealAverageVolume =
+                    (totalVolume + overflow.Volume + puddle.OverflowVolume) / (args.Neighbors.Count + 1);
+
+                if (idealAverageVolume > sourceCurrentVolume)
+                {
+                    continue;
+                }
+
+                // Work our how far off the ideal average this neighbour is.
+                var spillThisNeighbor = idealAverageVolume - neighborSolution.Volume;
+
+                // Skip if we want to spill negative amounts of fluid to this neighbour
+                if (spillThisNeighbor < FixedPoint2.Zero)
                 {
                     continue;
                 }
 
-                var split = overflow.SplitSolution(spillPerNeighbor);
+                // Try to send them as much towards the average ideal as we can
+                var split = overflow.SplitSolution(spillThisNeighbor);
 
-                if (!_solutionContainerSystem.TryAddSolution(puddle.Solution.Value, split))
+                // If we can't do it, move on.
+                if (puddle.Solution != null && !_solutionContainerSystem.TryAddSolution(puddle.Solution.Value, split))
                     continue;
 
+                // If we succeed, then ensure that this neighbour is also able to spread it's overflow onwards
                 EnsureComp<ActiveEdgeSpreaderComponent>(neighbor);
                 args.Updates--;
 
@@ -219,7 +300,8 @@ public sealed partial class PuddleSystem : SharedPuddleSystem
         if (!_random.Prob(0.5f))
             return;
 
-        if (!_solutionContainerSystem.ResolveSolution(entity.Owner, entity.Comp.SolutionName, ref entity.Comp.Solution, out var solution))
+        if (!_solutionContainerSystem.ResolveSolution(entity.Owner, entity.Comp.SolutionName, ref entity.Comp.Solution,
+                out var solution))
             return;
 
         _popups.PopupEntity(Loc.GetString("puddle-component-slipped-touch-reaction", ("puddle", entity.Owner)),
@@ -238,6 +320,7 @@ public sealed partial class PuddleSystem : SharedPuddleSystem
         {
             Del(ent);
         }
+
         _deletionQueue.Clear();
 
         TickEvaporation();
@@ -245,7 +328,8 @@ public sealed partial class PuddleSystem : SharedPuddleSystem
 
     private void OnPuddleInit(Entity<PuddleComponent> entity, ref ComponentInit args)
     {
-        _solutionContainerSystem.EnsureSolution(entity.Owner, entity.Comp.SolutionName, FixedPoint2.New(PuddleVolume), out _);
+        _solutionContainerSystem.EnsureSolution(entity.Owner, entity.Comp.SolutionName, FixedPoint2.New(PuddleVolume),
+            out _);
     }
 
     private void OnSolutionUpdate(Entity<PuddleComponent> entity, ref SolutionContainerChangedEvent args)
@@ -266,7 +350,8 @@ public sealed partial class PuddleSystem : SharedPuddleSystem
         UpdateAppearance(entity, entity.Comp);
     }
 
-    private void UpdateAppearance(EntityUid uid, PuddleComponent? puddleComponent = null, AppearanceComponent? appearance = null)
+    private void UpdateAppearance(EntityUid uid, PuddleComponent? puddleComponent = null,
+        AppearanceComponent? appearance = null)
     {
         if (!Resolve(uid, ref puddleComponent, ref appearance, false))
         {
@@ -276,7 +361,8 @@ public sealed partial class PuddleSystem : SharedPuddleSystem
         var volume = FixedPoint2.Zero;
         Color color = Color.White;
 
-        if (_solutionContainerSystem.ResolveSolution(uid, puddleComponent.SolutionName, ref puddleComponent.Solution, out var solution))
+        if (_solutionContainerSystem.ResolveSolution(uid, puddleComponent.SolutionName, ref puddleComponent.Solution,
+                out var solution))
         {
             volume = solution.Volume / puddleComponent.OverflowVolume;
 
@@ -294,7 +380,8 @@ public sealed partial class PuddleSystem : SharedPuddleSystem
                     continue;
 
                 var interpolateValue = quantity.Float() / solution.Volume.Float();
-                color = Color.InterpolateBetween(color, _prototypeManager.Index<ReagentPrototype>(standout).SubstanceColor, interpolateValue);
+                color = Color.InterpolateBetween(color,
+                    _prototypeManager.Index<ReagentPrototype>(standout).SubstanceColor, interpolateValue);
             }
         }
 
@@ -347,6 +434,7 @@ public sealed partial class PuddleSystem : SharedPuddleSystem
             var reagentProto = _prototypeManager.Index<ReagentPrototype>(reagent.Prototype);
             maxViscosity = Math.Max(maxViscosity, reagentProto.Viscosity);
         }
+
         if (maxViscosity > 0)
         {
             var comp = EnsureComp<SlowContactsComponent>(uid);
@@ -398,7 +486,8 @@ public sealed partial class PuddleSystem : SharedPuddleSystem
         if (!Resolve(uid, ref puddleComponent))
             return FixedPoint2.Zero;
 
-        return _solutionContainerSystem.ResolveSolution(uid, puddleComponent.SolutionName, ref puddleComponent.Solution, out var solution)
+        return _solutionContainerSystem.ResolveSolution(uid, puddleComponent.SolutionName, ref puddleComponent.Solution,
+            out var solution)
             ? solution.Volume
             : FixedPoint2.Zero;
     }
@@ -422,7 +511,8 @@ public sealed partial class PuddleSystem : SharedPuddleSystem
             return false;
 
         if (addedSolution.Volume == 0 ||
-            !_solutionContainerSystem.ResolveSolution(puddleUid, puddleComponent.SolutionName, ref puddleComponent.Solution))
+            !_solutionContainerSystem.ResolveSolution(puddleUid, puddleComponent.SolutionName,
+                ref puddleComponent.Solution))
         {
             return false;
         }
@@ -470,14 +560,16 @@ public sealed partial class PuddleSystem : SharedPuddleSystem
     /// </summary>
     public Solution GetOverflowSolution(EntityUid uid, PuddleComponent? puddle = null)
     {
-        if (!Resolve(uid, ref puddle) || !_solutionContainerSystem.ResolveSolution(uid, puddle.SolutionName, ref puddle.Solution))
+        if (!Resolve(uid, ref puddle) ||
+            !_solutionContainerSystem.ResolveSolution(uid, puddle.SolutionName, ref puddle.Solution))
         {
             return new Solution(0);
         }
 
         // TODO: This is going to fail with struct solutions.
         var remaining = puddle.OverflowVolume;
-        var split = _solutionContainerSystem.SplitSolution(puddle.Solution.Value, CurrentVolume(uid, puddle) - remaining);
+        var split = _solutionContainerSystem.SplitSolution(puddle.Solution.Value,
+            CurrentVolume(uid, puddle) - remaining);
         return split;
     }
 
@@ -521,10 +613,13 @@ public sealed partial class PuddleSystem : SharedPuddleSystem
 
             targets.Add(owner);
             _reactive.DoEntityReaction(owner, splitSolution, ReactionMethod.Touch);
-            _popups.PopupEntity(Loc.GetString("spill-land-spilled-on-other", ("spillable", uid), ("target", Identity.Entity(owner, EntityManager))), owner, PopupType.SmallCaution);
+            _popups.PopupEntity(
+                Loc.GetString("spill-land-spilled-on-other", ("spillable", uid),
+                    ("target", Identity.Entity(owner, EntityManager))), owner, PopupType.SmallCaution);
         }
 
-        _color.RaiseEffect(solution.GetColor(_prototypeManager), targets, Filter.Pvs(uid, entityManager: EntityManager));
+        _color.RaiseEffect(solution.GetColor(_prototypeManager), targets,
+            Filter.Pvs(uid, entityManager: EntityManager));
 
         return TrySpillAt(coordinates, solution, out puddleUid, sound);
     }
@@ -553,7 +648,8 @@ public sealed partial class PuddleSystem : SharedPuddleSystem
     /// <summary>
     /// <see cref="TrySpillAt(Robust.Shared.Map.EntityCoordinates,Content.Shared.Chemistry.Components.Solution,out Robust.Shared.GameObjects.EntityUid,bool)"/>
     /// </summary>
-    public bool TrySpillAt(EntityUid uid, Solution solution, out EntityUid puddleUid, bool sound = true, TransformComponent? transformComponent = null)
+    public bool TrySpillAt(EntityUid uid, Solution solution, out EntityUid puddleUid, bool sound = true,
+        TransformComponent? transformComponent = null)
     {
         if (!Resolve(uid, ref transformComponent, false))
         {
@@ -567,7 +663,8 @@ public sealed partial class PuddleSystem : SharedPuddleSystem
     /// <summary>
     /// <see cref="TrySpillAt(Robust.Shared.Map.EntityCoordinates,Content.Shared.Chemistry.Components.Solution,out Robust.Shared.GameObjects.EntityUid,bool)"/>
     /// </summary>
-    public bool TrySpillAt(TileRef tileRef, Solution solution, out EntityUid puddleUid, bool sound = true, bool tileReact = true)
+    public bool TrySpillAt(TileRef tileRef, Solution solution, out EntityUid puddleUid, bool sound = true,
+        bool tileReact = true)
     {
         if (solution.Volume <= 0)
         {
@@ -637,6 +734,7 @@ public sealed partial class PuddleSystem : SharedPuddleSystem
         {
             EnsureComp<ActiveEdgeSpreaderComponent>(puddleUid);
         }
+
         return true;
     }
 
@@ -646,7 +744,6 @@ public sealed partial class PuddleSystem : SharedPuddleSystem
     {
         for (var i = solution.Contents.Count - 1; i >= 0; i--)
         {
-
             var (reagent, quantity) = solution.Contents[i];
             var proto = _prototypeManager.Index<ReagentPrototype>(reagent.Prototype);
             var removed = proto.ReactionTile(tileRef, quantity);