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. *
* 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 must 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; } } }