From 257c7d4f873145ff2a29815c772aa3380671bee1 Mon Sep 17 00:00:00 2001 From: Brian Neumann-Fopiano Date: Thu, 15 Oct 2026 19:04:45 -0400 Subject: [PATCH] Filtered visible biome locate - /locate biome on non-stacked Iris dimensions searches the visible biomes chunks are filled with instead of falling back to the vanilla structure-biome search - BiomeSearchFilter rejects columns whose region, land, sea, shore, cave, carving, subterrain or image-map biomes cannot hold a target, so terrain height is computed only for columns that can match - Hydrology planning state is consulted only when a target comes from a river policy, so a locate no longer queues hydrology tile plans across its whole radius - Admitted samples resolve inside their generation-history coordinate scope, so saved-biome reads for generated chunks no longer fail the command - HintedLocator surface hints and IrisBiome visible key enumeration are shared with the locate filter - Bukkit and modded biome policies share the same filtered search --- .../arcane/iris/modded/ModdedBiomePolicy.java | 81 +++++- .../iris/modded/ModdedBiomePolicyTest.java | 98 +++++++ .../iris/generation/biome/IrisBiome.java | 4 + .../biome/IrisBiomeDerivatives.java | 14 + .../generation/locator/BiomeSearchFilter.java | 183 ++++++++++++ .../generation/locator/HintedLocator.java | 111 ++++---- .../bukkit/nms/BukkitBiomePolicy.java | 102 +++++-- .../biome/IrisBiomeVisibleKeysTest.java | 55 ++++ .../locator/BiomeSearchFilterTest.java | 267 ++++++++++++++++++ .../bukkit/nms/BukkitBiomeLocateTest.java | 130 +++++++++ 10 files changed, 960 insertions(+), 85 deletions(-) create mode 100644 core/src/main/java/art/arcane/iris/generation/locator/BiomeSearchFilter.java create mode 100644 core/src/test/java/art/arcane/iris/generation/biome/IrisBiomeVisibleKeysTest.java create mode 100644 core/src/test/java/art/arcane/iris/generation/locator/BiomeSearchFilterTest.java create mode 100644 core/src/test/java/art/arcane/iris/platform/bukkit/nms/BukkitBiomeLocateTest.java diff --git a/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedBiomePolicy.java b/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedBiomePolicy.java index d7266c6a2..8cd709adf 100644 --- a/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedBiomePolicy.java +++ b/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedBiomePolicy.java @@ -42,6 +42,7 @@ import art.arcane.iris.generation.biome.IrisBiome; import art.arcane.iris.generation.biome.IrisBiomeCustom; import art.arcane.iris.generation.terrain.IrisDimension; import art.arcane.iris.generation.context.IrisContext; +import art.arcane.iris.generation.locator.BiomeSearchFilter; import art.arcane.volmlib.util.math.RNG; import java.io.IOException; @@ -300,10 +301,6 @@ final class ModdedBiomePolicy implements NativeModdedBiomePolicy { if (!isReady(engine)) { throw new IllegalStateException("Iris visible biome search has no active engine runtime"); } - DimensionStackContext stackContext = engine.getDimensionStackContext(); - if (stackContext == null) { - return query.defaultFallback().get(); - } Set candidates = new HashSet<>(); for (H biome : collectVisibleBiomeHolders()) { if (query.allowed().test(biome)) { @@ -313,16 +310,86 @@ final class ModdedBiomePolicy implements NativeModdedBiomePolicy { if (candidates.isEmpty()) { return null; } + DimensionStackContext stackContext = engine.getDimensionStackContext(); + if (stackContext != null) { + return query.search().search(candidates, (blockX, blockZ) -> { + int quartX = blockX >> 2; + int quartZ = blockZ >> 2; + DimensionStackLayout stackLayout = stackContext.sample(quartX << 2, quartZ << 2); + return blockY -> resolveVisibleBiome(engine, quartX, blockY >> 2, quartZ, + query.sampler(), stackLayout); + }); + } + BiomeSearchFilter filter = BiomeSearchFilter.of(engine, searchBiomeKeys(engine, candidates)); + int minHeight = engine.getMinHeight(); return query.search().search(candidates, (blockX, blockZ) -> { int quartX = blockX >> 2; int quartZ = blockZ >> 2; - DimensionStackLayout stackLayout = stackContext.sample(quartX << 2, quartZ << 2); - return blockY -> resolveVisibleBiome(engine, quartX, blockY >> 2, quartZ, - query.sampler(), stackLayout); + BiomeSearchFilter.Column column = filter.column(quartX << 2, quartZ << 2); + return blockY -> column.admits(((blockY >> 2) << 2) - minHeight) + ? locateVisibleBiome(engine, quartX, blockY >> 2, quartZ, query.sampler()) + : null; }); } } + private H locateVisibleBiome(Engine engine, int quartX, int quartY, int quartZ, S sampler) { + try (GenerationHistoryRuntimeRouter.CoordinateScope ignored = openHistoryCoordinateScope( + engine, quartX << 2, quartZ << 2, "modded_locate_visible_biome")) { + return resolveVisibleBiome(engine, quartX, quartY, quartZ, sampler, null); + } + } + + private Set searchBiomeKeys(Engine engine, Set candidates) { + NativeBiomeSourceAccess.RegistryView registry = biomeRegistry(); + if (registry == null) { + throw new IllegalStateException("Iris visible biome search has no biome registry"); + } + H plains = resolveHolder(registry, "minecraft:plains"); + boolean fallbackCandidate = plains != null && candidates.contains(plains); + Set keys = new HashSet<>(); + for (OwnedBiome ownedBiome : getOwnedBiomes(engine)) { + if (emitsCandidate(engine, ownedBiome, registry, candidates, fallbackCandidate)) { + keys.add(ownedBiome.biome().getLoadKey()); + } + } + return keys; + } + + private boolean emitsCandidate(Engine engine, OwnedBiome ownedBiome, NativeBiomeSourceAccess.RegistryView registry, + Set candidates, boolean fallbackCandidate) { + IrisBiome irisBiome = ownedBiome.biome(); + if (irisBiome.isCustom()) { + for (IrisBiomeCustom customBiome : irisBiome.getCustomDerivitives()) { + if (customBiome == null) { + if (fallbackCandidate) { + return true; + } + continue; + } + String key = ownedBiome.dimension() == engine.getDimension() && ownedBiome.data() == engine.getData() + ? ModdedWorldgenIds.biomeRef(engine, customBiome.getId()) + : ownedBiome.data().customBiomeResourceKey(ownedBiome.dimension(), customBiome); + if (emits(registry, key, candidates, fallbackCandidate)) { + return true; + } + } + return false; + } + for (String key : irisBiome.getVisibleBiomeKeys(engine)) { + if (emits(registry, key, candidates, fallbackCandidate)) { + return true; + } + } + return false; + } + + private static boolean emits(NativeBiomeSourceAccess.RegistryView registry, String key, + Set candidates, boolean fallbackCandidate) { + H holder = key == null ? null : resolveHolder(registry, key); + return holder == null ? fallbackCandidate : candidates.contains(holder); + } + private H getNoiseBiome(Engine engine, int quartX, int quartY, int quartZ, S sampler) { if (isGuaranteedSurfaceBiome(engine, quartY)) { diff --git a/adapters/modded-common/src/test/java/art/arcane/iris/modded/ModdedBiomePolicyTest.java b/adapters/modded-common/src/test/java/art/arcane/iris/modded/ModdedBiomePolicyTest.java index 59a5d4d88..f2aff0f79 100644 --- a/adapters/modded-common/src/test/java/art/arcane/iris/modded/ModdedBiomePolicyTest.java +++ b/adapters/modded-common/src/test/java/art/arcane/iris/modded/ModdedBiomePolicyTest.java @@ -6,11 +6,17 @@ import art.arcane.iris.generation.runtime.DimensionStackLayout; import art.arcane.iris.generation.terrain.IrisDimension; import art.arcane.iris.generation.subterrain.IrisSubterrainFeature; import art.arcane.iris.pack.loading.IrisData; +import art.arcane.iris.pack.loading.ResourceLoader; +import art.arcane.iris.generation.terrain.IrisRegion; import art.arcane.volmlib.util.collection.KList; import art.arcane.iris.generation.runtime.GenerationSessionException; import art.arcane.iris.generation.runtime.GenerationSessionLease; import art.arcane.iris.generation.runtime.IrisComplex; +import art.arcane.iris.generation.runtime.IrisEngine; +import art.arcane.iris.world.history.GenerationHistoryRuntimeRouter; import art.arcane.volmlib.nativelib.terrain.NativeBiomeSourceAccess; +import art.arcane.volmlib.nativelib.terrain.NativeBiomeSourcePolicy; +import art.arcane.volmlib.nativelib.terrain.NativeModdedBiomePolicy; import art.arcane.volmlib.util.stream.ProceduralStream; import art.arcane.volmlib.util.stream.interpolation.Interpolated; import org.junit.Test; @@ -20,7 +26,9 @@ import java.io.File; import java.lang.reflect.Field; import java.lang.reflect.Method; import java.util.List; +import java.util.Optional; import java.util.Set; +import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicReference; import java.util.function.Consumer; @@ -29,8 +37,12 @@ import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertSame; import static org.junit.Assert.assertThrows; import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyNoInteractions; import static org.mockito.Mockito.when; @@ -140,6 +152,92 @@ public class ModdedBiomePolicyTest { assertThrows(IllegalStateException.class, () -> boundTo(unready).getVisibleSurfaceBiome(8, 8)); } + /** + * Vanilla /locate biome samples a 6400 block radius every 32 blocks. Each Iris surface sample needs the column's + * terrain height, so the search may only resolve columns whose region and biome streams can hold the target, and + * it must answer with the visible biome a chunk is filled with. + */ + @Test + public void locateResolvesVisibleBiomesOnlyInColumnsThatCanHoldTheTarget() throws Exception { + BiomeToken mirage = new BiomeToken("terralith:mirage_isles"); + BiomeToken plains = new BiomeToken("minecraft:plains"); + IrisBiome isles = new IrisBiome().setDerivative("terralith:mirage_isles"); + isles.setLoadKey("isles"); + IrisBiome meadow = new IrisBiome().setDerivative("minecraft:plains"); + meadow.setLoadKey("meadow"); + IrisRegion main = new IrisRegion().setLandBiomes(new KList<>("meadow")); + main.setLoadKey("main"); + IrisRegion archipelago = new IrisRegion().setLandBiomes(new KList<>("isles")); + archipelago.setLoadKey("archipelago"); + IrisDimension dimension = mock(IrisDimension.class); + when(dimension.getReachableBiomes(any())).thenReturn(new KList<>(meadow, isles)); + when(dimension.getAllRegions(any())).thenReturn(new KList<>(main, archipelago)); + when(dimension.getCarving()).thenReturn(new KList<>()); + when(dimension.getSubterrainFeatures()).thenReturn(new KList<>()); + @SuppressWarnings("unchecked") + ResourceLoader biomes = (ResourceLoader) mock(ResourceLoader.class); + when(biomes.load("meadow")).thenReturn(meadow); + when(biomes.load("isles")).thenReturn(isles); + IrisData data = mock(IrisData.class); + when(data.getDataFolder()).thenReturn(new File("synthetic-pack")); + when(data.getBiomeLoader()).thenReturn(biomes); + when(data.physicalBiomeResourceKey(any(), anyString())).thenAnswer(invocation -> invocation.getArgument(1)); + IrisComplex complex = mock(IrisComplex.class); + when(complex.getRegionStream()).thenReturn(ProceduralStream.of( + (x, z) -> x >= 4096D ? archipelago : main, Interpolated.of(value -> 0D, value -> main))); + when(complex.getLandBiomeStream()).thenReturn(ProceduralStream.of( + (x, z) -> x >= 4096D ? isles : meadow, Interpolated.of(value -> 0D, value -> meadow))); + when(complex.getSeaBiomeStream()).thenReturn(ProceduralStream.of( + (x, z) -> meadow, Interpolated.of(value -> 0D, value -> meadow))); + when(complex.getShoreBiomeStream()).thenReturn(ProceduralStream.of( + (x, z) -> meadow, Interpolated.of(value -> 0D, value -> meadow))); + when(complex.historicalPhysicalBiomeKeyAt(anyInt(), anyInt(), anyInt())).thenReturn(Optional.empty()); + GenerationHistoryRuntimeRouter router = mock(GenerationHistoryRuntimeRouter.class); + GenerationHistoryRuntimeRouter.CoordinateScope scope = mock(GenerationHistoryRuntimeRouter.CoordinateScope.class); + when(router.openCoordinateScope(anyInt(), anyInt())).thenReturn(scope); + IrisEngine engine = mock(IrisEngine.class); + when(engine.getGenerationHistoryRuntimeRouter()).thenReturn(Optional.of(router)); + when(engine.getDimension()).thenReturn(dimension); + when(engine.getData()).thenReturn(data); + when(engine.getComplex()).thenReturn(complex); + when(engine.getMinHeight()).thenReturn(-64); + when(engine.getCacheID()).thenReturn(1); + when(engine.acquireGenerationLease(anyString())).thenReturn(GenerationSessionLease.noop()); + when(engine.answersFromNaturalTerrain(anyInt(), anyInt())).thenReturn(true); + AtomicInteger heightSamples = new AtomicInteger(); + when(engine.getHostSurfaceBiome(anyInt(), anyInt())).thenAnswer(invocation -> { + heightSamples.incrementAndGet(); + return invocation.getArgument(0) >= 4096 ? isles : meadow; + }); + ModdedBiomePolicy policy = new ModdedBiomePolicy<>( + new Access(new ArrayList<>(List.of(plains, mirage)))); + policy.bind(new ModdedBiomePolicy.RuntimeCallbacks(() -> engine, () -> engine, bound -> false, + () -> Set.of(plains.key(), mirage.key()), bound -> Set.of(), () -> 0L)); + int[] columns = {0, 1024, 2048, 3072, 4096, 5120}; + + NativeBiomeSourcePolicy.BiomeLocation found = policy.findClosestBiome3d( + new NativeModdedBiomePolicy.ClosestQuery<>(mirage::equals, null, + () -> { + throw new AssertionError("A bound engine must not use the unbound search"); + }, + (candidates, resolver) -> { + for (int x : columns) { + NativeBiomeSourcePolicy.Column column = resolver.resolve(x, 0); + BiomeToken biome = column.sample(100); + if (candidates.contains(biome)) { + return new NativeBiomeSourcePolicy.BiomeLocation<>(x, 100, 0, biome); + } + } + return null; + })); + + assertEquals(new NativeBiomeSourcePolicy.BiomeLocation<>(4096, 100, 0, mirage), found); + assertEquals(1, heightSamples.get()); + verify(router).openCoordinateScope(4096, 0); + verify(router, times(1)).openCoordinateScope(anyInt(), anyInt()); + verify(scope).close(); + } + private static ModdedBiomePolicy boundTo(Engine engine) { ModdedBiomePolicy policy = new ModdedBiomePolicy<>(new Access(new ArrayList<>())); policy.bind(new ModdedBiomePolicy.RuntimeCallbacks(() -> engine, () -> engine, bound -> true, diff --git a/core/src/main/java/art/arcane/iris/generation/biome/IrisBiome.java b/core/src/main/java/art/arcane/iris/generation/biome/IrisBiome.java index 9f7c9b7d7..e70c2ef91 100644 --- a/core/src/main/java/art/arcane/iris/generation/biome/IrisBiome.java +++ b/core/src/main/java/art/arcane/iris/generation/biome/IrisBiome.java @@ -669,6 +669,10 @@ public class IrisBiome extends IrisRegistrant implements Rarity { return IrisBiomeDerivatives.getGroundBiomeKey(this, rng, engine, x, y, z); } + public KList getVisibleBiomeKeys(Engine engine) { + return IrisBiomeDerivatives.getVisibleBiomeKeys(this, engine); + } + public Color getColor(Engine engine, RenderType type) { return IrisBiomeColorRenderer.getColor(this, engine, type); } diff --git a/core/src/main/java/art/arcane/iris/generation/biome/IrisBiomeDerivatives.java b/core/src/main/java/art/arcane/iris/generation/biome/IrisBiomeDerivatives.java index 4e42116e5..132b9f2e9 100644 --- a/core/src/main/java/art/arcane/iris/generation/biome/IrisBiomeDerivatives.java +++ b/core/src/main/java/art/arcane/iris/generation/biome/IrisBiomeDerivatives.java @@ -144,6 +144,20 @@ final class IrisBiomeDerivatives { return physicalBiomeKey(engine, logicalKey); } + static KList getVisibleBiomeKeys(IrisBiome biome, Engine engine) { + KList keys = new KList<>(); + if (biome.getBiomeScatter().isEmpty()) { + keys.add(physicalBiomeKey(engine, biome.getDerivativeKey())); + } + for (String scatter : biome.getBiomeScatter()) { + keys.add(physicalBiomeKey(engine, namespacedBiomeKey(scatter))); + } + for (String scatter : biome.getBiomeSkyScatter()) { + keys.add(physicalBiomeKey(engine, namespacedBiomeKey(scatter))); + } + return keys; + } + static IrisBiomeCustom getCustomBiome(IrisBiome biome, RNG rng, Engine engine, double x, double y, double z) { KList customDerivitives = biome.getCustomDerivitives(); diff --git a/core/src/main/java/art/arcane/iris/generation/locator/BiomeSearchFilter.java b/core/src/main/java/art/arcane/iris/generation/locator/BiomeSearchFilter.java new file mode 100644 index 000000000..6f042f09f --- /dev/null +++ b/core/src/main/java/art/arcane/iris/generation/locator/BiomeSearchFilter.java @@ -0,0 +1,183 @@ +package art.arcane.iris.generation.locator; + +import art.arcane.iris.generation.biome.IrisBiome; +import art.arcane.iris.generation.hydrology.IrisRiverPolicy; +import art.arcane.iris.generation.image.IrisImageMapApplication; +import art.arcane.iris.generation.image.IrisImageMapRuntime; +import art.arcane.iris.generation.runtime.Engine; +import art.arcane.iris.generation.runtime.IrisComplex; +import art.arcane.iris.generation.subterrain.IrisSubterrainFeature; +import art.arcane.iris.generation.subterrain.SubterrainCell; +import art.arcane.iris.generation.subterrain.SubterrainPlanner; +import art.arcane.iris.generation.terrain.IrisDimension; +import art.arcane.iris.generation.terrain.IrisDimensionCarvingEntry; +import art.arcane.iris.generation.terrain.IrisDimensionCarvingResolver; +import art.arcane.iris.generation.terrain.IrisRegion; + +import java.util.Objects; +import java.util.Set; + +public final class BiomeSearchFilter { + private static final Column ALL = y -> true; + private static final Column NONE = y -> false; + + private final Engine engine; + private final IrisComplex complex; + private final Set biomes; + private final boolean unfiltered; + private final boolean hydrology; + private final HintedLocator.CoarseSample surface; + private final IrisImageMapRuntime imageMaps; + private final boolean caves; + private final boolean carving; + private final boolean subterrain; + private final boolean transitions; + private final int minHeight; + private final int caveSwitchY; + + private BiomeSearchFilter(Engine engine, Set biomes) { + this.engine = engine; + this.complex = engine.getComplex(); + this.biomes = Set.copyOf(biomes); + IrisDimension dimension = engine.getDimension(); + unfiltered = engine.getDimensionStackContext() != null || complex.getFocusBiome() != null + || complex.getFocusRegion() != null + || complex.getBiomeBuffet() != null && !complex.getBiomeBuffet().cells().isEmpty(); + hydrology = complex.getHydrologyRuntime() != null && hostsRiverBiome(dimension); + surface = this.biomes.isEmpty() ? null : HintedLocator.surfaceCoarse(engine, this.biomes); + IrisImageMapRuntime maps = complex.getImageMapRuntime(); + imageMaps = maps != null && maps.has(IrisImageMapApplication.BIOME) ? maps : null; + caves = hostsCaveBiome(dimension); + carving = carvesBiome(dimension); + subterrain = hostsSubterrainBiome(dimension); + transitions = complex.getTransitionGenerationPlan() != null; + minHeight = engine.getMinHeight(); + caveSwitchY = Math.max(-8 - minHeight, 40); + } + + public static BiomeSearchFilter of(Engine engine, Set biomeKeys) { + return new BiomeSearchFilter(Objects.requireNonNull(engine, "engine"), + Objects.requireNonNull(biomeKeys, "biome keys")); + } + + public Column column(int x, int z) { + if (unfiltered || hydrology && !engine.answersFromNaturalTerrain(x, z)) { + return ALL; + } + if (surfaceAdmits(x, z)) { + return ALL; + } + boolean cave = caves && target(complex.getCaveBiomeStream().get(x, z)); + if (!cave && !carving && !subterrain && !transitions) { + return NONE; + } + return y -> admits(x, y, z, cave); + } + + private boolean surfaceAdmits(int x, int z) { + IrisBiome mapped = imageMaps == null ? null : imageMaps.sampleBiome(x, z); + if (mapped != null) { + return target(mapped); + } + return surface != null && surface.test(x, z); + } + + private boolean admits(int x, int y, int z, boolean cave) { + if (transitions && complex.historicalPhysicalBiomeKeyAt(x, y + minHeight, z).isPresent()) { + return true; + } + if (subterrain && target(subterrainBiome(x, y + minHeight, z))) { + return true; + } + if (y > caveSwitchY) { + return false; + } + return cave || carving && target(IrisDimensionCarvingResolver.resolveBiome(engine, x, y + minHeight, z, null)); + } + + private String subterrainBiome(int x, int worldY, int z) { + SubterrainPlanner planner = complex.getSubterrainPlanner(); + SubterrainCell cell = planner == null ? SubterrainCell.OUTSIDE : planner.sample(x, worldY, z); + return cell.occupied() ? cell.room().biome() : null; + } + + private boolean target(IrisBiome biome) { + return biome != null && target(biome.getLoadKey()); + } + + private boolean target(String key) { + return key != null && biomes.contains(key.trim()); + } + + private boolean hostsRiverBiome(IrisDimension dimension) { + if (placesTarget(dimension.getRiverPolicy())) { + return true; + } + for (IrisRegion region : dimension.getAllRegions(engine)) { + if (region != null && placesTarget(region.getRiverPolicy())) { + return true; + } + } + for (IrisBiome biome : dimension.getReachableBiomes(engine)) { + if (biome != null && placesTarget(biome.getRiverPolicy())) { + return true; + } + } + return false; + } + + private boolean placesTarget(IrisRiverPolicy policy) { + if (policy == null) { + return false; + } + for (String key : policy.getAllBiomeIds()) { + if (HintedLocator.closureContains(engine, key, biomes)) { + return true; + } + } + return false; + } + + private boolean hostsCaveBiome(IrisDimension dimension) { + for (IrisRegion region : dimension.getAllRegions(engine)) { + if (region == null) { + continue; + } + for (String key : region.getCaveBiomes()) { + if (target(key)) { + return true; + } + } + } + return false; + } + + private boolean carvesBiome(IrisDimension dimension) { + if (dimension.getCarving() == null) { + return false; + } + for (IrisDimensionCarvingEntry entry : dimension.getCarving()) { + if (entry != null && entry.isEnabled() && target(entry.getBiome())) { + return true; + } + } + return false; + } + + private boolean hostsSubterrainBiome(IrisDimension dimension) { + if (dimension.getSubterrainFeatures() == null) { + return false; + } + for (IrisSubterrainFeature feature : dimension.getSubterrainFeatures()) { + if (feature != null && feature.isEnabled() && target(feature.getBiome())) { + return true; + } + } + return false; + } + + @FunctionalInterface + public interface Column { + boolean admits(int y); + } +} diff --git a/core/src/main/java/art/arcane/iris/generation/locator/HintedLocator.java b/core/src/main/java/art/arcane/iris/generation/locator/HintedLocator.java index e9d3ef308..cef9887bf 100644 --- a/core/src/main/java/art/arcane/iris/generation/locator/HintedLocator.java +++ b/core/src/main/java/art/arcane/iris/generation/locator/HintedLocator.java @@ -41,6 +41,7 @@ import art.arcane.volmlib.util.scheduling.PrecisionStopwatch; import java.util.ArrayDeque; import java.util.Comparator; +import java.util.Set; import java.util.concurrent.CompletableFuture; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; @@ -377,56 +378,16 @@ public final class HintedLocator implements Locator { } IrisComplex complex = engine.getComplex(); - BiomeSource source = new BiomeSource(engine); - KSet landHosts = new KSet<>(); - KSet seaHosts = new KSet<>(); - KSet shoreHosts = new KSet<>(); - KSet surfaceRegions = new KSet<>(); - KSet caveRegions = new KSet<>(); - - for (IrisRegion region : engine.getDimension().getAllRegions(engine)) { - if (region == null) { - continue; - } - - boolean hosted = false; - - for (String root : region.getLandBiomes()) { - if (source.closureContains(root, biomeKey)) { - landHosts.add(root); - hosted = true; - } - } - - for (String root : region.getSeaBiomes()) { - if (source.closureContains(root, biomeKey)) { - seaHosts.add(root); - hosted = true; - } - } - - for (String root : region.getShoreBiomes()) { - if (source.closureContains(root, biomeKey)) { - shoreHosts.add(root); - hosted = true; - } - } - - if (hosted) { - surfaceRegions.add(region.getLoadKey()); - } - - if (region.getCaveBiomes().contains(biomeKey)) { - caveRegions.add(region.getLoadKey()); - } + CoarseSample surface = surfaceCoarse(engine, Set.of(biomeKey)); + if (surface != null) { + return SearchPlan.of(surface, BIOME_STRIDE_CHUNKS, null); } - if (!surfaceRegions.isEmpty()) { - return SearchPlan.of( - surfaceCoarse(complex, surfaceRegions, landHosts, seaHosts, shoreHosts), - BIOME_STRIDE_CHUNKS, - null - ); + KSet caveRegions = new KSet<>(); + for (IrisRegion region : engine.getDimension().getAllRegions(engine)) { + if (region != null && region.getCaveBiomes().contains(biomeKey)) { + caveRegions.add(region.getLoadKey()); + } } if (!caveRegions.isEmpty()) { @@ -567,6 +528,54 @@ public final class HintedLocator implements Locator { return SearchPlan.of(coarse, 1, null); } + static boolean closureContains(Engine engine, String rootKey, Set biomeKeys) { + return new BiomeSource(engine).closureContains(rootKey, biomeKeys); + } + + static CoarseSample surfaceCoarse(Engine engine, Set biomeKeys) { + BiomeSource source = new BiomeSource(engine); + KSet landHosts = new KSet<>(); + KSet seaHosts = new KSet<>(); + KSet shoreHosts = new KSet<>(); + KSet surfaceRegions = new KSet<>(); + + for (IrisRegion region : engine.getDimension().getAllRegions(engine)) { + if (region == null) { + continue; + } + + boolean hosted = false; + + for (String root : region.getLandBiomes()) { + if (source.closureContains(root, biomeKeys)) { + landHosts.add(root); + hosted = true; + } + } + + for (String root : region.getSeaBiomes()) { + if (source.closureContains(root, biomeKeys)) { + seaHosts.add(root); + hosted = true; + } + } + + for (String root : region.getShoreBiomes()) { + if (source.closureContains(root, biomeKeys)) { + shoreHosts.add(root); + hosted = true; + } + } + + if (hosted) { + surfaceRegions.add(region.getLoadKey()); + } + } + + return surfaceRegions.isEmpty() ? null + : surfaceCoarse(engine.getComplex(), surfaceRegions, landHosts, seaHosts, shoreHosts); + } + private static CoarseSample surfaceCoarse(IrisComplex complex, KSet regions, KSet landHosts, KSet seaHosts, KSet shoreHosts) { return (x, z) -> { IrisRegion region = complex.getRegionStream().get(x, z); @@ -654,8 +663,8 @@ public final class HintedLocator implements Locator { return loaded; } - private boolean closureContains(String rootKey, String targetKey) { - if (rootKey == null || targetKey == null) { + private boolean closureContains(String rootKey, Set targetKeys) { + if (rootKey == null) { return false; } @@ -670,7 +679,7 @@ public final class HintedLocator implements Locator { continue; } - if (key.equals(targetKey)) { + if (targetKeys.contains(key)) { return true; } diff --git a/core/src/main/java/art/arcane/iris/platform/bukkit/nms/BukkitBiomePolicy.java b/core/src/main/java/art/arcane/iris/platform/bukkit/nms/BukkitBiomePolicy.java index ce9320045..91d6354f3 100644 --- a/core/src/main/java/art/arcane/iris/platform/bukkit/nms/BukkitBiomePolicy.java +++ b/core/src/main/java/art/arcane/iris/platform/bukkit/nms/BukkitBiomePolicy.java @@ -20,6 +20,7 @@ import art.arcane.iris.generation.terrain.IrisDimension; import art.arcane.iris.generation.terrain.IrisDimensionCarvingResolver; import art.arcane.iris.platform.generation.BukkitChunkGenerator; import art.arcane.iris.generation.context.IrisContext; +import art.arcane.iris.generation.locator.BiomeSearchFilter; import art.arcane.volmlib.util.collection.KMap; import art.arcane.volmlib.util.math.RNG; import org.bukkit.block.Biome; @@ -116,28 +117,7 @@ public final class BukkitBiomePolicy implements NativeBiomeSourcePolicy LinkedHashSet biomes = new LinkedHashSet<>(); boolean fallbackPossible = false; for (OwnedBiome ownedBiome : getOwnedBiomes(engine)) { - IrisBiome irisBiome = ownedBiome.biome(); - if (irisBiome.isCustom()) { - for (IrisBiomeCustom customBiome : irisBiome.getCustomDerivitives()) { - H customHolder = resolveCustomBiomeHolder( - customRegistry, engine, ownedBiome.dimension(), customBiome.getId()); - if (customHolder == null) { - throw new IllegalStateException("Iris custom visible biome '" - + customBiomeKey(ownedBiome.dimension(), customBiome.getId()) - + "' is not registered"); - } - biomes.add(customHolder); - } - continue; - } - fallbackPossible |= !addVisibleBiomeHolder( - biomes, registry, irisBiome.getDerivativeKey()); - for (String scatter : irisBiome.getBiomeScatter()) { - fallbackPossible |= !addVisibleBiomeHolder(biomes, registry, scatter); - } - for (String scatter : irisBiome.getBiomeSkyScatter()) { - fallbackPossible |= !addVisibleBiomeHolder(biomes, registry, scatter); - } + fallbackPossible |= addVisibleBiomeHolders(biomes, customRegistry, registry, engine, ownedBiome); } if (fallbackPossible && fallback != null) { biomes.add(fallback); @@ -145,6 +125,37 @@ public final class BukkitBiomePolicy implements NativeBiomeSourcePolicy return new ArrayList<>(biomes); } + private static boolean addVisibleBiomeHolders( + Set biomes, + NativeBiomeRegistry customRegistry, + NativeBiomeRegistry registry, + Engine engine, + OwnedBiome ownedBiome + ) { + IrisBiome irisBiome = ownedBiome.biome(); + if (irisBiome.isCustom()) { + for (IrisBiomeCustom customBiome : irisBiome.getCustomDerivitives()) { + H customHolder = resolveCustomBiomeHolder( + customRegistry, engine, ownedBiome.dimension(), customBiome.getId()); + if (customHolder == null) { + throw new IllegalStateException("Iris custom visible biome '" + + customBiomeKey(ownedBiome.dimension(), customBiome.getId()) + + "' is not registered"); + } + biomes.add(customHolder); + } + return false; + } + boolean fallbackPossible = !addVisibleBiomeHolder(biomes, registry, irisBiome.getDerivativeKey()); + for (String scatter : irisBiome.getBiomeScatter()) { + fallbackPossible |= !addVisibleBiomeHolder(biomes, registry, scatter); + } + for (String scatter : irisBiome.getBiomeSkyScatter()) { + fallbackPossible |= !addVisibleBiomeHolder(biomes, registry, scatter); + } + return fallbackPossible; + } + private static boolean addVisibleBiomeHolder( Set biomes, NativeBiomeRegistry registry, @@ -382,10 +393,6 @@ public final class BukkitBiomePolicy implements NativeBiomeSourcePolicy } try (lease; IrisContext.Scope ignored = IrisContext.open(engine, lease.sessionId(), null)) { prepareVisibleBiomeBatch(); - DimensionStackContext stackContext = engine.getDimensionStackContext(); - if (stackContext == null) { - return query.fallback().get(); - } Set candidates = new HashSet<>(); for (H biome : getVisibleBiomes( biomeCustomRegistry, biomeRegistry, engine, fallbackBiome)) { @@ -396,7 +403,16 @@ public final class BukkitBiomePolicy implements NativeBiomeSourcePolicy if (candidates.isEmpty()) { return null; } - return query.search().search(candidates, (blockX, blockZ) -> visibleColumn(stackContext, blockX, blockZ)); + if (platformGenerator != null && platformGenerator.usesFlatStudioTerrain()) { + H flat = getFallbackBiome(); + return query.search().search(candidates, (blockX, blockZ) -> blockY -> flat); + } + DimensionStackContext stackContext = engine.getDimensionStackContext(); + if (stackContext != null) { + return query.search().search(candidates, (blockX, blockZ) -> visibleColumn(stackContext, blockX, blockZ)); + } + BiomeSearchFilter filter = BiomeSearchFilter.of(engine, searchBiomeKeys(candidates)); + return query.search().search(candidates, (blockX, blockZ) -> visibleColumn(filter, blockX, blockZ)); } } @@ -407,6 +423,38 @@ public final class BukkitBiomePolicy implements NativeBiomeSourcePolicy return blockY -> resolveVisibleBiomeHolder(quartX, blockY >> 2, quartZ, null, stackLayout); } + private Column visibleColumn(BiomeSearchFilter filter, int blockX, int blockZ) { + int quartX = blockX >> 2; + int quartZ = blockZ >> 2; + int minHeight = engine.getWorld().minHeight(); + BiomeSearchFilter.Column column = filter.column(quartX << 2, quartZ << 2); + return blockY -> column.admits(((blockY >> 2) << 2) - minHeight) + ? locateVisibleBiomeHolder(quartX, blockY >> 2, quartZ) + : null; + } + + private H locateVisibleBiomeHolder(int quartX, int quartY, int quartZ) { + try (GenerationHistoryRuntimeRouter.CoordinateScope ignored = openHistoryCoordinateScope( + quartX << 2, quartZ << 2, "bukkit_locate_visible_biome")) { + return resolveVisibleBiomeHolder(quartX, quartY, quartZ, null); + } + } + + private Set searchBiomeKeys(Set candidates) { + H fallback = getFallbackBiome(); + Set keys = new HashSet<>(); + for (OwnedBiome ownedBiome : getHostOwnedBiomes(engine)) { + Set holders = new HashSet<>(); + if (addVisibleBiomeHolders(holders, biomeCustomRegistry, biomeRegistry, engine, ownedBiome)) { + holders.add(fallback); + } + if (!Collections.disjoint(holders, candidates)) { + keys.add(ownedBiome.biome().getLoadKey()); + } + } + return keys; + } + public static int horizontalBiomeSearchQuartStep(int blockY, int searchRadius) { return blockY == STRONGHOLD_RING_SEARCH_Y && searchRadius == STRONGHOLD_RING_SEARCH_RADIUS ? STRONGHOLD_RING_SEARCH_QUART_STEP diff --git a/core/src/test/java/art/arcane/iris/generation/biome/IrisBiomeVisibleKeysTest.java b/core/src/test/java/art/arcane/iris/generation/biome/IrisBiomeVisibleKeysTest.java new file mode 100644 index 000000000..f76c729fc --- /dev/null +++ b/core/src/test/java/art/arcane/iris/generation/biome/IrisBiomeVisibleKeysTest.java @@ -0,0 +1,55 @@ +package art.arcane.iris.generation.biome; + +import art.arcane.iris.generation.runtime.Engine; +import art.arcane.iris.generation.terrain.IrisDimension; +import art.arcane.iris.pack.loading.IrisData; +import art.arcane.volmlib.util.collection.KList; +import art.arcane.volmlib.util.math.RNG; +import org.junit.Test; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +public class IrisBiomeVisibleKeysTest { + @Test + public void visibleKeysCoverTheDerivativeOnlyWhenNoGroundScatterReplacesIt() { + Engine engine = engine(); + IrisBiome plain = new IrisBiome().setDerivative("terralith:mirage_isles"); + IrisBiome scattered = new IrisBiome().setDerivative("minecraft:plains") + .setBiomeScatter(new KList<>("minecraft:forest", "birch_forest")) + .setBiomeSkyScatter(new KList<>("example:custom")); + + assertEquals(new KList<>("terralith:mirage_isles"), plain.getVisibleBiomeKeys(engine)); + assertEquals(new KList<>("minecraft:forest", "minecraft:birch_forest", "physical:custom"), + scattered.getVisibleBiomeKeys(engine)); + } + + @Test + public void visibleKeysContainEverySelectedSkyAndGroundKey() { + Engine engine = engine(); + IrisBiome single = new IrisBiome().setDerivative("minecraft:plains") + .setBiomeScatter(new KList<>("minecraft:forest")) + .setBiomeSkyScatter(new KList<>("example:custom")); + RNG rng = new RNG(7L); + + assertTrue(single.getVisibleBiomeKeys(engine).contains(single.getGroundBiomeKey(rng, engine, 4, 8, 12))); + assertTrue(single.getVisibleBiomeKeys(engine).contains(single.getSkyBiomeKey(rng, engine, 4, 8, 12))); + } + + private static Engine engine() { + IrisDimension dimension = new IrisDimension(); + IrisData data = mock(IrisData.class); + when(data.physicalBiomeResourceKey(eq(dimension), anyString())).thenAnswer(invocation -> { + String key = invocation.getArgument(1); + return key.equals("example:custom") ? "physical:custom" : key; + }); + Engine engine = mock(Engine.class); + when(engine.getDimension()).thenReturn(dimension); + when(engine.getData()).thenReturn(data); + return engine; + } +} diff --git a/core/src/test/java/art/arcane/iris/generation/locator/BiomeSearchFilterTest.java b/core/src/test/java/art/arcane/iris/generation/locator/BiomeSearchFilterTest.java new file mode 100644 index 000000000..a596cc151 --- /dev/null +++ b/core/src/test/java/art/arcane/iris/generation/locator/BiomeSearchFilterTest.java @@ -0,0 +1,267 @@ +package art.arcane.iris.generation.locator; + +import art.arcane.iris.generation.biome.IrisBiome; +import art.arcane.iris.generation.hydrology.IrisRiverPolicy; +import art.arcane.iris.generation.hydrology.runtime.IrisHydrologyRuntime; +import art.arcane.iris.generation.image.IrisImageMapApplication; +import art.arcane.iris.generation.image.IrisImageMapRuntime; +import art.arcane.iris.generation.runtime.DimensionStackContext; +import art.arcane.iris.generation.runtime.Engine; +import art.arcane.iris.generation.runtime.IrisComplex; +import art.arcane.iris.generation.subterrain.IrisSubterrainFeature; +import art.arcane.iris.generation.subterrain.SubterrainCell; +import art.arcane.iris.generation.subterrain.SubterrainPlanner; +import art.arcane.iris.generation.subterrain.SubterrainRoom; +import art.arcane.iris.generation.terrain.IrisDimension; +import art.arcane.iris.generation.terrain.IrisDimensionCarvingEntry; +import art.arcane.iris.generation.terrain.IrisRegion; +import art.arcane.iris.pack.loading.IrisData; +import art.arcane.iris.pack.loading.ResourceLoader; +import art.arcane.volmlib.util.collection.KList; +import art.arcane.volmlib.util.stream.ProceduralStream; +import art.arcane.volmlib.util.stream.interpolation.Interpolated; +import org.junit.Before; +import org.junit.Test; + +import java.util.Set; +import java.util.function.BiFunction; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.anyDouble; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +public class BiomeSearchFilterTest { + private static final int SURFACE_Y = 200; + private static final int CAVE_Y = 20; + + private Engine engine; + private IrisDimension dimension; + private IrisComplex complex; + private ResourceLoader biomeLoader; + + @Before + @SuppressWarnings("unchecked") + public void setup() { + engine = mock(Engine.class); + dimension = mock(IrisDimension.class); + complex = mock(IrisComplex.class); + IrisData data = mock(IrisData.class); + biomeLoader = (ResourceLoader) mock(ResourceLoader.class); + when(engine.getDimension()).thenReturn(dimension); + when(engine.getComplex()).thenReturn(complex); + when(engine.getData()).thenReturn(data); + when(engine.getMinHeight()).thenReturn(-64); + when(engine.answersFromNaturalTerrain(anyInt(), anyInt())).thenReturn(true); + when(data.getBiomeLoader()).thenReturn(biomeLoader); + when(dimension.getCarving()).thenReturn(new KList<>()); + when(dimension.getSubterrainFeatures()).thenReturn(new KList<>()); + when(dimension.getReachableBiomes(engine)).thenReturn(new KList<>()); + when(complex.getLandBiomeStream()).thenReturn(constantStream(biome("land_root"))); + when(complex.getSeaBiomeStream()).thenReturn(constantStream(biome("sea_root"))); + when(complex.getShoreBiomeStream()).thenReturn(constantStream(biome("shore_root"))); + when(complex.getCaveBiomeStream()).thenReturn(constantStream(biome("other_cave"))); + } + + @Test + public void surfaceTargetIsAdmittedOnlyInsideItsHostRegionWithoutTerrainHeight() { + IrisRegion host = region("host"); + host.setLandBiomes(keys("land_root")); + IrisBiome root = biome("land_root"); + root.setChildren(keys("target")); + stubBiome(root); + stubBiome(biome("target")); + stubRegions(host); + IrisRegion foreign = region("foreign"); + when(complex.getRegionStream()).thenReturn(positionalStream((x, z) -> x < 0D ? foreign : host, host)); + + BiomeSearchFilter filter = BiomeSearchFilter.of(engine, Set.of("target")); + + assertTrue(filter.column(100, 100).admits(SURFACE_Y)); + assertTrue(filter.column(100, 100).admits(CAVE_Y)); + assertFalse(filter.column(-100, 100).admits(SURFACE_Y)); + assertFalse(filter.column(-100, 100).admits(CAVE_Y)); + verify(complex, never()).naturalSurfaceBiome(anyInt(), anyInt()); + verify(complex, never()).naturalTrueHeight(anyInt(), anyInt()); + verify(complex, never()).getNaturalHeightStream(); + verify(engine, never()).getHostSurfaceBiome(anyInt(), anyInt()); + } + + @Test + public void caveTargetIsAdmittedOnlyBelowTheCaveSwitch() { + IrisRegion host = region("host"); + host.setCaveBiomes(keys("cavey")); + stubBiome(biome("cavey")); + stubRegions(host); + when(complex.getRegionStream()).thenReturn(constantStream(host)); + when(complex.getCaveBiomeStream()).thenReturn(positionalStream( + (x, z) -> x >= 512D ? biome("cavey") : biome("other_cave"), biome("other_cave"))); + + BiomeSearchFilter filter = BiomeSearchFilter.of(engine, Set.of("cavey")); + + assertTrue(filter.column(600, 0).admits(CAVE_Y)); + assertFalse(filter.column(600, 0).admits(SURFACE_Y)); + assertFalse(filter.column(0, 0).admits(CAVE_Y)); + } + + @Test + public void imageMappedSurfaceDecidesTheSurfaceBiome() { + IrisRegion host = region("host"); + host.setLandBiomes(keys("land_root")); + stubBiome(biome("land_root")); + stubRegions(host); + when(complex.getRegionStream()).thenReturn(constantStream(host)); + IrisImageMapRuntime maps = mock(IrisImageMapRuntime.class); + when(maps.has(IrisImageMapApplication.BIOME)).thenReturn(true); + when(maps.sampleBiome(anyDouble(), anyDouble())).thenAnswer(invocation -> + invocation.getArgument(0) >= 64D ? biome("mapped") : null); + when(complex.getImageMapRuntime()).thenReturn(maps); + + BiomeSearchFilter mapped = BiomeSearchFilter.of(engine, Set.of("mapped")); + BiomeSearchFilter natural = BiomeSearchFilter.of(engine, Set.of("land_root")); + + assertTrue(mapped.column(100, 0).admits(SURFACE_Y)); + assertFalse(mapped.column(0, 0).admits(SURFACE_Y)); + assertTrue(natural.column(0, 0).admits(SURFACE_Y)); + assertFalse(natural.column(100, 0).admits(SURFACE_Y)); + } + + @Test + public void plannedHydrologyColumnsAreAdmittedForRiverPolicyBiomes() { + stubRegions(); + when(complex.getRegionStream()).thenReturn(constantStream(region("host"))); + when(complex.getHydrologyRuntime()).thenReturn(mock(IrisHydrologyRuntime.class)); + when(dimension.getRiverPolicy()).thenReturn(new IrisRiverPolicy().setSurfaceBiomes(keys("river_root"))); + IrisBiome river = biome("river_root"); + river.setChildren(keys("target")); + stubBiome(river); + when(engine.answersFromNaturalTerrain(32, 32)).thenReturn(false); + + BiomeSearchFilter filter = BiomeSearchFilter.of(engine, Set.of("target")); + + assertTrue(filter.column(32, 32).admits(SURFACE_Y)); + assertFalse(filter.column(0, 0).admits(SURFACE_Y)); + } + + @Test + public void hydrologyIsNotConsultedForBiomesNoRiverPolicyPlaces() { + stubRegions(); + when(complex.getRegionStream()).thenReturn(constantStream(region("host"))); + when(complex.getHydrologyRuntime()).thenReturn(mock(IrisHydrologyRuntime.class)); + when(dimension.getRiverPolicy()).thenReturn(new IrisRiverPolicy().setSurfaceBiomes(keys("river_root"))); + stubBiome(biome("river_root")); + when(engine.answersFromNaturalTerrain(anyInt(), anyInt())).thenReturn(false); + + BiomeSearchFilter filter = BiomeSearchFilter.of(engine, Set.of("target")); + + assertFalse(filter.column(32, 32).admits(SURFACE_Y)); + verify(engine, never()).answersFromNaturalTerrain(anyInt(), anyInt()); + } + + @Test + public void columnsWithoutHydrologyStayFilteredWhenNotAnsweringFromNaturalTerrain() { + stubRegions(); + when(complex.getRegionStream()).thenReturn(constantStream(region("host"))); + when(engine.answersFromNaturalTerrain(anyInt(), anyInt())).thenReturn(false); + + BiomeSearchFilter filter = BiomeSearchFilter.of(engine, Set.of("target")); + + assertFalse(filter.column(32, 32).admits(SURFACE_Y)); + } + + @Test + public void focusedAndStackedEnginesAreNeverFiltered() { + stubRegions(); + when(complex.getRegionStream()).thenReturn(constantStream(region("host"))); + when(complex.getFocusBiome()).thenReturn(biome("focus")); + + assertTrue(BiomeSearchFilter.of(engine, Set.of("target")).column(0, 0).admits(SURFACE_Y)); + + when(complex.getFocusBiome()).thenReturn(null); + when(engine.getDimensionStackContext()).thenReturn(mock(DimensionStackContext.class)); + + assertTrue(BiomeSearchFilter.of(engine, Set.of("target")).column(0, 0).admits(SURFACE_Y)); + } + + @Test + public void subterrainFeatureTargetIsAdmittedWhereTheFeatureOwnsTheSample() { + stubRegions(); + when(complex.getRegionStream()).thenReturn(constantStream(region("host"))); + when(dimension.getSubterrainFeatures()).thenReturn(new KList<>( + new IrisSubterrainFeature().setId("room").setBiome("hall"))); + SubterrainPlanner planner = mock(SubterrainPlanner.class); + when(planner.sample(anyInt(), anyInt(), anyInt())).thenReturn(SubterrainCell.OUTSIDE); + when(planner.sample(0, 56, 0)).thenReturn(new SubterrainCell(SubterrainCell.Kind.AIR, "", + new SubterrainRoom("room", "hall", null, 0, 56, 0, 0, 56, 0, 50, 60, 0D, 0, + false, false, SubterrainCell.Kind.AIR))); + when(complex.getSubterrainPlanner()).thenReturn(planner); + + BiomeSearchFilter filter = BiomeSearchFilter.of(engine, Set.of("hall")); + + assertTrue(filter.column(0, 0).admits(120)); + assertFalse(filter.column(0, 0).admits(SURFACE_Y)); + verify(engine, never()).getSubterrainBiome(anyInt(), anyInt(), anyInt()); + } + + @Test + public void carvingTargetIsAdmittedWhereTheCarvingResolvesIt() { + stubRegions(); + when(complex.getRegionStream()).thenReturn(constantStream(region("host"))); + IrisDimensionCarvingEntry entry = new IrisDimensionCarvingEntry(); + entry.setId("deep"); + entry.setBiome("carved"); + stubBiome(biome("carved")); + when(dimension.getCarving()).thenReturn(new KList<>(entry)); + + BiomeSearchFilter filter = BiomeSearchFilter.of(engine, Set.of("carved")); + BiomeSearchFilter unrelated = BiomeSearchFilter.of(engine, Set.of("elsewhere")); + + assertTrue(filter.column(0, 0).admits(CAVE_Y)); + assertFalse(filter.column(0, 0).admits(SURFACE_Y)); + assertFalse(unrelated.column(0, 0).admits(CAVE_Y)); + } + + private static IrisBiome biome(String key) { + IrisBiome biome = new IrisBiome(); + biome.setLoadKey(key); + return biome; + } + + private static IrisRegion region(String key) { + IrisRegion region = new IrisRegion(); + region.setLoadKey(key); + return region; + } + + private static KList keys(String... values) { + KList list = new KList<>(); + for (String value : values) { + list.add(value); + } + return list; + } + + private static ProceduralStream constantStream(T value) { + return ProceduralStream.of((x, z) -> value, Interpolated.of(a -> 0D, a -> value)); + } + + private static ProceduralStream positionalStream(BiFunction function, T fallback) { + return ProceduralStream.of(function::apply, Interpolated.of(a -> 0D, a -> fallback)); + } + + private void stubRegions(IrisRegion... regions) { + KList list = new KList<>(); + for (IrisRegion region : regions) { + list.add(region); + } + when(dimension.getAllRegions(engine)).thenReturn(list); + } + + private void stubBiome(IrisBiome biome) { + when(biomeLoader.load(biome.getLoadKey())).thenReturn(biome); + } +} diff --git a/core/src/test/java/art/arcane/iris/platform/bukkit/nms/BukkitBiomeLocateTest.java b/core/src/test/java/art/arcane/iris/platform/bukkit/nms/BukkitBiomeLocateTest.java new file mode 100644 index 000000000..e3f40de5c --- /dev/null +++ b/core/src/test/java/art/arcane/iris/platform/bukkit/nms/BukkitBiomeLocateTest.java @@ -0,0 +1,130 @@ +package art.arcane.iris.platform.bukkit.nms; + +import art.arcane.iris.generation.biome.IrisBiome; +import art.arcane.iris.generation.biome.IrisBiomeCustom; +import art.arcane.iris.generation.runtime.Engine; +import art.arcane.iris.generation.runtime.IrisEngine; +import art.arcane.iris.generation.runtime.GenerationSessionLease; +import art.arcane.iris.generation.runtime.IrisComplex; +import art.arcane.iris.generation.terrain.IrisDimension; +import art.arcane.iris.generation.terrain.IrisRegion; +import art.arcane.iris.pack.loading.IrisData; +import art.arcane.iris.pack.loading.ResourceLoader; +import art.arcane.iris.world.IrisWorld; +import art.arcane.iris.world.history.GenerationHistoryRuntimeRouter; +import art.arcane.volmlib.nativelib.terrain.NativeBiomeRegistry; +import art.arcane.volmlib.nativelib.terrain.NativeBiomeSourcePolicy; +import art.arcane.volmlib.util.collection.KList; +import art.arcane.volmlib.util.stream.ProceduralStream; +import art.arcane.volmlib.util.stream.interpolation.Interpolated; +import org.junit.Test; + +import java.lang.reflect.Field; +import java.util.Optional; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicInteger; + +import static org.junit.Assert.assertEquals; +import static org.mockito.Answers.CALLS_REAL_METHODS; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +public class BukkitBiomeLocateTest { + @Test + public void locateResolvesVisibleBiomesOnlyInColumnsThatCanHoldTheTarget() throws Exception { + IrisBiome isles = new IrisBiome().setCustomDerivitives(new KList<>(new IrisBiomeCustom().setId("mirage"))); + isles.setLoadKey("isles"); + IrisBiome meadow = new IrisBiome().setDerivative("minecraft:plains"); + meadow.setLoadKey("meadow"); + IrisRegion main = new IrisRegion().setLandBiomes(new KList<>("meadow")); + main.setLoadKey("main"); + IrisRegion archipelago = new IrisRegion().setLandBiomes(new KList<>("isles")); + archipelago.setLoadKey("archipelago"); + IrisDimension dimension = mock(IrisDimension.class); + when(dimension.getReachableBiomes(any())).thenReturn(new KList<>(meadow, isles)); + when(dimension.getAllRegions(any())).thenReturn(new KList<>(main, archipelago)); + when(dimension.getCarving()).thenReturn(new KList<>()); + when(dimension.getSubterrainFeatures()).thenReturn(new KList<>()); + when(dimension.getCustomBiomeKey("mirage")).thenReturn("pack:mirage"); + @SuppressWarnings("unchecked") + ResourceLoader biomes = (ResourceLoader) mock(ResourceLoader.class); + when(biomes.load("meadow")).thenReturn(meadow); + when(biomes.load("isles")).thenReturn(isles); + IrisData data = mock(IrisData.class); + when(data.getBiomeLoader()).thenReturn(biomes); + when(data.customBiomeResourceKey(any(IrisDimension.class), anyString())) + .thenAnswer(invocation -> "pack:" + invocation.getArgument(1)); + IrisComplex complex = mock(IrisComplex.class); + when(complex.getRegionStream()).thenReturn(ProceduralStream.of( + (x, z) -> x >= 4096D ? archipelago : main, Interpolated.of(value -> 0D, value -> main))); + when(complex.getLandBiomeStream()).thenReturn(ProceduralStream.of( + (x, z) -> x >= 4096D ? isles : meadow, Interpolated.of(value -> 0D, value -> meadow))); + when(complex.getSeaBiomeStream()).thenReturn(ProceduralStream.of( + (x, z) -> meadow, Interpolated.of(value -> 0D, value -> meadow))); + when(complex.getShoreBiomeStream()).thenReturn(ProceduralStream.of( + (x, z) -> meadow, Interpolated.of(value -> 0D, value -> meadow))); + when(complex.historicalPhysicalBiomeKeyAt(anyInt(), anyInt(), anyInt())).thenReturn(Optional.empty()); + GenerationHistoryRuntimeRouter.CoordinateScope scope = mock(GenerationHistoryRuntimeRouter.CoordinateScope.class); + IrisEngine engine = mock(IrisEngine.class); + when(engine.openGenerationHistoryCoordinateScope(anyInt(), anyInt())).thenReturn(scope); + when(engine.getDimension()).thenReturn(dimension); + when(engine.getData()).thenReturn(data); + when(engine.getComplex()).thenReturn(complex); + when(engine.getWorld()).thenReturn(IrisWorld.builder().minHeight(-64).maxHeight(320).build()); + when(engine.getMinHeight()).thenReturn(-64); + when(engine.getCacheID()).thenReturn(1); + when(engine.acquireGenerationLease(anyString())).thenReturn(GenerationSessionLease.noop()); + when(engine.answersFromNaturalTerrain(anyInt(), anyInt())).thenReturn(true); + AtomicInteger heightSamples = new AtomicInteger(); + when(engine.getHostSurfaceBiome(anyInt(), anyInt())).thenAnswer(invocation -> { + heightSamples.incrementAndGet(); + return invocation.getArgument(0) >= 4096 ? isles : meadow; + }); + @SuppressWarnings("unchecked") + NativeBiomeRegistry registry = mock(NativeBiomeRegistry.class); + when(registry.lookup("minecraft:plains")).thenReturn("minecraft:plains"); + when(registry.lookupCustom("pack:mirage")).thenReturn("pack:mirage"); + BukkitBiomePolicy policy = policy(engine, registry, "minecraft:plains"); + int[] columns = {0, 1024, 2048, 3072, 4096, 5120}; + + NativeBiomeSourcePolicy.BiomeLocation found = policy.findClosestBiome3d( + new NativeBiomeSourcePolicy.ClosestQuery<>("pack:mirage"::equals, (candidates, resolver) -> { + for (int x : columns) { + String biome = resolver.resolve(x, 0).sample(100); + if (candidates.contains(biome)) { + return new NativeBiomeSourcePolicy.BiomeLocation<>(x, 100, 0, biome); + } + } + return null; + })); + + assertEquals(new NativeBiomeSourcePolicy.BiomeLocation<>(4096, 100, 0, "pack:mirage"), found); + assertEquals(1, heightSamples.get()); + verify(engine).openGenerationHistoryCoordinateScope(4096, 0); + verify(engine, times(1)).openGenerationHistoryCoordinateScope(anyInt(), anyInt()); + verify(scope).close(); + } + + @SuppressWarnings("unchecked") + private static BukkitBiomePolicy policy(Engine engine, NativeBiomeRegistry registry, + String fallback) throws Exception { + BukkitBiomePolicy policy = mock(BukkitBiomePolicy.class, CALLS_REAL_METHODS); + set(policy, "engine", engine); + set(policy, "biomeCustomRegistry", registry); + set(policy, "biomeRegistry", registry); + set(policy, "fallbackBiome", fallback); + set(policy, "runtimeBiomeStates", new ConcurrentHashMap<>()); + return policy; + } + + private static void set(Object target, String name, Object value) throws Exception { + Field field = BukkitBiomePolicy.class.getDeclaredField(name); + field.setAccessible(true); + field.set(target, value); + } +}