From: mr-bo-jangles Date: Thu, 11 Jan 2024 13:22:56 +0000 (+0000) Subject: Optimise the puddle system to reach an equilibrium quickly. (#23776) X-Git-Url: https://git.smokeofanarchy.ru/gitweb.cgi?a=commitdiff_plain;h=064d52db412d9cf80ace425db8573aea2dfc7d81;p=space-station-14.git Optimise the puddle system to reach an equilibrium quickly. (#23776) * 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 --- diff --git a/Content.Server/Fluids/EntitySystems/PuddleSystem.cs b/Content.Server/Fluids/EntitySystems/PuddleSystem.cs index 52948f2afb..c5eb42e05b 100644 --- a/Content.Server/Fluids/EntitySystems/PuddleSystem.cs +++ b/Content.Server/Fluids/EntitySystems/PuddleSystem.cs @@ -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; /// 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 _deletionQueue = new(); + private EntityQuery _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(); + // Shouldn't need re-anchoring. SubscribeLocalEvent(OnAnchorChanged); SubscribeLocalEvent(HandlePuddleExamined); @@ -99,6 +104,7 @@ public sealed partial class PuddleSystem : SharedPuddleSystem private void OnPuddleSpread(Entity 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(); - // 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(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(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(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(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 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 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(standout).SubstanceColor, interpolateValue); + color = Color.InterpolateBetween(color, + _prototypeManager.Index(standout).SubstanceColor, interpolateValue); } } @@ -347,6 +434,7 @@ public sealed partial class PuddleSystem : SharedPuddleSystem var reagentProto = _prototypeManager.Index(reagent.Prototype); maxViscosity = Math.Max(maxViscosity, reagentProto.Viscosity); } + if (maxViscosity > 0) { var comp = EnsureComp(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 /// 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 /// /// /// - 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 /// /// /// - 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(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(reagent.Prototype); var removed = proto.ReactionTile(tileRef, quantity);