diff --git a/core/src/main/java/art/arcane/iris/generation/stage/IrisPostModifier.java b/core/src/main/java/art/arcane/iris/generation/stage/IrisPostModifier.java index f83b6ff91..ff2f986bc 100644 --- a/core/src/main/java/art/arcane/iris/generation/stage/IrisPostModifier.java +++ b/core/src/main/java/art/arcane/iris/generation/stage/IrisPostModifier.java @@ -54,43 +54,44 @@ public class IrisPostModifier extends EngineAssignedModifier { int depth = output.getDepth(); int planeWidth = width + 2; int[] heights = heightPlane(x, z, width, depth, planeWidth); + boolean[] rivers = riverPlane(getComplex().getRiverCarveWeightStream(), x, z, width, depth); IrisDimension dimension = getDimension(); boolean walls = dimension.isPostProcessingWalls(); boolean slabs = dimension.isPostProcessingSlabs(); for (int i = 0; i < width; i++) { for (int j = 0; j < depth; j++) { - post(i, j, sync, i + x, j + z, context, heights, planeWidth, walls, slabs); + post(i, j, sync, i + x, j + z, context, heights, rivers, planeWidth, walls, slabs); } } getEngine().getMetrics().getPost().put(p.getMilliseconds()); } + static boolean[] riverPlane(ProceduralStream carveWeights, int x, int z, int width, int depth) { + int planeWidth = width + 2; + boolean[] rivers = new boolean[planeWidth * (depth + 2)]; + for (int j = -1; j <= depth; j++) { + boolean edge = j == -1 || j == depth; + int from = edge ? 0 : -1; + int to = edge ? width - 1 : width; + int row = (j + 1) * planeWidth; + for (int i = from; i <= to; i++) { + rivers[row + i + 1] = carveWeights.get(x + i, z + j) > 0D; + } + } + return rivers; + } + + static boolean skipsHydrologyColumn(boolean[] rivers, int center, int planeWidth) { + return rivers[center] || rivers[center + 1] || rivers[center - 1] + || rivers[center + planeWidth] || rivers[center - planeWidth]; + } + /** * Every column reads its own height plus its four neighbours, so adjacent columns would otherwise * resolve the same height stream entry up to five times. Resolve the padded plane once instead. The * four diagonal corners are never read, so they are left unresolved. */ - private boolean riverColumn(int x, int z) { - ProceduralStream carveWeights = getComplex().getRiverCarveWeightStream(); - return skipsHydrologyColumn(new double[] { - carveWeights.get(x, z), - carveWeights.get(x + 1, z), - carveWeights.get(x - 1, z), - carveWeights.get(x, z + 1), - carveWeights.get(x, z - 1) - }); - } - - static boolean skipsHydrologyColumn(double[] carveWeights) { - for (double weight : carveWeights) { - if (weight > 0D) { - return true; - } - } - return false; - } - private int[] heightPlane(int x, int z, int width, int depth, int planeWidth) { int[] heights = new int[planeWidth * (depth + 2)]; EngineMantle mantle = getEngine().getMantle(); @@ -109,7 +110,7 @@ public class IrisPostModifier extends EngineAssignedModifier { return heights; } - private void post(int currentPostX, int currentPostZ, Hunk currentData, int x, int z, ChunkContext context, int[] heights, int planeWidth, boolean walls, boolean slabs) { + private void post(int currentPostX, int currentPostZ, Hunk currentData, int x, int z, ChunkContext context, int[] heights, boolean[] rivers, int planeWidth, boolean walls, boolean slabs) { // x/z are world coordinates, the hunk is indexed relative to this chunk origin. int originX = x - currentPostX; int originZ = z - currentPostZ; @@ -120,7 +121,7 @@ public class IrisPostModifier extends EngineAssignedModifier { int hc = heights[center - 1]; int hd = heights[center - planeWidth]; int fluidHeight = (int) Math.round(getComplex().getRiverWaterSurfaceStream().get(x, z)); - boolean river = riverColumn(x, z); + boolean river = skipsHydrologyColumn(rivers, center, planeWidth); // Floating Nibs int g = 0; diff --git a/core/src/test/java/art/arcane/iris/generation/stage/IrisPostModifierHydrologyTest.java b/core/src/test/java/art/arcane/iris/generation/stage/IrisPostModifierHydrologyTest.java index c41a878d8..ed572c868 100644 --- a/core/src/test/java/art/arcane/iris/generation/stage/IrisPostModifierHydrologyTest.java +++ b/core/src/test/java/art/arcane/iris/generation/stage/IrisPostModifierHydrologyTest.java @@ -1,20 +1,87 @@ package art.arcane.iris.generation.stage; +import art.arcane.volmlib.util.stream.ProceduralStream; import org.junit.Test; +import java.util.HashMap; +import java.util.Map; + +import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; public class IrisPostModifierHydrologyTest { @Test public void columnsTouchingARiverFootprintSkipThePostPasses() { - assertTrue(IrisPostModifier.skipsHydrologyColumn(new double[] {1D, 0D, 0D, 0D, 0D})); - assertTrue(IrisPostModifier.skipsHydrologyColumn(new double[] {0D, 0D, 0.5D, 0D, 0D})); - assertTrue(IrisPostModifier.skipsHydrologyColumn(new double[] {0D, 0D, 0D, 0D, 0.75D})); + for (int index : new int[] {4, 3, 5, 1, 7}) { + boolean[] rivers = new boolean[9]; + rivers[index] = true; + assertTrue(IrisPostModifier.skipsHydrologyColumn(rivers, 4, 3)); + } } @Test public void columnsAwayFromRiversKeepThePostPasses() { - assertFalse(IrisPostModifier.skipsHydrologyColumn(new double[] {0D, 0D, 0D, 0D, 0D})); + boolean[] rivers = new boolean[9]; + rivers[0] = true; + rivers[2] = true; + rivers[6] = true; + rivers[8] = true; + assertFalse(IrisPostModifier.skipsHydrologyColumn(rivers, 4, 3)); + } + + @Test + public void paddedPlaneSamplesEachRequiredCoordinateOnce() { + Map samples = new HashMap<>(); + ProceduralStream stream = ProceduralStream.ofDouble((x, z) -> { + long key = ((long) (int) x << 32) | ((int) z & 0xffffffffL); + samples.merge(key, 1, Integer::sum); + return weight((int) x, (int) z); + }); + boolean[] rivers = IrisPostModifier.riverPlane(stream, -32, 48, 16, 16); + assertEquals(320, samples.size()); + for (int count : samples.values()) { + assertEquals(1, count); + } + assertPlaneMatchesPointSampling(rivers, -32, 48, 16, 16); + } + + @Test + public void rectangularPlanesPreservePositiveWeightRules() { + ProceduralStream stream = ProceduralStream.ofDouble((x, z) -> weight((int) x, (int) z)); + for (int width : new int[] {1, 3, 16}) { + for (int depth : new int[] {1, 5, 16}) { + boolean[] rivers = IrisPostModifier.riverPlane(stream, -19, -37, width, depth); + assertPlaneMatchesPointSampling(rivers, -19, -37, width, depth); + } + } + } + + private static void assertPlaneMatchesPointSampling(boolean[] rivers, int x, int z, int width, int depth) { + int planeWidth = width + 2; + for (int localX = 0; localX < width; localX++) { + for (int localZ = 0; localZ < depth; localZ++) { + int worldX = x + localX; + int worldZ = z + localZ; + boolean expected = weight(worldX, worldZ) > 0D || weight(worldX + 1, worldZ) > 0D + || weight(worldX - 1, worldZ) > 0D || weight(worldX, worldZ + 1) > 0D + || weight(worldX, worldZ - 1) > 0D; + assertEquals(expected, IrisPostModifier.skipsHydrologyColumn(rivers, + (localZ + 1) * planeWidth + localX + 1, planeWidth)); + } + } + } + + private static double weight(int x, int z) { + return switch (Math.floorMod(x * 31 + z * 17, 8)) { + case 0 -> Double.NaN; + case 1 -> -1D; + case 2 -> 0D; + case 3 -> -0D; + case 4 -> Double.MIN_VALUE; + case 5 -> 1D; + case 6 -> Double.POSITIVE_INFINITY; + default -> Double.NEGATIVE_INFINITY; + }; } } diff --git a/core/src/test/java/art/arcane/iris/generation/stage/IrisPostModifierSlabSupportTest.java b/core/src/test/java/art/arcane/iris/generation/stage/IrisPostModifierSlabSupportTest.java index 7a635f226..3e6f2cffd 100644 --- a/core/src/test/java/art/arcane/iris/generation/stage/IrisPostModifierSlabSupportTest.java +++ b/core/src/test/java/art/arcane/iris/generation/stage/IrisPostModifierSlabSupportTest.java @@ -91,9 +91,9 @@ public class IrisPostModifierSlabSupportTest { heights[2 * PLANE_WIDTH + 3] = SURFACE + 1; Method post = IrisPostModifier.class.getDeclaredMethod("post", int.class, int.class, Hunk.class, - int.class, int.class, ChunkContext.class, int[].class, int.class, boolean.class, boolean.class); + int.class, int.class, ChunkContext.class, int[].class, boolean[].class, int.class, boolean.class, boolean.class); post.setAccessible(true); - post.invoke(modifier, 1, 1, output, 1, 1, context, heights, PLANE_WIDTH, false, true); + post.invoke(modifier, 1, 1, output, 1, 1, context, heights, new boolean[heights.length], PLANE_WIDTH, false, true); return output.get(1, SURFACE + 1, 1); } diff --git a/core/src/test/java/art/arcane/iris/generation/stage/IrisPostModifierWallShoreTest.java b/core/src/test/java/art/arcane/iris/generation/stage/IrisPostModifierWallShoreTest.java index 88555daee..43ff0d941 100644 --- a/core/src/test/java/art/arcane/iris/generation/stage/IrisPostModifierWallShoreTest.java +++ b/core/src/test/java/art/arcane/iris/generation/stage/IrisPostModifierWallShoreTest.java @@ -123,9 +123,10 @@ public class IrisPostModifierWallShoreTest { heights[5] = SURFACE - 4; Method post = IrisPostModifier.class.getDeclaredMethod("post", int.class, int.class, Hunk.class, - int.class, int.class, ChunkContext.class, int[].class, int.class, boolean.class, boolean.class); + int.class, int.class, ChunkContext.class, int[].class, boolean[].class, int.class, boolean.class, boolean.class); post.setAccessible(true); - post.invoke(modifier, 0, 0, data, 0, 0, context, heights, PLANE_WIDTH, walls, false); + boolean[] rivers = IrisPostModifier.riverPlane(carveWeights, 0, 0, PLANE_WIDTH - 2, PLANE_WIDTH - 2); + post.invoke(modifier, 0, 0, data, 0, 0, context, heights, rivers, PLANE_WIDTH, walls, false); return data.get(0, SURFACE, 0); }