Fluid spread refactor (#11908)

Co-authored-by: metalgearsloth <31366439+metalgearsloth@users.noreply.github.com>
Fix undefined
This commit is contained in:
Ygg01
2022-11-15 12:30:59 +01:00
committed by GitHub
parent 89b959f931
commit 75ea093d78
17 changed files with 719 additions and 336 deletions

View File

@@ -55,5 +55,20 @@ namespace Content.Server.Fluids.EntitySystems
EntityManager.RemoveComponent(evaporationComponent.Owner, evaporationComponent);
}
}
/// <summary>
/// Copy constructor to copy initial fields from source to destination.
/// </summary>
/// <param name="destUid">Entity to which we copy <paramref name="srcEvaporation"/> properties</param>
/// <param name="srcEvaporation">Component that contains relevant properties</param>
public void CopyConstruct(EntityUid destUid, EvaporationComponent srcEvaporation)
{
var destEvaporation = EntityManager.EnsureComponent<EvaporationComponent>(destUid);
destEvaporation.EvaporateTime = srcEvaporation.EvaporateTime;
destEvaporation.EvaporationToggle = srcEvaporation.EvaporationToggle;
destEvaporation.SolutionName = srcEvaporation.SolutionName;
destEvaporation.LowerLimit = srcEvaporation.LowerLimit;
destEvaporation.UpperLimit = srcEvaporation.UpperLimit;
}
}
}

View File

@@ -1,237 +1,145 @@
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using Content.Server.Chemistry.EntitySystems;
using Content.Server.Fluids.Components;
using Content.Shared.Chemistry.Components;
using Content.Shared;
using Content.Shared.Directions;
using Content.Shared.FixedPoint;
using Content.Shared.Physics;
using JetBrains.Annotations;
using Robust.Shared.Map;
using Robust.Shared.Physics;
using Robust.Shared.Utility;
using Robust.Shared.Physics.Components;
using Robust.Shared.Timing;
namespace Content.Server.Fluids.EntitySystems;
/// <summary>
/// Component that governs overflowing puddles. Controls how Puddles spread and updat
/// </summary>
[UsedImplicitly]
public sealed class FluidSpreaderSystem : EntitySystem
{
[Dependency] private readonly IGameTiming _gameTiming = default!;
[Dependency] private readonly IMapManager _mapManager = default!;
[Dependency] private readonly PuddleSystem _puddleSystem = default!;
[Dependency] private readonly SolutionContainerSystem _solutionContainerSystem = default!;
private float _accumulatedTimeFrame;
private HashSet<EntityUid> _fluidSpread = new();
public override void Initialize()
/// <summary>
/// Adds an overflow component to the map data component tracking overflowing puddles
/// </summary>
/// <param name="puddleUid">EntityUid of overflowing puddle</param>
/// <param name="puddle">Optional PuddleComponent</param>
/// <param name="xform">Optional TransformComponent</param>
public void AddOverflowingPuddle(EntityUid puddleUid, PuddleComponent? puddle = null,
TransformComponent? xform = null)
{
SubscribeLocalEvent<FluidSpreaderComponent, ComponentAdd>((uid, component, _) =>
FluidSpreaderAdd(uid, component));
}
if (!Resolve(puddleUid, ref puddle, ref xform, false) || xform.MapUid == null)
return;
public void AddOverflowingPuddle(PuddleComponent puddleComponent, Solution? solution = null)
{
var puddleSolution = solution;
if (puddleSolution == null && !_solutionContainerSystem.TryGetSolution(puddleComponent.Owner,
puddleComponent.SolutionName,
out puddleSolution)) return;
var mapId = xform.MapUid.Value;
var spreaderComponent = EntityManager.EnsureComponent<FluidSpreaderComponent>(puddleComponent.Owner);
spreaderComponent.OverflownSolution = puddleSolution;
spreaderComponent.Enabled = true;
FluidSpreaderAdd(spreaderComponent.Owner, spreaderComponent);
}
private void FluidSpreaderAdd(EntityUid uid, FluidSpreaderComponent component)
{
if (component.Enabled)
_fluidSpread.Add(uid);
EntityManager.EnsureComponent<FluidMapDataComponent>(mapId, out var component);
component.Puddles.Add(puddleUid);
}
public override void Update(float frameTime)
{
_accumulatedTimeFrame += frameTime;
if (!(_accumulatedTimeFrame >= 1.0f))
return;
_accumulatedTimeFrame -= 1.0f;
base.Update(frameTime);
var remQueue = new RemQueue<EntityUid>();
foreach (var uid in _fluidSpread)
Span<Direction> exploreDirections = stackalloc Direction[]
{
if (!TryComp(uid, out MetaDataComponent? meta) || meta.Deleted)
{
remQueue.Add(uid);
continue;
}
Direction.North,
Direction.East,
Direction.South,
Direction.West,
};
var puddles = new List<PuddleComponent>(4);
var puddleQuery = GetEntityQuery<PuddleComponent>();
var xFormQuery = GetEntityQuery<TransformComponent>();
if (meta.EntityPaused)
foreach (var fluidMapData in EntityQuery<FluidMapDataComponent>())
{
if (fluidMapData.Puddles.Count == 0 || _gameTiming.CurTime <= fluidMapData.GoalTime)
continue;
remQueue.Add(uid);
SpreadFluid(uid);
}
foreach (var removeUid in remQueue)
{
_fluidSpread.Remove(removeUid);
}
}
private void SpreadFluid(EntityUid suid)
{
EntityUid GetOrCreate(EntityUid uid, string prototype, IMapGrid grid, Vector2i pos)
{
return uid == EntityUid.Invalid
? EntityManager.SpawnEntity(prototype, grid.GridTileToWorld(pos))
: uid;
}
PuddleComponent? puddleComponent = null;
MetaDataComponent? metadataOriginal = null;
TransformComponent? transformOrig = null;
FluidSpreaderComponent? spreader = null;
if (!Resolve(suid, ref puddleComponent, ref metadataOriginal, ref transformOrig, ref spreader, false))
return;
var prototypeName = metadataOriginal.EntityPrototype!.ID;
var visitedTiles = new HashSet<Vector2i>();
if (!_mapManager.TryGetGrid(transformOrig.GridUid, out var mapGrid))
return;
// skip origin puddle
var nextToExpand = new List<PuddlePlacer>(9);
ExpandPuddle(suid, visitedTiles, mapGrid, nextToExpand);
while (nextToExpand.Count > 0
&& spreader.OverflownSolution.CurrentVolume > FixedPoint2.Zero)
{
// we need to clamp to prevent spreading 0u fluids, while never going over spill limit
var divided = FixedPoint2.Clamp(spreader.OverflownSolution.CurrentVolume / nextToExpand.Count,
FixedPoint2.Epsilon, puddleComponent.OverflowVolume);
foreach (var posAndUid in nextToExpand)
var newIteration = new HashSet<EntityUid>();
foreach (var puddleUid in fluidMapData.Puddles)
{
var puddleUid = GetOrCreate(posAndUid.Uid, prototypeName, mapGrid, posAndUid.Pos);
if (!TryComp(puddleUid, out PuddleComponent? puddle))
if (!puddleQuery.TryGetComponent(puddleUid, out var puddle)
|| !xFormQuery.TryGetComponent(puddleUid, out var transform)
|| !_mapManager.TryGetGrid(transform.GridUid, out var mapGrid))
continue;
posAndUid.Uid = puddleUid;
puddles.Clear();
var pos = transform.Coordinates;
if (puddle.CurrentVolume >= puddle.OverflowVolume) continue;
var totalVolume = _puddleSystem.CurrentVolume(puddle.Owner, puddle);
exploreDirections.Shuffle();
foreach (var direction in exploreDirections)
{
var newPos = pos.Offset(direction);
if (CheckTile(puddle.Owner, puddle, newPos, mapGrid,
out var puddleComponent))
{
puddles.Add(puddleComponent);
totalVolume += _puddleSystem.CurrentVolume(puddleComponent.Owner, puddleComponent);
}
}
// -puddle.OverflowLeft is guaranteed to be >= 0
// iff puddle.CurrentVolume >= puddle.OverflowVolume
var split = FixedPoint2.Min(divided, -puddle.OverflowLeft);
_puddleSystem.TryAddSolution(
puddle.Owner,
spreader.OverflownSolution.SplitSolution(split),
false, false, puddle);
// if solution is spent do not explore
if (spreader.OverflownSolution.CurrentVolume <= FixedPoint2.Zero)
return;
_puddleSystem.EqualizePuddles(puddle.Owner, puddles, totalVolume, newIteration, puddle);
}
// find edges
nextToExpand = ExpandPuddles(nextToExpand, visitedTiles, mapGrid);
fluidMapData.Puddles.Clear();
fluidMapData.Puddles.UnionWith(newIteration);
fluidMapData.UpdateGoal(_gameTiming.CurTime);
}
}
private List<PuddlePlacer> ExpandPuddles(List<PuddlePlacer> toExpand,
HashSet<Vector2i> visitedTiles,
IMapGrid mapGrid)
/// <summary>
/// Check a tile is valid for solution allocation.
/// </summary>
/// <param name="srcUid">Entity Uid of original puddle</param>
/// <param name="srcPuddle">PuddleComponent attached to srcUid</param>
/// <param name="pos">at which to check tile</param>
/// <param name="mapGrid">helper param needed to extract entities</param>
/// <param name="puddle">either found or newly created PuddleComponent.</param>
/// <returns>true if tile is empty or occupied by a non-overflowing puddle (or a puddle close to being overflowing)</returns>
private bool CheckTile(EntityUid srcUid, PuddleComponent srcPuddle, EntityCoordinates pos, IMapGrid mapGrid,
[NotNullWhen(true)] out PuddleComponent? puddle)
{
var nextToExpand = new List<PuddlePlacer>(9);
foreach (var puddlePlacer in toExpand)
{
ExpandPuddle(puddlePlacer.Uid, visitedTiles, mapGrid, nextToExpand, puddlePlacer.Pos);
}
return nextToExpand;
}
private void ExpandPuddle(EntityUid puddle,
HashSet<Vector2i> visitedTiles,
IMapGrid mapGrid,
List<PuddlePlacer> nextToExpand,
Vector2i? pos = null)
{
TransformComponent? transform = null;
if (pos == null && !Resolve(puddle, ref transform, false))
{
return;
}
var puddlePos = pos ?? transform!.Coordinates.ToVector2i(EntityManager, _mapManager);
// prepare next set of puddles to be expanded
foreach (var direction in SharedDirectionExtensions.RandomDirections().ToArray())
{
var newPos = puddlePos.Offset(direction);
if (visitedTiles.Contains(newPos))
continue;
visitedTiles.Add(newPos);
if (CanExpand(newPos, mapGrid, out var uid))
nextToExpand.Add(new PuddlePlacer(newPos, (EntityUid) uid));
}
}
private bool CanExpand(Vector2i newPos, IMapGrid mapGrid,
[NotNullWhen(true)] out EntityUid? uid)
{
if (!mapGrid.TryGetTileRef(newPos, out var tileRef)
if (!mapGrid.TryGetTileRef(pos, out var tileRef)
|| tileRef.Tile.IsEmpty)
{
uid = null;
puddle = null;
return false;
}
foreach (var entity in mapGrid.GetAnchoredEntities(newPos))
{
IPhysBody? physics = null;
PuddleComponent? existingPuddle = null;
var puddleCurrentVolume = _puddleSystem.CurrentVolume(srcUid, srcPuddle);
// This is an invalid location
if (Resolve(entity, ref physics, false)
&& (physics.CollisionLayer & (int) CollisionGroup.Impassable) != 0)
foreach (var entity in mapGrid.GetAnchoredEntities(pos))
{
// If this is valid puddle check if we spread to it.
if (TryComp(entity, out PuddleComponent? existingPuddle))
{
uid = null;
return false;
// If current puddle has more volume than current we skip that field
if (_puddleSystem.CurrentVolume(existingPuddle.Owner, existingPuddle) >= puddleCurrentVolume)
{
puddle = null;
return false;
}
puddle = existingPuddle;
return true;
}
if (!Resolve(entity, ref existingPuddle, false))
continue;
uid = entity;
return true;
// if not puddle is this tile blocked by an object like wall or door
if (TryComp(entity, out PhysicsComponent? physComponent)
&& physComponent.CanCollide
&& (physComponent.CollisionLayer & (int) CollisionGroup.MobMask) != 0)
{
puddle = null;
return false;
}
}
uid = EntityUid.Invalid;
puddle = _puddleSystem.SpawnPuddle(srcUid, pos, srcPuddle);
return true;
}
}
// Helper to allow mutable pair of (Pos, Uid)
internal sealed class PuddlePlacer
{
internal Vector2i Pos;
internal EntityUid Uid;
public PuddlePlacer(Vector2i pos, EntityUid uid)
{
Pos = pos;
Uid = uid;
}
}

View File

@@ -0,0 +1,84 @@
using Content.Server.Fluids.Components;
using Content.Shared.Fluids;
using Robust.Server.Player;
using Robust.Shared.Map;
using Robust.Shared.Timing;
namespace Content.Server.Fluids.EntitySystems;
public sealed class PuddleDebugDebugOverlaySystem : SharedPuddleDebugOverlaySystem
{
[Dependency] private readonly IGameTiming _timing = default!;
[Dependency] private readonly IMapManager _mapManager = default!;
private readonly HashSet<IPlayerSession> _playerObservers = new();
public bool ToggleObserver(IPlayerSession observer)
{
NextTick ??= _timing.CurTime + Cooldown;
if (_playerObservers.Contains(observer))
{
RemoveObserver(observer);
return false;
}
_playerObservers.Add(observer);
return true;
}
private void RemoveObserver(IPlayerSession observer)
{
if (!_playerObservers.Remove(observer))
{
return;
}
var message = new PuddleOverlayDisableMessage();
RaiseNetworkEvent(message, observer.ConnectedClient);
}
public override void Update(float frameTime)
{
base.Update(frameTime);
if (NextTick == null || _timing.CurTime < NextTick)
return;
foreach (var session in _playerObservers)
{
if (session.AttachedEntity is not { Valid: true } entity)
continue;
var transform = EntityManager.GetComponent<TransformComponent>(entity);
var worldBounds = Box2.CenteredAround(transform.WorldPosition,
new Vector2(LocalViewRange, LocalViewRange));
foreach (var grid in _mapManager.FindGridsIntersecting(transform.MapID, worldBounds))
{
var data = new List<PuddleDebugOverlayData>();
var gridUid = grid.GridEntityId;
if (!Exists(gridUid))
continue;
foreach (var uid in grid.GetAnchoredEntities(worldBounds))
{
PuddleComponent? puddle = null;
TransformComponent? xform = null;
if (!Resolve(uid, ref puddle, ref xform, false))
continue;
var pos = xform.Coordinates.ToVector2i(EntityManager, _mapManager);
data.Add(new PuddleDebugOverlayData(pos, puddle.CurrentVolume));
}
RaiseNetworkEvent(new PuddleOverlayDebugMessage(gridUid, data.ToArray()));
}
}
NextTick = _timing.CurTime + Cooldown;
}
}

View File

@@ -1,15 +1,18 @@
using Content.Server.Chemistry.Components.SolutionManager;
using Content.Server.Chemistry.EntitySystems;
using Content.Server.Fluids.Components;
using Content.Shared.Chemistry.Components;
using Content.Shared.Examine;
using Content.Shared.FixedPoint;
using Content.Shared.Fluids;
using Content.Shared.StepTrigger;
using Content.Shared.Slippery;
using Content.Shared.StepTrigger.Components;
using Content.Shared.StepTrigger.Systems;
using JetBrains.Annotations;
using Robust.Server.GameObjects;
using Robust.Shared.Audio;
using Robust.Shared.Map;
using Robust.Shared.Player;
using Solution = Content.Shared.Chemistry.Components.Solution;
namespace Content.Server.Fluids.EntitySystems
{
@@ -19,6 +22,9 @@ namespace Content.Server.Fluids.EntitySystems
[Dependency] private readonly SolutionContainerSystem _solutionContainerSystem = default!;
[Dependency] private readonly FluidSpreaderSystem _fluidSpreaderSystem = default!;
[Dependency] private readonly StepTriggerSystem _stepTrigger = default!;
[Dependency] private readonly SlipperySystem _slipSystem = default!;
[Dependency] private readonly EvaporationSystem _evaporationSystem = default!;
public override void Initialize()
{
@@ -53,17 +59,23 @@ namespace Content.Server.Fluids.EntitySystems
// Opacity based on level of fullness to overflow
// Hard-cap lower bound for visibility reasons
var volumeScale = puddleComponent.CurrentVolume.Float() / puddleComponent.OverflowVolume.Float() * puddleComponent.OpacityModifier;
var volumeScale = CurrentVolume(puddleComponent.Owner, puddleComponent).Float() /
puddleComponent.OverflowVolume.Float() *
puddleComponent.OpacityModifier;
var puddleSolution = _solutionContainerSystem.EnsureSolution(uid, puddleComponent.SolutionName);
bool hasEvaporationComponent = EntityManager.TryGetComponent<EvaporationComponent>(uid, out var evaporationComponent);
bool hasEvaporationComponent =
EntityManager.TryGetComponent<EvaporationComponent>(uid, out var evaporationComponent);
bool canEvaporate = (hasEvaporationComponent &&
(evaporationComponent!.LowerLimit == 0 || puddleComponent.CurrentVolume > evaporationComponent.LowerLimit));
(evaporationComponent!.LowerLimit == 0 ||
CurrentVolume(puddleComponent.Owner, puddleComponent) >
evaporationComponent.LowerLimit));
// "Does this puddle's sprite need changing to the wet floor effect sprite?"
bool changeToWetFloor = (puddleComponent.CurrentVolume <= puddleComponent.WetFloorEffectThreshold
&& canEvaporate);
bool changeToWetFloor = (CurrentVolume(puddleComponent.Owner, puddleComponent) <=
puddleComponent.WetFloorEffectThreshold
&& canEvaporate);
appearanceComponent.SetData(PuddleVisuals.VolumeScale, volumeScale);
appearanceComponent.SetData(PuddleVisuals.SolutionColor, puddleSolution.Color);
@@ -73,12 +85,12 @@ namespace Content.Server.Fluids.EntitySystems
private void UpdateSlip(EntityUid entityUid, PuddleComponent puddleComponent)
{
if ((puddleComponent.SlipThreshold == FixedPoint2.New(-1) ||
puddleComponent.CurrentVolume < puddleComponent.SlipThreshold) &&
CurrentVolume(puddleComponent.Owner, puddleComponent) < puddleComponent.SlipThreshold) &&
TryComp(entityUid, out StepTriggerComponent? stepTrigger))
{
_stepTrigger.SetActive(entityUid, false, stepTrigger);
}
else if (puddleComponent.CurrentVolume >= puddleComponent.SlipThreshold)
else if (CurrentVolume(puddleComponent.Owner, puddleComponent) >= puddleComponent.SlipThreshold)
{
var comp = EnsureComp<StepTriggerComponent>(entityUid);
_stepTrigger.SetActive(entityUid, true, comp);
@@ -121,7 +133,7 @@ namespace Content.Server.Fluids.EntitySystems
}
/// <summary>
///
/// Try to add solution to <paramref name="puddleUid"/>.
/// </summary>
/// <param name="puddleUid">Puddle to which we add</param>
/// <param name="addedSolution">Solution that is added to puddleComponent</param>
@@ -140,23 +152,15 @@ namespace Content.Server.Fluids.EntitySystems
if (addedSolution.TotalVolume == 0 ||
!_solutionContainerSystem.TryGetSolution(puddleComponent.Owner, puddleComponent.SolutionName,
out var puddleSolution))
out var solution))
{
return false;
}
var result = _solutionContainerSystem
.TryMixAndOverflow(puddleComponent.Owner, puddleSolution, addedSolution, puddleComponent.OverflowVolume,
out var overflowSolution);
if (checkForOverflow && overflowSolution != null)
solution.AddSolution(addedSolution);
if (checkForOverflow && IsOverflowing(puddleUid, puddleComponent))
{
_fluidSpreaderSystem.AddOverflowingPuddle(puddleComponent, overflowSolution);
}
if (!result)
{
return false;
_fluidSpreaderSystem.AddOverflowingPuddle(puddleComponent.Owner, puddleComponent);
}
RaiseLocalEvent(puddleComponent.Owner, new SolutionChangedEvent(), true);
@@ -171,6 +175,46 @@ namespace Content.Server.Fluids.EntitySystems
return true;
}
/// <summary>
/// Given a large srcPuddle and smaller destination puddles, this method will equalize their <see cref="Solution.CurrentVolume"/>
/// </summary>
/// <param name="srcPuddle">puddle that donates liquids to other puddles</param>
/// <param name="destinationPuddles">List of puddles that we want to equalize, their puddle <see cref="Solution.CurrentVolume"/> should be less than sourcePuddleComponent</param>
/// <param name="totalVolume">Total volume of src and destination puddle</param>
/// <param name="stillOverflowing">optional parameter, that after equalization adds all still overflowing puddles.</param>
/// <param name="sourcePuddleComponent">puddleComponent for <paramref name="srcPuddle"/></param>
public void EqualizePuddles(EntityUid srcPuddle, List<PuddleComponent> destinationPuddles,
FixedPoint2 totalVolume,
HashSet<EntityUid>? stillOverflowing = null,
PuddleComponent? sourcePuddleComponent = null)
{
if (!Resolve(srcPuddle, ref sourcePuddleComponent)
|| !_solutionContainerSystem.TryGetSolution(srcPuddle, sourcePuddleComponent.SolutionName,
out var srcSolution))
return;
var dividedVolume = totalVolume / (destinationPuddles.Count + 1);
foreach (var destPuddle in destinationPuddles)
{
if (!_solutionContainerSystem.TryGetSolution(destPuddle.Owner, destPuddle.SolutionName,
out var destSolution))
continue;
var takeAmount = FixedPoint2.Max(0, dividedVolume - destSolution.CurrentVolume);
TryAddSolution(destPuddle.Owner, srcSolution.SplitSolution(takeAmount), false, false, destPuddle);
if (stillOverflowing != null && IsOverflowing(destPuddle.Owner, destPuddle))
{
stillOverflowing.Add(destPuddle.Owner);
}
}
if (stillOverflowing != null && srcSolution.CurrentVolume > sourcePuddleComponent.OverflowVolume)
{
stillOverflowing.Add(srcPuddle);
}
}
/// <summary>
/// Whether adding this solution to this puddle would overflow.
/// </summary>
@@ -183,7 +227,34 @@ namespace Content.Server.Fluids.EntitySystems
if (!Resolve(uid, ref puddle))
return false;
return puddle.CurrentVolume + solution.TotalVolume > puddle.OverflowVolume;
return CurrentVolume(uid, puddle) + solution.TotalVolume > puddle.OverflowVolume;
}
/// <summary>
/// Whether adding this solution to this puddle would overflow.
/// </summary>
/// <param name="uid">Uid of owning entity</param>
/// <param name="puddle">Puddle ref param</param>
/// <returns></returns>
private bool IsOverflowing(EntityUid uid, PuddleComponent? puddle = null)
{
if (!Resolve(uid, ref puddle))
return false;
return CurrentVolume(uid, puddle) > puddle.OverflowVolume;
}
public PuddleComponent SpawnPuddle(EntityUid srcUid, EntityCoordinates pos, PuddleComponent? srcPuddleComponent = null)
{
MetaDataComponent? metadata = null;
Resolve(srcUid, ref srcPuddleComponent, ref metadata);
var prototype = metadata?.EntityPrototype?.ID ?? "PuddleSmear"; // TODO Spawn a entity based on another entity
var destUid = EntityManager.SpawnEntity(prototype, pos);
var destPuddle = EntityManager.EnsureComponent<PuddleComponent>(destUid);
return destPuddle;
}
}
}