mirror of
https://github.com/VolmitSoftware/Iris.git
synced 2026-11-01 08:50:06 +00:00
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:
1 parent
c2eca3e6bd
commit
2462330787
11 files changed
+525
-50
No files matched your search
+40
-5
@@ -43,14 +43,14 @@ public final class IrisTransitionGeometryActuator extends EngineAssignedComponen
|
||||
if (plan == null || !plan.hasTransitionAtChunk(x >> 4, z >> 4)) {
|
||||
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);
|
||||
}
|
||||
|
||||
public static SavedTerrainChunk capture(int x, int z, Hunk<NativeBlockState> blocks,
|
||||
Hunk<NativeBiome> biomes, int minimumY,
|
||||
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
|
||||
? SavedTerrainChunk.captureBoundary(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) {
|
||||
continue;
|
||||
}
|
||||
double expectedFloor = context.getRoundedHeight(localX, localZ);
|
||||
int floor = blended.surfaceOffsetNear(expectedFloor);
|
||||
int ground = context.getRoundedHeight(localX, localZ);
|
||||
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();
|
||||
for (int offset = 0; offset < voxels.size(); 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) {
|
||||
fluidHeight++;
|
||||
}
|
||||
if (blended.capsWater(cap, floor)) {
|
||||
fluidHeight = cap.top();
|
||||
}
|
||||
if (fluidHeight == floor) {
|
||||
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 final int originX;
|
||||
private final int originZ;
|
||||
private final Hunk<NativeBlockState> blocks;
|
||||
private final Hunk<NativeBiome> biomes;
|
||||
private final int minimumY;
|
||||
@@ -116,7 +143,10 @@ public final class IrisTransitionGeometryActuator extends EngineAssignedComponen
|
||||
private NativeBlockState lastState;
|
||||
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.biomes = biomes;
|
||||
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))));
|
||||
}
|
||||
|
||||
@Override
|
||||
public OptionalInt waterSurface(int localX, int localZ) {
|
||||
return OptionalInt.of(waterHead(context.getComplex(), originX + localX, originZ + localZ, blocks.getHeight()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public String biome(int localX, int worldY, int localZ) throws IOException {
|
||||
NativeBiome biome = biomes.getRaw(localX, worldY - minimumY, localZ);
|
||||
|
||||
@@ -139,33 +139,73 @@ public final class BoundaryColumnGeometry implements NativeBlockColumn {
|
||||
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) {
|
||||
throw new IllegalArgumentException("A surface reference requires finite height and nonempty geometry");
|
||||
}
|
||||
int nearest = -1;
|
||||
double nearestDistance = Double.POSITIVE_INFINITY;
|
||||
for (int run = 0; run + 1 < runEnds.length; run++) {
|
||||
Voxel current = palette.get(paletteIndices[run]);
|
||||
Voxel above = palette.get(paletteIndices[run + 1]);
|
||||
if (current.phase() != Phase.SOLID || current.protectedContent()
|
||||
|| above.phase() == Phase.SOLID && !above.protectedContent()) {
|
||||
int offset = runEnds[run] - 1;
|
||||
if (!isTerrain(offset, runVoxel(run), cap) || isTerrain(offset + 1, runVoxel(run + 1), cap)) {
|
||||
continue;
|
||||
}
|
||||
int offset = runEnds[run] - 1;
|
||||
double distance = Math.abs(offset - expectedOffset);
|
||||
if (distance < nearestDistance) {
|
||||
nearest = offset;
|
||||
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) {
|
||||
return nearest;
|
||||
}
|
||||
for (int run = runEnds.length - 1; run >= 0; run--) {
|
||||
Voxel current = palette.get(paletteIndices[run]);
|
||||
if (current.phase() == Phase.SOLID && !current.protectedContent()) {
|
||||
return runEnds[run] - 1;
|
||||
Voxel current = runVoxel(run);
|
||||
if (current.phase() != Phase.SOLID || current.protectedContent()) {
|
||||
continue;
|
||||
}
|
||||
int top = cap.contains(runEnds[run] - 1) ? belowCap : runEnds[run] - 1;
|
||||
if (top >= (run == 0 ? 0 : runEnds[run - 1])) {
|
||||
return top;
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
@@ -220,6 +260,18 @@ public final class BoundaryColumnGeometry implements NativeBlockColumn {
|
||||
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() {
|
||||
if (palette.size() > Short.MAX_VALUE + 1 || runEnds.length > MAXIMUM_HEIGHT
|
||||
|| runEnds.length != paletteIndices.length) {
|
||||
@@ -333,6 +385,22 @@ public final class BoundaryColumnGeometry implements NativeBlockColumn {
|
||||
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 Voxel {
|
||||
Objects.requireNonNull(stateKey, "Block state key");
|
||||
|
||||
@@ -91,10 +91,15 @@ public final class SavedTerrainChunk {
|
||||
BoundaryColumnGeometry geometry = scan.runs.build(minimumY);
|
||||
OptionalInt groundSurface = source.groundSurface(x, z);
|
||||
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) {
|
||||
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);
|
||||
}
|
||||
columns.add(new TerrainBoundarySignature(
|
||||
@@ -200,6 +205,10 @@ public final class SavedTerrainChunk {
|
||||
return OptionalInt.empty();
|
||||
}
|
||||
|
||||
default OptionalInt waterSurface(int localX, int localZ) {
|
||||
return OptionalInt.empty();
|
||||
}
|
||||
|
||||
BoundaryColumnGeometry.Voxel voxel(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;
|
||||
if (wet) {
|
||||
int y = signature.geometry().minimumY() + signature.fluidHeight().getAsInt();
|
||||
fluidKey = signature.geometry().voxelAt(y).fluidStateKey();
|
||||
fluidKey = signature.geometry().fluidStateKeyAtOrBelow(signature.fluidHeight().getAsInt(),
|
||||
signature.oceanFloorHeight());
|
||||
}
|
||||
}
|
||||
column.total += weight;
|
||||
@@ -164,7 +164,7 @@ public final class TransitionDisplacementField {
|
||||
column.wetHeight += weight * signature.fluidHeight().getAsInt();
|
||||
column.minimumWetHeight = Math.min(column.minimumWetHeight, 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.fluidKey = fluidKey;
|
||||
}
|
||||
|
||||
+3
@@ -210,11 +210,14 @@ public final class EngineModeTerrainGenerationTest {
|
||||
when(biome.key()).thenReturn("minecraft:plains");
|
||||
ProceduralStream<Double> height = mock(ProceduralStream.class);
|
||||
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<IrisRegion> regions = mock(ProceduralStream.class);
|
||||
ProceduralStream<NativeBlockState> materials = mock(ProceduralStream.class);
|
||||
when(materials.get(anyDouble(), anyDouble())).thenReturn(stone);
|
||||
when(complex.getRawHeightStream()).thenReturn(height);
|
||||
when(complex.getWaterHeadStream()).thenReturn(waterHead);
|
||||
when(complex.getTrueBiomeStream()).thenReturn(biomes);
|
||||
when(complex.getCaveBiomeStream()).thenReturn(biomes);
|
||||
when(complex.getRegionStream()).thenReturn(regions);
|
||||
|
||||
+84
-11
@@ -23,6 +23,10 @@ import art.arcane.iris.testsupport.WaterScene;
|
||||
import art.arcane.iris.world.history.BoundaryColumnGeometry;
|
||||
import art.arcane.iris.world.history.BoundaryGeometryInfluence;
|
||||
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.TransitionGeometryBlender;
|
||||
import art.arcane.volmlib.nativelib.terrain.NativeBiome;
|
||||
@@ -43,7 +47,9 @@ import org.junit.Test;
|
||||
import org.junit.rules.TemporaryFolder;
|
||||
import org.junit.rules.Timeout;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
@@ -533,6 +539,39 @@ public class IrisComplexTransitionTest {
|
||||
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) {
|
||||
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));
|
||||
@@ -547,8 +586,10 @@ public class IrisComplexTransitionTest {
|
||||
private final NativeBlockState stone = block("minecraft:stone", false, false);
|
||||
private final NativeBlockState water = block("minecraft:water[level=0]", false, true);
|
||||
private final NativeBlockState air = block("minecraft:air", true, false);
|
||||
private final TransitionDisplacementField displacement = mock(TransitionDisplacementField.class);
|
||||
private final TransitionGenerationPlan plan = mock(TransitionGenerationPlan.class);
|
||||
private final NativeBlockState ice = block("minecraft:ice", false, false);
|
||||
private final TransitionDisplacementField displacement;
|
||||
private final TransitionGenerationPlan plan;
|
||||
private final int seaLayers;
|
||||
private final IrisComplex complex;
|
||||
private final FakeHydrology hydrology;
|
||||
|
||||
@@ -562,13 +603,14 @@ public class IrisComplexTransitionTest {
|
||||
|
||||
private TransitionStage(SeamWeights weights, boolean enabled, NaturalHeights natural, BoundaryColumnGeometry oldColumn)
|
||||
throws Exception {
|
||||
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));
|
||||
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))));
|
||||
this(seamPlan(weights, oldColumn), seamDisplacement(weights), enabled, natural, 0);
|
||||
}
|
||||
|
||||
private TransitionStage(TransitionGenerationPlan plan, TransitionDisplacementField displacement, boolean enabled,
|
||||
NaturalHeights natural, int seaLayers) throws Exception {
|
||||
this.plan = plan;
|
||||
this.displacement = displacement;
|
||||
this.seaLayers = seaLayers;
|
||||
AtomicReference<IrisComplex> built = new AtomicReference<>();
|
||||
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);
|
||||
@@ -581,7 +623,7 @@ public class IrisComplexTransitionTest {
|
||||
doReturn(mock(IrisImageMapRuntime.class)).when(complex).getImageMapRuntime();
|
||||
doReturn(water).when(complex).resolveWaterFluid(anyInt(), anyInt());
|
||||
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);
|
||||
when(registries.air()).thenReturn(air);
|
||||
when(registries.block("AIR")).thenReturn(air);
|
||||
@@ -594,6 +636,23 @@ public class IrisComplexTransitionTest {
|
||||
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) {
|
||||
return hydrology.runtime.raster(chunkX, 0);
|
||||
}
|
||||
@@ -622,7 +681,8 @@ public class IrisComplexTransitionTest {
|
||||
when(engine.getSeedManager()).thenReturn(mock(SeedManager.class));
|
||||
when(biome.generateLayers(eq(dimension), anyDouble(), anyDouble(), any(RNG.class), anyInt(), anyInt(), eq(data), same(complex)))
|
||||
.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);
|
||||
ChunkContext context = mock(ChunkContext.class);
|
||||
when(context.getRoundedHeight(anyInt(), anyInt())).thenAnswer(invocation ->
|
||||
@@ -712,6 +772,19 @@ public class IrisComplexTransitionTest {
|
||||
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) {
|
||||
ArrayList<BoundaryColumnGeometry.Voxel> voxels = new ArrayList<>(HEIGHT);
|
||||
for (int y = 0; y < HEIGHT; y++) {
|
||||
|
||||
+104
-1
@@ -7,6 +7,7 @@ import art.arcane.iris.world.history.BoundaryColumnGeometry;
|
||||
import art.arcane.iris.world.history.BoundaryGeometryInfluence;
|
||||
import art.arcane.iris.world.history.NativeTerrainReceipt;
|
||||
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.spi.IrisPlatform;
|
||||
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.generation.context.ChunkContext;
|
||||
import art.arcane.volmlib.util.hunk.Hunk;
|
||||
import art.arcane.volmlib.util.stream.ProceduralStream;
|
||||
import org.junit.After;
|
||||
import org.junit.Before;
|
||||
import org.junit.ClassRule;
|
||||
@@ -23,6 +25,7 @@ import org.junit.Test;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.OptionalInt;
|
||||
|
||||
import static org.junit.Assert.assertSame;
|
||||
import static org.junit.Assert.assertEquals;
|
||||
@@ -79,6 +82,9 @@ public class IrisTransitionGeometryActuatorTest {
|
||||
when(biome.key()).thenReturn("minecraft:plains");
|
||||
biomes.fill(biome);
|
||||
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);
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
@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) {
|
||||
return context(material, 3);
|
||||
}
|
||||
@@ -159,8 +257,12 @@ public class IrisTransitionGeometryActuatorTest {
|
||||
solid ? BoundaryColumnGeometry.Phase.SOLID : fluid ? BoundaryColumnGeometry.Phase.FLUID : BoundaryColumnGeometry.Phase.AIR,
|
||||
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,
|
||||
List.of(new BoundaryGeometryInfluence.Contribution(BoundaryColumnGeometry.fromVoxels(0, voxels), 1D)));
|
||||
List.of(new BoundaryGeometryInfluence.Contribution(historical, 1D)));
|
||||
TransitionGenerationPlan plan = mock(TransitionGenerationPlan.class);
|
||||
when(plan.geometryAt(anyInt(), anyInt())).thenReturn(influence);
|
||||
when(plan.hasTransitionAtChunk(anyInt(), anyInt())).thenReturn(true);
|
||||
@@ -169,6 +271,7 @@ public class IrisTransitionGeometryActuatorTest {
|
||||
when(plan.terrainSampleAt(anyInt(), anyInt())).thenReturn(sample);
|
||||
IrisComplex complex = mock(IrisComplex.class);
|
||||
when(complex.getTransitionGenerationPlan()).thenReturn(plan);
|
||||
when(complex.getWaterHeadStream()).thenReturn(ProceduralStream.ofDouble((x, z) -> waterHead));
|
||||
ChunkContext context = mock(ChunkContext.class);
|
||||
when(context.getComplex()).thenReturn(complex);
|
||||
when(context.getRoundedHeight(anyInt(), anyInt())).thenReturn(3);
|
||||
|
||||
@@ -23,6 +23,8 @@ public class BoundaryColumnGeometryTest {
|
||||
"minecraft:stone", BoundaryColumnGeometry.Phase.SOLID, "", false);
|
||||
private static final BoundaryColumnGeometry.Voxel AIR = new BoundaryColumnGeometry.Voxel(
|
||||
"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
|
||||
public void solidAboveMatchesDenseColumnsWithProtectionAndExtremeWorldCoordinates() {
|
||||
@@ -92,8 +94,65 @@ public class BoundaryColumnGeometryTest {
|
||||
}
|
||||
BoundaryColumnGeometry geometry = BoundaryColumnGeometry.fromVoxels(-64, voxels);
|
||||
|
||||
assertEquals(50, geometry.surfaceOffsetNear(50D));
|
||||
assertEquals(50, geometry.surfaceOffsetNear(70D));
|
||||
assertEquals(50, geometry.surfaceOffsetNear(50D, BoundaryColumnGeometry.WaterCap.none()));
|
||||
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
|
||||
@@ -114,24 +173,29 @@ public class BoundaryColumnGeometryTest {
|
||||
}
|
||||
}
|
||||
BoundaryColumnGeometry geometry = BoundaryColumnGeometry.fromVoxels(-64, voxels);
|
||||
for (BoundaryColumnGeometry.WaterCap cap : List.of(BoundaryColumnGeometry.WaterCap.none(),
|
||||
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), geometry.surfaceOffsetNear(expected));
|
||||
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));
|
||||
assertThrows(IllegalArgumentException.class, () -> solid.surfaceOffsetNear(Double.NaN));
|
||||
assertThrows(IllegalArgumentException.class, () -> solid.surfaceOffsetNear(Double.POSITIVE_INFINITY));
|
||||
assertThrows(IllegalArgumentException.class, () -> solid.surfaceOffsetNear(Double.NaN,
|
||||
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;
|
||||
double distance = Double.POSITIVE_INFINITY;
|
||||
for (int index = 0; index + 1 < voxels.size(); index++) {
|
||||
BoundaryColumnGeometry.Voxel current = voxels.get(index);
|
||||
BoundaryColumnGeometry.Voxel above = voxels.get(index + 1);
|
||||
if (current.phase() == BoundaryColumnGeometry.Phase.SOLID && !current.protectedContent()
|
||||
&& (above.phase() != BoundaryColumnGeometry.Phase.SOLID || above.protectedContent())
|
||||
if (denseTerrain(voxels, index, cap) && !denseTerrain(voxels, index + 1, cap)
|
||||
&& Math.abs(index - expected) < distance) {
|
||||
selected = index;
|
||||
distance = Math.abs(index - expected);
|
||||
@@ -141,14 +205,24 @@ public class BoundaryColumnGeometryTest {
|
||||
return selected;
|
||||
}
|
||||
for (int index = voxels.size() - 1; index >= 0; index--) {
|
||||
BoundaryColumnGeometry.Voxel voxel = voxels.get(index);
|
||||
if (voxel.phase() == BoundaryColumnGeometry.Phase.SOLID && !voxel.protectedContent()) {
|
||||
if (denseTerrain(voxels, index, cap)) {
|
||||
return index;
|
||||
}
|
||||
}
|
||||
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
|
||||
public void enclosedOpenQueriesIncludeUpperCavesAndExcludeOpenSky() {
|
||||
BoundaryColumnGeometry geometry = BoundaryColumnGeometry.fromVoxels(-4,
|
||||
|
||||
+67
-1
@@ -29,6 +29,7 @@ public class SavedTerrainCaptureEquivalenceTest {
|
||||
private static final List<BoundaryColumnGeometry.Voxel> STATES = List.of(
|
||||
voxel("minecraft:stone", 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:cave_air", BoundaryColumnGeometry.Phase.AIR, "", 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);
|
||||
OptionalInt groundSurface = source.groundSurface(x, z);
|
||||
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);
|
||||
}
|
||||
columns.add(new TerrainBoundarySignature(
|
||||
@@ -168,6 +176,50 @@ public class SavedTerrainCaptureEquivalenceTest {
|
||||
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) {
|
||||
TreeSet<BoundaryColumnGeometry.Voxel> used = new TreeSet<>(VOXEL_ORDER);
|
||||
BoundaryColumnGeometry.Voxel previous = null;
|
||||
@@ -216,6 +268,11 @@ public class SavedTerrainCaptureEquivalenceTest {
|
||||
return source.groundSurface(localX, localZ);
|
||||
}
|
||||
|
||||
@Override
|
||||
public OptionalInt waterSurface(int localX, int localZ) {
|
||||
return source.waterSurface(localX, localZ);
|
||||
}
|
||||
|
||||
@Override
|
||||
public BoundaryColumnGeometry.Voxel voxel(int localX, int worldY, int 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 String[][] biomes = new String[256][HEIGHT];
|
||||
private final int[] ground = new int[256];
|
||||
private final int[] water = new int[256];
|
||||
|
||||
private RandomSource(Random random, int sample) {
|
||||
for (int column = 0; column < 256; column++) {
|
||||
@@ -270,6 +328,8 @@ public class SavedTerrainCaptureEquivalenceTest {
|
||||
}
|
||||
}
|
||||
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);
|
||||
}
|
||||
|
||||
@Override
|
||||
public OptionalInt waterSurface(int localX, int localZ) {
|
||||
int value = water[localX * 16 + localZ];
|
||||
return value < 0 ? OptionalInt.empty() : OptionalInt.of(value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public BoundaryColumnGeometry.Voxel voxel(int localX, int worldY, int localZ) {
|
||||
return voxels[localX * 16 + localZ][worldY - MINIMUM_Y];
|
||||
|
||||
+49
-5
@@ -4,6 +4,7 @@ import org.junit.Test;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.OptionalInt;
|
||||
import java.util.concurrent.CyclicBarrier;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
@@ -22,6 +23,8 @@ public class TransitionDisplacementFieldTest extends GenerationHistorySupport {
|
||||
"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);
|
||||
private static final BoundaryColumnGeometry.Voxel ICE = new BoundaryColumnGeometry.Voxel(
|
||||
"minecraft:ice", BoundaryColumnGeometry.Phase.SOLID, "", false);
|
||||
|
||||
@Test
|
||||
public void matchesFlatSeamAndReturnsToNativeTerrainInBothDirections() throws Exception {
|
||||
@@ -108,12 +111,46 @@ 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)
|
||||
throws Exception {
|
||||
GenerationBoundary boundary = GenerationBoundary.freeze("boundary", List.of(
|
||||
new GenerationBoundary.ChunkCoordinate(-1, 0)));
|
||||
SavedTerrainChunk terrain = SavedTerrainChunk.captureBoundary(-1, 0, 0, 128, "minecraft:noise",
|
||||
new SavedTerrainChunk.VoxelSource() {
|
||||
return field(new SavedTerrainChunk.VoxelSource() {
|
||||
@Override
|
||||
public BoundaryColumnGeometry.Voxel voxel(int x, int y, int z) {
|
||||
return y <= ground ? STONE : y <= fluid ? WATER : AIR;
|
||||
@@ -123,7 +160,14 @@ public class TransitionDisplacementFieldTest extends GenerationHistorySupport {
|
||||
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(
|
||||
new GenerationBoundary.ChunkCoordinate(-1, 0)));
|
||||
SavedTerrainChunk terrain = SavedTerrainChunk.captureBoundary(-1, 0, 0, 128, "minecraft:noise", voxels);
|
||||
TerrainBoundarySignatureStore store = new TerrainBoundarySignatureStore(temporaryFolder.newFolder().toPath());
|
||||
TerrainBoundarySignatureStore.Snapshot snapshot = store.publish(2L, boundary, terrain::column);
|
||||
TransitionGenerationPlan plan = new TransitionGenerationPlan(new TransitionGenerationPlan.Specification(
|
||||
|
||||
@@ -67,7 +67,7 @@ public class TransitionGeometryBlenderTest {
|
||||
|
||||
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(447));
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user