This commit is contained in:
Brian Neumann-Fopiano
2026-10-04 15:17:12 -04:00
parent a6149b27c9
commit 65ae3f1b45
18 changed files with 1664 additions and 86 deletions
@@ -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")
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(
@Param(description = "The biome to look for", descriptionKey = "iris.director.commandfind.param.biome_look", customHandler = ReachableBiomeHandler.class)
IrisBiome biome,
@@ -31,6 +31,9 @@ import art.arcane.iris.generation.runtime.GenerationSessionException;
import art.arcane.iris.generation.runtime.GenerationSessionLease;
import art.arcane.iris.structure.placement.IrisStructureLocator;
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.placement.StructureReachability;
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)));
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;
}
@@ -545,12 +548,12 @@ final class ModdedLocateCommands {
previous.cancel(true);
}
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,
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) {
return;
}
@@ -576,15 +579,77 @@ final class ModdedLocateCommands {
});
return;
}
if (locator instanceof BiomeLocator caveLocator) {
prepareCaveLocate(new CaveLocateRequest(source, level, engine, player, label, server, playerId,
search, caveLocator, at));
return;
}
server.execute(() -> {
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,
NativeProtocolPlayer player, String label, Position2 at) {
private static void prepareCaveLocate(CaveLocateRequest request) {
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
// two minutes, and the captured NativeProtocolPlayer may be gone or elsewhere by then.
if (!player.connected()) {
@@ -600,6 +665,17 @@ final class ModdedLocateCommands {
try (GenerationSessionLease lease = engine.acquireGenerationLease("modded_locator_teleport");
IrisContext.Scope ignored = IrisContext.open(engine, lease.sessionId(), null)) {
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);
if (!teleported) {
IrisModdedCommands.fail(source, IrisLanguage.plain(
@@ -611,7 +687,8 @@ final class ModdedLocateCommands {
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)));
} 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)));
}
}
@@ -635,6 +712,14 @@ final class ModdedLocateCommands {
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,
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.subterrain.IrisSubterrainFamily;
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.SubterrainPlanner;
import art.arcane.iris.generation.subterrain.SubterrainPosition;
@@ -12,6 +13,7 @@ import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.function.Predicate;
import java.util.function.BooleanSupplier;
public final class SubterrainLocator {
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,
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(query, "query");
Objects.requireNonNull(allowed, "allowed");
@@ -51,6 +58,9 @@ public final class SubterrainLocator {
for (int offsetX = -ring; offsetX <= ring; offsetX++) {
int offsetZStep = Math.abs(offsetX) == ring ? 1 : Math.max(1, ring * 2);
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 tileZ = (centerTileZ + offsetZ) * SEARCH_TILE_SIZE;
long tileMaxX = tileX + SEARCH_TILE_SIZE - 1L;
@@ -107,6 +117,68 @@ public final class SubterrainLocator {
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 Query {
featureId = Objects.requireNonNullElse(featureId, "").trim();
@@ -10,6 +10,8 @@ public final class SubterrainPlan {
private final boolean rotated;
private final String id;
private final SubterrainBounds bounds;
private final double shapePhase;
private final ThreadLocal<ColumnCache> columnCache = ThreadLocal.withInitial(ColumnCache::new);
SubterrainPlan(Options options) {
definition = Objects.requireNonNull(options.definition());
@@ -18,6 +20,7 @@ public final class SubterrainPlan {
centerZ = options.centerZ();
rotated = options.rotated();
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 across = definition.radius();
int extentX = (rotated ? across : along) + 2;
@@ -66,35 +69,64 @@ public final class SubterrainPlan {
return anchor;
}
if (family() == IrisSubterrainFamily.TRAVERTINE_TERRACES) {
int path = definition.radius() - 4;
Column center = cachedColumn(0, 0);
if (center != null) {
int path = center.pathOffset();
anchor = occupiedAnchor(rotated ? centerX + path : centerX, y,
rotated ? centerZ : centerZ + path);
if (anchor != null) {
return anchor;
}
int firstTerrace = -definition.length() / 2 + 1;
anchor = occupiedAnchor(rotated ? centerX + path : centerX + firstTerrace, y,
rotated ? centerZ + firstTerrace : centerZ + path);
if (anchor != null) {
return anchor;
}
}
for (int u = -definition.length() / 2; u <= definition.length() / 2; u++) {
for (int v = -definition.radius(); v <= definition.radius(); v++) {
int x = rotated ? centerX + v : centerX + u;
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);
}
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) {
if (!bounds.contains(x, y, z)) {
return SubterrainCell.OUTSIDE;
}
int u = rotated ? z - centerZ : x - centerX;
int v = rotated ? x - centerX : z - centerZ;
Column column = switch (family()) {
case CENOTE -> cenote(u, v);
case TECTONIC_FAULT -> fault(u, v);
case LAVA_TUBE -> tube(u, v, y);
case TRAVERTINE_TERRACES -> terraces(u, v);
};
if (column == null || y < column.floor() - 2 || y > column.ceiling() + 2) {
Column column = family() == IrisSubterrainFamily.LAVA_TUBE ? tube(u, v, y) : cachedColumn(u, v);
if (column == null) {
return SubterrainCell.OUTSIDE;
}
int sealFloor = family() == IrisSubterrainFamily.CENOTE ? baseY() : column.floor();
if (y < sealFloor - 2 || y > column.ceiling() + 2) {
return SubterrainCell.OUTSIDE;
}
SubterrainCell.Kind kind = classify(column, u, v, y);
@@ -115,6 +147,28 @@ public final class SubterrainPlan {
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) {
if (!column.interior() || y <= column.floor() || y >= column.ceiling() || column.rim()) {
return SubterrainCell.Kind.SOLID;
@@ -136,21 +190,36 @@ public final class SubterrainPlan {
return false;
}
int spacing = definition.pillarSpacing();
if (spacing > 0 && Math.abs(Math.floorMod(u + spacing / 2, spacing) - spacing / 2) <= 1
&& Math.abs(Math.floorMod(v + spacing / 2, spacing) - spacing / 2) <= 1) {
if (spacing > 0) {
int pillarU = Math.floorDiv(u + spacing / 2, spacing);
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) {
return false;
}
int cellU = Math.floorDiv(u, 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 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 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
&& (y <= column.floor() + taperHeight || y >= column.ceiling() - taperHeight);
}
@@ -164,22 +233,25 @@ public final class SubterrainPlan {
double normalized = Math.min(1, distance / radius);
int ceiling = baseY() + Math.max(3, (int) Math.floor(definition.height() * Math.sqrt(1 - normalized * normalized)));
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;
return new Column(baseY(), ceiling, head, distance < radius, rim,
return new Column(floor, ceiling, head, distance < radius, rim,
radius - distance, Math.abs(v), 0);
}
private Column fault(int u, int v) {
int halfLength = definition.length() / 2;
int radius = definition.radius();
if (Math.abs(u) > halfLength + 2 || Math.abs(v) > radius + 2) {
Footprint footprint = footprint(u, v);
if (footprint == null) {
return null;
}
int step = Math.max(2, radius / 4);
int shelf = Math.max(0, (Math.abs(v) - radius / 2) / step) * Math.max(2, definition.height() / 8);
return new Column(baseY() + shelf, baseY() + definition.height(), baseY() - 1,
Math.abs(u) < halfLength && Math.abs(v) < radius, false,
Math.min(halfLength - Math.abs(u), radius - Math.abs(v)), Math.abs(v), 0);
int step = Math.max(2, definition.radius() / 4);
int shelf = Math.max(0, (int) (Math.abs(footprint.offset()) - footprint.width() * 0.5) / step)
* Math.max(2, definition.height() / 8);
int ceiling = vaultedCeiling(u, footprint, baseY() + shelf, baseY() - 1);
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) {
@@ -190,7 +262,13 @@ public final class SubterrainPlan {
if (Math.abs(u) > halfLength + 2 || Math.abs(offset) > width + 2) {
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 ceiling = centerY + Math.max(2, (int) Math.floor(definition.height() * 0.5 * arch));
int head = baseY() + definition.fluidDepth();
@@ -209,48 +287,100 @@ public final class SubterrainPlan {
}
}
boolean rim = definition.fluidDepth() > 0 && floor >= head && floor < head + 2;
return new Column(floor, ceiling, head, Math.abs(u) < halfLength && Math.abs(offset) < width,
rim, Math.min(halfLength - Math.abs(u), width - Math.abs(offset)), Math.abs(offset), path);
return new Column(floor, ceiling, head, Math.abs(u) < halfLength && Math.abs(offset) < taperedWidth,
rim, Math.min(halfLength - Math.abs(u), taperedWidth - Math.abs(offset)), Math.abs(offset), path);
}
private Column terraces(int u, int v) {
int halfLength = definition.length() / 2;
int radius = definition.radius();
if (Math.abs(u) > halfLength + 2 || Math.abs(v) > radius + 2) {
Footprint footprint = footprint(u, v);
if (footprint == null) {
return null;
}
int segment = Math.max(4, definition.length() / definition.terraceCount());
int index = Math.max(0, Math.min(definition.terraceCount() - 1, Math.floorDiv(u + halfLength, segment)));
double warped = u + definition.radius() * 0.2 * Math.sin(v / (double) Math.max(6, definition.radius()) * 2.4 + shapePhase)
+ 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 floor = baseY() + index * rise;
int head = floor + definition.fluidDepth();
int local = Math.floorMod(u + halfLength, segment);
boolean rim = definition.fluidDepth() > 0 && (local <= 1 || local >= segment - 2 || Math.abs(v) >= radius - 2);
int ceiling = baseY() + definition.height();
if (rim) {
floor = head + 1;
}
double pathDistance = Math.abs(Math.abs(v) - (radius - 4));
if (pathDistance <= 2) {
double local = (terrace - Math.floor(terrace)) * segment;
boolean rim = definition.fluidDepth() > 0 && (local <= 1.5 || local >= segment - 1.5 || footprint.boundary() <= 2);
double pathOffset = Math.copySign(Math.max(0, footprint.width() - 4), footprint.offset());
double pathDistance = Math.abs(footprint.offset() - pathOffset);
if (rim || pathDistance <= 2) {
floor = Math.max(floor, head + 1);
}
return new Column(floor, ceiling, head, Math.abs(u) < halfLength && Math.abs(v) < radius,
false, Math.min(halfLength - Math.abs(u), radius - Math.abs(v)), pathDistance,
v < 0 ? -radius + 4 : radius - 4);
int ceiling = vaultedCeiling(u, footprint, floor, head);
return new Column(floor, ceiling, head, footprint.boundary() > 0,
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) {
if (sample(x, preferredY, z).occupied()) {
if (anchorCell(sample(x, preferredY, z))) {
return new SubterrainPosition(x, preferredY, z);
}
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 null;
}
private boolean anchorCell(SubterrainCell cell) {
return cell.occupied() && (family() != IrisSubterrainFamily.TRAVERTINE_TERRACES || cell.room().reservedPassage());
}
private int baseY() {
return centerY - definition.height() / 2;
}
@@ -259,6 +389,23 @@ public final class SubterrainPlan {
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,
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.GenerationSessionLease;
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.PlacedObject;
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) {
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) {
@@ -625,12 +628,14 @@ public final class EngineBukkitOps {
}
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) {
ComponentMessenger.sendSection(player, IrisLanguage.text(BukkitRuntimeMessages.ENGINE_BUKKIT_OPS_COULD_NOT_FIND_WITHIN_SEARCH_RANGE, MessageArgument.untrusted("message", String.valueOf(message))));
return;
}
if (teleport) {
if (teleport && locator instanceof BiomeLocator) {
teleportToCave(player, location, message);
} else if (teleport) {
J.runEntity(player, () -> teleportAsyncSafely(player, location));
ComponentMessenger.sendSection(player, IrisLanguage.text(BukkitRuntimeMessages.ENGINE_BUKKIT_OPS_TELEPORTING, MessageArgument.untrusted("message", String.valueOf(message))));
} 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();
long ms = M.ms();
World world = player.getWorld();
@@ -667,8 +672,15 @@ public final class EngineBukkitOps {
try (GenerationSessionLease lease = engine.acquireGenerationLease("bukkit_locator_result");
IrisContext.Scope ignored = IrisContext.open(engine, lease.sessionId(), null)) {
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(
player, world, consumer, new Location(world, bx, by, bz), playerId, search);
player, world, consumer, destination, playerId, search);
}
} else {
resultDispatched = dispatchLocateResult(player, world, consumer, null, playerId, search);
@@ -678,6 +690,14 @@ public final class EngineBukkitOps {
Thread.currentThread().interrupt();
} catch (WrongEngineBroException | ExecutionException 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 {
if (search != null && !resultDispatched) {
ACTIVE_LOCATE_REQUESTS.remove(playerId, search);
@@ -704,6 +724,43 @@ public final class EngineBukkitOps {
}.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,
Location location, UUID playerId,
CompletableFuture<Position2> search) {
@@ -722,6 +779,30 @@ public final class EngineBukkitOps {
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) {
if (player == null || location == null) {
return;
@@ -30,6 +30,14 @@ public final class GenerationFindCatalog {
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) {
return merge(engine.getDimension().getAllRegions(engine), retained(engine).regions);
}
@@ -96,9 +104,11 @@ public final class GenerationFindCatalog {
return;
}
for (String key : semantics.surfaceBiomeKeys()) {
catalog.surfaceBiomes.add(normalizeKey(key));
addBiome(catalog, key);
}
for (String key : semantics.caveBiomeKeys()) {
catalog.caveBiomes.add(normalizeKey(key));
addBiome(catalog, key);
}
for (String key : semantics.regionKeys()) {
@@ -181,6 +191,8 @@ public final class GenerationFindCatalog {
private static final class RetainedCatalog {
private final long activeActivationId;
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 Set<String> objects = new LinkedHashSet<>();
private final Set<String> structures = new LinkedHashSet<>();
@@ -26,6 +26,7 @@ import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.function.Predicate;
public final class GenerationHistory {
private static final int MAXIMUM_CACHED_BOUNDARIES = 4;
@@ -848,6 +849,12 @@ public final class GenerationHistory {
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(
GenerationSemanticIndex.RiverQuery query
) {
@@ -18,6 +18,7 @@ import java.util.Optional;
import java.util.OptionalLong;
import java.util.function.IntConsumer;
import java.util.function.Predicate;
import java.util.function.BooleanSupplier;
public final class GenerationSemanticQueries {
private GenerationSemanticQueries() {
@@ -25,12 +26,25 @@ public final class GenerationSemanticQueries {
public static Optional<SubterrainLocator.Result> nearestSubterrain(
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");
Objects.requireNonNull(query, "query");
GenerationHistoryRuntimeRouter router = requiredEngine instanceof IrisEngine irisEngine
? irisEngine.getGenerationHistoryRuntimeRouter().orElse(null) : 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();
SubterrainLocator.Result nearest = null;
@@ -43,7 +57,7 @@ public final class GenerationSemanticQueries {
SubterrainPlanner planner = router.biomes().subterrainPlanner(activation.activationId());
SubterrainLocator.Result candidate = SubterrainLocator.nearest(planner, query, x, worldY, z,
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)
.compareTo(distanceSquared(x, worldY, z, nearest)) < 0)) {
nearest = candidate;
@@ -55,6 +69,24 @@ public final class GenerationSemanticQueries {
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,
long activationId, SubterrainLocator.Result result) {
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) {
if (engine.getData().hasBlockDropRules(material)) {
return true;
@@ -573,6 +582,32 @@ public final class SavedBiomeRuntime implements AutoCloseable {
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,
Map<SavedBiomeChunk.Cell, BiomeEnvironment> environments,
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.IrisSubterrainFeature;
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.pack.value.IrisRange;
import org.junit.Test;
@@ -20,6 +21,31 @@ import static org.mockito.Mockito.verifyNoMoreInteractions;
import static org.mockito.Mockito.when;
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
public void wideDistanceSearchReturnsOwnedAbsolutePositionWithoutGenerating() {
SubterrainPlanner planner = planner();
@@ -50,6 +76,12 @@ public class SubterrainLocatorTest {
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
public void repeatedSearchAndLargeNegativeCoordinatesAreStable() {
Engine engine = engine(planner());
@@ -8,6 +8,8 @@ import org.junit.Test;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Set;
import java.util.HashSet;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
@@ -23,9 +25,6 @@ public class SubterrainPlannerTest {
for (IrisSubterrainFluid fluid : IrisSubterrainFluid.values()) {
IrisSubterrainFeature feature = compactFeature(IrisSubterrainFamily.TRAVERTINE_TERRACES).setFluid(fluid);
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();
assertTrue(pillar.sample(pillarAnchor.x(), pillarAnchor.y(), pillarAnchor.z()).occupied());
assertTrue(pillar.sample(pillarAnchor.x(), pillarAnchor.y(), pillarAnchor.z()).room().reservedPassage());
@@ -173,17 +172,81 @@ public class SubterrainPlannerTest {
}
@Test
public void faultWallsRemainParallelBeyondTwoHundredBlocksAndHaveShelves() {
SubterrainPlan plan = direct(feature(IrisSubterrainFamily.TECTONIC_FAULT));
for (int x = -110; x <= 110; x++) {
assertTrue(plan.sample(x, 0, 0).occupied());
assertTrue(plan.sample(x, 0, 32).solid());
assertTrue(plan.sample(x, 0, -32).solid());
public void faultsAndTerracesHaveTaperedIrregularWallsAndVaultedCeilings() {
for (IrisSubterrainFamily family : List.of(IrisSubterrainFamily.TECTONIC_FAULT,
IrisSubterrainFamily.TRAVERTINE_TERRACES)) {
SubterrainPlan plan = direct(feature(family));
Set<Integer> widths = new HashSet<>();
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());
}
int centralFloor = plan.sample(0, 0, 0).room().floorY();
int ledgeFloor = plan.sample(0, 0, 28).room().floorY();
assertTrue(ledgeFloor > centralFloor);
assertTrue(plan.sample(0, ledgeFloor, 28).solid());
}
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());
}
}
@Test
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
@@ -217,9 +280,17 @@ public class SubterrainPlannerTest {
assertTrue(plan.sample(90, 0, 0).room().floorY() > plan.sample(-90, 0, 0).room().floorY());
SubterrainPlanner planner = planner(feature(IrisSubterrainFamily.TRAVERTINE_TERRACES));
assertTrue(assertRetained(planner, List.of(plan), SubterrainCell.Kind.WATER) > 1000);
int rimX = -128 + 42;
assertTrue(plan.sample(rimX, -10, 0).solid());
assertTrue(plan.sample(0, 0, 28).room().floorY() > plan.sample(0, 0, 28).room().fluidHeadY());
Set<Integer> rimLocations = new HashSet<>();
for (int z = -16; z <= 16; z += 4) {
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
@@ -227,8 +298,21 @@ public class SubterrainPlannerTest {
IrisSubterrainFeature feature = feature(IrisSubterrainFamily.CENOTE).setPillarSpacing(12).setFormationFraction(0.3);
SubterrainPlan plan = direct(feature);
SubterrainRoom room = plan.sample(12, 0, 12).room();
for (int y = room.floorY(); y <= room.ceilingY(); y++) {
assertTrue("pillar y=" + y, plan.sample(12, y, 12).solid());
Set<SubterrainPosition> previous = new HashSet<>();
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());
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;
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.generation.runtime.IrisEngine;
import art.arcane.iris.generation.biome.IrisBiome;
@@ -31,6 +34,34 @@ import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
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
public void findsRecordedNamesWithoutLoadingArchivedPacksOrRuntimes() throws Exception {
Fixture fixture = new Fixture();
@@ -26,6 +26,7 @@ import java.time.Duration;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
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
public void backgroundPreviewWaitsForTheReadWithoutRepeatingTheQuery() throws Exception {
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.SubterrainPlanner;
import art.arcane.iris.generation.subterrain.SubterrainPosition;
import art.arcane.iris.pack.value.IrisRange;
import org.junit.Test;
import java.util.List;
@@ -21,6 +22,39 @@ import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
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
public void locateUsesRetainedDefinitionOnlyInsideItsOwnedChunks() throws Exception {
IrisEngine engine = mock(IrisEngine.class);