sandpypi/Sand.App/ChunkPrototypeSimulationBackend.cs

563 lines
22 KiB
C#

using Sand.ChunkPrototype;
using Sand.Core;
namespace Sand.App;
internal sealed class ChunkPrototypeSimulationBackend : ISimulationBackend
{
private readonly PrototypeSparseSandAdapter _adapter;
private readonly SimulationSettings _settings;
private readonly ParticleLibrary _library;
private readonly Dictionary<string, PrototypeParticle> _particleProfiles;
private readonly PrototypeParticle _wallParticle;
private readonly AppSimulationFrameStats _frameStats = new();
private int _frame;
private int _trimCounter;
public ChunkPrototypeSimulationBackend(int width, int height, int particleSize, IParticleLibrary library, SimulationSettings settings)
{
_adapter = new PrototypeSparseSandAdapter(width, height, settings.AmbientTemperature);
_settings = settings;
_library = library as ParticleLibrary ?? throw new ArgumentException("Expected ParticleLibrary implementation.", nameof(library));
_particleProfiles = BuildParticleProfiles(_library);
foreach (var particle in _particleProfiles.Values)
{
_adapter.RegisterParticleProfile(particle);
}
_wallParticle = _particleProfiles.GetValueOrDefault("wall");
ParticleSize = particleSize;
RefreshSettingsState();
}
public string BackendName => "chunk";
public SimulationSettings Settings => _settings;
public AppSimulationFrameStats FrameStats => _frameStats;
public int Frame => _frame;
public int ParticleCount => _adapter.ParticleCount;
public int ParticleSize { get; }
public ReadOnlySpan<byte> BuildRgbaFrame()
{
var frame = _adapter.BuildRgbaFrame(_settings.EnableWindVisuals, _settings.EnablePressureVisuals);
var stats = _adapter.LastStepStats;
_frameStats.VisualDirtyPageCount = stats.VisualDirtyPages;
_frameStats.FrameBuildBytesTouched = stats.FrameBuildBytesTouched;
_frameStats.RenderTimeMicroseconds = stats.RenderTimeMicroseconds;
return frame;
}
public void Step(float dt)
{
if (_settings.PauseSim)
{
return;
}
_adapter.World.ClearDirtyChunks();
var processed = _adapter.Step();
_frame++;
_trimCounter++;
if (_trimCounter >= 30)
{
_adapter.TrimResidency(marginChunks: 1);
_trimCounter = 0;
}
_frameStats.Frame = _frame;
_frameStats.ProcessedCells = Math.Max(processed, _adapter.ParticleCount);
_frameStats.ParticleCount = _adapter.ParticleCount;
_frameStats.LoadedChunkCount = _adapter.World.LoadedChunkCount;
_frameStats.ActiveChunkCount = _adapter.World.ActiveChunkCount;
_frameStats.DirtyChunkCount = _adapter.World.DirtyChunkCount;
_frameStats.SteppedChunkCount = _adapter.LastStepStats.SteppedChunks;
_frameStats.SleepingChunkCount = _adapter.LastStepStats.SleepingChunks;
_frameStats.FieldPageCount = _adapter.LastStepStats.FieldPages;
_frameStats.MoveAttemptCount = _adapter.LastStepStats.MoveAttempts;
_frameStats.VerticalMoveAttemptCount = _adapter.LastStepStats.VerticalMoveAttempts;
_frameStats.DiagonalMoveAttemptCount = _adapter.LastStepStats.DiagonalMoveAttempts;
_frameStats.LateralMoveAttemptCount = _adapter.LastStepStats.LateralMoveAttempts;
_frameStats.SuccessfulMoveCount = _adapter.LastStepStats.SuccessfulMoves;
_frameStats.SwapAttemptCount = _adapter.LastStepStats.SwapAttempts;
_frameStats.StalledMovableCount = _adapter.LastStepStats.StalledMovableCells;
_frameStats.MovementOnlyFastPathCount = _adapter.LastStepStats.MovementOnlyFastPathCount;
_frameStats.FullRuntimeStepCount = _adapter.LastStepStats.FullRuntimeStepCount;
_frameStats.FullRuntimeSolidCount = _adapter.LastStepStats.FullRuntimeSolidCount;
_frameStats.FullRuntimeLiquidCount = _adapter.LastStepStats.FullRuntimeLiquidCount;
_frameStats.FullRuntimeGasCount = _adapter.LastStepStats.FullRuntimeGasCount;
_frameStats.MovedParticleCount = _adapter.LastStepStats.MovedParticles;
_frameStats.SwappedParticleCount = _adapter.LastStepStats.SwappedParticles;
_frameStats.VisualDirtyPageCount = _adapter.LastStepStats.VisualDirtyPages;
_frameStats.ActivationTimeMicroseconds = _adapter.LastStepStats.ActivationTimeMicroseconds;
_frameStats.MovementTimeMicroseconds = _adapter.LastStepStats.MovementTimeMicroseconds;
_frameStats.RuntimeTimeMicroseconds = _adapter.LastStepStats.RuntimeTimeMicroseconds;
_frameStats.FieldDecayTimeMicroseconds = _adapter.LastStepStats.FieldDecayTimeMicroseconds;
_frameStats.RenderTimeMicroseconds = _adapter.LastStepStats.RenderTimeMicroseconds;
UpdateBoundsStats();
}
public void Clear()
{
foreach (var (x, y) in _adapter.Particles.ToArray())
{
_adapter.RemoveParticle(x, y);
}
_adapter.ClearFields();
_frame = 0;
_trimCounter = 0;
_frameStats.Frame = 0;
_frameStats.ProcessedCells = 0;
_frameStats.ParticleCount = 0;
_frameStats.MinActiveX = 0;
_frameStats.MinActiveY = 0;
_frameStats.MaxActiveX = 0;
_frameStats.MaxActiveY = 0;
_frameStats.LoadedChunkCount = 0;
_frameStats.ActiveChunkCount = 0;
_frameStats.DirtyChunkCount = 0;
_frameStats.SteppedChunkCount = 0;
_frameStats.SleepingChunkCount = 0;
_frameStats.FieldPageCount = 0;
_frameStats.MoveAttemptCount = 0;
_frameStats.VerticalMoveAttemptCount = 0;
_frameStats.DiagonalMoveAttemptCount = 0;
_frameStats.LateralMoveAttemptCount = 0;
_frameStats.SuccessfulMoveCount = 0;
_frameStats.SwapAttemptCount = 0;
_frameStats.StalledMovableCount = 0;
_frameStats.MovementOnlyFastPathCount = 0;
_frameStats.FullRuntimeStepCount = 0;
_frameStats.FullRuntimeSolidCount = 0;
_frameStats.FullRuntimeLiquidCount = 0;
_frameStats.FullRuntimeGasCount = 0;
_frameStats.MovedParticleCount = 0;
_frameStats.SwappedParticleCount = 0;
_frameStats.VisualDirtyPageCount = 0;
_frameStats.FrameBuildBytesTouched = 0;
_frameStats.ActivationTimeMicroseconds = 0;
_frameStats.MovementTimeMicroseconds = 0;
_frameStats.RuntimeTimeMicroseconds = 0;
_frameStats.FieldDecayTimeMicroseconds = 0;
_frameStats.RenderTimeMicroseconds = 0;
RefreshSettingsState();
}
public void RefreshSettingsState()
{
if (_settings.OuterWall)
{
for (var x = 0; x < _adapter.Width; x++)
{
_adapter.AddParticle(x, 0, _wallParticle);
_adapter.AddParticle(x, _adapter.Height - 1, _wallParticle);
}
for (var y = 0; y < _adapter.Height; y++)
{
_adapter.AddParticle(0, y, _wallParticle);
_adapter.AddParticle(_adapter.Width - 1, y, _wallParticle);
}
}
else
{
for (var x = 0; x < _adapter.Width; x++)
{
RemoveBoundaryWallIfPresent(x, 0);
RemoveBoundaryWallIfPresent(x, _adapter.Height - 1);
}
for (var y = 0; y < _adapter.Height; y++)
{
RemoveBoundaryWallIfPresent(0, y);
RemoveBoundaryWallIfPresent(_adapter.Width - 1, y);
}
}
}
public void ClearParticleCircle(int centerX, int centerY, int brushRadius)
{
PaintCircle(centerX, centerY, brushRadius, particleId: null);
}
public void ClearParticlePourAtPixel(int centerX, int centerY, int brushRadius, int maxParticles, int seed)
{
PaintCirclePour(centerX, centerY, brushRadius, maxParticles, seed, particleId: null);
}
public void CreateParticleCircle(int centerX, int centerY, int brushRadius, string particleId)
{
PaintCircle(centerX, centerY, brushRadius, particleId);
}
public void CreateParticlePourAtPixel(int centerX, int centerY, int brushRadius, string particleId, int maxParticles, int seed)
{
PaintCirclePour(centerX, centerY, brushRadius, maxParticles, seed, particleId);
}
private void PaintCirclePour(int centerX, int centerY, int brushRadius, int maxParticles, int seed, string? particleId)
{
var gridCenterX = centerX / ParticleSize;
var gridCenterY = centerY / ParticleSize;
var diameter = (brushRadius * 2) + 1;
var touched = 0;
for (var i = 0; i < Math.Max(maxParticles * 6, diameter * diameter) && touched < maxParticles; i++)
{
var dx = ((seed + (i * 17)) % diameter) - brushRadius;
var dy = ((seed + (i * 31)) % diameter) - brushRadius;
if ((dx * dx) + (dy * dy) > brushRadius * brushRadius)
{
continue;
}
if (particleId is null)
{
var x = gridCenterX + dx;
var y = gridCenterY + dy;
if (TryNormalizeCoordinate(ref x, ref y) && (!_settings.OuterWall || !IsBoundary(x, y)) && _adapter.RemoveParticle(x, y))
{
touched++;
}
}
else if (TryPaintAtCell(gridCenterX + dx, gridCenterY + dy, particleId))
{
touched++;
}
}
}
public void ApplyWindBrushAtPixel(int centerX, int centerY, int brushRadius, float forceX, float forceY)
{
_adapter.ApplyWindBrush(centerX / ParticleSize, centerY / ParticleSize, brushRadius, forceX, forceY);
}
public void ApplyAirBrushAtPixel(int centerX, int centerY, int brushRadius, float forceX, float forceY)
{
_adapter.ApplyAirBrush(centerX / ParticleSize, centerY / ParticleSize, brushRadius, forceX, forceY);
}
public void ApplyGravityBrushAtPixel(int centerX, int centerY, int brushRadius, float strength)
{
_adapter.ApplyGravityBrush(centerX / ParticleSize, centerY / ParticleSize, brushRadius, strength);
}
public void ApplyRepulsorBrushAtPixel(int centerX, int centerY, int brushRadius, float strength)
{
_adapter.ApplyRepulsorBrush(centerX / ParticleSize, centerY / ParticleSize, brushRadius, strength);
}
private void PaintCircle(int centerX, int centerY, int brushRadius, string? particleId)
{
var gridCenterX = centerX / ParticleSize;
var gridCenterY = centerY / ParticleSize;
for (var dx = -brushRadius; dx <= brushRadius; dx++)
{
for (var dy = -brushRadius; dy <= brushRadius; dy++)
{
if ((dx * dx) + (dy * dy) > brushRadius * brushRadius)
{
continue;
}
if (particleId is null)
{
var x = gridCenterX + dx;
var y = gridCenterY + dy;
if (TryNormalizeCoordinate(ref x, ref y) && (!_settings.OuterWall || !IsBoundary(x, y)))
{
_adapter.RemoveParticle(x, y);
}
continue;
}
TryPaintAtCell(gridCenterX + dx, gridCenterY + dy, particleId);
}
}
}
private bool TryPaintAtCell(int x, int y, string particleId)
{
if (!TryNormalizeCoordinate(ref x, ref y))
{
return false;
}
if (!_particleProfiles.TryGetValue(particleId, out var particle) || particle.IsEmpty)
{
return false;
}
if (_settings.OuterWall && particle.MotionType != PrototypeParticleType.Wall && IsBoundary(x, y))
{
return false;
}
return _adapter.AddParticle(x, y, particle);
}
private void RemoveBoundaryWallIfPresent(int x, int y)
{
if (_adapter.GetParticleTypeAt(x, y) == PrototypeParticleType.Wall)
{
_adapter.RemoveParticle(x, y);
}
}
private void UpdateBoundsStats()
{
if (_adapter.ParticleCount == 0)
{
_frameStats.MinActiveX = 0;
_frameStats.MinActiveY = 0;
_frameStats.MaxActiveX = 0;
_frameStats.MaxActiveY = 0;
return;
}
var minX = int.MaxValue;
var minY = int.MaxValue;
var maxX = int.MinValue;
var maxY = int.MinValue;
foreach (var ((x, y), _) in _adapter.ParticleEntries)
{
minX = Math.Min(minX, x);
minY = Math.Min(minY, y);
maxX = Math.Max(maxX, x);
maxY = Math.Max(maxY, y);
}
_frameStats.MinActiveX = minX;
_frameStats.MinActiveY = minY;
_frameStats.MaxActiveX = maxX;
_frameStats.MaxActiveY = maxY;
}
private bool TryNormalizeCoordinate(ref int x, ref int y)
{
if (_settings.WrapParticles)
{
x = ((x % _adapter.Width) + _adapter.Width) % _adapter.Width;
y = ((y % _adapter.Height) + _adapter.Height) % _adapter.Height;
return true;
}
return x >= 0 && x < _adapter.Width && y >= 0 && y < _adapter.Height;
}
private bool IsBoundary(int x, int y) => x == 0 || y == 0 || x == _adapter.Width - 1 || y == _adapter.Height - 1;
private static Dictionary<string, PrototypeParticle> BuildParticleProfiles(ParticleLibrary library)
{
var idLookup = new Dictionary<string, ushort>(StringComparer.OrdinalIgnoreCase);
for (ushort typeId = 1; typeId <= library.Definitions.Count; typeId++)
{
var definition = library.GetDefinition(typeId);
idLookup[definition.Id] = typeId;
}
var result = new Dictionary<string, PrototypeParticle>(StringComparer.OrdinalIgnoreCase);
for (ushort typeId = 1; typeId <= library.Definitions.Count; typeId++)
{
var definition = library.GetDefinition(typeId);
result[definition.Id] = CreateParticleProfile(typeId, definition, idLookup);
}
return result;
}
private static PrototypeParticle CreateParticleProfile(ushort typeId, ParticleDef definition, IReadOnlyDictionary<string, ushort> idLookup)
{
var motionType = ResolveMotionType(definition);
if (motionType == PrototypeParticleType.Empty)
{
return default;
}
var runtime = ParticleRuntimeProfileBuilder.Build(definition);
var velocity = definition.Velocity > 0f
? definition.Velocity
: definition.Kind switch
{
ParticleKind.Solid => 0.45f,
ParticleKind.Liquid => 0.35f,
ParticleKind.Gas => 0.25f,
_ => 0.35f,
};
var friction = definition.Friction;
var viscosity = definition.Viscosity;
switch (motionType)
{
case PrototypeParticleType.Sand:
velocity = MathF.Max(0.5f, velocity);
friction = Math.Clamp(friction * 0.75f, 0f, 1f);
viscosity = Math.Clamp(viscosity * 0.75f, 0f, 1.25f);
break;
case PrototypeParticleType.Water:
velocity = MathF.Max(0.4f, velocity);
friction = Math.Clamp(friction * 0.6f, 0f, 1f);
viscosity = Math.Clamp(viscosity * 0.65f, 0f, 1.25f);
break;
case PrototypeParticleType.Steam:
velocity = MathF.Max(0.45f, velocity + (runtime.Balance.UpwardBias * 0.35f));
friction = Math.Clamp(friction * 0.4f, 0f, 0.75f);
viscosity = Math.Clamp(viscosity * 0.5f, 0f, 0.85f);
break;
}
var flags = PrototypeParticleFlags.None;
if (definition.Id is "water" or "wet_sand" or "mud")
{
flags |= PrototypeParticleFlags.WaterLike;
}
if (definition.Id is "fire" or "plasma" or "ember" or "energy")
{
flags |= PrototypeParticleFlags.FireLike | PrototypeParticleFlags.HotSource;
}
if (definition.Id == "lava" || definition.Id == "burning_wood" || definition.Id.StartsWith("molten_", StringComparison.Ordinal))
{
flags |= PrototypeParticleFlags.HotSource;
}
if (definition.Id == "acid")
{
flags |= PrototypeParticleFlags.Acidic;
}
var hydrateTargetTypeId = definition.Id switch
{
"sand" when idLookup.TryGetValue("wet_sand", out var wetSand) => wetSand,
"dirt" when idLookup.TryGetValue("mud", out var mud) => mud,
_ => (ushort)0,
};
var pressureResponse = MathF.Max(0.15f, runtime.Balance.PressureSensitivity);
return new PrototypeParticle(
TypeId: typeId,
Id: definition.Id,
MotionType: motionType,
Kind: definition.Kind,
BehaviorKind: runtime.Balance.BehaviorKind,
R: definition.Color.R,
G: definition.Color.G,
B: definition.Color.B,
Mass: MathF.Max(0.05f, definition.Mass),
Velocity: Math.Clamp(velocity, 0.05f, 1.4f),
Friction: Math.Clamp(friction, 0f, 1.5f),
Viscosity: Math.Clamp(viscosity, 0f, 2f),
IsStatic: definition.IsStatic || motionType == PrototypeParticleType.Wall,
Flags: flags,
IsMolten: definition.Id == "lava" || definition.Id.StartsWith("molten_", StringComparison.Ordinal),
HydrateTargetTypeId: hydrateTargetTypeId,
MeltTypeId: ResolveOptionalTarget(idLookup, definition.Melt),
EvaporateTypeId: ResolveOptionalTarget(idLookup, definition.Evaporate),
SolidifyTypeId: ResolveOptionalTarget(idLookup, definition.Solidify),
FreezeTypeId: ResolveOptionalTarget(idLookup, definition.Freeze),
BrokenTypeId: ResolveOptionalTarget(idLookup, definition.Broken),
PressureThreshold: definition.PressureThreshold,
PressureResistance: definition.PressureResistance,
PressureTolerance: definition.PressureTolerance,
PressureThresholdDuration: checked((short)Math.Clamp(definition.PressureThresholdDuration, 0, short.MaxValue)),
PressureResponse: pressureResponse,
ForceResponseMultiplier: MathF.Max(0.1f, runtime.Balance.ForceResponseMultiplier),
LateralFlowMultiplier: MathF.Max(0.05f, runtime.Balance.LateralFlowMultiplier),
DiagonalFlowMultiplier: MathF.Max(0.05f, runtime.Balance.DiagonalFlowMultiplier),
UpwardBias: runtime.Balance.UpwardBias,
SideDriftBias: runtime.Balance.SideDriftBias,
InitialTemperature: definition.Temperature,
MeltTemperature: definition.MeltTemperature ?? float.PositiveInfinity,
EvaporateTemperature: definition.EvaporateTemperature ?? float.PositiveInfinity,
SolidifyTemperature: definition.SolidifyTemperature ?? float.NegativeInfinity,
FreezeTemperature: definition.FreezeTemperature ?? float.NegativeInfinity,
BurnDuration: definition.BurnDuration,
BurnTemperature: definition.BurnTemperature,
BurnRate: MathF.Max(0.05f, definition.BurnRate),
BurningInit: definition.Burning,
DefaultLifetime: ResolveDefaultLifetime(definition, runtime.Balance),
HeatEmission: definition.HeatEmission * MathF.Max(0.01f, runtime.Balance.HeatEmissionMultiplier),
SmokeSpawnChance: runtime.Balance.SmokeSpawnChance,
EmberSpawnChance: runtime.Balance.EmberSpawnChance,
Hardness: definition.Hardness,
Durability: definition.Durability,
Flamability: definition.Flamability,
Conductivity: definition.Conductivity,
Conductive: definition.Conductive,
AmbientCoolingMultiplier: runtime.Balance.AmbientCoolingMultiplier,
NeighborHeatTransferMultiplier: runtime.Balance.NeighborHeatTransferMultiplier,
PhaseTransitionHysteresis: runtime.Balance.PhaseTransitionHysteresis,
ProduceTypeId: ResolveOptionalTarget(idLookup, definition.Produces),
ProducesOnDeathTypeId: ResolveOptionalTarget(idLookup, definition.ProducesOnDeath));
}
private static ushort ResolveOptionalTarget(IReadOnlyDictionary<string, ushort> idLookup, string? particleId)
{
if (string.IsNullOrWhiteSpace(particleId))
{
return 0;
}
return idLookup.TryGetValue(particleId, out var typeId) ? typeId : (ushort)0;
}
private static float ResolveDefaultLifetime(ParticleDef definition, ParticleBalanceProfile balance)
{
var lifetime = (definition.Lifetime ?? 0f) * MathF.Max(0.01f, balance.LifetimeMultiplier);
if (lifetime <= 0f && balance.MaxLifetimeTicks > 0f)
{
lifetime = (balance.MinLifetimeTicks + balance.MaxLifetimeTicks) * 0.5f;
}
if (balance.MinLifetimeTicks > 0f)
{
lifetime = MathF.Max(lifetime, balance.MinLifetimeTicks);
}
if (balance.MaxLifetimeTicks > 0f)
{
lifetime = MathF.Min(lifetime, balance.MaxLifetimeTicks);
}
return lifetime;
}
private static PrototypeParticleType ResolveMotionType(ParticleDef definition)
{
if (definition.Id is "air" or "wind" or "gravity_well" or "repulsor")
{
return PrototypeParticleType.Empty;
}
if (definition.Id == "wall" || definition.IsStatic)
{
return PrototypeParticleType.Wall;
}
if (definition.Id is "fire" or "plasma" or "smoke" or "steam" or "spark" or "energy")
{
return PrototypeParticleType.Steam;
}
if (definition.Id is "burning_wood")
{
return PrototypeParticleType.Wall;
}
if (definition.Id is "ember" or "snow")
{
return PrototypeParticleType.Sand;
}
return definition.Kind switch
{
ParticleKind.Gas => PrototypeParticleType.Steam,
ParticleKind.Liquid => PrototypeParticleType.Water,
ParticleKind.Solid => PrototypeParticleType.Sand,
_ => PrototypeParticleType.Empty,
};
}
}