sandpypi/Sand.Core/SandSimulation.Infrastructure.cs

800 lines
23 KiB
C#

namespace Sand.Core;
public sealed partial class SandSimulation
{
private uint GetVisualSettingsStamp()
{
unchecked
{
var stamp = 17u;
stamp = (stamp * 31u) + (_settings.EnablePressureVisuals ? 1u : 0u);
stamp = (stamp * 31u) + (_settings.EnableWindVisuals ? 1u : 0u);
stamp = (stamp * 31u) + (_settings.EnableTempVisuals ? 1u : 0u);
stamp = (stamp * 31u) + (_settings.EnableGasEffect ? 1u : 0u);
stamp = (stamp * 31u) + (_settings.EnableGlow ? 1u : 0u);
stamp = (stamp * 31u) + (uint)BitConverter.SingleToInt32Bits(_settings.AmbientTemperature);
return stamp;
}
}
private void EnsureVisualSettingsState()
{
var stamp = GetVisualSettingsStamp();
if (_visualSettingsStamp == stamp)
{
return;
}
_visualSettingsStamp = stamp;
MarkFullVisualDirty();
}
private void MarkFullVisualDirty()
{
_fullVisualDirty = true;
_hasDirtyVisualBounds = true;
_minDirtyVisualX = 0;
_minDirtyVisualY = 0;
_maxDirtyVisualX = Width - 1;
_maxDirtyVisualY = Height - 1;
}
private bool AreFieldVisualsEnabled() => _settings.EnablePressureVisuals || _settings.EnableWindVisuals;
private void MarkVisualDirty(int x, int y)
{
if (!InBounds(x, y))
{
return;
}
if (_deferVisualDirtyTracking)
{
return;
}
if (_fullVisualDirty)
{
return;
}
if (!_hasDirtyVisualBounds)
{
_minDirtyVisualX = _maxDirtyVisualX = x;
_minDirtyVisualY = _maxDirtyVisualY = y;
_hasDirtyVisualBounds = true;
return;
}
_minDirtyVisualX = Math.Min(_minDirtyVisualX, x);
_minDirtyVisualY = Math.Min(_minDirtyVisualY, y);
_maxDirtyVisualX = Math.Max(_maxDirtyVisualX, x);
_maxDirtyVisualY = Math.Max(_maxDirtyVisualY, y);
}
private void MarkVisualDirtyRect(int startX, int startY, int endX, int endY)
{
if (_deferVisualDirtyTracking)
{
return;
}
if (_fullVisualDirty)
{
return;
}
if (startX > endX || startY > endY)
{
return;
}
startX = Math.Max(0, startX);
startY = Math.Max(0, startY);
endX = Math.Min(Width - 1, endX);
endY = Math.Min(Height - 1, endY);
if (startX > endX || startY > endY)
{
return;
}
if (!_hasDirtyVisualBounds)
{
_minDirtyVisualX = startX;
_minDirtyVisualY = startY;
_maxDirtyVisualX = endX;
_maxDirtyVisualY = endY;
_hasDirtyVisualBounds = true;
return;
}
_minDirtyVisualX = Math.Min(_minDirtyVisualX, startX);
_minDirtyVisualY = Math.Min(_minDirtyVisualY, startY);
_maxDirtyVisualX = Math.Max(_maxDirtyVisualX, endX);
_maxDirtyVisualY = Math.Max(_maxDirtyVisualY, endY);
}
private bool TryGetDirtyVisualBounds(out int startX, out int startY, out int endX, out int endY)
{
if (_fullVisualDirty)
{
startX = 0;
startY = 0;
endX = Width - 1;
endY = Height - 1;
return true;
}
if (_hasDirtyVisualBounds)
{
startX = _minDirtyVisualX;
startY = _minDirtyVisualY;
endX = _maxDirtyVisualX;
endY = _maxDirtyVisualY;
return true;
}
startX = startY = endX = endY = 0;
return false;
}
private void ClearDirtyVisualBounds()
{
_fullVisualDirty = false;
_hasDirtyVisualBounds = false;
}
private void UpdateCachedVisualBuffers()
{
EnsureVisualSettingsState();
if (!TryGetDirtyVisualBounds(out var startX, out var startY, out var endX, out var endY))
{
return;
}
for (var y = startY; y <= endY; y++)
{
for (var x = startX; x <= endX; x++)
{
var color = ResolveVisualColor(x, y);
var pixelIndex = (y * Width) + x;
var rgbIndex = pixelIndex * 3;
var rgbaIndex = pixelIndex * 4;
_rgbBuffer[rgbIndex] = color.R;
_rgbBuffer[rgbIndex + 1] = color.G;
_rgbBuffer[rgbIndex + 2] = color.B;
_rgbaBuffer[rgbaIndex] = color.R;
_rgbaBuffer[rgbaIndex + 1] = color.G;
_rgbaBuffer[rgbaIndex + 2] = color.B;
_rgbaBuffer[rgbaIndex + 3] = 255;
}
}
ClearDirtyVisualBounds();
}
private bool HasDynamicFieldAt(int x, int y)
{
return
MathF.Abs(_windFieldX[x, y]) >= 0.02f ||
MathF.Abs(_windFieldY[x, y]) >= 0.02f ||
MathF.Abs(_forceFieldX[x, y]) >= 0.02f ||
MathF.Abs(_forceFieldY[x, y]) >= 0.02f ||
MathF.Abs(_airPressure[x, y]) >= 0.02f;
}
private void ExpandActiveFieldBoundsToInclude(int x, int y)
{
if (!_hasActiveFieldBounds)
{
_minActiveFieldX = _maxActiveFieldX = x;
_minActiveFieldY = _maxActiveFieldY = y;
_hasActiveFieldBounds = true;
return;
}
_minActiveFieldX = Math.Min(_minActiveFieldX, x);
_minActiveFieldY = Math.Min(_minActiveFieldY, y);
_maxActiveFieldX = Math.Max(_maxActiveFieldX, x);
_maxActiveFieldY = Math.Max(_maxActiveFieldY, y);
}
private void TrackDynamicFieldMutation(int x, int y, bool hadDynamicBefore)
{
if (!InBounds(x, y))
{
return;
}
var hasDynamicAfter = HasDynamicFieldAt(x, y);
if (hasDynamicAfter)
{
ExpandActiveFieldBoundsToInclude(x, y);
}
else if (hadDynamicBefore)
{
_fieldBoundsDirty = true;
}
if (AreFieldVisualsEnabled())
{
MarkVisualDirty(x, y);
}
}
private void ClearDynamicFieldsAt(int x, int y)
{
var hadDynamicBefore = HasDynamicFieldAt(x, y);
_windFieldX[x, y] = 0f;
_windFieldY[x, y] = 0f;
_forceFieldX[x, y] = 0f;
_forceFieldY[x, y] = 0f;
_airPressure[x, y] = 0f;
if (hadDynamicBefore)
{
_fieldBoundsDirty = true;
if (AreFieldVisualsEnabled())
{
MarkVisualDirty(x, y);
}
}
}
private bool TryGetOccupiedBoundsOnly(out int startX, out int startY, out int endX, out int endY)
{
if (_hasOccupiedBounds)
{
startX = _minOccupiedX;
startY = _minOccupiedY;
endX = _maxOccupiedX;
endY = _maxOccupiedY;
return true;
}
startX = startY = endX = endY = 0;
return false;
}
private bool TryGetSimulationBounds(out int startX, out int startY, out int endX, out int endY)
{
EnsureOccupiedBounds();
if (_fieldBoundsDirty && (_hasActiveFieldBounds || _hasOccupiedBounds))
{
var recomputeStartX = _hasActiveFieldBounds && _hasOccupiedBounds ? Math.Min(_minActiveFieldX, _minOccupiedX) : _hasActiveFieldBounds ? _minActiveFieldX : _minOccupiedX;
var recomputeStartY = _hasActiveFieldBounds && _hasOccupiedBounds ? Math.Min(_minActiveFieldY, _minOccupiedY) : _hasActiveFieldBounds ? _minActiveFieldY : _minOccupiedY;
var recomputeEndX = _hasActiveFieldBounds && _hasOccupiedBounds ? Math.Max(_maxActiveFieldX, _maxOccupiedX) : _hasActiveFieldBounds ? _maxActiveFieldX : _maxOccupiedX;
var recomputeEndY = _hasActiveFieldBounds && _hasOccupiedBounds ? Math.Max(_maxActiveFieldY, _maxOccupiedY) : _hasActiveFieldBounds ? _maxActiveFieldY : _maxOccupiedY;
RebuildActiveFieldBounds(recomputeStartX, recomputeStartY, recomputeEndX, recomputeEndY);
}
var hasBounds = false;
startX = startY = endX = endY = 0;
if (_hasOccupiedBounds)
{
startX = _minOccupiedX;
startY = _minOccupiedY;
endX = _maxOccupiedX;
endY = _maxOccupiedY;
hasBounds = true;
}
if (_hasActiveFieldBounds)
{
if (!hasBounds)
{
startX = _minActiveFieldX;
startY = _minActiveFieldY;
endX = _maxActiveFieldX;
endY = _maxActiveFieldY;
hasBounds = true;
}
else
{
startX = Math.Min(startX, _minActiveFieldX);
startY = Math.Min(startY, _minActiveFieldY);
endX = Math.Max(endX, _maxActiveFieldX);
endY = Math.Max(endY, _maxActiveFieldY);
}
}
return hasBounds;
}
private void MarkBoundsUnionDirty(bool hadFirstBounds, int firstStartX, int firstStartY, int firstEndX, int firstEndY)
{
int secondStartX;
int secondStartY;
int secondEndX;
int secondEndY;
var hasSecondBounds = AreFieldVisualsEnabled()
? TryGetSimulationBounds(out secondStartX, out secondStartY, out secondEndX, out secondEndY)
: TryGetOccupiedBoundsOnly(out secondStartX, out secondStartY, out secondEndX, out secondEndY);
if (hasSecondBounds)
{
if (!hadFirstBounds)
{
MarkVisualDirtyRect(secondStartX, secondStartY, secondEndX, secondEndY);
return;
}
MarkVisualDirtyRect(
Math.Min(firstStartX, secondStartX),
Math.Min(firstStartY, secondStartY),
Math.Max(firstEndX, secondEndX),
Math.Max(firstEndY, secondEndY));
return;
}
if (hadFirstBounds)
{
MarkVisualDirtyRect(firstStartX, firstStartY, firstEndX, firstEndY);
}
}
private ushort ResolveOptionalTypeId(string? particleId)
{
if (particleId is null)
{
return 0;
}
return _library.TryGetTypeId(particleId, out var typeId) ? typeId : (ushort)0;
}
private 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 ToolProfile BuildToolProfile(string id, float defaultStrength, int fallbackRadiusCells)
{
if (_library.TryGetTypeId(id, out var typeId))
{
var definition = _library.GetDefinition(typeId);
var radiusCells = definition.Radius > 0f ? Math.Max(fallbackRadiusCells, (int)MathF.Ceiling(definition.Radius / (ParticleSize * 4f))) : fallbackRadiusCells;
var strength = definition.WindStrength > 0f ? definition.WindStrength : definition.GravityStrength > 0f ? definition.GravityStrength : definition.RepulsionStrength > 0f ? definition.RepulsionStrength : defaultStrength;
return new ToolProfile(
definition.Id,
radiusCells,
strength,
definition.ForceFalloff <= 0f ? 1f : definition.ForceFalloff,
definition.Turbulence,
definition.Affects.Length == 0 ? ["all"] : definition.Affects);
}
return new ToolProfile(id, fallbackRadiusCells, defaultStrength, 1f, 0f, ["all"]);
}
private void ApplyDirectionalBrush(int centerX, int centerY, int brushRadius, float forceX, float forceY, ToolProfile profile, float[,] fieldX, float[,] fieldY, bool addPressure = false)
{
var gridCenterX = centerX / ParticleSize;
var gridCenterY = centerY / ParticleSize;
var effectiveRadius = Math.Max(brushRadius, profile.RadiusCells);
var magnitude = MathF.Sqrt((forceX * forceX) + (forceY * forceY));
if (magnitude <= 0.001f)
{
forceX = 1f;
forceY = 0f;
magnitude = 1f;
}
for (var dx = -effectiveRadius; dx <= effectiveRadius; dx++)
{
for (var dy = -effectiveRadius; dy <= effectiveRadius; dy++)
{
if ((dx * dx) + (dy * dy) > effectiveRadius * effectiveRadius)
{
continue;
}
var x = gridCenterX + dx;
var y = gridCenterY + dy;
if (!TryNormalizeCoordinate(ref x, ref y))
{
continue;
}
if (_settings.OuterWall && IsBoundary(x, y))
{
continue;
}
if (!ToolAffectsCell(profile.Affects, x, y))
{
continue;
}
var distance = MathF.Sqrt((dx * dx) + (dy * dy));
var falloff = ComputeFalloff(distance, effectiveRadius, profile.Falloff);
var turbulence = SampleTurbulence(x, y, profile.Turbulence);
var hadDynamicBefore = HasDynamicFieldAt(x, y);
fieldX[x, y] = Math.Clamp(fieldX[x, y] + (((forceX / magnitude) + turbulence.X) * falloff * profile.Strength), -8f, 8f);
fieldY[x, y] = Math.Clamp(fieldY[x, y] + (((forceY / magnitude) + turbulence.Y) * falloff * profile.Strength), -8f, 8f);
if (addPressure)
{
_airPressure[x, y] = Math.Clamp(_airPressure[x, y] + falloff * profile.Strength, -10f, 10f);
}
TrackDynamicFieldMutation(x, y, hadDynamicBefore);
}
}
}
private bool ToolAffectsCell(string[] affects, int x, int y)
{
if (affects.Length == 0 || affects.Contains("all", StringComparer.OrdinalIgnoreCase))
{
return true;
}
var typeId = TypeId[x, y];
if (typeId == 0)
{
return true;
}
var definition = _library.GetDefinition(typeId);
foreach (var affect in affects)
{
if (string.Equals(affect, definition.Id, StringComparison.OrdinalIgnoreCase))
{
return true;
}
if (affect == "solid" && definition.Kind == ParticleKind.Solid)
{
return true;
}
if (affect == "liquid" && definition.Kind == ParticleKind.Liquid)
{
return true;
}
if (affect == "gas" && definition.Kind == ParticleKind.Gas)
{
return true;
}
}
return false;
}
private static float ComputeFalloff(float distance, int radius, float exponent)
{
if (radius <= 0)
{
return 0f;
}
var normalized = 1f - (distance / radius);
if (normalized <= 0f)
{
return 0f;
}
return MathF.Pow(normalized, MathF.Max(0.1f, exponent));
}
private (float X, float Y) SampleTurbulence(int x, int y, float turbulence)
{
if (turbulence <= 0.001f)
{
return (0f, 0f);
}
var hash = Hash(x, y, Frame);
var tx = (((hash & 0xFFu) / 255f) - 0.5f) * turbulence;
var ty = ((((hash >> 8) & 0xFFu) / 255f) - 0.5f) * turbulence;
return (tx, ty);
}
private void TrySpawnOnDeath(int x, int y, ushort typeId)
{
var spawnType = _producesOnDeathTarget[typeId];
if (spawnType == 0)
{
return;
}
SpawnNeighborParticle(x, y, spawnType, Hash(x, y, Frame + 17));
}
private bool SpawnNeighborParticle(int x, int y, ushort spawnType, uint seed)
{
var offsets = new (int X, int Y)[]
{
(0, -1),
(-1, 0),
(1, 0),
(0, 1),
(-1, -1),
(1, -1),
(-1, 1),
(1, 1),
};
var start = (int)(seed % (uint)offsets.Length);
for (var i = 0; i < offsets.Length; i++)
{
var offset = offsets[(start + i) % offsets.Length];
var nx = x + offset.X;
var ny = y + offset.Y;
if (!TryNormalizeCoordinate(ref nx, ref ny))
{
continue;
}
if ((_settings.OuterWall && IsBoundary(nx, ny)) || TypeId[nx, ny] != 0)
{
continue;
}
SetCell(nx, ny, spawnType);
return true;
}
return false;
}
private bool BreakCellFromPressure(int x, int y, ushort typeId, ref uint seed)
{
var brokenType = _brokenTarget[typeId];
if (brokenType != 0 && brokenType != typeId)
{
ReplaceCell(x, y, brokenType);
Temperature[x, y] = MathF.Max(Temperature[x, y], _initialTemperature[brokenType]);
return true;
}
var isHotBreak = Temperature[x, y] >= Math.Max(200f, _burnTemperature[typeId]);
if (_idSmoke != 0 && isHotBreak && NextChance(ref seed, 0.08f))
{
SpawnNeighborParticle(x, y, _idSmoke, seed);
}
ResetCell(x, y);
return true;
}
private void EnsureOccupiedBounds()
{
if (_boundsDirty && _staleOccupiedBoundsFrames >= 8)
{
RecomputeOccupiedBounds();
}
}
private void MarkBoundsDirtyIfRemovingOccupiedCell(int x, int y)
{
if (ParticleCount == 0)
{
_hasOccupiedBounds = false;
_boundsDirty = false;
return;
}
if (!_hasOccupiedBounds)
{
_boundsDirty = true;
return;
}
if (x == _minOccupiedX || x == _maxOccupiedX || y == _minOccupiedY || y == _maxOccupiedY)
{
_boundsDirty = true;
}
}
private void ExpandOccupiedBoundsToInclude(int x, int y)
{
if (!_hasOccupiedBounds)
{
_minOccupiedX = _maxOccupiedX = x;
_minOccupiedY = _maxOccupiedY = y;
_hasOccupiedBounds = true;
return;
}
_minOccupiedX = Math.Min(_minOccupiedX, x);
_minOccupiedY = Math.Min(_minOccupiedY, y);
_maxOccupiedX = Math.Max(_maxOccupiedX, x);
_maxOccupiedY = Math.Max(_maxOccupiedY, y);
}
private void RecomputeOccupiedBounds()
{
_hasOccupiedBounds = false;
for (var y = 0; y < Height; y++)
{
for (var x = 0; x < Width; x++)
{
if (TypeId[x, y] != 0)
{
ExpandOccupiedBoundsToInclude(x, y);
}
}
}
_boundsDirty = false;
_staleOccupiedBoundsFrames = 0;
}
private void ClearBoundaryWalls()
{
for (var x = 0; x < Width; x++)
{
TryClearBoundaryWall(x, 0);
TryClearBoundaryWall(x, Height - 1);
}
for (var y = 0; y < Height; y++)
{
TryClearBoundaryWall(0, y);
TryClearBoundaryWall(Width - 1, y);
}
}
private void TryClearBoundaryWall(int x, int y)
{
if (TypeId[x, y] == _idWall)
{
ParticleCount = Math.Max(0, ParticleCount - 1);
MarkBoundsDirtyIfRemovingOccupiedCell(x, y);
ResetCellWithoutCountChange(x, y);
MarkVisualDirty(x, y);
}
}
private void ApplyBoundaryWallsFast()
{
for (var x = 0; x < Width; x++)
{
SetBoundaryWall(x, 0);
SetBoundaryWall(x, Height - 1);
}
for (var y = 0; y < Height; y++)
{
SetBoundaryWall(0, y);
SetBoundaryWall(Width - 1, y);
}
}
private void SetBoundaryWall(int x, int y)
{
if (TypeId[x, y] == _idWall)
{
return;
}
if (TypeId[x, y] == 0)
{
ParticleCount++;
}
TypeId[x, y] = _idWall;
Temperature[x, y] = _initialTemperature[_idWall];
BurnTime[x, y] = _burnDuration[_idWall];
Burning[x, y] = _burningInit[_idWall];
SparkTime[x, y] = 0;
Lifetime[x, y] = _defaultLifetime[_idWall];
_pressureDuration[x, y] = 0f;
_cellAge[x, y] = 0f;
_integrity[x, y] = _durability[_idWall];
ExpandOccupiedBoundsToInclude(x, y);
MarkVisualDirty(x, y);
MarkProcessed(x, y);
}
private bool InBounds(int x, int y) => x >= 0 && x < Width && y >= 0 && y < Height;
private bool IsBoundary(int x, int y) => x == 0 || y == 0 || x == Width - 1 || y == Height - 1;
private bool TryNormalizeCoordinate(ref int x, ref int y)
{
if (_settings.WrapParticles)
{
x = ((x % Width) + Width) % Width;
y = ((y % Height) + Height) % Height;
return true;
}
return InBounds(x, y);
}
private void MarkProcessed(int x, int y)
{
if (_activeStepToken != 0 && InBounds(x, y))
{
_processedFrame[x, y] = _activeStepToken;
}
}
private void ForEachNeighbor8(int x, int y, Action<int, int> visitor)
{
for (var dx = -1; dx <= 1; dx++)
{
for (var dy = -1; dy <= 1; dy++)
{
if (dx == 0 && dy == 0)
{
continue;
}
var nx = x + dx;
var ny = y + dy;
if (TryNormalizeCoordinate(ref nx, ref ny))
{
visitor(nx, ny);
}
}
}
}
private static uint Hash(int x, int y, int frame)
{
unchecked
{
var seed = (uint)(x * 73856093) ^ (uint)(y * 19349663) ^ (uint)(frame * 83492791);
seed ^= seed >> 13;
return seed * 1274126177u;
}
}
private static bool Chance(uint hash, float threshold) => (hash & 0xFFFF) / 65535f < threshold;
private static bool NextChance(ref uint seed, float threshold)
{
unchecked
{
seed = (seed * 1664525u) + 1013904223u;
}
return Chance(seed, threshold);
}
private static float Clamp(float value, float min, float max)
{
if (min > max)
{
(min, max) = (max, min);
}
return MathF.Min(max, MathF.Max(min, value));
}
private void RebuildActiveFieldBounds(int startX, int startY, int endX, int endY)
{
_hasActiveFieldBounds = false;
for (var y = startY; y <= endY; y++)
{
for (var x = startX; x <= endX; x++)
{
if (HasDynamicFieldAt(x, y))
{
ExpandActiveFieldBoundsToInclude(x, y);
}
}
}
_fieldBoundsDirty = false;
}
}