using Sand.Core; using System.Diagnostics; namespace Sand.ChunkPrototype; public sealed partial class PrototypeSparseSandAdapter { public void ClearFields() { _fieldPages.Clear(); foreach (var page in _cellPages.Values) { page.HasFieldActivity = false; } _fullFrameDirty = true; _dirtyVisualChunks.Clear(); foreach (var coord in _cellPages.Keys) { _dirtyVisualChunks.Add(coord); } } public (float X, float Y) GetWindAtCell(int x, int y) { if (!TryGetFieldPage(x, y, out _, out var page, out var localX, out var localY)) { return (0f, 0f); } var cell = page.GetCell(localX, localY); return (cell.WindX, cell.WindY); } public (float X, float Y) GetForceAtCell(int x, int y) { if (!TryGetFieldPage(x, y, out _, out var page, out var localX, out var localY)) { return (0f, 0f); } var cell = page.GetCell(localX, localY); return (cell.ForceX, cell.ForceY); } public float GetAirPressureAtCell(int x, int y) { if (!TryGetFieldPage(x, y, out _, out var page, out var localX, out var localY)) { return 0f; } return page.GetCell(localX, localY).Pressure; } public ReadOnlySpan BuildRgbaFrame(bool enableWindVisuals = false, bool enablePressureVisuals = false) { var renderStart = Stopwatch.GetTimestamp(); long bytesTouched; if (_fullFrameDirty) { Array.Clear(_rgbaBuffer); for (var i = 3; i < _rgbaBuffer.Length; i += 4) { _rgbaBuffer[i] = 255; } bytesTouched = _rgbaBuffer.Length; var rendered = new HashSet(); foreach (var coord in _cellPages.Keys) { RenderChunk(coord, enableWindVisuals, enablePressureVisuals); rendered.Add(coord); } foreach (var coord in _fieldPages.Keys) { if (rendered.Add(coord)) { RenderChunk(coord, enableWindVisuals, enablePressureVisuals); } } _dirtyVisualChunks.Clear(); _fullFrameDirty = false; } else { bytesTouched = 0; foreach (var coord in _dirtyVisualChunks) { bytesTouched += ClearChunkVisual(coord); RenderChunk(coord, enableWindVisuals, enablePressureVisuals); } _dirtyVisualChunks.Clear(); } var renderMicros = ToMicroseconds(renderStart, Stopwatch.GetTimestamp()); _lastStepStats = _lastStepStats with { FrameBuildBytesTouched = bytesTouched, RenderTimeMicroseconds = renderMicros, }; return _rgbaBuffer; } public ReadOnlySpan BuildRgbFrame() { var rgba = BuildRgbaFrame(); for (int sourceIndex = 0, targetIndex = 0; sourceIndex < rgba.Length; sourceIndex += 4, targetIndex += 3) { _rgbBuffer[targetIndex] = rgba[sourceIndex]; _rgbBuffer[targetIndex + 1] = rgba[sourceIndex + 1]; _rgbBuffer[targetIndex + 2] = rgba[sourceIndex + 2]; } return _rgbBuffer; } public int TrimResidency(int marginChunks = 1) { var unloaded = World.UnloadEmptyChunks(); unloaded += World.UnloadInactiveChunks(marginChunks); foreach (var coord in _cellPages.Keys.ToArray()) { if (_cellPages[coord].OccupancyCount == 0 && !_fieldPages.ContainsKey(coord)) { _cellPages.Remove(coord); } } foreach (var coord in _fieldPages.Keys.ToArray()) { if (_fieldPages[coord].IsEmpty()) { _fieldPages.Remove(coord); } } return unloaded; } public void ApplyWindBrush(int centerX, int centerY, int brushRadius, float forceX, float forceY) => ApplyDirectionalField(centerX, centerY, brushRadius, forceX, forceY, writeWind: true, writePressure: false); public void ApplyAirBrush(int centerX, int centerY, int brushRadius, float forceX, float forceY) => ApplyDirectionalField(centerX, centerY, brushRadius, forceX, forceY, writeWind: true, writePressure: true); public void ApplyGravityBrush(int centerX, int centerY, int brushRadius, float strength) => ApplyRadialField(centerX, centerY, brushRadius, MathF.Abs(strength), inward: true); public void ApplyRepulsorBrush(int centerX, int centerY, int brushRadius, float strength) => ApplyRadialField(centerX, centerY, brushRadius, MathF.Abs(strength), inward: false); private void ApplyDirectionalField(int centerX, int centerY, int brushRadius, float forceX, float forceY, bool writeWind, bool writePressure) { var radiusSquared = brushRadius * brushRadius; for (var offsetY = -brushRadius; offsetY <= brushRadius; offsetY++) { for (var offsetX = -brushRadius; offsetX <= brushRadius; offsetX++) { var distanceSquared = (offsetX * offsetX) + (offsetY * offsetY); if (distanceSquared > radiusSquared) { continue; } var x = centerX + offsetX; var y = centerY + offsetY; if (!InBounds(x, y)) { continue; } var distance = MathF.Sqrt(distanceSquared); var falloff = brushRadius <= 0 ? 1f : MathF.Max(0.1f, 1f - (distance / Math.Max(1f, brushRadius))); var coord = GetChunkCoord(x, y); var (localX, localY) = GetLocalCoord(x, y); var fieldPage = GetOrCreateFieldPage(coord); var cell = fieldPage.GetCell(localX, localY); fieldPage.SetCell(localX, localY, new FieldCellData( writeWind ? cell.WindX + (forceX * falloff) : cell.WindX, writeWind ? cell.WindY + (forceY * falloff) : cell.WindY, writeWind ? cell.ForceX : cell.ForceX + (forceX * 0.25f * falloff), writeWind ? cell.ForceY : cell.ForceY + (forceY * 0.25f * falloff), writePressure ? cell.Pressure + (MathF.Sqrt((forceX * forceX) + (forceY * forceY)) * falloff) : cell.Pressure)); fieldPage.LastDecayedFrame = _stepCounter; WakeChunk(coord); } } } private void ApplyRadialField(int centerX, int centerY, int brushRadius, float strength, bool inward) { var radiusSquared = brushRadius * brushRadius; for (var offsetY = -brushRadius; offsetY <= brushRadius; offsetY++) { for (var offsetX = -brushRadius; offsetX <= brushRadius; offsetX++) { var distanceSquared = (offsetX * offsetX) + (offsetY * offsetY); if (distanceSquared > radiusSquared) { continue; } var x = centerX + offsetX; var y = centerY + offsetY; if (!InBounds(x, y)) { continue; } var distance = MathF.Sqrt(Math.Max(1f, distanceSquared)); var scale = MathF.Max(0.1f, 1f - (distance / Math.Max(1f, brushRadius))); var directionX = offsetX / distance; var directionY = offsetY / distance; if (inward) { directionX = -directionX; directionY = -directionY; } var coord = GetChunkCoord(x, y); var (localX, localY) = GetLocalCoord(x, y); var fieldPage = GetOrCreateFieldPage(coord); var cell = fieldPage.GetCell(localX, localY); fieldPage.SetCell(localX, localY, new FieldCellData( cell.WindX, cell.WindY, cell.ForceX + (directionX * strength * scale), cell.ForceY + (directionY * strength * scale), cell.Pressure + ((inward ? 1f : -1f) * strength * 0.2f * scale))); fieldPage.LastDecayedFrame = _stepCounter; WakeChunk(coord); } } } private void DecayFields() { foreach (var coord in _fieldPages.Keys.ToArray()) { var fieldPage = _fieldPages[coord]; var changed = false; foreach (var (localX, localY, cell) in fieldPage.EnumerateActiveCells().ToArray()) { var decayed = new FieldCellData( cell.WindX * FieldDecayFactor, cell.WindY * FieldDecayFactor, cell.ForceX * FieldDecayFactor, cell.ForceY * FieldDecayFactor, cell.Pressure * FieldDecayFactor); fieldPage.SetCell(localX, localY, decayed); changed = true; } if (fieldPage.IsEmpty()) { _fieldPages.Remove(coord); if (_cellPages.TryGetValue(coord, out var cellPage)) { cellPage.HasFieldActivity = false; } _dirtyVisualChunks.Add(coord); continue; } if (changed) { fieldPage.LastDecayedFrame = _stepCounter; _dirtyVisualChunks.Add(coord); if (_cellPages.TryGetValue(coord, out var cellPage)) { cellPage.HasFieldActivity = true; cellPage.IsActive = true; } } } } private void MarkChunkActive(ChunkCoord coord, ChunkCellPage page) { page.IsActive = true; page.LastTouchedFrame = _stepCounter; _dirtyVisualChunks.Add(coord); } private void WakeNeighborsForBorderTouch(ChunkCoord coord, int localX, int localY) { if (localX == 0) { WakeChunk(new ChunkCoord(coord.X - 1, coord.Y), _config.ChunkWidth - 1, localY); } if (localX == _config.ChunkWidth - 1) { WakeChunk(new ChunkCoord(coord.X + 1, coord.Y), 0, localY); } if (localY == 0) { WakeChunk(new ChunkCoord(coord.X, coord.Y - 1), localX, _config.ChunkHeight - 1); } if (localY == _config.ChunkHeight - 1) { WakeChunk(new ChunkCoord(coord.X, coord.Y + 1), localX, 0); } } private void WakeChunk(ChunkCoord coord, int localX, int localY) { if (_cellPages.TryGetValue(coord, out var page)) { page.IsActive = true; page.LastTouchedFrame = _stepCounter; page.RequestFutureSteps(1); page.MarkDirtyBand(localX, localY); } _dirtyVisualChunks.Add(coord); } private void WakeChunk(ChunkCoord coord) { if (_cellPages.TryGetValue(coord, out var page)) { page.IsActive = true; page.HasFieldActivity = true; page.LastTouchedFrame = _stepCounter; page.RequestFutureSteps(1); } _dirtyVisualChunks.Add(coord); } private void RemoveEmptyCellPageIfUnused(ChunkCoord coord, ChunkCellPage page) { if (page.OccupancyCount == 0 && !_fieldPages.ContainsKey(coord)) { _cellPages.Remove(coord); } } private long ClearChunkVisual(ChunkCoord coord) { long bytesTouched = 0; var startX = Math.Max(0, (coord.X * _config.ChunkWidth) - RenderHaloCells); var startY = Math.Max(0, (coord.Y * _config.ChunkHeight) - RenderHaloCells); var endX = Math.Min(Width - 1, ((coord.X + 1) * _config.ChunkWidth) + RenderHaloCells - 1); var endY = Math.Min(Height - 1, ((coord.Y + 1) * _config.ChunkHeight) + RenderHaloCells - 1); for (var worldY = startY; worldY <= endY; worldY++) { for (var worldX = startX; worldX <= endX; worldX++) { var rgbaIndex = ((worldY * Width) + worldX) * 4; _rgbaBuffer[rgbaIndex] = 0; _rgbaBuffer[rgbaIndex + 1] = 0; _rgbaBuffer[rgbaIndex + 2] = 0; _rgbaBuffer[rgbaIndex + 3] = 255; bytesTouched += 4; } } return bytesTouched; } private void RenderChunk(ChunkCoord coord, bool enableWindVisuals, bool enablePressureVisuals) { var startX = Math.Max(0, (coord.X * _config.ChunkWidth) - RenderHaloCells); var startY = Math.Max(0, (coord.Y * _config.ChunkHeight) - RenderHaloCells); var endX = Math.Min(Width - 1, ((coord.X + 1) * _config.ChunkWidth) + RenderHaloCells - 1); var endY = Math.Min(Height - 1, ((coord.Y + 1) * _config.ChunkHeight) + RenderHaloCells - 1); for (var worldY = startY; worldY <= endY; worldY++) { for (var worldX = startX; worldX <= endX; worldX++) { var rgbaIndex = ((worldY * Width) + worldX) * 4; var particle = TryGetParticle(worldX, worldY, out var currentParticle) ? currentParticle : default; if (particle.TypeId != 0) { _rgbaBuffer[rgbaIndex] = particle.R; _rgbaBuffer[rgbaIndex + 1] = particle.G; _rgbaBuffer[rgbaIndex + 2] = particle.B; _rgbaBuffer[rgbaIndex + 3] = 255; continue; } if (TryResolveGasSeamRenderColor(worldX, worldY, out var gasColor)) { _rgbaBuffer[rgbaIndex] = gasColor.R; _rgbaBuffer[rgbaIndex + 1] = gasColor.G; _rgbaBuffer[rgbaIndex + 2] = gasColor.B; _rgbaBuffer[rgbaIndex + 3] = 255; continue; } var color = ResolveFieldColorAtWorld(worldX, worldY, enableWindVisuals, enablePressureVisuals); _rgbaBuffer[rgbaIndex] = color.R; _rgbaBuffer[rgbaIndex + 1] = color.G; _rgbaBuffer[rgbaIndex + 2] = color.B; _rgbaBuffer[rgbaIndex + 3] = 255; } } } private (byte R, byte G, byte B) ResolveFieldColorAtWorld(int x, int y, bool enableWindVisuals, bool enablePressureVisuals) { if (!TryGetFieldPage(x, y, out _, out var fieldPage, out var localX, out var localY)) { return (0, 0, 0); } return ResolveFieldColor(fieldPage, localX, localY, enableWindVisuals, enablePressureVisuals); } private bool TryResolveGasSeamRenderColor(int x, int y, out (byte R, byte G, byte B) color) { color = default; if (!IsNearChunkRowSeam(y)) { return false; } var gasNeighborCount = 0; var gasAboveCount = 0; var gasBelowCount = 0; var totalR = 0; var totalG = 0; var totalB = 0; for (var offsetY = -1; offsetY <= 1; offsetY++) { for (var offsetX = -1; offsetX <= 1; offsetX++) { if (offsetX == 0 && offsetY == 0) { continue; } var neighborX = x + offsetX; var neighborY = y + offsetY; if (!TryGetParticle(neighborX, neighborY, out var particle) || particle.MotionType != PrototypeParticleType.Steam) { continue; } gasNeighborCount++; if (offsetY < 0) { gasAboveCount++; } else if (offsetY > 0) { gasBelowCount++; } totalR += particle.R; totalG += particle.G; totalB += particle.B; } } if (gasNeighborCount < 3 || gasAboveCount == 0 || gasBelowCount == 0) { return false; } color = ((byte)(totalR / gasNeighborCount), (byte)(totalG / gasNeighborCount), (byte)(totalB / gasNeighborCount)); return true; } private bool IsNearChunkRowSeam(int y) { var localY = y % _config.ChunkHeight; return localY == 0 || localY == _config.ChunkHeight - 1; } private static (byte R, byte G, byte B) ResolveFieldColor(ChunkFieldPage? fieldPage, int localX, int localY, bool enableWindVisuals, bool enablePressureVisuals) { if (fieldPage is null || !fieldPage.TryGetCell(localX, localY, out var cell)) { return (0, 0, 0); } byte r = 0; byte g = 0; byte b = 0; if (enablePressureVisuals) { var pressure = MathF.Abs(cell.Pressure); if (pressure > 0.08f) { var tint = (byte)Math.Clamp((int)(pressure * 32f), 10, 140); if (cell.Pressure >= 0f) { r = (byte)Math.Max((int)r, 26 + tint); g = (byte)Math.Max((int)g, 18 + (tint / 3)); b = (byte)Math.Max((int)b, 24); } else { r = (byte)Math.Max((int)r, 18); g = (byte)Math.Max((int)g, 24 + (tint / 4)); b = (byte)Math.Max((int)b, 30 + tint); } } } if (enableWindVisuals) { var totalFieldX = cell.WindX + cell.ForceX; var totalFieldY = cell.WindY + cell.ForceY; var windStrength = MathF.Sqrt((totalFieldX * totalFieldX) + (totalFieldY * totalFieldY)); if (windStrength > 0.08f) { var tint = (byte)Math.Clamp((int)(windStrength * 40f), 10, 120); r = (byte)Math.Max((int)r, 16 + (tint / 3)); g = (byte)Math.Max((int)g, 24 + (tint / 2)); b = (byte)Math.Max((int)b, 32 + tint); } } return (r, g, b); } }