diff --git a/core/src/main/java/art/arcane/iris/generation/runtime/DimensionTerrainContext.java b/core/src/main/java/art/arcane/iris/generation/runtime/DimensionTerrainContext.java index 53b114a3c..9cd82c8cc 100644 --- a/core/src/main/java/art/arcane/iris/generation/runtime/DimensionTerrainContext.java +++ b/core/src/main/java/art/arcane/iris/generation/runtime/DimensionTerrainContext.java @@ -5,6 +5,7 @@ import art.arcane.iris.generation.stream.GenerationStreams; import art.arcane.iris.configuration.IrisSettings; import art.arcane.iris.pack.loading.IrisData; import art.arcane.iris.generation.image.IrisImageMapRuntime; +import art.arcane.iris.generation.terrain.ContinentGenerator; import art.arcane.iris.generation.terrain.InferredType; import art.arcane.iris.generation.biome.IrisBiome; import art.arcane.iris.generation.terrain.IrisDimension; @@ -258,9 +259,7 @@ public final class DimensionTerrainContext implements DataProvider { inferredStreams.put(InferredType.SHORE, shoreBiomeStream); ProceduralStream bridgeStream = focusBiome == null - ? dimension.getContinentalStyle() - .create(rng.nextParallelRNG(234234565), dimensionData) - .bake().scale(1D / dimension.getContinentZoom()).bake().stream() + ? ContinentGenerator.create(dimension, rng, dimensionData).stream() .convert(value -> value >= dimension.getLandChance() ? InferredType.SEA : InferredType.LAND) diff --git a/core/src/main/java/art/arcane/iris/generation/runtime/IrisComplex.java b/core/src/main/java/art/arcane/iris/generation/runtime/IrisComplex.java index 73b69f467..56c35773e 100644 --- a/core/src/main/java/art/arcane/iris/generation/runtime/IrisComplex.java +++ b/core/src/main/java/art/arcane/iris/generation/runtime/IrisComplex.java @@ -52,6 +52,7 @@ import art.arcane.iris.generation.decoration.IrisDecorationPart; import art.arcane.iris.generation.decoration.IrisDecorator; import art.arcane.iris.generation.noise.IrisGenerator; import art.arcane.iris.generation.noise.IrisGeneratorStyle; +import art.arcane.iris.generation.terrain.ContinentGenerator; import art.arcane.iris.generation.terrain.IrisDimension; import art.arcane.iris.generation.context.IrisContext; import art.arcane.iris.generation.context.ChunkContext; @@ -390,8 +391,7 @@ public class IrisComplex implements DataProvider { .convertAware2D(ProceduralStream::get), "shoreBiomeStream", engine, cacheSize); inferredStreams.put(InferredType.SHORE, shoreBiomeStream); bridgeStream = focusedBiomes != null ? focusedBiomes.convert(IrisBiome::getInferredType) : - GenerationStreams.cache2D(engine.getDimension().getContinentalStyle().create(rng.nextParallelRNG(234234565), getData()) - .bake().scale(1D / engine.getDimension().getContinentZoom()).bake().stream() + GenerationStreams.cache2D(ContinentGenerator.create(engine.getDimension(), rng, getData()).stream() .convert((v) -> v >= engine.getDimension().getLandChance() ? InferredType.SEA : InferredType.LAND), "bridgeStream", engine, cacheSize); ProceduralStream proceduralBaseBiomeStream = focusedBiomes != null ? focusedBiomes : GenerationStreams.cache2D(bridgeStream.convertAware2D((t, x, z) -> inferredStreams.get(t).get(x, z)) diff --git a/core/src/main/java/art/arcane/iris/generation/terrain/ContinentGenerator.java b/core/src/main/java/art/arcane/iris/generation/terrain/ContinentGenerator.java new file mode 100644 index 000000000..4423d81b9 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/generation/terrain/ContinentGenerator.java @@ -0,0 +1,28 @@ +package art.arcane.iris.generation.terrain; + +import art.arcane.iris.pack.loading.IrisData; +import art.arcane.volmlib.util.math.RNG; +import art.arcane.volmlib.util.noise.CNG; + +public final class ContinentGenerator { + private static final int CONTINENT_SEED_SALT = 234234565; + + private ContinentGenerator() { + } + + public static CNG create(IrisDimension dimension, RNG parentSeedSource, IrisData packData) { + return scaleByContinentZoom(createContinentalStyleGenerator(dimension, parentSeedSource, packData), dimension); + } + + private static CNG createContinentalStyleGenerator(IrisDimension dimension, RNG parentSeedSource, IrisData packData) { + return dimension.getContinentalStyle().createNoCache(deriveContinentSeed(parentSeedSource), packData); + } + + static RNG deriveContinentSeed(RNG parentSeedSource) { + return parentSeedSource.nextParallelRNG(CONTINENT_SEED_SALT); + } + + private static CNG scaleByContinentZoom(CNG generator, IrisDimension dimension) { + return generator.bake().scale(1D / dimension.getContinentZoom()).bake(); + } +} diff --git a/core/src/test/java/art/arcane/iris/generation/terrain/ContinentGeneratorTest.java b/core/src/test/java/art/arcane/iris/generation/terrain/ContinentGeneratorTest.java new file mode 100644 index 000000000..5b98f12c4 --- /dev/null +++ b/core/src/test/java/art/arcane/iris/generation/terrain/ContinentGeneratorTest.java @@ -0,0 +1,147 @@ +package art.arcane.iris.generation.terrain; + +import art.arcane.iris.generation.noise.IrisGeneratorStyle; +import art.arcane.iris.generation.noise.NoiseStyle; +import art.arcane.iris.generation.runtime.Engine; +import art.arcane.iris.pack.loading.IrisData; +import art.arcane.volmlib.util.math.RNG; +import art.arcane.volmlib.util.noise.CNG; +import org.junit.Test; + +import java.util.Arrays; + +import static org.junit.Assert.assertArrayEquals; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +public class ContinentGeneratorTest { + private static final long WORLD_SEED = 7L; + private static final double POWER_OF_TWO_CONTINENT_ZOOM = 4D; + private static final double SQUARED_CONTINENT_ZOOM = POWER_OF_TWO_CONTINENT_ZOOM * POWER_OF_TWO_CONTINENT_ZOOM; + private static final double OVERWORLD_STYLE_ZOOM = 6D; + private static final double[][] SAMPLE_BLOCKS = {{1000D, -2000D}, {12.5D, -7.25D}, {-517D, 333D}}; + private static final double[][] BLOCKS_SHRUNK_BY_CONTINENT_ZOOM = shrinkByContinentZoom(SAMPLE_BLOCKS); + private static final double[] CONTINENT_MAP_OF_EXISTING_WORLDS = + {0.21889892332042843D, 0.6984934456740721D, 0.2708759584352327D}; + + private final IrisData packData = createPackDataThatBelongsToOneRunningEngine(); + private final IrisDimension baseZoomDimension = createDimensionWithContinentZoom(POWER_OF_TWO_CONTINENT_ZOOM); + private final IrisDimension squaredZoomDimension = createDimensionWithContinentZoom(SQUARED_CONTINENT_ZOOM); + private final IrisDimension overworldStyleDimension = createDimensionWithOverworldContinentalStyle(); + + @Test + public void doesTheContinentMapAtWorldStartMatchExistingWorlds() { + assertArrayEquals(CONTINENT_MAP_OF_EXISTING_WORLDS, sampleContinentMapAtWorldStart(baseZoomDimension), 0D); + } + + @Test + public void doesSquaredContinentZoomStretchTheContinentMapByTheBaseZoom() { + assertArrayEquals(sampleBaseContinentMapStretchedByBaseZoom(), + sampleContinentMapAtWorldStart(squaredZoomDimension), 0D); + } + + @Test + public void doesRebuildingTheSameWorldZoomItsContinentMapOnlyOnce() { + assertArrayEquals(CONTINENT_MAP_OF_EXISTING_WORLDS, sampleContinentMapAfterRebuild(baseZoomDimension), 0D); + } + + @Test + public void doesRebuildingTheSameWorldTwiceZoomItsContinentMapOnlyOnce() { + assertArrayEquals(CONTINENT_MAP_OF_EXISTING_WORLDS, sampleContinentMapAfterSecondRebuild(baseZoomDimension), 0D); + } + + @Test + public void doesRebuildingAWorldWithTheOverworldContinentalStyleZoomItsContinentMapOnlyOnce() { + assertArrayEquals(sampleOverworldContinentMapAtWorldStart(), sampleContinentMapAfterRebuild(overworldStyleDimension), 0D); + } + + @Test + public void doesBuildingTheContinentMapLeaveTheSharedStyleGeneratorUnscaled() { + assertArrayEquals(sampleUntouchedSharedStyleGenerator(), sampleSharedStyleGeneratorAfterContinentMapBuilt(baseZoomDimension), 0D); + } + + private double[] sampleContinentMapAtWorldStart(IrisDimension dimension) { + return sampleContinentMap(dimension, SAMPLE_BLOCKS); + } + + private double[] sampleBaseContinentMapStretchedByBaseZoom() { + return sampleContinentMap(baseZoomDimension, BLOCKS_SHRUNK_BY_CONTINENT_ZOOM); + } + + private double[] sampleContinentMapAfterRebuild(IrisDimension dimension) { + sampleContinentMapAtWorldStart(dimension); + return sampleContinentMapAtWorldStart(dimension); + } + + private double[] sampleContinentMapAfterSecondRebuild(IrisDimension dimension) { + sampleContinentMapAtWorldStart(dimension); + return sampleContinentMapAfterRebuild(dimension); + } + + private double[] sampleOverworldContinentMapAtWorldStart() { + return sampleContinentMapAtWorldStart(createDimensionWithOverworldContinentalStyle()); + } + + private double[] sampleSharedStyleGeneratorAfterContinentMapBuilt(IrisDimension dimension) { + sampleContinentMapAtWorldStart(dimension); + return sampleSharedStyleGenerator(dimension.getContinentalStyle()); + } + + private double[] sampleUntouchedSharedStyleGenerator() { + return sampleSharedStyleGenerator(createSimplexStyle()); + } + + private double[] sampleSharedStyleGenerator(IrisGeneratorStyle style) { + return sampleAtBlocks(style.create(ContinentGenerator.deriveContinentSeed(new RNG(WORLD_SEED)), packData), SAMPLE_BLOCKS); + } + + private double[] sampleContinentMap(IrisDimension dimension, double[][] blocks) { + return sampleAtBlocks(ContinentGenerator.create(dimension, new RNG(WORLD_SEED), packData), blocks); + } + + private static double[] sampleAtBlocks(CNG continentGenerator, double[][] blocks) { + return Arrays.stream(blocks).mapToDouble(block -> sampleAt(continentGenerator, block)).toArray(); + } + + private static double sampleAt(CNG continentGenerator, double[] block) { + return continentGenerator.noise(block[0], block[1]); + } + + private static double[][] shrinkByContinentZoom(double[][] blocks) { + return Arrays.stream(blocks).map(block -> shrinkByContinentZoom(block)).toArray(double[][]::new); + } + + private static double[] shrinkByContinentZoom(double[] block) { + return new double[]{block[0] / POWER_OF_TWO_CONTINENT_ZOOM, block[1] / POWER_OF_TWO_CONTINENT_ZOOM}; + } + + private static IrisDimension createDimensionWithContinentZoom(double zoom) { + return new IrisDimension().setContinentalStyle(createSimplexStyle()).setContinentZoom(zoom); + } + + private static IrisGeneratorStyle createSimplexStyle() { + return new IrisGeneratorStyle(NoiseStyle.SIMPLEX); + } + + private static IrisDimension createDimensionWithOverworldContinentalStyle() { + return createDimensionWithContinentZoom(POWER_OF_TWO_CONTINENT_ZOOM).setContinentalStyle(createOverworldContinentalStyle()); + } + + private static IrisGeneratorStyle createOverworldContinentalStyle() { + return new IrisGeneratorStyle(NoiseStyle.NOWHERE_CELLULAR).setZoom(OVERWORLD_STYLE_ZOOM).setFracture(createSmokeFracture()); + } + + private static IrisGeneratorStyle createSmokeFracture() { + return new IrisGeneratorStyle(NoiseStyle.FRACTAL_SMOKE).setFracture(new IrisGeneratorStyle(NoiseStyle.STATIC)); + } + + private static IrisData createPackDataThatBelongsToOneRunningEngine() { + IrisData newPackData = mock(IrisData.class); + bindToNewRunningEngine(newPackData); + return newPackData; + } + + private static void bindToNewRunningEngine(IrisData packData) { + when(packData.getEngine()).thenReturn(mock(Engine.class)); + } +} diff --git a/core/src/test/java/art/arcane/iris/testsupport/ExpectedToFailUntilFixedRuleTest.java b/core/src/test/java/art/arcane/iris/testsupport/ExpectedToFailUntilFixedRuleTest.java new file mode 100644 index 000000000..2f8bd6d53 --- /dev/null +++ b/core/src/test/java/art/arcane/iris/testsupport/ExpectedToFailUntilFixedRuleTest.java @@ -0,0 +1,81 @@ +package art.arcane.iris.testsupport; + +import org.junit.AssumptionViolatedException; +import org.junit.Test; +import org.junit.function.ThrowingRunnable; +import org.junit.runner.Description; +import org.junit.runners.model.Statement; + +import java.lang.annotation.Annotation; + +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertThrows; +import static org.junit.Assert.assertTrue; + +public class ExpectedToFailUntilFixedRuleTest { + private static final String KNOWN_BUG = "known bug"; + private static final String BUG_STILL_PRESENT = "bug still present"; + private static final String REMOVAL_REMINDER = "remove @ExpectedToFailUntilFixed: " + KNOWN_BUG; + private static final Statement PASSING = new Statement() { + @Override + public void evaluate() { + } + }; + + private final ExpectedToFailUntilFixedRule rule = new ExpectedToFailUntilFixedRule(); + private final AssertionError bugStillPresent = new AssertionError(BUG_STILL_PRESENT); + private final IllegalStateException crash = new IllegalStateException(BUG_STILL_PRESENT); + + @Test + public void doesAMarkedTestThatStillFailsGetReportedAsSkippedWithItsFailure() { + assertSame(bugStillPresent, assertThrows(AssumptionViolatedException.class, runMarked(failingWith(bugStillPresent))).getCause()); + } + + @Test + public void doesAMarkedTestThatPassesFailWithARemovalReminder() { + assertTrue(messageOfAssertionFailure(runMarked(PASSING)).contains(REMOVAL_REMINDER)); + } + + @Test + public void doesACrashInAMarkedTestStillFail() { + assertSame(crash, assertThrows(IllegalStateException.class, runMarked(failingWith(crash)))); + } + + @Test + public void doesAnUnmarkedFailingTestStillFail() { + assertSame(bugStillPresent, assertThrows(AssertionError.class, runUnmarked(failingWith(bugStillPresent)))); + } + + @ExpectedToFailUntilFixed(KNOWN_BUG) + private void markedTestTemplate() { + } + + private ThrowingRunnable runMarked(Statement test) { + return () -> rule.apply(test, describe(markerOfTemplate())).evaluate(); + } + + private ThrowingRunnable runUnmarked(Statement test) { + return () -> rule.apply(test, describe()).evaluate(); + } + + private ExpectedToFailUntilFixed markerOfTemplate() throws NoSuchMethodException { + return getClass().getDeclaredMethod("markedTestTemplate").getAnnotation(ExpectedToFailUntilFixed.class); + } + + private static String messageOfAssertionFailure(ThrowingRunnable test) { + return assertThrows(AssertionError.class, test).getMessage(); + } + + private static Description describe(Annotation... annotations) { + return Description.createTestDescription(ExpectedToFailUntilFixedRuleTest.class, "example", annotations); + } + + private static Statement failingWith(Throwable failure) { + return new Statement() { + @Override + public void evaluate() throws Throwable { + throw failure; + } + }; + } +} diff --git a/core/src/testFixtures/java/art/arcane/iris/testsupport/ExpectedToFailUntilFixed.java b/core/src/testFixtures/java/art/arcane/iris/testsupport/ExpectedToFailUntilFixed.java new file mode 100644 index 000000000..78dd73a37 --- /dev/null +++ b/core/src/testFixtures/java/art/arcane/iris/testsupport/ExpectedToFailUntilFixed.java @@ -0,0 +1,12 @@ +package art.arcane.iris.testsupport; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.METHOD) +public @interface ExpectedToFailUntilFixed { + String value(); +} diff --git a/core/src/testFixtures/java/art/arcane/iris/testsupport/ExpectedToFailUntilFixedRule.java b/core/src/testFixtures/java/art/arcane/iris/testsupport/ExpectedToFailUntilFixedRule.java new file mode 100644 index 000000000..182f1b979 --- /dev/null +++ b/core/src/testFixtures/java/art/arcane/iris/testsupport/ExpectedToFailUntilFixedRule.java @@ -0,0 +1,42 @@ +package art.arcane.iris.testsupport; + +import org.junit.AssumptionViolatedException; +import org.junit.rules.TestRule; +import org.junit.runner.Description; +import org.junit.runners.model.Statement; + +public final class ExpectedToFailUntilFixedRule implements TestRule { + @Override + public Statement apply(Statement test, Description description) { + ExpectedToFailUntilFixed marker = markerOf(description); + return marker == null ? test : new ExpectedFailure(test, marker.value()); + } + + private static ExpectedToFailUntilFixed markerOf(Description description) { + return description.getAnnotation(ExpectedToFailUntilFixed.class); + } + + private static final class ExpectedFailure extends Statement { + private final Statement test; + private final String reason; + + private ExpectedFailure(Statement test, String reason) { + this.test = test; + this.reason = reason; + } + + @Override + public void evaluate() throws Throwable { + try { + test.evaluate(); + } catch (AssertionError stillFailing) { + throw reportAsSkipped(stillFailing); + } + throw new AssertionError("Passes now, remove @ExpectedToFailUntilFixed: " + reason); + } + + private AssumptionViolatedException reportAsSkipped(AssertionError stillFailing) { + return new AssumptionViolatedException("Still failing until fixed: " + reason, stillFailing); + } + } +}