Fluid spread refactor (#11908)
Co-authored-by: metalgearsloth <31366439+metalgearsloth@users.noreply.github.com> Fix undefined
This commit is contained in:
45
Content.Client/Fluids/PuddleDebugOverlaySystem.cs
Normal file
45
Content.Client/Fluids/PuddleDebugOverlaySystem.cs
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
using System.Collections;
|
||||||
|
using Content.Shared.Fluids;
|
||||||
|
using Robust.Client.Graphics;
|
||||||
|
|
||||||
|
namespace Content.Client.Fluids;
|
||||||
|
|
||||||
|
public sealed class PuddleDebugOverlaySystem : SharedPuddleDebugOverlaySystem
|
||||||
|
{
|
||||||
|
[Dependency] private readonly IOverlayManager _overlayManager = default!;
|
||||||
|
|
||||||
|
public readonly Dictionary<EntityUid, PuddleOverlayDebugMessage> TileData = new();
|
||||||
|
private PuddleOverlay? _overlay;
|
||||||
|
public override void Initialize()
|
||||||
|
{
|
||||||
|
base.Initialize();
|
||||||
|
|
||||||
|
SubscribeNetworkEvent<PuddleOverlayDisableMessage>(DisableOverlay);
|
||||||
|
SubscribeNetworkEvent<PuddleOverlayDebugMessage>(RenderDebugData);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void RenderDebugData(PuddleOverlayDebugMessage message)
|
||||||
|
{
|
||||||
|
TileData[message.GridUid] = message;
|
||||||
|
if (_overlay != null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
_overlay = new PuddleOverlay();
|
||||||
|
_overlayManager.AddOverlay(_overlay);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void DisableOverlay(PuddleOverlayDisableMessage message)
|
||||||
|
{
|
||||||
|
TileData.Clear();
|
||||||
|
if (_overlay == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
_overlayManager.RemoveOverlay(_overlay);
|
||||||
|
_overlay = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public PuddleDebugOverlayData[] GetData(EntityUid mapGridGridEntityId)
|
||||||
|
{
|
||||||
|
return TileData[mapGridGridEntityId].OverlayData;
|
||||||
|
}
|
||||||
|
}
|
||||||
117
Content.Client/Fluids/PuddleOverlay.cs
Normal file
117
Content.Client/Fluids/PuddleOverlay.cs
Normal file
@@ -0,0 +1,117 @@
|
|||||||
|
using Content.Shared.FixedPoint;
|
||||||
|
using Robust.Client.Graphics;
|
||||||
|
using Robust.Client.ResourceManagement;
|
||||||
|
using Robust.Shared.Enums;
|
||||||
|
using Robust.Shared.Map;
|
||||||
|
|
||||||
|
namespace Content.Client.Fluids;
|
||||||
|
|
||||||
|
public sealed class PuddleOverlay : Overlay
|
||||||
|
{
|
||||||
|
[Dependency] private readonly IMapManager _mapManager = default!;
|
||||||
|
[Dependency] private readonly IEyeManager _eyeManager = default!;
|
||||||
|
[Dependency] private readonly IEntityManager _entityManager = default!;
|
||||||
|
[Dependency] private readonly IEntitySystemManager _entitySystemManager = default!;
|
||||||
|
private readonly PuddleDebugOverlaySystem _debugOverlaySystem;
|
||||||
|
|
||||||
|
private readonly Color _heavyPuddle = new(0, 255, 255, 50);
|
||||||
|
private readonly Color _mediumPuddle = new(0, 150, 255, 50);
|
||||||
|
private readonly Color _lightPuddle = new(0, 50, 255, 50);
|
||||||
|
|
||||||
|
private readonly Font _font;
|
||||||
|
|
||||||
|
public override OverlaySpace Space => OverlaySpace.ScreenSpace | OverlaySpace.WorldSpace;
|
||||||
|
|
||||||
|
public PuddleOverlay()
|
||||||
|
{
|
||||||
|
IoCManager.InjectDependencies(this);
|
||||||
|
_debugOverlaySystem = _entitySystemManager.GetEntitySystem<PuddleDebugOverlaySystem>();
|
||||||
|
var cache = IoCManager.Resolve<IResourceCache>();
|
||||||
|
_font = new VectorFont(cache.GetResource<FontResource>("/Fonts/NotoSans/NotoSans-Regular.ttf"), 8);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void Draw(in OverlayDrawArgs args)
|
||||||
|
{
|
||||||
|
switch (args.Space)
|
||||||
|
{
|
||||||
|
case OverlaySpace.ScreenSpace:
|
||||||
|
DrawScreen(args);
|
||||||
|
break;
|
||||||
|
case OverlaySpace.WorldSpace:
|
||||||
|
DrawWorld(args);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void DrawWorld(in OverlayDrawArgs args)
|
||||||
|
{
|
||||||
|
var drawHandle = args.WorldHandle;
|
||||||
|
Box2 gridBounds;
|
||||||
|
var xformQuery = _entityManager.GetEntityQuery<TransformComponent>();
|
||||||
|
|
||||||
|
foreach (var gridId in _debugOverlaySystem.TileData.Keys)
|
||||||
|
{
|
||||||
|
if (!_mapManager.TryGetGrid(gridId, out var mapGrid))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
var gridXform = xformQuery.GetComponent(gridId);
|
||||||
|
var (_, _, worldMatrix, invWorldMatrix) = gridXform.GetWorldPositionRotationMatrixWithInv(xformQuery);
|
||||||
|
gridBounds = invWorldMatrix.TransformBox(args.WorldBounds).Enlarged(mapGrid.TileSize * 2);
|
||||||
|
drawHandle.SetTransform(worldMatrix);
|
||||||
|
|
||||||
|
foreach (var debugOverlayData in _debugOverlaySystem.GetData(mapGrid.GridEntityId))
|
||||||
|
{
|
||||||
|
var centre = ((Vector2) debugOverlayData.Pos + 0.5f) * mapGrid.TileSize;
|
||||||
|
|
||||||
|
// is the center of this tile visible
|
||||||
|
if (!gridBounds.Contains(centre))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
var box = Box2.UnitCentered.Translated(centre);
|
||||||
|
drawHandle.DrawRect(box, Color.Blue, false);
|
||||||
|
drawHandle.DrawRect(box, ColorMap(debugOverlayData.CurrentVolume));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
drawHandle.SetTransform(Matrix3.Identity);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void DrawScreen(in OverlayDrawArgs args)
|
||||||
|
{
|
||||||
|
var drawHandle = args.ScreenHandle;
|
||||||
|
var xformQuery = _entityManager.GetEntityQuery<TransformComponent>();
|
||||||
|
|
||||||
|
|
||||||
|
foreach (var gridId in _debugOverlaySystem.TileData.Keys)
|
||||||
|
{
|
||||||
|
if (!_mapManager.TryGetGrid(gridId, out var mapGrid))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
var gridXform = xformQuery.GetComponent(gridId);
|
||||||
|
var (_, _, matrix, invMatrix) = gridXform.GetWorldPositionRotationMatrixWithInv(xformQuery);
|
||||||
|
var gridBounds = invMatrix.TransformBox(args.WorldBounds).Enlarged(mapGrid.TileSize * 2);
|
||||||
|
|
||||||
|
foreach (var debugOverlayData in _debugOverlaySystem.GetData(mapGrid.GridEntityId))
|
||||||
|
{
|
||||||
|
var centre = ((Vector2) debugOverlayData.Pos + 0.5f) * mapGrid.TileSize;
|
||||||
|
|
||||||
|
// // is the center of this tile visible
|
||||||
|
if (!gridBounds.Contains(centre))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
var screenCenter = _eyeManager.WorldToScreen(matrix.Transform(centre));
|
||||||
|
|
||||||
|
drawHandle.DrawString(_font, screenCenter, debugOverlayData.CurrentVolume.ToString(), Color.White);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private Color ColorMap(FixedPoint2 intensity)
|
||||||
|
{
|
||||||
|
var fraction = 1 - intensity / FixedPoint2.New(20f);
|
||||||
|
var result = fraction < 0.5f
|
||||||
|
? Color.InterpolateBetween(_mediumPuddle, _heavyPuddle, fraction.Float() * 2)
|
||||||
|
: Color.InterpolateBetween(_lightPuddle, _mediumPuddle, (fraction.Float() - 0.5f) * 2);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
#nullable enable
|
#nullable enable
|
||||||
using System;
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Content.Server.Fluids.Components;
|
using Content.Server.Fluids.Components;
|
||||||
using Content.Server.Fluids.EntitySystems;
|
using Content.Server.Fluids.EntitySystems;
|
||||||
@@ -31,13 +32,9 @@ public sealed class FluidSpill
|
|||||||
private readonly Direction[] _dirs =
|
private readonly Direction[] _dirs =
|
||||||
{
|
{
|
||||||
Direction.East,
|
Direction.East,
|
||||||
Direction.SouthEast,
|
|
||||||
Direction.South,
|
Direction.South,
|
||||||
Direction.SouthWest,
|
|
||||||
Direction.West,
|
Direction.West,
|
||||||
Direction.NorthWest,
|
|
||||||
Direction.North,
|
Direction.North,
|
||||||
Direction.NorthEast,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
@@ -46,12 +43,13 @@ public sealed class FluidSpill
|
|||||||
[Test]
|
[Test]
|
||||||
public async Task SpillEvenlyTest()
|
public async Task SpillEvenlyTest()
|
||||||
{
|
{
|
||||||
await using var pairTracker = await PoolManager.GetServerClient(new PoolSettings{NoClient = true});
|
await using var pairTracker = await PoolManager.GetServerClient(new PoolSettings { NoClient = true });
|
||||||
var server = pairTracker.Pair.Server;
|
var server = pairTracker.Pair.Server;
|
||||||
var mapManager = server.ResolveDependency<IMapManager>();
|
var mapManager = server.ResolveDependency<IMapManager>();
|
||||||
var entityManager = server.ResolveDependency<IEntityManager>();
|
var entityManager = server.ResolveDependency<IEntityManager>();
|
||||||
var spillSystem = server.ResolveDependency<IEntitySystemManager>().GetEntitySystem<SpillableSystem>();
|
var spillSystem = server.ResolveDependency<IEntitySystemManager>().GetEntitySystem<SpillableSystem>();
|
||||||
var gameTiming = server.ResolveDependency<IGameTiming>();
|
var gameTiming = server.ResolveDependency<IGameTiming>();
|
||||||
|
var puddleSystem = server.ResolveDependency<IEntitySystemManager>().GetEntitySystem<PuddleSystem>();
|
||||||
MapId mapId;
|
MapId mapId;
|
||||||
EntityUid gridId = default;
|
EntityUid gridId = default;
|
||||||
|
|
||||||
@@ -89,38 +87,44 @@ public sealed class FluidSpill
|
|||||||
var puddle = GetPuddle(entityManager, grid, _origin);
|
var puddle = GetPuddle(entityManager, grid, _origin);
|
||||||
|
|
||||||
Assert.That(puddle, Is.Not.Null);
|
Assert.That(puddle, Is.Not.Null);
|
||||||
Assert.That(puddle!.CurrentVolume, Is.EqualTo(FixedPoint2.New(20)));
|
Assert.That(puddleSystem.CurrentVolume(puddle!.Owner, puddle), Is.EqualTo(FixedPoint2.New(20)));
|
||||||
|
|
||||||
foreach (var direction in _dirs)
|
foreach (var direction in _dirs)
|
||||||
{
|
{
|
||||||
var newPos = _origin.Offset(direction);
|
var newPos = _origin.Offset(direction);
|
||||||
var sidePuddle = GetPuddle(entityManager, grid, newPos);
|
var sidePuddle = GetPuddle(entityManager, grid, newPos);
|
||||||
Assert.That(sidePuddle, Is.Not.Null);
|
Assert.That(sidePuddle, Is.Not.Null);
|
||||||
Assert.That(sidePuddle!.CurrentVolume, Is.EqualTo(FixedPoint2.New(10)));
|
Assert.That(puddleSystem.CurrentVolume(sidePuddle!.Owner, sidePuddle), Is.EqualTo(FixedPoint2.New(20)));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
await pairTracker.CleanReturnAsync();
|
await pairTracker.CleanReturnAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
public async Task SpillSmallOverflowTest()
|
public async Task SpillCorner()
|
||||||
{
|
{
|
||||||
await using var pairTracker = await PoolManager.GetServerClient();
|
await using var pairTracker = await PoolManager.GetServerClient(new PoolSettings { NoClient = true });
|
||||||
var server = pairTracker.Pair.Server;
|
var server = pairTracker.Pair.Server;
|
||||||
|
|
||||||
var mapManager = server.ResolveDependency<IMapManager>();
|
var mapManager = server.ResolveDependency<IMapManager>();
|
||||||
var entityManager = server.ResolveDependency<IEntityManager>();
|
var entityManager = server.ResolveDependency<IEntityManager>();
|
||||||
var spillSystem = server.ResolveDependency<IEntitySystemManager>().GetEntitySystem<SpillableSystem>();
|
var spillSystem = server.ResolveDependency<IEntitySystemManager>().GetEntitySystem<SpillableSystem>();
|
||||||
|
var puddleSystem = server.ResolveDependency<IEntitySystemManager>().GetEntitySystem<PuddleSystem>();
|
||||||
var gameTiming = server.ResolveDependency<IGameTiming>();
|
var gameTiming = server.ResolveDependency<IGameTiming>();
|
||||||
MapId mapId;
|
MapId mapId;
|
||||||
EntityUid gridId = default;
|
EntityUid gridId = default;
|
||||||
|
|
||||||
|
/*
|
||||||
|
In this test, if o is spillage puddle and # are walls, we want to ensure all tiles are empty (`.`)
|
||||||
|
o # .
|
||||||
|
# . .
|
||||||
|
. . .
|
||||||
|
*/
|
||||||
await server.WaitPost(() =>
|
await server.WaitPost(() =>
|
||||||
{
|
{
|
||||||
mapId = mapManager.CreateMap();
|
mapId = mapManager.CreateMap();
|
||||||
var grid = mapManager.CreateGrid(mapId);
|
var grid = mapManager.CreateGrid(mapId);
|
||||||
|
gridId = grid.GridEntityId;
|
||||||
|
|
||||||
for (var x = 0; x < 3; x++)
|
for (var x = 0; x < 3; x++)
|
||||||
{
|
{
|
||||||
@@ -130,49 +134,47 @@ public sealed class FluidSpill
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
gridId = grid.GridEntityId;
|
entityManager.SpawnEntity("WallReinforced", grid.GridTileToLocal(new Vector2i(0, 1)));
|
||||||
|
entityManager.SpawnEntity("WallReinforced", grid.GridTileToLocal(new Vector2i(1, 0)));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
var puddleOrigin = new Vector2i(0, 0);
|
||||||
await server.WaitAssertion(() =>
|
await server.WaitAssertion(() =>
|
||||||
{
|
{
|
||||||
var solution = new Solution("Water", FixedPoint2.New(20.01));
|
|
||||||
var grid = mapManager.GetGrid(gridId);
|
var grid = mapManager.GetGrid(gridId);
|
||||||
var tileRef = grid.GetTileRef(_origin);
|
var solution = new Solution("Water", FixedPoint2.New(100));
|
||||||
|
var tileRef = grid.GetTileRef(puddleOrigin);
|
||||||
var puddle = spillSystem.SpillAt(tileRef, solution, "PuddleSmear");
|
var puddle = spillSystem.SpillAt(tileRef, solution, "PuddleSmear");
|
||||||
|
|
||||||
Assert.That(puddle, Is.Not.Null);
|
Assert.That(puddle, Is.Not.Null);
|
||||||
|
Assert.That(GetPuddle(entityManager, grid, puddleOrigin), Is.Not.Null);
|
||||||
});
|
});
|
||||||
|
|
||||||
var sTimeToWait = (int) Math.Ceiling(2f * gameTiming.TickRate);
|
var sTimeToWait = (int) Math.Ceiling(2f * gameTiming.TickRate);
|
||||||
await PoolManager.RunTicksSync(pairTracker.Pair, sTimeToWait);
|
await server.WaitRunTicks(sTimeToWait);
|
||||||
|
|
||||||
await server.WaitAssertion(() =>
|
await server.WaitAssertion(() =>
|
||||||
{
|
{
|
||||||
var grid = mapManager.GetGrid(gridId);
|
var grid = mapManager.GetGrid(gridId);
|
||||||
var puddle = GetPuddle(entityManager, grid, _origin);
|
var puddle = GetPuddle(entityManager, grid, puddleOrigin);
|
||||||
Assert.That(puddle, Is.Not.Null);
|
|
||||||
Assert.That(puddle!.CurrentVolume, Is.EqualTo(FixedPoint2.New(20)));
|
|
||||||
|
|
||||||
// we don't know where a spill would happen
|
Assert.That(puddle, Is.Not.Null);
|
||||||
// but there should be only one
|
Assert.That(puddleSystem.CurrentVolume(puddle!.Owner, puddle), Is.EqualTo(FixedPoint2.New(100)));
|
||||||
var emptyField = 0;
|
|
||||||
var fullField = 0;
|
for (var x = 0; x < 3; x++)
|
||||||
foreach (var direction in _dirs)
|
|
||||||
{
|
{
|
||||||
var newPos = _origin.Offset(direction);
|
for (var y = 0; y < 3; y++)
|
||||||
var sidePuddle = GetPuddle(entityManager, grid, newPos);
|
|
||||||
if (sidePuddle == null)
|
|
||||||
{
|
{
|
||||||
emptyField++;
|
if (x == 0 && y == 0 || x == 0 && y == 1 || x == 1 && y == 0)
|
||||||
}
|
{
|
||||||
else if (sidePuddle.CurrentVolume == FixedPoint2.Epsilon)
|
continue;
|
||||||
{
|
}
|
||||||
fullField++;
|
|
||||||
|
var newPos = new Vector2i(x, y);
|
||||||
|
var sidePuddle = GetPuddle(entityManager, grid, newPos);
|
||||||
|
Assert.That(sidePuddle, Is.Null);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Assert.That(emptyField, Is.EqualTo(7));
|
|
||||||
Assert.That(fullField, Is.EqualTo(1));
|
|
||||||
});
|
});
|
||||||
|
|
||||||
await pairTracker.CleanReturnAsync();
|
await pairTracker.CleanReturnAsync();
|
||||||
|
|||||||
35
Content.Server/Fluids/Components/FluidMapDataComponent.cs
Normal file
35
Content.Server/Fluids/Components/FluidMapDataComponent.cs
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
using Content.Server.Fluids.EntitySystems;
|
||||||
|
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom;
|
||||||
|
|
||||||
|
namespace Content.Server.Fluids.Components;
|
||||||
|
|
||||||
|
[RegisterComponent]
|
||||||
|
[Access(typeof(FluidSpreaderSystem))]
|
||||||
|
public sealed class FluidMapDataComponent : Component
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// At what time will <see cref="FluidSpreaderSystem"/> be checked next
|
||||||
|
/// </summary>
|
||||||
|
[DataField("goalTime", customTypeSerializer:typeof(TimeOffsetSerializer))]
|
||||||
|
public TimeSpan GoalTime;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Delay between two runs of <see cref="FluidSpreaderSystem"/>
|
||||||
|
/// </summary>
|
||||||
|
[DataField("delay")]
|
||||||
|
public TimeSpan Delay = TimeSpan.FromSeconds(2);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Puddles to be expanded.
|
||||||
|
/// </summary>
|
||||||
|
[DataField("puddles")] public HashSet<EntityUid> Puddles = new();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Convenience method for setting GoalTime to <paramref name="start"/> + <see cref="Delay"/>
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="start">Time to which to add <see cref="Delay"/>, defaults to current <see cref="GoalTime"/></param>
|
||||||
|
public void UpdateGoal(TimeSpan? start = null)
|
||||||
|
{
|
||||||
|
GoalTime = (start ?? GoalTime) + Delay;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
using Content.Server.Fluids.EntitySystems;
|
|
||||||
using Content.Shared.Chemistry.Components;
|
|
||||||
|
|
||||||
namespace Content.Server.Fluids.Components;
|
|
||||||
|
|
||||||
[RegisterComponent]
|
|
||||||
[Access(typeof(FluidSpreaderSystem))]
|
|
||||||
public sealed class FluidSpreaderComponent : Component
|
|
||||||
{
|
|
||||||
[ViewVariables]
|
|
||||||
public Solution OverflownSolution = default!;
|
|
||||||
|
|
||||||
public bool Enabled { get; set; }
|
|
||||||
}
|
|
||||||
@@ -53,10 +53,7 @@ namespace Content.Server.Fluids.Components
|
|||||||
/// How much should this puddle's opacity be multiplied by?
|
/// How much should this puddle's opacity be multiplied by?
|
||||||
/// Useful for puddles that have a high overflow volume but still want to be mostly opaque.
|
/// Useful for puddles that have a high overflow volume but still want to be mostly opaque.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[DataField("opacityModifier")]
|
[DataField("opacityModifier")] public float OpacityModifier = 1.0f;
|
||||||
public float OpacityModifier = 1.0f;
|
|
||||||
|
|
||||||
public FixedPoint2 OverflowLeft => CurrentVolume - OverflowVolume;
|
|
||||||
|
|
||||||
[DataField("solution")] public string SolutionName { get; set; } = DefaultSolutionName;
|
[DataField("solution")] public string SolutionName { get; set; } = DefaultSolutionName;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -55,5 +55,20 @@ namespace Content.Server.Fluids.EntitySystems
|
|||||||
EntityManager.RemoveComponent(evaporationComponent.Owner, evaporationComponent);
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,237 +1,145 @@
|
|||||||
using System.Diagnostics.CodeAnalysis;
|
using System.Diagnostics.CodeAnalysis;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using Content.Server.Chemistry.EntitySystems;
|
|
||||||
using Content.Server.Fluids.Components;
|
using Content.Server.Fluids.Components;
|
||||||
using Content.Shared.Chemistry.Components;
|
using Content.Shared;
|
||||||
using Content.Shared.Directions;
|
using Content.Shared.Directions;
|
||||||
using Content.Shared.FixedPoint;
|
|
||||||
using Content.Shared.Physics;
|
using Content.Shared.Physics;
|
||||||
using JetBrains.Annotations;
|
using JetBrains.Annotations;
|
||||||
using Robust.Shared.Map;
|
using Robust.Shared.Map;
|
||||||
using Robust.Shared.Physics;
|
using Robust.Shared.Physics.Components;
|
||||||
using Robust.Shared.Utility;
|
using Robust.Shared.Timing;
|
||||||
|
|
||||||
namespace Content.Server.Fluids.EntitySystems;
|
namespace Content.Server.Fluids.EntitySystems;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Component that governs overflowing puddles. Controls how Puddles spread and updat
|
||||||
|
/// </summary>
|
||||||
[UsedImplicitly]
|
[UsedImplicitly]
|
||||||
public sealed class FluidSpreaderSystem : EntitySystem
|
public sealed class FluidSpreaderSystem : EntitySystem
|
||||||
{
|
{
|
||||||
|
[Dependency] private readonly IGameTiming _gameTiming = default!;
|
||||||
[Dependency] private readonly IMapManager _mapManager = default!;
|
[Dependency] private readonly IMapManager _mapManager = default!;
|
||||||
[Dependency] private readonly PuddleSystem _puddleSystem = default!;
|
[Dependency] private readonly PuddleSystem _puddleSystem = default!;
|
||||||
[Dependency] private readonly SolutionContainerSystem _solutionContainerSystem = default!;
|
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
private float _accumulatedTimeFrame;
|
/// Adds an overflow component to the map data component tracking overflowing puddles
|
||||||
private HashSet<EntityUid> _fluidSpread = new();
|
/// </summary>
|
||||||
|
/// <param name="puddleUid">EntityUid of overflowing puddle</param>
|
||||||
public override void Initialize()
|
/// <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, _) =>
|
if (!Resolve(puddleUid, ref puddle, ref xform, false) || xform.MapUid == null)
|
||||||
FluidSpreaderAdd(uid, component));
|
return;
|
||||||
}
|
|
||||||
|
|
||||||
public void AddOverflowingPuddle(PuddleComponent puddleComponent, Solution? solution = null)
|
var mapId = xform.MapUid.Value;
|
||||||
{
|
|
||||||
var puddleSolution = solution;
|
|
||||||
if (puddleSolution == null && !_solutionContainerSystem.TryGetSolution(puddleComponent.Owner,
|
|
||||||
puddleComponent.SolutionName,
|
|
||||||
out puddleSolution)) return;
|
|
||||||
|
|
||||||
var spreaderComponent = EntityManager.EnsureComponent<FluidSpreaderComponent>(puddleComponent.Owner);
|
EntityManager.EnsureComponent<FluidMapDataComponent>(mapId, out var component);
|
||||||
spreaderComponent.OverflownSolution = puddleSolution;
|
component.Puddles.Add(puddleUid);
|
||||||
spreaderComponent.Enabled = true;
|
|
||||||
FluidSpreaderAdd(spreaderComponent.Owner, spreaderComponent);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void FluidSpreaderAdd(EntityUid uid, FluidSpreaderComponent component)
|
|
||||||
{
|
|
||||||
if (component.Enabled)
|
|
||||||
_fluidSpread.Add(uid);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public override void Update(float frameTime)
|
public override void Update(float frameTime)
|
||||||
{
|
{
|
||||||
_accumulatedTimeFrame += frameTime;
|
|
||||||
|
|
||||||
if (!(_accumulatedTimeFrame >= 1.0f))
|
|
||||||
return;
|
|
||||||
|
|
||||||
_accumulatedTimeFrame -= 1.0f;
|
|
||||||
|
|
||||||
base.Update(frameTime);
|
base.Update(frameTime);
|
||||||
|
Span<Direction> exploreDirections = stackalloc Direction[]
|
||||||
var remQueue = new RemQueue<EntityUid>();
|
|
||||||
foreach (var uid in _fluidSpread)
|
|
||||||
{
|
{
|
||||||
if (!TryComp(uid, out MetaDataComponent? meta) || meta.Deleted)
|
Direction.North,
|
||||||
{
|
Direction.East,
|
||||||
remQueue.Add(uid);
|
Direction.South,
|
||||||
continue;
|
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;
|
continue;
|
||||||
|
|
||||||
remQueue.Add(uid);
|
var newIteration = new HashSet<EntityUid>();
|
||||||
|
foreach (var puddleUid in fluidMapData.Puddles)
|
||||||
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 puddleUid = GetOrCreate(posAndUid.Uid, prototypeName, mapGrid, posAndUid.Pos);
|
if (!puddleQuery.TryGetComponent(puddleUid, out var puddle)
|
||||||
|
|| !xFormQuery.TryGetComponent(puddleUid, out var transform)
|
||||||
if (!TryComp(puddleUid, out PuddleComponent? puddle))
|
|| !_mapManager.TryGetGrid(transform.GridUid, out var mapGrid))
|
||||||
continue;
|
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
|
_puddleSystem.EqualizePuddles(puddle.Owner, puddles, totalVolume, newIteration, puddle);
|
||||||
// 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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// find edges
|
fluidMapData.Puddles.Clear();
|
||||||
nextToExpand = ExpandPuddles(nextToExpand, visitedTiles, mapGrid);
|
fluidMapData.Puddles.UnionWith(newIteration);
|
||||||
|
fluidMapData.UpdateGoal(_gameTiming.CurTime);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private List<PuddlePlacer> ExpandPuddles(List<PuddlePlacer> toExpand,
|
|
||||||
HashSet<Vector2i> visitedTiles,
|
/// <summary>
|
||||||
IMapGrid mapGrid)
|
/// 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);
|
if (!mapGrid.TryGetTileRef(pos, out var tileRef)
|
||||||
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)
|
|
||||||
|| tileRef.Tile.IsEmpty)
|
|| tileRef.Tile.IsEmpty)
|
||||||
{
|
{
|
||||||
uid = null;
|
puddle = null;
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach (var entity in mapGrid.GetAnchoredEntities(newPos))
|
var puddleCurrentVolume = _puddleSystem.CurrentVolume(srcUid, srcPuddle);
|
||||||
{
|
|
||||||
IPhysBody? physics = null;
|
|
||||||
PuddleComponent? existingPuddle = null;
|
|
||||||
|
|
||||||
// This is an invalid location
|
foreach (var entity in mapGrid.GetAnchoredEntities(pos))
|
||||||
if (Resolve(entity, ref physics, false)
|
{
|
||||||
&& (physics.CollisionLayer & (int) CollisionGroup.Impassable) != 0)
|
// If this is valid puddle check if we spread to it.
|
||||||
|
if (TryComp(entity, out PuddleComponent? existingPuddle))
|
||||||
{
|
{
|
||||||
uid = null;
|
// If current puddle has more volume than current we skip that field
|
||||||
return false;
|
if (_puddleSystem.CurrentVolume(existingPuddle.Owner, existingPuddle) >= puddleCurrentVolume)
|
||||||
|
{
|
||||||
|
puddle = null;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
puddle = existingPuddle;
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!Resolve(entity, ref existingPuddle, false))
|
// if not puddle is this tile blocked by an object like wall or door
|
||||||
continue;
|
if (TryComp(entity, out PhysicsComponent? physComponent)
|
||||||
|
&& physComponent.CanCollide
|
||||||
uid = entity;
|
&& (physComponent.CollisionLayer & (int) CollisionGroup.MobMask) != 0)
|
||||||
return true;
|
{
|
||||||
|
puddle = null;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
uid = EntityUid.Invalid;
|
puddle = _puddleSystem.SpawnPuddle(srcUid, pos, srcPuddle);
|
||||||
return true;
|
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,15 +1,18 @@
|
|||||||
|
using Content.Server.Chemistry.Components.SolutionManager;
|
||||||
using Content.Server.Chemistry.EntitySystems;
|
using Content.Server.Chemistry.EntitySystems;
|
||||||
using Content.Server.Fluids.Components;
|
using Content.Server.Fluids.Components;
|
||||||
using Content.Shared.Chemistry.Components;
|
|
||||||
using Content.Shared.Examine;
|
using Content.Shared.Examine;
|
||||||
using Content.Shared.FixedPoint;
|
using Content.Shared.FixedPoint;
|
||||||
using Content.Shared.Fluids;
|
using Content.Shared.Fluids;
|
||||||
using Content.Shared.StepTrigger;
|
using Content.Shared.Slippery;
|
||||||
using Content.Shared.StepTrigger.Components;
|
using Content.Shared.StepTrigger.Components;
|
||||||
using Content.Shared.StepTrigger.Systems;
|
using Content.Shared.StepTrigger.Systems;
|
||||||
using JetBrains.Annotations;
|
using JetBrains.Annotations;
|
||||||
|
using Robust.Server.GameObjects;
|
||||||
using Robust.Shared.Audio;
|
using Robust.Shared.Audio;
|
||||||
|
using Robust.Shared.Map;
|
||||||
using Robust.Shared.Player;
|
using Robust.Shared.Player;
|
||||||
|
using Solution = Content.Shared.Chemistry.Components.Solution;
|
||||||
|
|
||||||
namespace Content.Server.Fluids.EntitySystems
|
namespace Content.Server.Fluids.EntitySystems
|
||||||
{
|
{
|
||||||
@@ -19,6 +22,9 @@ namespace Content.Server.Fluids.EntitySystems
|
|||||||
[Dependency] private readonly SolutionContainerSystem _solutionContainerSystem = default!;
|
[Dependency] private readonly SolutionContainerSystem _solutionContainerSystem = default!;
|
||||||
[Dependency] private readonly FluidSpreaderSystem _fluidSpreaderSystem = default!;
|
[Dependency] private readonly FluidSpreaderSystem _fluidSpreaderSystem = default!;
|
||||||
[Dependency] private readonly StepTriggerSystem _stepTrigger = default!;
|
[Dependency] private readonly StepTriggerSystem _stepTrigger = default!;
|
||||||
|
[Dependency] private readonly SlipperySystem _slipSystem = default!;
|
||||||
|
[Dependency] private readonly EvaporationSystem _evaporationSystem = default!;
|
||||||
|
|
||||||
|
|
||||||
public override void Initialize()
|
public override void Initialize()
|
||||||
{
|
{
|
||||||
@@ -53,17 +59,23 @@ namespace Content.Server.Fluids.EntitySystems
|
|||||||
|
|
||||||
// Opacity based on level of fullness to overflow
|
// Opacity based on level of fullness to overflow
|
||||||
// Hard-cap lower bound for visibility reasons
|
// 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);
|
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 &&
|
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?"
|
// "Does this puddle's sprite need changing to the wet floor effect sprite?"
|
||||||
bool changeToWetFloor = (puddleComponent.CurrentVolume <= puddleComponent.WetFloorEffectThreshold
|
bool changeToWetFloor = (CurrentVolume(puddleComponent.Owner, puddleComponent) <=
|
||||||
&& canEvaporate);
|
puddleComponent.WetFloorEffectThreshold
|
||||||
|
&& canEvaporate);
|
||||||
|
|
||||||
appearanceComponent.SetData(PuddleVisuals.VolumeScale, volumeScale);
|
appearanceComponent.SetData(PuddleVisuals.VolumeScale, volumeScale);
|
||||||
appearanceComponent.SetData(PuddleVisuals.SolutionColor, puddleSolution.Color);
|
appearanceComponent.SetData(PuddleVisuals.SolutionColor, puddleSolution.Color);
|
||||||
@@ -73,12 +85,12 @@ namespace Content.Server.Fluids.EntitySystems
|
|||||||
private void UpdateSlip(EntityUid entityUid, PuddleComponent puddleComponent)
|
private void UpdateSlip(EntityUid entityUid, PuddleComponent puddleComponent)
|
||||||
{
|
{
|
||||||
if ((puddleComponent.SlipThreshold == FixedPoint2.New(-1) ||
|
if ((puddleComponent.SlipThreshold == FixedPoint2.New(-1) ||
|
||||||
puddleComponent.CurrentVolume < puddleComponent.SlipThreshold) &&
|
CurrentVolume(puddleComponent.Owner, puddleComponent) < puddleComponent.SlipThreshold) &&
|
||||||
TryComp(entityUid, out StepTriggerComponent? stepTrigger))
|
TryComp(entityUid, out StepTriggerComponent? stepTrigger))
|
||||||
{
|
{
|
||||||
_stepTrigger.SetActive(entityUid, false, 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);
|
var comp = EnsureComp<StepTriggerComponent>(entityUid);
|
||||||
_stepTrigger.SetActive(entityUid, true, comp);
|
_stepTrigger.SetActive(entityUid, true, comp);
|
||||||
@@ -121,7 +133,7 @@ namespace Content.Server.Fluids.EntitySystems
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
///
|
/// Try to add solution to <paramref name="puddleUid"/>.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="puddleUid">Puddle to which we add</param>
|
/// <param name="puddleUid">Puddle to which we add</param>
|
||||||
/// <param name="addedSolution">Solution that is added to puddleComponent</param>
|
/// <param name="addedSolution">Solution that is added to puddleComponent</param>
|
||||||
@@ -140,23 +152,15 @@ namespace Content.Server.Fluids.EntitySystems
|
|||||||
|
|
||||||
if (addedSolution.TotalVolume == 0 ||
|
if (addedSolution.TotalVolume == 0 ||
|
||||||
!_solutionContainerSystem.TryGetSolution(puddleComponent.Owner, puddleComponent.SolutionName,
|
!_solutionContainerSystem.TryGetSolution(puddleComponent.Owner, puddleComponent.SolutionName,
|
||||||
out var puddleSolution))
|
out var solution))
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
var result = _solutionContainerSystem
|
solution.AddSolution(addedSolution);
|
||||||
.TryMixAndOverflow(puddleComponent.Owner, puddleSolution, addedSolution, puddleComponent.OverflowVolume,
|
if (checkForOverflow && IsOverflowing(puddleUid, puddleComponent))
|
||||||
out var overflowSolution);
|
|
||||||
|
|
||||||
if (checkForOverflow && overflowSolution != null)
|
|
||||||
{
|
{
|
||||||
_fluidSpreaderSystem.AddOverflowingPuddle(puddleComponent, overflowSolution);
|
_fluidSpreaderSystem.AddOverflowingPuddle(puddleComponent.Owner, puddleComponent);
|
||||||
}
|
|
||||||
|
|
||||||
if (!result)
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
RaiseLocalEvent(puddleComponent.Owner, new SolutionChangedEvent(), true);
|
RaiseLocalEvent(puddleComponent.Owner, new SolutionChangedEvent(), true);
|
||||||
@@ -171,6 +175,46 @@ namespace Content.Server.Fluids.EntitySystems
|
|||||||
return true;
|
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>
|
/// <summary>
|
||||||
/// Whether adding this solution to this puddle would overflow.
|
/// Whether adding this solution to this puddle would overflow.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -183,7 +227,34 @@ namespace Content.Server.Fluids.EntitySystems
|
|||||||
if (!Resolve(uid, ref puddle))
|
if (!Resolve(uid, ref puddle))
|
||||||
return false;
|
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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
32
Content.Server/Fluids/ShowFluidsCommand.cs
Normal file
32
Content.Server/Fluids/ShowFluidsCommand.cs
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
using Content.Server.Administration;
|
||||||
|
using Content.Server.Fluids.EntitySystems;
|
||||||
|
using Content.Shared.Administration;
|
||||||
|
using Robust.Server.Player;
|
||||||
|
using Robust.Shared.Console;
|
||||||
|
|
||||||
|
namespace Content.Server.Fluids;
|
||||||
|
|
||||||
|
[AdminCommand(AdminFlags.Debug)]
|
||||||
|
public sealed class ShowFluidsCommand : IConsoleCommand
|
||||||
|
{
|
||||||
|
[Dependency] private readonly IEntitySystemManager _entitySystem = default!;
|
||||||
|
public string Command => "showfluids";
|
||||||
|
public string Description => "Toggles seeing puddle debug overlay.";
|
||||||
|
public string Help => $"Usage: {Command}";
|
||||||
|
public void Execute(IConsoleShell shell, string argStr, string[] args)
|
||||||
|
{
|
||||||
|
var player = shell.Player as IPlayerSession;
|
||||||
|
if (player == null)
|
||||||
|
{
|
||||||
|
shell.WriteLine("You must be a player to use this command.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var fluidDebug = _entitySystem.GetEntitySystem<PuddleDebugDebugOverlaySystem>();
|
||||||
|
var enabled = fluidDebug.ToggleObserver(player);
|
||||||
|
|
||||||
|
shell.WriteLine(enabled
|
||||||
|
? "Enabled the puddle debug overlay."
|
||||||
|
: "Disabled the puddle debug overlay.");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,83 +1,14 @@
|
|||||||
using Content.Shared.Maps;
|
using System.Collections;
|
||||||
|
using System.Linq;
|
||||||
using Robust.Shared.Map;
|
using Robust.Shared.Map;
|
||||||
using Robust.Shared.Random;
|
using Robust.Shared.Random;
|
||||||
|
|
||||||
namespace Content.Shared.Directions
|
namespace Content.Shared.Directions;
|
||||||
|
|
||||||
|
public static class SharedDirectionExtensions
|
||||||
{
|
{
|
||||||
public static class SharedDirectionExtensions
|
public static EntityCoordinates Offset(this EntityCoordinates coordinates, Direction direction)
|
||||||
{
|
{
|
||||||
/// <summary>
|
return coordinates.Offset(direction.ToVec());
|
||||||
/// Gets random directions until none are left
|
|
||||||
/// </summary>
|
|
||||||
/// <returns>An enumerable of the directions.</returns>
|
|
||||||
public static IEnumerable<Direction> RandomDirections()
|
|
||||||
{
|
|
||||||
var directions = new[]
|
|
||||||
{
|
|
||||||
Direction.East,
|
|
||||||
Direction.SouthEast,
|
|
||||||
Direction.South,
|
|
||||||
Direction.SouthWest,
|
|
||||||
Direction.West,
|
|
||||||
Direction.NorthWest,
|
|
||||||
Direction.North,
|
|
||||||
Direction.NorthEast,
|
|
||||||
};
|
|
||||||
|
|
||||||
var robustRandom = IoCManager.Resolve<IRobustRandom>();
|
|
||||||
var n = directions.Length;
|
|
||||||
|
|
||||||
while (n > 1)
|
|
||||||
{
|
|
||||||
n--;
|
|
||||||
var k = robustRandom.Next(n + 1);
|
|
||||||
var value = directions[k];
|
|
||||||
directions[k] = directions[n];
|
|
||||||
directions[n] = value;
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach (var direction in directions)
|
|
||||||
{
|
|
||||||
yield return direction;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets tiles in random directions from the given one.
|
|
||||||
/// </summary>
|
|
||||||
/// <returns>An enumerable of the adjacent tiles.</returns>
|
|
||||||
public static IEnumerable<TileRef> AdjacentTilesRandom(this TileRef tile, bool ignoreSpace = false)
|
|
||||||
{
|
|
||||||
return tile.GridPosition().AdjacentTilesRandom(ignoreSpace);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets tiles in random directions from the given one.
|
|
||||||
/// </summary>
|
|
||||||
/// <returns>An enumerable of the adjacent tiles.</returns>
|
|
||||||
public static IEnumerable<TileRef> AdjacentTilesRandom(this EntityCoordinates coordinates, bool ignoreSpace = false)
|
|
||||||
{
|
|
||||||
foreach (var direction in RandomDirections())
|
|
||||||
{
|
|
||||||
var adjacent = coordinates.Offset(direction).GetTileRef();
|
|
||||||
|
|
||||||
if (adjacent == null)
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (ignoreSpace && adjacent.Value.Tile.IsEmpty)
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
yield return adjacent.Value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static EntityCoordinates Offset(this EntityCoordinates coordinates, Direction direction)
|
|
||||||
{
|
|
||||||
return coordinates.Offset(direction.ToVec());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
50
Content.Shared/Fluids/SharedPuddleDebugOverlaySystem.cs
Normal file
50
Content.Shared/Fluids/SharedPuddleDebugOverlaySystem.cs
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
using Content.Shared.FixedPoint;
|
||||||
|
using Robust.Shared.Serialization;
|
||||||
|
|
||||||
|
namespace Content.Shared.Fluids;
|
||||||
|
|
||||||
|
public abstract class SharedPuddleDebugOverlaySystem : EntitySystem
|
||||||
|
{
|
||||||
|
protected const float LocalViewRange = 16;
|
||||||
|
protected TimeSpan? NextTick = null;
|
||||||
|
protected TimeSpan Cooldown = TimeSpan.FromSeconds(0.5f);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Message for disable puddle overlay
|
||||||
|
/// </summary>
|
||||||
|
[Serializable, NetSerializable]
|
||||||
|
public sealed class PuddleOverlayDisableMessage : EntityEventArgs
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Message for puddle overlay display data
|
||||||
|
/// </summary>
|
||||||
|
[Serializable, NetSerializable]
|
||||||
|
public sealed class PuddleOverlayDebugMessage : EntityEventArgs
|
||||||
|
{
|
||||||
|
public PuddleDebugOverlayData[] OverlayData { get; }
|
||||||
|
|
||||||
|
public EntityUid GridUid { get; }
|
||||||
|
|
||||||
|
|
||||||
|
public PuddleOverlayDebugMessage(EntityUid gridUid, PuddleDebugOverlayData[] overlayData)
|
||||||
|
{
|
||||||
|
GridUid = gridUid;
|
||||||
|
OverlayData = overlayData;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Serializable, NetSerializable]
|
||||||
|
public readonly struct PuddleDebugOverlayData
|
||||||
|
{
|
||||||
|
public readonly Vector2i Pos;
|
||||||
|
public readonly FixedPoint2 CurrentVolume;
|
||||||
|
|
||||||
|
public PuddleDebugOverlayData(Vector2i pos, FixedPoint2 currentVolume)
|
||||||
|
{
|
||||||
|
CurrentVolume = currentVolume;
|
||||||
|
Pos = pos;
|
||||||
|
}
|
||||||
|
}
|
||||||
28
Content.Shared/SharedArrayExtension.cs
Normal file
28
Content.Shared/SharedArrayExtension.cs
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
using Robust.Shared.Random;
|
||||||
|
|
||||||
|
namespace Content.Shared;
|
||||||
|
|
||||||
|
public static class SharedArrayExtension
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Randomizes the array mutating it in the process
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="array">array being randomized</param>
|
||||||
|
/// <param name="random">source of randomization</param>
|
||||||
|
/// <typeparam name="T">type of array element</typeparam>
|
||||||
|
public static void Shuffle<T>(this Span<T> array, IRobustRandom? random = null)
|
||||||
|
{
|
||||||
|
var n = array.Length;
|
||||||
|
if (n <= 1)
|
||||||
|
return;
|
||||||
|
IoCManager.Resolve(ref random);
|
||||||
|
|
||||||
|
while (n > 1)
|
||||||
|
{
|
||||||
|
n--;
|
||||||
|
var k = random.Next(n + 1);
|
||||||
|
(array[k], array[n]) =
|
||||||
|
(array[n], array[k]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -100,6 +100,14 @@ namespace Content.Shared.Slippery
|
|||||||
_adminLogger.Add(LogType.Slip, LogImpact.Low,
|
_adminLogger.Add(LogType.Slip, LogImpact.Low,
|
||||||
$"{ToPrettyString(other):mob} slipped on collision with {ToPrettyString(component.Owner):entity}");
|
$"{ToPrettyString(other):mob} slipped on collision with {ToPrettyString(component.Owner):entity}");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void CopyConstruct(EntityUid destUid, SlipperyComponent srcSlip)
|
||||||
|
{
|
||||||
|
var destEvaporation = EntityManager.EnsureComponent<SlipperyComponent>(destUid);
|
||||||
|
destEvaporation.SlipSound = srcSlip.SlipSound;
|
||||||
|
destEvaporation.ParalyzeTime = srcSlip.ParalyzeTime;
|
||||||
|
destEvaporation.LaunchForwardsMultiplier = srcSlip.LaunchForwardsMultiplier;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|||||||
@@ -176,6 +176,20 @@ public sealed class StepTriggerSystem : EntitySystem
|
|||||||
component.Active = active;
|
component.Active = active;
|
||||||
Dirty(component);
|
Dirty(component);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Copy constructor to copy initial fields from source to destination.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="destUid">Entity to which we copy <paramref name="srcStep"/> properties</param>
|
||||||
|
/// <param name="srcStep">Component that contains relevant properties</param>
|
||||||
|
public void CopyConstruct(EntityUid destUid, StepTriggerComponent srcStep)
|
||||||
|
{
|
||||||
|
var destTrigger = EntityManager.EnsureComponent<StepTriggerComponent>(destUid);
|
||||||
|
destTrigger.Active = srcStep.Active;
|
||||||
|
destTrigger.IntersectRatio = srcStep.IntersectRatio;
|
||||||
|
destTrigger.RequiredTriggerSpeed = srcStep.RequiredTriggerSpeed;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
[ByRefEvent]
|
[ByRefEvent]
|
||||||
|
|||||||
60
Content.Tests/Shared/DirectionRandomizerTest.cs
Normal file
60
Content.Tests/Shared/DirectionRandomizerTest.cs
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using Content.Shared;
|
||||||
|
using NUnit.Framework;
|
||||||
|
using Robust.Shared.Maths;
|
||||||
|
using Robust.UnitTesting;
|
||||||
|
|
||||||
|
namespace Content.Tests.Shared;
|
||||||
|
|
||||||
|
[TestFixture]
|
||||||
|
public sealed class DirectionRandomizerTest : RobustUnitTest
|
||||||
|
{
|
||||||
|
[Test]
|
||||||
|
[TestCase(new[]
|
||||||
|
{
|
||||||
|
Direction.East,
|
||||||
|
Direction.NorthEast,
|
||||||
|
Direction.West,
|
||||||
|
Direction.NorthWest,
|
||||||
|
Direction.South,
|
||||||
|
Direction.SouthWest,
|
||||||
|
Direction.North,
|
||||||
|
Direction.SouthEast,
|
||||||
|
})]
|
||||||
|
[TestCase(new[]
|
||||||
|
{
|
||||||
|
Direction.East,
|
||||||
|
Direction.West,
|
||||||
|
Direction.South,
|
||||||
|
Direction.North,
|
||||||
|
})]
|
||||||
|
[TestCase(new[]
|
||||||
|
{
|
||||||
|
Direction.East,
|
||||||
|
Direction.West,
|
||||||
|
})]
|
||||||
|
public void TestRandomization(Direction[] x)
|
||||||
|
{
|
||||||
|
var set = new HashSet<Direction>(x);
|
||||||
|
var randomizer = new Span<Direction>(x);
|
||||||
|
randomizer.Shuffle();
|
||||||
|
foreach (var direction in randomizer)
|
||||||
|
{
|
||||||
|
if (set.Contains(direction))
|
||||||
|
{
|
||||||
|
set.Remove(direction);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Asserts no double direction
|
||||||
|
Assert.Fail("Post randomization the enumerator had repeated direction");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Because of above foreach this asserts
|
||||||
|
// rand[1,2,3] - [1,2,3] == {}
|
||||||
|
// i.e. randomized set minus original set is empty
|
||||||
|
Assert.IsTrue(set.Count == 0, "Each element must appear once ");
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user