using Sand.Core; namespace Sand.ChunkPrototype; public sealed partial class PrototypeSparseSandAdapter { public bool AddParticle(int x, int y, PrototypeParticle particle) { if (!InBounds(x, y) || particle.IsEmpty) { return false; } RegisterParticleProfile(particle); var coord = GetChunkCoord(x, y); var (localX, localY) = GetLocalCoord(x, y); var page = GetOrCreateCellPage(coord); if (page.IsOccupied(localX, localY)) { if (particle.MotionType == PrototypeParticleType.Wall) { page.SetCell(localX, localY, particle); page.SetDriftState(localX, localY, 0); InitializeRuntimeState(page, localX, localY, particle); page.LastTouchedFrame = _stepCounter; MarkChunkActive(coord, page); World.SetOccupied(x, y, (byte)particle.MotionType); return true; } return false; } page.SetCell(localX, localY, particle); page.SetDriftState(localX, localY, GetDefaultDriftDirection(x, y)); InitializeRuntimeState(page, localX, localY, particle); page.LastTouchedFrame = _stepCounter; _particleCount++; if (_fieldPages.TryGetValue(coord, out var fieldPage)) { fieldPage.SetCell(localX, localY, default); page.HasFieldActivity = !fieldPage.IsEmpty(); } World.SetOccupied(x, y, (byte)particle.MotionType); MarkChunkActive(coord, page); WakeNeighborsForBorderTouch(coord, localX, localY); return true; } public bool HasParticle(int x, int y) => TryGetParticle(x, y, out _); public bool TryGetParticle(int x, int y, out PrototypeParticle particle) { particle = default; if (!TryGetCellPage(x, y, out _, out var page, out var localX, out var localY)) { return false; } particle = page[localX, localY]; return particle.TypeId != 0; } public PrototypeParticleType GetParticleTypeAt(int x, int y) => TryGetParticle(x, y, out var particle) ? particle.MotionType : PrototypeParticleType.Empty; public ushort GetTypeIdAt(int x, int y) => TryGetParticle(x, y, out var particle) ? particle.TypeId : (ushort)0; public float GetTemperatureAt(int x, int y) { if (!TryGetCellPage(x, y, out _, out var page, out var localX, out var localY)) { return _ambientTemperature; } return page.GetTemperature(localX, localY); } public bool RemoveParticle(int x, int y) { if (!TryGetCellPage(x, y, out var coord, out var page, out var localX, out var localY)) { return false; } var particle = page[localX, localY]; if (particle.TypeId == 0) { return false; } page.SetCell(localX, localY, default); page.SetDriftState(localX, localY, 0); page.LastTouchedFrame = _stepCounter; _particleCount--; World.ClearOccupied(x, y); MarkChunkActive(coord, page); WakeNeighborsForBorderTouch(coord, localX, localY); RemoveEmptyCellPageIfUnused(coord, page); return true; } private bool MoveParticle(int fromX, int fromY, int toX, int toY) { if (!TryGetCellPage(fromX, fromY, out var sourceCoord, out var sourcePage, out var sourceLocalX, out var sourceLocalY)) { return false; } var particle = sourcePage[sourceLocalX, sourceLocalY]; if (particle.TypeId == 0) { return false; } var destinationCoord = GetChunkCoord(toX, toY); var (destinationLocalX, destinationLocalY) = GetLocalCoord(toX, toY); var destinationPage = GetOrCreateCellPage(destinationCoord); if (destinationPage.IsOccupied(destinationLocalX, destinationLocalY)) { return false; } var driftState = sourcePage.GetDriftState(sourceLocalX, sourceLocalY); var pressureDuration = sourcePage.GetPressureDuration(sourceLocalX, sourceLocalY); var temperature = sourcePage.GetTemperature(sourceLocalX, sourceLocalY); var burnTime = sourcePage.GetBurnTime(sourceLocalX, sourceLocalY); var burning = sourcePage.GetBurning(sourceLocalX, sourceLocalY); var sparkTime = sourcePage.GetSparkTime(sourceLocalX, sourceLocalY); var lifetime = sourcePage.GetLifetime(sourceLocalX, sourceLocalY); var cellAge = sourcePage.GetCellAge(sourceLocalX, sourceLocalY); var integrity = sourcePage.GetIntegrity(sourceLocalX, sourceLocalY); sourcePage.SetCell(sourceLocalX, sourceLocalY, default); sourcePage.SetDriftState(sourceLocalX, sourceLocalY, 0); sourcePage.SetPressureDuration(sourceLocalX, sourceLocalY, 0f); destinationPage.SetCell(destinationLocalX, destinationLocalY, particle); destinationPage.SetDriftState(destinationLocalX, destinationLocalY, ResolveDriftAfterMove(fromX, toX, driftState)); destinationPage.SetPressureDuration(destinationLocalX, destinationLocalY, pressureDuration); destinationPage.SetTemperature(destinationLocalX, destinationLocalY, temperature); destinationPage.SetBurnTime(destinationLocalX, destinationLocalY, burnTime); destinationPage.SetBurning(destinationLocalX, destinationLocalY, burning); destinationPage.SetSparkTime(destinationLocalX, destinationLocalY, sparkTime); destinationPage.SetLifetime(destinationLocalX, destinationLocalY, lifetime); destinationPage.SetCellAge(destinationLocalX, destinationLocalY, cellAge); destinationPage.SetIntegrity(destinationLocalX, destinationLocalY, integrity); destinationPage.MarkProcessed(destinationLocalX, destinationLocalY, _stepCounter); sourcePage.LastTouchedFrame = _stepCounter; destinationPage.LastTouchedFrame = _stepCounter; MarkChunkActive(sourceCoord, sourcePage); MarkChunkActive(destinationCoord, destinationPage); World.MoveOccupied(fromX, fromY, toX, toY, (byte)particle.MotionType); WakeNeighborsForBorderTouch(sourceCoord, sourceLocalX, sourceLocalY); WakeNeighborsForBorderTouch(destinationCoord, destinationLocalX, destinationLocalY); RemoveEmptyCellPageIfUnused(sourceCoord, sourcePage); _movedParticles++; return true; } private bool MoveParticleWithinPage(ChunkCoord coord, ChunkCellPage page, int fromLocalX, int fromLocalY, int toLocalX, int toLocalY, int fromX, int fromY, int toX, int toY) { var particle = page[fromLocalX, fromLocalY]; if (particle.TypeId == 0 || page.IsOccupied(toLocalX, toLocalY)) { return false; } var driftState = page.GetDriftState(fromLocalX, fromLocalY); var pressureDuration = page.GetPressureDuration(fromLocalX, fromLocalY); var temperature = page.GetTemperature(fromLocalX, fromLocalY); var burnTime = page.GetBurnTime(fromLocalX, fromLocalY); var burning = page.GetBurning(fromLocalX, fromLocalY); var sparkTime = page.GetSparkTime(fromLocalX, fromLocalY); var lifetime = page.GetLifetime(fromLocalX, fromLocalY); var cellAge = page.GetCellAge(fromLocalX, fromLocalY); var integrity = page.GetIntegrity(fromLocalX, fromLocalY); page.SetCell(fromLocalX, fromLocalY, default); page.SetDriftState(fromLocalX, fromLocalY, 0); page.SetPressureDuration(fromLocalX, fromLocalY, 0f); page.SetCell(toLocalX, toLocalY, particle); page.SetDriftState(toLocalX, toLocalY, ResolveDriftAfterMove(fromX, toX, driftState)); page.SetPressureDuration(toLocalX, toLocalY, pressureDuration); page.SetTemperature(toLocalX, toLocalY, temperature); page.SetBurnTime(toLocalX, toLocalY, burnTime); page.SetBurning(toLocalX, toLocalY, burning); page.SetSparkTime(toLocalX, toLocalY, sparkTime); page.SetLifetime(toLocalX, toLocalY, lifetime); page.SetCellAge(toLocalX, toLocalY, cellAge); page.SetIntegrity(toLocalX, toLocalY, integrity); page.MarkProcessed(toLocalX, toLocalY, _stepCounter); page.LastTouchedFrame = _stepCounter; MarkChunkActive(coord, page); World.MoveOccupiedWithinChunk(coord.X, coord.Y, fromLocalX, fromLocalY, toLocalX, toLocalY, (byte)particle.MotionType); WakeNeighborsForBorderTouch(coord, fromLocalX, fromLocalY); WakeNeighborsForBorderTouch(coord, toLocalX, toLocalY); _movedParticles++; return true; } private bool SwapParticles(int firstX, int firstY, int secondX, int secondY) { if (!TryGetCellPage(firstX, firstY, out var firstCoord, out var firstPage, out var firstLocalX, out var firstLocalY) || !TryGetCellPage(secondX, secondY, out var secondCoord, out var secondPage, out var secondLocalX, out var secondLocalY)) { return false; } var firstParticle = firstPage[firstLocalX, firstLocalY]; var secondParticle = secondPage[secondLocalX, secondLocalY]; if (firstParticle.TypeId == 0 || secondParticle.TypeId == 0) { return false; } var firstDrift = firstPage.GetDriftState(firstLocalX, firstLocalY); var secondDrift = secondPage.GetDriftState(secondLocalX, secondLocalY); var firstPressureDuration = firstPage.GetPressureDuration(firstLocalX, firstLocalY); var secondPressureDuration = secondPage.GetPressureDuration(secondLocalX, secondLocalY); var firstTemperature = firstPage.GetTemperature(firstLocalX, firstLocalY); var secondTemperature = secondPage.GetTemperature(secondLocalX, secondLocalY); var firstBurnTime = firstPage.GetBurnTime(firstLocalX, firstLocalY); var secondBurnTime = secondPage.GetBurnTime(secondLocalX, secondLocalY); var firstBurning = firstPage.GetBurning(firstLocalX, firstLocalY); var secondBurning = secondPage.GetBurning(secondLocalX, secondLocalY); var firstSparkTime = firstPage.GetSparkTime(firstLocalX, firstLocalY); var secondSparkTime = secondPage.GetSparkTime(secondLocalX, secondLocalY); var firstLifetime = firstPage.GetLifetime(firstLocalX, firstLocalY); var secondLifetime = secondPage.GetLifetime(secondLocalX, secondLocalY); var firstAge = firstPage.GetCellAge(firstLocalX, firstLocalY); var secondAge = secondPage.GetCellAge(secondLocalX, secondLocalY); var firstIntegrity = firstPage.GetIntegrity(firstLocalX, firstLocalY); var secondIntegrity = secondPage.GetIntegrity(secondLocalX, secondLocalY); firstPage.SetCell(firstLocalX, firstLocalY, secondParticle); firstPage.SetDriftState(firstLocalX, firstLocalY, ResolveDriftAfterMove(secondX, firstX, secondDrift)); firstPage.SetPressureDuration(firstLocalX, firstLocalY, secondPressureDuration); firstPage.SetTemperature(firstLocalX, firstLocalY, secondTemperature); firstPage.SetBurnTime(firstLocalX, firstLocalY, secondBurnTime); firstPage.SetBurning(firstLocalX, firstLocalY, secondBurning); firstPage.SetSparkTime(firstLocalX, firstLocalY, secondSparkTime); firstPage.SetLifetime(firstLocalX, firstLocalY, secondLifetime); firstPage.SetCellAge(firstLocalX, firstLocalY, secondAge); firstPage.SetIntegrity(firstLocalX, firstLocalY, secondIntegrity); secondPage.SetCell(secondLocalX, secondLocalY, firstParticle); secondPage.SetDriftState(secondLocalX, secondLocalY, ResolveDriftAfterMove(firstX, secondX, firstDrift)); secondPage.SetPressureDuration(secondLocalX, secondLocalY, firstPressureDuration); secondPage.SetTemperature(secondLocalX, secondLocalY, firstTemperature); secondPage.SetBurnTime(secondLocalX, secondLocalY, firstBurnTime); secondPage.SetBurning(secondLocalX, secondLocalY, firstBurning); secondPage.SetSparkTime(secondLocalX, secondLocalY, firstSparkTime); secondPage.SetLifetime(secondLocalX, secondLocalY, firstLifetime); secondPage.SetCellAge(secondLocalX, secondLocalY, firstAge); secondPage.SetIntegrity(secondLocalX, secondLocalY, firstIntegrity); firstPage.MarkProcessed(firstLocalX, firstLocalY, _stepCounter); secondPage.MarkProcessed(secondLocalX, secondLocalY, _stepCounter); firstPage.LastTouchedFrame = _stepCounter; secondPage.LastTouchedFrame = _stepCounter; MarkChunkActive(firstCoord, firstPage); MarkChunkActive(secondCoord, secondPage); if (firstCoord == secondCoord) { World.SwapOccupiedWithinChunk(firstCoord.X, firstCoord.Y, firstLocalX, firstLocalY, secondLocalX, secondLocalY, (byte)firstParticle.MotionType, (byte)secondParticle.MotionType); } else { World.SwapOccupied(firstX, firstY, secondX, secondY, (byte)firstParticle.MotionType, (byte)secondParticle.MotionType); } WakeNeighborsForBorderTouch(firstCoord, firstLocalX, firstLocalY); WakeNeighborsForBorderTouch(secondCoord, secondLocalX, secondLocalY); _swappedParticles++; return true; } private bool ReplaceParticle(int x, int y, PrototypeParticle particle) { if (!TryGetCellPage(x, y, out var coord, out var page, out var localX, out var localY)) { return false; } if (particle.IsEmpty) { return RemoveParticle(x, y); } RegisterParticleProfile(particle); page.SetCell(localX, localY, particle); page.SetDriftState(localX, localY, GetDefaultDriftDirection(x, y)); InitializeRuntimeState(page, localX, localY, particle); page.LastTouchedFrame = _stepCounter; page.MarkProcessed(localX, localY, _stepCounter); MarkChunkActive(coord, page); World.SetOccupied(x, y, (byte)particle.MotionType); WakeNeighborsForBorderTouch(coord, localX, localY); return true; } private void InitializeRuntimeState(ChunkCellPage page, int localX, int localY, PrototypeParticle particle) { page.SetPressureDuration(localX, localY, 0f); page.SetTemperature(localX, localY, particle.InitialTemperature); page.SetBurnTime(localX, localY, particle.BurnDuration); page.SetBurning(localX, localY, particle.BurningInit ? (byte)1 : (byte)0); page.SetSparkTime(localX, localY, 0); page.SetLifetime(localX, localY, particle.DefaultLifetime); page.SetCellAge(localX, localY, 0f); page.SetIntegrity(localX, localY, particle.Durability); } private bool TryResolveProfile(ushort typeId, out PrototypeParticle particle) => _particleProfiles.TryGetValue(typeId, out particle); private bool TryFindProfileById(string particleId, out PrototypeParticle particle) { foreach (var profile in _particleProfiles.Values) { if (profile.TypeId != 0 && string.Equals(profile.Id, particleId, StringComparison.OrdinalIgnoreCase)) { particle = profile; return true; } } particle = default; return false; } private ChunkCoord GetChunkCoord(int x, int y) => new(x / _config.ChunkWidth, y / _config.ChunkHeight); private (int LocalX, int LocalY) GetLocalCoord(int x, int y) => (x % _config.ChunkWidth, y % _config.ChunkHeight); private bool TryGetCellPage(int x, int y, out ChunkCoord coord, out ChunkCellPage page, out int localX, out int localY) { coord = default; page = null!; localX = 0; localY = 0; if (!InBounds(x, y)) { return false; } coord = GetChunkCoord(x, y); if (!_cellPages.TryGetValue(coord, out page!)) { return false; } (localX, localY) = GetLocalCoord(x, y); return true; } private bool TryGetFieldPage(int x, int y, out ChunkCoord coord, out ChunkFieldPage page, out int localX, out int localY) { coord = default; page = null!; localX = 0; localY = 0; if (!InBounds(x, y)) { return false; } coord = GetChunkCoord(x, y); if (!_fieldPages.TryGetValue(coord, out page!)) { return false; } (localX, localY) = GetLocalCoord(x, y); return true; } private ChunkCellPage GetOrCreateCellPage(ChunkCoord coord) { if (_cellPages.TryGetValue(coord, out var page)) { return page; } page = new ChunkCellPage(_config.ChunkWidth, _config.ChunkHeight); _cellPages[coord] = page; return page; } private ChunkFieldPage GetOrCreateFieldPage(ChunkCoord coord) { if (_fieldPages.TryGetValue(coord, out var page)) { return page; } page = new ChunkFieldPage(_config.ChunkWidth, _config.ChunkHeight); _fieldPages[coord] = page; return page; } private int ToWorldX(ChunkCoord coord, int localX) => (coord.X * _config.ChunkWidth) + localX; private int ToWorldY(ChunkCoord coord, int localY) => (coord.Y * _config.ChunkHeight) + localY; private bool InBounds(int x, int y) => x >= 0 && x < Width && y >= 0 && y < Height; }