using Sand.Core; namespace Sand.ChunkPrototype; public sealed partial class PrototypeSparseSandAdapter { private void TryStepParticle(ChunkCoord coord, int localX, int localY, PrototypeParticle particle) { var worldX = ToWorldX(coord, localX); var worldY = ToWorldY(coord, localY); var seed = unchecked(_stepCounter + (worldX * 31) + (worldY * 131)); if (CanUseMovementFastPath(worldX, worldY, particle)) { ApplyMovementOnly(coord, localX, localY, particle, ref seed, worldX, worldY); return; } _fullRuntimeStepCount++; switch (particle.Kind) { case ParticleKind.Solid: _fullRuntimeSolidCount++; break; case ParticleKind.Liquid: _fullRuntimeLiquidCount++; break; case ParticleKind.Gas: _fullRuntimeGasCount++; break; } if (NeedsRuntimeAging(particle)) { TickCellAging(worldX, worldY, particle); } if (particle.HasLifetimeBehavior && TickLifetime(worldX, worldY, ref particle)) { return; } if (particle.HasBurnBehavior) { AutoIgnite(worldX, worldY, particle); } if (NeedsRuntimeEmission(particle)) { ApplyRuntimeEmissionAndProduction(worldX, worldY, particle, ref seed); } if (particle.HasBurnBehavior && TickBurning(worldX, worldY, ref particle)) { return; } if (particle.HasSpecialBehavior && TickSpecialBehavior(worldX, worldY, particle, ref seed)) { return; } if (particle.HasPhaseBehavior) { ApplyPhaseTransition(worldX, worldY, ref particle); if (!TryRefreshRuntimeParticle(worldX, worldY, ref particle)) { return; } } if (particle.HasReactionBehavior && ApplyLocalReaction(worldX, worldY, particle, ref seed)) { return; } if (particle.HasReactionBehavior && !TryRefreshRuntimeParticle(worldX, worldY, ref particle)) { return; } if (particle.HasPressureBehavior && ApplyPressureResponse(worldX, worldY, particle)) { return; } if (particle.HasPressureBehavior && !TryRefreshRuntimeParticle(worldX, worldY, ref particle)) { return; } if (particle.HasThermalBehavior) { ApplyTemperatureDiffusion(worldX, worldY, particle); } if (particle.IsStatic) { return; } ApplyMovementOnly(coord, localX, localY, particle, ref seed, worldX, worldY); } private static bool NeedsRuntimeAging(PrototypeParticle particle) => particle.PhaseTransitionHysteresis > 0f || particle.HasPressureBehavior; private static bool NeedsRuntimeEmission(PrototypeParticle particle) => particle.HasEmissionBehavior || particle.IsMolten; private bool TryRefreshRuntimeParticle(int x, int y, ref PrototypeParticle particle) => TryGetParticle(x, y, out particle); private void ApplyMovementOnly(ChunkCoord coord, int localX, int localY, PrototypeParticle particle, ref int seed, int worldX, int worldY) { if (particle.IsStatic) { return; } var (netForceX, netForceY) = GetNetForce(worldX, worldY, particle); netForceX *= GetForceResponse(particle); netForceY *= GetForceResponse(particle); var horizontalPreference = ResolveHorizontalPreference(coord, localX, localY, particle, netForceX); var leftFirst = horizontalPreference < 0; if (netForceX > 0.25f) { leftFirst = false; } else if (netForceX < -0.25f) { leftFirst = true; } switch (particle.BehaviorKind) { case ParticleBehaviorKind.Fire: MoveFire(worldX, worldY, leftFirst, ref seed, netForceX); return; case ParticleBehaviorKind.BurningWood: return; case ParticleBehaviorKind.Ember: MoveEmber(worldX, worldY, particle, leftFirst, ref seed, netForceX); return; case ParticleBehaviorKind.Plasma: MovePlasma(worldX, worldY, particle, leftFirst, ref seed, netForceX, netForceY); return; } switch (particle.Kind) { case ParticleKind.Solid: TryMoveSolid(coord, localX, localY, worldX, worldY, particle, leftFirst, ref seed, netForceX); break; case ParticleKind.Liquid: TryMoveLiquid(worldX, worldY, particle, leftFirst, ref seed, netForceX); break; case ParticleKind.Gas: TryMoveGas(worldX, worldY, particle, leftFirst, ref seed, netForceX, netForceY); break; } } private void StepMovementOnlyParticle(ChunkCoord coord, int localX, int localY, PrototypeParticle particle) { _movementOnlyFastPathCount++; var worldX = ToWorldX(coord, localX); var worldY = ToWorldY(coord, localY); var seed = unchecked(_stepCounter + (worldX * 31) + (worldY * 131)); ApplyMovementOnly(coord, localX, localY, particle, ref seed, worldX, worldY); } private bool CanUseMovementFastPath(int x, int y, PrototypeParticle particle) { if (particle.IsStatic) { return false; } if (!particle.RequiresFullRuntimeStep) { return true; } if (particle.HasLifetimeBehavior || particle.HasBurnBehavior || particle.HasEmissionBehavior || particle.HasSpecialBehavior) { return false; } if (particle.HasPressureBehavior && (GetLocalPressure(x, y) > 0.05f || GetFieldForceMagnitude(x, y) > 0.08f || GetPressureDurationAt(x, y) > 0f || GetIntegrityAt(x, y) < particle.Durability)) { return false; } if (particle.HasPhaseBehavior) { var temperature = GetTemperatureAt(x, y); if ((particle.MeltTypeId != 0 && temperature >= particle.MeltTemperature) || (particle.EvaporateTypeId != 0 && temperature >= particle.EvaporateTemperature) || (particle.FreezeTypeId != 0 && temperature <= particle.FreezeTemperature) || (particle.SolidifyTypeId != 0 && (temperature <= particle.SolidifyTemperature || (particle.MotionType == PrototypeParticleType.Steam && GetPressureLoad(x, y) >= MathF.Max(0.9f, particle.PressureThreshold * 0.35f))))) { return false; } } if (particle.HasReactionBehavior && HasPotentialReactiveNeighbor(x, y, particle)) { return false; } if (particle.HasThermalBehavior) { var temperature = GetTemperatureAt(x, y); if (CanThrottleGasRuntime(x, y, particle, temperature)) { return true; } if (MathF.Abs(temperature - _ambientTemperature) > 0.5f) { return false; } } return true; } private bool TryMoveSolid(ChunkCoord coord, int localX, int localY, int x, int y, PrototypeParticle particle, bool leftFirst, ref int seed, float netForceX) { if (TrySkipBlockedSolid(coord, localX, localY, netForceX)) { return false; } if (TryMoveSolidDown(x, y, particle)) { return true; } var canMoveLeft = CanMoveIntoEmpty(x - 1, y + 1); var canMoveRight = CanMoveIntoEmpty(x + 1, y + 1); if (!canMoveLeft && !canMoveRight) { CacheBlockedSolid(coord, localX, localY); return false; } if (!CanSlipSolid(particle, ref seed, netForceX)) { RecordStalledMovable(); KeepChunkAwakeAt(x, y); return false; } if (leftFirst) { if (canMoveLeft && TryMoveEmpty(x, y, x - 1, y + 1)) { return true; } if (canMoveRight && TryMoveEmpty(x, y, x + 1, y + 1)) { return true; } } else { if (canMoveRight && TryMoveEmpty(x, y, x + 1, y + 1)) { return true; } if (canMoveLeft && TryMoveEmpty(x, y, x - 1, y + 1)) { return true; } } RecordStalledMovable(); KeepChunkAwakeAt(x, y); return false; } private bool TryMoveLiquid(int x, int y, PrototypeParticle particle, bool leftFirst, ref int seed, float netForceX) { if (TryMoveLiquidDown(x, y, particle)) { return true; } var windPreferred = netForceX > 0.2f ? 1 : netForceX < -0.2f ? -1 : 0; if (windPreferred != 0 && CanFlowLiquid(particle, ref seed, lateral: true, netForceX) && TryMoveEmpty(x, y, x + windPreferred, y)) { return true; } var diagonalAllowed = CanFlowLiquid(particle, ref seed, lateral: false, netForceX); var lateralAllowed = diagonalAllowed || CanFlowLiquid(particle, ref seed, lateral: true, netForceX); var openLeftDiagonal = CanMoveIntoEmpty(x - 1, y + 1); var openRightDiagonal = CanMoveIntoEmpty(x + 1, y + 1); var openLeftLateral = CanMoveIntoEmpty(x - 1, y); var openRightLateral = CanMoveIntoEmpty(x + 1, y); if (leftFirst) { if (diagonalAllowed && openLeftDiagonal && TryMoveEmpty(x, y, x - 1, y + 1)) { return true; } if (lateralAllowed && openLeftLateral && TryMoveEmpty(x, y, x - 1, y)) { return true; } if (diagonalAllowed && openRightDiagonal && TryMoveEmpty(x, y, x + 1, y + 1)) { return true; } if (lateralAllowed && openRightLateral && TryMoveEmpty(x, y, x + 1, y)) { return true; } } else { if (diagonalAllowed && openRightDiagonal && TryMoveEmpty(x, y, x + 1, y + 1)) { return true; } if (lateralAllowed && openRightLateral && TryMoveEmpty(x, y, x + 1, y)) { return true; } if (diagonalAllowed && openLeftDiagonal && TryMoveEmpty(x, y, x - 1, y + 1)) { return true; } if (lateralAllowed && openLeftLateral && TryMoveEmpty(x, y, x - 1, y)) { return true; } } if (openLeftDiagonal || openRightDiagonal || openLeftLateral || openRightLateral) { RecordStalledMovable(); KeepChunkAwakeAt(x, y); } return false; } private bool TryMoveGas(int x, int y, PrototypeParticle particle, bool leftFirst, ref int seed, float netForceX, float netForceY) { var suppressRiseAndDiagonal = TrySkipGasRetry(x, y); if (suppressRiseAndDiagonal && CanMoveIntoEmpty(x, y - 1)) { ClearGasRetry(x, y); suppressRiseAndDiagonal = false; } var openAbove = false; if (!suppressRiseAndDiagonal) { openAbove = CanMoveIntoEmpty(x, y - 1); if (openAbove) { ClearBlockedGasRise(x, y); ClearGasRetry(x, y); if (CanRiseGas(particle, ref seed, netForceY) && TryMoveEmpty(x, y, x, y - 1)) { return true; } } else if (!TrySkipBlockedGasRise(x, y) && InBounds(x, y - 1)) { CacheBlockedGasRise(x, y); } } var windPreferred = netForceX > 0.15f ? 1 : netForceX < -0.15f ? -1 : 0; if (windPreferred != 0 && CanDriftGas(particle, ref seed, netForceX, lateral: true) && TryMoveEmpty(x, y, x + windPreferred, y)) { ClearGasRetry(x, y); return true; } var diagonalAllowed = CanDriftGas(particle, ref seed, netForceX, lateral: false); var lateralAllowed = diagonalAllowed || CanDriftGas(particle, ref seed, netForceX, lateral: true); if (suppressRiseAndDiagonal) { diagonalAllowed = false; } var openLeftDiagonal = !suppressRiseAndDiagonal && CanMoveIntoEmpty(x - 1, y - 1); var openRightDiagonal = !suppressRiseAndDiagonal && CanMoveIntoEmpty(x + 1, y - 1); var openLeftLateral = CanMoveIntoEmpty(x - 1, y); var openRightLateral = CanMoveIntoEmpty(x + 1, y); if (leftFirst) { if (diagonalAllowed && openLeftDiagonal && TryMoveEmpty(x, y, x - 1, y - 1)) { ClearGasRetry(x, y); return true; } if (lateralAllowed && openLeftLateral && TryMoveEmpty(x, y, x - 1, y)) { ClearGasRetry(x, y); return true; } if (diagonalAllowed && openRightDiagonal && TryMoveEmpty(x, y, x + 1, y - 1)) { ClearGasRetry(x, y); return true; } if (lateralAllowed && openRightLateral && TryMoveEmpty(x, y, x + 1, y)) { ClearGasRetry(x, y); return true; } } else { if (diagonalAllowed && openRightDiagonal && TryMoveEmpty(x, y, x + 1, y - 1)) { ClearGasRetry(x, y); return true; } if (lateralAllowed && openRightLateral && TryMoveEmpty(x, y, x + 1, y)) { ClearGasRetry(x, y); return true; } if (diagonalAllowed && openLeftDiagonal && TryMoveEmpty(x, y, x - 1, y - 1)) { ClearGasRetry(x, y); return true; } if (lateralAllowed && openLeftLateral && TryMoveEmpty(x, y, x - 1, y)) { ClearGasRetry(x, y); return true; } } if (!openAbove && !openLeftDiagonal && !openRightDiagonal) { CacheGasRetry(x, y, (openLeftLateral || openRightLateral) ? 1 : 2); } else { ClearGasRetry(x, y); } if (openLeftDiagonal || openRightDiagonal || openLeftLateral || openRightLateral) { RecordStalledMovable(); KeepChunkAwakeAt(x, y); } return false; } private void MoveFire(int x, int y, bool leftFirst, ref int seed, float netForceX) { if (TryMoveEmpty(x, y, x, y - 1)) { return; } var preferredRight = netForceX > 0.05f ? true : netForceX < -0.05f ? false : !leftFirst; if (preferredRight) { if (TryMoveEmpty(x, y, x + 1, y - 1) || TryMoveEmpty(x, y, x + 1, y)) { return; } if (TryMoveEmpty(x, y, x - 1, y - 1) || TryMoveEmpty(x, y, x - 1, y)) { return; } } else { if (TryMoveEmpty(x, y, x - 1, y - 1) || TryMoveEmpty(x, y, x - 1, y)) { return; } if (TryMoveEmpty(x, y, x + 1, y - 1) || TryMoveEmpty(x, y, x + 1, y)) { return; } } if (NextChance(ref seed) <= 0.15f) { RemoveParticle(x, y); } } private void MoveEmber(int x, int y, PrototypeParticle particle, bool leftFirst, ref int seed, float netForceX) { if (NextChance(ref seed) <= MathF.Max(0.1f, particle.UpwardBias)) { if (TryMoveEmpty(x, y, x, y - 1)) { return; } if (leftFirst) { if (TryMoveEmpty(x, y, x - 1, y - 1) || TryMoveEmpty(x, y, x - 1, y)) { return; } } else if (TryMoveEmpty(x, y, x + 1, y - 1) || TryMoveEmpty(x, y, x + 1, y)) { return; } } TryMoveSolid(GetChunkCoord(x, y), GetLocalCoord(x, y).LocalX, GetLocalCoord(x, y).LocalY, x, y, particle, leftFirst, ref seed, netForceX); } private void MovePlasma(int x, int y, PrototypeParticle particle, bool leftFirst, ref int seed, float netForceX, float netForceY) { if (TryMoveEmpty(x, y, x, y - 1)) { return; } var lateralBias = MathF.Max(0.15f, particle.SideDriftBias); if (NextChance(ref seed) <= lateralBias) { var dir = netForceX > 0.05f ? 1 : netForceX < -0.05f ? -1 : leftFirst ? -1 : 1; if (TryMoveEmpty(x, y, x + dir, y - 1) || TryMoveEmpty(x, y, x + dir, y)) { return; } } TryMoveGas(x, y, particle, leftFirst, ref seed, netForceX, netForceY); } private bool TryMoveSolidDown(int x, int y, PrototypeParticle particle) { if (TryMoveEmpty(x, y, x, y + 1)) { return true; } if (!TryGetParticle(x, y + 1, out var target)) { return false; } if ((target.Kind is ParticleKind.Liquid or ParticleKind.Gas) && particle.Mass > target.Mass) { RecordSwapAttempt(); return SwapParticles(x, y, x, y + 1); } return false; } private bool TryMoveLiquidDown(int x, int y, PrototypeParticle particle) { if (TryMoveEmpty(x, y, x, y + 1)) { return true; } if (!TryGetParticle(x, y + 1, out var target)) { return false; } if (target.Kind == ParticleKind.Gas && particle.Mass > target.Mass) { RecordSwapAttempt(); return SwapParticles(x, y, x, y + 1); } if (particle.IsMolten && target.Kind == ParticleKind.Liquid && particle.Mass > target.Mass) { RecordSwapAttempt(); return SwapParticles(x, y, x, y + 1); } return false; } private bool TryMoveEmpty(int fromX, int fromY, int toX, int toY) { RecordMoveAttempt(toX - fromX, toY - fromY); if (!InBounds(toX, toY)) { return false; } if (TryGetCellPage(fromX, fromY, out var sourceCoord, out var sourcePage, out var sourceLocalX, out var sourceLocalY)) { var destinationCoord = GetChunkCoord(toX, toY); if (destinationCoord == sourceCoord) { var (destinationLocalX, destinationLocalY) = GetLocalCoord(toX, toY); if (!sourcePage.IsOccupied(destinationLocalX, destinationLocalY)) { return MoveParticleWithinPage(sourceCoord, sourcePage, sourceLocalX, sourceLocalY, destinationLocalX, destinationLocalY, fromX, fromY, toX, toY); } return false; } } if (!CanMoveIntoEmpty(toX, toY)) { return false; } return MoveParticle(fromX, fromY, toX, toY); } private bool CanMoveIntoEmpty(int x, int y) => InBounds(x, y) && !HasParticle(x, y); private int ResolveHorizontalPreference(ChunkCoord coord, int localX, int localY, PrototypeParticle particle, float netForceX) { if (netForceX > 0.15f) { return 1; } if (netForceX < -0.15f) { return -1; } var page = _cellPages[coord]; var driftState = page.GetDriftState(localX, localY); if (driftState != 0) { return driftState; } return PreferLeft(unchecked((coord.X * 92821) + (coord.Y * 68917) + (localX * 31) + (localY * 131) + _stepCounter + particle.TypeId)) ? -1 : 1; } private (float X, float Y) GetNetForce(int x, int y, PrototypeParticle particle) { if (!TryGetFieldPage(x, y, out _, out var page, out var localX, out var localY)) { var gradientXOnly = SamplePressure(x - 1, y) - SamplePressure(x + 1, y); var gradientYOnly = SamplePressure(x, y + 1) - SamplePressure(x, y - 1); return (gradientXOnly * 0.12f * particle.PressureResponse, gradientYOnly * 0.08f * particle.PressureResponse); } var cell = page.GetCell(localX, localY); var forceX = cell.WindX + cell.ForceX; var forceY = cell.WindY + cell.ForceY; var pressureGradientX = SamplePressure(x - 1, y) - SamplePressure(x + 1, y); var pressureGradientY = SamplePressure(x, y + 1) - SamplePressure(x, y - 1); forceX += pressureGradientX * 0.12f * particle.PressureResponse; forceY += pressureGradientY * 0.08f * particle.PressureResponse; if (particle.MotionType == PrototypeParticleType.Steam) { forceY -= 0.2f; } if (MathF.Abs(cell.Pressure) > 0.05f) { forceY += particle.MotionType == PrototypeParticleType.Steam ? -cell.Pressure * 0.05f * particle.PressureResponse : cell.Pressure * 0.05f * particle.PressureResponse; } return (forceX, forceY); } private static float GetEffectiveVelocity(PrototypeParticle particle) { if (particle.Velocity > 0f) { return particle.Velocity; } return particle.Kind switch { ParticleKind.Solid => 0.45f, ParticleKind.Liquid => 0.35f, ParticleKind.Gas => 0.25f, _ => 0.35f, }; } private static float GetForceResponse(PrototypeParticle particle) { var mobility = 0.35f + GetEffectiveVelocity(particle); var damping = MathF.Max(0.25f, (particle.Mass * 0.85f) + (particle.Friction * 0.35f) + (particle.Viscosity * 0.45f)); return Math.Clamp((mobility / damping) * particle.ForceResponseMultiplier, 0.15f, 2f); } private static bool CanSlipSolid(PrototypeParticle particle, ref int seed, float netForceX) { if (MathF.Abs(netForceX) > 0.45f) { return true; } var chance = Math.Clamp(0.2f + (GetEffectiveVelocity(particle) * 0.95f) - (particle.Friction * 0.35f), 0.1f, 0.95f); return NextChance(ref seed) <= chance; } private static bool CanFlowLiquid(PrototypeParticle particle, ref int seed, bool lateral, float netForceX) { var forceBonus = MathF.Min(0.25f, MathF.Abs(netForceX) * 0.08f); var velocity = GetEffectiveVelocity(particle); var baseChance = lateral ? 0.18f + (velocity * 0.95f) - (particle.Viscosity * 0.42f) - (particle.Friction * 0.08f) : 0.28f + (velocity * 0.9f) - (particle.Viscosity * 0.28f) - (particle.Friction * 0.05f); var flowScale = lateral ? particle.LateralFlowMultiplier : particle.DiagonalFlowMultiplier; var chance = Math.Clamp((baseChance * flowScale) + forceBonus, 0.03f, 0.98f); return NextChance(ref seed) <= chance; } private static bool CanRiseGas(PrototypeParticle particle, ref int seed, float netForceY) { if (netForceY < -0.65f) { return true; } var velocity = GetEffectiveVelocity(particle); var buoyancy = MathF.Max(0f, -netForceY) * 0.08f; var chance = Math.Clamp(0.1f + (velocity * 1.8f) - (particle.Viscosity * 0.22f) + buoyancy, 0.05f, 0.99f); return NextChance(ref seed) <= chance; } private static bool CanDriftGas(PrototypeParticle particle, ref int seed, float netForceX, bool lateral) { var forceBonus = MathF.Min(0.28f, MathF.Abs(netForceX) * 0.08f); var velocity = GetEffectiveVelocity(particle); var baseChance = lateral ? 0.18f + (velocity * 1.15f) - (particle.Viscosity * 0.1f) : 0.22f + (velocity * 1.2f) - (particle.Viscosity * 0.16f); var chance = Math.Clamp(baseChance + forceBonus - (particle.Friction * 0.04f), 0.05f, 0.99f); return NextChance(ref seed) <= chance; } private static sbyte GetDefaultDriftDirection(int x, int y) => PreferLeft(unchecked((x * 73856093) ^ (y * 19349663))) ? (sbyte)-1 : (sbyte)1; private bool TrySkipBlockedSolid(ChunkCoord coord, int localX, int localY, float netForceX) { if (MathF.Abs(netForceX) > 0.1f || localX <= 0 || localX >= _config.ChunkWidth - 1 || localY >= _config.ChunkHeight - 1 || !_cellPages.TryGetValue(coord, out var page)) { return false; } var signature = ComputeSolidBlockSignature(page, localY); return signature != 0 && page.GetBlockedSolidSignature(localX, localY) == signature; } private void CacheBlockedSolid(ChunkCoord coord, int localX, int localY) { if (localX <= 0 || localX >= _config.ChunkWidth - 1 || localY >= _config.ChunkHeight - 1) { return; } if (_cellPages.TryGetValue(coord, out var page)) { page.SetBlockedSolidSignature(localX, localY, ComputeSolidBlockSignature(page, localY)); } } private static int ComputeSolidBlockSignature(ChunkCellPage page, int localY) { if (localY < 0 || localY >= page.Height - 1) { return 0; } return HashCode.Combine(page.GetRowRevision(localY), page.GetRowRevision(localY + 1)); } private bool CanThrottleGasRuntime(int x, int y, PrototypeParticle particle, float temperature) { if (particle.Kind != ParticleKind.Gas || particle.HasLifetimeBehavior || particle.HasBurnBehavior || particle.HasEmissionBehavior || particle.HasSpecialBehavior || particle.HasReactionBehavior) { return false; } var requiresPressureSensitiveRuntime = particle.HasPressureBehavior || particle.HasPhaseBehavior; if (requiresPressureSensitiveRuntime && (GetPressureLoad(x, y) > 0.2f || GetPressureDurationAt(x, y) > 0f)) { return false; } if (MathF.Abs(temperature - _ambientTemperature) <= 0.5f) { return false; } if (HasThermallyActiveNeighbor(x, y, temperature, out var hasPassiveBoundaryNeighbor)) { return false; } if (hasPassiveBoundaryNeighbor) { return ((_stepCounter + (x * 3) + (y * 5)) & 3) != 0; } return true; } private bool HasThermallyActiveNeighbor(int x, int y, float localTemperature, out bool hasPassiveBoundaryNeighbor) { hasPassiveBoundaryNeighbor = false; Span<(int X, int Y)> neighbors = [(x - 1, y), (x + 1, y), (x, y - 1), (x, y + 1)]; for (var i = 0; i < neighbors.Length; i++) { var (nx, ny) = neighbors[i]; if (!TryGetParticle(nx, ny, out var neighbor)) { continue; } var neighborTemperature = GetTemperatureAt(nx, ny); if (MathF.Abs(neighborTemperature - localTemperature) > 8f) { return true; } if (neighbor.HasBurnBehavior || neighbor.HasEmissionBehavior || neighbor.HasSpecialBehavior || neighbor.HasFlag(PrototypeParticleFlags.HotSource)) { return true; } if (neighbor.HasReactionBehavior || neighbor.HasPhaseBehavior || neighbor.HasPressureBehavior) { return true; } if (neighbor.Kind == ParticleKind.Gas) { continue; } if (neighbor.IsStatic || neighbor.MotionType == PrototypeParticleType.Wall) { hasPassiveBoundaryNeighbor = true; continue; } if (neighbor.Conductivity > 0.25f || MathF.Abs(neighborTemperature - localTemperature) > 24f) { hasPassiveBoundaryNeighbor = true; continue; } } return false; } private bool TrySkipBlockedGasRise(int x, int y) { if (!TryGetCellPage(x, y, out _, out var page, out var localX, out var localY) || localY <= 0) { return false; } if (!page.IsOccupied(localX, localY - 1)) { return false; } var signature = ComputeGasRiseBlockSignature(page, localX, localY); return signature != 0 && page.GetBlockedGasRiseSignature(localX, localY) == signature; } private void CacheBlockedGasRise(int x, int y) { if (!TryGetCellPage(x, y, out _, out var page, out var localX, out var localY) || localY <= 0) { return; } if (!page.IsOccupied(localX, localY - 1)) { page.SetBlockedGasRiseSignature(localX, localY, 0); return; } page.SetBlockedGasRiseSignature(localX, localY, ComputeGasRiseBlockSignature(page, localX, localY)); } private void ClearBlockedGasRise(int x, int y) { if (!TryGetCellPage(x, y, out _, out var page, out var localX, out var localY)) { return; } page.SetBlockedGasRiseSignature(localX, localY, 0); } private static int ComputeGasRiseBlockSignature(ChunkCellPage page, int localX, int localY) { if (localY <= 0 || localY >= page.Height) { return 0; } return HashCode.Combine(localX, page.GetRowRevision(localY), page.GetRowRevision(localY - 1)); } private bool TrySkipGasRetry(int x, int y) { if (!TryGetCellPage(x, y, out _, out var page, out var localX, out var localY)) { return false; } var signature = ComputeGasRetrySignature(page, localX, localY); if (signature == 0) { page.ClearGasRetryState(localX, localY); return false; } if (page.GetGasRetryUntilStep(localX, localY) > _stepCounter && page.GetGasRetrySignature(localX, localY) == signature) { return true; } if (page.GetGasRetrySignature(localX, localY) != signature) { page.ClearGasRetryState(localX, localY); } return false; } private void CacheGasRetry(int x, int y, int cooldownSteps) { if (!TryGetCellPage(x, y, out _, out var page, out var localX, out var localY)) { return; } var signature = ComputeGasRetrySignature(page, localX, localY); if (signature == 0) { page.ClearGasRetryState(localX, localY); return; } page.SetGasRetryState(localX, localY, signature, _stepCounter + Math.Max(1, cooldownSteps)); } private void ClearGasRetry(int x, int y) { if (!TryGetCellPage(x, y, out _, out var page, out var localX, out var localY)) { return; } page.ClearGasRetryState(localX, localY); } private static int ComputeGasRetrySignature(ChunkCellPage page, int localX, int localY) { if (localY <= 0 || localY >= page.Height) { return 0; } return HashCode.Combine( localX, page.GetRowRevision(localY), page.GetRowRevision(localY - 1), page.GetOccupiedRowCount(localY), page.GetOccupiedRowCount(localY - 1)); } private static bool PreferLeft(int seed) => ((seed ^ 0x5bd1e995) & 1) == 0; private static float NextChance(ref int seed) { seed = unchecked((seed * 1103515245) + 12345); return ((seed >> 8) & 0x00FFFFFF) / 16777215f; } private static sbyte ResolveDriftAfterMove(int fromX, int toX, sbyte currentDrift) { if (toX > fromX) { return 1; } if (toX < fromX) { return -1; } return currentDrift; } private static PrototypeParticle CreateLegacyParticle(PrototypeParticleType type) => type switch { PrototypeParticleType.Sand => new PrototypeParticle(1, "sand", type, ParticleKind.Solid, ParticleBehaviorKind.None, 214, 188, 96, 1.4f, 0.65f, 0.18f, 0.08f), PrototypeParticleType.Water => new PrototypeParticle(2, "water", type, ParticleKind.Liquid, ParticleBehaviorKind.None, 72, 132, 232, 1.0f, 0.55f, 0.04f, 0.12f, Flags: PrototypeParticleFlags.WaterLike), PrototypeParticleType.Steam => new PrototypeParticle(3, "steam", type, ParticleKind.Gas, ParticleBehaviorKind.None, 182, 196, 214, 0.2f, 0.7f, 0.01f, 0.03f, SolidifyTypeId: 2, PressureThreshold: 1.2f, InitialTemperature: 110f), PrototypeParticleType.Wall => new PrototypeParticle(4, "wall", type, ParticleKind.Solid, ParticleBehaviorKind.None, 96, 96, 104, 100f, 0f, 1f, 1f, IsStatic: true), _ => default, }; private void KeepChunkAwakeAt(int x, int y) { if (!TryGetCellPage(x, y, out _, out var page, out var localX, out var localY)) { return; } page.IsActive = true; page.LastTouchedFrame = _stepCounter; page.RequestFutureSteps(2); page.MarkDirtyBand(localX, localY); } }