sandpypi/Sand.ChunkPrototype/PrototypeSparseSandAdapter.Movement.cs

1062 lines
33 KiB
C#

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);
}
}