mirror of
https://github.com/VolmitSoftware/Iris.git
synced 2026-10-26 05:50:39 +00:00
Generation and runtime fixes
- Surface detail blends raw noise with cached six-block interpolation - Locked biome layers sample only the requested ranges - Surface layers skip noise when slope requirements fail - Stilt supports exclude configured materials and block entities - Ice and glass remain eligible foundation materials - Block-drop routing filters materials before loading biome history - Vision displays biome previews before river refinement - Interrupted previews retain reusable partial tiles - Hydrology waiters stop without cancelling shared planning tasks - Regression tests cover generation, supports, drop routing, and preview cancellation
This commit is contained in:
1 parent
e3c6a32968
commit
9a1e4b91d1
25 files changed
+1290
-84
No files matched your search
@@ -24,6 +24,8 @@ import static org.junit.Assert.assertTrue;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.same;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.times;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
@RunWith(Parameterized.class)
|
||||
@@ -70,6 +72,7 @@ public final class IrisBiomeLayerSlopeSourceTest {
|
||||
assertEquals(1, ledge.size());
|
||||
assertSame(block, ledge.get(0));
|
||||
assertTrue(cap.isEmpty());
|
||||
verify(heightGenerator, times(1)).fit(1, 1, 12D, -8D);
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -109,5 +112,6 @@ public final class IrisBiomeLayerSlopeSourceTest {
|
||||
assertTrue(hostLayers.isEmpty());
|
||||
assertEquals(1, sourceLayers.size());
|
||||
assertSame(surfaceBlock, sourceLayers.get(0));
|
||||
verify(heightGenerator, times(1)).fit(1, 1, 12D, -8D);
|
||||
}
|
||||
}
|
||||
@@ -1,14 +1,30 @@
|
||||
package art.arcane.iris.generation.biome;
|
||||
|
||||
import art.arcane.iris.generation.noise.NoiseStyle;
|
||||
import art.arcane.iris.generation.terrain.IrisSlopeClip;
|
||||
import art.arcane.iris.spi.IrisLogging;
|
||||
import art.arcane.iris.spi.PlatformBlockState;
|
||||
import art.arcane.volmlib.util.collection.KList;
|
||||
import art.arcane.volmlib.util.math.RNG;
|
||||
import art.arcane.volmlib.util.noise.CNG;
|
||||
import art.arcane.volmlib.util.stream.ProceduralStream;
|
||||
import org.junit.Test;
|
||||
import org.mockito.MockedStatic;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.mockito.ArgumentMatchers.anyDouble;
|
||||
import static org.mockito.ArgumentMatchers.anyInt;
|
||||
import static org.mockito.ArgumentMatchers.isNull;
|
||||
import static org.mockito.ArgumentMatchers.same;
|
||||
import static org.mockito.Mockito.doThrow;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.mockStatic;
|
||||
import static org.mockito.Mockito.spy;
|
||||
import static org.mockito.Mockito.times;
|
||||
import static org.mockito.Mockito.verify;
|
||||
|
||||
public class IrisBiomeLockedLayerTest {
|
||||
@Test
|
||||
@@ -32,4 +48,133 @@ public class IrisBiomeLockedLayerTest {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void sampledBandsMatchExpandedVariableMixedPalettes() {
|
||||
PlatformBlockState first = mock(PlatformBlockState.class);
|
||||
PlatformBlockState second = mock(PlatformBlockState.class);
|
||||
PlatformBlockState third = mock(PlatformBlockState.class);
|
||||
long[] seeds = {1L, 37L, -291L, Long.MAX_VALUE};
|
||||
int[] heights = {Integer.MIN_VALUE, -1000, -1, 0, 509, 512, 513, 1000, Integer.MAX_VALUE};
|
||||
int[] depths = {0, 1, 7, 39};
|
||||
double[] coordinates = {-71.5D, -1D, 0D, 13.25D, 127D};
|
||||
for (long seed : seeds) {
|
||||
RNG rng = new RNG(seed);
|
||||
IrisBiome biome = new IrisBiome().setLockLayers(true).setLockLayersMax(31).setLayers(new KList<>(
|
||||
palette(0, 0, 1D, first),
|
||||
palette(2, 5, 0.6D, first, second, second),
|
||||
palette(0, 3, 3D, third, first),
|
||||
palette(0, 0, 1D, second),
|
||||
palette(1, 4, 1.5D, second, third, first, third),
|
||||
palette(0, 0, 1D, third)));
|
||||
for (double x : coordinates) {
|
||||
for (int height : heights) {
|
||||
for (int depth : depths) {
|
||||
assertEquals(expandedBands(biome, x, -x, rng, depth, height, 0D),
|
||||
biome.generateLockedLayers(x, -x, rng, depth, height, null, null));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void nullEntriesAndRepeatedCyclesMatchExpandedBands() {
|
||||
PlatformBlockState block = mock(PlatformBlockState.class);
|
||||
IrisBiome biome = new IrisBiome().setLockLayers(true).setLockLayersMax(30).setLayers(new KList<>(
|
||||
palette(1, 1, 1D, block), palette(2, 2, 1D, (PlatformBlockState) null)));
|
||||
RNG rng = new RNG(37L);
|
||||
assertEquals(expandedBands(biome, 3D, -11D, rng, 30, 1000, 0D),
|
||||
biome.generateLockedLayers(3D, -11D, rng, 30, 1000, null, null));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void zeroThicknessAndEmptyPalettesProduceNoBands() {
|
||||
IrisBiome biome = new IrisBiome().setLockLayers(true).setLayers(new KList<>());
|
||||
RNG rng = new RNG(37L);
|
||||
assertTrue(biome.generateLockedLayers(0D, 0D, rng, 7, 64, null, null).isEmpty());
|
||||
biome.setLayers(new KList<>(palette(0, 0, 1D, mock(PlatformBlockState.class))));
|
||||
assertTrue(biome.generateLockedLayers(0D, 0D, rng, 7, 64, null, null).isEmpty());
|
||||
assertTrue(biome.generateLockedLayers(0D, 0D, rng, -1, 64, null, null).isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void resolvesOnlyRequestedPositionsOfLongBands() {
|
||||
IrisBiomePaletteLayer layer = spy(palette(1000, 1000, 0.6D,
|
||||
mock(PlatformBlockState.class), mock(PlatformBlockState.class)));
|
||||
IrisBiome biome = new IrisBiome().setLockLayers(true).setLayers(new KList<>(layer));
|
||||
RNG rng = new RNG(37L);
|
||||
assertEquals(7, biome.generateLockedLayers(13D, -27D, rng, 7, 64, null, null).size());
|
||||
verify(layer, times(7)).get(same(rng), anyInt(), anyDouble(), anyDouble(), anyDouble(), isNull());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void repeatedCyclesResolveEachPositionOnce() {
|
||||
IrisBiomePaletteLayer layer = spy(palette(3, 3, 0.6D,
|
||||
mock(PlatformBlockState.class), mock(PlatformBlockState.class)));
|
||||
IrisBiome biome = new IrisBiome().setLockLayers(true).setLockLayersMax(100).setLayers(new KList<>(layer));
|
||||
RNG rng = new RNG(37L);
|
||||
assertEquals(100, biome.generateLockedLayers(13D, -27D, rng, 100, 64, null, null).size());
|
||||
verify(layer, times(3)).get(same(rng), anyInt(), anyDouble(), anyDouble(), anyDouble(), isNull());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void slopeFilteredBandsMatchExpandedCycle() {
|
||||
IrisBiome biome = new IrisBiome().setLockLayers(true).setLockLayersMax(30).setLayers(new KList<>(
|
||||
palette(2, 4, 0.6D, mock(PlatformBlockState.class)),
|
||||
palette(3, 8, 1D, mock(PlatformBlockState.class)).setSlopeCondition(new IrisSlopeClip(3D, 255D)),
|
||||
palette(1, 5, 2D, mock(PlatformBlockState.class)).setSlopeCondition(new IrisSlopeClip(0D, 3D))));
|
||||
RNG rng = new RNG(37L);
|
||||
double[] slopes = {0D, 3D, 8D};
|
||||
for (double slope : slopes) {
|
||||
ProceduralStream<Double> slopeStream = ProceduralStream.ofDouble((x, z) -> slope);
|
||||
assertEquals(expandedBands(biome, 13D, -27D, rng, 30, 64, slope),
|
||||
biome.generateLayersWithSlope(null, 13D, -27D, rng, 30, 64, null, slopeStream));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void sampledPaletteFailuresAreReported() {
|
||||
IrisBiomePaletteLayer layer = spy(palette(1, 1, 1D, mock(PlatformBlockState.class)));
|
||||
IrisBiome biome = new IrisBiome().setLockLayers(true).setLayers(new KList<>(layer));
|
||||
RNG rng = new RNG(37L);
|
||||
IllegalStateException failure = new IllegalStateException("Palette unavailable");
|
||||
doThrow(failure).when(layer).get(same(rng), anyInt(), anyDouble(), anyDouble(), anyDouble(), isNull());
|
||||
try (MockedStatic<IrisLogging> logging = mockStatic(IrisLogging.class)) {
|
||||
assertTrue(biome.generateLockedLayers(0D, 0D, rng, 7, 64, null, null).isEmpty());
|
||||
logging.verify(() -> IrisLogging.reportError(failure));
|
||||
}
|
||||
}
|
||||
|
||||
private static IrisBiomePaletteLayer palette(int minimum, int maximum, double zoom, PlatformBlockState... blocks) {
|
||||
IrisBiomePaletteLayer layer = new IrisBiomePaletteLayer().setMinHeight(minimum).setMaxHeight(maximum)
|
||||
.setZoom(zoom).setStyle(NoiseStyle.NOWHERE.style());
|
||||
layer.getBlockData().aquire(() -> new KList<>(blocks));
|
||||
return layer;
|
||||
}
|
||||
|
||||
private static KList<PlatformBlockState> expandedBands(IrisBiome biome, double x, double z, RNG rng,
|
||||
int maxDepth, int height, double slope) {
|
||||
KList<PlatformBlockState> expanded = new KList<>();
|
||||
KList<PlatformBlockState> result = new KList<>();
|
||||
KList<CNG> heights = biome.getLayerHeightGenerators(rng, null);
|
||||
for (int index = 0; index < biome.getLayers().size(); index++) {
|
||||
IrisBiomePaletteLayer layer = biome.getLayers().get(index);
|
||||
double zoom = layer.getZoom();
|
||||
int thickness = heights.get(index).fit(layer.getMinHeight(), layer.getMaxHeight(), x / zoom, z / zoom);
|
||||
if (!layer.getSlopeCondition().isValid(slope)) {
|
||||
continue;
|
||||
}
|
||||
for (int offset = 0; offset < thickness; offset++) {
|
||||
expanded.add(layer.get(rng, index + offset, (x + offset) / zoom, offset, (z - offset) / zoom, null));
|
||||
}
|
||||
}
|
||||
if (!expanded.isEmpty()) {
|
||||
for (int depth = 0; depth < Math.min(maxDepth, biome.getLockLayersMax()); depth++) {
|
||||
result.add(expanded.get(Math.floorMod(512L - height - depth, expanded.size())));
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -50,6 +50,57 @@ import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
public class HydrologyTileCacheTest {
|
||||
@Test
|
||||
public void interruptedWaiterExitsWithoutCancellingTheSharedPlan() throws Exception {
|
||||
HydrologyPlanner planner = mock(HydrologyPlanner.class);
|
||||
HydrologyTile tile = mock(HydrologyTile.class);
|
||||
HydrologyTileKey key = new HydrologyTileKey(0, 0);
|
||||
CountDownLatch started = new CountDownLatch(1);
|
||||
CountDownLatch release = new CountDownLatch(1);
|
||||
CountDownLatch waiterExited = new CountDownLatch(1);
|
||||
AtomicBoolean interrupted = new AtomicBoolean();
|
||||
AtomicReference<Thread> waiterThread = new AtomicReference<>();
|
||||
when(planner.plan(key)).thenAnswer(invocation -> {
|
||||
started.countDown();
|
||||
assertTrue(release.await(5L, TimeUnit.SECONDS));
|
||||
return tile;
|
||||
});
|
||||
ExecutorService workers = Executors.newSingleThreadExecutor();
|
||||
ExecutorService callers = Executors.newFixedThreadPool(2);
|
||||
HydrologyTileCache cache = new HydrologyTileCache(planner, 4, workers);
|
||||
try {
|
||||
Future<?> waiter = callers.submit(() -> {
|
||||
waiterThread.set(Thread.currentThread());
|
||||
try {
|
||||
assertThrows(CancellationException.class, () -> cache.get(key));
|
||||
interrupted.set(Thread.currentThread().isInterrupted());
|
||||
} finally {
|
||||
waiterExited.countDown();
|
||||
}
|
||||
});
|
||||
assertTrue(started.await(5L, TimeUnit.SECONDS));
|
||||
Future<HydrologyTile> other = callers.submit(() -> cache.get(key));
|
||||
|
||||
waiterThread.get().interrupt();
|
||||
assertTrue(waiterExited.await(2L, TimeUnit.SECONDS));
|
||||
waiter.get(2L, TimeUnit.SECONDS);
|
||||
assertTrue(interrupted.get());
|
||||
assertFalse(other.isDone());
|
||||
|
||||
release.countDown();
|
||||
assertSame(tile, other.get(5L, TimeUnit.SECONDS));
|
||||
assertSame(tile, cache.get(key));
|
||||
verify(planner, times(1)).plan(key);
|
||||
} finally {
|
||||
release.countDown();
|
||||
workers.shutdownNow();
|
||||
callers.shutdownNow();
|
||||
assertTrue(workers.awaitTermination(5L, TimeUnit.SECONDS));
|
||||
assertTrue(callers.awaitTermination(5L, TimeUnit.SECONDS));
|
||||
cache.close();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void closeDrainsBackgroundPlanningAndRejectsNewDemand() throws Exception {
|
||||
HydrologyPlanner planner = mock(HydrologyPlanner.class);
|
||||
|
||||
+194
@@ -0,0 +1,194 @@
|
||||
package art.arcane.iris.generation.noise;
|
||||
|
||||
import art.arcane.iris.generation.runtime.Engine;
|
||||
import art.arcane.iris.pack.loading.IrisData;
|
||||
import art.arcane.volmlib.util.collection.KList;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
import java.util.function.ToDoubleFunction;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
public class IrisGeneratorSurfaceDetailTest {
|
||||
@Test
|
||||
public void defaultUsesTheOriginalSampleOnce() {
|
||||
AtomicInteger samples = new AtomicInteger();
|
||||
IrisGenerator generator = generator((seed, x, z) -> {
|
||||
samples.incrementAndGet();
|
||||
return x * 0.01D + z * 0.02D;
|
||||
});
|
||||
|
||||
assertEquals(0.07D, generator.getHeight(1, 3, 27L), 1e-12D);
|
||||
assertEquals(1, samples.get());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void halvesSubgridDetailAndPreservesLatticeHeights() {
|
||||
IrisGenerator generator = generator((seed, x, z) ->
|
||||
(int) x % 6 == 0 && (int) z % 6 == 0 ? 0.2D : 0.8D);
|
||||
generator.setSurfaceDetail(0.5D);
|
||||
|
||||
for (int z = -12; z <= 12; z++) {
|
||||
for (int x = -12; x <= 12; x++) {
|
||||
double expected = x % 6 == 0 && z % 6 == 0 ? 0.2D : 0.5D;
|
||||
assertEquals(expected, generator.getHeight(x, z, 27L), 1e-12D);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void preservesPlanarSlopesAtNegativeAndFractionalCoordinates() {
|
||||
IrisGenerator generator = generator((seed, x, z) -> 0.5D + x * 0.01D + z * 0.02D);
|
||||
generator.setSurfaceDetail(0.5D);
|
||||
|
||||
for (double z = -4.25D; z < 4D; z += 0.5D) {
|
||||
for (double x = -4.75D; x < 4D; x += 0.5D) {
|
||||
assertEquals(0.5D + x * 0.01D + z * 0.02D,
|
||||
generator.getHeight(x, z, 27L), 1e-12D);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void reusesLatticeSamplesDuringDenseGeneration() {
|
||||
AtomicInteger samples = new AtomicInteger();
|
||||
IrisGenerator generator = generator((seed, x, z) -> {
|
||||
samples.incrementAndGet();
|
||||
return 0.5D;
|
||||
});
|
||||
generator.setSurfaceDetail(0.5D);
|
||||
|
||||
for (int z = 0; z < 16; z++) {
|
||||
for (int x = 0; x < 16; x++) {
|
||||
assertEquals(0.5D, generator.getHeight(x, z, 27L), 0D);
|
||||
}
|
||||
}
|
||||
|
||||
assertEquals(263, samples.get());
|
||||
assertTrue(samples.get() < 256 * 1.1D);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void fourBlockPreviewSamplingReusesCoarseCorners() {
|
||||
AtomicInteger samples = new AtomicInteger();
|
||||
IrisGenerator generator = generator((seed, x, z) -> {
|
||||
samples.incrementAndGet();
|
||||
return 0.5D;
|
||||
});
|
||||
generator.setSurfaceDetail(0.5D);
|
||||
|
||||
for (int z = -16; z < 16; z += 4) {
|
||||
for (int x = -16; x < 16; x += 4) {
|
||||
generator.getHeight(x, z, 27L);
|
||||
}
|
||||
}
|
||||
|
||||
assertEquals(91, samples.get());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void cachedLatticeHeightsRemainIsolatedBySeed() {
|
||||
IrisGenerator generator = generator((seed, x, z) -> seed * 0.000001D);
|
||||
generator.setSurfaceDetail(0.5D);
|
||||
double first = generator.getHeight(1, 1, 27L);
|
||||
double second = generator.getHeight(1, 1, 28L);
|
||||
|
||||
assertEquals(0.000001D, second - first, 1e-12D);
|
||||
assertEquals(first, generator.getHeight(1, 1, 27L), 0D);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void cachedLatticeHeightsRemainIsolatedByEngine() {
|
||||
Engine firstEngine = mock(Engine.class);
|
||||
Engine secondEngine = mock(Engine.class);
|
||||
AtomicReference<Engine> activeEngine = new AtomicReference<>(firstEngine);
|
||||
IrisData data = mock(IrisData.class);
|
||||
when(data.getEngine()).thenAnswer(ignored -> activeEngine.get());
|
||||
IrisGenerator generator = new IrisGenerator().setSurfaceDetail(0.5D).setComposite(new KList<>(
|
||||
new ContextNoise(loader -> loader.getEngine() == firstEngine ? 0.2D : 0.8D)));
|
||||
generator.setLoader(data);
|
||||
|
||||
assertEquals(0.2D, generator.getHeight(0, 0, 27L), 1e-12D);
|
||||
assertEquals(0.2D, generator.getHeight(1, 1, 27L), 1e-12D);
|
||||
activeEngine.set(secondEngine);
|
||||
assertEquals(0.8D, generator.getHeight(0, 0, 27L), 1e-12D);
|
||||
assertEquals(0.8D, generator.getHeight(1, 1, 27L), 1e-12D);
|
||||
activeEngine.set(firstEngine);
|
||||
assertEquals(0.2D, generator.getHeight(1, 1, 27L), 1e-12D);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void cachedLatticeHeightsRemainIsolatedByLoader() {
|
||||
Engine engine = mock(Engine.class);
|
||||
IrisData firstData = mock(IrisData.class);
|
||||
IrisData secondData = mock(IrisData.class);
|
||||
when(firstData.getEngine()).thenReturn(engine);
|
||||
when(secondData.getEngine()).thenReturn(engine);
|
||||
IrisGenerator generator = new IrisGenerator().setSurfaceDetail(0.5D).setComposite(new KList<>(
|
||||
new ContextNoise(loader -> loader == firstData ? 0.2D : 0.8D)));
|
||||
generator.setLoader(firstData);
|
||||
|
||||
assertEquals(0.2D, generator.getHeight(0, 0, 27L), 1e-12D);
|
||||
assertEquals(0.2D, generator.getHeight(1, 1, 27L), 1e-12D);
|
||||
generator.setLoader(secondData);
|
||||
assertEquals(0.8D, generator.getHeight(0, 0, 27L), 1e-12D);
|
||||
assertEquals(0.8D, generator.getHeight(1, 1, 27L), 1e-12D);
|
||||
generator.setLoader(firstData);
|
||||
assertEquals(0.2D, generator.getHeight(1, 1, 27L), 1e-12D);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void rescalingClearsCachedLatticeHeights() {
|
||||
IrisGenerator generator = generator((seed, x, z) -> x * 0.01D + z * 0.02D);
|
||||
generator.setSurfaceDetail(0.5D);
|
||||
assertEquals(0.06D, generator.getHeight(2, 2, 27L), 1e-12D);
|
||||
|
||||
generator.rescale(2D);
|
||||
|
||||
assertEquals(0.12D, generator.getHeight(2, 2, 27L), 1e-12D);
|
||||
}
|
||||
|
||||
@Test(expected = IllegalArgumentException.class)
|
||||
public void rejectsNonfiniteDetail() {
|
||||
generator((seed, x, z) -> 0D).setSurfaceDetail(Double.NaN).getHeight(1, 1, 27L);
|
||||
}
|
||||
|
||||
private static IrisGenerator generator(Sampler sampler) {
|
||||
return new IrisGenerator().setComposite(new KList<>(new SampledNoise(sampler)));
|
||||
}
|
||||
|
||||
private interface Sampler {
|
||||
double sample(long seed, double x, double z);
|
||||
}
|
||||
|
||||
private static final class SampledNoise extends IrisNoiseGenerator {
|
||||
private final Sampler sampler;
|
||||
|
||||
private SampledNoise(Sampler sampler) {
|
||||
this.sampler = sampler;
|
||||
}
|
||||
|
||||
@Override
|
||||
public double getNoise(long superSeed, double x, double z, IrisData data) {
|
||||
return sampler.sample(superSeed, x, z);
|
||||
}
|
||||
}
|
||||
|
||||
private static final class ContextNoise extends IrisNoiseGenerator {
|
||||
private final ToDoubleFunction<IrisData> sampler;
|
||||
|
||||
private ContextNoise(ToDoubleFunction<IrisData> sampler) {
|
||||
this.sampler = sampler;
|
||||
}
|
||||
|
||||
@Override
|
||||
public double getNoise(long superSeed, double x, double z, IrisData data) {
|
||||
return sampler.applyAsDouble(data);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -12,6 +12,7 @@ import art.arcane.iris.platform.bukkit.BukkitWorldBinding;
|
||||
import art.arcane.iris.world.task.J;
|
||||
import art.arcane.volmlib.util.collection.KList;
|
||||
import org.bukkit.Location;
|
||||
import org.bukkit.GameMode;
|
||||
import org.bukkit.World;
|
||||
import org.bukkit.block.Block;
|
||||
import org.bukkit.block.data.BlockData;
|
||||
@@ -41,6 +42,30 @@ import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
public class WorldBlockDropRouterTest {
|
||||
@Test
|
||||
public void packWithoutDropRulesAllowsTheFirstCreativeAndSurvivalBreakWhileBiomesAreCold() {
|
||||
for (GameMode mode : new GameMode[] {GameMode.CREATIVE, GameMode.SURVIVAL}) {
|
||||
try (Fixture fixture = new Fixture()) {
|
||||
when(fixture.data.hasBlockDropRules("minecraft:stone")).thenReturn(false);
|
||||
when(fixture.data.hasBlockDropRules("minecraft:grass_block")).thenReturn(true);
|
||||
when(fixture.player.getGameMode()).thenReturn(mode);
|
||||
fixture.queries.when(() -> EngineBukkitOps.getBiome(fixture.engine, fixture.location))
|
||||
.thenThrow(new SavedBiomeUnavailableException("Biome loading", true));
|
||||
BlockBreakEvent event = fixture.event();
|
||||
|
||||
fixture.router.onBlockBreak(event);
|
||||
|
||||
assertFalse(mode.name(), event.isCancelled());
|
||||
assertTrue(event.isDropItems());
|
||||
fixture.queries.verifyNoInteractions();
|
||||
verify(fixture.drops, never()).fillDrops(anyBoolean(), any());
|
||||
assertEquals(1, fixture.delayed.size());
|
||||
fixture.delayed.remove().run();
|
||||
assertEquals(1, fixture.markers.size());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void coldBiomeCancelsBeforeChangingDropsOrSchedulingWork() {
|
||||
try (Fixture fixture = new Fixture()) {
|
||||
@@ -206,6 +231,7 @@ public class WorldBlockDropRouterTest {
|
||||
when(manager.getEngine()).thenReturn(engine);
|
||||
when(manager.getTarget()).thenReturn(target);
|
||||
when(manager.getData()).thenReturn(data);
|
||||
when(data.hasBlockDropRules(anyString())).thenReturn(true);
|
||||
when(target.getWorld()).thenReturn(irisWorld);
|
||||
when(engine.getWorld()).thenReturn(irisWorld);
|
||||
when(irisWorld.minHeight()).thenReturn(-64);
|
||||
@@ -217,6 +243,7 @@ public class WorldBlockDropRouterTest {
|
||||
when(block.getZ()).thenReturn(32);
|
||||
when(block.getLocation()).thenReturn(location);
|
||||
when(block.getBlockData()).thenReturn(blockData);
|
||||
when(blockData.getAsString()).thenReturn("minecraft:stone");
|
||||
when(biome.getBlockDrops()).thenReturn(new KList<>(drops));
|
||||
when(region.getBlockDrops()).thenReturn(new KList<>());
|
||||
when(dimension.getBlockDrops()).thenReturn(new KList<>());
|
||||
|
||||
@@ -3,6 +3,9 @@ package art.arcane.iris.pack.loading;
|
||||
import art.arcane.iris.configuration.IrisSettings;
|
||||
import art.arcane.iris.generation.runtime.PreservationRegistry;
|
||||
import art.arcane.iris.generation.noise.IrisGenerator;
|
||||
import art.arcane.iris.generation.block.B;
|
||||
import art.arcane.iris.spi.PlatformBlockState;
|
||||
import art.arcane.iris.pack.validation.ContentGate;
|
||||
import art.arcane.iris.spi.IrisServices;
|
||||
import art.arcane.iris.testsupport.PlatformLeakGuard;
|
||||
import org.junit.After;
|
||||
@@ -11,6 +14,7 @@ import org.junit.ClassRule;
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.junit.rules.TemporaryFolder;
|
||||
import org.mockito.MockedStatic;
|
||||
|
||||
import java.io.File;
|
||||
import java.nio.file.Files;
|
||||
@@ -25,6 +29,8 @@ import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertSame;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.mockStatic;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
public class IrisDataAuthoringCacheTest {
|
||||
@ClassRule
|
||||
@@ -105,4 +111,61 @@ public class IrisDataAuthoringCacheTest {
|
||||
assertSame(keys, entry.getKey().getPossibleKeys());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void dropRulePresenceIncludesEachScopeAndIsResetByPackReload() throws Exception {
|
||||
for (String folder : List.of("dimensions", "regions", "biomes")) {
|
||||
Path root = temporary.newFolder(folder + "-drops").toPath();
|
||||
Files.createDirectories(root.resolve(folder));
|
||||
Path resource = root.resolve(folder + "/sample.json");
|
||||
Files.writeString(resource, "{}");
|
||||
source = IrisData.openRuntime(root.toFile());
|
||||
try (MockedStatic<B> blocks = mockStatic(B.class)) {
|
||||
PlatformBlockState state = mock(PlatformBlockState.class);
|
||||
when(state.placementBaseState()).thenReturn(state);
|
||||
when(state.materialKey()).thenReturn("minecraft:stone");
|
||||
blocks.when(() -> B.getStateOrNull("minecraft:stone", false)).thenReturn(state);
|
||||
source.setContentGate(mock(ContentGate.class));
|
||||
assertTrue(source.hasBlockDropRules("minecraft:stone"));
|
||||
source.prepareBlockDropRules();
|
||||
assertFalse(source.hasBlockDropRules("minecraft:stone"));
|
||||
Files.writeString(resource, "{\"blockDrops\":[{\"blocks\":[{\"block\":\"minecraft:stone\"}]}]}");
|
||||
source.hotloaded();
|
||||
source.setContentGate(mock(ContentGate.class));
|
||||
assertTrue(source.hasBlockDropRules("minecraft:stone"));
|
||||
source.prepareBlockDropRules();
|
||||
assertTrue(source.hasBlockDropRules("minecraft:stone"));
|
||||
assertFalse(source.hasBlockDropRules("minecraft:grass_block"));
|
||||
} finally {
|
||||
source.close();
|
||||
source = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void customDropCandidatesUseTheResolvedCarrierAndKeepExactStateRulesEligible() throws Exception {
|
||||
Path root = temporary.newFolder("custom-drops").toPath();
|
||||
Files.createDirectories(root.resolve("biomes"));
|
||||
Files.writeString(root.resolve("biomes/sample.json"), """
|
||||
{"blockDrops":[{"exactBlocks":true,"blocks":[{"block":"rocks:ruby_ore"}]}]}
|
||||
""");
|
||||
source = IrisData.openRuntime(root.toFile());
|
||||
source.setContentGate(mock(ContentGate.class));
|
||||
PlatformBlockState custom = mock(PlatformBlockState.class);
|
||||
PlatformBlockState carrier = mock(PlatformBlockState.class);
|
||||
when(custom.materialKey()).thenReturn("rocks:ruby_ore");
|
||||
when(custom.placementBaseState()).thenReturn(carrier);
|
||||
when(carrier.materialKey()).thenReturn("minecraft:note_block");
|
||||
|
||||
try (MockedStatic<B> blocks = mockStatic(B.class)) {
|
||||
blocks.when(() -> B.getStateOrNull("rocks:ruby_ore", false)).thenReturn(custom);
|
||||
|
||||
source.prepareBlockDropRules();
|
||||
|
||||
assertTrue(source.hasBlockDropRules("minecraft:note_block"));
|
||||
assertFalse(source.hasBlockDropRules("minecraft:stone"));
|
||||
assertFalse(source.hasBlockDropRules("rocks:ruby_ore"));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -78,7 +78,7 @@ public class SchemaBuilderIdentityTest {
|
||||
@ClassRule
|
||||
public static final PlatformLeakGuard PLATFORM_GUARD = PlatformLeakGuard.clean();
|
||||
|
||||
private static final String EXPECTED_SCHEMA_DIGEST = "da993d49349b961b46f86a4966b8d2ab228a6b005c2a4ee279018e03e1139783";
|
||||
private static final String EXPECTED_SCHEMA_DIGEST = "545cbfcdaa4b4c8dd03fca96f6b2617c37bbe9bf73d2011679d2fabcf3f2a6be";
|
||||
|
||||
private static final List<Class<?>> SCHEMA_ROOTS = List.of(
|
||||
IrisDimension.class,
|
||||
|
||||
@@ -22,6 +22,7 @@ import art.arcane.iris.pack.loading.IrisData;
|
||||
import art.arcane.iris.pack.loading.IrisRegistrant;
|
||||
import art.arcane.iris.pack.loading.ResourceLoader;
|
||||
import art.arcane.iris.generation.block.IrisBlockData;
|
||||
import art.arcane.iris.generation.decoration.IrisStiltSettings;
|
||||
import art.arcane.iris.generation.noise.IrisExpression;
|
||||
import art.arcane.iris.generation.terrain.IrisDimension;
|
||||
import art.arcane.iris.structure.jigsaw.IrisJigsawPiece;
|
||||
@@ -124,6 +125,17 @@ public class SchemaBuilderParityTest {
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void stiltExclusionsExposeRegisteredMaterialKeys() {
|
||||
IrisData data = structureSchemaData();
|
||||
when(data.getBlockLoader().getPossibleKeys()).thenReturn(new String[] {"minecraft:barrel", "minecraft:calcite"});
|
||||
JSONObject schema = new SchemaBuilder(IrisStiltSettings.class, data).construct();
|
||||
JSONObject exclude = rootProperties(schema).getJSONObject("exclude");
|
||||
|
||||
assertEquals("array", exclude.getString("type"));
|
||||
assertTrue(exclude.getJSONObject("items").getString("$ref").endsWith("enum-block-type"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void registryDependentEnumsMatchLegacyTransforms() {
|
||||
JSONObject definitions = new SchemaBuilder(RegistryModel.class, (IrisData) null).construct().getJSONObject("definitions");
|
||||
|
||||
+193
@@ -1,6 +1,11 @@
|
||||
package art.arcane.iris.structure.object;
|
||||
|
||||
import art.arcane.iris.generation.block.TileData;
|
||||
import art.arcane.iris.generation.decoration.IrisStiltSettings;
|
||||
import art.arcane.iris.generation.block.IrisBlockData;
|
||||
import art.arcane.iris.generation.decoration.formation.FormationGenerator;
|
||||
import art.arcane.iris.generation.decoration.formation.IrisFormation;
|
||||
import art.arcane.iris.generation.decoration.formation.IrisFormationForm;
|
||||
import art.arcane.iris.generation.noise.IrisGeneratorStyle;
|
||||
import art.arcane.iris.generation.noise.NoiseStyle;
|
||||
import art.arcane.iris.generation.terrain.IrisMaterialPalette;
|
||||
@@ -415,6 +420,194 @@ public class IrisObjectPlacementRunnerRegressionTest {
|
||||
assertTrue(placer.writes().isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void everyStiltModePreservesTheBarrelAndItsTileWithoutRepeatingIt() {
|
||||
PlatformBlockState barrel = state("minecraft:barrel[facing=up,open=false]", true);
|
||||
when(barrel.materialKey()).thenReturn("minecraft:barrel");
|
||||
when(barrel.isStorage()).thenReturn(true);
|
||||
when(barrel.hasTileEntity()).thenReturn(true);
|
||||
TileData tile = new TileData("minecraft:barrel", new KMap<>());
|
||||
for (ObjectPlaceMode mode : List.of(ObjectPlaceMode.STILT, ObjectPlaceMode.FAST_STILT,
|
||||
ObjectPlaceMode.MIN_STILT, ObjectPlaceMode.FAST_MIN_STILT, ObjectPlaceMode.CENTER_STILT,
|
||||
ObjectPlaceMode.ERODE_STILT, ObjectPlaceMode.ORGANIC_STILT, ObjectPlaceMode.CEILING_HANG)) {
|
||||
RecordingPlacer placer = new RecordingPlacer(null);
|
||||
for (int x = -2; x <= 2; x++) {
|
||||
for (int z = -2; z <= 2; z++) {
|
||||
placer.set(x, ANCHOR_Y, z, solid);
|
||||
placer.set(x, ANCHOR_Y + 20, z, solid);
|
||||
}
|
||||
}
|
||||
placer.writes().clear();
|
||||
IrisObject object = boxObject(5, 1, 5);
|
||||
object.setUnsigned(3, 0, 2, barrel);
|
||||
object.setUnsignedTile(3, 0, 2, tile);
|
||||
IrisObjectPlacement placement = placement().setMode(mode)
|
||||
.setRotation(IrisObjectRotation.of(0, 0, 0))
|
||||
.setStiltSettings(new IrisStiltSettings().setOrganicJitter(0).setOrganicScratch(0));
|
||||
|
||||
object.place(0, ANCHOR_Y + 10, 0, placer, placement, new RNG(2L), data);
|
||||
|
||||
assertEquals(mode.name(), 1, placer.writesOf(barrel).size());
|
||||
assertSame(mode.name(), barrel, placer.get(1, ANCHOR_Y + 10, 0));
|
||||
assertSame(mode.name(), tile, placer.getData(1, ANCHOR_Y + 10, 0, TileData.class));
|
||||
assertTrue(mode.name(), placer.writesOf(solid).stream()
|
||||
.anyMatch(write -> write.y() != ANCHOR_Y + 10));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void excludedSourceMaterialKeepsItsBlockAndDoesNotUseTheStiltPalette() {
|
||||
PlatformBlockState calcite = state("minecraft:calcite", true);
|
||||
PlatformBlockState cobblestone = state("minecraft:cobblestone", true);
|
||||
IrisMaterialPalette palette = mock(IrisMaterialPalette.class);
|
||||
when(palette.get(any(RNG.class), anyDouble(), anyDouble(), anyDouble(), any(IrisData.class)))
|
||||
.thenReturn(cobblestone);
|
||||
IrisObject object = lineObject(3);
|
||||
object.setUnsigned(2, 0, 0, calcite);
|
||||
RecordingPlacer placer = new RecordingPlacer(null);
|
||||
IrisObjectPlacement placement = placement().setMode(ObjectPlaceMode.CENTER_STILT)
|
||||
.setRotation(IrisObjectRotation.of(0, 0, 0))
|
||||
.setStiltSettings(new IrisStiltSettings().setExclude(new KList<>("CALCITE")).setPalette(palette));
|
||||
|
||||
object.place(0, ANCHOR_Y + 10, 0, placer, placement, new RNG(2L), data);
|
||||
|
||||
assertEquals(1, placer.writesOf(calcite).size());
|
||||
assertSame(calcite, placer.get(1, ANCHOR_Y + 10, 0));
|
||||
assertNull(placer.get(1, ANCHOR_Y + 9, 0));
|
||||
assertSame(cobblestone, placer.get(0, ANCHOR_Y + 9, 0));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void translucentFullBlocksGrowFoundationsWhileCrystalModelsRemainUnextended() {
|
||||
PlatformBlockState ice = state("minecraft:ice", true);
|
||||
when(ice.isOccluding()).thenReturn(false);
|
||||
PlatformBlockState cluster = state("minecraft:amethyst_cluster", true);
|
||||
when(cluster.isOccluding()).thenReturn(false);
|
||||
IrisObject object = new IrisObject(3, 1, 1);
|
||||
object.setUnsigned(0, 0, 0, ice);
|
||||
object.setUnsigned(2, 0, 0, cluster);
|
||||
IrisMaterialPalette palette = mock(IrisMaterialPalette.class);
|
||||
when(palette.get(any(RNG.class), anyDouble(), anyDouble(), anyDouble(), any(IrisData.class)))
|
||||
.thenReturn(solid);
|
||||
RecordingPlacer placer = new RecordingPlacer(null);
|
||||
IrisObjectPlacement placement = placement().setMode(ObjectPlaceMode.CENTER_STILT)
|
||||
.setRotation(IrisObjectRotation.of(0, 0, 0))
|
||||
.setStiltSettings(new IrisStiltSettings().setPalette(palette));
|
||||
|
||||
object.place(0, ANCHOR_Y + 10, 0, placer, placement, new RNG(2L), data);
|
||||
|
||||
assertSame(solid, placer.get(-1, ANCHOR_Y + 9, 0));
|
||||
assertEquals(1, placer.writesOf(cluster).size());
|
||||
assertNull(placer.get(1, ANCHOR_Y + 9, 0));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void generatedGlacialOverhangGrowsPackedIceRootsToTerrain() {
|
||||
PlatformBlockState ice = state("minecraft:ice", true);
|
||||
when(ice.isOccluding()).thenReturn(false);
|
||||
PlatformBlockState packedIce = state("minecraft:packed_ice", true);
|
||||
IrisMaterialPalette body = mock(IrisMaterialPalette.class);
|
||||
when(body.getPalette()).thenReturn(new KList<>(new IrisBlockData("minecraft:ice")));
|
||||
when(body.get(any(RNG.class), anyDouble(), anyDouble(), anyDouble(), any(IrisData.class)))
|
||||
.thenReturn(ice);
|
||||
IrisMaterialPalette roots = mock(IrisMaterialPalette.class);
|
||||
when(roots.get(any(RNG.class), anyDouble(), anyDouble(), anyDouble(), any(IrisData.class)))
|
||||
.thenReturn(packedIce);
|
||||
IrisFormation formation = new IrisFormation().setForm(IrisFormationForm.OVERHANG)
|
||||
.setHeightMin(16).setHeightMax(25).setBaseWidthMin(5).setBaseWidthMax(8)
|
||||
.setOverhangReach(13).setOverhangDrop(6).setRoughness(0.3).setJitter(0.08)
|
||||
.setBlockPalette(body);
|
||||
IrisObject object = FormationGenerator.generate(formation, 16, new RNG(5055004L + 16L), data);
|
||||
RecordingPlacer placer = new RecordingPlacer(null);
|
||||
for (int x = -32; x <= 32; x++) {
|
||||
for (int z = -32; z <= 32; z++) {
|
||||
placer.set(x, ANCHOR_Y, z, solid);
|
||||
}
|
||||
}
|
||||
placer.writes().clear();
|
||||
IrisObjectPlacement placement = placement().setMode(ObjectPlaceMode.ORGANIC_STILT)
|
||||
.setRotation(IrisObjectRotation.of(0, 0, 0))
|
||||
.setStiltSettings(new IrisStiltSettings().setOrganicMaxScan(96).setOrganicJitter(0)
|
||||
.setOrganicScratch(0).setPalette(roots));
|
||||
|
||||
object.place(0, ANCHOR_Y + object.getCenter().getBlockY() + 12, 0, placer, placement,
|
||||
new RNG(2L), data);
|
||||
|
||||
List<BlockWrite> rootWrites = placer.writesOf(packedIce);
|
||||
assertFalse(rootWrites.isEmpty());
|
||||
assertTrue(rootWrites.stream().anyMatch(write -> write.y() == ANCHOR_Y + 1));
|
||||
for (BlockWrite root : rootWrites) {
|
||||
for (int y = ANCHOR_Y + 1; y < root.y(); y++) {
|
||||
assertSame(packedIce, placer.get(root.x(), y, root.z()));
|
||||
}
|
||||
}
|
||||
assertFalse(placer.writesOf(ice).isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void excludedLowestBlockDoesNotMoveTheFoundationUpIntoTheOriginalObject() {
|
||||
PlatformBlockState machine = state("test:machine", true);
|
||||
when(machine.hasTileEntity()).thenReturn(true);
|
||||
IrisObject object = new IrisObject(1, 3, 1);
|
||||
object.setUnsigned(0, 0, 0, machine);
|
||||
object.setUnsigned(0, 1, 0, solid);
|
||||
RecordingPlacer placer = new RecordingPlacer(null);
|
||||
|
||||
object.place(0, ANCHOR_Y + 10, 0, placer, placement().setMode(ObjectPlaceMode.CENTER_STILT),
|
||||
new RNG(2L), data);
|
||||
|
||||
assertEquals(2, placer.writes().size());
|
||||
assertSame(machine, placer.get(0, ANCHOR_Y + 9, 0));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void stiltPaletteCannotIntroduceExcludedMaterialsOrContainers() {
|
||||
PlatformBlockState calcite = state("minecraft:calcite", true);
|
||||
PlatformBlockState barrel = state("minecraft:barrel", true);
|
||||
when(barrel.isStorage()).thenReturn(true);
|
||||
for (PlatformBlockState replacement : List.of(calcite, barrel)) {
|
||||
IrisMaterialPalette palette = mock(IrisMaterialPalette.class);
|
||||
when(palette.get(any(RNG.class), anyDouble(), anyDouble(), anyDouble(), any(IrisData.class)))
|
||||
.thenReturn(replacement);
|
||||
RecordingPlacer placer = new RecordingPlacer(null);
|
||||
IrisObjectPlacement placement = placement().setMode(ObjectPlaceMode.CENTER_STILT)
|
||||
.setStiltSettings(new IrisStiltSettings().setExclude(new KList<>("minecraft:calcite"))
|
||||
.setPalette(palette));
|
||||
|
||||
lineObject(3).place(0, ANCHOR_Y + 10, 0, placer, placement, new RNG(2L), data);
|
||||
|
||||
assertEquals(replacement.key(), 3, placer.writes().size());
|
||||
assertTrue(placer.writesOf(replacement).isEmpty());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void objectEditsCannotIntroduceExcludedMaterialsOrTileEntitiesIntoStilts() {
|
||||
PlatformBlockState calcite = state("minecraft:calcite", true);
|
||||
PlatformBlockState machine = state("test:machine", true);
|
||||
when(machine.hasTileEntity()).thenReturn(true);
|
||||
for (PlatformBlockState replacement : List.of(calcite, machine)) {
|
||||
IrisObjectReplace edit = mock(IrisObjectReplace.class);
|
||||
IrisMaterialPalette palette = mock(IrisMaterialPalette.class);
|
||||
when(edit.getChance()).thenReturn(1F);
|
||||
when(edit.getFind(data)).thenReturn(new KList<>(solid));
|
||||
when(edit.getReplace(any(RNG.class), anyDouble(), anyDouble(), anyDouble(), any(IrisData.class)))
|
||||
.thenReturn(replacement);
|
||||
when(edit.getReplace()).thenReturn(palette);
|
||||
when(palette.getTile(any(RNG.class), anyDouble(), anyDouble(), anyDouble(), any(IrisData.class)))
|
||||
.thenReturn(Optional.empty());
|
||||
RecordingPlacer placer = new RecordingPlacer(null);
|
||||
IrisObjectPlacement placement = placement().setMode(ObjectPlaceMode.CENTER_STILT)
|
||||
.setStiltSettings(new IrisStiltSettings().setExclude(new KList<>("minecraft:calcite")));
|
||||
placement.getEdit().add(edit);
|
||||
|
||||
lineObject(3).place(0, ANCHOR_Y + 10, 0, placer, placement, new RNG(2L), data);
|
||||
|
||||
assertEquals(replacement.key(), 3, placer.writesOf(replacement).size());
|
||||
assertTrue(placer.writesOf(replacement).stream().allMatch(write -> write.y() == ANCHOR_Y + 10));
|
||||
}
|
||||
}
|
||||
|
||||
private IrisObjectPlacement placement() {
|
||||
IrisObjectPlacement placement = new IrisObjectPlacement();
|
||||
placement.setMode(ObjectPlaceMode.CENTER_HEIGHT);
|
||||
|
||||
+21
@@ -139,6 +139,27 @@ public class IrisRendererPerformanceContractTest {
|
||||
assertTrue(samples.get() < 128);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void studioBiomeBaseDoesNotEnterHydrologyPlanning() {
|
||||
Engine engine = mock(Engine.class);
|
||||
IrisComplex complex = mock(IrisComplex.class);
|
||||
IrisBiome biome = mock(IrisBiome.class);
|
||||
@SuppressWarnings("unchecked")
|
||||
ProceduralStream<IrisBiome> base = mock(ProceduralStream.class);
|
||||
when(engine.getComplex()).thenReturn(complex);
|
||||
when(complex.getBaseBiomeStream()).thenReturn(base);
|
||||
when(base.get(org.mockito.ArgumentMatchers.anyDouble(), org.mockito.ArgumentMatchers.anyDouble()))
|
||||
.thenReturn(biome);
|
||||
when(biome.getColor(engine, RenderType.BIOME)).thenReturn(Color.GREEN);
|
||||
|
||||
BufferedImage image = new IrisRenderer(engine).renderStudioBase(
|
||||
0D, 0D, 256D, 64, RenderType.BIOME, () -> false);
|
||||
|
||||
assertEquals(Color.GREEN.getRGB(), image.getRGB(0, 0));
|
||||
assertEquals(Color.GREEN.getRGB(), image.getRGB(63, 63));
|
||||
verify(complex, never()).getHydrologyRuntime();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void planarStudioHeightPagesInterpolateWithoutPerPixelEngineSampling() {
|
||||
Engine engine = mock(Engine.class);
|
||||
|
||||
@@ -134,6 +134,32 @@ public class IrisRendererRiverTest {
|
||||
verify(runtime).sampleRenderFootprint(12D, -7D, 20D, 1D);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void biomeRefinementCopiesTheBaseAndRetainsTheFullRiverOverlay() {
|
||||
Engine engine = mock(Engine.class);
|
||||
IrisComplex complex = mock(IrisComplex.class);
|
||||
IrisHydrologyRuntime runtime = mock(IrisHydrologyRuntime.class);
|
||||
IrisBiome biome = mock(IrisBiome.class);
|
||||
@SuppressWarnings("unchecked")
|
||||
ProceduralStream<IrisBiome> base = mock(ProceduralStream.class);
|
||||
when(engine.getComplex()).thenReturn(complex);
|
||||
when(complex.getHydrologyRuntime()).thenReturn(runtime);
|
||||
when(complex.getBaseBiomeStream()).thenReturn(base);
|
||||
when(base.get(12D, -7D)).thenReturn(biome);
|
||||
when(biome.getColor(engine, RenderType.BIOME)).thenReturn(Color.GREEN);
|
||||
when(runtime.sampleRenderFootprint(12D, -7D, 20D, 1D))
|
||||
.thenReturn(sample(HydrologyFeatureType.SURFACE_POOL, false));
|
||||
IrisRenderer renderer = new IrisRenderer(engine);
|
||||
BufferedImage preview = renderer.renderStudioBase(12D, -7D, 8D, 1, RenderType.BIOME, () -> false);
|
||||
|
||||
BufferedImage refined = renderer.refineStudioBiome(12D, -7D, 8D, preview, () -> false);
|
||||
BufferedImage complete = renderer.renderStudio(12D, -7D, 8D, 1, RenderType.BIOME, () -> false);
|
||||
|
||||
assertEquals(Color.GREEN.getRGB(), preview.getRGB(0, 0));
|
||||
assertNotEquals(preview.getRGB(0, 0), refined.getRGB(0, 0));
|
||||
assertEquals(complete.getRGB(0, 0), refined.getRGB(0, 0));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void riverVisionShowsAProjectedCandidateWithoutTreatingItAsAccepted() {
|
||||
Engine engine = mock(Engine.class);
|
||||
|
||||
@@ -11,6 +11,7 @@ import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.CancellationException;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.function.BooleanSupplier;
|
||||
@@ -68,7 +69,7 @@ public class VisionRenderControllerTest {
|
||||
public void everyPublishedPageIsFinalResolutionAndIdenticalRequestsAreCacheHits() throws Exception {
|
||||
IrisRenderer renderer = mock(IrisRenderer.class);
|
||||
AtomicInteger calls = new AtomicInteger();
|
||||
when(renderer.renderStudio(
|
||||
when(renderer.renderStudioBase(
|
||||
anyDouble(),
|
||||
anyDouble(),
|
||||
anyDouble(),
|
||||
@@ -84,6 +85,7 @@ public class VisionRenderControllerTest {
|
||||
BufferedImage.TYPE_INT_RGB
|
||||
);
|
||||
});
|
||||
passThroughRefinement(renderer);
|
||||
VisionRenderController controller = controller(2);
|
||||
VisionRenderController.RenderSpec spec = spec(renderer, RenderType.BIOME, 1L, 0D, 0D, 4D, 128, 128);
|
||||
try {
|
||||
@@ -153,6 +155,12 @@ public class VisionRenderControllerTest {
|
||||
latestStarted.countDown();
|
||||
return new BufferedImage(64, 64, BufferedImage.TYPE_INT_RGB);
|
||||
});
|
||||
when(renderer.renderStudioBase(anyDouble(), anyDouble(), anyDouble(), anyInt(),
|
||||
any(RenderType.class), any(BooleanSupplier.class))).thenAnswer(invocation -> {
|
||||
latestStarted.countDown();
|
||||
return new BufferedImage(64, 64, BufferedImage.TYPE_INT_RGB);
|
||||
});
|
||||
passThroughRefinement(renderer);
|
||||
VisionRenderController controller = controller(2);
|
||||
try {
|
||||
controller.request(spec(renderer, RenderType.REGION, 1L, 0D, 0D, 4D, 64, 64));
|
||||
@@ -166,6 +174,103 @@ public class VisionRenderControllerTest {
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void completeBaseIsVisibleBeforeBlockedHydrologyRefinementStarts() throws Exception {
|
||||
IrisRenderer renderer = exactRenderer();
|
||||
CountDownLatch overlayStarted = new CountDownLatch(1);
|
||||
CountDownLatch releaseOverlay = new CountDownLatch(1);
|
||||
AtomicInteger baseCalls = new AtomicInteger();
|
||||
AtomicInteger overlays = new AtomicInteger();
|
||||
when(renderer.renderStudioBase(anyDouble(), anyDouble(), anyDouble(), anyInt(),
|
||||
any(RenderType.class), any(BooleanSupplier.class))).thenAnswer(invocation -> {
|
||||
baseCalls.incrementAndGet();
|
||||
return new BufferedImage(64, 64, BufferedImage.TYPE_INT_RGB);
|
||||
});
|
||||
when(renderer.refineStudioBiome(anyDouble(), anyDouble(), anyDouble(), any(BufferedImage.class),
|
||||
any(BooleanSupplier.class))).thenAnswer(invocation -> {
|
||||
overlays.incrementAndGet();
|
||||
overlayStarted.countDown();
|
||||
assertTrue(releaseOverlay.await(2L, TimeUnit.SECONDS));
|
||||
return invocation.getArgument(3);
|
||||
});
|
||||
VisionRenderController controller = controller(3);
|
||||
try {
|
||||
VisionRenderController.RenderSpec spec = spec(renderer, RenderType.BIOME, 1L, 0D, 0D, 4D, 192, 128);
|
||||
VisionRenderController.Frame frame = controller.request(spec);
|
||||
assertTrue(overlayStarted.await(2L, TimeUnit.SECONDS));
|
||||
assertEquals(frame.tiles().size(), baseCalls.get());
|
||||
assertEquals(1, overlays.get());
|
||||
assertEquals(0, controller.progress(frame).ready());
|
||||
for (VisionRenderController.VisibleTile tile : frame.tiles()) {
|
||||
assertEquals(64, controller.image(frame, tile).getWidth());
|
||||
}
|
||||
|
||||
releaseOverlay.countDown();
|
||||
Await.until("every biome overlay to complete", Duration.ofSeconds(2L),
|
||||
() -> controller.progress(frame).ready() == frame.tiles().size());
|
||||
assertEquals(frame.tiles().size(), overlays.get());
|
||||
|
||||
VisionRenderController.Frame cached = controller.request(spec);
|
||||
assertEquals(frame.tiles().size(), controller.progress(cached).ready());
|
||||
assertEquals(frame.tiles().size(), baseCalls.get());
|
||||
assertEquals(frame.tiles().size(), overlays.get());
|
||||
} finally {
|
||||
releaseOverlay.countDown();
|
||||
controller.close();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void cancelledOverlayKeepsCachedBaseWithoutMarkingItComplete() throws Exception {
|
||||
IrisRenderer renderer = exactRenderer();
|
||||
CountDownLatch staleStarted = new CountDownLatch(1);
|
||||
CountDownLatch latestStarted = new CountDownLatch(1);
|
||||
CountDownLatch releaseLatest = new CountDownLatch(1);
|
||||
AtomicInteger baseCalls = new AtomicInteger();
|
||||
AtomicInteger overlayCalls = new AtomicInteger();
|
||||
when(renderer.renderStudioBase(anyDouble(), anyDouble(), anyDouble(), anyInt(),
|
||||
any(RenderType.class), any(BooleanSupplier.class))).thenAnswer(invocation -> {
|
||||
baseCalls.incrementAndGet();
|
||||
return new BufferedImage(64, 64, BufferedImage.TYPE_INT_RGB);
|
||||
});
|
||||
when(renderer.refineStudioBiome(anyDouble(), anyDouble(), anyDouble(), any(BufferedImage.class),
|
||||
any(BooleanSupplier.class))).thenAnswer(invocation -> {
|
||||
if (overlayCalls.getAndIncrement() == 0) {
|
||||
staleStarted.countDown();
|
||||
BooleanSupplier cancelled = invocation.getArgument(4);
|
||||
while (!cancelled.getAsBoolean()) {
|
||||
Thread.onSpinWait();
|
||||
}
|
||||
throw new CancellationException();
|
||||
}
|
||||
latestStarted.countDown();
|
||||
assertTrue(releaseLatest.await(2L, TimeUnit.SECONDS));
|
||||
return invocation.getArgument(3);
|
||||
});
|
||||
VisionRenderController controller = controller(2);
|
||||
try {
|
||||
VisionRenderController.RenderSpec spec = spec(renderer, RenderType.BIOME, 1L, 0D, 0D, 4D, 128, 128);
|
||||
VisionRenderController.Frame stale = controller.request(spec);
|
||||
assertTrue(staleStarted.await(2L, TimeUnit.SECONDS));
|
||||
|
||||
VisionRenderController.Frame latest = controller.request(spec);
|
||||
assertTrue(latestStarted.await(2L, TimeUnit.SECONDS));
|
||||
assertEquals(stale.tiles().size(), baseCalls.get());
|
||||
assertEquals(0, controller.progress(latest).ready());
|
||||
for (VisionRenderController.VisibleTile tile : latest.tiles()) {
|
||||
assertEquals(64, controller.image(latest, tile).getWidth());
|
||||
}
|
||||
|
||||
releaseLatest.countDown();
|
||||
Await.until("the replacement frame to finish its overlays", Duration.ofSeconds(2L),
|
||||
() -> controller.progress(latest).ready() == latest.tiles().size());
|
||||
assertEquals(0, controller.progress(stale).ready());
|
||||
} finally {
|
||||
releaseLatest.countDown();
|
||||
controller.close();
|
||||
}
|
||||
}
|
||||
|
||||
private static VisionRenderController controller(int workers) {
|
||||
return new VisionRenderController(
|
||||
() -> {
|
||||
@@ -188,9 +293,18 @@ public class VisionRenderControllerTest {
|
||||
invocation.getArgument(3),
|
||||
BufferedImage.TYPE_INT_RGB
|
||||
));
|
||||
when(renderer.renderStudioBase(anyDouble(), anyDouble(), anyDouble(), anyInt(),
|
||||
any(RenderType.class), any(BooleanSupplier.class))).thenAnswer(invocation -> new BufferedImage(
|
||||
invocation.getArgument(3), invocation.getArgument(3), BufferedImage.TYPE_INT_RGB));
|
||||
passThroughRefinement(renderer);
|
||||
return renderer;
|
||||
}
|
||||
|
||||
private static void passThroughRefinement(IrisRenderer renderer) {
|
||||
when(renderer.refineStudioBiome(anyDouble(), anyDouble(), anyDouble(), any(BufferedImage.class),
|
||||
any(BooleanSupplier.class))).thenAnswer(invocation -> invocation.getArgument(3));
|
||||
}
|
||||
|
||||
private static Set<VisionRenderController.TileKey> keys(VisionRenderController.Frame frame) {
|
||||
Set<VisionRenderController.TileKey> keys = new HashSet<>();
|
||||
for (VisionRenderController.VisibleTile tile : frame.tiles()) {
|
||||
|
||||
@@ -37,6 +37,7 @@ import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.concurrent.locks.ReentrantReadWriteLock;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertSame;
|
||||
import static org.junit.Assert.assertThrows;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
@@ -45,6 +46,7 @@ import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
import static org.mockito.Mockito.times;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.verifyNoInteractions;
|
||||
|
||||
public class SavedBiomeRuntimeTest {
|
||||
@ClassRule
|
||||
@@ -102,6 +104,39 @@ public class SavedBiomeRuntimeTest {
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void packWithoutDropRulesDoesNotNeedAChunkQueryForTheOnlyEpoch() throws Exception {
|
||||
IrisData current = mock(IrisData.class);
|
||||
when(engine.getData()).thenReturn(current);
|
||||
GenerationManifest manifest = history.manifest();
|
||||
GenerationEpoch active = manifest.epoch("a".repeat(64)).orElseThrow();
|
||||
when(manifest.activeEpoch()).thenReturn(active);
|
||||
when(manifest.epochs()).thenReturn(List.of(active));
|
||||
|
||||
try (SavedBiomeRuntime runtime = new SavedBiomeRuntime(engine, history)) {
|
||||
assertFalse(runtime.mayHaveBlockDropRules("minecraft:stone"));
|
||||
verifyNoInteractions(store);
|
||||
when(current.hasBlockDropRules("minecraft:stone")).thenReturn(true);
|
||||
assertTrue(runtime.mayHaveBlockDropRules("minecraft:stone"));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void unloadedHistoricalPackRetainsDropChecksWhenTheCurrentPackHasNoRules() throws Exception {
|
||||
when(engine.getData()).thenReturn(mock(IrisData.class));
|
||||
GenerationManifest manifest = history.manifest();
|
||||
GenerationEpoch previous = manifest.epoch("a".repeat(64)).orElseThrow();
|
||||
GenerationEpoch active = mock(GenerationEpoch.class);
|
||||
when(active.epochId()).thenReturn("b".repeat(64));
|
||||
when(manifest.activeEpoch()).thenReturn(active);
|
||||
when(manifest.epochs()).thenReturn(List.of(previous, active));
|
||||
|
||||
try (SavedBiomeRuntime runtime = new SavedBiomeRuntime(engine, history)) {
|
||||
assertTrue(runtime.mayHaveBlockDropRules("minecraft:stone"));
|
||||
verifyNoInteractions(store);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void chunkReadinessDoesNotRequireEveryCellToHaveAResolvedIdentity() throws Exception {
|
||||
when(store.get(0, 0)).thenReturn(Optional.of(unresolvedChunk(1)));
|
||||
|
||||
Reference in new issue
Block a user