namespace Sand.Core; public sealed partial class SandSimulation { public void Step(float dt) { if (_settings.PauseSim) { return; } if (_settings.EnableAcceleration && Accelerator is not null) { Accelerator.Step(this, dt); return; } int preStepStartX; int preStepStartY; int preStepEndX; int preStepEndY; var hadPreStepBounds = AreFieldVisualsEnabled() ? TryGetSimulationBounds(out preStepStartX, out preStepStartY, out preStepEndX, out preStepEndY) : TryGetOccupiedBoundsOnly(out preStepStartX, out preStepStartY, out preStepEndX, out preStepEndY); _deferVisualDirtyTracking = true; DecayFields(dt); if (ParticleCount == 0) { _deferVisualDirtyTracking = false; MarkBoundsUnionDirty(hadPreStepBounds, preStepStartX, preStepStartY, preStepEndX, preStepEndY); Frame++; FrameStats.Frame = Frame; 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; return; } EnsureOccupiedBounds(); var margin = _settings.WrapParticles ? 0 : 1; var startX = _settings.WrapParticles ? 0 : Math.Max(0, _minOccupiedX - margin); var endX = _settings.WrapParticles ? Width - 1 : Math.Min(Width - 1, _maxOccupiedX + margin); var startY = _settings.WrapParticles ? 0 : Math.Max(0, _minOccupiedY - margin); var endY = _settings.WrapParticles ? Height - 1 : Math.Min(Height - 1, _maxOccupiedY + margin); _activeStepToken = Frame + 1; var processedCells = 0; for (var y = endY; y >= startY; y--) { for (var x = startX; x <= endX; x++) { if (_processedFrame[x, y] == _activeStepToken) { continue; } var typeId = TypeId[x, y]; if (typeId == 0) { continue; } var seed = Hash(x, y, Frame); var leftFirst = (seed & 1u) == 0; MarkProcessed(x, y); processedCells++; if (TickLifetime(x, y, dt)) { continue; } typeId = TypeId[x, y]; if (TickSpark(x, y, typeId, ref seed)) { continue; } typeId = TypeId[x, y]; AutoIgnite(x, y, typeId); ApplyRuntimeEmissionAndProduction(x, y, typeId, ref seed); if (TickBurning(x, y, typeId, ref seed)) { continue; } typeId = TypeId[x, y]; if (TickSpecialBehavior(x, y, typeId, ref seed)) { continue; } typeId = TypeId[x, y]; ApplyPhaseTransitions(x, y, ref typeId); if (typeId == 0) { continue; } if (ApplyLocalReactions(x, y, ref typeId, ref seed)) { continue; } typeId = TypeId[x, y]; if (TickPressure(x, y, typeId, ref seed)) { continue; } ApplyTemperatureDiffusion(x, y, typeId); ApplyMovement(x, y, typeId, leftFirst, ref seed); } } if (_settings.OuterWall && _idWall != 0) { ApplyBoundaryWallsFast(); } _deferVisualDirtyTracking = false; MarkBoundsUnionDirty(hadPreStepBounds, preStepStartX, preStepStartY, preStepEndX, preStepEndY); Frame++; _activeStepToken = 0; _staleOccupiedBoundsFrames = _boundsDirty ? _staleOccupiedBoundsFrames + 1 : 0; FrameStats.Frame = Frame; FrameStats.ProcessedCells = processedCells; FrameStats.ParticleCount = ParticleCount; FrameStats.MinActiveX = _hasOccupiedBounds ? _minOccupiedX : 0; FrameStats.MinActiveY = _hasOccupiedBounds ? _minOccupiedY : 0; FrameStats.MaxActiveX = _hasOccupiedBounds ? _maxOccupiedX : 0; FrameStats.MaxActiveY = _hasOccupiedBounds ? _maxOccupiedY : 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; } public ReadOnlySpan BuildRgbFrame() { UpdateCachedVisualBuffers(); return _rgbBuffer; } public ReadOnlySpan BuildRgbaFrame() { UpdateCachedVisualBuffers(); FrameStats.FrameBuildBytesTouched = (long)_rgbaBuffer.Length; FrameStats.RenderTimeMicroseconds = 0; return _rgbaBuffer; } public void BuildRgbaFrame(Span destination) { var expectedLength = Width * Height * 4; if (destination.Length < expectedLength) { throw new ArgumentException($"RGBA destination must be at least {expectedLength} bytes.", nameof(destination)); } UpdateCachedVisualBuffers(); _rgbaBuffer.AsSpan().CopyTo(destination); FrameStats.FrameBuildBytesTouched = (long)_rgbaBuffer.Length; FrameStats.RenderTimeMicroseconds = 0; } private Rgb24 ResolveVisualColor(int x, int y) { var typeId = TypeId[x, y]; if (typeId == 0) { if (_settings.EnablePressureVisuals) { var pressure = MathF.Abs(_airPressure[x, y]); if (pressure > 0.08f) { var tint = (byte)Math.Clamp((int)(pressure * 32f), 10, 140); if (_airPressure[x, y] >= 0f) { return new Rgb24((byte)(26 + tint), (byte)(18 + tint / 3), 24); } return new Rgb24(18, (byte)(24 + tint / 4), (byte)(30 + tint)); } } if (_settings.EnableWindVisuals) { var totalFieldX = _windFieldX[x, y] + _forceFieldX[x, y]; var totalFieldY = _windFieldY[x, y] + _forceFieldY[x, y]; var windStrength = MathF.Sqrt((totalFieldX * totalFieldX) + (totalFieldY * totalFieldY)); if (windStrength > 0.08f) { var tint = (byte)Math.Clamp((int)(windStrength * 40f), 10, 120); return new Rgb24((byte)(16 + tint / 3), (byte)(24 + tint / 2), (byte)(32 + tint)); } } return new Rgb24(0, 0, 0); } var color = SparkTime[x, y] > 0 ? new Rgb24(255, 255, 150) : _colorLut[typeId]; if (_settings.EnableTempVisuals) { var delta = Temperature[x, y] - _settings.AmbientTemperature; if (delta > 5f) { var heat = (byte)Math.Clamp((int)(delta * 4f), 0, 120); color = new Rgb24( (byte)Math.Min(255, color.R + heat), (byte)Math.Max(0, color.G - heat / 3), (byte)Math.Max(0, color.B - heat / 2)); } else if (delta < -5f) { var cool = (byte)Math.Clamp((int)(Math.Abs(delta) * 4f), 0, 120); color = new Rgb24( (byte)Math.Max(0, color.R - cool / 2), (byte)Math.Max(0, color.G - cool / 3), (byte)Math.Min(255, color.B + cool)); } } if (_settings.EnableGasEffect && _kind[typeId] == (byte)ParticleKind.Gas) { var bias = (byte)(Hash(x, y, Frame) & 0x0F); color = new Rgb24( (byte)Math.Min(255, color.R + bias), (byte)Math.Min(255, color.G + bias), (byte)Math.Min(255, color.B + bias)); } if (_settings.EnableGlow && (Burning[x, y] != 0 || typeId == _idFire || typeId == _idLava || SparkTime[x, y] > 0)) { color = new Rgb24( (byte)Math.Min(255, color.R + 25), (byte)Math.Min(255, color.G + 18), (byte)Math.Min(255, color.B + 12)); } return color; } }