Fluid spread refactor (#11908)
Co-authored-by: metalgearsloth <31366439+metalgearsloth@users.noreply.github.com> Fix undefined
This commit is contained in:
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user