minecraft-src/net/minecraft/world/level/levelgen/Aquifer.java
2025-07-04 02:00:41 +03:00

499 lines
17 KiB
Java

package net.minecraft.world.level.levelgen;
import java.util.Arrays;
import net.minecraft.core.BlockPos;
import net.minecraft.core.SectionPos;
import net.minecraft.util.Mth;
import net.minecraft.util.RandomSource;
import net.minecraft.world.level.ChunkPos;
import net.minecraft.world.level.biome.OverworldBiomeBuilder;
import net.minecraft.world.level.block.Blocks;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.dimension.DimensionType;
import net.minecraft.world.level.levelgen.DensityFunction.FunctionContext;
import net.minecraft.world.level.levelgen.DensityFunction.SinglePointContext;
import org.apache.commons.lang3.mutable.MutableDouble;
import org.jetbrains.annotations.Nullable;
/**
* Aquifers are responsible for non-sea level fluids found in terrain generation, but also managing that different aquifers don't intersect with each other in ways that would create undesirable fluid placement.
* The aquifer interface itself is a modifier on a per-block basis. It computes a block state to be placed for each position in the world.
* <p>
* Aquifers work by first partitioning a single chunk into a low resolution grid. They then generate, via various noise layers, an {@link NoiseBasedAquifer.AquiferStatus} at each grid point.
* At each point, the grid cell containing that point is calculated, and then of the eight grid corners, the three closest aquifers are found, by square euclidean distance.
* Borders between aquifers are created by comparing nearby aquifers to see if the given point is near-equidistant from them, indicating a border if so, or fluid/air depending on the aquifer height if not.
*/
public interface Aquifer {
/**
* Creates a standard noise based aquifer. This aquifer will place liquid (both water and lava), air, and stone as described above.
*/
static Aquifer create(
NoiseChunk chunk,
ChunkPos chunkPos,
NoiseRouter noiseRouter,
PositionalRandomFactory positionalRandomFactory,
int minY,
int height,
Aquifer.FluidPicker globalFluidPicker
) {
return new Aquifer.NoiseBasedAquifer(chunk, chunkPos, noiseRouter, positionalRandomFactory, minY, height, globalFluidPicker);
}
/**
* Creates a disabled, or no-op aquifer. This will fill any open areas below sea level with the default fluid.
*/
static Aquifer createDisabled(Aquifer.FluidPicker defaultFluid) {
return new Aquifer() {
@Nullable
@Override
public BlockState computeSubstance(FunctionContext context, double substance) {
return substance > 0.0 ? null : defaultFluid.computeFluid(context.blockX(), context.blockY(), context.blockZ()).at(context.blockY());
}
@Override
public boolean shouldScheduleFluidUpdate() {
return false;
}
};
}
@Nullable
BlockState computeSubstance(FunctionContext context, double substance);
/**
* Returns {@code true} if there should be a fluid update scheduled - due to a fluid block being placed in a possibly unsteady position - at the last position passed into {@link #computeState}.
* This <strong>must</strong> be invoked only after {@link #computeState}, and will be using the same parameters as that method.
*/
boolean shouldScheduleFluidUpdate();
public interface FluidPicker {
Aquifer.FluidStatus computeFluid(int i, int j, int k);
}
public record FluidStatus(int fluidLevel, BlockState fluidType) {
public BlockState at(int y) {
return y < this.fluidLevel ? this.fluidType : Blocks.AIR.defaultBlockState();
}
}
public static class NoiseBasedAquifer implements Aquifer {
private static final int X_RANGE = 10;
private static final int Y_RANGE = 9;
private static final int Z_RANGE = 10;
private static final int X_SEPARATION = 6;
private static final int Y_SEPARATION = 3;
private static final int Z_SEPARATION = 6;
private static final int X_SPACING = 16;
private static final int Y_SPACING = 12;
private static final int Z_SPACING = 16;
private static final int MAX_REASONABLE_DISTANCE_TO_AQUIFER_CENTER = 11;
private static final double FLOWING_UPDATE_SIMULARITY = similarity(Mth.square(10), Mth.square(12));
private final NoiseChunk noiseChunk;
private final DensityFunction barrierNoise;
private final DensityFunction fluidLevelFloodednessNoise;
private final DensityFunction fluidLevelSpreadNoise;
private final DensityFunction lavaNoise;
private final PositionalRandomFactory positionalRandomFactory;
private final Aquifer.FluidStatus[] aquiferCache;
private final long[] aquiferLocationCache;
private final Aquifer.FluidPicker globalFluidPicker;
private final DensityFunction erosion;
private final DensityFunction depth;
private boolean shouldScheduleFluidUpdate;
private final int minGridX;
private final int minGridY;
private final int minGridZ;
private final int gridSizeX;
private final int gridSizeZ;
private static final int[][] SURFACE_SAMPLING_OFFSETS_IN_CHUNKS = new int[][]{
{0, 0}, {-2, -1}, {-1, -1}, {0, -1}, {1, -1}, {-3, 0}, {-2, 0}, {-1, 0}, {1, 0}, {-2, 1}, {-1, 1}, {0, 1}, {1, 1}
};
NoiseBasedAquifer(
NoiseChunk noiseChunk,
ChunkPos chunkPos,
NoiseRouter noiseRouter,
PositionalRandomFactory positionalRandomFactory,
int minY,
int height,
Aquifer.FluidPicker globalFluidPicker
) {
this.noiseChunk = noiseChunk;
this.barrierNoise = noiseRouter.barrierNoise();
this.fluidLevelFloodednessNoise = noiseRouter.fluidLevelFloodednessNoise();
this.fluidLevelSpreadNoise = noiseRouter.fluidLevelSpreadNoise();
this.lavaNoise = noiseRouter.lavaNoise();
this.erosion = noiseRouter.erosion();
this.depth = noiseRouter.depth();
this.positionalRandomFactory = positionalRandomFactory;
this.minGridX = this.gridX(chunkPos.getMinBlockX()) - 1;
this.globalFluidPicker = globalFluidPicker;
int i = this.gridX(chunkPos.getMaxBlockX()) + 1;
this.gridSizeX = i - this.minGridX + 1;
this.minGridY = this.gridY(minY) - 1;
int j = this.gridY(minY + height) + 1;
int k = j - this.minGridY + 1;
this.minGridZ = this.gridZ(chunkPos.getMinBlockZ()) - 1;
int l = this.gridZ(chunkPos.getMaxBlockZ()) + 1;
this.gridSizeZ = l - this.minGridZ + 1;
int m = this.gridSizeX * k * this.gridSizeZ;
this.aquiferCache = new Aquifer.FluidStatus[m];
this.aquiferLocationCache = new long[m];
Arrays.fill(this.aquiferLocationCache, Long.MAX_VALUE);
}
/**
* @return A cache index based on grid positions.
*/
private int getIndex(int gridX, int gridY, int gridZ) {
int i = gridX - this.minGridX;
int j = gridY - this.minGridY;
int k = gridZ - this.minGridZ;
return (j * this.gridSizeZ + k) * this.gridSizeX + i;
}
@Nullable
@Override
public BlockState computeSubstance(FunctionContext context, double substance) {
int i = context.blockX();
int j = context.blockY();
int k = context.blockZ();
if (substance > 0.0) {
this.shouldScheduleFluidUpdate = false;
return null;
} else {
Aquifer.FluidStatus fluidStatus = this.globalFluidPicker.computeFluid(i, j, k);
if (fluidStatus.at(j).is(Blocks.LAVA)) {
this.shouldScheduleFluidUpdate = false;
return Blocks.LAVA.defaultBlockState();
} else {
int l = Math.floorDiv(i - 5, 16);
int m = Math.floorDiv(j + 1, 12);
int n = Math.floorDiv(k - 5, 16);
int o = Integer.MAX_VALUE;
int p = Integer.MAX_VALUE;
int q = Integer.MAX_VALUE;
int r = Integer.MAX_VALUE;
long s = 0L;
long t = 0L;
long u = 0L;
long v = 0L;
for (int w = 0; w <= 1; w++) {
for (int x = -1; x <= 1; x++) {
for (int y = 0; y <= 1; y++) {
int z = l + w;
int aa = m + x;
int ab = n + y;
int ac = this.getIndex(z, aa, ab);
long ad = this.aquiferLocationCache[ac];
long ae;
if (ad != Long.MAX_VALUE) {
ae = ad;
} else {
RandomSource randomSource = this.positionalRandomFactory.at(z, aa, ab);
ae = BlockPos.asLong(z * 16 + randomSource.nextInt(10), aa * 12 + randomSource.nextInt(9), ab * 16 + randomSource.nextInt(10));
this.aquiferLocationCache[ac] = ae;
}
int af = BlockPos.getX(ae) - i;
int ag = BlockPos.getY(ae) - j;
int ah = BlockPos.getZ(ae) - k;
int ai = af * af + ag * ag + ah * ah;
if (o >= ai) {
v = u;
u = t;
t = s;
s = ae;
r = q;
q = p;
p = o;
o = ai;
} else if (p >= ai) {
v = u;
u = t;
t = ae;
r = q;
q = p;
p = ai;
} else if (q >= ai) {
v = u;
u = ae;
r = q;
q = ai;
} else if (r >= ai) {
v = ae;
r = ai;
}
}
}
}
Aquifer.FluidStatus fluidStatus2 = this.getAquiferStatus(s);
double d = similarity(o, p);
BlockState blockState = fluidStatus2.at(j);
if (d <= 0.0) {
if (d >= FLOWING_UPDATE_SIMULARITY) {
Aquifer.FluidStatus fluidStatus3 = this.getAquiferStatus(t);
this.shouldScheduleFluidUpdate = !fluidStatus2.equals(fluidStatus3);
} else {
this.shouldScheduleFluidUpdate = false;
}
return blockState;
} else if (blockState.is(Blocks.WATER) && this.globalFluidPicker.computeFluid(i, j - 1, k).at(j - 1).is(Blocks.LAVA)) {
this.shouldScheduleFluidUpdate = true;
return blockState;
} else {
MutableDouble mutableDouble = new MutableDouble(Double.NaN);
Aquifer.FluidStatus fluidStatus4 = this.getAquiferStatus(t);
double e = d * this.calculatePressure(context, mutableDouble, fluidStatus2, fluidStatus4);
if (substance + e > 0.0) {
this.shouldScheduleFluidUpdate = false;
return null;
} else {
Aquifer.FluidStatus fluidStatus5 = this.getAquiferStatus(u);
double f = similarity(o, q);
if (f > 0.0) {
double g = d * f * this.calculatePressure(context, mutableDouble, fluidStatus2, fluidStatus5);
if (substance + g > 0.0) {
this.shouldScheduleFluidUpdate = false;
return null;
}
}
double g = similarity(p, q);
if (g > 0.0) {
double h = d * g * this.calculatePressure(context, mutableDouble, fluidStatus4, fluidStatus5);
if (substance + h > 0.0) {
this.shouldScheduleFluidUpdate = false;
return null;
}
}
boolean bl = !fluidStatus2.equals(fluidStatus4);
boolean bl2 = g >= FLOWING_UPDATE_SIMULARITY && !fluidStatus4.equals(fluidStatus5);
boolean bl3 = f >= FLOWING_UPDATE_SIMULARITY && !fluidStatus2.equals(fluidStatus5);
if (!bl && !bl2 && !bl3) {
this.shouldScheduleFluidUpdate = f >= FLOWING_UPDATE_SIMULARITY
&& similarity(o, r) >= FLOWING_UPDATE_SIMULARITY
&& !fluidStatus2.equals(this.getAquiferStatus(v));
} else {
this.shouldScheduleFluidUpdate = true;
}
return blockState;
}
}
}
}
}
@Override
public boolean shouldScheduleFluidUpdate() {
return this.shouldScheduleFluidUpdate;
}
/**
* Compares two distances (between aquifers).
* @return {@code 1.0} if the distances are equal, and returns smaller values the more different in absolute value the two distances are.
*/
private static double similarity(int firstDistance, int secondDistance) {
double d = 25.0;
return 1.0 - Math.abs(secondDistance - firstDistance) / 25.0;
}
private double calculatePressure(FunctionContext context, MutableDouble substance, Aquifer.FluidStatus firstFluid, Aquifer.FluidStatus secondFluid) {
int i = context.blockY();
BlockState blockState = firstFluid.at(i);
BlockState blockState2 = secondFluid.at(i);
if ((!blockState.is(Blocks.LAVA) || !blockState2.is(Blocks.WATER)) && (!blockState.is(Blocks.WATER) || !blockState2.is(Blocks.LAVA))) {
int j = Math.abs(firstFluid.fluidLevel - secondFluid.fluidLevel);
if (j == 0) {
return 0.0;
} else {
double d = 0.5 * (firstFluid.fluidLevel + secondFluid.fluidLevel);
double e = i + 0.5 - d;
double f = j / 2.0;
double g = 0.0;
double h = 2.5;
double k = 1.5;
double l = 3.0;
double m = 10.0;
double n = 3.0;
double o = f - Math.abs(e);
double q;
if (e > 0.0) {
double p = 0.0 + o;
if (p > 0.0) {
q = p / 1.5;
} else {
q = p / 2.5;
}
} else {
double p = 3.0 + o;
if (p > 0.0) {
q = p / 3.0;
} else {
q = p / 10.0;
}
}
double px = 2.0;
double r;
if (!(q < -2.0) && !(q > 2.0)) {
double s = substance.getValue();
if (Double.isNaN(s)) {
double t = this.barrierNoise.compute(context);
substance.setValue(t);
r = t;
} else {
r = s;
}
} else {
r = 0.0;
}
return 2.0 * (r + q);
}
} else {
return 2.0;
}
}
private int gridX(int x) {
return Math.floorDiv(x, 16);
}
private int gridY(int y) {
return Math.floorDiv(y, 12);
}
private int gridZ(int z) {
return Math.floorDiv(z, 16);
}
/**
* Calculates the aquifer at a given location. Internally references a cache using the grid positions as an index. If the cache is not populated, computes a new aquifer at that grid location using {@link #computeFluid}.
*
* @param packedPos The aquifer block position, packed into a {@code long}.
*/
private Aquifer.FluidStatus getAquiferStatus(long packedPos) {
int i = BlockPos.getX(packedPos);
int j = BlockPos.getY(packedPos);
int k = BlockPos.getZ(packedPos);
int l = this.gridX(i);
int m = this.gridY(j);
int n = this.gridZ(k);
int o = this.getIndex(l, m, n);
Aquifer.FluidStatus fluidStatus = this.aquiferCache[o];
if (fluidStatus != null) {
return fluidStatus;
} else {
Aquifer.FluidStatus fluidStatus2 = this.computeFluid(i, j, k);
this.aquiferCache[o] = fluidStatus2;
return fluidStatus2;
}
}
private Aquifer.FluidStatus computeFluid(int x, int y, int z) {
Aquifer.FluidStatus fluidStatus = this.globalFluidPicker.computeFluid(x, y, z);
int i = Integer.MAX_VALUE;
int j = y + 12;
int k = y - 12;
boolean bl = false;
for (int[] is : SURFACE_SAMPLING_OFFSETS_IN_CHUNKS) {
int l = x + SectionPos.sectionToBlockCoord(is[0]);
int m = z + SectionPos.sectionToBlockCoord(is[1]);
int n = this.noiseChunk.preliminarySurfaceLevel(l, m);
int o = n + 8;
boolean bl2 = is[0] == 0 && is[1] == 0;
if (bl2 && k > o) {
return fluidStatus;
}
boolean bl3 = j > o;
if (bl3 || bl2) {
Aquifer.FluidStatus fluidStatus2 = this.globalFluidPicker.computeFluid(l, o, m);
if (!fluidStatus2.at(o).isAir()) {
if (bl2) {
bl = true;
}
if (bl3) {
return fluidStatus2;
}
}
}
i = Math.min(i, n);
}
int p = this.computeSurfaceLevel(x, y, z, fluidStatus, i, bl);
return new Aquifer.FluidStatus(p, this.computeFluidType(x, y, z, fluidStatus, p));
}
private int computeSurfaceLevel(int x, int y, int z, Aquifer.FluidStatus fluidStatus, int maxSurfaceLevel, boolean fluidPresent) {
SinglePointContext singlePointContext = new SinglePointContext(x, y, z);
double d;
double e;
if (OverworldBiomeBuilder.isDeepDarkRegion(this.erosion, this.depth, singlePointContext)) {
d = -1.0;
e = -1.0;
} else {
int i = maxSurfaceLevel + 8 - y;
int j = 64;
double f = fluidPresent ? Mth.clampedMap((double)i, 0.0, 64.0, 1.0, 0.0) : 0.0;
double g = Mth.clamp(this.fluidLevelFloodednessNoise.compute(singlePointContext), -1.0, 1.0);
double h = Mth.map(f, 1.0, 0.0, -0.3, 0.8);
double k = Mth.map(f, 1.0, 0.0, -0.8, 0.4);
d = g - k;
e = g - h;
}
int i;
if (e > 0.0) {
i = fluidStatus.fluidLevel;
} else if (d > 0.0) {
i = this.computeRandomizedFluidSurfaceLevel(x, y, z, maxSurfaceLevel);
} else {
i = DimensionType.WAY_BELOW_MIN_Y;
}
return i;
}
private int computeRandomizedFluidSurfaceLevel(int x, int y, int z, int maxSurfaceLevel) {
int i = 16;
int j = 40;
int k = Math.floorDiv(x, 16);
int l = Math.floorDiv(y, 40);
int m = Math.floorDiv(z, 16);
int n = l * 40 + 20;
int o = 10;
double d = this.fluidLevelSpreadNoise.compute(new SinglePointContext(k, l, m)) * 10.0;
int p = Mth.quantize(d, 3);
int q = n + p;
return Math.min(maxSurfaceLevel, q);
}
private BlockState computeFluidType(int x, int y, int z, Aquifer.FluidStatus fluidStatus, int surfaceLevel) {
BlockState blockState = fluidStatus.fluidType;
if (surfaceLevel <= -10 && surfaceLevel != DimensionType.WAY_BELOW_MIN_Y && fluidStatus.fluidType != Blocks.LAVA.defaultBlockState()) {
int i = 64;
int j = 40;
int k = Math.floorDiv(x, 64);
int l = Math.floorDiv(y, 40);
int m = Math.floorDiv(z, 64);
double d = this.lavaNoise.compute(new SinglePointContext(k, l, m));
if (Math.abs(d) > 0.3) {
blockState = Blocks.LAVA.defaultBlockState();
}
}
return blockState;
}
}
}