Transition water caps

- Saved and blended columns find solid layers that cap water at the water head
- Surface search passes over a water cap to the ground below it
- A capped column keeps its fluid height at the top of the cap
- A painted cap counts only where the blended column keeps the same blocks
- Saved terrain sources report their water surface
- Displacement fluid keys come from the first fluid at or below the fluid height
- Empty fluid keys never win the fluid blend
This commit is contained in:
Brian Neumann-Fopiano committed 2026-10-31 19:23:25 -04:00
1 parent c2eca3e6bd
commit 2462330787
11 files changed
+533 -58

No files matched your search

@@ -43,14 +43,14 @@ public final class IrisTransitionGeometryActuator extends EngineAssignedComponen
if (plan == null || !plan.hasTransitionAtChunk(x >> 4, z >> 4)) { if (plan == null || !plan.hasTransitionAtChunk(x >> 4, z >> 4)) {
return; return;
} }
GeometrySource source = new GeometrySource(blocks, biomes, getEngine().getMinHeight(), context); GeometrySource source = new GeometrySource(x, z, blocks, biomes, getEngine().getMinHeight(), context);
blendGeometry(x, z, blocks, context, source, plan); blendGeometry(x, z, blocks, context, source, plan);
} }
public static SavedTerrainChunk capture(int x, int z, Hunk<NativeBlockState> blocks, public static SavedTerrainChunk capture(int x, int z, Hunk<NativeBlockState> blocks,
Hunk<NativeBiome> biomes, int minimumY, Hunk<NativeBiome> biomes, int minimumY,
ChunkContext context, boolean boundaryOnly) throws IOException { ChunkContext context, boolean boundaryOnly) throws IOException {
GeometrySource source = new GeometrySource(blocks, biomes, minimumY, context); GeometrySource source = new GeometrySource(x, z, blocks, biomes, minimumY, context);
return boundaryOnly return boundaryOnly
? SavedTerrainChunk.captureBoundary(x >> 4, z >> 4, minimumY, blocks.getHeight(), "minecraft:noise", source) ? SavedTerrainChunk.captureBoundary(x >> 4, z >> 4, minimumY, blocks.getHeight(), "minecraft:noise", source)
: SavedTerrainChunk.capture(x >> 4, z >> 4, minimumY, blocks.getHeight(), "minecraft:noise", source); : SavedTerrainChunk.capture(x >> 4, z >> 4, minimumY, blocks.getHeight(), "minecraft:noise", source);
@@ -74,8 +74,10 @@ public final class IrisTransitionGeometryActuator extends EngineAssignedComponen
if (blended == current) { if (blended == current) {
continue; continue;
} }
double expectedFloor = context.getRoundedHeight(localX, localZ); int ground = context.getRoundedHeight(localX, localZ);
int floor = blended.surfaceOffsetNear(expectedFloor); BoundaryColumnGeometry.WaterCap cap = paintedCap(current, blended, ground,
waterHead(complex, x + localX, z + localZ, current.height()));
int floor = blended.surfaceOffsetNear(ground, cap);
List<BoundaryColumnGeometry.Voxel> voxels = blended.voxels(); List<BoundaryColumnGeometry.Voxel> voxels = blended.voxels();
for (int offset = 0; offset < voxels.size(); offset++) { for (int offset = 0; offset < voxels.size(); offset++) {
BoundaryColumnGeometry.Voxel voxel = voxels.get(offset); BoundaryColumnGeometry.Voxel voxel = voxels.get(offset);
@@ -95,6 +97,9 @@ public final class IrisTransitionGeometryActuator extends EngineAssignedComponen
&& voxels.get(fluidHeight + 1).phase() == BoundaryColumnGeometry.Phase.FLUID) { && voxels.get(fluidHeight + 1).phase() == BoundaryColumnGeometry.Phase.FLUID) {
fluidHeight++; fluidHeight++;
} }
if (blended.capsWater(cap, floor)) {
fluidHeight = cap.top();
}
if (fluidHeight == floor) { if (fluidHeight == floor) {
fluidHeight = -1; fluidHeight = -1;
} }
@@ -105,7 +110,29 @@ public final class IrisTransitionGeometryActuator extends EngineAssignedComponen
} }
} }
private static BoundaryColumnGeometry.WaterCap paintedCap(BoundaryColumnGeometry painted, BoundaryColumnGeometry blended,
int ground, int waterHead) {
BoundaryColumnGeometry.WaterCap cap = painted.waterCap(ground, waterHead);
if (!cap.present()) {
return cap;
}
int top = Math.min(cap.top() + 1, painted.height() - 1);
for (int offset = cap.bottom(); offset <= top; offset++) {
int y = painted.minimumY() + offset;
if (!painted.voxelAt(y).equals(blended.voxelAt(y))) {
return BoundaryColumnGeometry.WaterCap.none();
}
}
return cap;
}
private static int waterHead(IrisComplex complex, int blockX, int blockZ, int height) {
return Math.clamp(Math.round(complex.getWaterHeadStream().getDouble(blockX, blockZ)), -1, height - 1);
}
private static final class GeometrySource implements SavedTerrainChunk.VoxelSource { private static final class GeometrySource implements SavedTerrainChunk.VoxelSource {
private final int originX;
private final int originZ;
private final Hunk<NativeBlockState> blocks; private final Hunk<NativeBlockState> blocks;
private final Hunk<NativeBiome> biomes; private final Hunk<NativeBiome> biomes;
private final int minimumY; private final int minimumY;
@@ -116,7 +143,10 @@ public final class IrisTransitionGeometryActuator extends EngineAssignedComponen
private NativeBlockState lastState; private NativeBlockState lastState;
private BoundaryColumnGeometry.Voxel lastVoxel; private BoundaryColumnGeometry.Voxel lastVoxel;
private GeometrySource(Hunk<NativeBlockState> blocks, Hunk<NativeBiome> biomes, int minimumY, ChunkContext context) { private GeometrySource(int originX, int originZ, Hunk<NativeBlockState> blocks, Hunk<NativeBiome> biomes,
int minimumY, ChunkContext context) {
this.originX = originX;
this.originZ = originZ;
this.blocks = blocks; this.blocks = blocks;
this.biomes = biomes; this.biomes = biomes;
this.minimumY = minimumY; this.minimumY = minimumY;
@@ -178,6 +208,11 @@ public final class IrisTransitionGeometryActuator extends EngineAssignedComponen
return OptionalInt.of(Math.max(0, Math.min(blocks.getHeight() - 1, context.getRoundedHeight(localX, localZ)))); return OptionalInt.of(Math.max(0, Math.min(blocks.getHeight() - 1, context.getRoundedHeight(localX, localZ))));
} }
@Override
public OptionalInt waterSurface(int localX, int localZ) {
return OptionalInt.of(waterHead(context.getComplex(), originX + localX, originZ + localZ, blocks.getHeight()));
}
@Override @Override
public String biome(int localX, int worldY, int localZ) throws IOException { public String biome(int localX, int worldY, int localZ) throws IOException {
NativeBiome biome = biomes.getRaw(localX, worldY - minimumY, localZ); NativeBiome biome = biomes.getRaw(localX, worldY - minimumY, localZ);
@@ -139,33 +139,73 @@ public final class BoundaryColumnGeometry implements NativeBlockColumn {
return List.copyOf(values); return List.copyOf(values);
} }
public int surfaceOffsetNear(double expectedOffset) { public WaterCap waterCap(int groundOffset, int waterSurfaceOffset) {
if (waterSurfaceOffset <= groundOffset || waterSurfaceOffset < 0 || waterSurfaceOffset >= height()
|| waterSurfaceOffset + 1 < height() && voxelAtOffset(waterSurfaceOffset + 1).phase() != Phase.AIR) {
return WaterCap.none();
}
int lowest = Math.max(0, groundOffset + 1);
int bottom = waterSurfaceOffset + 1;
for (int run = runAt(waterSurfaceOffset); run >= 0 && bottom > lowest && isCapMaterial(runVoxel(run)); run--) {
bottom = Math.max(lowest, run == 0 ? 0 : runEnds[run - 1]);
}
return bottom > waterSurfaceOffset ? WaterCap.none() : new WaterCap(bottom, waterSurfaceOffset);
}
public boolean capsWater(WaterCap cap, int floorOffset) {
if (!cap.present() || cap.top() <= floorOffset) {
return false;
}
int below = cap.bottom() - 1;
return below == floorOffset || below >= 0 && !voxelAtOffset(below).fluidStateKey().isEmpty();
}
public String fluidStateKeyAtOrBelow(int surfaceOffset, int floorOffset) {
for (int offset = Math.min(surfaceOffset, height() - 1); offset > floorOffset && offset >= 0; offset--) {
String fluidStateKey = voxelAtOffset(offset).fluidStateKey();
if (!fluidStateKey.isEmpty()) {
return fluidStateKey;
}
}
return "";
}
public int surfaceOffsetNear(double expectedOffset, WaterCap cap) {
Objects.requireNonNull(cap, "Water cap");
if (!Double.isFinite(expectedOffset) || height() == 0) { if (!Double.isFinite(expectedOffset) || height() == 0) {
throw new IllegalArgumentException("A surface reference requires finite height and nonempty geometry"); throw new IllegalArgumentException("A surface reference requires finite height and nonempty geometry");
} }
int nearest = -1; int nearest = -1;
double nearestDistance = Double.POSITIVE_INFINITY; double nearestDistance = Double.POSITIVE_INFINITY;
for (int run = 0; run + 1 < runEnds.length; run++) { for (int run = 0; run + 1 < runEnds.length; run++) {
Voxel current = palette.get(paletteIndices[run]); int offset = runEnds[run] - 1;
Voxel above = palette.get(paletteIndices[run + 1]); if (!isTerrain(offset, runVoxel(run), cap) || isTerrain(offset + 1, runVoxel(run + 1), cap)) {
if (current.phase() != Phase.SOLID || current.protectedContent()
|| above.phase() == Phase.SOLID && !above.protectedContent()) {
continue; continue;
} }
int offset = runEnds[run] - 1;
double distance = Math.abs(offset - expectedOffset); double distance = Math.abs(offset - expectedOffset);
if (distance < nearestDistance) { if (distance < nearestDistance) {
nearest = offset; nearest = offset;
nearestDistance = distance; nearestDistance = distance;
} }
} }
int belowCap = cap.bottom() - 1;
if (cap.present() && belowCap >= 0 && cap.bottom() < height() && isTerrain(belowCap, voxelAtOffset(belowCap), cap)) {
double distance = Math.abs(belowCap - expectedOffset);
if (distance < nearestDistance || distance == nearestDistance && belowCap < nearest) {
nearest = belowCap;
}
}
if (nearest >= 0) { if (nearest >= 0) {
return nearest; return nearest;
} }
for (int run = runEnds.length - 1; run >= 0; run--) { for (int run = runEnds.length - 1; run >= 0; run--) {
Voxel current = palette.get(paletteIndices[run]); Voxel current = runVoxel(run);
if (current.phase() == Phase.SOLID && !current.protectedContent()) { if (current.phase() != Phase.SOLID || current.protectedContent()) {
return runEnds[run] - 1; continue;
}
int top = cap.contains(runEnds[run] - 1) ? belowCap : runEnds[run] - 1;
if (top >= (run == 0 ? 0 : runEnds[run - 1])) {
return top;
} }
} }
return 0; return 0;
@@ -220,6 +260,18 @@ public final class BoundaryColumnGeometry implements NativeBlockColumn {
return index >= 0 ? index : -index - 1; return index >= 0 ? index : -index - 1;
} }
private static boolean isTerrain(int offset, Voxel voxel, WaterCap cap) {
return voxel.phase() == Phase.SOLID && !voxel.protectedContent() && !cap.contains(offset);
}
private static boolean isCapMaterial(Voxel voxel) {
return voxel.phase() == Phase.SOLID && !voxel.protectedContent() && voxel.fluidStateKey().isEmpty();
}
private Voxel voxelAtOffset(int offset) {
return palette.get(paletteIndices[runAt(offset)]);
}
private void validate() { private void validate() {
if (palette.size() > Short.MAX_VALUE + 1 || runEnds.length > MAXIMUM_HEIGHT if (palette.size() > Short.MAX_VALUE + 1 || runEnds.length > MAXIMUM_HEIGHT
|| runEnds.length != paletteIndices.length) { || runEnds.length != paletteIndices.length) {
@@ -333,6 +385,22 @@ public final class BoundaryColumnGeometry implements NativeBlockColumn {
FLUID FLUID
} }
public record WaterCap(int bottom, int top) {
private static final WaterCap NONE = new WaterCap(0, -1);
public static WaterCap none() {
return NONE;
}
public boolean present() {
return bottom <= top;
}
public boolean contains(int offset) {
return offset >= bottom && offset <= top;
}
}
public record Voxel(String stateKey, Phase phase, String fluidStateKey, boolean protectedContent) { public record Voxel(String stateKey, Phase phase, String fluidStateKey, boolean protectedContent) {
public Voxel { public Voxel {
Objects.requireNonNull(stateKey, "Block state key"); Objects.requireNonNull(stateKey, "Block state key");
@@ -91,10 +91,15 @@ public final class SavedTerrainChunk {
BoundaryColumnGeometry geometry = scan.runs.build(minimumY); BoundaryColumnGeometry geometry = scan.runs.build(minimumY);
OptionalInt groundSurface = source.groundSurface(x, z); OptionalInt groundSurface = source.groundSurface(x, z);
if (groundSurface.isPresent()) { if (groundSurface.isPresent()) {
floor = geometry.surfaceOffsetNear(groundSurface.getAsInt()); int ground = groundSurface.getAsInt();
BoundaryColumnGeometry.WaterCap cap = geometry.waterCap(ground, source.waterSurface(x, z).orElse(ground));
floor = geometry.surfaceOffsetNear(ground, cap);
if (floor < 0 || floor >= height) { if (floor < 0 || floor >= height) {
throw new IOException("Natural terrain ground surface is outside the chunk height"); throw new IOException("Natural terrain ground surface is outside the chunk height");
} }
if (geometry.capsWater(cap, floor)) {
fluid = cap.top();
}
surface = Math.max(floor, fluid); surface = Math.max(floor, fluid);
} }
columns.add(new TerrainBoundarySignature( columns.add(new TerrainBoundarySignature(
@@ -200,6 +205,10 @@ public final class SavedTerrainChunk {
return OptionalInt.empty(); return OptionalInt.empty();
} }
default OptionalInt waterSurface(int localX, int localZ) {
return OptionalInt.empty();
}
BoundaryColumnGeometry.Voxel voxel(int localX, int worldY, int localZ) throws IOException; BoundaryColumnGeometry.Voxel voxel(int localX, int worldY, int localZ) throws IOException;
String biome(int localX, int worldY, int localZ) throws IOException; String biome(int localX, int worldY, int localZ) throws IOException;
@@ -153,8 +153,8 @@ public final class TransitionDisplacementField {
} }
residual = signature.oceanFloorHeight() - nativeHeight; residual = signature.oceanFloorHeight() - nativeHeight;
if (wet) { if (wet) {
int y = signature.geometry().minimumY() + signature.fluidHeight().getAsInt(); fluidKey = signature.geometry().fluidStateKeyAtOrBelow(signature.fluidHeight().getAsInt(),
fluidKey = signature.geometry().voxelAt(y).fluidStateKey(); signature.oceanFloorHeight());
} }
} }
column.total += weight; column.total += weight;
@@ -164,7 +164,7 @@ public final class TransitionDisplacementField {
column.wetHeight += weight * signature.fluidHeight().getAsInt(); column.wetHeight += weight * signature.fluidHeight().getAsInt();
column.minimumWetHeight = Math.min(column.minimumWetHeight, signature.fluidHeight().getAsInt()); column.minimumWetHeight = Math.min(column.minimumWetHeight, signature.fluidHeight().getAsInt());
column.maximumWetHeight = Math.max(column.maximumWetHeight, signature.fluidHeight().getAsInt()); column.maximumWetHeight = Math.max(column.maximumWetHeight, signature.fluidHeight().getAsInt());
if (weight > column.fluidKeyWeight) { if (weight > column.fluidKeyWeight && !fluidKey.isEmpty()) {
column.fluidKeyWeight = weight; column.fluidKeyWeight = weight;
column.fluidKey = fluidKey; column.fluidKey = fluidKey;
} }
@@ -210,11 +210,14 @@ public final class EngineModeTerrainGenerationTest {
when(biome.key()).thenReturn("minecraft:plains"); when(biome.key()).thenReturn("minecraft:plains");
ProceduralStream<Double> height = mock(ProceduralStream.class); ProceduralStream<Double> height = mock(ProceduralStream.class);
when(height.getDouble(anyDouble(), anyDouble())).thenReturn(3D); when(height.getDouble(anyDouble(), anyDouble())).thenReturn(3D);
ProceduralStream<Double> waterHead = mock(ProceduralStream.class);
when(waterHead.getDouble(anyDouble(), anyDouble())).thenReturn(0D);
ProceduralStream<IrisBiome> biomes = mock(ProceduralStream.class); ProceduralStream<IrisBiome> biomes = mock(ProceduralStream.class);
ProceduralStream<IrisRegion> regions = mock(ProceduralStream.class); ProceduralStream<IrisRegion> regions = mock(ProceduralStream.class);
ProceduralStream<NativeBlockState> materials = mock(ProceduralStream.class); ProceduralStream<NativeBlockState> materials = mock(ProceduralStream.class);
when(materials.get(anyDouble(), anyDouble())).thenReturn(stone); when(materials.get(anyDouble(), anyDouble())).thenReturn(stone);
when(complex.getRawHeightStream()).thenReturn(height); when(complex.getRawHeightStream()).thenReturn(height);
when(complex.getWaterHeadStream()).thenReturn(waterHead);
when(complex.getTrueBiomeStream()).thenReturn(biomes); when(complex.getTrueBiomeStream()).thenReturn(biomes);
when(complex.getCaveBiomeStream()).thenReturn(biomes); when(complex.getCaveBiomeStream()).thenReturn(biomes);
when(complex.getRegionStream()).thenReturn(regions); when(complex.getRegionStream()).thenReturn(regions);
@@ -23,6 +23,10 @@ import art.arcane.iris.testsupport.WaterScene;
import art.arcane.iris.world.history.BoundaryColumnGeometry; import art.arcane.iris.world.history.BoundaryColumnGeometry;
import art.arcane.iris.world.history.BoundaryGeometryInfluence; import art.arcane.iris.world.history.BoundaryGeometryInfluence;
import art.arcane.iris.world.history.GenerationBlend; import art.arcane.iris.world.history.GenerationBlend;
import art.arcane.iris.world.history.GenerationBoundary;
import art.arcane.iris.world.history.GenerationTransition;
import art.arcane.iris.world.history.SavedTerrainChunk;
import art.arcane.iris.world.history.TerrainBoundarySignatureStore;
import art.arcane.iris.world.history.TransitionGenerationPlan; import art.arcane.iris.world.history.TransitionGenerationPlan;
import art.arcane.iris.world.history.TransitionGeometryBlender; import art.arcane.iris.world.history.TransitionGeometryBlender;
import art.arcane.volmlib.nativelib.terrain.NativeBiome; import art.arcane.volmlib.nativelib.terrain.NativeBiome;
@@ -43,7 +47,9 @@ import org.junit.Test;
import org.junit.rules.TemporaryFolder; import org.junit.rules.TemporaryFolder;
import org.junit.rules.Timeout; import org.junit.rules.Timeout;
import java.io.IOException;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap; import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
@@ -533,6 +539,39 @@ public class IrisComplexTransitionTest {
return oldChunkWeight(x, z) == 0D ? 158.3D : 150.3D; return oldChunkWeight(x, z) == 0D ? 158.3D : 150.3D;
} }
@Test
public void transitionedChunkPaintsSeaLayersAtTheHeightOfTheOriginalSea() throws Exception {
NaturalHeights seabed = (x, z) -> 100D;
Hunk<NativeBlockState> original;
SavedTerrainChunk receipt;
try (TransitionStage stage = new TransitionStage(TransitionStage.seamPlan((x, z) -> 1D,
TransitionStage.savedColumn(100, 100, -1)), TransitionStage.seamDisplacement((x, z) -> 1D), false, seabed, 4)) {
original = stage.paint(-1);
receipt = stage.capture(-1, original);
assertSame(stage.ice, original.getRaw(15, 127, 0));
assertSame(stage.ice, original.getRaw(15, 124, 0));
assertSame(stage.water, original.getRaw(15, 123, 0));
}
GenerationBoundary boundary = GenerationBoundary.freeze("boundary", List.of(new GenerationBoundary.ChunkCoordinate(-1, 0)));
TerrainBoundarySignatureStore.Snapshot snapshot = new TerrainBoundarySignatureStore(temporaryFolder.newFolder().toPath())
.publish(2L, boundary, receipt::column);
TransitionGenerationPlan plan = new TransitionGenerationPlan(new TransitionGenerationPlan.Specification(2L, "old", "new",
GenerationTransition.CURRENT_ALGORITHM_VERSION, 32, boundary.identity(), snapshot.identity()), boundary, snapshot);
TransitionDisplacementField displacement = new TransitionDisplacementField(plan,
new TransitionDisplacementField.Sources((x, z) -> 100D, TransitionStage.HEIGHT));
try (TransitionStage stage = new TransitionStage(plan, displacement, false, seabed, 4)) {
Hunk<NativeBlockState> transitioned = stage.blend(0, stage.paint(0));
for (int lz = 0; lz < 16; lz++) {
for (int lx = 0; lx < 16; lx++) {
for (int y = 0; y < TransitionStage.HEIGHT; y++) {
assertEquals(lx + "," + y + "," + lz, stage.key(original.getRaw(15, y, lz)),
stage.key(transitioned.getRaw(lx, y, lz)));
}
}
}
}
}
private static void assertColumn(Hunk<NativeBlockState> painted, Hunk<NativeBlockState> blended, int lx, int lz) { private static void assertColumn(Hunk<NativeBlockState> painted, Hunk<NativeBlockState> blended, int lx, int lz) {
for (int y = 0; y < TransitionStage.HEIGHT; y++) { for (int y = 0; y < TransitionStage.HEIGHT; y++) {
assertSame(lx + "," + y + "," + lz + " was re-blended", painted.getRaw(lx, y, lz), blended.getRaw(lx, y, lz)); assertSame(lx + "," + y + "," + lz + " was re-blended", painted.getRaw(lx, y, lz), blended.getRaw(lx, y, lz));
@@ -547,8 +586,10 @@ public class IrisComplexTransitionTest {
private final NativeBlockState stone = block("minecraft:stone", false, false); private final NativeBlockState stone = block("minecraft:stone", false, false);
private final NativeBlockState water = block("minecraft:water[level=0]", false, true); private final NativeBlockState water = block("minecraft:water[level=0]", false, true);
private final NativeBlockState air = block("minecraft:air", true, false); private final NativeBlockState air = block("minecraft:air", true, false);
private final TransitionDisplacementField displacement = mock(TransitionDisplacementField.class); private final NativeBlockState ice = block("minecraft:ice", false, false);
private final TransitionGenerationPlan plan = mock(TransitionGenerationPlan.class); private final TransitionDisplacementField displacement;
private final TransitionGenerationPlan plan;
private final int seaLayers;
private final IrisComplex complex; private final IrisComplex complex;
private final FakeHydrology hydrology; private final FakeHydrology hydrology;
@@ -562,13 +603,14 @@ public class IrisComplexTransitionTest {
private TransitionStage(SeamWeights weights, boolean enabled, NaturalHeights natural, BoundaryColumnGeometry oldColumn) private TransitionStage(SeamWeights weights, boolean enabled, NaturalHeights natural, BoundaryColumnGeometry oldColumn)
throws Exception { throws Exception {
when(displacement.seamWeight(anyInt(), anyInt())).thenAnswer(invocation -> this(seamPlan(weights, oldColumn), seamDisplacement(weights), enabled, natural, 0);
weights.weight(invocation.getArgument(0), invocation.getArgument(1))); }
when(displacement.fluidHeight(anyInt(), anyInt(), anyDouble())).thenAnswer(invocation -> invocation.getArgument(2));
when(plan.hasTransitionAtChunk(anyInt(), anyInt())).thenReturn(true); private TransitionStage(TransitionGenerationPlan plan, TransitionDisplacementField displacement, boolean enabled,
when(plan.geometryAt(anyInt(), anyInt())).thenAnswer(invocation -> new BoundaryGeometryInfluence( NaturalHeights natural, int seaLayers) throws Exception {
weights.weight(invocation.getArgument(0), invocation.getArgument(1)), 0D, this.plan = plan;
List.of(new BoundaryGeometryInfluence.Contribution(oldColumn, 1D)))); this.displacement = displacement;
this.seaLayers = seaLayers;
AtomicReference<IrisComplex> built = new AtomicReference<>(); AtomicReference<IrisComplex> built = new AtomicReference<>();
hydrology = enabled ? new FakeHydrology(() -> false, (x, z) -> built.get().naturalTrueHeight(x, z), 127, 156) : null; hydrology = enabled ? new FakeHydrology(() -> false, (x, z) -> built.get().naturalTrueHeight(x, z), 127, 156) : null;
complex = fixture.transitionComplex(enabled ? hydrology.runtime : FakeHydrology.disabled(), plan, displacement); complex = fixture.transitionComplex(enabled ? hydrology.runtime : FakeHydrology.disabled(), plan, displacement);
@@ -581,7 +623,7 @@ public class IrisComplexTransitionTest {
doReturn(mock(IrisImageMapRuntime.class)).when(complex).getImageMapRuntime(); doReturn(mock(IrisImageMapRuntime.class)).when(complex).getImageMapRuntime();
doReturn(water).when(complex).resolveWaterFluid(anyInt(), anyInt()); doReturn(water).when(complex).resolveWaterFluid(anyInt(), anyInt());
doReturn(water).when(complex).resolveSeaFluid(anyDouble(), anyDouble()); doReturn(water).when(complex).resolveSeaFluid(anyDouble(), anyDouble());
Map<String, NativeBlockState> blocks = Map.of(stone.key(), stone, water.key(), water, air.key(), air); Map<String, NativeBlockState> blocks = Map.of(stone.key(), stone, water.key(), water, air.key(), air, ice.key(), ice);
PlatformRegistries registries = mock(PlatformRegistries.class); PlatformRegistries registries = mock(PlatformRegistries.class);
when(registries.air()).thenReturn(air); when(registries.air()).thenReturn(air);
when(registries.block("AIR")).thenReturn(air); when(registries.block("AIR")).thenReturn(air);
@@ -594,6 +636,23 @@ public class IrisComplexTransitionTest {
IrisPlatforms.bind(platform); IrisPlatforms.bind(platform);
} }
private static TransitionGenerationPlan seamPlan(SeamWeights weights, BoundaryColumnGeometry oldColumn) {
TransitionGenerationPlan plan = mock(TransitionGenerationPlan.class);
when(plan.hasTransitionAtChunk(anyInt(), anyInt())).thenReturn(true);
when(plan.geometryAt(anyInt(), anyInt())).thenAnswer(invocation -> new BoundaryGeometryInfluence(
weights.weight(invocation.getArgument(0), invocation.getArgument(1)), 0D,
List.of(new BoundaryGeometryInfluence.Contribution(oldColumn, 1D))));
return plan;
}
private static TransitionDisplacementField seamDisplacement(SeamWeights weights) {
TransitionDisplacementField displacement = mock(TransitionDisplacementField.class);
when(displacement.seamWeight(anyInt(), anyInt())).thenAnswer(invocation ->
weights.weight(invocation.getArgument(0), invocation.getArgument(1)));
when(displacement.fluidHeight(anyInt(), anyInt(), anyDouble())).thenAnswer(invocation -> invocation.getArgument(2));
return displacement;
}
private ChunkWaterRaster raster(int chunkX) { private ChunkWaterRaster raster(int chunkX) {
return hydrology.runtime.raster(chunkX, 0); return hydrology.runtime.raster(chunkX, 0);
} }
@@ -622,7 +681,8 @@ public class IrisComplexTransitionTest {
when(engine.getSeedManager()).thenReturn(mock(SeedManager.class)); when(engine.getSeedManager()).thenReturn(mock(SeedManager.class));
when(biome.generateLayers(eq(dimension), anyDouble(), anyDouble(), any(RNG.class), anyInt(), anyInt(), eq(data), same(complex))) when(biome.generateLayers(eq(dimension), anyDouble(), anyDouble(), any(RNG.class), anyInt(), anyInt(), eq(data), same(complex)))
.thenAnswer(invocation -> new KList<>(List.of(stone))); .thenAnswer(invocation -> new KList<>(List.of(stone)));
when(biome.generateSeaLayers(anyDouble(), anyDouble(), any(RNG.class), anyInt(), eq(data))).thenAnswer(invocation -> new KList<>()); when(biome.generateSeaLayers(anyDouble(), anyDouble(), any(RNG.class), anyInt(), eq(data)))
.thenAnswer(invocation -> new KList<>(Collections.nCopies(seaLayers, ice)));
int[] grounds = grounds(chunkX); int[] grounds = grounds(chunkX);
ChunkContext context = mock(ChunkContext.class); ChunkContext context = mock(ChunkContext.class);
when(context.getRoundedHeight(anyInt(), anyInt())).thenAnswer(invocation -> when(context.getRoundedHeight(anyInt(), anyInt())).thenAnswer(invocation ->
@@ -712,6 +772,19 @@ public class IrisComplexTransitionTest {
return blocks; return blocks;
} }
private SavedTerrainChunk capture(int chunkX, Hunk<NativeBlockState> blocks) throws IOException {
int[] grounds = grounds(chunkX);
ChunkContext context = mock(ChunkContext.class);
when(context.getComplex()).thenReturn(complex);
when(context.getRoundedHeight(anyInt(), anyInt())).thenAnswer(invocation ->
grounds[invocation.<Integer>getArgument(1) << 4 | invocation.<Integer>getArgument(0)]);
NativeBiome biome = mock(NativeBiome.class);
when(biome.key()).thenReturn("minecraft:frozen_ocean");
Hunk<NativeBiome> biomes = Hunk.newArrayHunk(16, HEIGHT, 16);
biomes.fill(biome);
return IrisTransitionGeometryActuator.capture(chunkX << 4, 0, blocks, biomes, 0, context, true);
}
private BoundaryColumnGeometry geometry(Hunk<NativeBlockState> blocks, int lx, int lz) { private BoundaryColumnGeometry geometry(Hunk<NativeBlockState> blocks, int lx, int lz) {
ArrayList<BoundaryColumnGeometry.Voxel> voxels = new ArrayList<>(HEIGHT); ArrayList<BoundaryColumnGeometry.Voxel> voxels = new ArrayList<>(HEIGHT);
for (int y = 0; y < HEIGHT; y++) { for (int y = 0; y < HEIGHT; y++) {
@@ -7,6 +7,7 @@ import art.arcane.iris.world.history.BoundaryColumnGeometry;
import art.arcane.iris.world.history.BoundaryGeometryInfluence; import art.arcane.iris.world.history.BoundaryGeometryInfluence;
import art.arcane.iris.world.history.NativeTerrainReceipt; import art.arcane.iris.world.history.NativeTerrainReceipt;
import art.arcane.iris.world.history.SavedTerrainChunk; import art.arcane.iris.world.history.SavedTerrainChunk;
import art.arcane.iris.world.history.TerrainBoundarySignature;
import art.arcane.iris.world.history.TransitionGenerationPlan; import art.arcane.iris.world.history.TransitionGenerationPlan;
import art.arcane.iris.spi.IrisPlatform; import art.arcane.iris.spi.IrisPlatform;
import art.arcane.iris.spi.IrisPlatforms; import art.arcane.iris.spi.IrisPlatforms;
@@ -16,6 +17,7 @@ import art.arcane.iris.spi.PlatformRegistries;
import art.arcane.iris.testsupport.PlatformLeakGuard; import art.arcane.iris.testsupport.PlatformLeakGuard;
import art.arcane.iris.generation.context.ChunkContext; import art.arcane.iris.generation.context.ChunkContext;
import art.arcane.volmlib.util.hunk.Hunk; import art.arcane.volmlib.util.hunk.Hunk;
import art.arcane.volmlib.util.stream.ProceduralStream;
import org.junit.After; import org.junit.After;
import org.junit.Before; import org.junit.Before;
import org.junit.ClassRule; import org.junit.ClassRule;
@@ -23,6 +25,7 @@ import org.junit.Test;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.OptionalInt;
import static org.junit.Assert.assertSame; import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertEquals;
@@ -79,6 +82,9 @@ public class IrisTransitionGeometryActuatorTest {
when(biome.key()).thenReturn("minecraft:plains"); when(biome.key()).thenReturn("minecraft:plains");
biomes.fill(biome); biomes.fill(biome);
ChunkContext context = mock(ChunkContext.class); ChunkContext context = mock(ChunkContext.class);
IrisComplex complex = mock(IrisComplex.class);
when(complex.getWaterHeadStream()).thenReturn(ProceduralStream.ofDouble((x, z) -> 3D));
when(context.getComplex()).thenReturn(complex);
when(context.getRoundedHeight(anyInt(), anyInt())).thenReturn(3); when(context.getRoundedHeight(anyInt(), anyInt())).thenReturn(3);
SavedTerrainChunk captured = IrisTransitionGeometryActuator.capture(0, 0, blocks, biomes, 0, context, true); SavedTerrainChunk captured = IrisTransitionGeometryActuator.capture(0, 0, blocks, biomes, 0, context, true);
@@ -146,6 +152,98 @@ public class IrisTransitionGeometryActuatorTest {
Terrain3DColumn.fromOccupancy(3, 16, y -> y == 0 || y == 3), 6); Terrain3DColumn.fromOccupancy(3, 16, y -> y == 0 || y == 3), 6);
} }
@Test
public void boundaryReceiptRecordsSeaLayerCapsAtTheWaterHeadAboveTheSeabed() throws Exception {
NativeBlockState water = fluid("minecraft:water[level=0]");
NativeBlockState ice = state("minecraft:ice", false);
Hunk<NativeBlockState> blocks = filled();
for (int x = 0; x < 16; x++) {
for (int z = 0; z < 16; z++) {
int head = z == 15 ? 6 : 12;
int capBottom = z == 0 ? 9 : z == 15 ? 4 : head + 1;
for (int y = 4; y < 16; y++) {
blocks.setRaw(x, y, z, y > head ? air : y >= capBottom ? ice : water);
}
}
}
ChunkContext context = mock(ChunkContext.class);
IrisComplex complex = mock(IrisComplex.class);
when(complex.getWaterHeadStream()).thenReturn(ProceduralStream.ofDouble((x, z) -> z == 15D ? 6D : 12D));
when(context.getComplex()).thenReturn(complex);
when(context.getRoundedHeight(anyInt(), anyInt())).thenReturn(3);
SavedTerrainChunk captured = IrisTransitionGeometryActuator.capture(0, 0, blocks, biomes(), 0, context, true);
SavedTerrainChunk restored = NativeTerrainReceipt.decode(NativeTerrainReceipt.encode(captured, 7, "epoch"),
"minecraft:noise").terrain();
assertWaterColumn(restored.column(0, 0), 3, 12);
assertWaterColumn(restored.column(0, 15), 3, 6);
assertWaterColumn(restored.column(0, 7), 3, 12);
}
@Test
public void transitionKeepsSeaLayerCapsAtTheWaterHeadForDecoration() {
NativeBlockState water = fluid("minecraft:water[level=0]");
NativeBlockState ice = state("minecraft:ice", false);
when(registries.blockOrNull(ice.key())).thenReturn(ice);
for (int capBottom : new int[]{9, 4}) {
ArrayList<BoundaryColumnGeometry.Voxel> historical = new ArrayList<>(16);
for (int y = 0; y < 16; y++) {
historical.add(y <= 3 ? voxel("minecraft:stone", BoundaryColumnGeometry.Phase.SOLID, "")
: y > 12 ? voxel("minecraft:air", BoundaryColumnGeometry.Phase.AIR, "")
: y >= capBottom ? voxel(ice.key(), BoundaryColumnGeometry.Phase.SOLID, "")
: voxel(water.key(), BoundaryColumnGeometry.Phase.FLUID, water.key()));
}
ChunkContext context = context(BoundaryColumnGeometry.fromVoxels(0, historical), 12D);
Hunk<NativeBlockState> blocks = filled();
for (int x = 0; x < 16; x++) {
for (int z = 0; z < 16; z++) {
for (int y = 1; y < 16; y++) {
blocks.setRaw(x, y, z, y < 3 || y > 12 ? air : y == 3 ? stone : y >= capBottom ? ice : water);
}
}
}
new IrisTransitionGeometryActuator(mock(Engine.class)).generate(16, 0, blocks,
Hunk.newArrayHunk(16, 16, 16), false, context);
assertSame(stone, blocks.getRaw(0, 2, 0));
assertSame(ice, blocks.getRaw(0, 12, 0));
assertSame(capBottom == 4 ? ice : water, blocks.getRaw(0, 4, 0));
assertSame(air, blocks.getRaw(0, 13, 0));
int iceBottom = capBottom;
verify(context).setTerrainHeight(0, 0, 3);
verify(context).setTransitionTerrain(0, 0,
Terrain3DColumn.fromOccupancy(3, 16, y -> y <= 3 || y >= iceBottom && y <= 12), 12);
}
}
private static void assertWaterColumn(TerrainBoundarySignature signature, int seabed, int waterSurface) {
assertEquals(seabed, signature.oceanFloorHeight());
assertEquals(OptionalInt.of(waterSurface), signature.fluidHeight());
assertEquals(waterSurface, signature.surfaceHeight());
}
private static BoundaryColumnGeometry.Voxel voxel(String key, BoundaryColumnGeometry.Phase phase, String fluid) {
return new BoundaryColumnGeometry.Voxel(key, phase, fluid, false);
}
private NativeBlockState fluid(String key) {
NativeBlockState state = state(key, false);
when(state.isSolid()).thenReturn(false);
when(state.isFluid()).thenReturn(true);
when(registries.blockOrNull(key)).thenReturn(state);
return state;
}
private static Hunk<NativeBiome> biomes() {
Hunk<NativeBiome> biomes = Hunk.newArrayHunk(16, 16, 16);
NativeBiome biome = mock(NativeBiome.class);
when(biome.key()).thenReturn("minecraft:frozen_ocean");
biomes.fill(biome);
return biomes;
}
private ChunkContext context(String material) { private ChunkContext context(String material) {
return context(material, 3); return context(material, 3);
} }
@@ -159,8 +257,12 @@ public class IrisTransitionGeometryActuatorTest {
solid ? BoundaryColumnGeometry.Phase.SOLID : fluid ? BoundaryColumnGeometry.Phase.FLUID : BoundaryColumnGeometry.Phase.AIR, solid ? BoundaryColumnGeometry.Phase.SOLID : fluid ? BoundaryColumnGeometry.Phase.FLUID : BoundaryColumnGeometry.Phase.AIR,
fluid ? "minecraft:water[level=0]" : "", false)); fluid ? "minecraft:water[level=0]" : "", false));
} }
return context(BoundaryColumnGeometry.fromVoxels(0, voxels), 3D);
}
private ChunkContext context(BoundaryColumnGeometry historical, double waterHead) {
BoundaryGeometryInfluence influence = new BoundaryGeometryInfluence(0D, 0D, BoundaryGeometryInfluence influence = new BoundaryGeometryInfluence(0D, 0D,
List.of(new BoundaryGeometryInfluence.Contribution(BoundaryColumnGeometry.fromVoxels(0, voxels), 1D))); List.of(new BoundaryGeometryInfluence.Contribution(historical, 1D)));
TransitionGenerationPlan plan = mock(TransitionGenerationPlan.class); TransitionGenerationPlan plan = mock(TransitionGenerationPlan.class);
when(plan.geometryAt(anyInt(), anyInt())).thenReturn(influence); when(plan.geometryAt(anyInt(), anyInt())).thenReturn(influence);
when(plan.hasTransitionAtChunk(anyInt(), anyInt())).thenReturn(true); when(plan.hasTransitionAtChunk(anyInt(), anyInt())).thenReturn(true);
@@ -169,6 +271,7 @@ public class IrisTransitionGeometryActuatorTest {
when(plan.terrainSampleAt(anyInt(), anyInt())).thenReturn(sample); when(plan.terrainSampleAt(anyInt(), anyInt())).thenReturn(sample);
IrisComplex complex = mock(IrisComplex.class); IrisComplex complex = mock(IrisComplex.class);
when(complex.getTransitionGenerationPlan()).thenReturn(plan); when(complex.getTransitionGenerationPlan()).thenReturn(plan);
when(complex.getWaterHeadStream()).thenReturn(ProceduralStream.ofDouble((x, z) -> waterHead));
ChunkContext context = mock(ChunkContext.class); ChunkContext context = mock(ChunkContext.class);
when(context.getComplex()).thenReturn(complex); when(context.getComplex()).thenReturn(complex);
when(context.getRoundedHeight(anyInt(), anyInt())).thenReturn(3); when(context.getRoundedHeight(anyInt(), anyInt())).thenReturn(3);
@@ -23,6 +23,8 @@ public class BoundaryColumnGeometryTest {
"minecraft:stone", BoundaryColumnGeometry.Phase.SOLID, "", false); "minecraft:stone", BoundaryColumnGeometry.Phase.SOLID, "", false);
private static final BoundaryColumnGeometry.Voxel AIR = new BoundaryColumnGeometry.Voxel( private static final BoundaryColumnGeometry.Voxel AIR = new BoundaryColumnGeometry.Voxel(
"minecraft:air", BoundaryColumnGeometry.Phase.AIR, "", false); "minecraft:air", BoundaryColumnGeometry.Phase.AIR, "", false);
private static final BoundaryColumnGeometry.Voxel WATER = new BoundaryColumnGeometry.Voxel(
"minecraft:water[level=0]", BoundaryColumnGeometry.Phase.FLUID, "minecraft:water[level=0]", false);
@Test @Test
public void solidAboveMatchesDenseColumnsWithProtectionAndExtremeWorldCoordinates() { public void solidAboveMatchesDenseColumnsWithProtectionAndExtremeWorldCoordinates() {
@@ -92,8 +94,65 @@ public class BoundaryColumnGeometryTest {
} }
BoundaryColumnGeometry geometry = BoundaryColumnGeometry.fromVoxels(-64, voxels); BoundaryColumnGeometry geometry = BoundaryColumnGeometry.fromVoxels(-64, voxels);
assertEquals(50, geometry.surfaceOffsetNear(50D)); assertEquals(50, geometry.surfaceOffsetNear(50D, BoundaryColumnGeometry.WaterCap.none()));
assertEquals(50, geometry.surfaceOffsetNear(70D)); assertEquals(50, geometry.surfaceOffsetNear(70D, BoundaryColumnGeometry.WaterCap.none()));
}
@Test
public void waterCapSpansSolidLayersFromTheWaterSurfaceDownToWaterOrTheSeabed() {
BoundaryColumnGeometry.Voxel ice = new BoundaryColumnGeometry.Voxel(
"minecraft:ice", BoundaryColumnGeometry.Phase.SOLID, "", false);
BoundaryColumnGeometry.Voxel packedIce = new BoundaryColumnGeometry.Voxel(
"minecraft:packed_ice", BoundaryColumnGeometry.Phase.SOLID, "", false);
BoundaryColumnGeometry iced = column(STONE, STONE, STONE, STONE, WATER, WATER, WATER, WATER, WATER,
packedIce, packedIce, ice, ice, AIR, AIR, AIR);
BoundaryColumnGeometry.WaterCap cap = iced.waterCap(3, 12);
assertEquals(new BoundaryColumnGeometry.WaterCap(9, 12), cap);
assertEquals(3, iced.surfaceOffsetNear(3D, cap));
assertTrue(iced.capsWater(cap, 3));
assertEquals(WATER.fluidStateKey(), iced.fluidStateKeyAtOrBelow(12, 3));
BoundaryColumnGeometry frozen = column(STONE, STONE, STONE, ice, ice, ice, ice, AIR);
BoundaryColumnGeometry.WaterCap frozenCap = frozen.waterCap(2, 6);
assertEquals(new BoundaryColumnGeometry.WaterCap(3, 6), frozenCap);
assertEquals(6, frozen.surfaceOffsetNear(2D, BoundaryColumnGeometry.WaterCap.none()));
assertEquals(2, frozen.surfaceOffsetNear(2D, frozenCap));
assertTrue(frozen.capsWater(frozenCap, 2));
assertEquals("", frozen.fluidStateKeyAtOrBelow(6, 2));
BoundaryColumnGeometry frozenBed = column(STONE, ice, ice, ice, ice, AIR);
BoundaryColumnGeometry.WaterCap bedCap = frozenBed.waterCap(1, 4);
assertEquals(new BoundaryColumnGeometry.WaterCap(2, 4), bedCap);
assertEquals(1, frozenBed.surfaceOffsetNear(1D, bedCap));
assertEquals(1, frozenBed.surfaceOffsetNear(4D, bedCap));
}
@Test
public void waterCapIgnoresOpenWaterLandAndSolidsThatContinueAboveTheSurface() {
BoundaryColumnGeometry.Voxel ice = new BoundaryColumnGeometry.Voxel(
"minecraft:ice", BoundaryColumnGeometry.Phase.SOLID, "", false);
BoundaryColumnGeometry.Voxel waterlogged = new BoundaryColumnGeometry.Voxel(
"minecraft:oak_slab[type=bottom,waterlogged=true]", BoundaryColumnGeometry.Phase.SOLID,
WATER.fluidStateKey(), false);
BoundaryColumnGeometry.Voxel protectedSolid = new BoundaryColumnGeometry.Voxel(
"minecraft:oak_log[axis=y]", BoundaryColumnGeometry.Phase.SOLID, "", true);
assertFalse(column(STONE, STONE, WATER, WATER, AIR).waterCap(1, 3).present());
assertFalse(column(STONE, STONE, STONE, STONE, AIR).waterCap(3, 3).present());
assertFalse(column(STONE, STONE, WATER, ice, ice, AIR).waterCap(1, 3).present());
assertFalse(column(STONE, STONE, WATER, waterlogged, AIR).waterCap(1, 3).present());
assertFalse(column(STONE, STONE, WATER, protectedSolid, AIR).waterCap(1, 3).present());
assertFalse(column(STONE, STONE, WATER, ice).waterCap(1, 4).present());
assertFalse(column(STONE, STONE, WATER, ice, AIR).waterCap(1, -1).present());
assertTrue(column(STONE, STONE, WATER, ice).waterCap(1, 3).present());
BoundaryColumnGeometry overAir = column(STONE, STONE, WATER, AIR, ice, AIR);
BoundaryColumnGeometry.WaterCap cap = overAir.waterCap(1, 4);
assertEquals(new BoundaryColumnGeometry.WaterCap(4, 4), cap);
assertFalse(overAir.capsWater(cap, 1));
assertFalse(overAir.capsWater(BoundaryColumnGeometry.WaterCap.none(), 1));
} }
@Test @Test
@@ -114,24 +173,29 @@ public class BoundaryColumnGeometryTest {
} }
} }
BoundaryColumnGeometry geometry = BoundaryColumnGeometry.fromVoxels(-64, voxels); BoundaryColumnGeometry geometry = BoundaryColumnGeometry.fromVoxels(-64, voxels);
for (double expected : new double[]{-10D, 0D, 1.5D, 3D, 5.5D, 8D, 12D, 100D}) { for (BoundaryColumnGeometry.WaterCap cap : List.of(BoundaryColumnGeometry.WaterCap.none(),
assertEquals(denseSurface(voxels, expected), geometry.surfaceOffsetNear(expected)); new BoundaryColumnGeometry.WaterCap(0, 0), new BoundaryColumnGeometry.WaterCap(2, 4),
new BoundaryColumnGeometry.WaterCap(5, 9), new BoundaryColumnGeometry.WaterCap(11, 17))) {
for (double expected : new double[]{-10D, 0D, 1.5D, 3D, 5.5D, 8D, 12D, 100D}) {
assertEquals(denseSurface(voxels, expected, cap), geometry.surfaceOffsetNear(expected, cap));
}
} }
} }
assertThrows(IllegalArgumentException.class, () -> BoundaryColumnGeometry.empty().surfaceOffsetNear(0D)); assertThrows(IllegalArgumentException.class, () -> BoundaryColumnGeometry.empty()
.surfaceOffsetNear(0D, BoundaryColumnGeometry.WaterCap.none()));
BoundaryColumnGeometry solid = BoundaryColumnGeometry.fromVoxels(-64, List.of(STONE)); BoundaryColumnGeometry solid = BoundaryColumnGeometry.fromVoxels(-64, List.of(STONE));
assertThrows(IllegalArgumentException.class, () -> solid.surfaceOffsetNear(Double.NaN)); assertThrows(IllegalArgumentException.class, () -> solid.surfaceOffsetNear(Double.NaN,
assertThrows(IllegalArgumentException.class, () -> solid.surfaceOffsetNear(Double.POSITIVE_INFINITY)); BoundaryColumnGeometry.WaterCap.none()));
assertThrows(IllegalArgumentException.class, () -> solid.surfaceOffsetNear(Double.POSITIVE_INFINITY,
BoundaryColumnGeometry.WaterCap.none()));
} }
private static int denseSurface(List<BoundaryColumnGeometry.Voxel> voxels, double expected) { private static int denseSurface(List<BoundaryColumnGeometry.Voxel> voxels, double expected,
BoundaryColumnGeometry.WaterCap cap) {
int selected = -1; int selected = -1;
double distance = Double.POSITIVE_INFINITY; double distance = Double.POSITIVE_INFINITY;
for (int index = 0; index + 1 < voxels.size(); index++) { for (int index = 0; index + 1 < voxels.size(); index++) {
BoundaryColumnGeometry.Voxel current = voxels.get(index); if (denseTerrain(voxels, index, cap) && !denseTerrain(voxels, index + 1, cap)
BoundaryColumnGeometry.Voxel above = voxels.get(index + 1);
if (current.phase() == BoundaryColumnGeometry.Phase.SOLID && !current.protectedContent()
&& (above.phase() != BoundaryColumnGeometry.Phase.SOLID || above.protectedContent())
&& Math.abs(index - expected) < distance) { && Math.abs(index - expected) < distance) {
selected = index; selected = index;
distance = Math.abs(index - expected); distance = Math.abs(index - expected);
@@ -141,14 +205,24 @@ public class BoundaryColumnGeometryTest {
return selected; return selected;
} }
for (int index = voxels.size() - 1; index >= 0; index--) { for (int index = voxels.size() - 1; index >= 0; index--) {
BoundaryColumnGeometry.Voxel voxel = voxels.get(index); if (denseTerrain(voxels, index, cap)) {
if (voxel.phase() == BoundaryColumnGeometry.Phase.SOLID && !voxel.protectedContent()) {
return index; return index;
} }
} }
return 0; return 0;
} }
private static boolean denseTerrain(List<BoundaryColumnGeometry.Voxel> voxels, int index,
BoundaryColumnGeometry.WaterCap cap) {
BoundaryColumnGeometry.Voxel voxel = voxels.get(index);
return voxel.phase() == BoundaryColumnGeometry.Phase.SOLID && !voxel.protectedContent()
&& (index < cap.bottom() || index > cap.top());
}
private static BoundaryColumnGeometry column(BoundaryColumnGeometry.Voxel... voxels) {
return BoundaryColumnGeometry.fromVoxels(-64, List.of(voxels));
}
@Test @Test
public void enclosedOpenQueriesIncludeUpperCavesAndExcludeOpenSky() { public void enclosedOpenQueriesIncludeUpperCavesAndExcludeOpenSky() {
BoundaryColumnGeometry geometry = BoundaryColumnGeometry.fromVoxels(-4, BoundaryColumnGeometry geometry = BoundaryColumnGeometry.fromVoxels(-4,
@@ -29,6 +29,7 @@ public class SavedTerrainCaptureEquivalenceTest {
private static final List<BoundaryColumnGeometry.Voxel> STATES = List.of( private static final List<BoundaryColumnGeometry.Voxel> STATES = List.of(
voxel("minecraft:stone", BoundaryColumnGeometry.Phase.SOLID, "", false), voxel("minecraft:stone", BoundaryColumnGeometry.Phase.SOLID, "", false),
voxel("minecraft:deepslate", BoundaryColumnGeometry.Phase.SOLID, "", false), voxel("minecraft:deepslate", BoundaryColumnGeometry.Phase.SOLID, "", false),
voxel("minecraft:ice", BoundaryColumnGeometry.Phase.SOLID, "", false),
voxel("minecraft:air", BoundaryColumnGeometry.Phase.AIR, "", false), voxel("minecraft:air", BoundaryColumnGeometry.Phase.AIR, "", false),
voxel("minecraft:cave_air", BoundaryColumnGeometry.Phase.AIR, "", false), voxel("minecraft:cave_air", BoundaryColumnGeometry.Phase.AIR, "", false),
voxel("minecraft:water[level=0]", BoundaryColumnGeometry.Phase.FLUID, "minecraft:water[level=0]", false), voxel("minecraft:water[level=0]", BoundaryColumnGeometry.Phase.FLUID, "minecraft:water[level=0]", false),
@@ -153,7 +154,14 @@ public class SavedTerrainCaptureEquivalenceTest {
BoundaryColumnGeometry geometry = referenceGeometry(MINIMUM_Y, voxels); BoundaryColumnGeometry geometry = referenceGeometry(MINIMUM_Y, voxels);
OptionalInt groundSurface = source.groundSurface(x, z); OptionalInt groundSurface = source.groundSurface(x, z);
if (groundSurface.isPresent()) { if (groundSurface.isPresent()) {
floor = geometry.surfaceOffsetNear(groundSurface.getAsInt()); int ground = groundSurface.getAsInt();
int waterSurface = source.waterSurface(x, z).orElse(ground);
int capBottom = referenceCapBottom(voxels, ground, waterSurface);
floor = referenceFloor(voxels, ground, capBottom, waterSurface);
if (capBottom <= waterSurface && waterSurface > floor && (capBottom - 1 == floor
|| capBottom > 0 && !voxels.get(capBottom - 1).fluidStateKey().isEmpty())) {
fluid = waterSurface;
}
surface = Math.max(floor, fluid); surface = Math.max(floor, fluid);
} }
columns.add(new TerrainBoundarySignature( columns.add(new TerrainBoundarySignature(
@@ -168,6 +176,50 @@ public class SavedTerrainCaptureEquivalenceTest {
return new SavedTerrainChunk(chunkX, chunkZ, "minecraft:noise", columns); return new SavedTerrainChunk(chunkX, chunkZ, "minecraft:noise", columns);
} }
private static int referenceCapBottom(List<BoundaryColumnGeometry.Voxel> voxels, int ground, int waterSurface) {
if (waterSurface <= ground || waterSurface < 0 || waterSurface >= voxels.size()
|| waterSurface + 1 < voxels.size() && voxels.get(waterSurface + 1).phase() != BoundaryColumnGeometry.Phase.AIR) {
return waterSurface + 1;
}
int bottom = waterSurface + 1;
while (bottom - 1 >= Math.max(0, ground + 1) && capMaterial(voxels.get(bottom - 1))) {
bottom--;
}
return bottom;
}
private static boolean capMaterial(BoundaryColumnGeometry.Voxel voxel) {
return voxel.phase() == BoundaryColumnGeometry.Phase.SOLID && !voxel.protectedContent()
&& voxel.fluidStateKey().isEmpty();
}
private static int referenceFloor(List<BoundaryColumnGeometry.Voxel> voxels, int ground, int capBottom, int capTop) {
int selected = -1;
int distance = Integer.MAX_VALUE;
for (int offset = 0; offset + 1 < voxels.size(); offset++) {
if (referenceTerrain(voxels, offset, capBottom, capTop) && !referenceTerrain(voxels, offset + 1, capBottom, capTop)
&& Math.abs(offset - ground) < distance) {
selected = offset;
distance = Math.abs(offset - ground);
}
}
if (selected >= 0) {
return selected;
}
for (int offset = voxels.size() - 1; offset >= 0; offset--) {
if (referenceTerrain(voxels, offset, capBottom, capTop)) {
return offset;
}
}
return 0;
}
private static boolean referenceTerrain(List<BoundaryColumnGeometry.Voxel> voxels, int offset, int capBottom, int capTop) {
BoundaryColumnGeometry.Voxel voxel = voxels.get(offset);
return voxel.phase() == BoundaryColumnGeometry.Phase.SOLID && !voxel.protectedContent()
&& (offset < capBottom || offset > capTop);
}
private static BoundaryColumnGeometry referenceGeometry(int minimumY, List<BoundaryColumnGeometry.Voxel> voxels) { private static BoundaryColumnGeometry referenceGeometry(int minimumY, List<BoundaryColumnGeometry.Voxel> voxels) {
TreeSet<BoundaryColumnGeometry.Voxel> used = new TreeSet<>(VOXEL_ORDER); TreeSet<BoundaryColumnGeometry.Voxel> used = new TreeSet<>(VOXEL_ORDER);
BoundaryColumnGeometry.Voxel previous = null; BoundaryColumnGeometry.Voxel previous = null;
@@ -216,6 +268,11 @@ public class SavedTerrainCaptureEquivalenceTest {
return source.groundSurface(localX, localZ); return source.groundSurface(localX, localZ);
} }
@Override
public OptionalInt waterSurface(int localX, int localZ) {
return source.waterSurface(localX, localZ);
}
@Override @Override
public BoundaryColumnGeometry.Voxel voxel(int localX, int worldY, int localZ) { public BoundaryColumnGeometry.Voxel voxel(int localX, int worldY, int localZ) {
return source.voxel(localX, worldY, localZ); return source.voxel(localX, worldY, localZ);
@@ -246,6 +303,7 @@ public class SavedTerrainCaptureEquivalenceTest {
private final BoundaryColumnGeometry.Voxel[][] voxels = new BoundaryColumnGeometry.Voxel[256][HEIGHT]; private final BoundaryColumnGeometry.Voxel[][] voxels = new BoundaryColumnGeometry.Voxel[256][HEIGHT];
private final String[][] biomes = new String[256][HEIGHT]; private final String[][] biomes = new String[256][HEIGHT];
private final int[] ground = new int[256]; private final int[] ground = new int[256];
private final int[] water = new int[256];
private RandomSource(Random random, int sample) { private RandomSource(Random random, int sample) {
for (int column = 0; column < 256; column++) { for (int column = 0; column < 256; column++) {
@@ -270,6 +328,8 @@ public class SavedTerrainCaptureEquivalenceTest {
} }
} }
ground[column] = sample % 4 == 0 ? -1 : random.nextInt(HEIGHT); ground[column] = sample % 4 == 0 ? -1 : random.nextInt(HEIGHT);
water[column] = ground[column] < 0 || random.nextInt(3) == 0 ? -1
: Math.min(HEIGHT - 1, ground[column] + random.nextInt(48));
} }
} }
@@ -279,6 +339,12 @@ public class SavedTerrainCaptureEquivalenceTest {
return value < 0 ? OptionalInt.empty() : OptionalInt.of(value); return value < 0 ? OptionalInt.empty() : OptionalInt.of(value);
} }
@Override
public OptionalInt waterSurface(int localX, int localZ) {
int value = water[localX * 16 + localZ];
return value < 0 ? OptionalInt.empty() : OptionalInt.of(value);
}
@Override @Override
public BoundaryColumnGeometry.Voxel voxel(int localX, int worldY, int localZ) { public BoundaryColumnGeometry.Voxel voxel(int localX, int worldY, int localZ) {
return voxels[localX * 16 + localZ][worldY - MINIMUM_Y]; return voxels[localX * 16 + localZ][worldY - MINIMUM_Y];
@@ -4,6 +4,7 @@ import org.junit.Test;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.OptionalInt;
import java.util.concurrent.CyclicBarrier; import java.util.concurrent.CyclicBarrier;
import java.util.concurrent.ExecutorService; import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors; import java.util.concurrent.Executors;
@@ -22,6 +23,8 @@ public class TransitionDisplacementFieldTest extends GenerationHistorySupport {
"minecraft:air", BoundaryColumnGeometry.Phase.AIR, "", false); "minecraft:air", BoundaryColumnGeometry.Phase.AIR, "", false);
private static final BoundaryColumnGeometry.Voxel WATER = new BoundaryColumnGeometry.Voxel( private static final BoundaryColumnGeometry.Voxel WATER = new BoundaryColumnGeometry.Voxel(
"minecraft:water[level=0]", BoundaryColumnGeometry.Phase.FLUID, "minecraft:water[level=0]", false); "minecraft:water[level=0]", BoundaryColumnGeometry.Phase.FLUID, "minecraft:water[level=0]", false);
private static final BoundaryColumnGeometry.Voxel ICE = new BoundaryColumnGeometry.Voxel(
"minecraft:ice", BoundaryColumnGeometry.Phase.SOLID, "", false);
@Test @Test
public void matchesFlatSeamAndReturnsToNativeTerrainInBothDirections() throws Exception { public void matchesFlatSeamAndReturnsToNativeTerrainInBothDirections() throws Exception {
@@ -108,22 +111,63 @@ public class TransitionDisplacementFieldTest extends GenerationHistorySupport {
} }
} }
@Test
public void seaLayerCapsKeepTheHistoricalSeaSurfaceAboveTheSeabed() throws Exception {
for (int capBottom : new int[]{37, 25}) {
TransitionDisplacementField field = field(new SavedTerrainChunk.VoxelSource() {
@Override
public OptionalInt groundSurface(int localX, int localZ) {
return OptionalInt.of(24);
}
@Override
public OptionalInt waterSurface(int localX, int localZ) {
return OptionalInt.of(40);
}
@Override
public BoundaryColumnGeometry.Voxel voxel(int x, int y, int z) {
return y <= 24 ? STONE : y < capBottom ? WATER : y <= 40 ? ICE : AIR;
}
@Override
public String biome(int x, int y, int z) {
return "minecraft:frozen_ocean";
}
}, (x, z) -> 24D);
for (int x = 0; x < 32; x++) {
assertEquals(24D, field.height(x, 8, 24D), 0D);
assertEquals(40D, field.fluidHeight(x, 8, 40D), 0.0000001D);
}
assertEquals(40D, field.fluidHeight(0, 8, 48D), 0.0000001D);
if (capBottom == 37) {
assertEquals("minecraft:water[level=0]", field.fluidStateKey(0, 8));
} else {
assertNull(field.fluidStateKey(0, 8));
}
}
}
private TransitionDisplacementField field(int ground, int fluid, TransitionDisplacementField.HeightSource source) private TransitionDisplacementField field(int ground, int fluid, TransitionDisplacementField.HeightSource source)
throws Exception { throws Exception {
return field(new SavedTerrainChunk.VoxelSource() {
@Override
public BoundaryColumnGeometry.Voxel voxel(int x, int y, int z) {
return y <= ground ? STONE : y <= fluid ? WATER : AIR;
}
@Override
public String biome(int x, int y, int z) {
return "minecraft:plains";
}
}, source);
}
private TransitionDisplacementField field(SavedTerrainChunk.VoxelSource voxels, TransitionDisplacementField.HeightSource source)
throws Exception {
GenerationBoundary boundary = GenerationBoundary.freeze("boundary", List.of( GenerationBoundary boundary = GenerationBoundary.freeze("boundary", List.of(
new GenerationBoundary.ChunkCoordinate(-1, 0))); new GenerationBoundary.ChunkCoordinate(-1, 0)));
SavedTerrainChunk terrain = SavedTerrainChunk.captureBoundary(-1, 0, 0, 128, "minecraft:noise", SavedTerrainChunk terrain = SavedTerrainChunk.captureBoundary(-1, 0, 0, 128, "minecraft:noise", voxels);
new SavedTerrainChunk.VoxelSource() {
@Override
public BoundaryColumnGeometry.Voxel voxel(int x, int y, int z) {
return y <= ground ? STONE : y <= fluid ? WATER : AIR;
}
@Override
public String biome(int x, int y, int z) {
return "minecraft:plains";
}
});
TerrainBoundarySignatureStore store = new TerrainBoundarySignatureStore(temporaryFolder.newFolder().toPath()); TerrainBoundarySignatureStore store = new TerrainBoundarySignatureStore(temporaryFolder.newFolder().toPath());
TerrainBoundarySignatureStore.Snapshot snapshot = store.publish(2L, boundary, terrain::column); TerrainBoundarySignatureStore.Snapshot snapshot = store.publish(2L, boundary, terrain::column);
TransitionGenerationPlan plan = new TransitionGenerationPlan(new TransitionGenerationPlan.Specification( TransitionGenerationPlan plan = new TransitionGenerationPlan(new TransitionGenerationPlan.Specification(
@@ -67,7 +67,7 @@ public class TransitionGeometryBlenderTest {
BoundaryColumnGeometry blended = TransitionGeometryBlender.blendColumn(influence, 31, 0, current); BoundaryColumnGeometry blended = TransitionGeometryBlender.blendColumn(influence, 31, 0, current);
assertEquals(200, blended.surfaceOffsetNear(200D)); assertEquals(200, blended.surfaceOffsetNear(200D, BoundaryColumnGeometry.WaterCap.none()));
assertEquals(STONE, blended.voxelAt(416)); assertEquals(STONE, blended.voxelAt(416));
assertEquals(STONE, blended.voxelAt(447)); assertEquals(STONE, blended.voxelAt(447));
} }