mirror of
https://github.com/VolmitSoftware/Iris.git
synced 2026-10-11 19:14:11 +00:00
Cleanup
This commit is contained in:
@@ -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,
|
||||
|
||||
+92
-7
@@ -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) {
|
||||
}
|
||||
|
||||
+93
@@ -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;
|
||||
anchor = occupiedAnchor(rotated ? centerX + path : centerX, y,
|
||||
rotated ? centerZ : centerZ + path);
|
||||
if (anchor != null) {
|
||||
return anchor;
|
||||
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) {
|
||||
return true;
|
||||
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());
|
||||
|
||||
+102
-18
@@ -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());
|
||||
}
|
||||
}
|
||||
widths.add(width);
|
||||
}
|
||||
assertTrue(family.toString(), widths.size() > 4);
|
||||
assertTrue(family.toString(), ceilings.size() > 5);
|
||||
assertFalse(plan.sample(120, 0, 28).occupied());
|
||||
assertFalse(plan.sample(-120, 0, -28).occupied());
|
||||
assertTrue(plan.sample(0, 0, 0).room().ceilingY() > plan.sample(0, 0, 20).room().ceilingY());
|
||||
}
|
||||
int centralFloor = plan.sample(0, 0, 0).room().floorY();
|
||||
int ledgeFloor = plan.sample(0, 0, 28).room().floorY();
|
||||
assertTrue(ledgeFloor > centralFloor);
|
||||
assertTrue(plan.sample(0, ledgeFloor, 28).solid());
|
||||
}
|
||||
|
||||
@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());
|
||||
|
||||
+80
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user