mirror of
https://github.com/VolmitSoftware/Iris.git
synced 2026-10-20 23:36:52 +00:00
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
This commit is contained in:
1 parent
e1049949c6
commit
257c7d4f87
10 files changed
+960
-85
No files matched your search
@@ -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<H, S> implements NativeModdedBiomePolicy<H, S> {
|
||||
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<H> candidates = new HashSet<>();
|
||||
for (H biome : collectVisibleBiomeHolders()) {
|
||||
if (query.allowed().test(biome)) {
|
||||
@@ -313,16 +310,86 @@ final class ModdedBiomePolicy<H, S> implements NativeModdedBiomePolicy<H, S> {
|
||||
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<String> searchBiomeKeys(Engine engine, Set<H> candidates) {
|
||||
NativeBiomeSourceAccess.RegistryView<H> 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<String> 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<H> registry,
|
||||
Set<H> 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 <H> boolean emits(NativeBiomeSourceAccess.RegistryView<H> registry, String key,
|
||||
Set<H> 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)) {
|
||||
|
||||
+98
@@ -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<IrisBiome> biomes = (ResourceLoader<IrisBiome>) 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.<Integer>getArgument(0) >= 4096 ? isles : meadow;
|
||||
});
|
||||
ModdedBiomePolicy<BiomeToken, Object> 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<BiomeToken> 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<BiomeToken> 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<BiomeToken, Object> boundTo(Engine engine) {
|
||||
ModdedBiomePolicy<BiomeToken, Object> policy = new ModdedBiomePolicy<>(new Access(new ArrayList<>()));
|
||||
policy.bind(new ModdedBiomePolicy.RuntimeCallbacks(() -> engine, () -> engine, bound -> true,
|
||||
|
||||
@@ -669,6 +669,10 @@ public class IrisBiome extends IrisRegistrant implements Rarity {
|
||||
return IrisBiomeDerivatives.getGroundBiomeKey(this, rng, engine, x, y, z);
|
||||
}
|
||||
|
||||
public KList<String> getVisibleBiomeKeys(Engine engine) {
|
||||
return IrisBiomeDerivatives.getVisibleBiomeKeys(this, engine);
|
||||
}
|
||||
|
||||
public Color getColor(Engine engine, RenderType type) {
|
||||
return IrisBiomeColorRenderer.getColor(this, engine, type);
|
||||
}
|
||||
|
||||
@@ -144,6 +144,20 @@ final class IrisBiomeDerivatives {
|
||||
return physicalBiomeKey(engine, logicalKey);
|
||||
}
|
||||
|
||||
static KList<String> getVisibleBiomeKeys(IrisBiome biome, Engine engine) {
|
||||
KList<String> 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<IrisBiomeCustom> customDerivitives = biome.getCustomDerivitives();
|
||||
|
||||
|
||||
@@ -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<String> 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<String> 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<String> 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);
|
||||
}
|
||||
}
|
||||
@@ -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<T> implements Locator<T> {
|
||||
}
|
||||
|
||||
IrisComplex complex = engine.getComplex();
|
||||
BiomeSource source = new BiomeSource(engine);
|
||||
KSet<String> landHosts = new KSet<>();
|
||||
KSet<String> seaHosts = new KSet<>();
|
||||
KSet<String> shoreHosts = new KSet<>();
|
||||
KSet<String> surfaceRegions = new KSet<>();
|
||||
KSet<String> 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<String> 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<T> implements Locator<T> {
|
||||
return SearchPlan.of(coarse, 1, null);
|
||||
}
|
||||
|
||||
static boolean closureContains(Engine engine, String rootKey, Set<String> biomeKeys) {
|
||||
return new BiomeSource(engine).closureContains(rootKey, biomeKeys);
|
||||
}
|
||||
|
||||
static CoarseSample surfaceCoarse(Engine engine, Set<String> biomeKeys) {
|
||||
BiomeSource source = new BiomeSource(engine);
|
||||
KSet<String> landHosts = new KSet<>();
|
||||
KSet<String> seaHosts = new KSet<>();
|
||||
KSet<String> shoreHosts = new KSet<>();
|
||||
KSet<String> 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<String> regions, KSet<String> landHosts, KSet<String> seaHosts, KSet<String> shoreHosts) {
|
||||
return (x, z) -> {
|
||||
IrisRegion region = complex.getRegionStream().get(x, z);
|
||||
@@ -654,8 +663,8 @@ public final class HintedLocator<T> implements Locator<T> {
|
||||
return loaded;
|
||||
}
|
||||
|
||||
private boolean closureContains(String rootKey, String targetKey) {
|
||||
if (rootKey == null || targetKey == null) {
|
||||
private boolean closureContains(String rootKey, Set<String> targetKeys) {
|
||||
if (rootKey == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -670,7 +679,7 @@ public final class HintedLocator<T> implements Locator<T> {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (key.equals(targetKey)) {
|
||||
if (targetKeys.contains(key)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -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<H, V> implements NativeBiomeSourcePolicy<H>
|
||||
LinkedHashSet<H> 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<H, V> implements NativeBiomeSourcePolicy<H>
|
||||
return new ArrayList<>(biomes);
|
||||
}
|
||||
|
||||
private static <H, V> boolean addVisibleBiomeHolders(
|
||||
Set<H> biomes,
|
||||
NativeBiomeRegistry<H, V> customRegistry,
|
||||
NativeBiomeRegistry<H, V> 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 <H, V> boolean addVisibleBiomeHolder(
|
||||
Set<H> biomes,
|
||||
NativeBiomeRegistry<H, V> registry,
|
||||
@@ -382,10 +393,6 @@ public final class BukkitBiomePolicy<H, V> implements NativeBiomeSourcePolicy<H>
|
||||
}
|
||||
try (lease; IrisContext.Scope ignored = IrisContext.open(engine, lease.sessionId(), null)) {
|
||||
prepareVisibleBiomeBatch();
|
||||
DimensionStackContext stackContext = engine.getDimensionStackContext();
|
||||
if (stackContext == null) {
|
||||
return query.fallback().get();
|
||||
}
|
||||
Set<H> candidates = new HashSet<>();
|
||||
for (H biome : getVisibleBiomes(
|
||||
biomeCustomRegistry, biomeRegistry, engine, fallbackBiome)) {
|
||||
@@ -396,7 +403,16 @@ public final class BukkitBiomePolicy<H, V> implements NativeBiomeSourcePolicy<H>
|
||||
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<H, V> implements NativeBiomeSourcePolicy<H>
|
||||
return blockY -> resolveVisibleBiomeHolder(quartX, blockY >> 2, quartZ, null, stackLayout);
|
||||
}
|
||||
|
||||
private Column<H> 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<String> searchBiomeKeys(Set<H> candidates) {
|
||||
H fallback = getFallbackBiome();
|
||||
Set<String> keys = new HashSet<>();
|
||||
for (OwnedBiome ownedBiome : getHostOwnedBiomes(engine)) {
|
||||
Set<H> 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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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<IrisBiome> 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<IrisBiome>) 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.<Double>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<String> keys(String... values) {
|
||||
KList<String> list = new KList<>();
|
||||
for (String value : values) {
|
||||
list.add(value);
|
||||
}
|
||||
return list;
|
||||
}
|
||||
|
||||
private static <T> ProceduralStream<T> constantStream(T value) {
|
||||
return ProceduralStream.of((x, z) -> value, Interpolated.of(a -> 0D, a -> value));
|
||||
}
|
||||
|
||||
private static <T> ProceduralStream<T> positionalStream(BiFunction<Double, Double, T> function, T fallback) {
|
||||
return ProceduralStream.of(function::apply, Interpolated.of(a -> 0D, a -> fallback));
|
||||
}
|
||||
|
||||
private void stubRegions(IrisRegion... regions) {
|
||||
KList<IrisRegion> 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);
|
||||
}
|
||||
}
|
||||
@@ -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<IrisBiome> biomes = (ResourceLoader<IrisBiome>) 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.<Integer>getArgument(0) >= 4096 ? isles : meadow;
|
||||
});
|
||||
@SuppressWarnings("unchecked")
|
||||
NativeBiomeRegistry<String, String> registry = mock(NativeBiomeRegistry.class);
|
||||
when(registry.lookup("minecraft:plains")).thenReturn("minecraft:plains");
|
||||
when(registry.lookupCustom("pack:mirage")).thenReturn("pack:mirage");
|
||||
BukkitBiomePolicy<String, String> policy = policy(engine, registry, "minecraft:plains");
|
||||
int[] columns = {0, 1024, 2048, 3072, 4096, 5120};
|
||||
|
||||
NativeBiomeSourcePolicy.BiomeLocation<String> 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<String, String> policy(Engine engine, NativeBiomeRegistry<String, String> registry,
|
||||
String fallback) throws Exception {
|
||||
BukkitBiomePolicy<String, String> 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);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user