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

No files matched your search

@@ -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,