This commit is contained in:
Brian Neumann-Fopiano committed 2026-10-04 15:17:12 -04:00
1 parent a6149b27c9
commit 65ae3f1b45
18 files changed
+1664 -86

No files matched your search

@@ -74,7 +74,7 @@ import java.util.Set;
@Director(name = "find", origin = DirectorOrigin.PLAYER, description = "Iris Find commands", descriptionKey = "iris.director.commandfind.director.iris_find_commands", aliases = "goto") @Director(name = "find", origin = DirectorOrigin.PLAYER, description = "Iris Find commands", descriptionKey = "iris.director.commandfind.director.iris_find_commands", aliases = "goto")
public class CommandFind implements DirectorExecutor { public class CommandFind implements DirectorExecutor {
@Director(description = "Find a biome", descriptionKey = "iris.director.commandfind.director.find_biome") @Director(description = "Find a biome", sync = true, descriptionKey = "iris.director.commandfind.director.find_biome")
public void biome( public void biome(
@Param(description = "The biome to look for", descriptionKey = "iris.director.commandfind.param.biome_look", customHandler = ReachableBiomeHandler.class) @Param(description = "The biome to look for", descriptionKey = "iris.director.commandfind.param.biome_look", customHandler = ReachableBiomeHandler.class)
IrisBiome biome, IrisBiome biome,
@@ -31,6 +31,9 @@ import art.arcane.iris.generation.runtime.GenerationSessionException;
import art.arcane.iris.generation.runtime.GenerationSessionLease; import art.arcane.iris.generation.runtime.GenerationSessionLease;
import art.arcane.iris.structure.placement.IrisStructureLocator; import art.arcane.iris.structure.placement.IrisStructureLocator;
import art.arcane.iris.generation.locator.Locator; import art.arcane.iris.generation.locator.Locator;
import art.arcane.iris.generation.locator.BiomeLocator;
import art.arcane.iris.generation.subterrain.SubterrainPosition;
import art.arcane.iris.world.history.SavedBiomeRuntime;
import art.arcane.iris.structure.nativegen.NativeStructureGenerationPolicy; import art.arcane.iris.structure.nativegen.NativeStructureGenerationPolicy;
import art.arcane.iris.structure.placement.StructureReachability; import art.arcane.iris.structure.placement.StructureReachability;
import art.arcane.iris.generation.runtime.WrongEngineBroException; import art.arcane.iris.generation.runtime.WrongEngineBroException;
@@ -82,7 +85,7 @@ final class ModdedLocateCommands {
IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_UNKNOWN_BIOME_2, MessageArgument.untrusted("key", key))); IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_UNKNOWN_BIOME_2, MessageArgument.untrusted("key", key)));
return 0; return 0;
} }
locate(source, level, engine, player, Locator.surfaceBiome(biome.getLoadKey()), "biome " + biome.getLoadKey()); locate(source, level, engine, player, BiomeLocator.forBiome(engine, biome.getLoadKey(), player.blockY()), "biome " + biome.getLoadKey());
return 1; return 1;
} }
@@ -545,12 +548,12 @@ final class ModdedLocateCommands {
previous.cancel(true); previous.cancel(true);
} }
search.whenComplete((Position2 at, Throwable error) -> completeLocate( search.whenComplete((Position2 at, Throwable error) -> completeLocate(
source, level, engine, player, label, server, playerId, search, at, error)); source, level, engine, player, label, server, playerId, search, locator, at, error));
} }
private static void completeLocate(NativeCommandSource source, NativeWorld level, Engine engine, private static void completeLocate(NativeCommandSource source, NativeWorld level, Engine engine,
NativeProtocolPlayer player, String label, NativeModdedServer server, UUID playerId, NativeProtocolPlayer player, String label, NativeModdedServer server, UUID playerId,
CompletableFuture<Position2> search, Position2 at, Throwable error) { CompletableFuture<Position2> search, Locator<?> locator, Position2 at, Throwable error) {
if (ACTIVE_LOCATE_REQUESTS.get(playerId) != search) { if (ACTIVE_LOCATE_REQUESTS.get(playerId) != search) {
return; return;
} }
@@ -576,15 +579,77 @@ final class ModdedLocateCommands {
}); });
return; return;
} }
if (locator instanceof BiomeLocator caveLocator) {
prepareCaveLocate(new CaveLocateRequest(source, level, engine, player, label, server, playerId,
search, caveLocator, at));
return;
}
server.execute(() -> { server.execute(() -> {
if (ACTIVE_LOCATE_REQUESTS.remove(playerId, search)) { if (ACTIVE_LOCATE_REQUESTS.remove(playerId, search)) {
teleportToLocateResult(source, level, engine, player, label, at); teleportToLocateResult(source, level, engine, player, label, locator, at, Optional.empty());
} }
}); });
} }
private static void teleportToLocateResult(NativeCommandSource source, NativeWorld level, Engine engine, private static void prepareCaveLocate(CaveLocateRequest request) {
NativeProtocolPlayer player, String label, Position2 at) { CompletableFuture.supplyAsync(() -> resolveCavePosition(request))
.whenComplete((target, error) -> request.server().execute(() -> loadCaveDestination(request, target, error)));
}
private static SubterrainPosition resolveCavePosition(CaveLocateRequest request) {
try (GenerationSessionLease lease = request.engine().acquireGenerationLease("modded_cave_position");
IrisContext.Scope ignored = IrisContext.open(request.engine(), lease.sessionId(), null)) {
return request.locator().position(request.engine(), request.at());
} catch (GenerationSessionException error) {
throw new CompletionException(error);
}
}
private static void loadCaveDestination(CaveLocateRequest request, SubterrainPosition target, Throwable error) {
if (ACTIVE_LOCATE_REQUESTS.get(request.playerId()) != request.search()) {
return;
}
if (error != null) {
failCaveLocate(request, error);
return;
}
if (target == null || !request.player().connected() || !request.player().inWorld(request.level())) {
if (ACTIVE_LOCATE_REQUESTS.remove(request.playerId(), request.search())) {
IrisModdedCommands.fail(request.source(), "The cave destination is no longer available.");
}
return;
}
try {
request.level().getBlock(target.x(), target.y(), target.z());
request.locator().prepareRead(request.engine(), new Position2(target.x() >> 4, target.z() >> 4))
.whenComplete((read, failure) -> request.server().execute(() -> finishCaveLocate(request, target, read, failure)));
} catch (RuntimeException failure) {
failCaveLocate(request, failure);
}
}
private static void finishCaveLocate(CaveLocateRequest request, SubterrainPosition target,
Optional<SavedBiomeRuntime.ReadSession> read, Throwable error) {
if (error != null) {
failCaveLocate(request, error);
return;
}
if (ACTIVE_LOCATE_REQUESTS.remove(request.playerId(), request.search())) {
teleportToLocateResult(request.source(), request.level(), request.engine(), request.player(),
request.label(), request.locator(), request.at(), Optional.of(new CaveDestination(target, read)));
}
}
private static void failCaveLocate(CaveLocateRequest request, Throwable error) {
ModdedIrisLog.error("Iris cave locate failed for {}", request.label(), error);
if (ACTIVE_LOCATE_REQUESTS.remove(request.playerId(), request.search())) {
IrisModdedCommands.fail(request.source(), "Could not prepare the cave destination. See the server log for details.");
}
}
static void teleportToLocateResult(NativeCommandSource source, NativeWorld level, Engine engine,
NativeProtocolPlayer player, String label, Locator<?> locator, Position2 at,
Optional<CaveDestination> caveDestination) {
// Same liveness guards the structure completion path has: the search can take up to // Same liveness guards the structure completion path has: the search can take up to
// two minutes, and the captured NativeProtocolPlayer may be gone or elsewhere by then. // two minutes, and the captured NativeProtocolPlayer may be gone or elsewhere by then.
if (!player.connected()) { if (!player.connected()) {
@@ -600,6 +665,17 @@ final class ModdedLocateCommands {
try (GenerationSessionLease lease = engine.acquireGenerationLease("modded_locator_teleport"); try (GenerationSessionLease lease = engine.acquireGenerationLease("modded_locator_teleport");
IrisContext.Scope ignored = IrisContext.open(engine, lease.sessionId(), null)) { IrisContext.Scope ignored = IrisContext.open(engine, lease.sessionId(), null)) {
int blockY = engine.getMinHeight() + engine.getHeight(blockX, blockZ, false) + 2; int blockY = engine.getMinHeight() + engine.getHeight(blockX, blockZ, false) + 2;
if (locator instanceof BiomeLocator caveLocator) {
CaveDestination prepared = caveDestination.orElseThrow();
SubterrainPosition landing = caveLocator.landing(engine, level, prepared.target(), prepared.read());
if (landing == null) {
IrisModdedCommands.fail(source, "No safe cave landing found for " + label + ".");
return;
}
blockX = landing.x();
blockY = landing.y();
blockZ = landing.z();
}
boolean teleported = player.teleport(level, blockX + 0.5D, blockY, blockZ + 0.5D); boolean teleported = player.teleport(level, blockX + 0.5D, blockY, blockZ + 0.5D);
if (!teleported) { if (!teleported) {
IrisModdedCommands.fail(source, IrisLanguage.plain( IrisModdedCommands.fail(source, IrisLanguage.plain(
@@ -611,7 +687,8 @@ final class ModdedLocateCommands {
return; return;
} }
IrisModdedCommands.ok(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_TELEPORTED_AT_2, MessageArgument.untrusted("label", label), MessageArgument.untrusted("blockX", blockX), MessageArgument.untrusted("blockY", blockY), MessageArgument.untrusted("blockZ", blockZ))); IrisModdedCommands.ok(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_TELEPORTED_AT_2, MessageArgument.untrusted("label", label), MessageArgument.untrusted("blockX", blockX), MessageArgument.untrusted("blockY", blockY), MessageArgument.untrusted("blockZ", blockZ)));
} catch (GenerationSessionException e) { } catch (GenerationSessionException | RuntimeException e) {
ModdedIrisLog.error("Iris locate teleport failed for {}", label, e);
IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_ENGINE_CHANGED_WHILE_LOCATING_TRY_AGAIN, MessageArgument.untrusted("label", label))); IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_ENGINE_CHANGED_WHILE_LOCATING_TRY_AGAIN, MessageArgument.untrusted("label", label)));
} }
} }
@@ -635,6 +712,14 @@ final class ModdedLocateCommands {
NO_PLACEMENT NO_PLACEMENT
} }
record CaveDestination(SubterrainPosition target, Optional<SavedBiomeRuntime.ReadSession> read) {
}
private record CaveLocateRequest(NativeCommandSource source, NativeWorld level, Engine engine,
NativeProtocolPlayer player, String label, NativeModdedServer server,
UUID playerId, CompletableFuture<Position2> search, BiomeLocator locator, Position2 at) {
}
private record NativeStructureTarget(String key, NativeStructureQueries.Reference holder, private record NativeStructureTarget(String key, NativeStructureQueries.Reference holder,
NativeStructureAvailability availability) { NativeStructureAvailability availability) {
} }
@@ -0,0 +1,93 @@
package art.arcane.iris.modded.command;
import art.arcane.iris.generation.context.IrisContext;
import art.arcane.iris.generation.locator.BiomeLocator;
import art.arcane.iris.generation.runtime.Engine;
import art.arcane.iris.generation.runtime.GenerationSessionLease;
import art.arcane.iris.generation.subterrain.SubterrainPosition;
import art.arcane.volmlib.nativelib.minecraft26_2.modded.NativeCommandSource;
import art.arcane.volmlib.nativelib.minecraft26_2.modded.NativeProtocolPlayer;
import art.arcane.volmlib.nativelib.terrain.NativeWorld;
import art.arcane.volmlib.util.math.Position2;
import org.junit.Test;
import org.mockito.MockedStatic;
import java.util.Optional;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyDouble;
import static org.mockito.ArgumentMatchers.anyLong;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.mockStatic;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoInteractions;
import static org.mockito.Mockito.when;
public class ModdedCaveLocateTest {
@Test
public void caveTeleportUsesValidatedUndergroundPosition() throws Exception {
Fixture fixture = new Fixture();
when(fixture.player.teleport(fixture.world, 8.5, -42, 8.5)).thenReturn(true);
try (MockedStatic<IrisModdedCommands> commands = mockStatic(IrisModdedCommands.class);
MockedStatic<IrisContext> context = mockStatic(IrisContext.class)) {
fixture.context(context);
fixture.teleport();
verify(fixture.player).teleport(fixture.world, 8.5, -42, 8.5);
commands.verify(() -> IrisModdedCommands.ok(eq(fixture.source), anyString()));
commands.verify(() -> IrisModdedCommands.fail(eq(fixture.source), anyString()), never());
}
}
@Test
public void failedTeleportNeverReportsSuccess() throws Exception {
Fixture fixture = new Fixture();
try (MockedStatic<IrisModdedCommands> commands = mockStatic(IrisModdedCommands.class);
MockedStatic<IrisContext> context = mockStatic(IrisContext.class)) {
fixture.context(context);
fixture.teleport();
commands.verify(() -> IrisModdedCommands.fail(eq(fixture.source), anyString()));
commands.verify(() -> IrisModdedCommands.ok(eq(fixture.source), anyString()), never());
}
}
@Test
public void worldChangeCancelsBeforeCaveLookupOrTeleport() throws Exception {
Fixture fixture = new Fixture();
when(fixture.player.inWorld(fixture.world)).thenReturn(false);
try (MockedStatic<IrisModdedCommands> commands = mockStatic(IrisModdedCommands.class)) {
fixture.teleport();
verifyNoInteractions(fixture.locator);
verify(fixture.player, never()).teleport(any(), anyDouble(), anyDouble(), anyDouble());
commands.verify(() -> IrisModdedCommands.fail(eq(fixture.source), anyString()));
}
}
private static final class Fixture {
private final NativeCommandSource source = mock(NativeCommandSource.class);
private final NativeWorld world = mock(NativeWorld.class);
private final NativeProtocolPlayer player = mock(NativeProtocolPlayer.class);
private final Engine engine = mock(Engine.class);
private final BiomeLocator locator = mock(BiomeLocator.class);
private Fixture() throws Exception {
when(player.connected()).thenReturn(true);
when(player.inWorld(world)).thenReturn(true);
when(engine.acquireGenerationLease("modded_locator_teleport")).thenReturn(mock(GenerationSessionLease.class));
SubterrainPosition target = new SubterrainPosition(8, -40, 8);
when(locator.position(engine, new Position2(0, 0))).thenReturn(target);
when(locator.landing(engine, world, target, Optional.empty())).thenReturn(new SubterrainPosition(8, -42, 8));
}
private void context(MockedStatic<IrisContext> context) {
context.when(() -> IrisContext.open(eq(engine), anyLong(), any())).thenReturn(mock(IrisContext.Scope.class));
}
private void teleport() {
ModdedLocateCommands.teleportToLocateResult(source, world, engine, player, "biome deep", locator,
new Position2(0, 0), Optional.of(new ModdedLocateCommands.CaveDestination(
new SubterrainPosition(8, -40, 8), Optional.empty())));
}
}
}
@@ -0,0 +1,429 @@
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.context.ChunkContext;
import art.arcane.iris.generation.context.IrisContext;
import art.arcane.iris.generation.runtime.Engine;
import art.arcane.iris.generation.runtime.IrisEngine;
import art.arcane.iris.generation.runtime.BiomeEnvironment;
import art.arcane.iris.world.history.GenerationHistoryRuntimeRouter;
import art.arcane.iris.world.history.SavedBiomeRuntime;
import art.arcane.iris.generation.runtime.GenerationSessionException;
import art.arcane.iris.generation.runtime.GenerationSessionLease;
import art.arcane.iris.generation.runtime.WrongEngineBroException;
import art.arcane.iris.generation.subterrain.IrisSubterrainFeature;
import art.arcane.iris.generation.subterrain.SubterrainCell;
import art.arcane.iris.generation.subterrain.SubterrainPlan;
import art.arcane.iris.generation.subterrain.SubterrainPosition;
import art.arcane.iris.generation.terrain.IrisDimensionCarvingEntry;
import art.arcane.iris.generation.terrain.IrisRegion;
import art.arcane.iris.world.history.GenerationSemanticIndex;
import art.arcane.iris.world.history.GenerationSemanticQueries;
import art.arcane.iris.world.history.GenerationFindCatalog;
import art.arcane.iris.world.history.GenerationHistory;
import art.arcane.iris.world.history.ChunkGenerationSemantics;
import art.arcane.volmlib.nativelib.terrain.NativeBlockState;
import art.arcane.volmlib.nativelib.terrain.NativeWorld;
import art.arcane.volmlib.util.math.Position2;
import art.arcane.volmlib.util.matter.MatterCavern;
import java.util.ArrayDeque;
import java.util.HashSet;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
import java.util.function.BooleanSupplier;
import java.util.Set;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CompletionException;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Consumer;
public final class BiomeLocator implements Locator<IrisBiome> {
private static final Set<String> HAZARDOUS_FLOORS = Set.of("minecraft:magma_block", "minecraft:campfire",
"minecraft:soul_campfire", "minecraft:cactus", "minecraft:powder_snow", "minecraft:pointed_dripstone");
private volatile long searchDeadline = Long.MAX_VALUE;
private final String key;
private final boolean authored;
private Set<String> caveHosts = Set.of();
private boolean configured;
private int originWorldY;
private boolean surfaceAllowed;
private final Set<Position2> surfaceTargets = ConcurrentHashMap.newKeySet();
private volatile SubterrainPosition authoredTarget;
private final ConcurrentHashMap<Position2, SubterrainPosition> cavernTargets = new ConcurrentHashMap<>();
public BiomeLocator(String key, boolean authored) {
this.key = key;
this.authored = authored;
}
public static Locator<IrisBiome> forBiome(Engine engine, String key, int originWorldY) {
boolean authored = false;
boolean configured = GenerationFindCatalog.hasRetainedCaveBiome(engine, key);
Set<String> caveHosts = new HashSet<>();
boolean surfaceAllowed = GenerationFindCatalog.hasRetainedSurfaceBiome(engine, key);
for (IrisRegion region : engine.getDimension().getAllRegions(engine)) {
for (String root : region.getLandBiomes()) {
surfaceAllowed |= caveBranch(engine, root).contains(key);
}
for (String root : region.getSeaBiomes()) {
surfaceAllowed |= caveBranch(engine, root).contains(key);
}
for (String root : region.getShoreBiomes()) {
surfaceAllowed |= caveBranch(engine, root).contains(key);
}
configured |= floodedBiome(region.getRiverPolicy(), key);
for (String root : region.getCaveBiomes()) {
Set<String> branch = caveBranch(engine, root);
if (branch.contains(key)) {
caveHosts.addAll(branch);
}
}
}
for (IrisDimensionCarvingEntry entry : engine.getDimension().getCarving()) {
if (entry.isEnabled() && !entry.getBiome().isEmpty() && caveBranch(engine, entry.getBiome()).contains(key)) {
configured = true;
}
}
for (IrisSubterrainFeature feature : engine.getDimension().getSubterrainFeatures()) {
if (feature.isEnabled() && key.equals(feature.getBiome())) {
authored = true;
}
}
configured |= floodedBiome(engine.getDimension().getRiverPolicy(), key);
for (IrisBiome biome : engine.getAllBiomes()) {
configured |= key.equals(biome.getCarvingBiome()) || floodedBiome(biome.getRiverPolicy(), key);
}
if (!authored && !configured && caveHosts.isEmpty()) {
return Locator.surfaceBiome(key);
}
BiomeLocator locator = new BiomeLocator(key, authored);
locator.caveHosts = Set.copyOf(caveHosts);
locator.configured = configured;
locator.originWorldY = originWorldY;
locator.surfaceAllowed = surfaceAllowed;
return locator;
}
private static boolean floodedBiome(IrisRiverPolicy policy, String key) {
return policy != null && policy.getFloodedCaveBiomes() != null && policy.getFloodedCaveBiomes().contains(key);
}
private static Set<String> caveBranch(Engine engine, String root) {
Set<String> visited = new HashSet<>();
ArrayDeque<String> pending = new ArrayDeque<>();
pending.add(root);
while (!pending.isEmpty()) {
String candidate = pending.removeFirst();
if (!visited.add(candidate)) {
continue;
}
IrisBiome biome = engine.getData().getBiomeLoader().load(candidate);
if (biome != null) {
pending.addAll(biome.getChildren());
}
}
return visited;
}
@Override
public CompletableFuture<Position2> find(Engine engine, Position2 origin, long timeout, Consumer<Integer> checks)
throws WrongEngineBroException {
searchDeadline = System.nanoTime() + timeout * 1_000_000L;
if (!authored || surfaceAllowed) {
return Locator.super.find(engine, origin, timeout, checks);
}
CompletableFuture<Position2> result = new CompletableFuture<>();
CompletableFuture.runAsync(() -> {
long deadline = System.nanoTime() + timeout * 1_000_000L;
try (GenerationSessionLease lease = engine.acquireGenerationLease("cave_biome_locator");
IrisContext.Scope ignored = IrisContext.open(engine, lease.sessionId(), null)) {
BooleanSupplier running = () -> !result.isCancelled() && !engine.isClosing() && System.nanoTime() < deadline;
Map<SubterrainLocator.Result, SubterrainLocator.Result> safeTargets = new HashMap<>();
SubterrainLocator.Result found = GenerationSemanticQueries.nearestSubterrain(engine,
new SubterrainLocator.Query("", null, key), (origin.getX() << 4) + 8,
originWorldY, (origin.getZ() << 4) + 8, 32768,
running, candidate -> {
SubterrainLocator.Result safe = dryAuthoredTarget(engine, candidate, running);
if (safe == null) {
return false;
}
safeTargets.put(candidate, safe);
return true;
}).orElse(null);
if (found != null) {
found = safeTargets.get(found);
authoredTarget = new SubterrainPosition(found.x(), found.y(), found.z());
}
checks.accept(1);
result.complete(found == null ? null : new Position2(found.x() >> 4, found.z() >> 4));
} catch (Throwable error) {
result.completeExceptionally(error);
}
});
return result;
}
@Override
public SearchCandidate nearestRecordedCandidate(Engine engine, Position2 origin, int maximumRadius) {
GenerationHistory history = GenerationLocatorPolicy.history(engine).orElse(null);
if (history == null) {
return null;
}
Set<Position2> rejected = new HashSet<>();
GenerationSemanticIndex.Query query = GenerationSemanticIndex.Query.acrossActivations(
GenerationSemanticIndex.SemanticKind.CAVE_BIOME, key,
new ChunkGenerationSemantics.BlockPosition((origin.getX() << 4) + 8, originWorldY,
(origin.getZ() << 4) + 8), maximumRadius);
while (!engine.isClosing() && !Thread.currentThread().isInterrupted() && System.nanoTime() < searchDeadline) {
GenerationSemanticIndex.Match match = history.findRecorded(query,
record -> !rejected.contains(new Position2(record.chunkX(), record.chunkZ()))).orElse(null);
if (match == null) {
return null;
}
Position2 chunk = new Position2(match.chunk().chunkX(), match.chunk().chunkZ());
SubterrainPosition target = position(engine, chunk);
if (target != null) {
cavernTargets.put(chunk, target);
return new SearchCandidate(chunk, target.x(), target.z());
}
rejected.add(chunk);
}
return null;
}
@Override
public boolean matches(Engine engine, Position2 chunk) {
if (surfaceAllowed && Locator.chunkContainsSurfaceBiome(engine, chunk, key)) {
surfaceTargets.add(chunk);
return true;
}
if (authored) {
return position(engine, chunk) != null;
}
if (position(engine, chunk) == null) {
return false;
}
try (GenerationSessionLease lease = engine.acquireGenerationLease("cave_biome_matter")) {
ChunkContext context = new ChunkContext(chunk.getX() << 4, chunk.getZ() << 4, engine.getComplex(),
lease.sessionId(), false, ChunkContext.PrefillPlan.NONE, null);
try (IrisContext.Scope ignored = IrisContext.open(engine, lease.sessionId(), context)) {
engine.generateMatter(chunk.getX(), chunk.getZ(), true, context);
AtomicReference<SubterrainPosition> target = new AtomicReference<>();
engine.getMantle().getMantle().iterateChunk(chunk.getX(), chunk.getZ(), MatterCavern.class, (x, y, z, cavern) -> {
if (target.get() != null || !dryCavern(cavern)) {
return;
}
int blockX = (chunk.getX() << 4) + x;
int blockZ = (chunk.getZ() << 4) + z;
SubterrainPosition candidate = cavernPosition(engine, blockX, y, blockZ, cavern);
if (candidate != null) {
target.set(candidate);
}
});
if (target.get() == null) {
return false;
}
cavernTargets.put(chunk, target.get());
return true;
}
} catch (GenerationSessionException error) {
throw new IllegalStateException(error);
}
}
public SubterrainPosition position(Engine engine, Position2 chunk) {
if (authoredTarget != null || cavernTargets.containsKey(chunk) || surfaceTargets.contains(chunk) || authored
|| activePredictionWithoutHistory(engine, chunk)) {
return position(engine, chunk, Optional.empty());
}
try {
CompletableFuture<Optional<SavedBiomeRuntime.ReadSession>> preparation = prepareRead(engine, chunk);
Optional<SavedBiomeRuntime.ReadSession> read = searchDeadline == Long.MAX_VALUE
? preparation.get() : preparation.get(Math.max(1L, searchDeadline - System.nanoTime()), TimeUnit.NANOSECONDS);
return position(engine, chunk, read);
} catch (TimeoutException error) {
return null;
} catch (InterruptedException error) {
Thread.currentThread().interrupt();
throw new CompletionException(error);
} catch (ExecutionException error) {
throw new CompletionException(error.getCause());
}
}
private SubterrainPosition position(Engine engine, Position2 chunk, Optional<SavedBiomeRuntime.ReadSession> read) {
return GenerationLocatorPolicy.evaluateValueScoped(engine, chunk, () -> positionScoped(engine, chunk, read));
}
private SubterrainPosition positionScoped(Engine engine, Position2 chunk, Optional<SavedBiomeRuntime.ReadSession> read) {
if (surfaceTargets.contains(chunk)) {
int x = (chunk.getX() << 4) + 8;
int z = (chunk.getZ() << 4) + 8;
return new SubterrainPosition(x, engine.getMinHeight() + engine.getHeight(x, z, false) + 2, z);
}
SubterrainPosition cavern = cavernTargets.get(chunk);
if (cavern != null) {
return cavern;
}
int x = (chunk.getX() << 4) + 8;
int z = (chunk.getZ() << 4) + 8;
if (authored) {
if (authoredTarget != null && authoredTarget.x() >> 4 == chunk.getX()
&& authoredTarget.z() >> 4 == chunk.getZ()) {
return authoredTarget;
}
for (SubterrainPlan plan : engine.getComplex().getSubterrainPlanner().plansForBounds(x - 8, z - 8, x + 7, z + 7)) {
if (!key.equals(plan.biome())) {
continue;
}
SubterrainPosition anchor = plan.anchor();
SubterrainCell owner = engine.getComplex().getSubterrainPlanner().sample(anchor.x(), anchor.y(), anchor.z());
if (owner.occupied() && plan.id().equals(owner.room().featureId())) {
SubterrainLocator.Result safe = dryAuthoredTarget(engine,
new SubterrainLocator.Result(plan.id(), plan.family(), plan.biome(), anchor.x(), anchor.y(), anchor.z()),
() -> !engine.isClosing());
if (safe != null) {
return new SubterrainPosition(safe.x(), safe.y(), safe.z());
}
}
}
return null;
}
if (read.isEmpty() && !configured && !caveHosts.isEmpty()) {
IrisBiome host = engine.getCaveBiome(x, z);
if (host == null || !caveHosts.contains(host.getLoadKey())) {
return null;
}
}
for (int radius = 0; radius <= (read.isPresent() ? 8 : 0); radius++) {
for (int columnX = Math.max(x - 8, x - radius); columnX <= Math.min(x + 7, x + radius); columnX++) {
for (int columnZ = Math.max(z - 8, z - radius); columnZ <= Math.min(z + 7, z + radius); columnZ++) {
if (Math.max(Math.abs(columnX - x), Math.abs(columnZ - z)) != radius) {
continue;
}
int top = Math.min(engine.getHeight() - 2, engine.getHeight(columnX, columnZ) - 2);
for (int y = top; y >= 1; y--) {
if (Thread.currentThread().isInterrupted() || engine.isClosing() || System.nanoTime() >= searchDeadline) {
return null;
}
IrisBiome saved = savedBiomeAt(columnX, y + engine.getMinHeight(), columnZ, read);
IrisBiome biome = saved != null ? saved : engine.getCaveBiome(columnX, y, columnZ);
if (biome != null && key.equals(biome.getLoadKey())) {
return new SubterrainPosition(columnX, y + engine.getMinHeight(), columnZ);
}
}
}
}
}
return null;
}
private static SubterrainLocator.Result dryAuthoredTarget(Engine engine, SubterrainLocator.Result candidate,
BooleanSupplier running) {
return GenerationSemanticQueries.drySubterrainLanding(engine, candidate, running).orElse(null);
}
private static boolean activePredictionWithoutHistory(Engine engine, Position2 chunk) {
if (!(engine instanceof IrisEngine irisEngine)) {
return true;
}
GenerationHistoryRuntimeRouter router = irisEngine.getGenerationHistoryRuntimeRouter().orElse(null);
if (router == null) {
return true;
}
GenerationHistory history = router.history();
return history.isActiveUnowned(chunk.getX(), chunk.getZ()) && history.semantics(chunk.getX(), chunk.getZ()).isEmpty();
}
public CompletableFuture<Optional<SavedBiomeRuntime.ReadSession>> prepareRead(Engine engine, Position2 chunk) {
if (engine instanceof IrisEngine irisEngine) {
GenerationHistoryRuntimeRouter router = irisEngine.getGenerationHistoryRuntimeRouter().orElse(null);
if (router != null) {
return router.biomes().readChunkAsync(chunk.getX(), chunk.getZ()).thenApply(Optional::of);
}
}
return CompletableFuture.completedFuture(Optional.empty());
}
public SubterrainPosition landing(Engine engine, NativeWorld world, SubterrainPosition target,
Optional<SavedBiomeRuntime.ReadSession> read) {
return GenerationLocatorPolicy.evaluateValueScoped(engine, new Position2(target.x() >> 4, target.z() >> 4),
() -> landingScoped(engine, world, target, read));
}
private SubterrainPosition landingScoped(Engine engine, NativeWorld world, SubterrainPosition target,
Optional<SavedBiomeRuntime.ReadSession> read) {
if (surfaceTargets.contains(new Position2(target.x() >> 4, target.z() >> 4))) {
return target;
}
int chunkX = target.x() >> 4;
int chunkZ = target.z() >> 4;
int minimumX = chunkX << 4;
int minimumZ = chunkZ << 4;
for (int radius = 0; radius <= 15; radius++) {
for (int x = Math.max(minimumX, target.x() - radius); x <= Math.min(minimumX + 15, target.x() + radius); x++) {
for (int z = Math.max(minimumZ, target.z() - radius); z <= Math.min(minimumZ + 15, target.z() + radius); z++) {
if (Math.max(Math.abs(x - target.x()), Math.abs(z - target.z())) != radius) {
continue;
}
int top = Math.min(world.maxHeight() - 2, engine.getMinHeight() + engine.getHeight(x, z) - 2);
for (int y = top; y > world.minHeight(); y--) {
NativeBlockState feet = world.getBlock(x, y, z);
if (!feet.isAir() || !world.getBlock(x, y + 1, z).isAir()) {
continue;
}
NativeBlockState floor = world.getBlock(x, y - 1, z);
if (!floor.isSolid() || floor.isFluid() || floor.isWaterLogged() || hazardous(floor)) {
continue;
}
IrisBiome biome = biomeAt(engine, x, y, z, read);
if (biome != null && key.equals(biome.getLoadKey())) {
return new SubterrainPosition(x, y, z);
}
}
}
}
}
return null;
}
private static IrisBiome biomeAt(Engine engine, int x, int worldY, int z,
Optional<SavedBiomeRuntime.ReadSession> read) {
IrisBiome saved = savedBiomeAt(x, worldY, z, read);
return saved != null ? saved : engine.getBiomeOrMantle(x, worldY - engine.getMinHeight(), z);
}
private static IrisBiome savedBiomeAt(int x, int worldY, int z,
Optional<SavedBiomeRuntime.ReadSession> read) {
return read.flatMap(session -> session.resolve(x, worldY, z, false)).map(BiomeEnvironment::biome).orElse(null);
}
SubterrainPosition cavernPosition(Engine engine, int x, int y, int z, MatterCavern cavern) {
if (!dryCavern(cavern)) {
return null;
}
IrisBiome biome = engine.getBiomeOrMantle(x, y, z);
return biome != null && key.equals(biome.getLoadKey())
? new SubterrainPosition(x, y + engine.getMinHeight(), z) : null;
}
private static boolean dryCavern(MatterCavern cavern) {
return cavern != null && cavern.isCavern() && (cavern.isAir() || cavern.getLiquid() == 3);
}
private static boolean hazardous(NativeBlockState state) {
String key = state.materialKey();
if (key == null) {
key = state.key().split("\\[", 2)[0];
}
return HAZARDOUS_FLOORS.contains(key);
}
}
@@ -3,6 +3,7 @@ package art.arcane.iris.generation.locator;
import art.arcane.iris.generation.runtime.Engine; import art.arcane.iris.generation.runtime.Engine;
import art.arcane.iris.generation.subterrain.IrisSubterrainFamily; import art.arcane.iris.generation.subterrain.IrisSubterrainFamily;
import art.arcane.iris.generation.subterrain.SubterrainCell; import art.arcane.iris.generation.subterrain.SubterrainCell;
import art.arcane.iris.generation.subterrain.SubterrainBounds;
import art.arcane.iris.generation.subterrain.SubterrainPlan; import art.arcane.iris.generation.subterrain.SubterrainPlan;
import art.arcane.iris.generation.subterrain.SubterrainPlanner; import art.arcane.iris.generation.subterrain.SubterrainPlanner;
import art.arcane.iris.generation.subterrain.SubterrainPosition; import art.arcane.iris.generation.subterrain.SubterrainPosition;
@@ -12,6 +13,7 @@ import java.util.Objects;
import java.util.Optional; import java.util.Optional;
import java.util.Set; import java.util.Set;
import java.util.function.Predicate; import java.util.function.Predicate;
import java.util.function.BooleanSupplier;
public final class SubterrainLocator { public final class SubterrainLocator {
private static final int SEARCH_TILE_SIZE = 512; private static final int SEARCH_TILE_SIZE = 512;
@@ -31,6 +33,11 @@ public final class SubterrainLocator {
public static Optional<Result> nearest(SubterrainPlanner planner, Query query, int x, int worldY, int z, public static Optional<Result> nearest(SubterrainPlanner planner, Query query, int x, int worldY, int z,
int maximumDistance, Predicate<Result> allowed) { int maximumDistance, Predicate<Result> allowed) {
return nearest(planner, query, x, worldY, z, maximumDistance, allowed, () -> true);
}
public static Optional<Result> nearest(SubterrainPlanner planner, Query query, int x, int worldY, int z,
int maximumDistance, Predicate<Result> allowed, BooleanSupplier running) {
Objects.requireNonNull(planner, "planner"); Objects.requireNonNull(planner, "planner");
Objects.requireNonNull(query, "query"); Objects.requireNonNull(query, "query");
Objects.requireNonNull(allowed, "allowed"); Objects.requireNonNull(allowed, "allowed");
@@ -51,6 +58,9 @@ public final class SubterrainLocator {
for (int offsetX = -ring; offsetX <= ring; offsetX++) { for (int offsetX = -ring; offsetX <= ring; offsetX++) {
int offsetZStep = Math.abs(offsetX) == ring ? 1 : Math.max(1, ring * 2); int offsetZStep = Math.abs(offsetX) == ring ? 1 : Math.max(1, ring * 2);
for (int offsetZ = -ring; offsetZ <= ring; offsetZ += offsetZStep) { for (int offsetZ = -ring; offsetZ <= ring; offsetZ += offsetZStep) {
if (!running.getAsBoolean() || Thread.currentThread().isInterrupted()) {
return Optional.empty();
}
long tileX = (centerTileX + offsetX) * SEARCH_TILE_SIZE; long tileX = (centerTileX + offsetX) * SEARCH_TILE_SIZE;
long tileZ = (centerTileZ + offsetZ) * SEARCH_TILE_SIZE; long tileZ = (centerTileZ + offsetZ) * SEARCH_TILE_SIZE;
long tileMaxX = tileX + SEARCH_TILE_SIZE - 1L; long tileMaxX = tileX + SEARCH_TILE_SIZE - 1L;
@@ -107,6 +117,68 @@ public final class SubterrainLocator {
return Optional.ofNullable(nearest); return Optional.ofNullable(nearest);
} }
public static Optional<Result> dryLanding(SubterrainPlanner planner, Result target, BooleanSupplier running) {
return dryLanding(planner, target, running, ignored -> true);
}
public static Optional<Result> dryLanding(SubterrainPlanner planner, Result target, BooleanSupplier running,
Predicate<Result> allowed) {
for (SubterrainPlan plan : planner.plansForBounds(target.x(), target.z(), target.x(), target.z())) {
if (!plan.id().equals(target.featureId())) {
continue;
}
SubterrainBounds bounds = plan.bounds();
int radiusLimit = Math.min(512, Math.max(Math.max(target.x() - bounds.minX(), bounds.maxX() - target.x()),
Math.max(target.z() - bounds.minZ(), bounds.maxZ() - target.z())));
for (int radius = 0; radius <= radiusLimit; radius++) {
if (!running.getAsBoolean() || Thread.currentThread().isInterrupted()) {
return Optional.empty();
}
int stride = Math.max(1, radius / 8);
for (int offset = -radius; offset <= radius; offset += stride) {
if (!running.getAsBoolean() || Thread.currentThread().isInterrupted()) {
return Optional.empty();
}
SubterrainPosition landing = eligibleDryLanding(planner, plan, target, target.x() + offset, target.z() - radius, allowed);
if (landing == null && radius != 0) {
landing = eligibleDryLanding(planner, plan, target, target.x() + offset, target.z() + radius, allowed);
}
if (landing == null && radius != 0) {
landing = eligibleDryLanding(planner, plan, target, target.x() - radius, target.z() + offset, allowed);
}
if (landing == null && radius != 0) {
landing = eligibleDryLanding(planner, plan, target, target.x() + radius, target.z() + offset, allowed);
}
if (landing != null) {
return Optional.of(new Result(target.featureId(), target.family(), target.biome(),
landing.x(), landing.y(), landing.z()));
}
}
}
}
return Optional.empty();
}
private static SubterrainPosition eligibleDryLanding(SubterrainPlanner planner, SubterrainPlan plan,
Result target, int x, int z, Predicate<Result> allowed) {
SubterrainPosition landing = dryOwnedLanding(planner, plan, x, z);
return landing != null && allowed.test(new Result(target.featureId(), target.family(), target.biome(),
landing.x(), landing.y(), landing.z())) ? landing : null;
}
private static SubterrainPosition dryOwnedLanding(SubterrainPlanner planner, SubterrainPlan plan, int x, int z) {
SubterrainPosition landing = plan.dryLandingAt(x, z);
if (landing == null) {
return null;
}
SubterrainCell feet = planner.sample(landing.x(), landing.y(), landing.z());
SubterrainCell head = planner.sample(landing.x(), landing.y() + 1, landing.z());
SubterrainCell floor = planner.sample(landing.x(), landing.y() - 1, landing.z());
return feet.kind() == SubterrainCell.Kind.AIR && head.kind() == SubterrainCell.Kind.AIR && floor.solid()
&& feet.room().featureId().equals(plan.id()) && head.room().featureId().equals(plan.id())
? landing : null;
}
public record Query(String featureId, IrisSubterrainFamily family, String biome) { public record Query(String featureId, IrisSubterrainFamily family, String biome) {
public Query { public Query {
featureId = Objects.requireNonNullElse(featureId, "").trim(); featureId = Objects.requireNonNullElse(featureId, "").trim();
@@ -10,6 +10,8 @@ public final class SubterrainPlan {
private final boolean rotated; private final boolean rotated;
private final String id; private final String id;
private final SubterrainBounds bounds; private final SubterrainBounds bounds;
private final double shapePhase;
private final ThreadLocal<ColumnCache> columnCache = ThreadLocal.withInitial(ColumnCache::new);
SubterrainPlan(Options options) { SubterrainPlan(Options options) {
definition = Objects.requireNonNull(options.definition()); definition = Objects.requireNonNull(options.definition());
@@ -18,6 +20,7 @@ public final class SubterrainPlan {
centerZ = options.centerZ(); centerZ = options.centerZ();
rotated = options.rotated(); rotated = options.rotated();
id = options.id(); id = options.id();
shapePhase = (SubterrainPlanner.mix(id.hashCode() ^ definition.seedSalt()) >>> 11) * 0x1.0p-53 * Math.PI * 2;
int along = family() == IrisSubterrainFamily.CENOTE ? definition.radius() : definition.length() / 2; int along = family() == IrisSubterrainFamily.CENOTE ? definition.radius() : definition.length() / 2;
int across = definition.radius(); int across = definition.radius();
int extentX = (rotated ? across : along) + 2; int extentX = (rotated ? across : along) + 2;
@@ -66,35 +69,64 @@ public final class SubterrainPlan {
return anchor; return anchor;
} }
if (family() == IrisSubterrainFamily.TRAVERTINE_TERRACES) { if (family() == IrisSubterrainFamily.TRAVERTINE_TERRACES) {
int path = definition.radius() - 4; Column center = cachedColumn(0, 0);
anchor = occupiedAnchor(rotated ? centerX + path : centerX, y, if (center != null) {
rotated ? centerZ : centerZ + path); int path = center.pathOffset();
if (anchor != null) { anchor = occupiedAnchor(rotated ? centerX + path : centerX, y,
return anchor; rotated ? centerZ : centerZ + path);
if (anchor != null) {
return anchor;
}
} }
int firstTerrace = -definition.length() / 2 + 1; }
anchor = occupiedAnchor(rotated ? centerX + path : centerX + firstTerrace, y, for (int u = -definition.length() / 2; u <= definition.length() / 2; u++) {
rotated ? centerZ + firstTerrace : centerZ + path); for (int v = -definition.radius(); v <= definition.radius(); v++) {
if (anchor != null) { int x = rotated ? centerX + v : centerX + u;
return anchor; int z = rotated ? centerZ + u : centerZ + v;
Column column = family() == IrisSubterrainFamily.LAVA_TUBE ? tube(u, v, centerY) : cachedColumn(u, v);
if (column == null || column.pathDistance() > 2) {
continue;
}
int candidateY = Math.max(column.floor(), column.head()) + 2;
SubterrainCell cell = sample(x, candidateY, z);
if (cell.occupied() && cell.room().reservedPassage()) {
return new SubterrainPosition(x, candidateY, z);
}
} }
} }
throw new IllegalStateException("Subterrain plan has no occupied anchor: " + id); throw new IllegalStateException("Subterrain plan has no occupied anchor: " + id);
} }
public SubterrainPosition dryLandingAt(int x, int z) {
int u = rotated ? z - centerZ : x - centerX;
int v = rotated ? x - centerX : z - centerZ;
Column column = family() == IrisSubterrainFamily.LAVA_TUBE ? tube(u, v, centerY) : cachedColumn(u, v);
if (column == null) {
return null;
}
for (int y = Math.max(bounds.minY() + 1, column.floor() + 1);
y < Math.min(bounds.maxY(), column.ceiling() - 1); y++) {
if (sample(x, y, z).kind() == SubterrainCell.Kind.AIR
&& sample(x, y + 1, z).kind() == SubterrainCell.Kind.AIR
&& sample(x, y - 1, z).solid()) {
return new SubterrainPosition(x, y, z);
}
}
return null;
}
public SubterrainCell sample(int x, int y, int z) { public SubterrainCell sample(int x, int y, int z) {
if (!bounds.contains(x, y, z)) { if (!bounds.contains(x, y, z)) {
return SubterrainCell.OUTSIDE; return SubterrainCell.OUTSIDE;
} }
int u = rotated ? z - centerZ : x - centerX; int u = rotated ? z - centerZ : x - centerX;
int v = rotated ? x - centerX : z - centerZ; int v = rotated ? x - centerX : z - centerZ;
Column column = switch (family()) { Column column = family() == IrisSubterrainFamily.LAVA_TUBE ? tube(u, v, y) : cachedColumn(u, v);
case CENOTE -> cenote(u, v); if (column == null) {
case TECTONIC_FAULT -> fault(u, v); return SubterrainCell.OUTSIDE;
case LAVA_TUBE -> tube(u, v, y); }
case TRAVERTINE_TERRACES -> terraces(u, v); int sealFloor = family() == IrisSubterrainFamily.CENOTE ? baseY() : column.floor();
}; if (y < sealFloor - 2 || y > column.ceiling() + 2) {
if (column == null || y < column.floor() - 2 || y > column.ceiling() + 2) {
return SubterrainCell.OUTSIDE; return SubterrainCell.OUTSIDE;
} }
SubterrainCell.Kind kind = classify(column, u, v, y); SubterrainCell.Kind kind = classify(column, u, v, y);
@@ -115,6 +147,28 @@ public final class SubterrainPlan {
return new SubterrainCell(kind, material, room); return new SubterrainCell(kind, material, room);
} }
private Column cachedColumn(int u, int v) {
ColumnCache cache = columnCache.get();
long key = ((long) u << 32) ^ (v & 0xffffffffL);
for (int i = 0; i < cache.count; i++) {
if (cache.keys[i] == key) {
return cache.columns[i];
}
}
Column column = switch (family()) {
case CENOTE -> cenote(u, v);
case TECTONIC_FAULT -> fault(u, v);
case TRAVERTINE_TERRACES -> terraces(u, v);
case LAVA_TUBE -> throw new IllegalStateException("Tube columns depend on height");
};
int slot = cache.next;
cache.keys[slot] = key;
cache.columns[slot] = column;
cache.next = (slot + 1) % cache.keys.length;
cache.count = Math.min(cache.keys.length, cache.count + 1);
return column;
}
private SubterrainCell.Kind classify(Column column, int u, int v, int y) { private SubterrainCell.Kind classify(Column column, int u, int v, int y) {
if (!column.interior() || y <= column.floor() || y >= column.ceiling() || column.rim()) { if (!column.interior() || y <= column.floor() || y >= column.ceiling() || column.rim()) {
return SubterrainCell.Kind.SOLID; return SubterrainCell.Kind.SOLID;
@@ -136,21 +190,36 @@ public final class SubterrainPlan {
return false; return false;
} }
int spacing = definition.pillarSpacing(); int spacing = definition.pillarSpacing();
if (spacing > 0 && Math.abs(Math.floorMod(u + spacing / 2, spacing) - spacing / 2) <= 1 if (spacing > 0) {
&& Math.abs(Math.floorMod(v + spacing / 2, spacing) - spacing / 2) <= 1) { int pillarU = Math.floorDiv(u + spacing / 2, spacing);
return true; int pillarV = Math.floorDiv(v + spacing / 2, spacing);
long pillarHash = SubterrainPlanner.mix(definition.seedSalt() ^ id.hashCode()
^ pillarU * 73428767L ^ pillarV * 912931L);
int jitter = Math.max(2, spacing / 5);
int offsetU = u - pillarU * spacing - (int) Math.floorMod(pillarHash, jitter * 2 + 1) + jitter;
int offsetV = v - pillarV * spacing - (int) Math.floorMod(pillarHash >>> 8, jitter * 2 + 1) + jitter;
double relativeHeight = (y - column.floor()) / (double) Math.max(1, column.ceiling() - column.floor());
double taper = relativeHeight * 2 - 1;
double radius = 1.3 + 1.2 * taper * taper;
double leanU = ((pillarHash >>> 16 & 255L) / 255.0 - 0.5) * 2;
double leanV = ((pillarHash >>> 24 & 255L) / 255.0 - 0.5) * 2;
double distanceU = offsetU - leanU * taper;
double distanceV = offsetV - leanV * taper;
if (distanceU * distanceU + distanceV * distanceV <= radius * radius) {
return true;
}
} }
if (column.head() >= y) { if (column.head() >= y) {
return false; return false;
} }
int cellU = Math.floorDiv(u, 12); int cellU = Math.floorDiv(u, 12);
int cellV = Math.floorDiv(v, 12); int cellV = Math.floorDiv(v, 12);
long hash = SubterrainPlanner.mix(definition.seedSalt() ^ cellU * 73428767L ^ cellV * 912931L); long hash = SubterrainPlanner.mix(definition.seedSalt() ^ id.hashCode() ^ cellU * 73428767L ^ cellV * 912931L);
int anchorU = cellU * 12 + 3 + (int) Math.floorMod(hash, 6); int anchorU = cellU * 12 + 3 + (int) Math.floorMod(hash, 6);
int anchorV = cellV * 12 + 3 + (int) Math.floorMod(hash >>> 8, 6); int anchorV = cellV * 12 + 3 + (int) Math.floorMod(hash >>> 8, 6);
int distance = Math.max(Math.abs(u - anchorU), Math.abs(v - anchorV)); double distance = Math.hypot(u - anchorU, v - anchorV);
int height = (int) Math.floor((column.ceiling() - column.floor() - 1) * definition.formationFraction()); int height = (int) Math.floor((column.ceiling() - column.floor() - 1) * definition.formationFraction());
int taperHeight = Math.max(0, height - distance * Math.max(1, height / 3)); int taperHeight = Math.max(0, (int) Math.floor(height - distance * Math.max(1, height / 3)));
return distance <= 2 && taperHeight > 0 return distance <= 2 && taperHeight > 0
&& (y <= column.floor() + taperHeight || y >= column.ceiling() - taperHeight); && (y <= column.floor() + taperHeight || y >= column.ceiling() - taperHeight);
} }
@@ -164,22 +233,25 @@ public final class SubterrainPlan {
double normalized = Math.min(1, distance / radius); double normalized = Math.min(1, distance / radius);
int ceiling = baseY() + Math.max(3, (int) Math.floor(definition.height() * Math.sqrt(1 - normalized * normalized))); int ceiling = baseY() + Math.max(3, (int) Math.floor(definition.height() * Math.sqrt(1 - normalized * normalized)));
int head = baseY() + definition.fluidDepth(); int head = baseY() + definition.fluidDepth();
double bank = Math.max(0, Math.min(1, (normalized - 0.65) / 0.25));
double slope = bank * bank * (3 - 2 * bank);
int floor = baseY() + (int) Math.round((definition.fluidDepth() + 2) * slope);
boolean rim = definition.fluidDepth() > 0 && ceiling <= head + 2; boolean rim = definition.fluidDepth() > 0 && ceiling <= head + 2;
return new Column(baseY(), ceiling, head, distance < radius, rim, return new Column(floor, ceiling, head, distance < radius, rim,
radius - distance, Math.abs(v), 0); radius - distance, Math.abs(v), 0);
} }
private Column fault(int u, int v) { private Column fault(int u, int v) {
int halfLength = definition.length() / 2; Footprint footprint = footprint(u, v);
int radius = definition.radius(); if (footprint == null) {
if (Math.abs(u) > halfLength + 2 || Math.abs(v) > radius + 2) {
return null; return null;
} }
int step = Math.max(2, radius / 4); int step = Math.max(2, definition.radius() / 4);
int shelf = Math.max(0, (Math.abs(v) - radius / 2) / step) * Math.max(2, definition.height() / 8); int shelf = Math.max(0, (int) (Math.abs(footprint.offset()) - footprint.width() * 0.5) / step)
return new Column(baseY() + shelf, baseY() + definition.height(), baseY() - 1, * Math.max(2, definition.height() / 8);
Math.abs(u) < halfLength && Math.abs(v) < radius, false, int ceiling = vaultedCeiling(u, footprint, baseY() + shelf, baseY() - 1);
Math.min(halfLength - Math.abs(u), radius - Math.abs(v)), Math.abs(v), 0); return new Column(baseY() + shelf, ceiling, baseY() - 1,
footprint.boundary() > 0, false, footprint.boundary(), Math.abs(footprint.offset()), footprint.path());
} }
private Column tube(int u, int v, int y) { private Column tube(int u, int v, int y) {
@@ -190,7 +262,13 @@ public final class SubterrainPlan {
if (Math.abs(u) > halfLength + 2 || Math.abs(offset) > width + 2) { if (Math.abs(u) > halfLength + 2 || Math.abs(offset) > width + 2) {
return null; return null;
} }
double arch = Math.sqrt(Math.max(0, 1 - Math.pow(Math.min(1, Math.abs(offset) / (double) width), 2))); double normalizedAlong = Math.min(1, Math.abs(u / (double) Math.max(1, halfLength)));
double endTaper = Math.sqrt(Math.max(0, 1 - Math.pow(normalizedAlong, 12)));
double taperedWidth = width * endTaper;
if (Math.abs(offset) > taperedWidth + 2) {
return null;
}
double arch = Math.sqrt(Math.max(0, 1 - Math.pow(Math.min(1, Math.abs(offset) / Math.max(1, taperedWidth)), 2))) * endTaper;
int floor = centerY - Math.max(2, (int) Math.floor(definition.height() * 0.5 * arch)); int floor = centerY - Math.max(2, (int) Math.floor(definition.height() * 0.5 * arch));
int ceiling = centerY + Math.max(2, (int) Math.floor(definition.height() * 0.5 * arch)); int ceiling = centerY + Math.max(2, (int) Math.floor(definition.height() * 0.5 * arch));
int head = baseY() + definition.fluidDepth(); int head = baseY() + definition.fluidDepth();
@@ -209,48 +287,100 @@ public final class SubterrainPlan {
} }
} }
boolean rim = definition.fluidDepth() > 0 && floor >= head && floor < head + 2; boolean rim = definition.fluidDepth() > 0 && floor >= head && floor < head + 2;
return new Column(floor, ceiling, head, Math.abs(u) < halfLength && Math.abs(offset) < width, return new Column(floor, ceiling, head, Math.abs(u) < halfLength && Math.abs(offset) < taperedWidth,
rim, Math.min(halfLength - Math.abs(u), width - Math.abs(offset)), Math.abs(offset), path); rim, Math.min(halfLength - Math.abs(u), taperedWidth - Math.abs(offset)), Math.abs(offset), path);
} }
private Column terraces(int u, int v) { private Column terraces(int u, int v) {
int halfLength = definition.length() / 2; Footprint footprint = footprint(u, v);
int radius = definition.radius(); if (footprint == null) {
if (Math.abs(u) > halfLength + 2 || Math.abs(v) > radius + 2) {
return null; return null;
} }
int segment = Math.max(4, definition.length() / definition.terraceCount()); double warped = u + definition.radius() * 0.2 * Math.sin(v / (double) Math.max(6, definition.radius()) * 2.4 + shapePhase)
int index = Math.max(0, Math.min(definition.terraceCount() - 1, Math.floorDiv(u + halfLength, segment))); + definition.radius() * 0.08 * Math.sin(u / 29.0 + v / 19.0 + shapePhase);
double segment = Math.max(4.0, definition.length() / (double) definition.terraceCount());
double terrace = (warped + definition.length() / 2.0) / segment;
int index = Math.max(0, Math.min(definition.terraceCount() - 1, (int) Math.floor(terrace)));
int rise = Math.max(1, (definition.height() / 3) / definition.terraceCount()); int rise = Math.max(1, (definition.height() / 3) / definition.terraceCount());
int floor = baseY() + index * rise; int floor = baseY() + index * rise;
int head = floor + definition.fluidDepth(); int head = floor + definition.fluidDepth();
int local = Math.floorMod(u + halfLength, segment); double local = (terrace - Math.floor(terrace)) * segment;
boolean rim = definition.fluidDepth() > 0 && (local <= 1 || local >= segment - 2 || Math.abs(v) >= radius - 2); boolean rim = definition.fluidDepth() > 0 && (local <= 1.5 || local >= segment - 1.5 || footprint.boundary() <= 2);
int ceiling = baseY() + definition.height(); double pathOffset = Math.copySign(Math.max(0, footprint.width() - 4), footprint.offset());
if (rim) { double pathDistance = Math.abs(footprint.offset() - pathOffset);
floor = head + 1; if (rim || pathDistance <= 2) {
}
double pathDistance = Math.abs(Math.abs(v) - (radius - 4));
if (pathDistance <= 2) {
floor = Math.max(floor, head + 1); floor = Math.max(floor, head + 1);
} }
return new Column(floor, ceiling, head, Math.abs(u) < halfLength && Math.abs(v) < radius, int ceiling = vaultedCeiling(u, footprint, floor, head);
false, Math.min(halfLength - Math.abs(u), radius - Math.abs(v)), pathDistance, return new Column(floor, ceiling, head, footprint.boundary() > 0,
v < 0 ? -radius + 4 : radius - 4); false, footprint.boundary(), pathDistance, footprint.path() + (int) Math.round(pathOffset));
}
private Profile profile(int u) {
ColumnCache cache = columnCache.get();
for (int i = 0; i < cache.profileCount; i++) {
if (cache.profileKeys[i] == u) {
return cache.profiles[i];
}
}
double along = Math.min(1, Math.abs(u / (double) Math.max(1, definition.length() / 2)));
double taper = Math.sqrt(Math.max(0, 1 - Math.pow(along, 6)));
double width = definition.radius() * taper * (0.83 + 0.08 * Math.sin(u / 31.0 + shapePhase)
+ 0.05 * Math.sin(u / 13.0 + shapePhase * 1.7));
double bend = family() == IrisSubterrainFamily.TECTONIC_FAULT ? 0.08 : 0.12;
int path = (int) Math.round(definition.radius() * bend * Math.sin(u / 47.0 + shapePhase) * taper);
double vaultWave = 0.87 + 0.07 * Math.sin(u / 37.0 + shapePhase) + 0.04 * Math.sin(u / 17.0 - shapePhase);
Profile profile = new Profile(Math.min(width, definition.radius() - Math.abs(path)), path, vaultWave);
int slot = cache.profileNext;
cache.profileKeys[slot] = u;
cache.profiles[slot] = profile;
cache.profileNext = (slot + 1) % cache.profiles.length;
cache.profileCount = Math.min(cache.profiles.length, cache.profileCount + 1);
return profile;
}
private Footprint footprint(int u, int v) {
int halfLength = definition.length() / 2;
if (Math.abs(u) > halfLength + 2) {
return null;
}
Profile profile = profile(u);
double width = profile.width();
int path = profile.path();
double offset = v - path;
double boundary = Math.min(halfLength - Math.abs(u), width - Math.abs(offset));
if (boundary < -2) {
return null;
}
return new Footprint(width, offset, boundary, path);
}
private int vaultedCeiling(int u, Footprint footprint, int floor, int head) {
double cross = Math.min(1, Math.abs(footprint.offset()) / Math.max(1, footprint.width()));
double arch = Math.sqrt(Math.max(0, 1 - cross * cross));
double wave = profile(u).vaultWave();
double roofVariation = 0.05 * Math.sin(u / 37.0 + footprint.offset() / 17.0 + shapePhase) * arch
+ 0.06 * footprint.offset() / Math.max(1, footprint.width()) * Math.sin(u / 41.0 - shapePhase);
int ceiling = baseY() + (int) Math.floor(definition.height() * wave * (0.4 + 0.6 * arch + roofVariation));
return Math.min(baseY() + definition.height(), Math.max(Math.max(floor, head) + 3, ceiling));
} }
private SubterrainPosition occupiedAnchor(int x, int preferredY, int z) { private SubterrainPosition occupiedAnchor(int x, int preferredY, int z) {
if (sample(x, preferredY, z).occupied()) { if (anchorCell(sample(x, preferredY, z))) {
return new SubterrainPosition(x, preferredY, z); return new SubterrainPosition(x, preferredY, z);
} }
for (int candidateY = bounds.minY(); candidateY <= bounds.maxY(); candidateY++) { for (int candidateY = bounds.minY(); candidateY <= bounds.maxY(); candidateY++) {
if (sample(x, candidateY, z).occupied()) { if (anchorCell(sample(x, candidateY, z))) {
return new SubterrainPosition(x, candidateY, z); return new SubterrainPosition(x, candidateY, z);
} }
} }
return null; return null;
} }
private boolean anchorCell(SubterrainCell cell) {
return cell.occupied() && (family() != IrisSubterrainFamily.TRAVERTINE_TERRACES || cell.room().reservedPassage());
}
private int baseY() { private int baseY() {
return centerY - definition.height() / 2; return centerY - definition.height() / 2;
} }
@@ -259,6 +389,23 @@ public final class SubterrainPlan {
boolean rotated, String id) { boolean rotated, String id) {
} }
private static final class ColumnCache {
private final long[] keys = new long[8];
private final Column[] columns = new Column[8];
private final int[] profileKeys = new int[3];
private final Profile[] profiles = new Profile[3];
private int profileCount;
private int profileNext;
private int count;
private int next;
}
private record Profile(double width, int path, double vaultWave) {
}
private record Footprint(double width, double offset, double boundary, int path) {
}
private record Column(int floor, int ceiling, int head, boolean interior, boolean rim, private record Column(int floor, int ceiling, int head, boolean interior, boolean rim,
double boundary, double pathDistance, int pathOffset) { double boundary, double pathDistance, int pathOffset) {
} }
@@ -27,6 +27,9 @@ import art.arcane.iris.integration.ExternalDataSVC;
import art.arcane.iris.generation.runtime.Engine; import art.arcane.iris.generation.runtime.Engine;
import art.arcane.iris.generation.runtime.GenerationSessionLease; import art.arcane.iris.generation.runtime.GenerationSessionLease;
import art.arcane.iris.generation.locator.Locator; import art.arcane.iris.generation.locator.Locator;
import art.arcane.iris.generation.locator.BiomeLocator;
import art.arcane.iris.generation.subterrain.SubterrainPosition;
import art.arcane.iris.platform.bukkit.BukkitPlatform;
import art.arcane.iris.structure.placement.LootResolver; import art.arcane.iris.structure.placement.LootResolver;
import art.arcane.iris.structure.placement.PlacedObject; import art.arcane.iris.structure.placement.PlacedObject;
import art.arcane.iris.generation.runtime.WrongEngineBroException; import art.arcane.iris.generation.runtime.WrongEngineBroException;
@@ -600,7 +603,7 @@ public final class EngineBukkitOps {
} }
public static void gotoBiome(Engine engine, IrisBiome biome, Player player, boolean teleport) { public static void gotoBiome(Engine engine, IrisBiome biome, Player player, boolean teleport) {
find(engine, Locator.surfaceBiome(biome.getLoadKey()), player, teleport, "Biome " + biome.getName()); find(engine, BiomeLocator.forBiome(engine, biome.getLoadKey(), player.getLocation().getBlockY()), player, teleport, "Biome " + biome.getName());
} }
public static void gotoObject(Engine engine, String s, Player player, boolean teleport) { public static void gotoObject(Engine engine, String s, Player player, boolean teleport) {
@@ -625,12 +628,14 @@ public final class EngineBukkitOps {
} }
private static void find(Engine engine, Locator<?> locator, Player player, boolean teleport, String message) { private static void find(Engine engine, Locator<?> locator, Player player, boolean teleport, String message) {
find(engine, locator, player, 120_000, location -> { find(engine, locator, player, 120_000, teleport, location -> {
if (location == null) { if (location == null) {
ComponentMessenger.sendSection(player, IrisLanguage.text(BukkitRuntimeMessages.ENGINE_BUKKIT_OPS_COULD_NOT_FIND_WITHIN_SEARCH_RANGE, MessageArgument.untrusted("message", String.valueOf(message)))); ComponentMessenger.sendSection(player, IrisLanguage.text(BukkitRuntimeMessages.ENGINE_BUKKIT_OPS_COULD_NOT_FIND_WITHIN_SEARCH_RANGE, MessageArgument.untrusted("message", String.valueOf(message))));
return; return;
} }
if (teleport) { if (teleport && locator instanceof BiomeLocator) {
teleportToCave(player, location, message);
} else if (teleport) {
J.runEntity(player, () -> teleportAsyncSafely(player, location)); J.runEntity(player, () -> teleportAsyncSafely(player, location));
ComponentMessenger.sendSection(player, IrisLanguage.text(BukkitRuntimeMessages.ENGINE_BUKKIT_OPS_TELEPORTING, MessageArgument.untrusted("message", String.valueOf(message)))); ComponentMessenger.sendSection(player, IrisLanguage.text(BukkitRuntimeMessages.ENGINE_BUKKIT_OPS_TELEPORTING, MessageArgument.untrusted("message", String.valueOf(message))));
} else { } else {
@@ -639,7 +644,7 @@ public final class EngineBukkitOps {
}); });
} }
private static void find(Engine engine, Locator<?> locator, Player player, long timeout, Consumer<Location> consumer) { private static void find(Engine engine, Locator<?> locator, Player player, long timeout, boolean resolveLanding, Consumer<Location> consumer) {
AtomicLong checks = new AtomicLong(); AtomicLong checks = new AtomicLong();
long ms = M.ms(); long ms = M.ms();
World world = player.getWorld(); World world = player.getWorld();
@@ -667,8 +672,15 @@ public final class EngineBukkitOps {
try (GenerationSessionLease lease = engine.acquireGenerationLease("bukkit_locator_result"); try (GenerationSessionLease lease = engine.acquireGenerationLease("bukkit_locator_result");
IrisContext.Scope ignored = IrisContext.open(engine, lease.sessionId(), null)) { IrisContext.Scope ignored = IrisContext.open(engine, lease.sessionId(), null)) {
int by = engine.getMinHeight() + engine.getHeight(bx, bz, false) + 2; int by = engine.getMinHeight() + engine.getHeight(bx, bz, false) + 2;
Location destination = new Location(world, bx, by, bz);
if (locator instanceof BiomeLocator caveLocator) {
SubterrainPosition target = caveLocator.position(engine, at);
destination = target == null ? null : resolveLanding
? caveDestination(engine, caveLocator, world, target).get()
: new Location(world, target.x(), target.y(), target.z());
}
resultDispatched = dispatchLocateResult( resultDispatched = dispatchLocateResult(
player, world, consumer, new Location(world, bx, by, bz), playerId, search); player, world, consumer, destination, playerId, search);
} }
} else { } else {
resultDispatched = dispatchLocateResult(player, world, consumer, null, playerId, search); resultDispatched = dispatchLocateResult(player, world, consumer, null, playerId, search);
@@ -678,6 +690,14 @@ public final class EngineBukkitOps {
Thread.currentThread().interrupt(); Thread.currentThread().interrupt();
} catch (WrongEngineBroException | ExecutionException e) { } catch (WrongEngineBroException | ExecutionException e) {
IrisLogging.reportError(e); IrisLogging.reportError(e);
if (search != null) {
resultDispatched = dispatchLocateFailure(player, world, playerId, search);
}
} catch (RuntimeException e) {
IrisLogging.reportError(e);
if (search != null) {
resultDispatched = dispatchLocateFailure(player, world, playerId, search);
}
} finally { } finally {
if (search != null && !resultDispatched) { if (search != null && !resultDispatched) {
ACTIVE_LOCATE_REQUESTS.remove(playerId, search); ACTIVE_LOCATE_REQUESTS.remove(playerId, search);
@@ -704,6 +724,43 @@ public final class EngineBukkitOps {
}.execute(new VolmitSender(player)); }.execute(new VolmitSender(player));
} }
static CompletableFuture<Location> caveDestination(Engine engine, BiomeLocator locator, World world,
SubterrainPosition target) {
CompletableFuture<Location> destination = new CompletableFuture<>();
BukkitPlatform.chunkAtAsync(world, target.x() >> 4, target.z() >> 4, true)
.thenCompose(chunk -> locator.prepareRead(engine, new Position2(target.x() >> 4, target.z() >> 4)))
.whenComplete((read, error) -> {
if (error != null) {
destination.completeExceptionally(error);
return;
}
boolean scheduled = J.runRegion(world, target.x() >> 4, target.z() >> 4, () -> {
try (GenerationSessionLease lease = engine.acquireGenerationLease("bukkit_cave_landing");
IrisContext.Scope ignored = IrisContext.open(engine, lease.sessionId(), null)) {
SubterrainPosition landing = locator.landing(engine, engine.getWorld().platformWorld(), target, read);
destination.complete(landing == null ? null
: new Location(world, landing.x() + 0.5, landing.y(), landing.z() + 0.5));
} catch (Throwable failure) {
destination.completeExceptionally(failure);
}
});
if (!scheduled) {
destination.completeExceptionally(new IllegalStateException("Could not schedule cave landing lookup"));
}
});
return destination;
}
private static boolean dispatchLocateFailure(Player player, World world, UUID playerId,
CompletableFuture<Position2> search) {
return J.runEntity(player, () -> {
if (!ACTIVE_LOCATE_REQUESTS.remove(playerId, search) || !player.isOnline() || !world.equals(player.getWorld())) {
return;
}
ComponentMessenger.sendSection(player, "Could not complete the search. See the server console for details.");
});
}
private static boolean dispatchLocateResult(Player player, World world, Consumer<Location> consumer, private static boolean dispatchLocateResult(Player player, World world, Consumer<Location> consumer,
Location location, UUID playerId, Location location, UUID playerId,
CompletableFuture<Position2> search) { CompletableFuture<Position2> search) {
@@ -722,6 +779,30 @@ public final class EngineBukkitOps {
return scheduled; return scheduled;
} }
static void teleportToCave(Player player, Location location, String message) {
try {
player.teleportAsync(location).whenComplete((teleported, error) -> {
if (error != null) {
IrisLogging.reportError(error);
}
J.runEntity(player, () -> {
if (!player.isOnline()) {
return;
}
if (error != null || !Boolean.TRUE.equals(teleported)) {
ComponentMessenger.sendSection(player, "Could not teleport to " + message + ".");
} else {
ComponentMessenger.sendSection(player, IrisLanguage.text(BukkitRuntimeMessages.ENGINE_BUKKIT_OPS_TELEPORTING,
MessageArgument.untrusted("message", message)));
}
});
});
} catch (Throwable error) {
IrisLogging.reportError(error);
ComponentMessenger.sendSection(player, "Could not teleport to " + message + ".");
}
}
private static void teleportAsyncSafely(Player player, Location location) { private static void teleportAsyncSafely(Player player, Location location) {
if (player == null || location == null) { if (player == null || location == null) {
return; return;
@@ -30,6 +30,14 @@ public final class GenerationFindCatalog {
return find(biomes(engine), key); return find(biomes(engine), key);
} }
public static boolean hasRetainedCaveBiome(Engine engine, String key) {
return containsKey(retained(engine).caveBiomes, normalizeKey(key));
}
public static boolean hasRetainedSurfaceBiome(Engine engine, String key) {
return containsKey(retained(engine).surfaceBiomes, normalizeKey(key));
}
public static KList<IrisRegion> regions(Engine engine) { public static KList<IrisRegion> regions(Engine engine) {
return merge(engine.getDimension().getAllRegions(engine), retained(engine).regions); return merge(engine.getDimension().getAllRegions(engine), retained(engine).regions);
} }
@@ -96,9 +104,11 @@ public final class GenerationFindCatalog {
return; return;
} }
for (String key : semantics.surfaceBiomeKeys()) { for (String key : semantics.surfaceBiomeKeys()) {
catalog.surfaceBiomes.add(normalizeKey(key));
addBiome(catalog, key); addBiome(catalog, key);
} }
for (String key : semantics.caveBiomeKeys()) { for (String key : semantics.caveBiomeKeys()) {
catalog.caveBiomes.add(normalizeKey(key));
addBiome(catalog, key); addBiome(catalog, key);
} }
for (String key : semantics.regionKeys()) { for (String key : semantics.regionKeys()) {
@@ -181,6 +191,8 @@ public final class GenerationFindCatalog {
private static final class RetainedCatalog { private static final class RetainedCatalog {
private final long activeActivationId; private final long activeActivationId;
private final Map<String, IrisBiome> biomes = new LinkedHashMap<>(); private final Map<String, IrisBiome> biomes = new LinkedHashMap<>();
private final Set<String> surfaceBiomes = new LinkedHashSet<>();
private final Set<String> caveBiomes = new LinkedHashSet<>();
private final Map<String, IrisRegion> regions = new LinkedHashMap<>(); private final Map<String, IrisRegion> regions = new LinkedHashMap<>();
private final Set<String> objects = new LinkedHashSet<>(); private final Set<String> objects = new LinkedHashSet<>();
private final Set<String> structures = new LinkedHashSet<>(); private final Set<String> structures = new LinkedHashSet<>();
@@ -26,6 +26,7 @@ import java.util.Objects;
import java.util.Optional; import java.util.Optional;
import java.util.Set; import java.util.Set;
import java.util.concurrent.ArrayBlockingQueue; import java.util.concurrent.ArrayBlockingQueue;
import java.util.function.Predicate;
public final class GenerationHistory { public final class GenerationHistory {
private static final int MAXIMUM_CACHED_BOUNDARIES = 4; private static final int MAXIMUM_CACHED_BOUNDARIES = 4;
@@ -848,6 +849,12 @@ public final class GenerationHistory {
return semantics.findNearest(query, ChunkGenerationSemantics::sealed); return semantics.findNearest(query, ChunkGenerationSemantics::sealed);
} }
public synchronized Optional<GenerationSemanticIndex.Match> findRecorded(
GenerationSemanticIndex.Query query, Predicate<ChunkGenerationSemantics> eligibility
) {
return semantics.findNearest(query, record -> record.sealed() && eligibility.test(record));
}
public synchronized Optional<GenerationSemanticIndex.RiverMatch> findRecordedRiver( public synchronized Optional<GenerationSemanticIndex.RiverMatch> findRecordedRiver(
GenerationSemanticIndex.RiverQuery query GenerationSemanticIndex.RiverQuery query
) { ) {
@@ -18,6 +18,7 @@ import java.util.Optional;
import java.util.OptionalLong; import java.util.OptionalLong;
import java.util.function.IntConsumer; import java.util.function.IntConsumer;
import java.util.function.Predicate; import java.util.function.Predicate;
import java.util.function.BooleanSupplier;
public final class GenerationSemanticQueries { public final class GenerationSemanticQueries {
private GenerationSemanticQueries() { private GenerationSemanticQueries() {
@@ -25,12 +26,25 @@ public final class GenerationSemanticQueries {
public static Optional<SubterrainLocator.Result> nearestSubterrain( public static Optional<SubterrainLocator.Result> nearestSubterrain(
Engine engine, SubterrainLocator.Query query, int x, int worldY, int z, int maximumDistance) { Engine engine, SubterrainLocator.Query query, int x, int worldY, int z, int maximumDistance) {
return nearestSubterrain(engine, query, x, worldY, z, maximumDistance, () -> true);
}
public static Optional<SubterrainLocator.Result> nearestSubterrain(
Engine engine, SubterrainLocator.Query query, int x, int worldY, int z, int maximumDistance,
BooleanSupplier running) {
return nearestSubterrain(engine, query, x, worldY, z, maximumDistance, running, ignored -> true);
}
public static Optional<SubterrainLocator.Result> nearestSubterrain(
Engine engine, SubterrainLocator.Query query, int x, int worldY, int z, int maximumDistance,
BooleanSupplier running, Predicate<SubterrainLocator.Result> allowed) {
Engine requiredEngine = Objects.requireNonNull(engine, "engine"); Engine requiredEngine = Objects.requireNonNull(engine, "engine");
Objects.requireNonNull(query, "query"); Objects.requireNonNull(query, "query");
GenerationHistoryRuntimeRouter router = requiredEngine instanceof IrisEngine irisEngine GenerationHistoryRuntimeRouter router = requiredEngine instanceof IrisEngine irisEngine
? irisEngine.getGenerationHistoryRuntimeRouter().orElse(null) : null; ? irisEngine.getGenerationHistoryRuntimeRouter().orElse(null) : null;
if (router == null) { if (router == null) {
return SubterrainLocator.nearest(requiredEngine, query, x, worldY, z, maximumDistance); return SubterrainLocator.nearest(requiredEngine.getComplex().getSubterrainPlanner(), query, x, worldY, z,
maximumDistance, allowed, running);
} }
GenerationHistory history = router.history(); GenerationHistory history = router.history();
SubterrainLocator.Result nearest = null; SubterrainLocator.Result nearest = null;
@@ -43,7 +57,7 @@ public final class GenerationSemanticQueries {
SubterrainPlanner planner = router.biomes().subterrainPlanner(activation.activationId()); SubterrainPlanner planner = router.biomes().subterrainPlanner(activation.activationId());
SubterrainLocator.Result candidate = SubterrainLocator.nearest(planner, query, x, worldY, z, SubterrainLocator.Result candidate = SubterrainLocator.nearest(planner, query, x, worldY, z,
maximumDistance, result -> allowsSubterrainPrediction(requiredEngine, history, maximumDistance, result -> allowsSubterrainPrediction(requiredEngine, history,
activation.activationId(), result)).orElse(null); activation.activationId(), result) && allowed.test(result), running).orElse(null);
if (candidate != null && (nearest == null || distanceSquared(x, worldY, z, candidate) if (candidate != null && (nearest == null || distanceSquared(x, worldY, z, candidate)
.compareTo(distanceSquared(x, worldY, z, nearest)) < 0)) { .compareTo(distanceSquared(x, worldY, z, nearest)) < 0)) {
nearest = candidate; nearest = candidate;
@@ -55,6 +69,24 @@ public final class GenerationSemanticQueries {
return Optional.ofNullable(nearest); return Optional.ofNullable(nearest);
} }
public static Optional<SubterrainLocator.Result> drySubterrainLanding(
Engine engine, SubterrainLocator.Result target, BooleanSupplier running) {
GenerationHistoryRuntimeRouter router = engine instanceof IrisEngine irisEngine
? irisEngine.getGenerationHistoryRuntimeRouter().orElse(null) : null;
if (router == null) {
return SubterrainLocator.dryLanding(engine.getComplex().getSubterrainPlanner(), target, running);
}
GenerationHistory history = router.history();
GenerationActivation owner = history.resolveActivation(target.x() >> 4, target.z() >> 4);
try {
SubterrainPlanner planner = router.biomes().subterrainPlanner(owner.activationId());
return SubterrainLocator.dryLanding(planner, target, running,
landing -> allowsSubterrainPrediction(engine, history, owner.activationId(), landing));
} catch (IOException failure) {
throw new UncheckedIOException("Unable to resolve saved subterrain landing", failure);
}
}
private static boolean allowsSubterrainPrediction(Engine engine, GenerationHistory history, private static boolean allowsSubterrainPrediction(Engine engine, GenerationHistory history,
long activationId, SubterrainLocator.Result result) { long activationId, SubterrainLocator.Result result) {
int chunkX = Math.floorDiv(result.x(), 16); int chunkX = Math.floorDiv(result.x(), 16);
@@ -120,6 +120,15 @@ public final class SavedBiomeRuntime implements AutoCloseable {
} }
} }
public CompletableFuture<ReadSession> readChunkAsync(int chunkX, int chunkZ) {
return queryAsync(chunkX, chunkZ).thenApply(prepared -> {
if (prepared.failure() != null) {
throw prepared.failure();
}
return new ReadSession(new ReadOptions(this, prepared, chunkX, chunkZ));
});
}
public boolean mayHaveBlockDropRules(String material) { public boolean mayHaveBlockDropRules(String material) {
if (engine.getData().hasBlockDropRules(material)) { if (engine.getData().hasBlockDropRules(material)) {
return true; return true;
@@ -573,6 +582,32 @@ public final class SavedBiomeRuntime implements AutoCloseable {
return ((long) chunkX << 32) | (chunkZ & 0xFFFFFFFFL); return ((long) chunkX << 32) | (chunkZ & 0xFFFFFFFFL);
} }
public static final class ReadSession {
private final ReadOptions options;
private ReadSession(ReadOptions options) {
this.options = options;
}
public Optional<BiomeEnvironment> resolve(int blockX, int worldY, int blockZ, boolean surface) {
if (blockX >> 4 != options.chunkX() || blockZ >> 4 != options.chunkZ()) {
throw new IllegalArgumentException("Saved biome read coordinates must remain inside the prepared chunk");
}
SavedBiomeRuntime runtime = options.runtime();
runtime.consumption.readLock().lock();
try {
runtime.requireOpen();
return runtime.resolve(options.query(), blockX, worldY, blockZ,
surface ? QueryKind.SURFACE : QueryKind.VOLUME);
} finally {
runtime.consumption.readLock().unlock();
}
}
}
private record ReadOptions(SavedBiomeRuntime runtime, PreparedQuery query, int chunkX, int chunkZ) {
}
private record PreparedQuery(Optional<SavedBiomeChunk> chunk, private record PreparedQuery(Optional<SavedBiomeChunk> chunk,
Map<SavedBiomeChunk.Cell, BiomeEnvironment> environments, Map<SavedBiomeChunk.Cell, BiomeEnvironment> environments,
Map<SavedBiomeChunk.Cell, SavedBiomeUnavailableException> cellFailures, Map<SavedBiomeChunk.Cell, SavedBiomeUnavailableException> cellFailures,
@@ -0,0 +1,270 @@
package art.arcane.iris.generation.locator;
import art.arcane.iris.generation.biome.IrisBiome;
import art.arcane.iris.pack.loading.IrisData;
import art.arcane.iris.generation.runtime.Engine;
import art.arcane.iris.generation.runtime.IrisEngine;
import art.arcane.iris.generation.runtime.BiomeEnvironment;
import art.arcane.iris.world.history.GenerationHistoryRuntimeRouter;
import art.arcane.iris.world.history.SavedBiomeRuntime;
import art.arcane.iris.world.history.GenerationHistory;
import art.arcane.iris.world.history.GenerationSemanticIndex;
import art.arcane.iris.world.history.ChunkGenerationSemantics;
import java.util.function.Predicate;
import art.arcane.iris.generation.subterrain.SubterrainPosition;
import art.arcane.iris.generation.terrain.IrisDimension;
import art.arcane.iris.generation.terrain.IrisRegion;
import art.arcane.volmlib.util.collection.KList;
import art.arcane.volmlib.nativelib.terrain.NativeBlockState;
import art.arcane.volmlib.nativelib.terrain.NativeWorld;
import art.arcane.volmlib.util.math.Position2;
import art.arcane.volmlib.util.matter.MatterCavern;
import org.junit.Test;
import java.util.Optional;
import java.util.concurrent.CompletableFuture;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.assertFalse;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.RETURNS_DEEP_STUBS;
import static org.mockito.Mockito.when;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.doReturn;
import static org.mockito.ArgumentMatchers.any;
public class BiomeLocatorTest {
@Test
public void unownedActivePredictionDoesNotQueueSavedBiomeReads() {
Fixture fixture = new Fixture();
IrisEngine engine = (IrisEngine) fixture.engine;
GenerationHistoryRuntimeRouter router = mock(GenerationHistoryRuntimeRouter.class);
GenerationHistory history = mock(GenerationHistory.class);
SavedBiomeRuntime saved = mock(SavedBiomeRuntime.class);
when(engine.getGenerationHistoryRuntimeRouter()).thenReturn(Optional.of(router));
when(router.history()).thenReturn(history);
when(router.biomes()).thenReturn(saved);
when(history.isActiveUnowned(0, 0)).thenReturn(true);
when(history.semantics(0, 0)).thenReturn(Optional.empty());
when(engine.getHeight()).thenReturn(384);
when(engine.getCaveBiome(8, 44, 8)).thenReturn(biome("deep"));
assertEquals(new SubterrainPosition(8, -20, 8), fixture.locator.position(engine, new Position2(0, 0)));
verify(saved, never()).readChunkAsync(0, 0);
}
@Test
public void recordedPositionReadsSavedVerticalIdentityWithoutActiveDefinition() {
Fixture fixture = new Fixture();
IrisEngine retained = (IrisEngine) fixture.engine;
GenerationHistoryRuntimeRouter router = mock(GenerationHistoryRuntimeRouter.class);
SavedBiomeRuntime saved = mock(SavedBiomeRuntime.class);
BiomeEnvironment environment = mock(BiomeEnvironment.class);
when(retained.getGenerationHistoryRuntimeRouter()).thenReturn(Optional.of(router));
when(router.history()).thenReturn(mock(GenerationHistory.class));
when(router.biomes()).thenReturn(saved);
when(environment.biome()).thenReturn(biome("deep"));
when(saved.resolve(8, -20, 8, false)).thenReturn(Optional.of(environment));
SavedBiomeRuntime.ReadSession read = mock(SavedBiomeRuntime.ReadSession.class);
when(saved.readChunkAsync(0, 0)).thenReturn(CompletableFuture.completedFuture(read));
when(read.resolve(8, -20, 8, false)).thenReturn(Optional.of(environment));
when(retained.getHeight()).thenReturn(384);
assertEquals(new SubterrainPosition(8, -20, 8), fixture.locator.position(retained, new Position2(0, 0)));
verify(retained, never()).getCaveBiome(8, 44, 8);
}
@Test
public void recordedMetadataWithoutVolumeMatchDoesNotBoundSearch() {
IrisEngine engine = mock(IrisEngine.class);
GenerationHistoryRuntimeRouter router = mock(GenerationHistoryRuntimeRouter.class);
GenerationHistory history = mock(GenerationHistory.class);
when(engine.getGenerationHistoryRuntimeRouter()).thenReturn(Optional.of(router));
when(router.history()).thenReturn(history);
Position2 near = new Position2(1, 0);
Position2 far = new Position2(4, 0);
BiomeLocator locator = spy(new BiomeLocator("deep", false));
doReturn(null).when(locator).position(engine, near);
doReturn(new SubterrainPosition(65, -20, 3)).when(locator).position(engine, far);
ChunkGenerationSemantics nearRecord = mock(ChunkGenerationSemantics.class);
when(nearRecord.chunkX()).thenReturn(1);
GenerationSemanticIndex.Match nearMatch = new GenerationSemanticIndex.Match(
GenerationSemanticIndex.SemanticKind.CAVE_BIOME, "deep",
new GenerationSemanticIndex.ChunkReference(1, 0, 1), Optional.empty());
GenerationSemanticIndex.Match farMatch = new GenerationSemanticIndex.Match(
GenerationSemanticIndex.SemanticKind.CAVE_BIOME, "deep",
new GenerationSemanticIndex.ChunkReference(4, 0, 1), Optional.empty());
when(history.findRecorded(any(), any())).thenAnswer(invocation -> {
Predicate<ChunkGenerationSemantics> eligible = invocation.getArgument(1);
return Optional.of(eligible.test(nearRecord) ? nearMatch : farMatch);
});
Locator.SearchCandidate candidate = locator.nearestRecordedCandidate(engine, new Position2(0, 0), 20);
assertEquals(far, candidate.chunk());
assertEquals(65, candidate.blockX());
verify(locator).position(engine, near);
verify(locator).position(engine, far);
}
@Test
public void mantleCandidatesRequireDryCavernAndCanonicalVolumeIdentity() {
Fixture fixture = new Fixture();
when(fixture.engine.getCaveOrMantleBiome(8, 44, 8)).thenReturn(biome("deep"));
when(fixture.engine.getBiomeOrMantle(8, 44, 8)).thenReturn(biome("surface"));
assertNull(fixture.locator.cavernPosition(fixture.engine, 8, 44, 8, new MatterCavern(true, "", (byte) 0)));
when(fixture.engine.getBiomeOrMantle(8, 44, 8)).thenReturn(biome("deep"));
assertNull(fixture.locator.cavernPosition(fixture.engine, 8, 44, 8, new MatterCavern(false, "", (byte) 0)));
assertNull(fixture.locator.cavernPosition(fixture.engine, 8, 44, 8, new MatterCavern(true, "", (byte) 1)));
assertNull(fixture.locator.cavernPosition(fixture.engine, 8, 44, 8, new MatterCavern(true, "", (byte) 2)));
assertEquals(new SubterrainPosition(8, -20, 8), fixture.locator.cavernPosition(fixture.engine, 8, 44, 8,
new MatterCavern(true, "", (byte) 3)));
}
@Test
public void recordedCavePositionFindsBiomeAwayFromChunkCenter() {
Fixture fixture = new Fixture();
IrisEngine retained = (IrisEngine) fixture.engine;
GenerationHistoryRuntimeRouter router = mock(GenerationHistoryRuntimeRouter.class);
SavedBiomeRuntime saved = mock(SavedBiomeRuntime.class);
SavedBiomeRuntime.ReadSession read = mock(SavedBiomeRuntime.ReadSession.class);
BiomeEnvironment environment = mock(BiomeEnvironment.class);
when(environment.biome()).thenReturn(biome("deep"));
when(retained.getGenerationHistoryRuntimeRouter()).thenReturn(Optional.of(router));
when(router.history()).thenReturn(mock(GenerationHistory.class));
when(router.biomes()).thenReturn(saved);
when(saved.readChunkAsync(0, 0)).thenReturn(CompletableFuture.completedFuture(read));
when(read.resolve(0, -20, 0, false)).thenReturn(Optional.of(environment));
when(retained.getHeight()).thenReturn(384);
IrisDimension dimension = mock(IrisDimension.class);
when(retained.getDimension()).thenReturn(dimension);
when(dimension.getAllRegions(retained)).thenReturn(new KList<>(new IrisRegion().setCaveBiomes(new KList<>("deep"))));
when(dimension.getCarving()).thenReturn(new KList<>());
when(dimension.getSubterrainFeatures()).thenReturn(new KList<>());
when(retained.getAllBiomes()).thenReturn(new KList<>());
when(retained.getData()).thenReturn(mock(IrisData.class, RETURNS_DEEP_STUBS));
when(retained.getData().getBiomeLoader().load("deep")).thenReturn(biome("deep"));
when(retained.getCaveBiome(8, 8)).thenReturn(biome("other"));
when(retained.getGenerationHistoryRuntimeRouter()).thenReturn(Optional.empty());
BiomeLocator locator = (BiomeLocator) BiomeLocator.forBiome(retained, "deep", 59);
when(retained.getGenerationHistoryRuntimeRouter()).thenReturn(Optional.of(router));
assertEquals(new SubterrainPosition(0, -20, 0), locator.position(retained, new Position2(0, 0)));
verify(read).resolve(0, -20, 0, false);
}
@Test
public void liveLandingReadsExactRetainedBiomeDespiteDifferentActiveBiome() {
Fixture fixture = new Fixture();
IrisEngine retained = (IrisEngine) fixture.engine;
GenerationHistoryRuntimeRouter router = mock(GenerationHistoryRuntimeRouter.class);
SavedBiomeRuntime saved = mock(SavedBiomeRuntime.class);
BiomeEnvironment environment = mock(BiomeEnvironment.class);
when(retained.getGenerationHistoryRuntimeRouter()).thenReturn(Optional.of(router));
when(router.history()).thenReturn(mock(GenerationHistory.class));
when(router.biomes()).thenReturn(saved);
when(environment.biome()).thenReturn(biome("deep"));
SavedBiomeRuntime.ReadSession read = mock(SavedBiomeRuntime.ReadSession.class);
when(read.resolve(8, -20, 8, false)).thenReturn(Optional.of(environment));
when(retained.getBiomeOrMantle(8, 44, 8)).thenReturn(biome("active-surface"));
assertEquals(new SubterrainPosition(8, -20, 8), fixture.locator.landing(retained, fixture.world,
new SubterrainPosition(8, -15, 8), Optional.of(read)));
verify(retained, never()).getBiomeOrMantle(8, 44, 8);
verify(read).resolve(8, -20, 8, false);
verify(saved, never()).resolve(8, -20, 8, false);
}
@Test
public void registeredCarvingBiomeUsesCaveSearchAndSurfaceKeepsItsLocator() {
Engine engine = catalogEngine();
IrisBiome surface = biome("surface");
surface.setCarvingBiome("cave");
when(engine.getAllBiomes()).thenReturn(new KList<>(surface, biome("cave")));
assertTrue(BiomeLocator.forBiome(engine, "cave", 80) instanceof BiomeLocator);
assertFalse(BiomeLocator.forBiome(engine, "surface", 80) instanceof BiomeLocator);
}
@Test
public void sharedSurfaceAndCaveKeyStillReturnsSurfacePosition() {
Engine engine = catalogEngine();
IrisRegion region = new IrisRegion().setLandBiomes(new KList<>("shared")).setCaveBiomes(new KList<>("shared"));
when(engine.getDimension().getAllRegions(engine)).thenReturn(new KList<>(region));
IrisBiome shared = biome("shared");
when(engine.getData().getBiomeLoader().load("shared")).thenReturn(shared);
when(engine.getSurfaceBiome(8, 8)).thenReturn(shared);
when(engine.getHeight(8, 8, false)).thenReturn(100);
BiomeLocator locator = (BiomeLocator) BiomeLocator.forBiome(engine, "shared", 80);
assertTrue(locator.matches(engine, new Position2(0, 0)));
assertEquals(new SubterrainPosition(8, 102, 8), locator.position(engine, new Position2(0, 0)));
}
private static Engine catalogEngine() {
Engine engine = mock(Engine.class, RETURNS_DEEP_STUBS);
IrisDimension dimension = mock(IrisDimension.class);
when(engine.getDimension()).thenReturn(dimension);
when(dimension.getAllRegions(engine)).thenReturn(new KList<>());
when(dimension.getCarving()).thenReturn(new KList<>());
when(dimension.getSubterrainFeatures()).thenReturn(new KList<>());
when(engine.getAllBiomes()).thenReturn(new KList<>());
return engine;
}
@Test
public void depthBiomeMatchRetainsAbsoluteNegativeY() {
Engine engine = mock(Engine.class);
when(engine.getMinHeight()).thenReturn(-64);
when(engine.getHeight()).thenReturn(384);
when(engine.getHeight(8, 8)).thenReturn(100);
IrisBiome cave = biome("deep");
when(engine.getCaveBiome(8, 11, 8)).thenReturn(cave);
BiomeLocator locator = new BiomeLocator("deep", false);
assertEquals(new SubterrainPosition(8, -53, 8), locator.position(engine, new Position2(0, 0)));
assertNull(new BiomeLocator("other", false).position(engine, new Position2(0, 0)));
}
@Test
public void landingUsesActualCaveFloorAndExactBiomeInsteadOfSurface() {
Fixture fixture = new Fixture();
assertEquals(new SubterrainPosition(8, -20, 8), fixture.locator.landing(fixture.engine, fixture.world,
new SubterrainPosition(8, -15, 8), Optional.empty()));
when(fixture.engine.getBiomeOrMantle(8, 44, 8)).thenReturn(biome("surface"));
assertNull(fixture.locator.landing(fixture.engine, fixture.world, new SubterrainPosition(8, -15, 8), Optional.empty()));
}
@Test
public void fluidAndHazardousFloorsCannotBeUsedAsLanding() {
Fixture fixture = new Fixture();
when(fixture.floor.isWaterLogged()).thenReturn(true);
assertNull(fixture.locator.landing(fixture.engine, fixture.world, new SubterrainPosition(8, -15, 8), Optional.empty()));
when(fixture.floor.isWaterLogged()).thenReturn(false);
when(fixture.floor.materialKey()).thenReturn("minecraft:magma_block");
assertNull(fixture.locator.landing(fixture.engine, fixture.world, new SubterrainPosition(8, -15, 8), Optional.empty()));
}
private static IrisBiome biome(String key) {
IrisBiome biome = new IrisBiome();
biome.setLoadKey(key);
return biome;
}
private static final class Fixture {
private final Engine engine = mock(IrisEngine.class);
private final NativeWorld world = mock(NativeWorld.class);
private final NativeBlockState floor = mock(NativeBlockState.class);
private final BiomeLocator locator = new BiomeLocator("deep", false);
private Fixture() {
NativeBlockState air = mock(NativeBlockState.class);
when(air.isAir()).thenReturn(true);
when(floor.isSolid()).thenReturn(true);
when(floor.materialKey()).thenReturn("minecraft:stone");
when(world.minHeight()).thenReturn(-64);
when(world.maxHeight()).thenReturn(320);
when(world.getBlock(anyInt(), anyInt(), anyInt())).thenReturn(floor);
when(world.getBlock(8, -20, 8)).thenReturn(air);
when(world.getBlock(8, -19, 8)).thenReturn(air);
when(engine.getMinHeight()).thenReturn(-64);
when(engine.getHeight(anyInt(), anyInt())).thenReturn(100);
when(engine.getBiomeOrMantle(8, 44, 8)).thenReturn(biome("deep"));
}
}
}
@@ -5,6 +5,7 @@ import art.arcane.iris.generation.runtime.IrisComplex;
import art.arcane.iris.generation.subterrain.IrisSubterrainFamily; import art.arcane.iris.generation.subterrain.IrisSubterrainFamily;
import art.arcane.iris.generation.subterrain.IrisSubterrainFeature; import art.arcane.iris.generation.subterrain.IrisSubterrainFeature;
import art.arcane.iris.generation.subterrain.SubterrainCell; import art.arcane.iris.generation.subterrain.SubterrainCell;
import art.arcane.iris.generation.subterrain.IrisSubterrainFluid;
import art.arcane.iris.generation.subterrain.SubterrainPlanner; import art.arcane.iris.generation.subterrain.SubterrainPlanner;
import art.arcane.iris.pack.value.IrisRange; import art.arcane.iris.pack.value.IrisRange;
import org.junit.Test; import org.junit.Test;
@@ -20,6 +21,31 @@ import static org.mockito.Mockito.verifyNoMoreInteractions;
import static org.mockito.Mockito.when; import static org.mockito.Mockito.when;
public class SubterrainLocatorTest { public class SubterrainLocatorTest {
@Test
public void wetCenoteSelectsDryOwnedBankOutsideCenterChunk() {
SubterrainPlanner water = wetCenote(IrisSubterrainFluid.WATER);
SubterrainLocator.Result center = SubterrainLocator.nearest(water,
new SubterrainLocator.Query("basin", null, "wet-cave"), 0, 0, 0, 4096, ignored -> true).orElseThrow();
SubterrainCell centerAir = water.sample(center.x(), center.y(), center.z());
assertEquals(SubterrainCell.Kind.WATER, water.sample(center.x(), centerAir.room().fluidHeadY(), center.z()).kind());
SubterrainLocator.Result bank = SubterrainLocator.dryLanding(water, center, () -> true).orElseThrow();
assertTrue(center.x() >> 4 != bank.x() >> 4 || center.z() >> 4 != bank.z() >> 4);
assertEquals(SubterrainCell.Kind.AIR, water.sample(bank.x(), bank.y(), bank.z()).kind());
assertEquals(SubterrainCell.Kind.AIR, water.sample(bank.x(), bank.y() + 1, bank.z()).kind());
assertTrue(water.sample(bank.x(), bank.y() - 1, bank.z()).solid());
assertEquals(center.featureId(), water.sample(bank.x(), bank.y(), bank.z()).room().featureId());
SubterrainPlanner lava = wetCenote(IrisSubterrainFluid.LAVA);
assertEquals(bank, SubterrainLocator.dryLanding(lava, center, () -> true).orElseThrow());
assertFalse(SubterrainLocator.dryLanding(water, center, () -> false).isPresent());
}
private static SubterrainPlanner wetCenote(IrisSubterrainFluid fluid) {
IrisSubterrainFeature feature = new IrisSubterrainFeature().setId("basin").setBiome("wet-cave")
.setProbability(1).setWorldYRange(new IrisRange(-100, 100)).setRadius(28).setHeight(40)
.setFluidDepth(8).setFluid(fluid).setPillarSpacing(0).setFormationFraction(0);
return new SubterrainPlanner(new SubterrainPlanner.Options(List.of(feature), 74119L, -128, 256));
}
@Test @Test
public void wideDistanceSearchReturnsOwnedAbsolutePositionWithoutGenerating() { public void wideDistanceSearchReturnsOwnedAbsolutePositionWithoutGenerating() {
SubterrainPlanner planner = planner(); SubterrainPlanner planner = planner();
@@ -50,6 +76,12 @@ public class SubterrainLocatorTest {
new SubterrainLocator.Query("", null, ""), 0, 0, 0, 1024, ignored -> false).isPresent()); new SubterrainLocator.Query("", null, ""), 0, 0, 0, 1024, ignored -> false).isPresent());
} }
@Test
public void cancelledSearchStopsBeforeInspectingPlans() {
assertFalse(SubterrainLocator.nearest(planner(), new SubterrainLocator.Query("", null, "wet-cave"),
0, 0, 0, 32768, ignored -> true, () -> false).isPresent());
}
@Test @Test
public void repeatedSearchAndLargeNegativeCoordinatesAreStable() { public void repeatedSearchAndLargeNegativeCoordinatesAreStable() {
Engine engine = engine(planner()); Engine engine = engine(planner());
@@ -8,6 +8,8 @@ import org.junit.Test;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections; import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.Set;
import java.util.HashSet;
import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertFalse;
@@ -23,9 +25,6 @@ public class SubterrainPlannerTest {
for (IrisSubterrainFluid fluid : IrisSubterrainFluid.values()) { for (IrisSubterrainFluid fluid : IrisSubterrainFluid.values()) {
IrisSubterrainFeature feature = compactFeature(IrisSubterrainFamily.TRAVERTINE_TERRACES).setFluid(fluid); IrisSubterrainFeature feature = compactFeature(IrisSubterrainFamily.TRAVERTINE_TERRACES).setFluid(fluid);
SubterrainPlan pillar = direct(feature); SubterrainPlan pillar = direct(feature);
for (int y = pillar.bounds().minY(); y <= pillar.bounds().maxY(); y++) {
assertFalse(pillar.sample(0, y, 0).occupied());
}
SubterrainPosition pillarAnchor = pillar.anchor(); SubterrainPosition pillarAnchor = pillar.anchor();
assertTrue(pillar.sample(pillarAnchor.x(), pillarAnchor.y(), pillarAnchor.z()).occupied()); assertTrue(pillar.sample(pillarAnchor.x(), pillarAnchor.y(), pillarAnchor.z()).occupied());
assertTrue(pillar.sample(pillarAnchor.x(), pillarAnchor.y(), pillarAnchor.z()).room().reservedPassage()); assertTrue(pillar.sample(pillarAnchor.x(), pillarAnchor.y(), pillarAnchor.z()).room().reservedPassage());
@@ -173,17 +172,81 @@ public class SubterrainPlannerTest {
} }
@Test @Test
public void faultWallsRemainParallelBeyondTwoHundredBlocksAndHaveShelves() { public void faultsAndTerracesHaveTaperedIrregularWallsAndVaultedCeilings() {
SubterrainPlan plan = direct(feature(IrisSubterrainFamily.TECTONIC_FAULT)); for (IrisSubterrainFamily family : List.of(IrisSubterrainFamily.TECTONIC_FAULT,
for (int x = -110; x <= 110; x++) { IrisSubterrainFamily.TRAVERTINE_TERRACES)) {
assertTrue(plan.sample(x, 0, 0).occupied()); SubterrainPlan plan = direct(feature(family));
assertTrue(plan.sample(x, 0, 32).solid()); Set<Integer> widths = new HashSet<>();
assertTrue(plan.sample(x, 0, -32).solid()); Set<Integer> ceilings = new HashSet<>();
for (int x = -100; x <= 100; x += 5) {
int width = 0;
for (int z = -32; z <= 32; z++) {
SubterrainCell cell = plan.sample(x, 0, z);
if (cell.occupied()) {
width++;
ceilings.add(cell.room().ceilingY());
}
}
widths.add(width);
}
assertTrue(family.toString(), widths.size() > 4);
assertTrue(family.toString(), ceilings.size() > 5);
assertFalse(plan.sample(120, 0, 28).occupied());
assertFalse(plan.sample(-120, 0, -28).occupied());
assertTrue(plan.sample(0, 0, 0).room().ceilingY() > plan.sample(0, 0, 20).room().ceilingY());
} }
int centralFloor = plan.sample(0, 0, 0).room().floorY(); }
int ledgeFloor = plan.sample(0, 0, 28).room().floorY();
assertTrue(ledgeFloor > centralFloor); @Test
assertTrue(plan.sample(0, ledgeFloor, 28).solid()); public void organicShapesRemainSealedInsideMaximumBoundsAcrossSeedsAndRotation() {
for (IrisSubterrainFamily family : List.of(IrisSubterrainFamily.TECTONIC_FAULT,
IrisSubterrainFamily.TRAVERTINE_TERRACES, IrisSubterrainFamily.LAVA_TUBE)) {
IrisSubterrainFeature feature = feature(family).setLength(2048).setRadius(256).setSpacing(4096);
for (int seed = 0; seed < 4; seed++) {
for (boolean rotated : List.of(false, true)) {
SubterrainPlan plan = new SubterrainPlan(new SubterrainPlan.Options(
SubterrainPlanner.Definition.from(feature), 0, 0, 0, rotated, feature.getId() + ":" + seed));
SubterrainBounds bounds = plan.bounds();
for (int y = bounds.minY(); y <= bounds.maxY(); y++) {
for (int x = bounds.minX(); x <= bounds.maxX(); x += 13) {
assertFalse(plan.sample(x, y, bounds.minZ()).occupied());
assertFalse(plan.sample(x, y, bounds.maxZ()).occupied());
}
for (int z = bounds.minZ(); z <= bounds.maxZ(); z += 13) {
assertFalse(plan.sample(bounds.minX(), y, z).occupied());
assertFalse(plan.sample(bounds.maxX(), y, z).occupied());
}
}
}
}
}
}
@Test
public void organicGeometryVariesPerPlanAndRotatesWithoutChangingOccupancy() {
IrisSubterrainFeature feature = feature(IrisSubterrainFamily.TRAVERTINE_TERRACES);
SubterrainPlanner.Definition definition = SubterrainPlanner.Definition.from(feature);
SubterrainPlan original = new SubterrainPlan(new SubterrainPlan.Options(definition, 0, 0, 0, false, "terrace:one"));
SubterrainPlan rotated = new SubterrainPlan(new SubterrainPlan.Options(definition, 0, 0, 0, true, "terrace:one"));
SubterrainPlan another = new SubterrainPlan(new SubterrainPlan.Options(definition, 0, 0, 0, false, "terrace:two"));
int changed = 0;
for (int x = -120; x <= 120; x += 4) {
for (int z = -30; z <= 30; z += 3) {
for (int y = -16; y <= 16; y += 4) {
SubterrainCell cell = original.sample(x, y, z);
SubterrainCell turned = rotated.sample(z, y, x);
assertEquals(cell.kind(), turned.kind());
if (cell.owned()) {
assertEquals(cell.room().floorY(), turned.room().floorY());
assertEquals(cell.room().ceilingY(), turned.room().ceilingY());
}
if (cell.kind() != another.sample(x, y, z).kind()) {
changed++;
}
}
}
}
assertTrue(changed > 100);
} }
@Test @Test
@@ -217,9 +280,17 @@ public class SubterrainPlannerTest {
assertTrue(plan.sample(90, 0, 0).room().floorY() > plan.sample(-90, 0, 0).room().floorY()); assertTrue(plan.sample(90, 0, 0).room().floorY() > plan.sample(-90, 0, 0).room().floorY());
SubterrainPlanner planner = planner(feature(IrisSubterrainFamily.TRAVERTINE_TERRACES)); SubterrainPlanner planner = planner(feature(IrisSubterrainFamily.TRAVERTINE_TERRACES));
assertTrue(assertRetained(planner, List.of(plan), SubterrainCell.Kind.WATER) > 1000); assertTrue(assertRetained(planner, List.of(plan), SubterrainCell.Kind.WATER) > 1000);
int rimX = -128 + 42; Set<Integer> rimLocations = new HashSet<>();
assertTrue(plan.sample(rimX, -10, 0).solid()); for (int z = -16; z <= 16; z += 4) {
assertTrue(plan.sample(0, 0, 28).room().floorY() > plan.sample(0, 0, 28).room().fluidHeadY()); for (int x = -90; x < -35; x++) {
SubterrainCell cell = plan.sample(x, -10, z);
if (cell.owned() && cell.room().floorY() > cell.room().fluidHeadY()) {
rimLocations.add(x);
break;
}
}
}
assertTrue(rimLocations.size() > 2);
} }
@Test @Test
@@ -227,8 +298,21 @@ public class SubterrainPlannerTest {
IrisSubterrainFeature feature = feature(IrisSubterrainFamily.CENOTE).setPillarSpacing(12).setFormationFraction(0.3); IrisSubterrainFeature feature = feature(IrisSubterrainFamily.CENOTE).setPillarSpacing(12).setFormationFraction(0.3);
SubterrainPlan plan = direct(feature); SubterrainPlan plan = direct(feature);
SubterrainRoom room = plan.sample(12, 0, 12).room(); SubterrainRoom room = plan.sample(12, 0, 12).room();
for (int y = room.floorY(); y <= room.ceilingY(); y++) { Set<SubterrainPosition> previous = new HashSet<>();
assertTrue("pillar y=" + y, plan.sample(12, y, 12).solid()); for (int y = room.floorY() + 1; y < room.ceilingY(); y++) {
Set<SubterrainPosition> layer = new HashSet<>();
for (int x = 7; x <= 17; x++) {
for (int z = 7; z <= 17; z++) {
if (plan.sample(x, y, z).solid()) {
layer.add(new SubterrainPosition(x, 0, z));
}
}
}
assertFalse("pillar layer y=" + y, layer.isEmpty());
if (!previous.isEmpty()) {
assertTrue("pillar connected y=" + y, layer.stream().anyMatch(previous::contains));
}
previous = layer;
} }
assertTrue(plan.sample(0, 0, 0).room().reservedPassage()); assertTrue(plan.sample(0, 0, 0).room().reservedPassage());
assertFalse(plan.sample(0, 0, 0).solid()); assertFalse(plan.sample(0, 0, 0).solid());
@@ -0,0 +1,80 @@
package art.arcane.iris.platform.generation;
import art.arcane.iris.generation.context.IrisContext;
import art.arcane.iris.generation.locator.BiomeLocator;
import art.arcane.iris.generation.runtime.Engine;
import art.arcane.iris.generation.runtime.GenerationSessionLease;
import art.arcane.iris.generation.subterrain.SubterrainPosition;
import art.arcane.iris.platform.bukkit.BukkitPlatform;
import art.arcane.iris.world.IrisWorld;
import art.arcane.iris.world.history.SavedBiomeRuntime;
import art.arcane.volmlib.util.math.Position2;
import art.arcane.iris.world.task.J;
import art.arcane.volmlib.nativelib.terrain.NativeWorld;
import org.bukkit.Chunk;
import org.bukkit.Location;
import org.bukkit.World;
import org.junit.Test;
import org.mockito.MockedStatic;
import java.util.concurrent.CompletableFuture;
import java.util.Optional;
import java.util.concurrent.atomic.AtomicReference;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.assertNull;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyLong;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.mockStatic;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoInteractions;
import static org.mockito.Mockito.when;
public class EngineBukkitOpsCaveDestinationTest {
@Test
public void landingWaitsForChunkLoadThenRunsOnDestinationRegion() throws Exception {
World world = mock(World.class);
NativeWorld nativeWorld = mock(NativeWorld.class);
Engine engine = mock(Engine.class);
GenerationSessionLease lease = mock(GenerationSessionLease.class);
when(engine.acquireGenerationLease("bukkit_cave_landing")).thenReturn(lease);
when(engine.getWorld()).thenReturn(IrisWorld.builder().platformWorld(nativeWorld).build());
BiomeLocator locator = mock(BiomeLocator.class);
SubterrainPosition target = new SubterrainPosition(-25, -40, 35);
SubterrainPosition landing = new SubterrainPosition(-24, -42, 36);
SavedBiomeRuntime.ReadSession readSession = mock(SavedBiomeRuntime.ReadSession.class);
CompletableFuture<Optional<SavedBiomeRuntime.ReadSession>> prepared = new CompletableFuture<>();
when(locator.prepareRead(engine, new Position2(-2, 2))).thenReturn(prepared);
when(locator.landing(engine, nativeWorld, target, Optional.of(readSession))).thenReturn(landing);
CompletableFuture<Chunk> loaded = new CompletableFuture<>();
AtomicReference<Runnable> regionTask = new AtomicReference<>();
try (MockedStatic<BukkitPlatform> platform = mockStatic(BukkitPlatform.class);
MockedStatic<J> scheduler = mockStatic(J.class);
MockedStatic<IrisContext> context = mockStatic(IrisContext.class)) {
platform.when(() -> BukkitPlatform.chunkAtAsync(world, -2, 2, true)).thenReturn(loaded);
scheduler.when(() -> J.runRegion(eq(world), eq(-2), eq(2), any(Runnable.class))).thenAnswer(call -> {
regionTask.set(call.getArgument(3));
return true;
});
context.when(() -> IrisContext.open(eq(engine), anyLong(), any())).thenReturn(mock(IrisContext.Scope.class));
CompletableFuture<Location> result = EngineBukkitOps.caveDestination(engine, locator, world, target);
assertFalse(result.isDone());
verifyNoInteractions(locator);
loaded.complete(mock(Chunk.class));
assertFalse(result.isDone());
assertNull(regionTask.get());
prepared.complete(Optional.of(readSession));
assertFalse(result.isDone());
regionTask.get().run();
assertTrue(result.isDone());
assertEquals(-23.5, result.get().getX(), 0);
assertEquals(-42, result.get().getY(), 0);
assertEquals(36.5, result.get().getZ(), 0);
verify(locator).landing(engine, nativeWorld, target, Optional.of(readSession));
}
}
}
@@ -1,6 +1,9 @@
package art.arcane.iris.world.history; package art.arcane.iris.world.history;
import art.arcane.iris.pack.loading.IrisData; import art.arcane.iris.pack.loading.IrisData;
import art.arcane.iris.generation.locator.BiomeLocator;
import art.arcane.iris.generation.locator.Locator;
import art.arcane.volmlib.util.math.Position2;
import art.arcane.iris.pack.loading.ResourceLoader; import art.arcane.iris.pack.loading.ResourceLoader;
import art.arcane.iris.generation.runtime.IrisEngine; import art.arcane.iris.generation.runtime.IrisEngine;
import art.arcane.iris.generation.biome.IrisBiome; import art.arcane.iris.generation.biome.IrisBiome;
@@ -31,6 +34,34 @@ import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when; import static org.mockito.Mockito.when;
public class GenerationFindCatalogTest { public class GenerationFindCatalogTest {
@Test
public void retainedCaveRolesSelectCaveSearchWithoutActiveDefinitions() throws Exception {
Fixture fixture = new Fixture();
when(fixture.engine.getDimension().getCarving()).thenReturn(new KList<>());
when(fixture.engine.getDimension().getSubterrainFeatures()).thenReturn(new KList<>());
doAnswer(invocation -> {
GenerationSemanticIndex.RecordConsumer consumer = invocation.getArgument(0);
consumer.accept(ChunkGenerationSemantics.builder(0, 0, 1L)
.addCaveBiome("old-cave").addCaveBiome("shared")
.addSurfaceBiome("old-surface").addSurfaceBiome("shared").seal().build());
consumer.accept(ChunkGenerationSemantics.builder(1, 0, 1L).addCaveBiome("unsealed").build());
consumer.accept(ChunkGenerationSemantics.builder(2, 0, 2L).addCaveBiome("current").seal().build());
return null;
}).when(fixture.history).forEachRecordedSemantic(any());
assertTrue(GenerationFindCatalog.hasRetainedCaveBiome(fixture.engine, "OLD-CAVE"));
assertFalse(GenerationFindCatalog.hasRetainedSurfaceBiome(fixture.engine, "old-cave"));
assertTrue(BiomeLocator.forBiome(fixture.engine, "old-cave", -30) instanceof BiomeLocator);
assertFalse(BiomeLocator.forBiome(fixture.engine, "old-surface", -30) instanceof BiomeLocator);
assertFalse(GenerationFindCatalog.hasRetainedCaveBiome(fixture.engine, "unsealed"));
assertFalse(GenerationFindCatalog.hasRetainedCaveBiome(fixture.engine, "current"));
IrisBiome shared = new IrisBiome();
shared.setLoadKey("shared");
when(fixture.engine.getSurfaceBiome(8, 8)).thenReturn(shared);
Locator<IrisBiome> locator = BiomeLocator.forBiome(fixture.engine, "shared", -30);
assertTrue(locator.matches(fixture.engine, new Position2(0, 0)));
verify(fixture.history, never()).packRoot(anyLong());
}
@Test @Test
public void findsRecordedNamesWithoutLoadingArchivedPacksOrRuntimes() throws Exception { public void findsRecordedNamesWithoutLoadingArchivedPacksOrRuntimes() throws Exception {
Fixture fixture = new Fixture(); Fixture fixture = new Fixture();
@@ -26,6 +26,7 @@ import java.time.Duration;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.Optional; import java.util.Optional;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CountDownLatch; import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService; import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors; import java.util.concurrent.Executors;
@@ -368,6 +369,59 @@ public class SavedBiomeRuntimeTest {
} }
} }
@Test
public void asynchronousChunkSessionWaitsForDiskAndConsumesTheCompletedRead() throws Exception {
allowUnownedChunks();
CountDownLatch entered = new CountDownLatch(1);
CountDownLatch release = new CountDownLatch(1);
when(store.get(0, 0)).thenAnswer(invocation -> {
entered.countDown();
assertTrue(release.await(5L, TimeUnit.SECONDS));
return Optional.empty();
});
try (SavedBiomeRuntime runtime = new SavedBiomeRuntime(engine, history)) {
CompletableFuture<SavedBiomeRuntime.ReadSession> reading = runtime.readChunkAsync(0, 0);
try {
assertTrue(entered.await(5L, TimeUnit.SECONDS));
assertFalse(reading.isDone());
assertEquals(1, runtime.pendingQueryCount());
assertThrows(TimeoutException.class, () -> reading.get(100L, TimeUnit.MILLISECONDS));
} finally {
release.countDown();
}
SavedBiomeRuntime.ReadSession session = reading.get(5L, TimeUnit.SECONDS);
awaitIdle(runtime);
assertEquals(Optional.empty(), session.resolve(0, 0, 0, true));
assertEquals(Optional.empty(), session.resolve(15, -32, 15, false));
assertEquals(0, runtime.pendingQueryCount());
verify(store).get(0, 0);
} finally {
release.countDown();
}
}
@Test
public void asynchronousChunkSessionSurvivesQueryEvictionAndRejectsClosedOrOtherChunks() throws Exception {
allowUnownedChunks();
SavedBiomeRuntime.ReadSession session;
try (SavedBiomeRuntime runtime = new SavedBiomeRuntime(engine, history)) {
session = runtime.readChunkAsync(0, 0).get(5L, TimeUnit.SECONDS);
for (int chunkX = 1; chunkX <= 130; chunkX++) {
runtime.readChunkAsync(chunkX, 0).get(5L, TimeUnit.SECONDS);
}
awaitIdle(runtime);
assertEquals(128, runtime.cachedQueryCount());
assertEquals(Optional.empty(), session.resolve(0, 0, 0, true));
assertEquals(Optional.empty(), session.resolve(15, 32, 15, false));
assertThrows(IllegalArgumentException.class, () -> session.resolve(16, 0, 0, true));
assertThrows(IllegalArgumentException.class, () -> session.resolve(-1, 0, 0, false));
assertEquals(0, runtime.pendingQueryCount());
verify(store).get(0, 0);
}
assertThrows(IllegalStateException.class, () -> session.resolve(0, 0, 0, true));
verify(store).get(0, 0);
}
@Test @Test
public void backgroundPreviewWaitsForTheReadWithoutRepeatingTheQuery() throws Exception { public void backgroundPreviewWaitsForTheReadWithoutRepeatingTheQuery() throws Exception {
allowUnownedChunks(); allowUnownedChunks();
@@ -7,6 +7,7 @@ import art.arcane.iris.generation.subterrain.IrisSubterrainFeature;
import art.arcane.iris.generation.subterrain.SubterrainPlan; import art.arcane.iris.generation.subterrain.SubterrainPlan;
import art.arcane.iris.generation.subterrain.SubterrainPlanner; import art.arcane.iris.generation.subterrain.SubterrainPlanner;
import art.arcane.iris.generation.subterrain.SubterrainPosition; import art.arcane.iris.generation.subterrain.SubterrainPosition;
import art.arcane.iris.pack.value.IrisRange;
import org.junit.Test; import org.junit.Test;
import java.util.List; import java.util.List;
@@ -21,6 +22,39 @@ import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when; import static org.mockito.Mockito.when;
public class SavedSubterrainQueriesTest { public class SavedSubterrainQueriesTest {
@Test
public void dryShoreUsesRetainedPlannerAndRejectsAnotherActivationsChunks() throws Exception {
IrisEngine engine = mock(IrisEngine.class);
GenerationHistory history = mock(GenerationHistory.class);
GenerationHistoryRuntimeRouter router = mock(GenerationHistoryRuntimeRouter.class);
SavedBiomeRuntime saved = mock(SavedBiomeRuntime.class);
GenerationActivation old = GenerationActivation.initial("a".repeat(64), 1L);
GenerationActivation active = GenerationActivation.next(2L, "b".repeat(64), 1L, 2L, 64);
IrisSubterrainFeature feature = new IrisSubterrainFeature().setId("old-basin").setProbability(1D)
.setBiome("retired-cave").setRadius(28).setHeight(40).setFluidDepth(8)
.setWorldYRange(new IrisRange(-100, 100)).setFormationFraction(0).setPillarSpacing(0);
SubterrainPlanner historical = new SubterrainPlanner(new SubterrainPlanner.Options(List.of(feature), 1191L, -128, 256));
SubterrainPlan plan = historical.plansForBounds(0, 0, 511, 511).getFirst();
SubterrainPosition anchor = plan.anchor();
SubterrainLocator.Result target = new SubterrainLocator.Result(plan.id(), plan.family(), plan.biome(),
anchor.x(), anchor.y(), anchor.z());
when(engine.getGenerationHistoryRuntimeRouter()).thenReturn(Optional.of(router));
when(router.history()).thenReturn(history);
when(router.biomes()).thenReturn(saved);
when(saved.subterrainPlanner(old.activationId())).thenReturn(historical);
when(history.resolveActivation(anyInt(), anyInt())).thenReturn(old);
SubterrainLocator.Result dry = GenerationSemanticQueries.drySubterrainLanding(engine, target, () -> true).orElseThrow();
assertTrue(target.x() >> 4 != dry.x() >> 4 || target.z() >> 4 != dry.z() >> 4);
assertTrue(historical.sample(dry.x(), dry.y() - 1, dry.z()).solid());
verify(engine, never()).getComplex();
verify(engine, never()).getActiveGenerationRuntimeBinding();
verify(saved, never()).subterrainPlanner(active.activationId());
when(history.resolveActivation(anyInt(), anyInt())).thenReturn(active);
when(history.resolveActivation(target.x() >> 4, target.z() >> 4)).thenReturn(old);
assertTrue(GenerationSemanticQueries.drySubterrainLanding(engine, target, () -> true).isEmpty());
assertTrue(GenerationSemanticQueries.drySubterrainLanding(engine, target, () -> false).isEmpty());
}
@Test @Test
public void locateUsesRetainedDefinitionOnlyInsideItsOwnedChunks() throws Exception { public void locateUsesRetainedDefinitionOnlyInsideItsOwnedChunks() throws Exception {
IrisEngine engine = mock(IrisEngine.class); IrisEngine engine = mock(IrisEngine.class);