mirror of
https://github.com/VolmitSoftware/Iris.git
synced 2026-10-27 22:40:13 +00:00
Cleanup
This commit is contained in:
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,
|
||||
|
||||
Reference in new issue
Block a user