d
This commit is contained in:
Brian Neumann-Fopiano
2026-08-23 13:56:23 -04:00
parent dce7af1955
commit fe5651f854
216 changed files with 21919 additions and 1865 deletions
@@ -61,6 +61,7 @@ public class CustomBiomeSource extends BiomeSource {
private final ConcurrentHashMap<Long, Holder<Biome>> noiseBiomeCache = new ConcurrentHashMap<>();
private final ConcurrentHashMap<Long, Holder<Biome>> structureBiomeCache = new ConcurrentHashMap<>();
private final ConcurrentHashMap<Long, Holder<Biome>> surfaceStructureBiomeCache = new ConcurrentHashMap<>();
private final ConcurrentHashMap<Long, Holder<Biome>> naturalSurfaceStructureBiomeCache = new ConcurrentHashMap<>();
private volatile KMap<String, Holder<Biome>> customBiomes;
private volatile Map<Biome, Holder<Biome>> vanillaSpawnBiomes;
private volatile IrisDimension cacheDimension;
@@ -330,8 +331,18 @@ public class CustomBiomeSource extends BiomeSource {
if (quartStep == 1) {
return super.findBiomeHorizontal(x, y, z, searchRadius, allowed, random, sampler);
}
return super.findBiomeHorizontal(
x, y, z, searchRadius, quartStep, allowed, random, false, sampler);
GenerationSessionLease lease = tryAcquireGenerationLease("bukkit_structure_ring_biome");
if (lease == null) {
throw new IllegalStateException("Iris structure ring biome lookup was rejected during an engine transition");
}
try (lease; IrisContext.Scope ignored = IrisContext.open(engine, lease.sessionId(), null)) {
if (!isRuntimeAvailable()) {
throw new IllegalStateException("Iris structure ring biome lookup has no active engine runtime");
}
ensureCachesCurrent();
return findNaturalSurfaceBiomeHorizontal(
x, y, z, searchRadius, quartStep, allowed, random);
}
}
static int horizontalBiomeSearchQuartStep(int blockY, int searchRadius) {
@@ -389,6 +400,60 @@ public class CustomBiomeSource extends BiomeSource {
return resolvedSurfaceHolder;
}
private Pair<BlockPos, Holder<Biome>> findNaturalSurfaceBiomeHorizontal(
int x,
int y,
int z,
int searchRadius,
int quartStep,
Predicate<Holder<Biome>> allowed,
RandomSource random
) {
int centerQuartX = QuartPos.fromBlock(x);
int centerQuartZ = QuartPos.fromBlock(z);
int quartRadius = QuartPos.fromBlock(searchRadius);
Pair<BlockPos, Holder<Biome>> selected = null;
int matches = 0;
for (int radius = 0; radius <= quartRadius; radius += quartStep) {
for (int offsetZ = -radius; offsetZ <= radius; offsetZ += quartStep) {
for (int offsetX = -radius; offsetX <= radius; offsetX += quartStep) {
int quartX = centerQuartX + offsetX;
int quartZ = centerQuartZ + offsetZ;
Holder<Biome> holder = getNaturalSurfaceStructureBiomeHolder(quartX, quartZ);
if (!allowed.test(holder)) {
continue;
}
if (selected == null || random.nextInt(matches + 1) == 0) {
selected = Pair.of(new BlockPos(
QuartPos.toBlock(quartX),
y,
QuartPos.toBlock(quartZ)), holder);
}
matches++;
}
}
}
return selected;
}
private Holder<Biome> getNaturalSurfaceStructureBiomeHolder(int x, int z) {
long columnKey = packColumnKey(x, z);
Holder<Biome> cachedHolder = naturalSurfaceStructureBiomeCache.get(columnKey);
if (cachedHolder != null) {
return cachedHolder;
}
Holder<Biome> resolvedHolder = resolveNaturalSurfaceStructureBiomeHolder(x, z);
Holder<Biome> existingHolder = naturalSurfaceStructureBiomeCache.putIfAbsent(
columnKey, resolvedHolder);
if (existingHolder != null) {
return existingHolder;
}
if (naturalSurfaceStructureBiomeCache.size() > NOISE_BIOME_CACHE_MAX) {
naturalSurfaceStructureBiomeCache.clear();
}
return resolvedHolder;
}
private boolean isGuaranteedSurfaceBiome(int quartY) {
if (engine == null || engine.isClosed() || engine.getComplex() == null) {
return false;
@@ -416,36 +481,66 @@ public class CustomBiomeSource extends BiomeSource {
return holder;
}
private Holder<Biome> resolveNaturalSurfaceStructureBiomeHolder(int x, int z) {
int blockX = x << 2;
int blockZ = z << 2;
IrisBiome irisBiome = engine.getComplex().getNaturalTrueBiomeStream().get(blockX, blockZ);
if (irisBiome == null) {
throw new IllegalStateException("Iris returned no natural structure biome at block "
+ blockX + "," + blockZ);
}
Holder<Biome> holder = resolveBiomeHolder(biomeRegistry, irisBiome.getStructureDerivativeKey());
if (holder == null) {
throw new IllegalStateException("Iris natural structure biome derivative '"
+ irisBiome.getStructureDerivativeKey() + "' is not registered at block "
+ blockX + "," + blockZ);
}
return holder;
}
public Holder<Biome> getVisibleNoiseBiome(int x, int y, int z, Climate.Sampler sampler) {
GenerationSessionLease lease = tryAcquireGenerationLease("bukkit_visible_biome");
if (lease == null) {
throw new IllegalStateException("Iris visible biome lookup was rejected during an engine transition");
}
try (lease; IrisContext.Scope ignored = IrisContext.open(engine, lease.sessionId(), null)) {
if (!isRuntimeAvailable()) {
throw new IllegalStateException("Iris visible biome lookup has no active engine runtime");
}
ensureCachesCurrent();
long cacheKey = packNoiseKey(x, y, z);
Holder<Biome> cachedHolder = noiseBiomeCache.get(cacheKey);
if (cachedHolder != null) {
return cachedHolder;
}
Holder<Biome> resolvedHolder = resolveVisibleBiomeHolder(x, y, z);
Holder<Biome> existingHolder = noiseBiomeCache.putIfAbsent(cacheKey, resolvedHolder);
if (existingHolder != null) {
return existingHolder;
}
if (noiseBiomeCache.size() > NOISE_BIOME_CACHE_MAX) {
noiseBiomeCache.clear();
}
return resolvedHolder;
prepareVisibleBiomeBatch();
return getVisibleNoiseBiomeWithActiveGenerationLease(x, y, z, sampler);
}
}
void prepareVisibleBiomeBatch() {
if (!isRuntimeAvailable()) {
throw new IllegalStateException("Iris visible biome lookup has no active engine runtime");
}
ensureCachesCurrent();
}
Holder<Biome> getVisibleNoiseBiomeWithActiveGenerationLease(
int x,
int y,
int z,
Climate.Sampler sampler
) {
long cacheKey = packNoiseKey(x, y, z);
Holder<Biome> cachedHolder = noiseBiomeCache.get(cacheKey);
if (cachedHolder != null) {
return cachedHolder;
}
Holder<Biome> resolvedHolder = resolveVisibleBiomeHolder(x, y, z);
Holder<Biome> existingHolder = noiseBiomeCache.putIfAbsent(cacheKey, resolvedHolder);
if (existingHolder != null) {
return existingHolder;
}
if (noiseBiomeCache.size() > NOISE_BIOME_CACHE_MAX) {
noiseBiomeCache.clear();
}
return resolvedHolder;
}
private GenerationSessionLease tryAcquireGenerationLease(String operation) {
if (engine.isClosed()) {
return null;
@@ -482,6 +577,7 @@ public class CustomBiomeSource extends BiomeSource {
noiseBiomeCache.clear();
structureBiomeCache.clear();
surfaceStructureBiomeCache.clear();
naturalSurfaceStructureBiomeCache.clear();
customBiomes = refreshedCustomBiomes;
vanillaSpawnBiomes = refreshedSpawnBiomes;
cacheDimension = dimension;
@@ -573,11 +669,20 @@ public class CustomBiomeSource extends BiomeSource {
return null;
}
return createBiomeResolution(irisBiome, underground, blockX, blockY, blockZ);
}
private BiomeResolution createBiomeResolution(
IrisBiome irisBiome,
boolean underground,
int blockX,
int blockY,
int blockZ
) {
RNG noiseRng = new RNG(seed
^ (((long) blockX) * 341873128712L)
^ (((long) blockY) * 132897987541L)
^ (((long) blockZ) * 42317861L));
return new BiomeResolution(irisBiome, underground, blockX, blockY, blockZ, noiseRng);
}
@@ -642,7 +642,10 @@ public class IrisChunkGenerator extends CustomChunkGenerator {
try (BukkitChunkGenerator.GenerationStagePermit stage = requireGenerationStage("bukkit_nms_create_biomes");
GenerationSessionLease lease = requireGenerationLease("bukkit_nms_create_biomes");
IrisContext.Scope ignored = IrisContext.open(engine, lease.sessionId(), null)) {
ichunkaccess.fillBiomesFromNoise(customBiomeSource::getVisibleNoiseBiome, randomstate.sampler());
customBiomeSource.prepareVisibleBiomeBatch();
ichunkaccess.fillBiomesFromNoise(
customBiomeSource::getVisibleNoiseBiomeWithActiveGenerationLease,
randomstate.sampler());
return CompletableFuture.completedFuture(ichunkaccess);
}
}
@@ -168,6 +168,7 @@ import java.util.Map;
import java.util.Optional;
import java.util.List;
import java.util.Set;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.Executor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
@@ -1295,16 +1296,16 @@ public class NMSBinding implements INMSBinding {
}
@Override
public void completeStudioStructureBootstrap(World world) throws NoSuchFieldException, IllegalAccessException {
public CompletableFuture<Void> completeStudioStructureBootstrap(World world) throws NoSuchFieldException, IllegalAccessException {
ServerLevel level = ((CraftWorld) world).getHandle();
ChunkMap chunkMap = level.getChunkSource().chunkMap;
IrisChunkGenerator generator = requireIrisGenerator(level.getChunkSource().getGenerator());
IrisChunkGenerator.StudioStructureState retained =
generator.retainedStudioStructureState(level, chunkMap);
if (retained == null) {
return;
return CompletableFuture.completedFuture(null);
}
generator.activateStudioStructureState(retained);
return generator.activateStudioStructureState(retained);
}
@Override
@@ -45,8 +45,53 @@ public class CustomBiomeSourceStructureContractTest {
assertTrue(source.contains("private static final int STRONGHOLD_RING_SEARCH_Y = 0"));
assertTrue(source.contains("private static final int STRONGHOLD_RING_SEARCH_RADIUS = 112"));
assertTrue(source.contains("private static final int STRONGHOLD_RING_SEARCH_QUART_STEP = 4"));
assertTrue(source.contains("x, y, z, searchRadius, quartStep, allowed, random, false, sampler"));
assertTrue(source.contains(
"return super.findBiomeHorizontal(x, y, z, searchRadius, allowed, random, sampler)"));
assertTrue(source.contains("tryAcquireGenerationLease(\"bukkit_structure_ring_biome\")"));
assertTrue(source.contains("findNaturalSurfaceBiomeHorizontal("));
assertTrue(source.contains("radius += quartStep"));
assertTrue(source.contains("offsetZ += quartStep"));
assertTrue(source.contains("offsetX += quartStep"));
assertTrue(source.contains("random.nextInt(matches + 1) == 0"));
}
@Test
public void onlyConcentricRingSuitabilityUsesTheNaturalTerrainStream() throws IOException {
String source = Files.readString(Path.of(System.getProperty("iris.customBiomeSource")));
int realResolutionStart = source.indexOf("private Holder<Biome> resolveSurfaceStructureBiomeHolder(");
int naturalResolutionStart = source.indexOf(
"private Holder<Biome> resolveNaturalSurfaceStructureBiomeHolder(");
int resolutionEnd = source.indexOf("public Holder<Biome> getVisibleNoiseBiome(", naturalResolutionStart);
assertTrue(realResolutionStart >= 0);
assertTrue(naturalResolutionStart > realResolutionStart);
assertTrue(resolutionEnd > naturalResolutionStart);
String realResolution = source.substring(realResolutionStart, naturalResolutionStart);
String naturalResolution = source.substring(naturalResolutionStart, resolutionEnd);
assertTrue(realResolution.contains("engine.getComplex().getTrueBiomeStream().get(blockX, blockZ)"));
assertFalse(realResolution.contains("getNaturalTrueBiomeStream()"));
assertTrue(naturalResolution.contains(
"engine.getComplex().getNaturalTrueBiomeStream().get(blockX, blockZ)"));
assertFalse(naturalResolution.contains("getTrueBiomeStream()"));
assertFalse(source.contains("studioBootstrapSurfaceStructureBiomeCache"));
}
@Test
public void visibleBiomeBatchReusesTheCallersGenerationLease() throws IOException {
String source = Files.readString(Path.of(System.getProperty("iris.customBiomeSource")));
int lookupStart = source.indexOf("public Holder<Biome> getVisibleNoiseBiome(");
int lookupEnd = source.indexOf("private GenerationSessionLease tryAcquireGenerationLease(", lookupStart);
assertTrue(lookupStart >= 0);
assertTrue(lookupEnd > lookupStart);
String lookup = source.substring(lookupStart, lookupEnd);
assertTrue(lookup.contains("tryAcquireGenerationLease(\"bukkit_visible_biome\")"));
assertTrue(lookup.contains("prepareVisibleBiomeBatch()"));
assertTrue(lookup.contains("getVisibleNoiseBiomeWithActiveGenerationLease(x, y, z, sampler)"));
assertTrue(lookup.indexOf("prepareVisibleBiomeBatch()")
< lookup.indexOf("getVisibleNoiseBiomeWithActiveGenerationLease(x, y, z, sampler)"));
assertFalse(source.contains("studioBootstrapNoiseBiomeCache"));
}
}
@@ -219,30 +219,89 @@ public class IrisChunkGeneratorFailureContractTest {
assertTrue(source.contains("int minY = chunk.getMinY() + 1;"));
}
@Test
public void everyChunkUsesTheProductionGenerationStages() throws IOException {
String source = Files.readString(Path.of(System.getProperty("iris.nmsChunkGeneratorSource")))
.replace("\r\n", "\n");
String createBiomes = method(
source,
"public CompletableFuture<ChunkAccess> createBiomes",
"public void buildSurface");
String buildSurface = method(
source,
"public void buildSurface",
"public void applyCarvers");
String carvers = method(
source,
"public void applyCarvers",
"public CompletableFuture<ChunkAccess> fillFromNoise");
String noise = method(
source,
"public CompletableFuture<ChunkAccess> fillFromNoise",
"private static boolean isCancellationFailure");
String decoration = method(
source,
"public void applyBiomeDecoration(WorldGenLevel generatoraccessseed, ChunkAccess ichunkaccess, StructureManager structuremanager, boolean vanilla)",
"public BiomeGenerationSettings getBiomeGenerationSettings");
String spawning = method(
source,
"public void spawnOriginalMobs",
"private static WeightedList<MobSpawnSettings.SpawnerData> mergeSpawnTables");
String baseHeight = method(
source,
"public int getBaseHeight",
"public NoiseColumn getBaseColumn");
String baseColumn = method(
source,
"public NoiseColumn getBaseColumn",
"private GenerationSessionLease requireGenerationLease");
assertTrue(createBiomes.contains("requireGenerationStage(\"bukkit_nms_create_biomes\")"));
assertTrue(createBiomes.contains("customBiomeSource.prepareVisibleBiomeBatch()"));
assertTrue(createBiomes.contains("customBiomeSource::getVisibleNoiseBiomeWithActiveGenerationLease"));
assertTrue(buildSurface.contains("delegate.buildSurface("));
assertTrue(carvers.contains("delegate.applyCarvers("));
assertTrue(noise.contains("requireNoiseGenerationStage("));
assertTrue(noise.contains("\"bukkit_nms_chunk_pipeline\""));
assertTrue(noise.contains("stage.close()"));
assertTrue(noise.contains("completion.cancel(false)"));
assertTrue(decoration.contains("requireGenerationStage(\"bukkit_nms_biome_decoration\")"));
assertTrue(spawning.contains("NaturalSpawner.spawnMobsForChunkGeneration("));
assertTrue(baseHeight.contains("engine.getHeight("));
assertTrue(baseColumn.contains("engine.getHeight("));
assertFalse(source.contains("synthetic_entry"));
assertFalse(source.contains("StudioEntryChunkGenerator"));
}
@Test
public void fillFromNoiseLeaseSpansTheDelegateAndHeightmapPipeline() throws IOException {
String source = Files.readString(Path.of(System.getProperty("iris.nmsChunkGeneratorSource"))).replace("\r\n", "\n");
int fillStart = source.indexOf("public CompletableFuture<ChunkAccess> fillFromNoise");
int fillEnd = source.indexOf("private static boolean isCancellationFailure", fillStart);
String fill = source.substring(fillStart, fillEnd);
int completionStart = fill.indexOf("pipeline.whenComplete(");
int completionEnd = fill.indexOf(" return completion;", completionStart);
int productionStart = fill.indexOf(
" BukkitChunkGenerator.GenerationStagePermit stage = requireNoiseGenerationStage(",
0);
assertTrue(productionStart >= 0);
String productionFill = fill.substring(productionStart);
int completionStart = productionFill.indexOf("pipeline.whenComplete(");
int completionEnd = productionFill.indexOf(" return completion;", completionStart);
assertTrue(completionStart >= 0);
assertTrue(completionEnd > completionStart);
String completion = fill.substring(completionStart, completionEnd);
String completion = productionFill.substring(completionStart, completionEnd);
assertBefore(fill,
assertBefore(productionFill,
"BukkitChunkGenerator.GenerationStagePermit stage = requireNoiseGenerationStage(",
"GenerationSessionLease lease");
assertBefore(fill,
assertBefore(productionFill,
"requireNoiseGenerationStage(",
"\"bukkit_nms_chunk_pipeline\")");
assertBefore(fill,
assertBefore(productionFill,
"lease = requireGenerationLease(\"bukkit_nms_chunk_pipeline\")",
".fillFromNoise(blender, randomstate, structuremanager, ichunkaccess)");
assertTrue(fill.contains("IrisContext.open(engine, lease.sessionId(), null)"));
assertBefore(fill, "primeWorldgenHeightmaps(filled)", "pipeline.whenComplete(");
assertTrue(fill.contains("CompletableFuture<ChunkAccess> completion = new CompletableFuture<>()"));
assertTrue(productionFill.contains("IrisContext.open(engine, lease.sessionId(), null)"));
assertBefore(productionFill, "primeWorldgenHeightmaps(filled)", "pipeline.whenComplete(");
assertTrue(productionFill.contains("CompletableFuture<ChunkAccess> completion = new CompletableFuture<>()"));
assertBefore(completion, "boolean cancelled = isCancellationFailure(failure);", "lease.close();");
assertBefore(completion, "lease.close();", "stage.close();");
assertBefore(completion, "stage.close();", "completion.complete(filled)");
@@ -251,12 +310,12 @@ public class IrisChunkGeneratorFailureContractTest {
assertTrue(completion.contains("else if (cancelled)"));
assertFalse(completion.contains("pipeline.isCancelled()"));
assertFalse(completion.contains("finally"));
assertTrue(fill.contains("catch (RuntimeException | Error failure)"));
assertTrue(fill.contains("lease.close();\n stage.close();\n throw failure;"));
assertTrue(fill.contains("return completion;"));
assertFalse(fill.contains("return pipeline;"));
assertFalse(fill.contains("pipeline.cancel("));
assertFalse(fill.contains("bukkit_nms_worldgen_heightmaps"));
assertTrue(productionFill.contains("catch (RuntimeException | Error failure)"));
assertTrue(productionFill.contains("lease.close();\n stage.close();\n throw failure;"));
assertTrue(productionFill.contains("return completion;"));
assertFalse(productionFill.contains("return pipeline;"));
assertFalse(productionFill.contains("pipeline.cancel("));
assertFalse(productionFill.contains("bukkit_nms_worldgen_heightmaps"));
}
@Test
@@ -356,7 +356,9 @@ public class NMSBindingDatapackStructureScopeTest {
Path chunkGeneratorSource = Path.of(System.getProperty("iris.nmsChunkGeneratorSource"));
String source = Files.readString(chunkGeneratorSource.resolveSibling("NMSBinding.java")).replace("\r\n", "\n");
int methodStart = source.indexOf("public DatapackStructureScopeResult scopeDatapackStructures(");
int methodEnd = source.indexOf("\n @Override\n public void completeStudioStructureBootstrap", methodStart);
int methodEnd = source.indexOf(
"\n @Override\n public CompletableFuture<Void> completeStudioStructureBootstrap",
methodStart);
assertTrue(methodStart >= 0);
assertTrue(methodEnd > methodStart);
@@ -390,19 +392,21 @@ public class NMSBindingDatapackStructureScopeTest {
}
@Test
public void standardCompletionActivatesTheAlreadyPublishedStateWithoutReplacingIt() throws IOException {
public void standardCompletionReturnsTheExactActivationFutureWithoutReplacingState() throws IOException {
Path chunkGeneratorSource = Path.of(System.getProperty("iris.nmsChunkGeneratorSource"));
String source = Files.readString(chunkGeneratorSource.resolveSibling("NMSBinding.java")).replace("\r\n", "\n");
int methodStart = source.indexOf("public void completeStudioStructureBootstrap(World world)");
int methodStart = source.indexOf("public CompletableFuture<Void> completeStudioStructureBootstrap(World world)");
int methodEnd = source.indexOf("\n @Override\n public void abandonStudioStructureBootstrap", methodStart);
assertTrue(methodStart >= 0);
assertTrue(methodEnd > methodStart);
String method = source.substring(methodStart, methodEnd);
int retained = method.indexOf("generator.retainedStudioStructureState(level, chunkMap)");
int activation = method.indexOf("generator.activateStudioStructureState(retained);");
int emptyCompletion = method.indexOf("return CompletableFuture.completedFuture(null);");
int activation = method.indexOf("return generator.activateStudioStructureState(retained);");
assertTrue(retained >= 0);
assertTrue(emptyCompletion > retained);
assertTrue(activation > retained);
assertFalse(method.contains("stateField.set("));
assertFalse(method.contains("retained.fullState()"));
@@ -184,6 +184,7 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
private final AtomicBoolean serverStopTeardownDeferred = new AtomicBoolean(false);
private final AtomicBoolean servicesDisabled = new AtomicBoolean(false);
private final AtomicBoolean sharedRuntimeClosed = new AtomicBoolean(false);
private final AtomicBoolean startupBoundaryRestart = new AtomicBoolean(false);
private final AtomicBoolean terminalCleanupCompleted = new AtomicBoolean(false);
private volatile PlaceholderRegistration papiRegistration;
private volatile IrisPapiListener papiListener;
@@ -588,6 +589,7 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
serverStopTeardownDeferred.set(false);
servicesDisabled.set(false);
sharedRuntimeClosed.set(false);
startupBoundaryRestart.set(false);
terminalCleanupCompleted.set(false);
deferredShutdownGenerators.clear();
MultiBurst.burst.reopen();
@@ -827,6 +829,12 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
BukkitGuiHost.install();
// super.onEnable() already registers this instance as a listener.
super.onEnable();
if (IrisStartupValidation.isRestartRequired()) {
String restartReason = IrisStartupValidation.denialReason()
.orElse("Iris startup validation requires a restart.");
startupBoundaryRestart.set(true);
ServerConfigurator.restartAtStartupBoundary(restartReason);
}
}
/**
@@ -863,7 +871,10 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
public void onDisable() {
teardownPapi();
boolean serverStopping = IrisToolbelt.isServerStopping();
if (serverStopping) {
boolean restartingAtStartupBoundary = startupBoundaryRestart.get();
if (restartingAtStartupBoundary) {
teardownRuntime("startup-boundary-restart", 30L);
} else if (serverStopping) {
quiesceRuntimeForServerShutdown("onDisable");
startPostStopFinisher();
} else {
@@ -881,7 +892,7 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
}
// super.onDisable() cancels plugin tasks and unregisters every listener.
super.onDisable();
if (!serverStopping) {
if (!serverStopping || restartingAtStartupBoundary) {
finishTerminalCleanup();
}
}
@@ -1017,6 +1028,10 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
}
private void runShutdownHook() {
if (startupBoundaryRestart.get()) {
finishDeferredRuntimeTeardown("startup-boundary-restart-hook", 30L);
return;
}
if (!awaitServerShutdownBoundary()) {
Iris.warn("Iris skipped JVM-hook runtime teardown because Paper did not reach its post-world-close boundary.");
return;
@@ -2,6 +2,11 @@ package art.arcane.iris.api.terrain;
public enum IrisColumnField {
SURFACE_HEIGHT,
NATURAL_HEIGHT,
SURFACE_KIND,
BIOME_KEY
BIOME_KEY,
RIVER_STATE,
RIVER_DISTANCE,
RIVER_FLOW,
RIVER_WATER_SURFACE_Y
}
@@ -0,0 +1,64 @@
package art.arcane.iris.api.terrain;
import java.util.Objects;
public record IrisColumnSample(
int blockX,
int blockZ,
int surfaceHeight,
int naturalHeight,
IrisSurfaceKind surfaceKind,
String biomeKey,
IrisRiverState riverState,
double riverDistance,
int riverFlow,
int riverWaterSurfaceY
) {
public static final int UNAVAILABLE_HEIGHT = Integer.MIN_VALUE;
public static final double UNAVAILABLE_RIVER_DISTANCE = Double.NaN;
public static final int UNAVAILABLE_RIVER_FLOW = -1;
public IrisColumnSample {
Objects.requireNonNull(surfaceKind, "surfaceKind");
Objects.requireNonNull(riverState, "riverState");
biomeKey = biomeKey == null || biomeKey.isBlank() ? null : biomeKey;
if (!Double.isNaN(riverDistance) && (!Double.isFinite(riverDistance) || riverDistance < 0D)) {
throw new IllegalArgumentException("riverDistance must be non-negative, finite, or unavailable");
}
if (riverFlow < UNAVAILABLE_RIVER_FLOW) {
throw new IllegalArgumentException("riverFlow must be non-negative or unavailable");
}
}
public boolean hasSurfaceHeight() {
return surfaceHeight != UNAVAILABLE_HEIGHT;
}
public boolean hasNaturalHeight() {
return naturalHeight != UNAVAILABLE_HEIGHT;
}
public boolean hasSurfaceKind() {
return surfaceKind != IrisSurfaceKind.UNKNOWN;
}
public boolean hasBiomeKey() {
return biomeKey != null;
}
public boolean hasRiverState() {
return riverState != IrisRiverState.NONE;
}
public boolean hasRiverDistance() {
return !Double.isNaN(riverDistance);
}
public boolean hasRiverFlow() {
return riverFlow != UNAVAILABLE_RIVER_FLOW;
}
public boolean hasRiverWaterSurfaceY() {
return riverWaterSurfaceY != UNAVAILABLE_HEIGHT;
}
}
@@ -2,5 +2,5 @@ package art.arcane.iris.api.terrain;
@FunctionalInterface
public interface IrisColumnSink {
void accept(int blockX, int blockZ, int surfaceHeight, IrisSurfaceKind kind, String biomeKey);
void accept(IrisColumnSample sample);
}
@@ -0,0 +1,7 @@
package art.arcane.iris.api.terrain;
public enum IrisRiverState {
NONE,
WET,
DRY
}
@@ -4,6 +4,9 @@ public enum IrisSurfaceKind {
UNKNOWN,
LAND,
SHORE,
RIVER,
RIVER_SHORE,
DRY_CHANNEL,
OCEAN,
VOID
}
@@ -31,22 +31,21 @@ import java.util.List;
import java.util.Objects;
import java.util.Random;
/**
* Answer to a generator-plugin discovery probe.
* <p>
* Multiverse-Core calls {@code getDefaultWorldGenerator} with an empty dimension id and the name of
* an already loaded world purely to find out whether a plugin is a generator plugin, then throws the
* returned instance away. A non-null answer keeps Iris in {@code /mv generators} and in Multiverse's
* generator tab-completion without Multiverse logging a warning block on every boot.
* <p>
* Nothing here can build terrain: every generation entry point refuses, so an instance that escapes
* the probe fails loudly instead of silently producing vanilla chunks.
*/
final class IrisProbeChunkGenerator extends ChunkGenerator {
private final String worldName;
final class IrisFailClosedChunkGenerator extends ChunkGenerator {
private final String refusalMessage;
IrisProbeChunkGenerator(String worldName) {
this.worldName = Objects.requireNonNull(worldName, "worldName");
private IrisFailClosedChunkGenerator(String refusalMessage) {
this.refusalMessage = Objects.requireNonNull(refusalMessage, "refusalMessage");
}
static IrisFailClosedChunkGenerator discoveryProbe(String worldName) {
return new IrisFailClosedChunkGenerator("Iris generator-discovery probe for '" + worldName
+ "' was asked to generate terrain. Iris worlds are created with /iris create.");
}
static IrisFailClosedChunkGenerator startupLock(String worldName, String denialReason) {
return new IrisFailClosedChunkGenerator("Iris generation for '" + worldName
+ "' remains locked: " + denialReason);
}
@Override
@@ -84,6 +83,11 @@ final class IrisProbeChunkGenerator extends ChunkGenerator {
throw refusal();
}
@Override
public boolean canSpawn(@NotNull World world, int x, int z) {
throw refusal();
}
@Override
public List<BlockPopulator> getDefaultPopulators(@NotNull World world) {
throw refusal();
@@ -94,8 +98,72 @@ final class IrisProbeChunkGenerator extends ChunkGenerator {
throw refusal();
}
@Override
public boolean shouldGenerateNoise() {
return false;
}
@Override
public boolean shouldGenerateNoise(@NotNull WorldInfo worldInfo, @NotNull Random random, int x, int z) {
return false;
}
@Override
public boolean shouldGenerateSurface() {
return false;
}
@Override
public boolean shouldGenerateSurface(@NotNull WorldInfo worldInfo, @NotNull Random random, int x, int z) {
return false;
}
@Override
public boolean shouldGenerateBedrock() {
return false;
}
@Override
public boolean shouldGenerateCaves() {
return false;
}
@Override
public boolean shouldGenerateCaves(@NotNull WorldInfo worldInfo, @NotNull Random random, int x, int z) {
return false;
}
@Override
public boolean shouldGenerateDecorations() {
return false;
}
@Override
public boolean shouldGenerateDecorations(@NotNull WorldInfo worldInfo, @NotNull Random random, int x, int z) {
return false;
}
@Override
public boolean shouldGenerateMobs() {
return false;
}
@Override
public boolean shouldGenerateMobs(@NotNull WorldInfo worldInfo, @NotNull Random random, int x, int z) {
return false;
}
@Override
public boolean shouldGenerateStructures() {
return false;
}
@Override
public boolean shouldGenerateStructures(@NotNull WorldInfo worldInfo, @NotNull Random random, int x, int z) {
return false;
}
private IllegalStateException refusal() {
return new IllegalStateException("Iris generator-discovery probe for '" + worldName
+ "' was asked to generate terrain. Iris worlds are created with /iris create.");
return new IllegalStateException(refusalMessage);
}
}
@@ -349,9 +349,14 @@ public final class IrisWorldGeneratorResolver {
}
if (isGeneratorDiscoveryProbe(worldName, id)) {
Iris.debug("Generator discovery probe for loaded world " + worldName);
return new IrisProbeChunkGenerator(worldName);
return IrisFailClosedChunkGenerator.discoveryProbe(worldName);
}
Optional<String> startupDenial = IrisStartupValidation.denialReason();
if (startupDenial.isPresent()) {
Iris.warn("Keeping configured Iris world '" + worldName
+ "' generation-locked: " + startupDenial.get());
return IrisFailClosedChunkGenerator.startupLock(worldName, startupDenial.get());
}
IrisStartupValidation.requireWorldCreationReady();
ChunkGenerator stagedGenerator = WorldLifecycleStaging.consumeGenerator(worldName);
if (stagedGenerator != null) {
Iris.debug("Using staged runtime generator for " + worldName);
@@ -250,7 +250,9 @@ public class CommandObject implements DirectorExecutor {
@Override
public int getFluidHeight() {
return 63;
return targetEngine == null
? 63
: targetEngine.getMinHeight() + targetEngine.getDimension().getFluidHeight();
}
@Override
@@ -18,7 +18,6 @@
package art.arcane.iris.core.commands;
import art.arcane.iris.platform.bukkit.BukkitWorldBinding;
import art.arcane.iris.Iris;
import art.arcane.iris.platform.bukkit.BukkitPlatform;
import art.arcane.iris.core.IrisSettings;
@@ -47,7 +46,6 @@ import art.arcane.iris.engine.object.IrisNoiseGenerator;
import art.arcane.iris.engine.object.IrisObject;
import art.arcane.iris.engine.object.IrisObjectPlacement;
import art.arcane.iris.engine.object.IrisRegion;
import art.arcane.iris.engine.object.IrisWorld;
import art.arcane.iris.engine.object.NoiseStyle;
import art.arcane.iris.engine.platform.EngineBukkitOps;
import art.arcane.iris.engine.platform.PlatformChunkGenerator;
@@ -62,7 +60,6 @@ import art.arcane.volmlib.util.director.annotations.Director;
import art.arcane.volmlib.util.director.annotations.Param;
import art.arcane.iris.util.common.format.C;
import art.arcane.volmlib.util.format.Form;
import art.arcane.volmlib.util.function.Function2;
import art.arcane.volmlib.util.function.NoiseProvider;
import art.arcane.iris.util.project.interpolation.InterpolationMethod;
import art.arcane.volmlib.util.io.IO;
@@ -102,7 +99,6 @@ import java.util.Objects;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Supplier;
import art.arcane.iris.core.localization.IrisLanguage;
import art.arcane.iris.core.localization.BukkitCommandMessages;
@@ -264,8 +260,8 @@ public class CommandStudio implements DirectorExecutor {
return;
}
Supplier<Function2<Double, Double, Double>> supplier = () -> (x, z) -> generator.getHeight(x, z, new RNG(seed).nextParallelRNG(3245).lmax());
NoiseExplorerGUI.launch(supplier, "Custom Generator");
String generatorKey = generator.getLoadKey();
NoiseExplorerGUI.launchGeneratorKey(generatorKey, generator, seed);
}
@Director(description = "Show loot if a chest were right here", descriptionKey = "iris.director.commandstudio.director.show_loot_if_chest_were_right_here", origin = DirectorOrigin.PLAYER, sync = true)
@@ -648,7 +644,8 @@ public class CommandStudio implements DirectorExecutor {
@Director(description = "Teleport to the active studio world", descriptionKey = "iris.director.commandstudio.director.teleport_active_studio_world", aliases = "stp", origin = DirectorOrigin.PLAYER, sync = true)
public void tpstudio() {
if (!Iris.service(StudioSVC.class).isProjectOpen()) {
StudioSVC studioService = Iris.service(StudioSVC.class);
if (!studioService.isProjectOpen()) {
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_STUDIO_NO_STUDIO_WORLD_IS_OPEN));
return;
}
@@ -660,13 +657,16 @@ public class CommandStudio implements DirectorExecutor {
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_STUDIO_SENDING_YOU_STUDIO_WORLD));
Player player = player();
IrisWorld studioWorld = Iris.service(StudioSVC.class)
.getActiveProject()
.getActiveProvider()
.getTarget()
.getWorld();
BukkitPlatform.teleportAsync(player, BukkitWorldBinding.spawnLocation(studioWorld))
.thenRun(() -> player.setGameMode(GameMode.CREATIVE));
studioService.teleportToActiveProject(player)
.whenComplete((teleported, failure) -> {
if (failure != null) {
Iris.reportError("Studio teleport failed for player \"" + player.getName() + "\".", failure);
return;
}
if (Boolean.TRUE.equals(teleported)) {
J.runEntity(player, () -> player.setGameMode(GameMode.CREATIVE));
}
});
}
@Director(description = "Update your dimension projects VSCode workspace", descriptionKey = "iris.director.commandstudio.director.update_your_dimension_projects_vscode_workspace")
@@ -169,7 +169,7 @@ public final class BukkitVisionOverlay implements GuiOverlay {
public String openInEditor(double worldX, double worldZ, RenderType type) {
IrisComplex complex = engine.getComplex();
File file = switch (type) {
case BIOME, LAYER_LOAD, DECORATOR_LOAD, OBJECT_LOAD, HEIGHT ->
case BIOME, LAYER_LOAD, DECORATOR_LOAD, OBJECT_LOAD, HEIGHT, RIVER ->
complex.getTrueBiomeStream().get(worldX, worldZ).openInVSCode();
case BIOME_LAND -> complex.getLandBiomeStream().get(worldX, worldZ).openInVSCode();
case BIOME_SEA -> complex.getSeaBiomeStream().get(worldX, worldZ).openInVSCode();
@@ -2,7 +2,9 @@ package art.arcane.iris.core.service;
import art.arcane.iris.api.terrain.IrisColumnField;
import art.arcane.iris.api.terrain.IrisColumnQuery;
import art.arcane.iris.api.terrain.IrisColumnSample;
import art.arcane.iris.api.terrain.IrisColumnSink;
import art.arcane.iris.api.terrain.IrisRiverState;
import art.arcane.iris.api.terrain.IrisSurfaceKind;
import art.arcane.iris.api.terrain.IrisTerrainService;
import art.arcane.iris.api.terrain.IrisWorldInfo;
@@ -17,6 +19,8 @@ import art.arcane.iris.engine.object.InferredType;
import art.arcane.iris.engine.object.IrisBiome;
import art.arcane.iris.engine.object.IrisRegion;
import art.arcane.iris.engine.platform.PlatformChunkGenerator;
import art.arcane.iris.engine.river.RiverRouteState;
import art.arcane.iris.engine.river.runtime.IrisRiverSurfaceSample;
import art.arcane.iris.platform.bukkit.BukkitPlatform;
import art.arcane.iris.spi.IrisLogging;
import art.arcane.iris.spi.IrisServices;
@@ -101,13 +105,14 @@ public class IrisTerrainSVC implements IrisService, IrisTerrainService {
try {
int surface = engine.getHeight(blockX, blockZ);
int fluid = engine.getDimension().getFluidHeight();
IrisRiverSurfaceSample riverSurface = engine.getComplex().getRiverSurfaceStream().get(blockX, blockZ);
int fluid = (int) Math.round(riverSurface.waterSurfaceY());
InferredType inferredType = null;
if (IrisSurfaceClassifier.requiresSurfaceBiome(surface, fluid)) {
IrisBiome biome = engine.getSurfaceBiome(blockX, blockZ);
inferredType = biome == null ? null : biome.getInferredType();
}
return IrisSurfaceClassifier.classify(surface, fluid, inferredType);
return IrisSurfaceClassifier.classify(surface, fluid, inferredType, riverSurface);
} catch (Throwable error) {
reportQueryFault("surfaceKind", world, error);
return IrisSurfaceKind.UNKNOWN;
@@ -224,26 +229,72 @@ public class IrisTerrainSVC implements IrisService, IrisTerrainService {
EnumSet<IrisColumnField> fields = query.fields();
boolean wantHeight = fields.contains(IrisColumnField.SURFACE_HEIGHT);
boolean wantNaturalHeight = fields.contains(IrisColumnField.NATURAL_HEIGHT);
boolean wantKind = fields.contains(IrisColumnField.SURFACE_KIND);
boolean wantBiome = fields.contains(IrisColumnField.BIOME_KEY);
boolean wantRiverState = fields.contains(IrisColumnField.RIVER_STATE);
boolean wantRiverDistance = fields.contains(IrisColumnField.RIVER_DISTANCE);
boolean wantRiverFlow = fields.contains(IrisColumnField.RIVER_FLOW);
boolean wantRiverWaterSurface = fields.contains(IrisColumnField.RIVER_WATER_SURFACE_Y);
boolean wantRiver = wantKind || wantRiverState || wantRiverDistance || wantRiverFlow
|| wantRiverWaterSurface;
try {
int minHeight = engine.getMinHeight();
int fluid = engine.getDimension().getFluidHeight();
long visited = IrisColumnWalk.walk(query, (int blockX, int blockZ) -> {
if (engine.isClosed()) {
return false;
}
IrisRiverSurfaceSample riverSurface = wantRiver
? engine.getComplex().getRiverSurfaceStream().get(blockX, blockZ)
: null;
int surface = wantHeight || wantKind ? engine.getHeight(blockX, blockZ) : 0;
int fluid = riverSurface == null
? engine.getDimension().getFluidHeight()
: (int) Math.round(riverSurface.waterSurfaceY());
boolean needsBiome = wantBiome
|| (wantKind && IrisSurfaceClassifier.requiresSurfaceBiome(surface, fluid));
IrisBiome biome = needsBiome ? engine.getSurfaceBiome(blockX, blockZ) : null;
IrisSurfaceKind kind = wantKind
? IrisSurfaceClassifier.classify(surface, fluid, biome == null ? null : biome.getInferredType())
? IrisSurfaceClassifier.classify(
surface,
fluid,
biome == null ? null : biome.getInferredType(),
riverSurface
)
: IrisSurfaceKind.UNKNOWN;
String biomeKey = wantBiome && biome != null ? biome.getLoadKey() : null;
sink.accept(blockX, blockZ, wantHeight ? surface + minHeight : -1, kind, biomeKey);
int natural = wantNaturalHeight
? (int) Math.round(engine.getComplex().getNaturalHeightStream().get(blockX, blockZ)) + minHeight
: IrisColumnSample.UNAVAILABLE_HEIGHT;
boolean riverPresent = riverSurface != null && riverSurface.river().present();
IrisRiverState riverState = wantRiverState && riverSurface != null
? riverState(riverSurface)
: IrisRiverState.NONE;
double riverDistance = wantRiverDistance && riverPresent
? riverSurface.river().distance()
: IrisColumnSample.UNAVAILABLE_RIVER_DISTANCE;
int riverFlow = wantRiverFlow && riverPresent
? riverSurface.river().flow()
: IrisColumnSample.UNAVAILABLE_RIVER_FLOW;
int riverWaterSurfaceY = wantRiverWaterSurface
&& riverPresent
&& riverSurface.river().state() == RiverRouteState.WET
? fluid + minHeight
: IrisColumnSample.UNAVAILABLE_HEIGHT;
sink.accept(new IrisColumnSample(
blockX,
blockZ,
wantHeight ? surface + minHeight : IrisColumnSample.UNAVAILABLE_HEIGHT,
natural,
kind,
biomeKey,
riverState,
riverDistance,
riverFlow,
riverWaterSurfaceY
));
return true;
});
return visited == query.columnCount();
@@ -253,6 +304,17 @@ public class IrisTerrainSVC implements IrisService, IrisTerrainService {
}
}
static IrisRiverState riverState(IrisRiverSurfaceSample surface) {
if (!surface.river().present()) {
return IrisRiverState.NONE;
}
return switch (surface.river().state()) {
case WET -> IrisRiverState.WET;
case DRY -> IrisRiverState.DRY;
case SUPPRESSED -> IrisRiverState.NONE;
};
}
private static Optional<String> key(IrisBiome biome) {
String loadKey = biome == null ? null : biome.getLoadKey();
return loadKey == null || loadKey.isEmpty() ? Optional.empty() : Optional.of(loadKey);
@@ -2,6 +2,9 @@ package art.arcane.iris.core.service.terrain;
import art.arcane.iris.api.terrain.IrisSurfaceKind;
import art.arcane.iris.engine.object.InferredType;
import art.arcane.iris.engine.river.RiverRouteState;
import art.arcane.iris.engine.river.RiverSection;
import art.arcane.iris.engine.river.runtime.IrisRiverSurfaceSample;
public final class IrisSurfaceClassifier {
private IrisSurfaceClassifier() {
@@ -22,4 +25,32 @@ public final class IrisSurfaceClassifier {
return inferredType == InferredType.SHORE ? IrisSurfaceKind.SHORE : IrisSurfaceKind.LAND;
}
public static IrisSurfaceKind classify(
int engineSurfaceHeight,
int engineFluidHeight,
InferredType inferredType,
IrisRiverSurfaceSample riverSurface
) {
if (engineSurfaceHeight <= 0) {
return IrisSurfaceKind.VOID;
}
if (riverSurface != null && riverSurface.river().present() && !riverSurface.subterranean()) {
if (riverSurface.river().state() == RiverRouteState.DRY) {
return riverSurface.river().section() == RiverSection.DRY_CHANNEL
? IrisSurfaceKind.DRY_CHANNEL
: IrisSurfaceKind.LAND;
}
if (riverSurface.river().state() == RiverRouteState.WET) {
RiverSection section = riverSurface.river().section();
if (section == RiverSection.BANK) {
return IrisSurfaceKind.RIVER_SHORE;
}
if (section == RiverSection.CHANNEL || section == RiverSection.MOUTH) {
return IrisSurfaceKind.RIVER;
}
}
}
return classify(engineSurfaceHeight, engineFluidHeight, inferredType);
}
}
@@ -31,12 +31,17 @@ public class IrisShutdownOrderingTest {
"public void quiesceForServerShutdown()", "public boolean isStudio()");
assertOrdered(onDisable,
"if (serverStopping)",
"startupBoundaryRestart.get()",
"teardownRuntime(\"startup-boundary-restart\", 30L)",
"else if (serverStopping)",
"quiesceRuntimeForServerShutdown(\"onDisable\")",
"startPostStopFinisher()",
"else",
"} else {",
"teardownRuntime(\"onDisable\", 30L)");
assertOrdered(shutdownHook,
"startupBoundaryRestart.get()",
"finishDeferredRuntimeTeardown(\"startup-boundary-restart-hook\", 30L)",
"return;",
"awaitServerShutdownBoundary()",
"finishDeferredRuntimeTeardown(\"shutdown-hook\", 30L)");
assertOrdered(finisher,
@@ -0,0 +1,145 @@
package art.arcane.iris.api.terrain;
import org.junit.Test;
import java.util.concurrent.atomic.AtomicReference;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
public class IrisColumnSampleTest {
@Test
public void unavailableFieldsHaveUnambiguousSentinels() {
IrisColumnSample sample = sample(
IrisColumnSample.UNAVAILABLE_HEIGHT,
IrisColumnSample.UNAVAILABLE_HEIGHT,
IrisSurfaceKind.UNKNOWN,
null,
IrisRiverState.NONE,
IrisColumnSample.UNAVAILABLE_RIVER_DISTANCE,
IrisColumnSample.UNAVAILABLE_RIVER_FLOW,
IrisColumnSample.UNAVAILABLE_HEIGHT
);
assertFalse(sample.hasSurfaceHeight());
assertFalse(sample.hasNaturalHeight());
assertFalse(sample.hasSurfaceKind());
assertFalse(sample.hasBiomeKey());
assertFalse(sample.hasRiverState());
assertFalse(sample.hasRiverDistance());
assertFalse(sample.hasRiverFlow());
assertFalse(sample.hasRiverWaterSurfaceY());
}
@Test
public void negativeWorldHeightsRemainAvailableValues() {
IrisColumnSample sample = sample(
-1,
-64,
IrisSurfaceKind.DRY_CHANNEL,
"test:river",
IrisRiverState.DRY,
0D,
0,
-1
);
assertTrue(sample.hasSurfaceHeight());
assertTrue(sample.hasNaturalHeight());
assertTrue(sample.hasSurfaceKind());
assertTrue(sample.hasBiomeKey());
assertTrue(sample.hasRiverState());
assertTrue(sample.hasRiverDistance());
assertTrue(sample.hasRiverFlow());
assertTrue(sample.hasRiverWaterSurfaceY());
}
@Test
public void blankBiomeKeysNormalizeToUnavailable() {
IrisColumnSample sample = sample(
64,
65,
IrisSurfaceKind.LAND,
" ",
IrisRiverState.NONE,
IrisColumnSample.UNAVAILABLE_RIVER_DISTANCE,
IrisColumnSample.UNAVAILABLE_RIVER_FLOW,
IrisColumnSample.UNAVAILABLE_HEIGHT
);
assertNull(sample.biomeKey());
assertFalse(sample.hasBiomeKey());
}
@Test
public void theSinkReceivesTheTypedSample() {
IrisColumnSample sample = sample(
64,
65,
IrisSurfaceKind.RIVER,
"test:river",
IrisRiverState.WET,
0.5D,
3,
67
);
AtomicReference<IrisColumnSample> received = new AtomicReference<>();
IrisColumnSink sink = received::set;
sink.accept(sample);
assertSame(sample, received.get());
}
@Test
public void invalidHydrologyValuesAreRejected() {
assertInvalid(Double.POSITIVE_INFINITY, 1);
assertInvalid(-0.1D, 1);
assertInvalid(0D, -2);
}
private static void assertInvalid(double distance, int flow) {
try {
sample(
64,
65,
IrisSurfaceKind.RIVER,
"test:river",
IrisRiverState.WET,
distance,
flow,
67
);
fail("Expected invalid hydrology values to be rejected");
} catch (IllegalArgumentException expected) {
assertTrue(expected.getMessage().startsWith("river"));
}
}
private static IrisColumnSample sample(
int surfaceHeight,
int naturalHeight,
IrisSurfaceKind surfaceKind,
String biomeKey,
IrisRiverState riverState,
double riverDistance,
int riverFlow,
int riverWaterSurfaceY
) {
return new IrisColumnSample(
12,
-7,
surfaceHeight,
naturalHeight,
surfaceKind,
biomeKey,
riverState,
riverDistance,
riverFlow,
riverWaterSurfaceY
);
}
}
@@ -29,6 +29,18 @@ public class IrisStartupOrderingTest {
"generatorResolver.validateAllPacks();");
}
@Test
public void restartRequiredStartupTerminatesAfterPluginInitialization() throws Exception {
String source = Files.readString(Path.of(System.getProperty("iris.startupSource")));
String onEnable = section(source, "public void onEnable()", "public void onDisable()");
assertOrdered(onEnable,
"BukkitGuiHost.install();",
"super.onEnable();",
"IrisStartupValidation.isRestartRequired()",
"ServerConfigurator.restartAtStartupBoundary(restartReason);");
}
private static String section(String source, String startMarker, String endMarker) {
int start = source.indexOf(startMarker);
int end = source.indexOf(endMarker, start);
@@ -1,6 +1,7 @@
package art.arcane.iris.core;
import art.arcane.iris.Iris;
import art.arcane.iris.core.lifecycle.WorldLifecycleStaging;
import art.arcane.iris.core.pack.BrokenPackException;
import art.arcane.iris.core.pack.PackValidationRegistry;
import art.arcane.iris.core.pack.PackValidationResult;
@@ -22,11 +23,14 @@ import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
import java.util.Random;
import java.util.function.Supplier;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNotSame;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertTrue;
import static org.mockito.Mockito.mock;
@@ -39,8 +43,10 @@ public class IrisWorldGeneratorResolverTest {
public TemporaryFolder temporaryFolder = new TemporaryFolder();
@After
public void clearValidationRegistry() {
public void clearValidationState() {
PackValidationRegistry.clear();
IrisStartupValidation.disable();
WorldLifecycleStaging.clearAll("world_nether");
}
@Test
@@ -234,7 +240,9 @@ public class IrisWorldGeneratorResolverTest {
int plotSquaredProbe = resolver.indexOf("isPlotSquaredGeneratorDiscoveryProbe(worldName, id)");
int probe = resolver.indexOf("isGeneratorDiscoveryProbe(worldName, id)", plotSquaredProbe);
int readiness = resolver.indexOf("IrisStartupValidation.requireWorldCreationReady()");
int denial = resolver.indexOf("IrisStartupValidation.denialReason()", probe);
int failClosed = resolver.indexOf("IrisFailClosedChunkGenerator.startupLock(", denial);
int staged = resolver.indexOf("WorldLifecycleStaging.consumeGenerator(worldName)", failClosed);
int duplicateGuard = resolver.indexOf("requireWorldKeyAvailable(worldName, worldKey)");
int ownership = resolver.indexOf("requireOwnedWorld(worldName, levelRoot, worldKey)");
int frozen = resolver.indexOf("return resolveFrozenWorldGenerator(", ownership);
@@ -245,8 +253,10 @@ public class IrisWorldGeneratorResolverTest {
assertTrue(plotSquaredProbe >= 0);
assertTrue(probe > plotSquaredProbe);
assertTrue(readiness > probe);
assertTrue(duplicateGuard > readiness);
assertTrue(denial > probe);
assertTrue(failClosed > denial);
assertTrue(staged > failClosed);
assertTrue(duplicateGuard > staged);
assertTrue(ownership > duplicateGuard);
assertTrue(frozen > ownership);
assertTrue(failureCapture > frozen);
@@ -302,6 +312,8 @@ public class IrisWorldGeneratorResolverTest {
try (MockedStatic<Bukkit> bukkit = mockStatic(Bukkit.class);
MockedStatic<Iris> iris = mockStatic(Iris.class)) {
bukkit.when(() -> Bukkit.getWorld("world")).thenReturn(mock(World.class));
IrisStartupValidation.begin();
IrisStartupValidation.requireRestart("restart boundary");
ChunkGenerator probe = new IrisWorldGeneratorResolver(null)
.resolveDefaultWorldGenerator("world", "");
@@ -312,6 +324,95 @@ public class IrisWorldGeneratorResolverTest {
IllegalStateException.class,
() -> probe.generateNoise(mock(WorldInfo.class), new Random(), 0, 0, null));
assertTrue(refusal.getMessage(), refusal.getMessage().contains("'world'"));
assertTrue(refusal.getMessage(), refusal.getMessage().contains("discovery probe"));
}
}
@Test
public void restartRequiredDefaultWorldCannotFallBackToVanilla() {
ChunkGenerator stagedGenerator = mock(ChunkGenerator.class);
ChunkGenerator vanillaFallback = mock(ChunkGenerator.class);
WorldLifecycleStaging.stageGenerator("world_nether", stagedGenerator, null);
IrisStartupValidation.begin();
IrisStartupValidation.requireRestart("updated external datapacks require restart");
try (MockedStatic<Bukkit> bukkit = mockStatic(Bukkit.class);
MockedStatic<Iris> iris = mockStatic(Iris.class)) {
bukkit.when(() -> Bukkit.getWorld("world_nether")).thenReturn(null);
ChunkGenerator selected = craftBukkitGeneratorOrFallback(
() -> new IrisWorldGeneratorResolver(null)
.resolveDefaultWorldGenerator("world_nether", "underworld"),
vanillaFallback
);
assertNotSame(vanillaFallback, selected);
assertNotSame(stagedGenerator, selected);
assertSame(stagedGenerator, WorldLifecycleStaging.consumeGenerator("world_nether"));
WorldInfo worldInfo = mock(WorldInfo.class);
Random random = new Random();
assertFalse(selected.shouldGenerateNoise());
assertFalse(selected.shouldGenerateNoise(worldInfo, random, 0, 0));
assertFalse(selected.shouldGenerateSurface());
assertFalse(selected.shouldGenerateSurface(worldInfo, random, 0, 0));
assertFalse(selected.shouldGenerateBedrock());
assertFalse(selected.shouldGenerateCaves());
assertFalse(selected.shouldGenerateCaves(worldInfo, random, 0, 0));
assertFalse(selected.shouldGenerateDecorations());
assertFalse(selected.shouldGenerateDecorations(worldInfo, random, 0, 0));
assertFalse(selected.shouldGenerateMobs());
assertFalse(selected.shouldGenerateMobs(worldInfo, random, 0, 0));
assertFalse(selected.shouldGenerateStructures());
assertFalse(selected.shouldGenerateStructures(worldInfo, random, 0, 0));
IllegalStateException refusal = assertThrows(
IllegalStateException.class,
() -> selected.generateNoise(
worldInfo,
random,
0,
0,
mock(ChunkGenerator.ChunkData.class)
)
);
assertTrue(refusal.getMessage(), refusal.getMessage().contains("world_nether"));
assertTrue(refusal.getMessage(), refusal.getMessage().contains("updated external datapacks require restart"));
}
}
@Test
public void everyBlockingStartupStateReturnsFailClosedGenerator() {
try (MockedStatic<Bukkit> bukkit = mockStatic(Bukkit.class);
MockedStatic<Iris> iris = mockStatic(Iris.class)) {
IrisStartupValidation.begin();
assertStartupStateFailsClosed("world_pending", "external datapacks");
IrisStartupValidation.begin();
IrisStartupValidation.markDatapacksInvalid("invalid external datapack state");
assertStartupStateFailsClosed("world_datapack_invalid", "invalid external datapack state");
IrisStartupValidation.begin();
IrisStartupValidation.markDatapacksReady();
IrisStartupValidation.markPacksInvalid(List.of("invalid dimension pack state"));
assertStartupStateFailsClosed("world_pack_invalid", "invalid dimension pack state");
}
}
@Test
public void readyStartupStillConsumesStagedGenerator() {
ChunkGenerator stagedGenerator = mock(ChunkGenerator.class);
WorldLifecycleStaging.stageGenerator("world_nether", stagedGenerator, null);
IrisStartupValidation.begin();
IrisStartupValidation.markDatapacksReady();
IrisStartupValidation.markPacksReady();
try (MockedStatic<Bukkit> bukkit = mockStatic(Bukkit.class);
MockedStatic<Iris> iris = mockStatic(Iris.class)) {
bukkit.when(() -> Bukkit.getWorld("world_nether")).thenReturn(null);
ChunkGenerator selected = new IrisWorldGeneratorResolver(null)
.resolveDefaultWorldGenerator("world_nether", "underworld");
assertSame(stagedGenerator, selected);
}
}
@@ -436,4 +537,38 @@ public class IrisWorldGeneratorResolverTest {
"{\"name\":\"Biome\"}",
StandardCharsets.UTF_8);
}
private static void assertStartupStateFailsClosed(String worldName, String expectedReason) {
ChunkGenerator vanillaFallback = mock(ChunkGenerator.class);
ChunkGenerator selected = craftBukkitGeneratorOrFallback(
() -> new IrisWorldGeneratorResolver(null)
.resolveDefaultWorldGenerator(worldName, "overworld"),
vanillaFallback
);
assertNotSame(vanillaFallback, selected);
IllegalStateException refusal = assertThrows(
IllegalStateException.class,
() -> selected.generateNoise(
mock(WorldInfo.class),
new Random(),
0,
0,
mock(ChunkGenerator.ChunkData.class)
)
);
assertTrue(refusal.getMessage(), refusal.getMessage().contains(worldName));
assertTrue(refusal.getMessage(), refusal.getMessage().contains(expectedReason));
}
private static ChunkGenerator craftBukkitGeneratorOrFallback(
Supplier<ChunkGenerator> resolution,
ChunkGenerator fallback
) {
try {
ChunkGenerator selected = resolution.get();
return selected == null ? fallback : selected;
} catch (Throwable ignoredGeneratorFailure) {
return fallback;
}
}
}
@@ -0,0 +1,45 @@
package art.arcane.iris.core.commands;
import art.arcane.iris.engine.IrisComplex;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.object.IObjectPlacer;
import art.arcane.iris.engine.object.IrisDimension;
import art.arcane.iris.util.project.stream.ProceduralStream;
import org.bukkit.World;
import org.bukkit.block.Block;
import org.bukkit.block.data.BlockData;
import org.junit.Test;
import java.util.HashMap;
import java.util.Map;
import static org.junit.Assert.assertEquals;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
public class CommandObjectFluidHeightTest {
@Test
public void absoluteObjectPlacementShiftsTheColumnRiverHeadFromNegativeMinY() {
World world = mock(World.class);
Engine engine = mock(Engine.class);
IrisDimension dimension = mock(IrisDimension.class);
IrisComplex complex = mock(IrisComplex.class);
@SuppressWarnings("unchecked")
ProceduralStream<Double> riverHead = mock(ProceduralStream.class);
Map<Block, BlockData> future = new HashMap<>();
when(engine.getMinHeight()).thenReturn(-64);
when(engine.getDimension()).thenReturn(dimension);
when(engine.getComplex()).thenReturn(complex);
when(dimension.getFluidHeight()).thenReturn(127);
when(complex.getRiverWaterSurfaceStream()).thenReturn(riverHead);
when(riverHead.get(12, -7)).thenReturn(131D);
IObjectPlacer placer = CommandObject.createPlacer(world, future, engine);
assertEquals(63, placer.getFluidHeight());
assertEquals(67, placer.getFluidHeight(12, -7));
verify(riverHead).get(12, -7);
}
}
@@ -21,4 +21,19 @@ public class StudioPlayerModeContractTest {
assertTrue(plugin.contains("GameMode.CREATIVE"));
assertTrue(commands.contains("GameMode.CREATIVE"));
}
@Test
public void tpStudioUsesThePreparedCoordinatorEntry() throws IOException {
String commands = Files.readString(Path.of(
"src/main/java/art/arcane/iris/core/commands/CommandStudio.java")).replace("\r\n", "\n");
int methodStart = commands.indexOf("public void tpstudio()");
int methodEnd = commands.indexOf("\n @Director", methodStart);
String method = commands.substring(methodStart, methodEnd);
assertTrue(method.contains("StudioSVC studioService = Iris.service(StudioSVC.class)"));
assertTrue(method.contains("studioService.teleportToActiveProject(player)"));
assertFalse(method.contains("getActiveProject()"));
assertFalse(method.contains("BukkitPlatform.teleportAsync"));
assertFalse(method.contains("BukkitWorldBinding.spawnLocation"));
}
}
@@ -2,8 +2,15 @@ package art.arcane.iris.core.service;
import art.arcane.iris.api.terrain.IrisColumnField;
import art.arcane.iris.api.terrain.IrisColumnQuery;
import art.arcane.iris.api.terrain.IrisRiverState;
import art.arcane.iris.api.terrain.IrisSurfaceKind;
import art.arcane.iris.api.terrain.IrisTerrainService;
import art.arcane.iris.engine.river.RiverEdgeId;
import art.arcane.iris.engine.river.RiverNodeId;
import art.arcane.iris.engine.river.RiverRouteState;
import art.arcane.iris.engine.river.RiverSample;
import art.arcane.iris.engine.river.RiverSection;
import art.arcane.iris.engine.river.runtime.IrisRiverSurfaceSample;
import art.arcane.iris.util.common.plugin.IrisService;
import org.bukkit.World;
import org.junit.Test;
@@ -82,9 +89,7 @@ public class IrisTerrainSVCTest {
IrisTerrainSVC service = new IrisTerrainSVC();
AtomicInteger sinkCalls = new AtomicInteger();
boolean answered = service.sampleColumns(null, SMALL,
(int blockX, int blockZ, int surfaceHeight, IrisSurfaceKind kind, String biomeKey)
-> sinkCalls.incrementAndGet());
boolean answered = service.sampleColumns(null, SMALL, sample -> sinkCalls.incrementAndGet());
assertFalse(answered);
assertEquals(0, sinkCalls.get());
@@ -97,4 +102,38 @@ public class IrisTerrainSVCTest {
assertFalse(service.sampleColumns(null, null, null));
assertFalse(service.sampleColumns(null, SMALL, null));
}
@Test
public void riverRouteStatesMapToThePublicDiagnosticStates() {
assertEquals(IrisRiverState.NONE, IrisTerrainSVC.riverState(
new IrisRiverSurfaceSample(RiverSample.none(), 70D, 70D, 70D, false, false)
));
assertEquals(IrisRiverState.WET, IrisTerrainSVC.riverState(river(RiverRouteState.WET)));
assertEquals(IrisRiverState.DRY, IrisTerrainSVC.riverState(river(RiverRouteState.DRY)));
assertEquals(IrisRiverState.NONE, IrisTerrainSVC.riverState(river(RiverRouteState.SUPPRESSED)));
}
private static IrisRiverSurfaceSample river(RiverRouteState state) {
RiverSection section = switch (state) {
case WET -> RiverSection.CHANNEL;
case DRY -> RiverSection.DRY_CHANNEL;
case SUPPRESSED -> RiverSection.NONE;
};
RiverSample sample = new RiverSample(
true,
state,
section,
0D,
0.5D,
1D,
1,
1,
8D,
4D,
3D,
false,
RiverEdgeId.of(new RiverNodeId(0, 0), new RiverNodeId(1, 0))
);
return new IrisRiverSurfaceSample(sample, 70D, 60D, 63D, false, state == RiverRouteState.WET);
}
}
@@ -2,6 +2,12 @@ package art.arcane.iris.core.service.terrain;
import art.arcane.iris.api.terrain.IrisSurfaceKind;
import art.arcane.iris.engine.object.InferredType;
import art.arcane.iris.engine.river.RiverEdgeId;
import art.arcane.iris.engine.river.RiverNodeId;
import art.arcane.iris.engine.river.RiverRouteState;
import art.arcane.iris.engine.river.RiverSample;
import art.arcane.iris.engine.river.RiverSection;
import art.arcane.iris.engine.river.runtime.IrisRiverSurfaceSample;
import org.junit.Test;
import static org.junit.Assert.assertEquals;
@@ -58,4 +64,86 @@ public class IrisSurfaceClassifierTest {
}
}
}
@Test
public void activeRiverGeometryOverridesGenericOceanAndLandKinds() {
assertEquals(IrisSurfaceKind.RIVER, IrisSurfaceClassifier.classify(
60,
63,
InferredType.SEA,
river(RiverRouteState.WET, RiverSection.CHANNEL)
));
assertEquals(IrisSurfaceKind.RIVER, IrisSurfaceClassifier.classify(
60,
63,
InferredType.SEA,
river(RiverRouteState.WET, RiverSection.MOUTH)
));
assertEquals(IrisSurfaceKind.RIVER_SHORE, IrisSurfaceClassifier.classify(
64,
63,
InferredType.SHORE,
river(RiverRouteState.WET, RiverSection.BANK)
));
assertEquals(IrisSurfaceKind.DRY_CHANNEL, IrisSurfaceClassifier.classify(
60,
60,
InferredType.LAND,
river(RiverRouteState.DRY, RiverSection.DRY_CHANNEL)
));
assertEquals(IrisSurfaceKind.LAND, IrisSurfaceClassifier.classify(
60,
60,
InferredType.LAND,
river(RiverRouteState.DRY, RiverSection.DRY_BANK)
));
}
@Test
public void voidClassificationWinsOverRiverGeometry() {
for (RiverSection section : RiverSection.values()) {
if (section == RiverSection.NONE) {
continue;
}
RiverRouteState state = section == RiverSection.DRY_CHANNEL || section == RiverSection.DRY_BANK
? RiverRouteState.DRY
: RiverRouteState.WET;
assertEquals(IrisSurfaceKind.VOID, IrisSurfaceClassifier.classify(
0,
63,
InferredType.LAND,
river(state, section)
));
}
}
@Test
public void suppressedRoutesDoNotCreatePublicRiverSurfaceKinds() {
assertEquals(IrisSurfaceKind.LAND, IrisSurfaceClassifier.classify(
64,
63,
InferredType.LAND,
river(RiverRouteState.SUPPRESSED, RiverSection.CHANNEL)
));
}
private static IrisRiverSurfaceSample river(RiverRouteState state, RiverSection section) {
RiverSample sample = new RiverSample(
true,
state,
section,
0D,
0.5D,
1D,
1,
1,
8D,
4D,
3D,
false,
RiverEdgeId.of(new RiverNodeId(0, 0), new RiverNodeId(1, 0))
);
double waterSurface = state == RiverRouteState.WET ? 63D : 60D;
return new IrisRiverSurfaceSample(sample, 70D, 60D, waterSurface, false, state == RiverRouteState.WET);
}
}
@@ -44,7 +44,6 @@ import art.arcane.iris.util.common.parallel.BurstExecutor;
import art.arcane.iris.util.common.parallel.MultiBurst;
import art.arcane.volmlib.util.collection.KMap;
import art.arcane.volmlib.util.format.Form;
import art.arcane.volmlib.util.function.Function2;
import art.arcane.volmlib.util.io.IO;
import art.arcane.volmlib.util.json.JSONObject;
import art.arcane.volmlib.util.math.M;
@@ -80,7 +79,6 @@ import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Predicate;
import java.util.function.Supplier;
import java.util.regex.Pattern;
import art.arcane.iris.core.localization.IrisLanguage;
@@ -246,9 +244,8 @@ public final class ModdedStudioCommands {
IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.MODDED_STUDIO_COMMANDS_UNKNOWN_GENERATOR_PACK, MessageArgument.untrusted("generatorKey", generatorKey), MessageArgument.untrusted("value", engine.getDimension().getLoadKey())));
return 0;
}
long mixedSeed = new RNG(seed).nextParallelRNG(3245).lmax();
Supplier<Function2<Double, Double, Double>> supplier = () -> (Double x, Double z) -> generator.getHeight(x, z, mixedSeed);
NoiseExplorerGUI.launch(supplier, generatorKey.trim());
String selectedGeneratorKey = generatorKey.trim();
NoiseExplorerGUI.launchGeneratorKey(selectedGeneratorKey, generator, seed);
IrisModdedCommands.ok(source, IrisLanguage.plain(ModdedCommandMessages.MODDED_STUDIO_COMMANDS_OPENING_NOISE_EXPLORER_GENERATOR_SEED, MessageArgument.untrusted("value", generatorKey.trim()), MessageArgument.untrusted("seed", seed)));
return 1;
}
@@ -104,7 +104,7 @@ public final class ModdedVisionOverlay implements GuiOverlay {
}
IrisComplex complex = engine.getComplex();
File file = switch (type) {
case BIOME, LAYER_LOAD, DECORATOR_LOAD, OBJECT_LOAD, HEIGHT ->
case BIOME, LAYER_LOAD, DECORATOR_LOAD, OBJECT_LOAD, HEIGHT, RIVER ->
complex.getTrueBiomeStream().get(worldX, worldZ).openInVSCode();
case BIOME_LAND -> complex.getLandBiomeStream().get(worldX, worldZ).openInVSCode();
case BIOME_SEA -> complex.getSeaBiomeStream().get(worldX, worldZ).openInVSCode();
@@ -79,6 +79,11 @@ public final class IrisStartupValidation {
return isReady(snapshot);
}
public static boolean isRestartRequired() {
Snapshot current = snapshot;
return current.enforced() && current.datapacks() == ValidationState.RESTART_REQUIRED;
}
public static Optional<String> denialReason() {
Snapshot current = snapshot;
if (!current.enforced() || isReady(current)) {
@@ -124,6 +124,7 @@ public class ServerConfigurator {
&& pinLoadedDatapackCompilerInputs()
&& pinLoadedDatapackRegistryRequirements();
if (result.restartRequired()) {
requireDatapackRestart();
IrisLogging.warn("Iris datapack changes require another server restart before worlds can use them.");
}
}
@@ -1020,6 +1021,24 @@ public class ServerConfigurator {
}));
}
public static void restartAtStartupBoundary(String reason) {
String restartReason = reason == null || reason.isBlank()
? "Iris startup validation requires a restart."
: reason.trim();
IrisLogging.warn(restartReason + " Restarting server before default worlds are loaded.");
try {
Bukkit.restart();
} catch (Throwable failure) {
IrisLogging.reportError("Unable to restart the server at the Iris startup boundary.", failure);
}
IrisLogging.error("The immediate Iris startup restart returned unexpectedly; stopping the server instead.");
try {
Bukkit.shutdown();
} catch (Throwable failure) {
IrisLogging.reportError("Unable to stop the server after the Iris startup restart returned.", failure);
}
}
public static boolean verifyDataPackInstalled(IrisDimension dimension) {
KSet<String> keys = new KSet<>();
boolean warn = false;
@@ -24,13 +24,19 @@ import art.arcane.iris.engine.framework.Engine;
import javax.swing.JFrame;
import java.awt.Desktop;
import java.awt.EventQueue;
import java.awt.GraphicsEnvironment;
import java.awt.desktop.QuitResponse;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.util.Locale;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicBoolean;
public final class GuiHost {
private static final AtomicBoolean DESKTOP_QUIT_GUARD_INSTALLED = new AtomicBoolean(false);
private static final Set<JFrame> MANAGED_FRAMES = ConcurrentHashMap.newKeySet();
private static volatile Provider provider = new Provider() {
};
private static volatile boolean desktopSuppressed = false;
@@ -78,6 +84,13 @@ public final class GuiHost {
public static void prepareFrame(JFrame frame) {
frame.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);
MANAGED_FRAMES.add(frame);
frame.addWindowListener(new WindowAdapter() {
@Override
public void windowClosed(WindowEvent event) {
MANAGED_FRAMES.remove(frame);
}
});
prepareServerDesktop();
}
@@ -96,15 +109,20 @@ public final class GuiHost {
if (!desktop.isSupported(Desktop.Action.APP_QUIT_HANDLER)) {
return;
}
desktop.setQuitHandler((event, response) -> cancelDesktopQuit(response));
desktop.setQuitHandler((event, response) -> closeDesktopWindowsAndCancelQuit(response));
} catch (Throwable error) {
IrisLogging.reportError(error);
IrisLogging.info("Unable to install the Iris desktop quit guard; use the server stop command instead of macOS Quit");
}
}
static void cancelDesktopQuit(QuitResponse response) {
static void closeDesktopWindowsAndCancelQuit(QuitResponse response) {
response.cancelQuit();
EventQueue.invokeLater(() -> {
for (JFrame frame : MANAGED_FRAMES) {
frame.dispose();
}
});
}
/**
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,96 @@
package art.arcane.iris.core.gui;
import java.awt.Color;
enum NoisePalette {
TERRAIN("Terrain", 0D, 1D, new int[]{0x071A2F, 0x155E75, 0x2A9D8F, 0xE9C46A, 0xF4F1DE}),
SIGNED("Signed", -1D, 1D, new int[]{0x173B66, 0x4F86C6, 0xE7EDF3, 0xE89555, 0x9D3B35}),
GRAYSCALE("Grayscale", 0D, 1D, new int[]{0x050505, 0xFFFFFF});
static final int INVALID_COLOR = 0xFF2DAA;
private final String label;
private final double minimum;
private final double maximum;
private final int[] lookup;
private final Color[] displayColors;
NoisePalette(String label, double minimum, double maximum, int[] stops) {
this.label = label;
this.minimum = minimum;
this.maximum = maximum;
this.lookup = buildLookup(stops);
this.displayColors = buildDisplayColors(lookup);
}
int color(double value) {
if (!Double.isFinite(value)) {
return INVALID_COLOR;
}
return colorFinite(value);
}
int colorFinite(double value) {
double normalized = (value - minimum) / (maximum - minimum);
return colorNormalized(normalized);
}
int colorNormalized(double normalized) {
double clipped = Math.max(0D, Math.min(1D, normalized));
return lookup[(int) Math.round(clipped * (lookup.length - 1))];
}
Color displayColorNormalized(double normalized) {
double clipped = Math.max(0D, Math.min(1D, normalized));
return displayColors[(int) Math.round(clipped * (displayColors.length - 1))];
}
String label() {
return label;
}
double minimum() {
return minimum;
}
double maximum() {
return maximum;
}
@Override
public String toString() {
return label;
}
private static int[] buildLookup(int[] stops) {
int[] values = new int[256];
for (int index = 0; index < values.length; index++) {
double position = (index / 255D) * (stops.length - 1);
int lowerIndex = Math.min(stops.length - 2, (int) position);
double fraction = position - lowerIndex;
values[index] = interpolate(stops[lowerIndex], stops[lowerIndex + 1], fraction);
}
return values;
}
private static Color[] buildDisplayColors(int[] lookup) {
Color[] colors = new Color[lookup.length];
for (int index = 0; index < lookup.length; index++) {
colors[index] = new Color(lookup[index]);
}
return colors;
}
private static int interpolate(int from, int to, double fraction) {
int red = channel(from, 16, to, fraction);
int green = channel(from, 8, to, fraction);
int blue = channel(from, 0, to, fraction);
return (red << 16) | (green << 8) | blue;
}
private static int channel(int from, int shift, int to, double fraction) {
int fromChannel = (from >>> shift) & 0xFF;
int toChannel = (to >>> shift) & 0xFF;
return (int) Math.round(fromChannel + ((toChannel - fromChannel) * fraction));
}
}
@@ -0,0 +1,393 @@
package art.arcane.iris.core.gui;
import art.arcane.iris.engine.framework.PreservationRegistry;
import art.arcane.iris.spi.IrisServices;
import art.arcane.volmlib.util.function.NoiseProvider;
import java.awt.image.BufferedImage;
import java.awt.image.DataBufferInt;
import java.util.Objects;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.RejectedExecutionException;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.atomic.AtomicReference;
final class NoiseRenderCoordinator implements AutoCloseable {
private final int workerCount;
private final Listener listener;
private final ThreadFactory workerThreadFactory;
private final ThreadFactory coordinatorThreadFactory;
private final AtomicReference<RenderGeneration> latestRequested = new AtomicReference<>();
private final AtomicLong latestRevision = new AtomicLong();
private final AtomicBoolean closed = new AtomicBoolean();
NoiseRenderCoordinator(Listener listener) {
this(Math.min(4, Math.max(1, Runtime.getRuntime().availableProcessors() / 2)), listener);
}
NoiseRenderCoordinator(int workerCount, Listener listener) {
this.workerCount = Math.max(1, workerCount);
this.listener = Objects.requireNonNull(listener, "listener");
AtomicInteger threadIds = new AtomicInteger();
workerThreadFactory = runnable -> {
Thread thread = new Thread(runnable, "Iris Noise Renderer " + threadIds.incrementAndGet());
thread.setDaemon(true);
thread.setPriority(Thread.MIN_PRIORITY);
return thread;
};
coordinatorThreadFactory = runnable -> {
Thread thread = new Thread(runnable, "Iris Noise Coordinator " + threadIds.incrementAndGet());
thread.setDaemon(true);
thread.setPriority(Thread.MIN_PRIORITY);
return thread;
};
}
void request(Request request) {
Objects.requireNonNull(request, "request");
if (closed.get()) {
return;
}
latestRevision.accumulateAndGet(request.revision(), Math::max);
cancelActiveRender();
RenderGeneration generation = createGeneration(request);
latestRequested.set(generation);
generation.start();
}
void cancel(long revision) {
latestRevision.accumulateAndGet(revision, Math::max);
cancelActiveRender();
}
boolean isClosed() {
return closed.get();
}
static int sampleStepForBudget(int width, int height, long sampleBudget) {
if (width < 1 || height < 1 || sampleBudget < 1L) {
throw new IllegalArgumentException("Invalid noise sample budget");
}
long totalSamples = (long) width * height;
int sampleStep = Math.max(1, (int) Math.ceil(Math.sqrt(totalSamples / (double) sampleBudget)));
while (sampleCount(width, height, sampleStep) > sampleBudget) {
sampleStep++;
}
return sampleStep;
}
static long sampleCount(int width, int height, int sampleStep) {
if (width < 1 || height < 1 || sampleStep < 1) {
throw new IllegalArgumentException("Invalid noise sample dimensions");
}
long outputWidth = ((long) width + sampleStep - 1L) / sampleStep;
long outputHeight = ((long) height + sampleStep - 1L) / sampleStep;
return outputWidth * outputHeight;
}
static int nextRefinementStep(int width, int height, int currentStep, long sampleBudget) {
if (currentStep <= 1) {
throw new IllegalArgumentException("Noise refinement requires a coarse input");
}
return Math.min(currentStep, sampleStepForBudget(width, height, sampleBudget));
}
static long timeBoundSampleBudget(long completedSamples, double milliseconds, double targetMilliseconds,
long minimumBudget, long maximumBudget) {
if (completedSamples < 1L
|| !Double.isFinite(milliseconds)
|| milliseconds <= 0D
|| !Double.isFinite(targetMilliseconds)
|| targetMilliseconds <= 0D
|| minimumBudget < 1L
|| maximumBudget < minimumBudget) {
throw new IllegalArgumentException("Invalid timed noise sample budget");
}
double projectedSamples = Math.ceil((completedSamples / milliseconds) * targetMilliseconds);
long timeBoundBudget = (long) Math.min(maximumBudget, projectedSamples);
return Math.max(minimumBudget, timeBoundBudget);
}
@Override
public void close() {
if (!closed.compareAndSet(false, true)) {
return;
}
latestRevision.incrementAndGet();
cancelActiveRender();
}
private void renderGeneration(RenderGeneration generation) {
Request request = generation.request();
if (!isCurrent(request)) {
generation.close();
return;
}
listener.onRenderStarted(request);
try {
Result result = render(generation);
if (result != null && isCurrent(request)) {
listener.onRenderCompleted(result);
}
} catch (Throwable error) {
if (isCurrent(request)) {
listener.onRenderFailed(request, error);
}
} finally {
latestRequested.compareAndSet(generation, null);
generation.close();
}
}
private Result render(RenderGeneration generation) throws InterruptedException {
Request request = generation.request();
long started = System.nanoTime();
int outputWidth = Math.max(1, (request.width() + request.sampleStep() - 1) / request.sampleStep());
int outputHeight = Math.max(1, (request.height() + request.sampleStep() - 1) / request.sampleStep());
BufferedImage image = new BufferedImage(outputWidth, outputHeight, BufferedImage.TYPE_INT_RGB);
int[] pixels = ((DataBufferInt) image.getRaster().getDataBuffer()).getData();
int bandCount = Math.min(workerCount, outputHeight);
BandStats[] bandStats = new BandStats[bandCount];
CountDownLatch completion = new CountDownLatch(bandCount);
AtomicReference<Throwable> failure = new AtomicReference<>();
for (int band = 0; band < bandCount; band++) {
bandStats[band] = new BandStats();
}
for (int band = 0; band < bandCount; band++) {
int bandIndex = band;
Runnable task = () -> {
try {
renderBand(request, image, pixels, bandIndex, bandCount, bandStats[bandIndex]);
} catch (Throwable error) {
failure.compareAndSet(null, error);
} finally {
completion.countDown();
}
};
try {
generation.executor().execute(task);
} catch (RejectedExecutionException exception) {
failure.compareAndSet(null, exception);
completion.countDown();
}
}
completion.await();
Throwable renderFailure = failure.get();
if (renderFailure != null) {
if (renderFailure instanceof RuntimeException runtimeException) {
throw runtimeException;
}
if (renderFailure instanceof Error error) {
throw error;
}
throw new IllegalStateException("Noise rendering failed", renderFailure);
}
if (!isCurrent(request)) {
return null;
}
RenderStats combined = combine(bandStats);
double milliseconds = (System.nanoTime() - started) / 1_000_000D;
return new Result(request, image, milliseconds, combined.samples, combined.minimum, combined.maximum,
combined.centerValue, combined.underflow, combined.overflow, combined.invalid);
}
private void renderBand(Request request, BufferedImage image, int[] pixels, int bandIndex, int bandCount,
BandStats stats) {
int outputWidth = image.getWidth();
int outputHeight = image.getHeight();
int fromY = (outputHeight * bandIndex) / bandCount;
int toY = (outputHeight * (bandIndex + 1)) / bandCount;
double sampleOffset = request.sampleStep() * 0.5D;
double startWorldX = request.viewport().worldX(sampleOffset, request.width());
double worldStep = request.viewport().blocksPerPixel() * request.sampleStep();
NoisePalette palette = request.palette();
double paletteMinimum = palette.minimum();
double paletteMaximum = palette.maximum();
for (int y = fromY; y < toY; y++) {
double screenY = (y * (double) request.sampleStep()) + sampleOffset;
double worldZ = request.viewport().worldZ(screenY, request.height());
double worldX = startWorldX;
int pixelIndex = y * outputWidth;
boolean centerRow = y == outputHeight / 2;
for (int x = 0; x < outputWidth; x++) {
if ((x & 31) == 0 && (!isCurrent(request) || Thread.currentThread().isInterrupted())) {
return;
}
double value = request.sampler().noise(worldX, worldZ);
boolean center = centerRow && x == outputWidth / 2;
if (Double.isFinite(value)) {
pixels[pixelIndex++] = palette.colorFinite(value);
stats.acceptFinite(value, center, paletteMinimum, paletteMaximum);
} else {
pixels[pixelIndex++] = NoisePalette.INVALID_COLOR;
stats.acceptInvalid(value, center);
}
worldX += worldStep;
}
}
}
private boolean isCurrent(Request request) {
return !closed.get()
&& latestRequested.get() != null
&& latestRequested.get().request() == request
&& request.revision() >= latestRevision.get();
}
private void cancelActiveRender() {
RenderGeneration generation = latestRequested.getAndSet(null);
if (generation != null) {
generation.close();
}
}
private RenderGeneration createGeneration(Request request) {
ThreadPoolExecutor executor = new ThreadPoolExecutor(
workerCount,
workerCount,
0L,
TimeUnit.MILLISECONDS,
new ArrayBlockingQueue<>(Math.max(1, workerCount)),
workerThreadFactory,
new ThreadPoolExecutor.AbortPolicy()
);
PreservationRegistry preservation = IrisServices.getOrNull(PreservationRegistry.class);
if (preservation != null) {
preservation.register(executor);
}
RenderGeneration generation = new RenderGeneration(request, executor);
Thread actualRunner = coordinatorThreadFactory.newThread(() -> renderGeneration(generation));
generation.setRunner(actualRunner);
if (preservation != null) {
preservation.register(actualRunner);
}
return generation;
}
private static RenderStats combine(BandStats[] bands) {
long samples = 0L;
long underflow = 0L;
long overflow = 0L;
long invalid = 0L;
double minimum = Double.POSITIVE_INFINITY;
double maximum = Double.NEGATIVE_INFINITY;
double centerValue = Double.NaN;
for (BandStats band : bands) {
samples += band.samples;
underflow += band.underflow;
overflow += band.overflow;
invalid += band.invalid;
minimum = Math.min(minimum, band.minimum);
maximum = Math.max(maximum, band.maximum);
if (!Double.isNaN(band.centerValue)) {
centerValue = band.centerValue;
}
}
if (minimum == Double.POSITIVE_INFINITY) {
minimum = Double.NaN;
maximum = Double.NaN;
}
return new RenderStats(samples, minimum, maximum, centerValue, underflow, overflow, invalid);
}
interface Listener {
void onRenderStarted(Request request);
void onRenderCompleted(Result result);
void onRenderFailed(Request request, Throwable error);
}
record Request(long revision, NoiseProvider sampler, NoiseViewport viewport, NoisePalette palette,
int width, int height, int sampleStep) {
Request {
Objects.requireNonNull(sampler, "sampler");
Objects.requireNonNull(viewport, "viewport");
Objects.requireNonNull(palette, "palette");
if (revision < 0L || width < 1 || height < 1 || sampleStep < 1) {
throw new IllegalArgumentException("Invalid noise render request");
}
}
}
record Result(Request request, BufferedImage image, double milliseconds, long samples, double minimum,
double maximum, double centerValue, long underflow, long overflow, long invalid) {
}
private static final class RenderGeneration {
private final Request request;
private final ThreadPoolExecutor executor;
private volatile Thread runner;
private RenderGeneration(Request request, ThreadPoolExecutor executor) {
this.request = Objects.requireNonNull(request, "request");
this.executor = Objects.requireNonNull(executor, "executor");
}
private Request request() {
return request;
}
private ThreadPoolExecutor executor() {
return executor;
}
private void setRunner(Thread runner) {
this.runner = Objects.requireNonNull(runner, "runner");
}
private void start() {
runner.start();
}
private void close() {
Thread activeRunner = runner;
if (activeRunner != null) {
activeRunner.interrupt();
}
executor.shutdownNow();
}
}
private static final class BandStats {
private long samples;
private long underflow;
private long overflow;
private long invalid;
private double minimum = Double.POSITIVE_INFINITY;
private double maximum = Double.NEGATIVE_INFINITY;
private double centerValue = Double.NaN;
private void acceptFinite(double value, boolean center, double paletteMinimum, double paletteMaximum) {
samples++;
if (center) {
centerValue = value;
}
minimum = Math.min(minimum, value);
maximum = Math.max(maximum, value);
if (value < paletteMinimum) {
underflow++;
} else if (value > paletteMaximum) {
overflow++;
}
}
private void acceptInvalid(double value, boolean center) {
samples++;
invalid++;
if (center) {
centerValue = value;
}
}
}
private record RenderStats(long samples, double minimum, double maximum, double centerValue,
long underflow, long overflow, long invalid) {
}
}
@@ -0,0 +1,44 @@
package art.arcane.iris.core.gui;
record NoiseViewport(double centerX, double centerZ, double blocksPerPixel) {
static final double MIN_BLOCKS_PER_PIXEL = 0.0001D;
static final double MAX_BLOCKS_PER_PIXEL = 1_000_000D;
NoiseViewport {
if (!Double.isFinite(centerX) || !Double.isFinite(centerZ)) {
throw new IllegalArgumentException("Viewport center must be finite");
}
if (!Double.isFinite(blocksPerPixel) || blocksPerPixel <= 0D) {
throw new IllegalArgumentException("Viewport scale must be finite and positive");
}
}
double worldX(double screenX, int width) {
return centerX + ((screenX - (width / 2D)) * blocksPerPixel);
}
double worldZ(double screenZ, int height) {
return centerZ + ((screenZ - (height / 2D)) * blocksPerPixel);
}
NoiseViewport panPixels(double deltaX, double deltaZ) {
return new NoiseViewport(
centerX - (deltaX * blocksPerPixel),
centerZ - (deltaZ * blocksPerPixel),
blocksPerPixel
);
}
NoiseViewport zoomAt(double screenX, double screenZ, int width, int height, double factor) {
if (!Double.isFinite(factor) || factor <= 0D) {
throw new IllegalArgumentException("Zoom factor must be finite and positive");
}
double anchorX = worldX(screenX, width);
double anchorZ = worldZ(screenZ, height);
double nextScale = Math.max(MIN_BLOCKS_PER_PIXEL,
Math.min(MAX_BLOCKS_PER_PIXEL, blocksPerPixel * factor));
double nextCenterX = anchorX - ((screenX - (width / 2D)) * nextScale);
double nextCenterZ = anchorZ - ((screenZ - (height / 2D)) * nextScale);
return new NoiseViewport(nextCenterX, nextCenterZ, nextScale);
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,626 @@
/*
* Iris is a World Generator for Minecraft Servers
* Copyright (c) 2026 Arcane Arts (Volmit Software)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package art.arcane.iris.core.gui;
import art.arcane.iris.engine.framework.PreservationRegistry;
import art.arcane.iris.engine.framework.render.IrisRenderer;
import art.arcane.iris.engine.framework.render.RenderType;
import art.arcane.iris.spi.IrisLogging;
import art.arcane.iris.spi.IrisServices;
import java.awt.EventQueue;
import java.awt.image.BufferedImage;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.Callable;
import java.util.concurrent.CancellationException;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Future;
import java.util.concurrent.FutureTask;
import java.util.concurrent.RejectedExecutionException;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicLong;
import java.util.function.Consumer;
final class VisionRenderController implements AutoCloseable {
static final int TILE_PIXELS = 64;
static final double MINIMUM_BLOCKS_PER_PIXEL = 1D;
static final double MAXIMUM_BLOCKS_PER_PIXEL = 4_096D;
private static final int MAXIMUM_WORKERS = 3;
private static final int MAXIMUM_VISIBLE_TILES = 32_768;
private static final int RENDER_QUEUE_CAPACITY = 12;
private static final int PROBE_QUEUE_CAPACITY = 1;
private static final long CACHE_BYTES = 64L * 1024L * 1024L;
private final Runnable listener;
private final int renderWorkerCount;
private final ThreadPoolExecutor renderExecutor;
private final ThreadPoolExecutor probeExecutor;
private final WeightedTileCache cache;
private final AtomicLong viewSequence;
private final AtomicLong probeSequence;
private final AtomicBoolean closed;
private final AtomicBoolean publicationQueued;
private final AtomicBoolean publicationDirty;
private final Set<Future<?>> activeRenderTasks;
private volatile Frame currentFrame;
private volatile WorkState currentWork;
private volatile CancellationToken currentToken;
VisionRenderController(Runnable listener) {
this(listener, RuntimeOptions.production());
}
VisionRenderController(Runnable listener, RuntimeOptions options) {
this.listener = Objects.requireNonNull(listener, "listener");
Objects.requireNonNull(options, "options");
AtomicInteger threadSequence = new AtomicInteger();
this.renderWorkerCount = options.workers();
this.renderExecutor = new ThreadPoolExecutor(
options.workers(),
options.workers(),
0L,
TimeUnit.MILLISECONDS,
new ArrayBlockingQueue<>(options.renderQueueCapacity()),
daemonFactory("Iris Vision Render", Thread.NORM_PRIORITY, threadSequence),
new ThreadPoolExecutor.AbortPolicy()
);
this.probeExecutor = new ThreadPoolExecutor(
1,
1,
0L,
TimeUnit.MILLISECONDS,
new ArrayBlockingQueue<>(PROBE_QUEUE_CAPACITY),
daemonFactory("Iris Vision Probe", Thread.MIN_PRIORITY, threadSequence),
new ThreadPoolExecutor.DiscardOldestPolicy()
);
this.cache = new WeightedTileCache(CACHE_BYTES);
this.viewSequence = new AtomicLong();
this.probeSequence = new AtomicLong();
this.closed = new AtomicBoolean();
this.publicationQueued = new AtomicBoolean();
this.publicationDirty = new AtomicBoolean();
this.activeRenderTasks = ConcurrentHashMap.newKeySet();
if (options.registerPreservation()) {
PreservationRegistry preservation = IrisServices.getOrNull(PreservationRegistry.class);
if (preservation != null) {
preservation.register(renderExecutor);
preservation.register(probeExecutor);
}
}
}
synchronized Frame request(RenderSpec spec) {
Objects.requireNonNull(spec, "spec");
if (closed.get()) {
throw new IllegalStateException("Vision render controller is closed");
}
CancellationToken previousToken = currentToken;
if (previousToken != null) {
previousToken.cancel();
}
renderExecutor.getQueue().clear();
cancelActiveRenderTasks();
probeSequence.incrementAndGet();
probeExecutor.getQueue().clear();
List<VisibleTile> tiles = visibleTiles(spec);
Frame frame = new Frame(viewSequence.incrementAndGet(), spec, tiles);
for (VisibleTile tile : tiles) {
tile.setImage(cache.get(frame.key(tile)));
}
CancellationToken token = new CancellationToken();
WorkState work = new WorkState(frame, token);
currentFrame = frame;
currentToken = token;
currentWork = work;
schedule(work);
publish(frame, token);
return frame;
}
Frame currentFrame() {
return currentFrame;
}
BufferedImage image(Frame frame, VisibleTile tile) {
if (frame == null || tile == null) {
return null;
}
return tile.image();
}
Progress progress(Frame frame) {
if (frame == null) {
return new Progress(0, 0, 0, 0);
}
int ready = 0;
for (VisibleTile tile : frame.tiles()) {
if (tile.image() != null) {
ready++;
}
}
return new Progress(frame.tiles().size(), ready, renderExecutor.getActiveCount(), renderExecutor.getQueue().size());
}
<T> void submitProbe(Frame frame, Callable<T> probe, Consumer<T> consumer) {
Objects.requireNonNull(probe, "probe");
Objects.requireNonNull(consumer, "consumer");
if (frame == null || closed.get() || currentFrame != frame) {
return;
}
long probeRevision = probeSequence.incrementAndGet();
probeExecutor.getQueue().clear();
try {
probeExecutor.execute(() -> runProbe(frame, probeRevision, probe, consumer));
} catch (RejectedExecutionException ignored) {
probeExecutor.getQueue().clear();
}
}
@Override
public synchronized void close() {
if (!closed.compareAndSet(false, true)) {
return;
}
CancellationToken token = currentToken;
if (token != null) {
token.cancel();
}
currentWork = null;
currentFrame = null;
renderExecutor.getQueue().clear();
cancelActiveRenderTasks();
probeExecutor.getQueue().clear();
renderExecutor.shutdownNow();
probeExecutor.shutdownNow();
cache.clear();
}
static List<VisibleTile> visibleTiles(RenderSpec spec) {
Objects.requireNonNull(spec, "spec");
double blocksPerPixel = spec.blocksPerPixel();
double tileSpan = TILE_PIXELS * blocksPerPixel;
double halfWidth = spec.width() * blocksPerPixel * 0.5D;
double halfHeight = spec.height() * blocksPerPixel * 0.5D;
long minimumX = floorTile(spec.centerX() - halfWidth, tileSpan);
long maximumX = floorTile(Math.nextDown(spec.centerX() + halfWidth), tileSpan);
long minimumZ = floorTile(spec.centerZ() - halfHeight, tileSpan);
long maximumZ = floorTile(Math.nextDown(spec.centerZ() + halfHeight), tileSpan);
long tileCount = Math.multiplyExact(maximumX - minimumX + 1L, maximumZ - minimumZ + 1L);
if (tileCount > MAXIMUM_VISIBLE_TILES) {
throw new IllegalArgumentException("Vision viewport contains too many tiles");
}
ArrayList<VisibleTile> tiles = new ArrayList<>((int) tileCount);
double centerTileX = spec.centerX() / tileSpan;
double centerTileZ = spec.centerZ() / tileSpan;
for (long tileZ = minimumZ; ; tileZ++) {
for (long tileX = minimumX; ; tileX++) {
int screenX = (int) Math.round(spec.width() * 0.5D + (tileX * tileSpan - spec.centerX()) / blocksPerPixel);
int screenY = (int) Math.round(spec.height() * 0.5D + (tileZ * tileSpan - spec.centerZ()) / blocksPerPixel);
double deltaX = tileX + 0.5D - centerTileX;
double deltaZ = tileZ + 0.5D - centerTileZ;
tiles.add(new VisibleTile(tileX, tileZ, screenX, screenY, deltaX * deltaX + deltaZ * deltaZ));
if (tileX == maximumX) {
break;
}
}
if (tileZ == maximumZ) {
break;
}
}
tiles.sort(Comparator.comparingDouble(VisibleTile::distanceSquared)
.thenComparingLong(VisibleTile::tileZ)
.thenComparingLong(VisibleTile::tileX));
return List.copyOf(tiles);
}
static long sampleCount(int tileCount) {
if (tileCount < 1) {
throw new IllegalArgumentException("Vision tile count must be positive");
}
return Math.multiplyExact((long) tileCount, (long) TILE_PIXELS * TILE_PIXELS);
}
private static long floorTile(double coordinate, double tileSpan) {
double tile = Math.floor(coordinate / tileSpan);
if (tile < Long.MIN_VALUE || tile > Long.MAX_VALUE) {
throw new IllegalArgumentException("Vision viewport exceeds tile coordinate range");
}
return (long) tile;
}
private static ThreadFactory daemonFactory(String name, int priority, AtomicInteger sequence) {
return (Runnable runnable) -> {
Thread thread = new Thread(runnable, name + " " + sequence.incrementAndGet());
thread.setDaemon(true);
thread.setPriority(priority);
thread.setUncaughtExceptionHandler((Thread failedThread, Throwable error) -> IrisLogging.reportError(error));
return thread;
};
}
private void schedule(WorkState work) {
synchronized (work) {
if (!isCurrent(work)) {
return;
}
int admissionLimit = work.frame().spec().type() == RenderType.RIVER ? 1 : renderWorkerCount;
while (work.inFlight() < admissionLimit) {
VisibleTile tile = work.nextMissing();
if (tile == null) {
return;
}
work.incrementInFlight();
TrackedRenderTask task = new TrackedRenderTask(() -> render(work, tile), activeRenderTasks);
activeRenderTasks.add(task);
try {
renderExecutor.execute(task);
} catch (RejectedExecutionException ignored) {
task.cancel(false);
work.decrementInFlight();
return;
}
}
}
}
private void render(WorkState work, VisibleTile tile) {
Frame frame = work.frame();
CancellationToken token = work.token();
try {
if (!isCurrent(frame, token)) {
return;
}
double tileSpan = TILE_PIXELS * frame.spec().blocksPerPixel();
BufferedImage image = frame.spec().renderer().renderStudio(
tile.tileX() * tileSpan,
tile.tileZ() * tileSpan,
tileSpan,
TILE_PIXELS,
frame.spec().type(),
() -> !isCurrent(frame, token)
);
if (!isCurrent(frame, token)) {
return;
}
cache.put(frame.key(tile), image);
tile.setImage(image);
publish(frame, token);
} catch (CancellationException ignored) {
} catch (Throwable error) {
IrisLogging.debug("Vision tile render failed: " + error.getClass().getSimpleName() + ": " + error.getMessage());
} finally {
complete(work);
}
}
private void complete(WorkState work) {
synchronized (work) {
work.decrementInFlight();
if (!isCurrent(work)) {
return;
}
}
schedule(work);
}
private <T> void runProbe(Frame frame, long probeRevision, Callable<T> probe, Consumer<T> consumer) {
try {
if (!isProbeCurrent(frame, probeRevision)) {
return;
}
T result = probe.call();
if (!isProbeCurrent(frame, probeRevision)) {
return;
}
EventQueue.invokeLater(() -> {
if (isProbeCurrent(frame, probeRevision)) {
consumer.accept(result);
}
});
} catch (InterruptedException interrupted) {
Thread.currentThread().interrupt();
} catch (Throwable error) {
IrisLogging.debug("Vision probe failed: " + error.getClass().getSimpleName() + ": " + error.getMessage());
}
}
private boolean isProbeCurrent(Frame frame, long probeRevision) {
return !closed.get() && currentFrame == frame && probeSequence.get() == probeRevision;
}
private boolean isCurrent(Frame frame, CancellationToken token) {
return !closed.get() && !token.cancelled() && currentFrame == frame;
}
private boolean isCurrent(WorkState work) {
return currentWork == work && isCurrent(work.frame(), work.token());
}
private void publish(Frame frame, CancellationToken token) {
if (!isCurrent(frame, token)) {
return;
}
publicationDirty.set(true);
queuePublication();
}
private void queuePublication() {
if (!publicationQueued.compareAndSet(false, true)) {
return;
}
EventQueue.invokeLater(() -> {
try {
if (publicationDirty.getAndSet(false) && !closed.get()) {
listener.run();
}
} finally {
publicationQueued.set(false);
if (publicationDirty.get() && !closed.get()) {
queuePublication();
}
}
});
}
private void cancelActiveRenderTasks() {
for (Future<?> task : activeRenderTasks) {
task.cancel(true);
}
activeRenderTasks.clear();
}
record RenderSpec(
IrisRenderer renderer,
RenderType type,
long contentRevision,
double centerX,
double centerZ,
double blocksPerPixel,
int width,
int height
) {
RenderSpec {
Objects.requireNonNull(renderer, "renderer");
Objects.requireNonNull(type, "type");
if (!Double.isFinite(centerX) || !Double.isFinite(centerZ)) {
throw new IllegalArgumentException("Vision center must be finite");
}
if (!Double.isFinite(blocksPerPixel)
|| blocksPerPixel < MINIMUM_BLOCKS_PER_PIXEL
|| blocksPerPixel > MAXIMUM_BLOCKS_PER_PIXEL) {
throw new IllegalArgumentException("Vision scale is outside the supported range");
}
if (width < 1 || height < 1) {
throw new IllegalArgumentException("Vision viewport must be positive");
}
}
}
record Frame(long viewRevision, RenderSpec spec, List<VisibleTile> tiles) {
Frame {
Objects.requireNonNull(spec, "spec");
tiles = List.copyOf(tiles);
}
TileKey key(VisibleTile tile) {
return new TileKey(spec.contentRevision(), spec.type(), spec.blocksPerPixel(), tile.tileX(), tile.tileZ());
}
}
static final class VisibleTile {
private final long tileX;
private final long tileZ;
private final int screenX;
private final int screenY;
private final double distanceSquared;
private volatile BufferedImage image;
private VisibleTile(long tileX, long tileZ, int screenX, int screenY, double distanceSquared) {
this.tileX = tileX;
this.tileZ = tileZ;
this.screenX = screenX;
this.screenY = screenY;
this.distanceSquared = distanceSquared;
}
long tileX() {
return tileX;
}
long tileZ() {
return tileZ;
}
int screenX() {
return screenX;
}
int screenY() {
return screenY;
}
double distanceSquared() {
return distanceSquared;
}
BufferedImage image() {
return image;
}
void setImage(BufferedImage image) {
this.image = image;
}
}
record TileKey(long contentRevision, RenderType type, double blocksPerPixel, long tileX, long tileZ) {
TileKey {
Objects.requireNonNull(type, "type");
if (!Double.isFinite(blocksPerPixel) || blocksPerPixel <= 0D) {
throw new IllegalArgumentException("Vision cache scale must be finite and positive");
}
}
}
record Progress(int total, int ready, int active, int queued) {
double completion() {
if (total == 0) {
return 1D;
}
return Math.min(1D, ready / (double) total);
}
}
record RuntimeOptions(int workers, int renderQueueCapacity, boolean registerPreservation) {
RuntimeOptions {
if (workers < 1 || renderQueueCapacity < 1) {
throw new IllegalArgumentException("Vision render runtime options are invalid");
}
}
static RuntimeOptions production() {
int processors = Math.max(1, Runtime.getRuntime().availableProcessors());
int workers = Math.max(1, Math.min(MAXIMUM_WORKERS, processors));
return new RuntimeOptions(workers, RENDER_QUEUE_CAPACITY, true);
}
}
private static final class CancellationToken {
private final AtomicBoolean cancelled = new AtomicBoolean();
void cancel() {
cancelled.set(true);
}
boolean cancelled() {
return cancelled.get();
}
}
private static final class TrackedRenderTask extends FutureTask<Void> {
private final Set<Future<?>> tasks;
private TrackedRenderTask(Runnable task, Set<Future<?>> tasks) {
super(task, null);
this.tasks = tasks;
}
@Override
protected void done() {
tasks.remove(this);
}
}
private static final class WorkState {
private final Frame frame;
private final CancellationToken token;
private int index;
private int inFlight;
private WorkState(Frame frame, CancellationToken token) {
this.frame = frame;
this.token = token;
}
Frame frame() {
return frame;
}
CancellationToken token() {
return token;
}
VisibleTile nextMissing() {
while (index < frame.tiles().size()) {
VisibleTile tile = frame.tiles().get(index++);
if (tile.image() == null) {
return tile;
}
}
return null;
}
int inFlight() {
return inFlight;
}
void incrementInFlight() {
inFlight++;
}
void decrementInFlight() {
inFlight--;
}
}
private static final class WeightedTileCache {
private final long maximumBytes;
private final LinkedHashMap<TileKey, CacheEntry> entries;
private long bytes;
private WeightedTileCache(long maximumBytes) {
this.maximumBytes = maximumBytes;
this.entries = new LinkedHashMap<>(128, 0.75F, true);
}
synchronized BufferedImage get(TileKey key) {
CacheEntry entry = entries.get(key);
return entry == null ? null : entry.image();
}
synchronized void put(TileKey key, BufferedImage image) {
long imageBytes = (long) image.getWidth() * image.getHeight() * Integer.BYTES;
CacheEntry previous = entries.put(key, new CacheEntry(image, imageBytes));
if (previous != null) {
bytes -= previous.bytes();
}
bytes += imageBytes;
while (bytes > maximumBytes && !entries.isEmpty()) {
Iterator<Map.Entry<TileKey, CacheEntry>> iterator = entries.entrySet().iterator();
Map.Entry<TileKey, CacheEntry> eldest = iterator.next();
bytes -= eldest.getValue().bytes();
iterator.remove();
}
}
synchronized void clear() {
entries.clear();
bytes = 0L;
}
}
private record CacheEntry(BufferedImage image, long bytes) {
}
}
@@ -0,0 +1,42 @@
package art.arcane.iris.core.gui;
record VisionViewport(double centerX, double centerZ, double blocksPerPixel) {
VisionViewport {
if (!Double.isFinite(centerX) || !Double.isFinite(centerZ)) {
throw new IllegalArgumentException("Vision viewport center must be finite");
}
if (!Double.isFinite(blocksPerPixel) || blocksPerPixel <= 0D) {
throw new IllegalArgumentException("Vision viewport scale must be finite and positive");
}
}
double worldX(double screenX, int width) {
return centerX + (screenX - width * 0.5D) * blocksPerPixel;
}
double worldZ(double screenZ, int height) {
return centerZ + (screenZ - height * 0.5D) * blocksPerPixel;
}
VisionViewport zoomAt(
double screenX,
double screenZ,
int width,
int height,
double factor,
double minimumScale,
double maximumScale
) {
if (!Double.isFinite(factor) || factor <= 0D) {
throw new IllegalArgumentException("Vision zoom factor must be finite and positive");
}
double anchorX = worldX(screenX, width);
double anchorZ = worldZ(screenZ, height);
double nextScale = Math.max(minimumScale, Math.min(maximumScale, blocksPerPixel * factor));
return new VisionViewport(
anchorX - (screenX - width * 0.5D) * nextScale,
anchorZ - (screenZ - height * 0.5D) * nextScale,
nextScale
);
}
}
@@ -10,7 +10,6 @@ public final class DesktopUiMessages {
public static final TextKey VISION_VIEW = TextKey.of("iris.desktop.vision.view", "View:");
public static final TextKey VISION_GRID = TextKey.of("iris.desktop.vision.grid", "Grid");
public static final TextKey VISION_FOLLOW = TextKey.of("iris.desktop.vision.follow", "Follow");
public static final TextKey VISION_LOW_QUALITY_SHORT = TextKey.of("iris.desktop.vision.low_quality_short", "LQ");
public static final TextKey VISION_REFRESHING = TextKey.of("iris.desktop.vision.refreshing", "Refreshing");
public static final TextKey VISION_FPS = TextKey.of("iris.desktop.vision.fps", "{fps} FPS");
public static final TextKey VISION_ZOOM_RESET = TextKey.of("iris.desktop.vision.zoom_reset", "Zoom reset");
@@ -19,8 +18,6 @@ public final class DesktopUiMessages {
public static final TextKey VISION_FOLLOWING = TextKey.of("iris.desktop.vision.following", "Following {player}");
public static final TextKey VISION_NO_PLAYER = TextKey.of("iris.desktop.vision.no_player", "No player in world");
public static final TextKey VISION_FOLLOW_DISABLED = TextKey.of("iris.desktop.vision.follow_disabled", "Follow disabled");
public static final TextKey VISION_LOW_QUALITY = TextKey.of("iris.desktop.vision.low_quality", "Low quality");
public static final TextKey VISION_HIGH_QUALITY = TextKey.of("iris.desktop.vision.high_quality", "High quality");
public static final TextKey VISION_STATUS_LEFT = TextKey.of("iris.desktop.vision.status_left", "{mode} | {bpp} bpp | {width} x {height} blocks");
public static final TextKey VISION_STATUS_RIGHT = TextKey.of("iris.desktop.vision.status_right", "X: {x} Z: {z} | {fps} FPS");
public static final TextKey VISION_ENTITY_POSITION = TextKey.of("iris.desktop.vision.entity_position", "Position: {x}, {y}, {z}");
@@ -31,8 +28,8 @@ public final class DesktopUiMessages {
public static final TextKey VISION_BIOME_KEY = TextKey.of("iris.desktop.vision.biome_key", "Key: {key}");
public static final TextKey VISION_BIOME_FILE = TextKey.of("iris.desktop.vision.biome_file", "File: {file}");
public static final TextKey VISION_VELOCITY = TextKey.of("iris.desktop.vision.velocity", "Velocity: {velocity}");
public static final TextKey VISION_TILES = TextKey.of("iris.desktop.vision.tiles", "Tiles: {high} HD / {low} LQ");
public static final TextKey VISION_WORKERS = TextKey.of("iris.desktop.vision.workers", "Workers: {high} HD / {low} LQ");
public static final TextKey VISION_TILES = TextKey.of("iris.desktop.vision.tiles", "Atlas pages: {ready} / {total} exact");
public static final TextKey VISION_WORKERS = TextKey.of("iris.desktop.vision.workers", "Workers: {active} active / {queued} queued");
public static final TextKey VISION_CENTER = TextKey.of("iris.desktop.vision.center", "Center: {x}, {z}");
public static final TextKey VISION_HELP_TOGGLE = TextKey.of("iris.desktop.vision.help.toggle", "Toggle help");
public static final TextKey VISION_HELP_REFRESH = TextKey.of("iris.desktop.vision.help.refresh", "Refresh tiles");
@@ -40,7 +37,6 @@ public final class DesktopUiMessages {
public static final TextKey VISION_HELP_ZOOM = TextKey.of("iris.desktop.vision.help.zoom", "Zoom in/out");
public static final TextKey VISION_HELP_RESET_ZOOM = TextKey.of("iris.desktop.vision.help.reset_zoom", "Reset zoom");
public static final TextKey VISION_HELP_CYCLE_MODE = TextKey.of("iris.desktop.vision.help.cycle_mode", "Cycle render mode");
public static final TextKey VISION_HELP_QUALITY = TextKey.of("iris.desktop.vision.help.quality", "Toggle tile quality");
public static final TextKey VISION_HELP_FPS = TextKey.of("iris.desktop.vision.help.fps", "Toggle 30/60 FPS");
public static final TextKey VISION_HELP_GRID = TextKey.of("iris.desktop.vision.help.grid", "Toggle grid");
public static final TextKey VISION_HELP_BIOME = TextKey.of("iris.desktop.vision.help.biome", "Detailed biome info");
@@ -53,6 +49,7 @@ public final class DesktopUiMessages {
public static final TextKey VISION_MODE_BIOME_SEA = TextKey.of("iris.desktop.vision.mode.biome_sea", "Biome sea");
public static final TextKey VISION_MODE_REGION = TextKey.of("iris.desktop.vision.mode.region", "Region");
public static final TextKey VISION_MODE_CAVE_LAND = TextKey.of("iris.desktop.vision.mode.cave_land", "Cave land");
public static final TextKey VISION_MODE_RIVER = TextKey.of("iris.desktop.vision.mode.river", "River network");
public static final TextKey VISION_MODE_HEIGHT = TextKey.of("iris.desktop.vision.mode.height", "Height");
public static final TextKey VISION_MODE_OBJECT_LOAD = TextKey.of("iris.desktop.vision.mode.object_load", "Object load");
public static final TextKey VISION_MODE_DECORATOR_LOAD = TextKey.of("iris.desktop.vision.mode.decorator_load", "Decorator load");
@@ -95,17 +92,17 @@ public final class DesktopUiMessages {
public static final TextKey PREGEN_MEMORY = TextKey.of("iris.desktop.pregen.memory", "Memory: {used} ({usage}) Pressure: {pressure}/s");
private static final List<MessageKey> KEYS = List.of(
VISION_TITLE, VISION_VIEW, VISION_GRID, VISION_FOLLOW, VISION_LOW_QUALITY_SHORT,
VISION_TITLE, VISION_VIEW, VISION_GRID, VISION_FOLLOW,
VISION_REFRESHING, VISION_FPS, VISION_ZOOM_RESET, VISION_GRID_ENABLED, VISION_GRID_DISABLED,
VISION_FOLLOWING, VISION_NO_PLAYER, VISION_FOLLOW_DISABLED, VISION_LOW_QUALITY, VISION_HIGH_QUALITY,
VISION_FOLLOWING, VISION_NO_PLAYER, VISION_FOLLOW_DISABLED,
VISION_STATUS_LEFT, VISION_STATUS_RIGHT, VISION_ENTITY_POSITION, VISION_ENTITY_HEALTH,
VISION_BLOCK_POSITION, VISION_CHUNK_POSITION, VISION_REGION_POSITION, VISION_BIOME_KEY,
VISION_BIOME_FILE, VISION_VELOCITY, VISION_TILES, VISION_WORKERS, VISION_CENTER,
VISION_HELP_TOGGLE, VISION_HELP_REFRESH, VISION_HELP_FOLLOW, VISION_HELP_ZOOM,
VISION_HELP_RESET_ZOOM, VISION_HELP_CYCLE_MODE, VISION_HELP_QUALITY, VISION_HELP_FPS,
VISION_HELP_RESET_ZOOM, VISION_HELP_CYCLE_MODE, VISION_HELP_FPS,
VISION_HELP_GRID, VISION_HELP_BIOME, VISION_HELP_TELEPORT, VISION_HELP_EDITOR, VISION_OPENED,
VISION_TELEPORTING, VISION_MODE_BIOME, VISION_MODE_BIOME_LAND, VISION_MODE_BIOME_SEA,
VISION_MODE_REGION, VISION_MODE_CAVE_LAND, VISION_MODE_HEIGHT, VISION_MODE_OBJECT_LOAD,
VISION_MODE_REGION, VISION_MODE_CAVE_LAND, VISION_MODE_RIVER, VISION_MODE_HEIGHT, VISION_MODE_OBJECT_LOAD,
VISION_MODE_DECORATOR_LOAD, VISION_MODE_CONTINENT, VISION_MODE_LAYER_LOAD, NOISE_TITLE,
NOISE_TITLE_GENERATOR, NOISE_SEARCH, NOISE_STATUS, NOISE_CATEGORY_CUSTOM,
NOISE_CATEGORY_PACK_GENERATORS, NOISE_CATEGORY_SIMPLEX, NOISE_CATEGORY_PERLIN,
@@ -239,7 +239,7 @@ public interface INMSBinding {
IrisImportedStructureControl importedStructures
) throws NoSuchFieldException, IllegalAccessException;
void completeStudioStructureBootstrap(World world) throws NoSuchFieldException, IllegalAccessException;
CompletableFuture<Void> completeStudioStructureBootstrap(World world) throws NoSuchFieldException, IllegalAccessException;
void abandonStudioStructureBootstrap(World world);
@@ -49,6 +49,7 @@ import org.bukkit.inventory.ItemStack;
import java.awt.Color;
import java.util.List;
import java.util.Set;
import java.util.concurrent.CompletableFuture;
import java.util.stream.StreamSupport;
public class NMSBinding1X implements INMSBinding {
@@ -117,7 +118,8 @@ public class NMSBinding1X implements INMSBinding {
}
@Override
public void completeStudioStructureBootstrap(World world) {
public CompletableFuture<Void> completeStudioStructureBootstrap(World world) {
return CompletableFuture.completedFuture(null);
}
@Override
@@ -0,0 +1,876 @@
package art.arcane.iris.core.pack;
import art.arcane.iris.engine.object.NoiseStyle;
import art.arcane.iris.engine.river.RiverTopologyComplexity;
import art.arcane.volmlib.util.json.JSONArray;
import art.arcane.volmlib.util.json.JSONObject;
import java.io.File;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
final class PackRiverValidator {
private static final Set<String> WATER_MODES = Set.of("SEA_LEVEL", "TERRACED");
private static final Set<String> TERMINAL_MODES = Set.of("SUPPRESS", "DRY_CHANNEL", "SINKHOLE_GROTTO");
private static final Set<String> ROUTING_POLICIES = Set.of("ALLOW", "AVOID", "BLOCK");
private static final Set<String> CAVE_MODES = Set.of(
"SEALED",
"FLOOD_CLOSED_COMPONENT",
"GENERATE_GROTTO",
"GROTTO_OR_CLOSED_COMPONENT",
"WATERFALL_POOL"
);
private static final Set<String> CAVE_FALLBACKS = Set.of("SEALED", "GENERATE_GROTTO");
private static final Set<String> EXISTING_FLUID_POLICIES = Set.of("REJECT", "ALLOW_SAME", "REPLACE");
private static final Set<String> UNSAFE_RIVER_STREAMS = Set.of("HEIGHT", "HEIGHT_OR_FLUID", "SLOPE");
private static final Set<String> NOISE_STYLES = noiseStyles();
private PackRiverValidator() {
}
static Validation validate(File packFolder, File[] dimensionFiles) {
List<String> errors = new ArrayList<>();
List<String> warnings = new ArrayList<>();
if (packFolder == null || !packFolder.isDirectory() || dimensionFiles == null) {
return new Validation(errors, warnings);
}
boolean enabled = false;
List<DimensionRiverContext> contexts = new ArrayList<>();
List<File> sortedDimensions = new ArrayList<>(List.of(dimensionFiles));
sortedDimensions.sort(Comparator.comparing(File::getPath));
for (File dimensionFile : sortedDimensions) {
JSONObject dimension = PackValidationIo.readJson(dimensionFile);
if (dimension == null || !dimension.has("rivers")) {
continue;
}
String dimensionKey = PackValidationIo.stripExtension(dimensionFile.getName());
String path = "Dimension '" + dimensionKey + "' rivers";
JSONObject rivers = requireObject(dimension, "rivers", path, errors);
if (rivers == null) {
continue;
}
PackJsonFieldChecks.validateOptionalBoolean(path, rivers, "enabled", errors);
if (!booleanValue(rivers, "enabled", false)) {
continue;
}
enabled = true;
DimensionRiverContext context = new DimensionRiverContext(
dimensionKey,
dimension,
rivers,
referencedKeys(dimension.optJSONArray("regions"))
);
contexts.add(context);
validateNetwork(packFolder, path, context, errors, warnings);
}
if (enabled) {
validateOverrides(packFolder, new File(packFolder, "regions"), "Region", contexts, errors, warnings);
validateOverrides(packFolder, new File(packFolder, "biomes"), "Biome", contexts, errors, warnings);
}
return new Validation(errors, warnings);
}
private static void validateNetwork(File packFolder, String path, DimensionRiverContext context,
List<String> errors, List<String> warnings) {
JSONObject rivers = context.rivers();
JSONObject topology = nestedObject(rivers, "topology", path, errors);
JSONObject terrain = nestedObject(rivers, "terrain", path, errors);
JSONObject water = nestedObject(rivers, "water", path, errors);
JSONObject biomes = nestedObject(rivers, "biomes", path, errors);
JSONObject caves = nestedObject(rivers, "caves", path, errors);
if (topology != null) {
validateTopology(packFolder, path + ".topology", topology, errors, warnings);
}
if (terrain != null) {
validateTerrain(packFolder, path + ".terrain", terrain, errors, warnings);
}
if (water != null) {
validateWater(path + ".water", water, errors);
}
if (biomes != null) {
validateBiomePools(packFolder, path + ".biomes", biomes, false, errors, warnings);
}
boolean sinkholeTerminal = terrain != null
&& "SINKHOLE_GROTTO".equals(stringValue(terrain, "terminalMode", "DRY_CHANNEL"));
if (caves != null) {
validateCaves(packFolder, path + ".caves", caves, sinkholeTerminal, errors, warnings);
}
if (topology != null && terrain != null) {
double meanderStrength = doubleValue(terrain, "meanderStrength", 72D);
int cellSize = integerValue(topology, "cellSize", 512);
if (Double.isFinite(meanderStrength) && meanderStrength > cellSize) {
warnings.add(path + ".terrain.meanderStrength exceeds topology.cellSize; reaches may require large cache halos.");
}
validateTopologyComplexity(path, topology, terrain, errors);
}
if (sinkholeTerminal && caves != null) {
validateSinkholeCapability(
path + ".terrain.terminalMode",
context,
caves,
path + ".caves",
errors
);
}
}
private static void validateTopology(File packFolder, String path, JSONObject topology,
List<String> errors, List<String> warnings) {
PackJsonFieldChecks.validateOptionalIntegerRange(path, topology, "cellSize", 64, 4096, errors);
PackJsonFieldChecks.validateOptionalIntegerRange(path, topology, "tileCells", 1, 64, errors);
PackJsonFieldChecks.validateOptionalDoubleRange(path, topology, "siteJitter", 0D, 0.49D, errors);
PackJsonFieldChecks.validateOptionalIntegerRange(path, topology, "maxRouteReaches", 1, 256, errors);
PackJsonFieldChecks.validateOptionalIntegerRange(path, topology, "minimumSourcesPerTile", 0, 64, errors);
PackJsonFieldChecks.validateOptionalIntegerRange(path, topology, "sinkSearchReaches", 0, 7, errors);
PackJsonFieldChecks.validateOptionalIntegerRange(path, topology, "routingBasinCells", 8, 256, errors);
PackJsonFieldChecks.validateOptionalDoubleRange(path, topology, "routingPlateauHeight", 1D, 64D, errors);
PackJsonFieldChecks.validateOptionalDoubleRange(path, topology, "routingNoiseWeight", 0D, 1024D, errors);
PackJsonFieldChecks.validateOptionalDoubleRange(path, topology, "terrainHeightWeight", 0D, 16D, errors);
PackJsonFieldChecks.validateOptionalDoubleRange(path, topology, "terrainSlopeWeight", 0D, 16D, errors);
PackJsonFieldChecks.validateOptionalDoubleRange(path, topology, "oceanAttraction", 0D, 16D, errors);
PackJsonFieldChecks.validateOptionalBoolean(path, topology, "requireOcean", errors);
validateNoiseChance(packFolder, topology, "source", path, errors);
validateNoiseChance(packFolder, topology, "continuation", path, errors);
validateStyle(packFolder, topology, "routingStyle", path, errors);
int tileCells = integerValue(topology, "tileCells", 4);
int minimumSourcesPerTile = integerValue(topology, "minimumSourcesPerTile", 0);
if (tileCells >= 1 && tileCells <= 64
&& minimumSourcesPerTile >= 0
&& minimumSourcesPerTile > tileCells * tileCells) {
errors.add(path + ".minimumSourcesPerTile must not exceed tileCells squared.");
}
}
private static void validateTerrain(File packFolder, String path, JSONObject terrain,
List<String> errors, List<String> warnings) {
validateStyledRange(packFolder, terrain, "channelWidth", path, 1D, 2048D, errors, warnings);
validateStyledRange(packFolder, terrain, "bankWidth", path, 0D, 2048D, errors, warnings);
validateStyledRange(packFolder, terrain, "depth", path, 1D, 512D, errors, warnings);
PackJsonFieldChecks.validateOptionalDoubleRange(path, terrain, "maxChannelWidth", 1D, 2048D, errors);
PackJsonFieldChecks.validateOptionalDoubleRange(path, terrain, "maxBankWidth", 0D, 2048D, errors);
PackJsonFieldChecks.validateOptionalDoubleRange(path, terrain, "maxDepth", 1D, 512D, errors);
PackJsonFieldChecks.validateOptionalDoubleRange(path, terrain, "orderWidthFactor", 0D, 8D, errors);
PackJsonFieldChecks.validateOptionalDoubleRange(path, terrain, "orderDepthFactor", 0D, 8D, errors);
PackJsonFieldChecks.validateOptionalIntegerRange(path, terrain, "maxIncision", 0, 512, errors);
PackJsonFieldChecks.validateOptionalDoubleRange(path, terrain, "bankExponent", 0.125D, 16D, errors);
PackJsonFieldChecks.validateOptionalDoubleRange(path, terrain, "meanderStrength", 0D, 1024D, errors);
PackJsonFieldChecks.validateOptionalIntegerRange(path, terrain, "meanderSubdivisions", 1, 64, errors);
PackJsonFieldChecks.validateOptionalDoubleRange(path, terrain, "bedRoughness", 0D, 8D, errors);
PackJsonFieldChecks.validateOptionalEnum(path, terrain, "terminalMode", TERMINAL_MODES, errors);
PackJsonFieldChecks.validateOptionalIntegerRange(path, terrain, "terminalTaper", 8, 1024, errors);
PackJsonFieldChecks.validateOptionalDoubleRange(path, terrain, "dryContinuationChance", 0D, 1D, errors);
validateNoiseChance(packFolder, terrain, "incision", path, errors);
validateStyle(packFolder, terrain, "meanderStyle", path, errors);
validateStyle(packFolder, terrain, "bedRoughnessStyle", path, errors);
}
private static void validateWater(String path, JSONObject water, List<String> errors) {
PackJsonFieldChecks.validateOptionalEnum(path, water, "mode", WATER_MODES, errors);
PackJsonFieldChecks.validateOptionalIntegerRange(path, water, "poolLength", 8, 4096, errors);
PackJsonFieldChecks.validateOptionalIntegerRange(path, water, "maximumPoolRise", 0, 64, errors);
PackJsonFieldChecks.validateOptionalIntegerRange(path, water, "dropHeight", 1, 32, errors);
String mode = stringValue(water, "mode", "SEA_LEVEL");
int maximumPoolRise = integerValue(water, "maximumPoolRise", 4);
int dropHeight = integerValue(water, "dropHeight", 1);
if ("TERRACED".equals(mode) && dropHeight > maximumPoolRise) {
errors.add(path + ".dropHeight must not exceed maximumPoolRise in TERRACED mode.");
}
}
private static void validateTopologyComplexity(
String path,
JSONObject topology,
JSONObject terrain,
List<String> errors
) {
int cellSize = integerValue(topology, "cellSize", 512);
int tileCells = integerValue(topology, "tileCells", 4);
double siteJitter = doubleValue(topology, "siteJitter", 0.35D);
int maxRouteReaches = integerValue(topology, "maxRouteReaches", 16);
double meanderStrength = doubleValue(terrain, "meanderStrength", 72D);
int meanderSubdivisions = integerValue(terrain, "meanderSubdivisions", 8);
double maximumChannelWidth = doubleValue(terrain, "maxChannelWidth", 10D);
double maximumBankWidth = doubleValue(terrain, "maxBankWidth", 4D);
if (cellSize < 64 || cellSize > 4096
|| tileCells < 1 || tileCells > 64
|| !Double.isFinite(siteJitter) || siteJitter < 0D || siteJitter > 0.49D
|| maxRouteReaches < 1 || maxRouteReaches > 256
|| !Double.isFinite(meanderStrength) || meanderStrength < 0D || meanderStrength > 1024D
|| meanderSubdivisions < 1 || meanderSubdivisions > 64
|| !Double.isFinite(maximumChannelWidth) || maximumChannelWidth < 1D || maximumChannelWidth > 2048D
|| !Double.isFinite(maximumBankWidth) || maximumBankWidth < 0D || maximumBankWidth > 2048D) {
return;
}
double maximumReachRadius = maximumChannelWidth * 0.5D + maximumBankWidth;
RiverTopologyComplexity.Estimate estimate = RiverTopologyComplexity.estimate(
cellSize,
tileCells,
siteJitter,
maxRouteReaches,
maximumReachRadius,
meanderStrength,
meanderSubdivisions
);
for (String violation : estimate.violations()) {
errors.add(path + " exceeds the safe derived complexity budget. " + violation);
}
}
private static void validateCaves(File packFolder, String path, JSONObject caves,
boolean forceGeneratedGrotto,
List<String> errors, List<String> warnings) {
PackJsonFieldChecks.validateOptionalEnum(path, caves, "mode", CAVE_MODES, errors);
PackJsonFieldChecks.validateOptionalIntegerRange(path, caves, "minimumSpacing", 16, 4096, errors);
PackJsonFieldChecks.validateOptionalIntegerRange(path, caves, "maximumPerReach", 0, 16, errors);
PackJsonFieldChecks.validateOptionalIntegerRange(path, caves, "maxBoreDepth", 1, 256, errors);
PackJsonFieldChecks.validateOptionalIntegerRange(path, caves, "throatRadius", 1, 16, errors);
PackJsonFieldChecks.validateOptionalIntegerRange(path, caves, "waterLevelOffset", -64, 64, errors);
PackJsonFieldChecks.validateOptionalIntegerRange(path, caves, "dryHeadroom", 0, 64, errors);
PackJsonFieldChecks.validateOptionalIntegerRange(path, caves, "grottoHorizontalRadius", 2, 128, errors);
PackJsonFieldChecks.validateOptionalIntegerRange(path, caves, "grottoVerticalRadius", 2, 128, errors);
PackJsonFieldChecks.validateOptionalDoubleRange(path, caves, "grottoWarpStrength", 0D, 32D, errors);
PackJsonFieldChecks.validateOptionalIntegerRange(path, caves, "maxFloodRadius", 4, 256, errors);
PackJsonFieldChecks.validateOptionalIntegerRange(path, caves, "maxFloodDepth", 4, 256, errors);
PackJsonFieldChecks.validateOptionalIntegerRange(path, caves, "maxFloodVolume", 64, 1048576, errors);
PackJsonFieldChecks.validateOptionalEnum(path, caves, "fallback", CAVE_FALLBACKS, errors);
PackJsonFieldChecks.validateOptionalEnum(path, caves, "existingFluidPolicy", EXISTING_FLUID_POLICIES, errors);
validateNoiseChance(packFolder, caves, "entry", path, errors);
validateStyle(packFolder, caves, "grottoShapeStyle", path, errors);
validateStyle(packFolder, caves, "grottoWarpStyle", path, errors);
String mode = stringValue(caves, "mode", "SEALED");
if ("SEALED".equals(mode) && !forceGeneratedGrotto) {
return;
}
int maximumPerReach = integerValue(caves, "maximumPerReach", 1);
double entryChance = noiseChanceValue(caves, "entry", 0.12D);
if (maximumPerReach == 0 || (!forceGeneratedGrotto && entryChance == 0D)) {
warnings.add(path + " enables cave hydrology but its entry gate cannot accept any connections.");
}
int maxBoreDepth = integerValue(caves, "maxBoreDepth", 48);
int throatRadius = integerValue(caves, "throatRadius", 2);
int maxFloodRadius = integerValue(caves, "maxFloodRadius", 48);
int maxFloodDepth = integerValue(caves, "maxFloodDepth", 32);
if (throatRadius >= maxFloodRadius) {
errors.add(path + ".throatRadius must be smaller than maxFloodRadius so the proof boundary can contain the throat.");
}
if (throatRadius >= maxFloodDepth) {
errors.add(path + ".throatRadius must be smaller than maxFloodDepth so the proof boundary can contain the throat.");
}
if (maxBoreDepth > maxFloodDepth) {
warnings.add(path + ".maxBoreDepth exceeds maxFloodDepth; deeper cave targets found by the bore search will be rejected by containment proof.");
}
String fallback = stringValue(caves, "fallback", "SEALED");
if (forceGeneratedGrotto || usesGeneratedGrotto(mode, fallback)) {
validateGrotto(path, caves, errors);
}
}
private static void validateGrotto(String path, JSONObject caves, List<String> errors) {
int throatRadius = integerValue(caves, "throatRadius", 2);
int dryHeadroom = integerValue(caves, "dryHeadroom", 4);
int horizontalRadius = integerValue(caves, "grottoHorizontalRadius", 12);
int verticalRadius = integerValue(caves, "grottoVerticalRadius", 7);
double warpStrength = doubleValue(caves, "grottoWarpStrength", 2D);
int maxFloodRadius = integerValue(caves, "maxFloodRadius", 48);
int maxFloodDepth = integerValue(caves, "maxFloodDepth", 32);
int maxFloodVolume = integerValue(caves, "maxFloodVolume", 8192);
if (throatRadius >= horizontalRadius || throatRadius >= verticalRadius) {
addDistinct(errors, path + ".throatRadius must be smaller than both grotto radii so a sealed chamber can surround the inlet.");
}
if (dryHeadroom >= (verticalRadius * 2) + 1) {
addDistinct(errors, path + ".dryHeadroom must fit inside the generated grotto height.");
}
int warpEnvelope = Double.isFinite(warpStrength) ? (int) Math.ceil(warpStrength) : 0;
int requiredRadius = horizontalRadius + warpEnvelope + 1;
int requiredDepth = verticalRadius + warpEnvelope + 1;
if (maxFloodRadius < requiredRadius) {
addDistinct(errors, path + ".maxFloodRadius must be at least " + requiredRadius
+ " to prove the configured grotto and its sealed shell.");
}
if (maxFloodDepth < requiredDepth) {
addDistinct(errors, path + ".maxFloodDepth must be at least " + requiredDepth
+ " to prove the configured grotto and its sealed shell.");
}
long volume = grottoVolume(horizontalRadius, verticalRadius);
if (volume > maxFloodVolume) {
addDistinct(errors, path + ".maxFloodVolume must be at least " + volume
+ " to contain the configured grotto before its throat and shell are considered.");
}
}
private static long grottoVolume(int horizontalRadius, int verticalRadius) {
long volume = 0L;
double horizontalSquared = (double) horizontalRadius * horizontalRadius;
double verticalSquared = (double) verticalRadius * verticalRadius;
for (int dx = -horizontalRadius; dx <= horizontalRadius; dx++) {
for (int dy = -verticalRadius; dy <= verticalRadius; dy++) {
double remaining = 1D - ((double) dx * dx / horizontalSquared)
- ((double) dy * dy / verticalSquared);
if (remaining < 0D) {
continue;
}
int maximumZ = (int) Math.floor(horizontalRadius * Math.sqrt(remaining));
volume += (maximumZ * 2L) + 1L;
}
}
return volume;
}
private static boolean usesGeneratedGrotto(String mode, String fallback) {
return "GENERATE_GROTTO".equals(mode)
|| "GROTTO_OR_CLOSED_COMPONENT".equals(mode)
|| "WATERFALL_POOL".equals(mode)
|| "GENERATE_GROTTO".equals(fallback);
}
private static void validateSinkholeCapability(
String terminalPath,
DimensionRiverContext context,
JSONObject caves,
String cavesPath,
List<String> errors
) {
String suffix = " in Dimension '" + context.dimensionKey() + "'.";
if (!booleanValue(context.dimension(), "carvingEnabled", true)) {
addDistinct(errors, terminalPath + " SINKHOLE_GROTTO requires carvingEnabled to be true" + suffix);
}
if (!booleanValue(context.dimension(), "useMantle", true)) {
addDistinct(errors, terminalPath + " SINKHOLE_GROTTO requires useMantle to be true" + suffix);
}
if (disabled(context.dimension(), "CARVED")) {
addDistinct(errors, terminalPath + " SINKHOLE_GROTTO requires CARVED to remain enabled" + suffix);
}
if (disabled(context.dimension(), "RIVER_HYDROLOGY")) {
addDistinct(errors, terminalPath + " SINKHOLE_GROTTO requires RIVER_HYDROLOGY to remain enabled" + suffix);
}
if ("SEALED".equals(stringValue(caves, "mode", "SEALED"))) {
addDistinct(errors, terminalPath + " SINKHOLE_GROTTO requires a non-SEALED caves.mode" + suffix);
}
if (integerValue(caves, "maximumPerReach", 1) <= 0) {
addDistinct(errors, terminalPath + " SINKHOLE_GROTTO requires caves.maximumPerReach above zero" + suffix);
}
validateGrotto(cavesPath, caves, errors);
}
private static void validateOverrideSinkhole(
File packFolder,
String terminalPath,
String resourceKey,
String resourceType,
List<DimensionRiverContext> contexts,
List<String> errors,
List<String> warnings
) {
boolean referenced = false;
for (DimensionRiverContext context : contexts) {
boolean reachable = "Region".equals(resourceType)
? context.regionKeys().contains(resourceKey)
: referencedSurfaceBiomes(packFolder, context.regionKeys()).contains(resourceKey);
if (!reachable) {
continue;
}
referenced = true;
JSONObject caves = nestedObject(context.rivers(), "caves", "Dimension '"
+ context.dimensionKey() + "' rivers", errors);
if (caves != null) {
validateSinkholeCapability(
terminalPath,
context,
caves,
"Dimension '" + context.dimensionKey() + "' rivers.caves",
errors
);
}
}
if (!referenced) {
addDistinct(warnings, terminalPath
+ " selects SINKHOLE_GROTTO but no enabled river dimension reaches this "
+ resourceType.toLowerCase() + ".");
}
}
private static Set<String> referencedSurfaceBiomes(File packFolder, Set<String> regionKeys) {
Set<String> biomes = new HashSet<>();
File regionsFolder = new File(packFolder, "regions");
for (String regionKey : regionKeys) {
JSONObject region = PackValidationIo.readJson(new File(regionsFolder, regionKey + ".json"));
if (region == null) {
continue;
}
collectBiomeKeys(region.optJSONArray("landBiomes"), biomes);
collectBiomeKeys(region.optJSONArray("seaBiomes"), biomes);
collectBiomeKeys(region.optJSONArray("shoreBiomes"), biomes);
}
Set<String> roots = Set.copyOf(biomes);
for (String biomeKey : roots) {
collectBiomeChildren(packFolder, biomeKey, 0, biomes);
}
return biomes;
}
private static void collectBiomeChildren(File packFolder, String biomeKey, int depth, Set<String> biomes) {
if (depth >= 4) {
return;
}
JSONObject biome = PackValidationIo.readJson(new File(packFolder, "biomes/" + biomeKey + ".json"));
if (biome == null) {
return;
}
JSONArray children = biome.optJSONArray("children");
if (children == null) {
return;
}
for (int index = 0; index < children.length(); index++) {
String child = children.optString(index, null);
if (child == null || child.isBlank()) {
continue;
}
boolean added = biomes.add(child);
if (added) {
collectBiomeChildren(packFolder, child, depth + 1, biomes);
}
}
}
private static Set<String> referencedKeys(JSONArray keys) {
Set<String> referenced = new HashSet<>();
collectBiomeKeys(keys, referenced);
return Set.copyOf(referenced);
}
private static void collectBiomeKeys(JSONArray keys, Set<String> destination) {
if (keys == null) {
return;
}
for (int index = 0; index < keys.length(); index++) {
String key = keys.optString(index, null);
if (key != null && !key.isBlank()) {
destination.add(key);
}
}
}
private static boolean disabled(JSONObject dimension, String flag) {
JSONArray disabled = dimension.optJSONArray("disabledComponents");
if (disabled == null) {
return false;
}
for (int index = 0; index < disabled.length(); index++) {
if (flag.equals(disabled.optString(index, null))) {
return true;
}
}
return false;
}
private static void addDistinct(List<String> destination, String value) {
if (!destination.contains(value)) {
destination.add(value);
}
}
private static void validateOverrides(File packFolder, File resourceFolder, String resourceType,
List<DimensionRiverContext> contexts,
List<String> errors, List<String> warnings) {
if (!resourceFolder.isDirectory()) {
return;
}
List<File> files = PackValidationIo.listJsonRecursive(resourceFolder);
files.sort(Comparator.comparing(File::getPath));
for (File file : files) {
JSONObject resource = PackValidationIo.readJson(file);
if (resource == null || !resource.has("riverOverride")) {
continue;
}
String key = PackValidationIo.deriveKey(resourceFolder, file);
String path = resourceType + " '" + key + "' riverOverride";
Object rawOverride = resource.opt("riverOverride");
if (rawOverride == JSONObject.NULL) {
continue;
}
if (!(rawOverride instanceof JSONObject override)) {
errors.add(path + " must be an object or null.");
continue;
}
validateOverride(packFolder, path, key, resourceType, override, contexts, errors, warnings);
}
}
private static void validateOverride(File packFolder, String path, String resourceKey, String resourceType,
JSONObject override, List<DimensionRiverContext> contexts,
List<String> errors, List<String> warnings) {
PackJsonFieldChecks.validateOptionalBoolean(path, override, "allowSources", errors);
PackJsonFieldChecks.validateOptionalEnum(path, override, "routingPolicy", ROUTING_POLICIES, errors);
PackJsonFieldChecks.validateOptionalDoubleRange(path, override, "routingCostMultiplier", 0D, 64D, errors);
PackJsonFieldChecks.validateOptionalDoubleRange(path, override, "widthMultiplier", 0.0001D, 16D, errors);
PackJsonFieldChecks.validateOptionalDoubleRange(path, override, "bankWidthMultiplier", 0D, 16D, errors);
PackJsonFieldChecks.validateOptionalDoubleRange(path, override, "depthMultiplier", 0.0001D, 16D, errors);
PackJsonFieldChecks.validateOptionalDoubleRange(path, override, "maxIncisionMultiplier", 0D, 16D, errors);
PackJsonFieldChecks.validateOptionalDoubleRange(path, override, "continuationChanceMultiplier", 0D, 16D, errors);
PackJsonFieldChecks.validateOptionalDoubleRange(path, override, "caveEntryMultiplier", 0D, 16D, errors);
PackJsonFieldChecks.validateOptionalEnum(path, override, "terminalMode", TERMINAL_MODES, errors);
validateBiomePool(packFolder, path, override, "channelBiomes", RiverBiomeRole.CHANNEL, true, errors, warnings);
validateBiomePool(packFolder, path, override, "bankBiomes", RiverBiomeRole.BANK, true, errors, warnings);
validateBiomePool(packFolder, path, override, "mouthBiomes", RiverBiomeRole.MOUTH, true, errors, warnings);
validateBiomePool(packFolder, path, override, "dryBiomes", RiverBiomeRole.DRY, true, errors, warnings);
validateBiomePool(packFolder, path, override, "floodedCaveBiomes", RiverBiomeRole.FLOODED_CAVE, true,
errors, warnings);
if ("SINKHOLE_GROTTO".equals(stringValue(override, "terminalMode", null))) {
validateOverrideSinkhole(
packFolder,
path + ".terminalMode",
resourceKey,
resourceType,
contexts,
errors,
warnings
);
}
}
private static void validateBiomePools(File packFolder, String path, JSONObject biomes, boolean allowNull,
List<String> errors, List<String> warnings) {
validateStyle(packFolder, biomes, "selectionStyle", path, errors);
validateBiomePool(packFolder, path, biomes, "channel", RiverBiomeRole.CHANNEL, allowNull, errors, warnings);
validateBiomePool(packFolder, path, biomes, "bank", RiverBiomeRole.BANK, allowNull, errors, warnings);
validateBiomePool(packFolder, path, biomes, "mouth", RiverBiomeRole.MOUTH, allowNull, errors, warnings);
validateBiomePool(packFolder, path, biomes, "dry", RiverBiomeRole.DRY, allowNull, errors, warnings);
validateBiomePool(packFolder, path, biomes, "floodedCave", RiverBiomeRole.FLOODED_CAVE, allowNull,
errors, warnings);
}
private static void validateBiomePool(File packFolder, String path, JSONObject owner, String field,
RiverBiomeRole role, boolean allowNull,
List<String> errors, List<String> warnings) {
if (!owner.has(field)) {
return;
}
Object rawPool = owner.opt(field);
if (rawPool == JSONObject.NULL && allowNull) {
return;
}
if (!(rawPool instanceof JSONArray pool)) {
errors.add(path + "." + field + " must be an array" + (allowNull ? " or null" : "") + ".");
return;
}
Set<String> seen = new HashSet<>();
File biomesFolder = new File(packFolder, "biomes");
for (int index = 0; index < pool.length(); index++) {
Object rawKey = pool.opt(index);
String entryPath = path + "." + field + "[" + index + "]";
if (!(rawKey instanceof String key) || key.isBlank()) {
errors.add(entryPath + " must name a biome resource.");
continue;
}
if (!seen.add(key)) {
warnings.add(entryPath + " duplicates biome '" + key + "' in the same river pool.");
continue;
}
File biomeFile = new File(biomesFolder, key + ".json");
if (!biomeFile.isFile()) {
errors.add(entryPath + " references missing biome '" + key + "'.");
continue;
}
validateBiomeSuitability(entryPath, key, role, PackValidationIo.readJson(biomeFile), warnings);
}
}
private static void validateBiomeSuitability(String path, String biomeKey, RiverBiomeRole role,
JSONObject biome, List<String> warnings) {
if (biome == null || role == RiverBiomeRole.DRY || role == RiverBiomeRole.FLOODED_CAVE) {
return;
}
String derivative = stringValue(biome, "vanillaDerivative", null);
if (derivative == null || derivative.isBlank()) {
derivative = stringValue(biome, "derivative", "minecraft:the_void");
}
String normalized = derivative.indexOf(':') >= 0 ? derivative : "minecraft:" + derivative;
if (!normalized.startsWith("minecraft:")) {
return;
}
boolean suitable = switch (role) {
case CHANNEL, MOUTH -> normalized.contains("ocean") || normalized.endsWith("river");
case BANK -> normalized.endsWith("beach") || normalized.endsWith("shore");
default -> true;
};
if (!suitable) {
warnings.add(path + " assigns biome '" + biomeKey + "' the inferred river role " + role.label
+ " but its vanilla derivative '" + normalized
+ "' does not match that role; native structure selection will use Iris's safe role fallback.");
}
}
private static void validateNoiseChance(File packFolder, JSONObject owner, String field, String path,
List<String> errors) {
if (!owner.has(field)) {
return;
}
JSONObject chance = requireObject(owner, field, path + "." + field, errors);
if (chance == null) {
return;
}
String chancePath = path + "." + field;
PackJsonFieldChecks.validateOptionalDoubleRange(chancePath, chance, "chance", 0D, 1D, errors);
PackJsonFieldChecks.validateOptionalDoubleRange(chancePath, chance, "influence", 0D, 1D, errors);
validateStyle(packFolder, chance, "style", chancePath, errors);
}
private static void validateStyledRange(File packFolder, JSONObject owner, String field, String path,
double minimum, double maximum,
List<String> errors, List<String> warnings) {
if (!owner.has(field)) {
return;
}
String rangePath = path + "." + field;
JSONObject range = resolveObject(packFolder, owner.opt(field), "snippet/style-range/", rangePath, errors);
if (range == null) {
return;
}
boolean hasMinimum = range.has("min") && range.opt("min") != JSONObject.NULL;
boolean hasMaximum = range.has("max") && range.opt("max") != JSONObject.NULL;
if (!hasMinimum && !hasMaximum) {
errors.add(rangePath + " must set min and max explicitly.");
} else if (!hasMinimum || !hasMaximum) {
warnings.add(rangePath
+ " should set both min and max explicitly; the omitted bound uses the shared style-range default.");
}
PackJsonFieldChecks.validateOptionalDoubleRange(rangePath, range, "min", minimum, maximum, errors);
PackJsonFieldChecks.validateOptionalDoubleRange(rangePath, range, "max", minimum, maximum, errors);
double minimumValue = doubleValue(range, "min", 16D);
double maximumValue = doubleValue(range, "max", 32D);
if (Double.isFinite(minimumValue) && Double.isFinite(maximumValue) && minimumValue > maximumValue) {
errors.add(rangePath + ".min must not exceed " + rangePath + ".max.");
}
validateStyle(packFolder, range, "style", rangePath, errors);
}
private static void validateStyle(File packFolder, JSONObject owner, String field, String path,
List<String> errors) {
if (!owner.has(field)) {
return;
}
validateStyle(packFolder, owner.opt(field), path + "." + field, errors, new HashSet<>());
}
private static void validateStyle(File packFolder, Object rawStyle, String path,
List<String> errors, Set<String> dependencyStack) {
String styleMarker = rawStyle instanceof String reference ? "style:" + reference : null;
if (styleMarker != null && !dependencyStack.add(styleMarker)) {
errors.add(path + " has a cyclic river-noise style snippet dependency.");
return;
}
try {
JSONObject style = resolveObject(packFolder, rawStyle, "snippet/style/", path, errors);
if (style != null) {
validateResolvedStyle(packFolder, style, path, errors, dependencyStack);
}
} finally {
if (styleMarker != null) {
dependencyStack.remove(styleMarker);
}
}
}
private static void validateResolvedStyle(File packFolder, JSONObject style, String path,
List<String> errors, Set<String> dependencyStack) {
PackJsonFieldChecks.validateOptionalEnum(path, style, "style", NOISE_STYLES, errors);
PackJsonFieldChecks.validateOptionalDoubleRange(path, style, "cellularFrequency", 0D, Double.MAX_VALUE, errors);
PackJsonFieldChecks.validateOptionalDoubleRange(path, style, "cellularZoom", 0.00001D, Double.MAX_VALUE, errors);
PackJsonFieldChecks.validateOptionalDoubleRange(path, style, "zoom", 0.00001D, Double.MAX_VALUE, errors);
PackJsonFieldChecks.validateOptionalDoubleRange(path, style, "multiplier",
-Double.MAX_VALUE, Double.MAX_VALUE, errors);
PackJsonFieldChecks.validateOptionalDoubleRange(path, style, "exponent", 0.01562D, 64D, errors);
PackJsonFieldChecks.validateOptionalIntegerRange(path, style, "cacheSize", 0, 8192, errors);
if (style.has("expression") && style.opt("expression") != JSONObject.NULL) {
Object rawExpression = style.opt("expression");
if (!(rawExpression instanceof String expressionKey) || expressionKey.isBlank()) {
errors.add(path + ".expression must name an expression resource.");
} else {
validateExpression(packFolder, expressionKey, path, errors, dependencyStack);
}
}
if (style.has("fracture") && style.opt("fracture") != JSONObject.NULL) {
validateStyle(packFolder, style.opt("fracture"), path + ".fracture", errors, dependencyStack);
}
}
private static void validateExpression(File packFolder, String expressionKey, String usePath,
List<String> errors, Set<String> dependencyStack) {
String expressionMarker = "expression:" + expressionKey;
if (!dependencyStack.add(expressionMarker)) {
errors.add(usePath + " has a cyclic river-noise expression dependency through '" + expressionKey + "'.");
return;
}
try {
File expressionFile = new File(new File(packFolder, "expressions"), expressionKey + ".json");
JSONObject expression = PackValidationIo.readJson(expressionFile);
if (expression == null) {
return;
}
scanExpressionEntries(packFolder, expression, "variables", expressionKey, usePath, errors, dependencyStack);
scanExpressionEntries(packFolder, expression, "functions", expressionKey, usePath, errors, dependencyStack);
} finally {
dependencyStack.remove(expressionMarker);
}
}
private static void scanExpressionEntries(File packFolder, JSONObject expression, String field,
String expressionKey, String usePath,
List<String> errors, Set<String> dependencyStack) {
JSONArray entries = expression.optJSONArray(field);
if (entries == null) {
return;
}
for (int index = 0; index < entries.length(); index++) {
Object rawEntry = entries.opt(index);
String snippetFolder = "variables".equals(field)
? "snippet/expression-load/"
: "snippet/expression-function/";
JSONObject entry = resolveExpressionEntry(packFolder, rawEntry, snippetFolder);
if (entry == null) {
continue;
}
String stream = stringValue(entry, "engineStreamValue", null);
if (stream != null && isUnsafeRiverStream(stream)) {
errors.add(usePath + " uses expression '" + expressionKey + "' " + field + "[" + index
+ "].engineStreamValue '" + stream
+ "', which depends on final river-shaped terrain and would recurse during river generation.");
}
if (entry.has("styleValue")) {
validateStyle(packFolder, entry.opt("styleValue"), usePath + " -> expression '" + expressionKey
+ "' " + field + "[" + index + "].styleValue", errors, dependencyStack);
}
}
}
private static JSONObject resolveExpressionEntry(File packFolder, Object rawEntry, String snippetFolder) {
if (rawEntry instanceof JSONObject entry) {
return entry;
}
if (!(rawEntry instanceof String reference) || !reference.startsWith("snippet/")) {
return null;
}
String resolved = reference.startsWith(snippetFolder)
? reference
: snippetFolder + reference.substring("snippet/".length());
return PackValidationIo.readJson(new File(packFolder, resolved + ".json"));
}
private static boolean isUnsafeRiverStream(String stream) {
return UNSAFE_RIVER_STREAMS.contains(stream) || stream.startsWith("RIVER_");
}
private static Set<String> noiseStyles() {
Set<String> styles = new HashSet<>();
for (NoiseStyle style : NoiseStyle.values()) {
styles.add(style.name());
}
return Set.copyOf(styles);
}
private static JSONObject nestedObject(JSONObject owner, String field, String path, List<String> errors) {
if (!owner.has(field)) {
return new JSONObject();
}
return requireObject(owner, field, path + "." + field, errors);
}
private static JSONObject requireObject(JSONObject owner, String field, String path, List<String> errors) {
Object raw = owner.opt(field);
if (!(raw instanceof JSONObject object)) {
errors.add(path + " must be an object.");
return null;
}
return object;
}
private static JSONObject resolveObject(File packFolder, Object raw, String snippetFolder,
String path, List<String> errors) {
if (raw instanceof JSONObject object) {
return object;
}
if (raw instanceof String reference && reference.startsWith("snippet/")) {
String resolved = reference.startsWith(snippetFolder)
? reference
: snippetFolder + reference.substring("snippet/".length());
return PackValidationIo.readJson(new File(packFolder, resolved + ".json"));
}
errors.add(path + " must be an object or snippet reference.");
return null;
}
private static boolean booleanValue(JSONObject object, String field, boolean defaultValue) {
Object raw = object.opt(field);
return raw instanceof Boolean value ? value : defaultValue;
}
private static int integerValue(JSONObject object, String field, int defaultValue) {
Object raw = object.opt(field);
if (!(raw instanceof Number number) || !Double.isFinite(number.doubleValue())) {
return defaultValue;
}
return number.intValue();
}
private static double doubleValue(JSONObject object, String field, double defaultValue) {
Object raw = object.opt(field);
return raw instanceof Number number ? number.doubleValue() : defaultValue;
}
private static String stringValue(JSONObject object, String field, String defaultValue) {
Object raw = object.opt(field);
return raw instanceof String value ? value : defaultValue;
}
private static double noiseChanceValue(JSONObject owner, String field, double defaultValue) {
JSONObject chance = owner.optJSONObject(field);
return chance == null ? defaultValue : doubleValue(chance, "chance", defaultValue);
}
record Validation(List<String> errors, List<String> warnings) {
Validation {
errors = List.copyOf(errors);
warnings = List.copyOf(warnings);
}
}
private record DimensionRiverContext(
String dimensionKey,
JSONObject dimension,
JSONObject rivers,
Set<String> regionKeys
) {
}
private enum RiverBiomeRole {
CHANNEL("SEA"),
BANK("SHORE"),
MOUTH("SEA"),
DRY("LAND"),
FLOODED_CAVE("CAVE");
private final String label;
RiverBiomeRole(String label) {
this.label = label;
}
}
}
@@ -76,6 +76,9 @@ public final class PackValidator {
}
PackDimensionValidator.validateDimensions(packFolder, dimensionFiles, blockingErrors, warnings);
PackRiverValidator.Validation riverValidation = PackRiverValidator.validate(packFolder, dimensionFiles);
addDistinct(blockingErrors, riverValidation.errors());
addDistinct(warnings, riverValidation.warnings());
blockingErrors.addAll(PackCaveProfileValidator.validateLegacyFields(packFolder));
PackLootValidator.LootGraphIssues lootIssues = PackLootValidator.validateLootGraph(packFolder);
addDistinct(blockingErrors, lootIssues.errors());
@@ -16,7 +16,6 @@ import art.arcane.iris.core.tools.IrisCreator;
import art.arcane.iris.core.tools.IrisToolbelt;
import art.arcane.iris.engine.platform.BukkitChunkGenerator;
import art.arcane.iris.engine.platform.PlatformChunkGenerator;
import art.arcane.iris.platform.bukkit.BukkitPlatform;
import art.arcane.iris.util.common.plugin.VolmitSender;
import art.arcane.iris.util.common.scheduling.J;
import art.arcane.volmlib.util.exceptions.IrisException;
@@ -55,6 +54,7 @@ import java.util.function.Supplier;
public final class StudioOpenCoordinator {
private static final long STUDIO_CLOSE_TIMEOUT_SECONDS = 120L;
private static final long STUDIO_ENTRY_LOAD_TIMEOUT_SECONDS = 30L;
private static final long STUDIO_ENTRY_RELEASE_TIMEOUT_SECONDS = 15L;
private static final long STUDIO_STRUCTURE_ACTIVATION_TIMEOUT_SECONDS = 30L;
private static final long STUDIO_ENTRY_CLEANUP_BOUNDARY_SECONDS = 120L;
private static volatile StudioOpenCoordinator instance;
@@ -103,10 +103,115 @@ public final class StudioOpenCoordinator {
return closeWorldCoordinated(provider, worldName, world, true, project);
}
public CompletableFuture<Boolean> teleportPlayerToProject(
IrisProject project,
Player player,
AtomicBoolean admission,
long deadlineNanos
) {
if (project == null || player == null) {
return CompletableFuture.completedFuture(false);
}
AtomicBoolean activeAdmission = Objects.requireNonNull(
admission,
"Studio teleport admission");
if (!isStudioTeleportAdmitted(activeAdmission, deadlineNanos)) {
return studioTeleportDeadlineFailure("entry loading");
}
PlatformChunkGenerator provider = project.getActiveProvider();
if (provider == null) {
return CompletableFuture.failedFuture(new IllegalStateException(
"Studio runtime provider is unavailable."));
}
World world = BukkitWorldBinding.world(provider.getTarget().getWorld());
if (world == null) {
return CompletableFuture.failedFuture(new IllegalStateException(
"Studio world is not loaded."));
}
Location entryAnchor = WorldRuntimeControlService.get().resolveEntryAnchor(world);
if (entryAnchor == null) {
return CompletableFuture.failedFuture(new IllegalStateException(
"Studio entry anchor could not be resolved."));
}
EntryChunkResolution entryResolution = loadEntryChunk(world, entryAnchor);
CompletableFuture<Boolean> teleportOperation = beforeStudioTeleportDeadline(
entryResolution.chunk(),
activeAdmission,
deadlineNanos,
"entry loading")
.thenCompose(ignored -> {
if (!isStudioTeleportAdmitted(activeAdmission, deadlineNanos)) {
return studioTeleportDeadlineFailure("safe-entry resolution");
}
return beforeStudioTeleportDeadline(
entryResolution.safeEntry(),
activeAdmission,
deadlineNanos,
"safe-entry resolution");
})
.thenCompose(entry -> {
if (entry == null) {
return CompletableFuture.failedFuture(new IllegalStateException(
"Studio entry point could not be resolved."));
}
if (System.nanoTime() >= deadlineNanos
|| !activeAdmission.compareAndSet(true, false)) {
return studioTeleportDeadlineFailure("native teleport delegation");
}
CompletableFuture<Boolean> teleport =
WorldRuntimeControlService.get().teleport(player, entry);
if (teleport == null) {
return CompletableFuture.failedFuture(new IllegalStateException(
"Studio native teleport returned no completion future."));
}
return teleport;
});
return teleportOperation;
}
private <T> CompletableFuture<T> beforeStudioTeleportDeadline(
CompletableFuture<T> stage,
AtomicBoolean admission,
long deadlineNanos,
String stageName
) {
if (stage == null) {
return CompletableFuture.failedFuture(new IllegalStateException(
"Studio " + stageName + " returned no completion future."));
}
long remainingNanos = deadlineNanos - System.nanoTime();
if (!admission.get() || remainingNanos <= 0L) {
return studioTeleportDeadlineFailure(stageName);
}
CompletableFuture<T> bounded = new CompletableFuture<>();
stage.whenComplete((value, failure) -> {
if (failure == null) {
bounded.complete(value);
} else {
bounded.completeExceptionally(failure);
}
});
CompletableFuture.delayedExecutor(remainingNanos, TimeUnit.NANOSECONDS)
.execute(() -> bounded.completeExceptionally(new TimeoutException(
"Studio teleport deadline expired during " + stageName + ".")));
return bounded;
}
private boolean isStudioTeleportAdmitted(AtomicBoolean admission, long deadlineNanos) {
return admission.get() && System.nanoTime() < deadlineNanos;
}
private <T> CompletableFuture<T> studioTeleportDeadlineFailure(String stageName) {
return CompletableFuture.failedFuture(new TimeoutException(
"Studio teleport deadline expired before " + stageName + "."));
}
private void executeOpen(StudioOpenRequest request, CompletableFuture<StudioOpenResult> future) {
World world = null;
PlatformChunkGenerator provider = null;
CompletableFuture<Void> entryLoadFuture = null;
CompletableFuture<Void> entryUseFuture = null;
CompletableFuture<Boolean> nativeTeleportFuture = null;
try {
long openStart = System.nanoTime();
long t = openStart;
@@ -158,35 +263,75 @@ public final class StudioOpenCoordinator {
}
t = logStudioPhase(request, "resolve_entry_anchor", t, openStart);
updateStage(request, "load_entry_chunk", 0.80D);
int entryChunkX = entryAnchor.getBlockX() >> 4;
int entryChunkZ = entryAnchor.getBlockZ() >> 4;
try {
entryLoadFuture = loadEntryChunk(world, entryChunkX, entryChunkZ);
entryLoads.register(request.worldName(), entryLoadFuture);
entryLoadFuture.get(STUDIO_ENTRY_LOAD_TIMEOUT_SECONDS, TimeUnit.SECONDS);
} catch (TimeoutException e) {
throw new IllegalStateException("Studio entry chunk did not load in time at "
+ entryChunkX + "," + entryChunkZ + " — chunk system may be stalled.", e);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new IllegalStateException("Studio entry chunk load was interrupted at "
+ entryChunkX + "," + entryChunkZ + ".", e);
long entryPrecomputeStartedAt = System.nanoTime();
CompletableFuture<Void> preparedEntryChunks = CompletableFuture.completedFuture(null);
if (requiresLoadedEntry(request)) {
if (!(provider instanceof BukkitChunkGenerator bukkitGenerator)) {
throw new IllegalStateException(
"Studio runtime provider cannot prepare its entry chunks.");
}
preparedEntryChunks = bukkitGenerator.prepareStudioEntryChunks(
world,
entryAnchor.getBlockX() >> 4,
entryAnchor.getBlockZ() >> 4
);
}
t = logStudioPhase(request, "load_entry_chunk", t, openStart);
updateStage(request, "resolve_safe_entry", 0.84D);
Location safeEntry;
try {
safeEntry = WorldRuntimeControlService.get().resolveSafeEntry(world, entryAnchor)
.get(5L, TimeUnit.SECONDS);
} catch (TimeoutException e) {
throw new IllegalStateException("Studio entry point resolution timed out — region thread may be stalled.");
updateStage(request, "prepare_structure_rings", 0.79D);
endStudioEntryBootstrap(world, provider);
t = logStudioPhase(request, "prepare_structure_rings", t, openStart);
if (requiresLoadedEntry(request)) {
try {
preparedEntryChunks.get(
STUDIO_ENTRY_LOAD_TIMEOUT_SECONDS,
TimeUnit.SECONDS
);
} catch (TimeoutException e) {
throw new IllegalStateException(
"Studio entry chunk precompute did not finish in time.", e);
}
t = logOverlappedStudioPhase(
request,
"prepare_entry_chunks",
entryPrecomputeStartedAt,
openStart
);
}
if (safeEntry == null) {
throw new IllegalStateException("Studio entry point could not be resolved for world \"" + request.worldName() + "\".");
Location safeEntry = entryAnchor;
if (requiresLoadedEntry(request)) {
updateStage(request, "load_entry_chunk", 0.80D);
int entryChunkX = entryAnchor.getBlockX() >> 4;
int entryChunkZ = entryAnchor.getBlockZ() >> 4;
EntryChunkResolution entryResolution = loadEntryChunk(world, entryAnchor);
CompletableFuture<Void> useSettlement = new CompletableFuture<>();
entryUseFuture = useSettlement;
entryLoadFuture = entryResolution.safeEntry().thenCompose(ignored -> useSettlement);
entryLoads.register(request.worldName(), entryLoadFuture);
try {
entryResolution.chunk().get(STUDIO_ENTRY_LOAD_TIMEOUT_SECONDS, TimeUnit.SECONDS);
} catch (TimeoutException e) {
throw new IllegalStateException("Studio entry chunk did not load in time at "
+ entryChunkX + "," + entryChunkZ + " — chunk system may be stalled.", e);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new IllegalStateException("Studio entry chunk load was interrupted at "
+ entryChunkX + "," + entryChunkZ + ".", e);
}
t = logStudioPhase(request, "load_entry_chunk", t, openStart);
updateStage(request, "resolve_safe_entry", 0.84D);
try {
safeEntry = entryResolution.safeEntry().get(5L, TimeUnit.SECONDS);
} catch (TimeoutException e) {
throw new IllegalStateException("Studio entry point resolution timed out — region thread may be stalled.");
}
if (safeEntry == null) {
throw new IllegalStateException("Studio entry point could not be resolved for world \"" + request.worldName() + "\".");
}
t = logStudioPhase(request, "resolve_safe_entry", t, openStart);
}
t = logStudioPhase(request, "resolve_safe_entry", t, openStart);
if (request.openKind().teleportThroughStandardEntry()
&& request.playerName() != null
@@ -199,7 +344,12 @@ public final class StudioOpenCoordinator {
Boolean teleported;
try {
teleported = WorldRuntimeControlService.get().teleport(player, safeEntry).get(60L, TimeUnit.SECONDS);
nativeTeleportFuture = WorldRuntimeControlService.get().teleport(player, safeEntry);
if (nativeTeleportFuture == null) {
throw new IllegalStateException(
"Studio native teleport returned no completion future.");
}
teleported = nativeTeleportFuture.get(60L, TimeUnit.SECONDS);
} catch (TimeoutException e) {
throw new IllegalStateException("Studio teleport timed out — destination region may still be generating.");
}
@@ -209,8 +359,6 @@ public final class StudioOpenCoordinator {
t = logStudioPhase(request, "teleport_standard_entry", t, openStart);
}
endStudioEntryBootstrap(world, provider);
updateStage(request, "finalize_open", 1.00D);
if (request.project() != null) {
request.project().setActiveProvider(provider);
@@ -221,11 +369,17 @@ public final class StudioOpenCoordinator {
runOpenFinalizer(request.onDone(), world);
t = logStudioPhase(request, "finalize_open", t, openStart);
settleEntryUseAfterOperation(entryUseFuture, nativeTeleportFuture);
if (entryLoadFuture != null) {
entryLoadFuture.get(STUDIO_ENTRY_RELEASE_TIMEOUT_SECONDS, TimeUnit.SECONDS);
}
IrisLogging.info("Studio open: " + world.getName() + " ready in "
+ elapsedMillis(openStart) + "ms");
entryLoads.release(request.worldName(), entryLoadFuture);
future.complete(new StudioOpenResult(world, safeEntry));
} catch (Throwable e) {
settleEntryUseAfterOperation(entryUseFuture, nativeTeleportFuture);
abandonStudioEntryBootstrap(world, e);
IrisLogging.reportError("Studio open failed for world \"" + request.worldName() + "\".", e);
if (!request.retainOnFailure()) {
@@ -262,11 +416,23 @@ public final class StudioOpenCoordinator {
+ request.worldName() + "\".", unwrapFailure(cleanupError));
}
}
} else if (entryLoadFuture != null) {
entryLoads.releaseAfterSuccessfulCompletion(
request.worldName(),
entryLoadFuture,
entryLoadFuture);
}
future.completeExceptionally(e);
}
}
static boolean requiresLoadedEntry(StudioOpenRequest request) {
Objects.requireNonNull(request, "Studio open request");
return request.openKind().teleportThroughStandardEntry()
&& request.playerName() != null
&& !request.playerName().isBlank();
}
private void deferFailedOpenCleanupToRestart(
PlatformChunkGenerator provider,
String worldName,
@@ -278,7 +444,12 @@ public final class StudioOpenCoordinator {
worldName,
new IllegalStateException("Studio cleanup deferred across the queued server restart."));
}
entryLoads.release(worldName, entryLoadFuture);
if (entryLoadFuture != null) {
entryLoads.releaseAfterSuccessfulCompletion(
worldName,
entryLoadFuture,
entryLoadFuture);
}
}
private boolean transientWorldStorageExists(String worldName) {
@@ -305,6 +476,22 @@ public final class StudioOpenCoordinator {
return now;
}
private long logOverlappedStudioPhase(
StudioOpenRequest request,
String phase,
long phaseStart,
long openStart
) {
long now = System.nanoTime();
IrisLogging.info("[Studio timing] world=%s kind=%s phase=%s duration=%dms cumulative=%dms",
request.worldName(),
request.openKind().name().toLowerCase(Locale.ROOT),
phase,
TimeUnit.NANOSECONDS.toMillis(now - phaseStart),
TimeUnit.NANOSECONDS.toMillis(now - openStart));
return now;
}
private void runOpenFinalizer(Consumer<World> finalizer, World world)
throws InterruptedException, ExecutionException, TimeoutException {
if (finalizer == null) {
@@ -331,14 +518,12 @@ public final class StudioOpenCoordinator {
duration);
}
private CompletableFuture<Void> loadEntryChunk(World world, int chunkX, int chunkZ) {
if (!J.isFolia()) {
return loadEntryChunkAsync(world, chunkX, chunkZ);
}
return scheduleEntryChunkRetention(world, chunkX, chunkZ);
}
private CompletableFuture<Void> loadEntryChunkAsync(World world, int chunkX, int chunkZ) {
private EntryChunkResolution loadEntryChunk(World world, Location entryAnchor) {
int chunkX = entryAnchor.getBlockX() >> 4;
int chunkZ = entryAnchor.getBlockZ() >> 4;
CompletableFuture<Chunk> chunkFuture = new CompletableFuture<>();
CompletableFuture<Location> safeEntryFuture = new CompletableFuture<>();
EntryChunkResolution resolution = new EntryChunkResolution(chunkFuture, safeEntryFuture);
CompletableFuture<Chunk> requested;
try {
requested = WorldRuntimeControlService.get().requestChunkAsync(
@@ -348,56 +533,67 @@ public final class StudioOpenCoordinator {
true,
true);
} catch (Throwable throwable) {
return CompletableFuture.failedFuture(throwable);
failEntryResolution(resolution, throwable);
return resolution;
}
if (requested == null) {
return CompletableFuture.failedFuture(new IllegalStateException(
failEntryResolution(resolution, new IllegalStateException(
"Entry-chunk async request did not return a future at " + chunkX + "," + chunkZ + "."));
return resolution;
}
return requested.thenCompose(chunk -> {
if (chunk == null) {
return CompletableFuture.failedFuture(new IllegalStateException(
"Entry-chunk async request returned no chunk at " + chunkX + "," + chunkZ + "."));
requested.whenComplete((chunk, failure) -> {
if (failure != null) {
failEntryResolution(resolution, failure);
return;
}
if (chunk == null) {
failEntryResolution(resolution, new IllegalStateException(
"Entry-chunk async request returned no chunk at " + chunkX + "," + chunkZ + "."));
return;
}
Runnable resolve = () -> {
chunkFuture.complete(chunk);
try {
Location safeEntry = WorldRuntimeControlService.findTopSafeStudioLocation(world, entryAnchor);
safeEntryFuture.complete(safeEntry);
} catch (Throwable resolutionFailure) {
failEntryResolution(resolution, resolutionFailure);
}
};
try {
if (J.isOwnedByCurrentRegion(world, chunkX, chunkZ)) {
resolve.run();
return;
}
if (!J.runRegion(world, chunkX, chunkZ, resolve)) {
failEntryResolution(resolution, new IllegalStateException(
"Failed to resolve the entry chunk on its owning region at "
+ chunkX + "," + chunkZ + "."));
}
} catch (Throwable schedulingFailure) {
failEntryResolution(resolution, schedulingFailure);
}
return scheduleEntryChunkRetention(world, chunkX, chunkZ);
});
return resolution;
}
private CompletableFuture<Void> scheduleEntryChunkRetention(World world, int chunkX, int chunkZ) {
CompletableFuture<Void> loaded = new CompletableFuture<>();
try {
J.s(() -> retainAndConfirmEntryChunk(world, chunkX, chunkZ)
.whenComplete((ignored, throwable) -> complete(loaded, throwable)));
} catch (Throwable throwable) {
loaded.completeExceptionally(throwable);
}
return loaded;
private void failEntryResolution(EntryChunkResolution resolution, Throwable failure) {
resolution.chunk().completeExceptionally(failure);
resolution.safeEntry().completeExceptionally(failure);
}
private CompletableFuture<Void> retainAndConfirmEntryChunk(World world, int chunkX, int chunkZ) {
CompletableFuture<Void> confirmed = new CompletableFuture<>();
try {
world.addPluginChunkTicket(
chunkX,
chunkZ,
BukkitPlatform.plugin());
} catch (Throwable throwable) {
confirmed.completeExceptionally(throwable);
return confirmed;
}
if (!J.runRegion(world, chunkX, chunkZ, () -> confirmed.complete(null))) {
confirmed.completeExceptionally(new IllegalStateException(
"Failed to confirm entry-chunk region at " + chunkX + "," + chunkZ + "."));
}
return confirmed;
}
private void complete(CompletableFuture<Void> target, Throwable throwable) {
if (throwable == null) {
target.complete(null);
private void settleEntryUseAfterOperation(
CompletableFuture<Void> entryUseFuture,
CompletableFuture<?> operation
) {
if (entryUseFuture == null) {
return;
}
target.completeExceptionally(throwable);
if (operation == null) {
entryUseFuture.complete(null);
return;
}
operation.whenComplete((ignored, failure) -> entryUseFuture.complete(null));
}
private void endStudioEntryBootstrap(World world, PlatformChunkGenerator provider) {
@@ -405,19 +601,28 @@ public final class StudioOpenCoordinator {
throw new IllegalStateException("Studio runtime provider cannot finish its entry bootstrap.");
}
AtomicBoolean activationClaim = new AtomicBoolean(true);
CompletableFuture<Void> activation = J.sfut(() -> {
CompletableFuture<CompletableFuture<Void>> scheduledActivation = J.sfut(() -> {
if (!activationClaim.compareAndSet(true, false)) {
INMS.get().abandonStudioStructureBootstrap(world);
return;
return CompletableFuture.failedFuture(new IllegalStateException(
"Studio native structure activation was cancelled before it began."));
}
try {
INMS.get().completeStudioStructureBootstrap(world);
bukkitGenerator.endStudioEntryBootstrap();
CompletableFuture<Void> nativeActivation =
INMS.get().completeStudioStructureBootstrap(world);
if (nativeActivation == null) {
return CompletableFuture.failedFuture(new IllegalStateException(
"Studio native structure activation returned no completion future."));
}
return nativeActivation;
} catch (ReflectiveOperationException e) {
throw new IllegalStateException(
"Studio native structure state could not be activated after entry bootstrap.", e);
return CompletableFuture.failedFuture(new IllegalStateException(
"Studio native structure state could not be activated after entry bootstrap.", e));
}
});
CompletableFuture<Void> activation = scheduledActivation
.thenCompose(nativeActivation -> nativeActivation)
.thenCompose(ignored -> J.sfut(bukkitGenerator::endStudioEntryBootstrap));
try {
activation.get(STUDIO_STRUCTURE_ACTIVATION_TIMEOUT_SECONDS, TimeUnit.SECONDS);
} catch (InterruptedException e) {
@@ -910,7 +1115,7 @@ public final class StudioOpenCoordinator {
};
}
private Throwable unwrapFailure(Throwable throwable) {
private static Throwable unwrapFailure(Throwable throwable) {
Throwable cursor = throwable;
while (cursor instanceof CompletionException || cursor instanceof ExecutionException) {
if (cursor.getCause() == null) {
@@ -1053,6 +1258,12 @@ public final class StudioOpenCoordinator {
}
}
private record EntryChunkResolution(
CompletableFuture<Chunk> chunk,
CompletableFuture<Location> safeEntry
) {
}
static final class EntryLoadRegistry {
private final ConcurrentHashMap<String, CompletableFuture<?>> entryLoads;
@@ -21,6 +21,7 @@ import org.bukkit.Material;
import org.bukkit.World;
import org.bukkit.block.Block;
import org.bukkit.block.data.BlockData;
import org.bukkit.block.data.Levelled;
import org.bukkit.block.data.Waterlogged;
import org.bukkit.entity.Player;
import org.bukkit.event.world.TimeSkipEvent;
@@ -430,6 +431,62 @@ public final class WorldRuntimeControlService {
return null;
}
static Location findTopSafeStudioLocation(World world, Location source) {
Location dryLocation = findTopSafeLocation(world, source);
if (dryLocation != null) {
return dryLocation;
}
int sourceX = source.getBlockX();
int sourceZ = source.getBlockZ();
int chunkX = sourceX >> 4;
int chunkZ = sourceZ >> 4;
if (!world.isChunkLoaded(chunkX, chunkZ)) {
return null;
}
int minimumFloorY = world.getMinHeight();
int maximumFloorY = world.getMaxHeight() - 3;
if (minimumFloorY > maximumFloorY) {
return null;
}
int minimumX = chunkX << 4;
int minimumZ = chunkZ << 4;
int maximumX = minimumX + 15;
int maximumZ = minimumZ + 15;
for (int radius = 0; radius <= MAX_SAFE_ENTRY_HORIZONTAL_RADIUS; radius++) {
for (int offsetX = -radius; offsetX <= radius; offsetX++) {
for (int offsetZ = -radius; offsetZ <= radius; offsetZ++) {
if (Math.max(Math.abs(offsetX), Math.abs(offsetZ)) != radius) {
continue;
}
int x = sourceX + offsetX;
int z = sourceZ + offsetZ;
if (x < minimumX || x > maximumX || z < minimumZ || z > maximumZ) {
continue;
}
Location waterLocation = findSafeWaterSurfaceLocationInColumn(
world,
x,
z,
minimumFloorY,
maximumFloorY,
source.getYaw(),
source.getPitch()
);
if (waterLocation != null) {
return waterLocation;
}
}
}
}
return null;
}
private static Location findSafeLocationInColumn(
World world,
int x,
@@ -458,6 +515,48 @@ public final class WorldRuntimeControlService {
return null;
}
private static Location findSafeWaterSurfaceLocationInColumn(
World world,
int x,
int z,
int minimumFloorY,
int maximumFloorY,
float yaw,
float pitch
) {
int surfaceY = world.getHighestBlockYAt(x, z, HeightMap.MOTION_BLOCKING_NO_LEAVES);
if (surfaceY <= minimumFloorY || surfaceY > maximumFloorY) {
return null;
}
Block surface = world.getBlockAt(x, surfaceY, z);
if (!isStableWater(surface)) {
return null;
}
Block support = world.getBlockAt(x, surfaceY - 1, z);
if (!isStableWater(support) && !isSafeFloor(support)) {
return null;
}
Block feet = world.getBlockAt(x, surfaceY + 1, z);
Block head = world.getBlockAt(x, surfaceY + 2, z);
if (!isClearEntryBlock(feet) || !isClearEntryBlock(head)) {
return null;
}
return new Location(world, x + BLOCK_CENTER, surfaceY + 1D, z + BLOCK_CENTER, yaw, pitch);
}
private static boolean isStableWater(Block block) {
if (block.getType() != Material.WATER || !block.isLiquid()) {
return false;
}
BlockData blockData = block.getBlockData();
return blockData instanceof Levelled levelled && levelled.getLevel() == 0;
}
private static boolean isSafeFloor(Block block) {
Material material = block.getType();
if (material == null
@@ -59,6 +59,7 @@ import art.arcane.iris.util.common.parallel.MultiBurst;
import art.arcane.iris.util.common.scheduling.J;
import org.bukkit.Bukkit;
import org.bukkit.World;
import org.bukkit.entity.Player;
import java.io.File;
import java.io.IOException;
@@ -82,6 +83,7 @@ import java.util.UUID;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.function.Consumer;
import java.util.function.Supplier;
import java.util.regex.Pattern;
@@ -93,6 +95,7 @@ import art.arcane.volmlib.util.localization.MessageArgument;
public class StudioSVC implements IrisService {
public static final String WORKSPACE_NAME = "packs";
private static final long DOWNLOAD_SHUTDOWN_POLL_SECONDS = 15L;
private static final long STUDIO_PLAYER_TELEPORT_TIMEOUT_SECONDS = 10L;
private static final Pattern PROJECT_NAME = Pattern.compile("[a-z0-9_-]+");
private static final AtomicCache<Integer> counter = new AtomicCache<>();
private final StudioTransitionQueue studioTransitions = new StudioTransitionQueue();
@@ -474,6 +477,28 @@ public class StudioSVC implements IrisService {
return activeProject != null && activeProject.isOpen();
}
public CompletableFuture<Boolean> teleportToActiveProject(Player player) {
Player target = Objects.requireNonNull(player, "Studio teleport player");
AtomicBoolean admission = new AtomicBoolean(true);
long deadlineNanos = System.nanoTime()
+ TimeUnit.SECONDS.toNanos(STUDIO_PLAYER_TELEPORT_TIMEOUT_SECONDS);
CompletableFuture<Boolean> transition = studioTransitions.submit(() -> {
IrisProject project = activeProject;
if (project == null || !project.isOpen()) {
return CompletableFuture.failedFuture(new IllegalStateException(
"No active Studio project is available for teleport."));
}
return StudioOpenCoordinator.get().teleportPlayerToProject(
project,
target,
admission,
deadlineNanos);
});
transition.orTimeout(STUDIO_PLAYER_TELEPORT_TIMEOUT_SECONDS, TimeUnit.SECONDS);
transition.whenComplete((ignored, failure) -> admission.set(false));
return transition;
}
public void open(VolmitSender sender, String dimm) {
open(sender, 1337, dimm);
}
@@ -207,7 +207,7 @@ public class TreeSVC implements IrisService {
@Override
public int getFluidHeight() {
return worldAccess.getEngine().getDimension().getFluidHeight();
return worldFluidHeight(engine);
}
@Override
@@ -290,6 +290,10 @@ public class TreeSVC implements IrisService {
}
}
static int worldFluidHeight(Engine engine) {
return engine.getMinHeight() + engine.getDimension().getFluidHeight();
}
/**
* Finds a single object placement (which may contain more than one object) for the requirements species, location &
* size
@@ -29,7 +29,16 @@ import art.arcane.iris.engine.object.IrisDecorator;
import art.arcane.iris.engine.object.IrisGenerator;
import art.arcane.iris.engine.object.IrisInterpolator;
import art.arcane.iris.engine.object.IrisRegion;
import art.arcane.iris.engine.object.IrisRiverOverride;
import art.arcane.iris.engine.object.IrisRiverRoutingPolicy;
import art.arcane.iris.engine.object.IrisRiverWaterMode;
import art.arcane.iris.engine.object.IrisShapedGeneratorStyle;
import art.arcane.iris.engine.river.runtime.IrisRiverRuntime;
import art.arcane.iris.engine.river.runtime.IrisRiverRuntimeContext;
import art.arcane.iris.engine.river.runtime.IrisRiverSurfaceSample;
import art.arcane.iris.engine.river.RiverRouteState;
import art.arcane.iris.engine.river.RiverSample;
import art.arcane.iris.engine.river.RiverSection;
import art.arcane.iris.spi.IrisPlatforms;
import art.arcane.iris.spi.IrisLogging;
import art.arcane.iris.spi.PlatformBiome;
@@ -61,8 +70,8 @@ import java.util.UUID;
import java.util.concurrent.atomic.AtomicLong;
@Data
@EqualsAndHashCode(exclude = {"data", "gridBoundsCache", "frozenInterpolators", "frozenGenerators"})
@ToString(exclude = {"data", "gridBoundsCache", "frozenInterpolators", "frozenGenerators"})
@EqualsAndHashCode(exclude = {"data", "gridBoundsCache", "frozenInterpolators", "frozenGenerators", "riverRuntime"})
@ToString(exclude = {"data", "gridBoundsCache", "frozenInterpolators", "frozenGenerators", "riverRuntime"})
public class IrisComplex implements DataProvider {
private static final NoiseBounds ZERO_NOISE_BOUNDS = new NoiseBounds(0D, 0D);
private static final AtomicLong lastBoundsFailureLog = new AtomicLong(0L);
@@ -96,14 +105,22 @@ public class IrisComplex implements DataProvider {
private ProceduralStream<IrisBiome> shoreBiomeStream;
private ProceduralStream<IrisBiome> baseBiomeStream;
private ProceduralStream<UUID> baseBiomeIDStream;
private ProceduralStream<IrisBiome> naturalTrueBiomeStream;
private ProceduralStream<IrisBiome> trueBiomeStream;
private ProceduralStream<PlatformBiome> trueBiomeDerivativeStream;
private ProceduralStream<Double> naturalHeightStream;
private ProceduralStream<Double> heightStream;
private ProceduralStream<Integer> roundedHeighteightStream;
private ProceduralStream<Double> maxHeightStream;
private ProceduralStream<Double> overlayStream;
private ProceduralStream<Double> heightFluidStream;
private ProceduralStream<Double> naturalSlopeStream;
private ProceduralStream<Double> slopeStream;
private ProceduralStream<IrisRiverSurfaceSample> riverSurfaceStream;
private ProceduralStream<Double> riverDistanceStream;
private ProceduralStream<Double> riverFlowStream;
private ProceduralStream<Double> riverCarveWeightStream;
private ProceduralStream<Double> riverWaterSurfaceStream;
private ProceduralStream<Integer> topSurfaceStream;
private ProceduralStream<IrisDecorator> terrainSurfaceDecoration;
private ProceduralStream<IrisDecorator> terrainCeilingDecoration;
@@ -118,6 +135,7 @@ public class IrisComplex implements DataProvider {
private IrisRegion focusRegion;
private Map<IrisInterpolator, IdentityHashMap<IrisBiome, GeneratorBounds>> generatorBounds;
private Set<IrisBiome> generatorBiomes;
private IrisRiverRuntime riverRuntime;
// Copy-on-write: reads happen per column on every burst thread; the synchronizedMap
// monitor was taken on every HIT. Writes are once per biome and bounded, so a fresh map
// per insert is cheap. Identity keying is load-bearing (IrisBiome is mutable/value-hashed).
@@ -150,7 +168,7 @@ public class IrisComplex implements DataProvider {
//@builder
if (focusRegion != null) {
prepareInferredBiomes(focusRegion);
focusRegion.getAllBiomes(this).forEach(this::registerGenerators);
focusRegion.getNaturalBiomes(this).forEach(this::registerGenerators);
} else {
engine.getDimension().getRegions().forEach(regionKey -> {
IrisRegion region = data.getRegionLoader().load(regionKey);
@@ -158,7 +176,7 @@ public class IrisComplex implements DataProvider {
return;
}
prepareInferredBiomes(region);
region.getAllBiomes(this).forEach(this::registerGenerators);
region.getNaturalBiomes(this).forEach(this::registerGenerators);
});
}
int interpolatorCount = generators.size();
@@ -246,25 +264,85 @@ public class IrisComplex implements DataProvider {
bridgeStream.convertAware2D((t, x, z) -> inferredStreams.get(t).get(x, z))
.convertAware2D(this::implode)
.cache2D("baseBiomeStream", engine, cacheSize);
heightStream = ProceduralStream.of((x, z) -> {
naturalHeightStream = ProceduralStream.of((x, z) -> {
IrisBiome b = focusBiome != null ? focusBiome : baseBiomeStream.get(x, z);
return getHeight(engine, b, x, z, engine.getSeedManager().getHeight());
}, Interpolated.DOUBLE).cache2DDouble("heightStream", engine, cacheSize);
}, Interpolated.DOUBLE).cache2DDouble("naturalHeightStream", engine, cacheSize);
naturalSlopeStream = naturalHeightStream.slope(3)
.cache2DDouble("naturalSlopeStream", engine, cacheSize);
naturalTrueBiomeStream = focusBiome != null ? ProceduralStream.of((x, y) -> focusBiome, Interpolated.of(a -> 0D,
b -> focusBiome))
.cache2D("naturalTrueBiomeStream-focus", engine, cacheSize) : naturalHeightStream
.convertAware2D((h, x, z) ->
fixBiomeType(h, baseBiomeStream.get(x, z), regionStream.get(x, z), x, z, fluidHeight))
.cache2D("naturalTrueBiomeStream", engine, cacheSize);
if (engine.getDimension().getRivers() != null && engine.getDimension().getRivers().isEnabled()) {
ProceduralStream<Boolean> naturalOceanStream = createNaturalOceanStream(
naturalHeightStream,
bridgeStream,
focusBiome,
fluidHeight,
engine.getDimension().getRivers().getWater().getMode()
).cache2D("naturalOceanStream", engine, cacheSize);
riverRuntime = new IrisRiverRuntime(new IrisRiverRuntimeContext(
engine.getSeedManager().getBodies(),
engine.getDimension().getRivers(),
data,
(int) Math.round(fluidHeight),
IrisEngineMantle.isRiverHydrologyEnabled(engine.getDimension()),
IrisEngineMantle.isRiverCaveHydrologyEnabled(engine.getDimension()),
blockingRiverRoutingPossible(engine),
variableMaxIncisionPossible(engine),
biomeRiverOverridesPossible(focusBiome, generatorBiomes),
(blockX, blockZ) -> naturalHeightBounds(engine, overlayNoise, blockX, blockZ),
naturalHeightStream,
naturalSlopeStream,
naturalOceanStream,
naturalTrueBiomeStream,
regionStream
));
riverSurfaceStream = ProceduralStream.of(
(x, z) -> riverRuntime.sample(x, z),
Interpolated.of(
IrisRiverSurfaceSample::terrainHeight,
value -> IrisRiverSurfaceSample.none(value, fluidHeight)
)
)
.cache2D("riverSurfaceStream", engine, cacheSize);
} else {
riverSurfaceStream = naturalHeightStream.convert(
value -> IrisRiverSurfaceSample.none(value, fluidHeight)
)
.cache2D("riverSurfaceStream-disabled", engine, cacheSize);
}
heightStream = riverSurfaceStream.convert(IrisRiverSurfaceSample::terrainHeight)
.cache2DDouble("heightStream", engine, cacheSize);
roundedHeighteightStream = heightStream.contextInjecting(engine, (c, x, z) -> c.getHeight().getDouble(x, z))
.round();
slopeStream = heightStream.contextInjecting(engine, (c, x, z) -> c.getHeight().getDouble(x, z))
.slope(3).cache2DDouble("slopeStream", engine, cacheSize);
trueBiomeStream = focusBiome != null ? ProceduralStream.of((x, y) -> focusBiome, Interpolated.of(a -> 0D,
b -> focusBiome))
.cache2D("trueBiomeStream-focus", engine, cacheSize) : heightStream
.convertAware2D((h, x, z) ->
fixBiomeType(h, baseBiomeStream.get(x, z),
regionStream.contextInjecting(engine, (c, xx, zz) -> c.getRegion().get(xx, zz)).get(x, z), x, z, fluidHeight))
.cache2D("trueBiomeStream-focus", engine, cacheSize) : riverSurfaceStream
.convertAware2D((sample, x, z) -> resolveRiverSurfaceBiome(sample, x, z))
.cache2D("trueBiomeStream", engine, cacheSize);
trueBiomeDerivativeStream = trueBiomeStream.contextInjecting(engine, (c, x, z) -> c.getBiome().get(x, z))
.convert((b) -> IrisPlatforms.get().registries().biome(b.getDerivativeKey())).cache2D("trueBiomeDerivativeStream", engine, cacheSize);
heightFluidStream = heightStream.contextInjecting(engine, (c, x, z) -> c.getHeight().getDouble(x, z))
.max(fluidHeight).cache2DDouble("heightFluidStream", engine, cacheSize);
riverDistanceStream = riverSurfaceStream.convert(sample -> sample.river().present()
? sample.river().distance()
: Double.MAX_VALUE)
.cache2DDouble("riverDistanceStream", engine, cacheSize);
riverFlowStream = riverSurfaceStream.convert(sample -> (double) sample.river().flow())
.cache2DDouble("riverFlowStream", engine, cacheSize);
riverCarveWeightStream = riverSurfaceStream.convert(sample -> sample.river().carveWeight())
.cache2DDouble("riverCarveWeightStream", engine, cacheSize);
riverWaterSurfaceStream = riverSurfaceStream.convert(IrisRiverSurfaceSample::waterSurfaceY)
.cache2DDouble("riverWaterSurfaceStream", engine, cacheSize);
heightFluidStream = ProceduralStream.ofDouble((x, z) -> Math.max(
heightStream.get(x, z),
riverWaterSurfaceStream.get(x, z)
))
.cache2DDouble("heightFluidStream", engine, cacheSize);
maxHeightStream = ProceduralStream.ofDouble((x, z) -> height);
terrainSurfaceDecoration = trueBiomeStream.contextInjecting(engine, (c, x, z) -> c.getBiome().get(x, z))
.convertAware2D((b, xx, zz) -> decorateFor(b, xx, zz, IrisDecorationPart.NONE)).cache2D("terrainSurfaceDecoration", engine, cacheSize);
@@ -290,6 +368,90 @@ public class IrisComplex implements DataProvider {
//@done
}
private boolean blockingRiverRoutingPossible(Engine engine) {
if (blocksRiverRouting(focusRegion == null ? null : focusRegion.getRiverOverride())
|| blocksRiverRouting(focusBiome == null ? null : focusBiome.getRiverOverride())) {
return true;
}
KList<IrisRegion> regions = engine.getDimension().getAllRegions(engine);
for (IrisRegion loadedRegion : regions) {
if (loadedRegion != null && blocksRiverRouting(loadedRegion.getRiverOverride())) {
return true;
}
}
KList<IrisBiome> biomes = engine.getDimension().getReachableBiomes(engine);
for (IrisBiome biome : biomes) {
if (biome != null && blocksRiverRouting(biome.getRiverOverride())) {
return true;
}
}
return false;
}
private static boolean blocksRiverRouting(IrisRiverOverride override) {
return override != null && override.getRoutingPolicy() == IrisRiverRoutingPolicy.BLOCK;
}
private boolean variableMaxIncisionPossible(Engine engine) {
if (changesMaxIncision(focusRegion == null ? null : focusRegion.getRiverOverride())
|| changesMaxIncision(focusBiome == null ? null : focusBiome.getRiverOverride())) {
return true;
}
KList<IrisRegion> regions = engine.getDimension().getAllRegions(engine);
for (IrisRegion loadedRegion : regions) {
if (loadedRegion != null && changesMaxIncision(loadedRegion.getRiverOverride())) {
return true;
}
}
KList<IrisBiome> biomes = engine.getDimension().getReachableBiomes(engine);
for (IrisBiome biome : biomes) {
if (biome != null && changesMaxIncision(biome.getRiverOverride())) {
return true;
}
}
return false;
}
static boolean changesMaxIncision(IrisRiverOverride override) {
if (override == null || override.getMaxIncisionMultiplier() == null) {
return false;
}
double multiplier = override.getMaxIncisionMultiplier();
return Double.isFinite(multiplier) && Double.compare(Math.max(0D, multiplier), 1D) != 0;
}
static boolean biomeRiverOverridesPossible(IrisBiome focusBiome, Iterable<IrisBiome> naturalBiomes) {
if (focusBiome != null) {
return focusBiome.getRiverOverride() != null;
}
for (IrisBiome biome : naturalBiomes) {
if (biome != null && biome.getRiverOverride() != null) {
return true;
}
}
return false;
}
static ProceduralStream<Boolean> createNaturalOceanStream(
ProceduralStream<Double> naturalHeightStream,
ProceduralStream<InferredType> bridgeStream,
IrisBiome focusBiome,
double fluidHeight,
IrisRiverWaterMode waterMode
) {
if (focusBiome != null) {
boolean ocean = focusBiome.getInferredType() == InferredType.SEA;
return ProceduralStream.of((x, z) -> ocean, Interpolated.BOOLEAN);
}
if (waterMode == IrisRiverWaterMode.SEA_LEVEL) {
return bridgeStream.convert(type -> type == InferredType.SEA);
}
return ProceduralStream.of(
(x, z) -> naturalHeightStream.getDouble(x, z) < fluidHeight - 1D,
Interpolated.BOOLEAN
);
}
public ProceduralStream<IrisBiome> getBiomeStream(InferredType type) {
switch (type) {
case CAVE:
@@ -343,6 +505,63 @@ public class IrisComplex implements DataProvider {
return null;
}
private IrisBiome resolveRiverSurfaceBiome(IrisRiverSurfaceSample sample, double x, double z) {
if (riverRuntime != null) {
if (sample.subterranean()) {
return fixBiomeType(
sample.naturalHeight(),
baseBiomeStream.get(x, z),
regionStream.get(x, z),
x,
z,
fluidHeight
);
}
IrisBiome riverBiome = riverRuntime.selectSurfaceBiome(sample, x, z);
if (riverBiome != null) {
return implode(riverBiome, x, z);
}
InferredType directFallback = directRiverFallback(sample.river());
if (directFallback != null) {
IrisBiome baseBiome = baseBiomeStream.get(x, z);
return implode(baseBiome.withInferredType(directFallback), x, z);
}
if (sample.river().present()
&& sample.river().state() == RiverRouteState.WET
&& sample.river().section() == RiverSection.BANK) {
return fixBiomeType(
sample.terrainHeight(),
baseBiomeStream.get(x, z),
regionStream.get(x, z),
x,
z,
sample.waterSurfaceY()
);
}
}
return fixBiomeType(
sample.terrainHeight(),
baseBiomeStream.get(x, z),
regionStream.get(x, z),
x,
z,
fluidHeight
);
}
static InferredType directRiverFallback(RiverSample river) {
if (!river.present()) {
return null;
}
if (river.state() == RiverRouteState.DRY) {
return InferredType.LAND;
}
return switch (river.section()) {
case CHANNEL, MOUTH -> InferredType.SEA;
default -> null;
};
}
private IrisBiome fixBiomeType(Double height, IrisBiome biome, IrisRegion region, Double x, Double z, double fluidHeight) {
IrisBiome resolved = resolveSurfaceBiome(
height,
@@ -491,6 +710,36 @@ public class IrisComplex implements DataProvider {
return h;
}
private NoiseBounds naturalHeightBounds(
Engine engine,
KList<IrisShapedGeneratorStyle> overlayNoise,
double x,
double z
) {
double minimum = fluidHeight;
double maximum = fluidHeight;
for (int interpolatorIndex = 0; interpolatorIndex < frozenInterpolators.length; interpolatorIndex++) {
NoiseBounds bounds = gridSampleBounds(
engine,
frozenInterpolators[interpolatorIndex],
interpolatorIndex,
frozenGenerators[interpolatorIndex],
x,
z
);
minimum += Math.min(bounds.min(), bounds.max());
maximum += Math.max(bounds.min(), bounds.max());
}
for (IrisShapedGeneratorStyle style : overlayNoise) {
minimum += Math.min(style.getMin(), style.getMax());
maximum += Math.max(style.getMin(), style.getMax());
}
return new NoiseBounds(
Math.max(0D, Math.min(engine.getHeight(), minimum)),
Math.max(0D, Math.min(engine.getHeight(), maximum))
);
}
private double getHeight(Engine engine, IrisBiome b, double x, double z, long seed) {
return Math.max(Math.min(getInterpolatedHeight(engine, x, z, seed) + fluidHeight + overlayStream.get(x, z), engine.getHeight()), 0);
}
@@ -922,6 +1171,8 @@ public class IrisComplex implements DataProvider {
}
public void close() {
if (riverRuntime != null) {
riverRuntime.close();
}
}
}
@@ -30,7 +30,9 @@ import art.arcane.iris.engine.mantle.MantlePass;
import art.arcane.iris.engine.mantle.components.MantleCarvingComponent;
import art.arcane.iris.engine.mantle.components.MantleFloatingObjectComponent;
import art.arcane.iris.engine.mantle.components.MantleObjectComponent;
import art.arcane.iris.engine.mantle.components.MantleRiverHydrologyComponent;
import art.arcane.iris.engine.mantle.components.IrisStructureComponent;
import art.arcane.iris.engine.object.IrisDimension;
import art.arcane.iris.spi.IrisLogging;
import art.arcane.iris.spi.PlatformBlockState;
import art.arcane.iris.util.project.matter.IrisMatterContext;
@@ -88,6 +90,7 @@ public class IrisEngineMantle implements EngineMantle {
this.mantle = createMantle(engine);
components = new KMap<>();
registerComponent(new MantleCarvingComponent(this));
registerComponent(new MantleRiverHydrologyComponent(this));
object = new MantleObjectComponent(this);
registerComponent(object);
registerComponent(new MantleFloatingObjectComponent(this));
@@ -126,9 +129,14 @@ public class IrisEngineMantle implements EngineMantle {
.mapToInt(MantleComponent::getRadius)
.max()
.orElse(0);
int cumulative = downstreamBlockRadius + passBlockRadius;
built[i] = new MantlePass(pass, Math.ceilDiv(cumulative, 16), downstreamBlockRadius);
downstreamBlockRadius = cumulative;
int passInputRadius = pass.stream()
.filter(MantleComponent::isEnabled)
.mapToInt(MantleComponent::getInputRadius)
.max()
.orElse(0);
int invocationRadius = downstreamBlockRadius + passBlockRadius;
built[i] = new MantlePass(pass, Math.ceilDiv(invocationRadius, 16), downstreamBlockRadius);
downstreamBlockRadius = invocationRadius + passInputRadius;
}
return List.of(built);
@@ -157,7 +165,7 @@ public class IrisEngineMantle implements EngineMantle {
@Override
public void hotload() {
disabledFlags.reset();
for (var component : registeredComponents.values()) {
for (MantleComponent component : registeredComponents.values()) {
component.hotload();
component.setEnabled(!getDisabledFlags().contains(component.getFlag()));
}
@@ -171,10 +179,22 @@ public class IrisEngineMantle implements EngineMantle {
if (!getDimension().isCarvingEnabled()) {
disabled.addIfMissing(ReservedFlag.CARVED);
}
if (disabled.contains(ReservedFlag.CARVED)
|| !isRiverHydrologyEnabled(getDimension())) {
disabled.addIfMissing(ReservedFlag.RIVER_HYDROLOGY);
}
return Set.copyOf(disabled);
});
}
static boolean isRiverHydrologyEnabled(IrisDimension dimension) {
return MantleRiverHydrologyComponent.isEnabledFor(dimension);
}
static boolean isRiverCaveHydrologyEnabled(IrisDimension dimension) {
return MantleRiverHydrologyComponent.isCaveConnectionsEnabledFor(dimension);
}
@Override
public MantleObjectComponent getObjectComponent() {
return object;
@@ -68,8 +68,8 @@ public class UpperDimensionContext implements DataProvider {
engine.getDimension(),
engine.getData(),
chunkHeight,
complex.getHeightStream(),
complex.getTrueBiomeStream(),
complex.getNaturalHeightStream(),
complex.getNaturalTrueBiomeStream(),
complex.getRegionStream(),
complex.getRockStream(),
true
@@ -94,7 +94,7 @@ public class UpperDimensionContext implements DataProvider {
upperDim.getRegions().forEach(regionKey -> {
IrisRegion region = upperData.getRegionLoader().load(regionKey);
if (region != null) {
region.getAllBiomes(dataProvider).forEach(biome -> {
region.getNaturalBiomes(dataProvider).forEach(biome -> {
allBiomes.add(biome);
biome.getGenerators().forEach(link -> {
IrisGenerator gen = link.getCachedGenerator(dataProvider);
@@ -27,6 +27,8 @@ import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.framework.EngineAssignedActuator;
import art.arcane.iris.engine.framework.EngineDecorator;
import art.arcane.iris.engine.object.IrisBiome;
import art.arcane.iris.engine.river.RiverRouteState;
import art.arcane.iris.engine.river.runtime.IrisRiverSurfaceSample;
import art.arcane.iris.util.common.data.B;
import art.arcane.iris.util.project.context.ChunkContext;
import art.arcane.volmlib.util.documentation.BlockCoordinates;
@@ -64,6 +66,12 @@ public class IrisDecorantActuator extends EngineAssignedActuator<PlatformBlockSt
seaFloorDecorator = new IrisSeaFloorDecorator(getEngine());
}
static boolean shouldDecorateShoreline(IrisRiverSurfaceSample sample, int height) {
return !sample.subterranean()
&& height == Math.round(sample.waterSurfaceY())
&& (!sample.river().present() || sample.river().state() != RiverRouteState.DRY);
}
@BlockCoordinates
@Override
public void onActuate(int x, int z, Hunk<PlatformBlockState> output, boolean multicore, ChunkContext context) {
@@ -86,23 +94,25 @@ public class IrisDecorantActuator extends EngineAssignedActuator<PlatformBlockSt
height = context.getRoundedHeight(i, j);
biome = context.getBiome().get(i, j);
cave = shouldRay ? context.getCave().get(i, j) : null;
IrisRiverSurfaceSample riverSurface = getComplex().getRiverSurfaceStream().get(realX, realZ);
int surfaceFluidHeight = (int) Math.round(riverSurface.waterSurfaceY());
if (biome.getDecorators().isEmpty() && (cave == null || cave.getDecorators().isEmpty())) {
continue;
}
if (height < getDimension().getFluidHeight() && PREDICATE_SOLID.test(output.get(i, height, j))
if (height < surfaceFluidHeight && PREDICATE_SOLID.test(output.get(i, height, j))
&& height + 1 < output.getHeight() && B.isWater(output.get(i, height + 1, j))) {
getSeaSurfaceDecorator().decorate(i, j,
realX, Math.round(i + 1), Math.round(x + i - 1),
realZ, Math.round(z + j + 1), Math.round(z + j - 1),
output, biome, getDimension().getFluidHeight(), getEngine().getHeight());
output, biome, surfaceFluidHeight, getEngine().getHeight());
getSeaFloorDecorator().decorate(i, j,
realX, realZ, output, biome, height + 1,
getDimension().getFluidHeight() + 1);
surfaceFluidHeight + 1);
}
if (height == getDimension().getFluidHeight()) {
if (shouldDecorateShoreline(riverSurface, height)) {
getShoreLineDecorator().decorate(i, j,
realX, Math.round(x + i + 1), Math.round(x + i - 1),
realZ, Math.round(z + j + 1), Math.round(z + j - 1),
@@ -67,10 +67,6 @@ public class IrisTerrainNormalActuator extends EngineAssignedActuator<PlatformBl
getEngine().getMetrics().getTerrain().put(p.getMilliseconds());
}
private int fluidOrHeight(int height) {
return Math.max(getDimension().getFluidHeight(), height);
}
/**
* This is calling 1/16th of a chunk x/z slice. It is a plane from sky to bedrock 1 thick in the x direction.
*
@@ -92,8 +88,6 @@ public class IrisTerrainNormalActuator extends EngineAssignedActuator<PlatformBl
IrisData data = getData();
IrisComplex complex = getComplex();
RNG localRng = rng;
int fluidHeight = dimension.getFluidHeight();
int clampedFluidHeight = Math.min(chunkHeight, fluidHeight);
boolean bedrockEnabled = dimension.isBedrock();
boolean hideOres = dimension.isHideOresForHiddenOre();
ChunkedDataCache<IrisBiome> biomeCache = context.getBiome();
@@ -114,7 +108,11 @@ public class IrisTerrainNormalActuator extends EngineAssignedActuator<PlatformBl
IrisBiome biome = biomeCache.get(xf, zf);
IrisRegion region = regionCache.get(xf, zf);
int he = Math.min(chunkHeight, context.getRoundedHeight(xf, zf));
int hf = Math.max(clampedFluidHeight, he);
int surfaceFluidHeight = Math.min(
chunkHeight,
(int) Math.round(complex.getRiverWaterSurfaceStream().get(realX, realZ))
);
int hf = Math.max(surfaceFluidHeight, he);
if (hf < 0) {
continue;
}
@@ -40,16 +40,17 @@ public class IrisShoreLineDecorator extends IrisEngineDecorator {
@Override
public void decorate(int x, int z, int realX, int realX1, int realX_1, int realZ, int realZ1, int realZ_1,
Hunk<PlatformBlockState> data, IrisBiome biome, int height, int max) {
if (height != getDimension().getFluidHeight()) {
double localFluidHeight = getComplex().getRiverWaterSurfaceStream().get(realX, realZ);
if (height != Math.round(localFluidHeight)) {
return;
}
double complexFluidHeight = getComplex().getFluidHeight();
ProceduralStream<Double> heightStream = getComplex().getHeightStream();
if (Math.round(heightStream.get(realX1, realZ)) >= complexFluidHeight
&& Math.round(heightStream.get(realX_1, realZ)) >= complexFluidHeight
&& Math.round(heightStream.get(realX, realZ1)) >= complexFluidHeight
&& Math.round(heightStream.get(realX, realZ_1)) >= complexFluidHeight) {
ProceduralStream<Double> fluidStream = getComplex().getRiverWaterSurfaceStream();
if (Math.round(heightStream.get(realX1, realZ)) >= Math.round(fluidStream.get(realX1, realZ))
&& Math.round(heightStream.get(realX_1, realZ)) >= Math.round(fluidStream.get(realX_1, realZ))
&& Math.round(heightStream.get(realX, realZ1)) >= Math.round(fluidStream.get(realX, realZ1))
&& Math.round(heightStream.get(realX, realZ_1)) >= Math.round(fluidStream.get(realX, realZ_1))) {
return;
}
@@ -58,7 +58,7 @@ public class IrisSurfaceDecorator extends IrisEngineDecorator {
@BlockCoordinates
public void decorate(int x, int z, int realX, int realX1, int realX_1, int realZ, int realZ1, int realZ_1,
Hunk<PlatformBlockState> data, IrisBiome biome, InferredType inferredType, int height, int max) {
int fluidHeight = getDimension().getFluidHeight();
int fluidHeight = (int) Math.round(getComplex().getRiverWaterSurfaceStream().get(realX, realZ));
if (inferredType == InferredType.SHORE && height < fluidHeight) {
return;
}
@@ -42,6 +42,8 @@ import art.arcane.iris.engine.object.IrisPosition;
import art.arcane.iris.engine.object.IrisRegion;
import art.arcane.iris.engine.object.IrisStructure;
import art.arcane.iris.engine.object.IrisWorld;
import art.arcane.iris.engine.river.cave.RiverCaveHydrology;
import art.arcane.iris.engine.river.cave.RiverCaveHydrologyStorage;
import art.arcane.iris.spi.IrisLogging;
import art.arcane.iris.spi.PlatformBiome;
import art.arcane.iris.spi.PlatformBlockState;
@@ -244,6 +246,14 @@ public interface Engine extends DataProvider, Fallible, BlockUpdater, Renderer,
@BlockCoordinates
default IrisBiome getCaveOrMantleBiome(int x, int y, int z) {
RiverCaveHydrology hydrology = RiverCaveHydrologyStorage.getIfPresent(
getMantle().getMantle(), x, y, z);
if (hydrology != null && !hydrology.floodedBiomeKey().isEmpty()) {
IrisBiome biome = getData().getBiomeLoader().load(hydrology.floodedBiomeKey());
if (biome != null) {
return biome;
}
}
MatterCavern m = getMantle().getMantle().get(x, y, z, MatterCavern.class);
if (m != null && m.getCustomBiome() != null && !m.getCustomBiome().isEmpty()) {
@@ -157,7 +157,10 @@ public final class NativeStructurePlacementPlanner {
}
static boolean isSubmerged(Engine engine, int blockX, int blockZ) {
return engine.getHeight(blockX, blockZ, true) < engine.getDimension().getFluidHeight();
int localFluidHeight = engine.getComplex() == null
? engine.getDimension().getFluidHeight()
: (int) Math.round(engine.getComplex().getRiverWaterSurfaceStream().get(blockX, blockZ));
return engine.getHeight(blockX, blockZ, true) < localFluidHeight;
}
private static int comparePlacementPriority(IrisStructurePlacement left, IrisStructurePlacement right) {
@@ -3,6 +3,8 @@ package art.arcane.iris.engine.framework;
import art.arcane.iris.engine.object.IrisBiome;
import art.arcane.iris.engine.object.IrisStructureAnchorMode;
import art.arcane.iris.engine.object.IrisStructurePlacement;
import art.arcane.iris.engine.river.cave.RiverCaveHydrology;
import art.arcane.iris.engine.river.cave.RiverCaveHydrologyStorage;
import art.arcane.volmlib.util.math.RNG;
import art.arcane.volmlib.util.matter.MatterCavern;
@@ -286,10 +288,12 @@ public final class StructureCaveAnchorResolver {
int mantleY,
int blockZ
) {
RiverCaveHydrology hydrology = hydrologyAt(engine, blockX, mantleY, blockZ);
MatterCavern cavern = cavernAt(engine, blockX, mantleY, blockZ);
return acceptsAnchorFluid(
placement.isUnderwater(),
cavern,
hydrology,
mantleY,
engine.getDimension().getCaveLavaHeight());
}
@@ -300,6 +304,19 @@ public final class StructureCaveAnchorResolver {
int mantleY,
int defaultLavaHeight
) {
return acceptsAnchorFluid(underwater, cavern, null, mantleY, defaultLavaHeight);
}
static boolean acceptsAnchorFluid(
boolean underwater,
MatterCavern cavern,
RiverCaveHydrology hydrology,
int mantleY,
int defaultLavaHeight
) {
if (hydrology != null && hydrology.protectsPlacement()) {
return false;
}
if (cavern == null || !cavern.isCavern()) {
return false;
}
@@ -313,7 +330,15 @@ public final class StructureCaveAnchorResolver {
}
private static MatterCavern cavernAt(Engine engine, int blockX, int mantleY, int blockZ) {
return engine.getMantle().getMantle().get(blockX, mantleY, blockZ, MatterCavern.class);
MatterCavern baseline = engine.getMantle().getMantle()
.get(blockX, mantleY, blockZ, MatterCavern.class);
RiverCaveHydrology hydrology = hydrologyAt(engine, blockX, mantleY, blockZ);
return hydrology == null ? baseline : hydrology.asCavern();
}
private static RiverCaveHydrology hydrologyAt(Engine engine, int blockX, int mantleY, int blockZ) {
return RiverCaveHydrologyStorage.getIfPresent(
engine.getMantle().getMantle(), blockX, mantleY, blockZ);
}
static int toMantleY(int worldY, int worldMinHeight) {
@@ -6,12 +6,17 @@ import art.arcane.iris.engine.object.IrisRegion;
import art.arcane.iris.engine.object.IrisStructurePlacement;
import art.arcane.volmlib.util.collection.KList;
import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.Collections;
import java.util.IdentityHashMap;
import java.util.List;
import java.util.Objects;
import java.util.Set;
public final class StructurePlacementScope {
private static final List<CachedScopeIndex> SCOPE_INDEXES = new ArrayList<>();
private StructurePlacementScope() {
}
@@ -22,20 +27,93 @@ public final class StructurePlacementScope {
int blockZ = (chunkZ << 4) + 8;
KList<IrisStructurePlacement> placements = new KList<>();
Set<IrisStructurePlacement> seen = Collections.newSetFromMap(new IdentityHashMap<>());
ScopeIndex index = scopeIndex(activeEngine);
if (complex != null) {
IrisBiome biome = complex.getTrueBiomeStream().get(blockX, blockZ);
IrisBiome caveBiome = complex.getCaveBiomeStream().get(blockX, blockZ);
IrisRegion region = complex.getRegionStream().get(blockX, blockZ);
addUnique(placements, seen, biome == null ? null : biome.getStructures());
addCaveUnique(placements, seen, caveBiome == null ? null : caveBiome.getStructures());
addUnique(placements, seen, region == null ? null : region.getStructures());
if (index.biome()) {
IrisBiome biome = complex.getTrueBiomeStream().get(blockX, blockZ);
addUnique(placements, seen, biome == null ? null : biome.getStructures());
}
if (index.caveBiome()) {
IrisBiome caveBiome = complex.getCaveBiomeStream().get(blockX, blockZ);
addCaveUnique(placements, seen, caveBiome == null ? null : caveBiome.getStructures());
}
if (index.region()) {
IrisRegion region = complex.getRegionStream().get(blockX, blockZ);
addUnique(placements, seen, region == null ? null : region.getStructures());
}
}
if (activeEngine.getDimension() != null) {
if (index.dimension()) {
addUnique(placements, seen, activeEngine.getDimension().getStructures());
}
return placements;
}
private static ScopeIndex scopeIndex(Engine engine) {
int revision = engine.getCacheID();
synchronized (SCOPE_INDEXES) {
for (int i = SCOPE_INDEXES.size() - 1; i >= 0; i--) {
CachedScopeIndex cached = SCOPE_INDEXES.get(i);
Engine indexedEngine = cached.engine().get();
if (indexedEngine == null) {
SCOPE_INDEXES.remove(i);
continue;
}
if (indexedEngine == engine) {
if (cached.revision() == revision) {
return cached.index();
}
SCOPE_INDEXES.remove(i);
break;
}
}
ScopeIndex index = buildScopeIndex(engine);
SCOPE_INDEXES.add(new CachedScopeIndex(new WeakReference<>(engine), revision, index));
return index;
}
}
private static ScopeIndex buildScopeIndex(Engine engine) {
boolean biome = false;
boolean caveBiome = false;
KList<IrisBiome> allBiomes = engine.getAllBiomes();
if (allBiomes == null) {
biome = true;
caveBiome = true;
} else {
for (IrisBiome candidate : allBiomes) {
KList<IrisStructurePlacement> structures = candidate.getStructures();
if (structures == null || structures.isEmpty()) {
continue;
}
biome = true;
for (IrisStructurePlacement placement : structures) {
if (placement != null && placement.resolvedAnchor().isCave()) {
caveBiome = true;
break;
}
}
}
}
boolean region = false;
if (engine.getDimension() != null) {
KList<IrisRegion> allRegions = engine.getDimension().getAllRegions(engine);
if (allRegions == null) {
region = true;
} else {
for (IrisRegion candidate : allRegions) {
if (candidate.getStructures() != null && !candidate.getStructures().isEmpty()) {
region = true;
break;
}
}
}
}
boolean dimension = engine.getDimension() != null
&& engine.getDimension().getStructures() != null
&& !engine.getDimension().getStructures().isEmpty();
return new ScopeIndex(biome, caveBiome, region, dimension);
}
private static void addUnique(KList<IrisStructurePlacement> destination,
Set<IrisStructurePlacement> seen,
KList<IrisStructurePlacement> source) {
@@ -61,4 +139,10 @@ public final class StructurePlacementScope {
}
}
}
private record CachedScopeIndex(WeakReference<Engine> engine, int revision, ScopeIndex index) {
}
private record ScopeIndex(boolean biome, boolean caveBiome, boolean region, boolean dimension) {
}
}
@@ -21,7 +21,6 @@ import art.arcane.iris.util.common.scheduling.J;
import art.arcane.volmlib.util.collection.KList;
import art.arcane.iris.util.common.data.IrisCustomData;
import art.arcane.volmlib.util.math.RNG;
import art.arcane.volmlib.util.matter.MatterCavern;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import org.bukkit.Bukkit;
@@ -112,7 +111,7 @@ public class WorldObjectPlacer implements IObjectPlacer {
@Override
public boolean isCarved(int x, int y, int z) {
return mantle.getMantle().get(x, y, z, MatterCavern.class) != null;
return mantle.isCarved(x, y, z);
}
@Override
@@ -18,63 +18,712 @@
package art.arcane.iris.engine.framework.render;
import art.arcane.iris.engine.IrisComplex;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.object.IrisBiome;
import art.arcane.iris.engine.object.IrisBiomeGeneratorLink;
import art.arcane.iris.util.project.interpolation.IrisInterpolation;
import art.arcane.iris.engine.object.IrisDimension;
import art.arcane.iris.engine.object.IrisRegion;
import art.arcane.iris.engine.river.RiverSample;
import art.arcane.iris.engine.river.RiverSection;
import art.arcane.iris.engine.river.runtime.IrisRiverRuntime;
import art.arcane.iris.util.project.stream.ProceduralStream;
import java.awt.Color;
import java.awt.image.BufferedImage;
import java.awt.image.DataBufferInt;
import java.util.function.BiFunction;
import java.util.Arrays;
import java.util.IdentityHashMap;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.CancellationException;
import java.util.function.BooleanSupplier;
public class IrisRenderer {
private static final int BLUE = Color.BLUE.getRGB();
private static final int YELLOW = Color.YELLOW.getRGB();
private static final int GREEN = Color.GREEN.getRGB();
public final class IrisRenderer {
private static final int BLUE = new Color(45, 91, 156).getRGB();
private static final int YELLOW = new Color(211, 164, 67).getRGB();
private static final int GREEN = new Color(78, 137, 83).getRGB();
private static final int RIVER_CHANNEL = new Color(48, 112, 190).getRGB();
private static final int RIVER_MOUTH = new Color(54, 164, 205).getRGB();
private static final int RIVER_BANK = new Color(92, 146, 78).getRGB();
private static final int DRY_CHANNEL = new Color(171, 128, 68).getRGB();
private static final int DRY_BANK = new Color(132, 105, 62).getRGB();
private static final int NO_RIVER = new Color(28, 31, 38).getRGB();
private static final int DEEP_WATER = new Color(20, 48, 92).getRGB();
private static final int SHALLOW_WATER = new Color(50, 112, 154).getRGB();
private static final int LOWLAND = new Color(78, 128, 76).getRGB();
private static final int HIGHLAND = new Color(151, 139, 92).getRGB();
private static final int ROCK = new Color(126, 119, 112).getRGB();
private static final int SNOW = new Color(226, 230, 232).getRGB();
private final Engine renderer;
public IrisRenderer(Engine renderer) {
this.renderer = renderer;
this.renderer = Objects.requireNonNull(renderer, "renderer");
}
public BufferedImage render(double sx, double sz, double size, int resolution, RenderType currentType) {
return render(sx, sz, size, resolution, currentType, () -> false);
}
public BufferedImage render(
double sx,
double sz,
double size,
int resolution,
RenderType currentType,
BooleanSupplier cancelled
) {
return render(sx, sz, size, resolution, currentType, cancelled, false);
}
public BufferedImage renderStudio(
double sx,
double sz,
double size,
int resolution,
RenderType currentType,
BooleanSupplier cancelled
) {
return render(sx, sz, size, resolution, currentType, cancelled, true);
}
private BufferedImage render(
double sx,
double sz,
double size,
int resolution,
RenderType currentType,
BooleanSupplier cancelled,
boolean studio
) {
if (!Double.isFinite(sx) || !Double.isFinite(sz) || !Double.isFinite(size) || size <= 0D) {
throw new IllegalArgumentException("Vision render coordinates and size must be finite");
}
if (resolution < 1) {
throw new IllegalArgumentException("Vision render resolution must be positive");
}
Objects.requireNonNull(currentType, "currentType");
Objects.requireNonNull(cancelled, "cancelled");
checkCancelled(cancelled);
BufferedImage image = new BufferedImage(resolution, resolution, BufferedImage.TYPE_INT_RGB);
int[] pixels = ((DataBufferInt) image.getRaster().getDataBuffer()).getData();
BiFunction<Double, Double, Integer> colorFunction = (d, dx) -> 0;
switch (currentType) {
case BIOME, DECORATOR_LOAD, OBJECT_LOAD, LAYER_LOAD ->
colorFunction = (x, z) -> renderer.getComplex().getTrueBiomeStream().get(x, z).getColor(renderer, currentType).getRGB();
case BIOME_LAND ->
colorFunction = (x, z) -> renderer.getComplex().getLandBiomeStream().get(x, z).getColor(renderer, currentType).getRGB();
case BIOME_SEA ->
colorFunction = (x, z) -> renderer.getComplex().getSeaBiomeStream().get(x, z).getColor(renderer, currentType).getRGB();
case REGION ->
colorFunction = (x, z) -> renderer.getComplex().getRegionStream().get(x, z).getColor(renderer.getComplex(), currentType).getRGB();
case CAVE_LAND ->
colorFunction = (x, z) -> renderer.getComplex().getCaveBiomeStream().get(x, z).getColor(renderer, currentType).getRGB();
case HEIGHT ->
colorFunction = (x, z) -> Color.getHSBColor(renderer.getComplex().getHeightStream().get(x, z).floatValue(), 1f, 1f).getRGB();
case CONTINENT -> colorFunction = (x, z) -> {
IrisBiome b = renderer.getBiome((int) Math.round(x), renderer.getMaxHeight() - 1, (int) Math.round(z));
IrisBiomeGeneratorLink g = b.getGenerators().get(0);
if (g.getMax() <= 0) return BLUE;
if (g.getMin() < 0) return YELLOW;
return GREEN;
};
double step = size / resolution;
if (studio && currentType == RenderType.HEIGHT) {
renderHeightAtlas(pixels, resolution, sx, sz, step, renderer, cancelled);
return image;
}
PixelShader shader = shader(currentType, step, studio);
if (studio && currentType == RenderType.RIVER) {
Arrays.fill(pixels, NO_RIVER);
renderRiverAtlas(pixels, resolution, sx, sz, step, renderer.getComplex(), cancelled, false);
return image;
}
if (studio && adaptiveStudioType(currentType)) {
renderAdaptiveAtlas(pixels, resolution, sx, sz, step, shader, cancelled);
if (currentType == RenderType.BIOME) {
renderRiverAtlas(pixels, resolution, sx, sz, step, renderer.getComplex(), cancelled, true);
}
return image;
}
int groupSize = sampleGroup(step, resolution);
double x, z;
for (int i = 0; i < resolution; i++) {
x = IrisInterpolation.lerp(sx, sx + size, (double) i / (double) resolution);
for (int j = 0; j < resolution; j++) {
z = IrisInterpolation.lerp(sz, sz + size, (double) j / (double) resolution);
pixels[j * resolution + i] = colorFunction.apply(x, z);
for (int groupZ = 0; groupZ < resolution; groupZ += groupSize) {
checkCancelled(cancelled);
int maximumZ = Math.min(resolution, groupZ + groupSize);
for (int groupX = 0; groupX < resolution; groupX += groupSize) {
checkCancelled(cancelled);
int maximumX = Math.min(resolution, groupX + groupSize);
for (int pixelZ = groupZ; pixelZ < maximumZ; pixelZ++) {
double z = sz + step * pixelZ;
int row = pixelZ * resolution;
for (int pixelX = groupX; pixelX < maximumX; pixelX++) {
checkCancelled(cancelled);
double x = sx + step * pixelX;
pixels[row + pixelX] = shader.color(x, z);
}
}
}
}
return image;
}
public static int riverColor(RiverSection section) {
Objects.requireNonNull(section, "section");
return switch (section) {
case CHANNEL -> RIVER_CHANNEL;
case MOUTH -> RIVER_MOUTH;
case BANK -> RIVER_BANK;
case DRY_CHANNEL -> DRY_CHANNEL;
case DRY_BANK -> DRY_BANK;
case NONE -> NO_RIVER;
};
}
public static int heightColor(double height, double maximumHeight, double fluidHeight) {
double boundedMaximum = Math.max(1D, maximumHeight);
double boundedHeight = clamp(height, 0D, boundedMaximum);
double boundedFluid = clamp(fluidHeight, 0D, boundedMaximum);
if (boundedHeight <= boundedFluid && boundedFluid > 0D) {
return blend(DEEP_WATER, SHALLOW_WATER, boundedHeight / boundedFluid);
}
double landRange = Math.max(1D, boundedMaximum - boundedFluid);
double land = clamp((boundedHeight - boundedFluid) / landRange, 0D, 1D);
if (land < 0.45D) {
return blend(LOWLAND, HIGHLAND, land / 0.45D);
}
if (land < 0.78D) {
return blend(HIGHLAND, ROCK, (land - 0.45D) / 0.33D);
}
return blend(ROCK, SNOW, (land - 0.78D) / 0.22D);
}
static int sampleGroup(double step, int resolution) {
double absoluteStep = Math.abs(step);
if (!Double.isFinite(absoluteStep) || absoluteStep <= 0D) {
return 1;
}
return Math.max(1, Math.min(resolution, (int) Math.floor(16D / absoluteStep)));
}
private PixelShader shader(RenderType currentType, double step, boolean studio) {
IrisComplex complex = renderer.getComplex();
return switch (currentType) {
case BIOME, DECORATOR_LOAD, OBJECT_LOAD, LAYER_LOAD -> biomeShader(
studio ? complex.getBaseBiomeStream() : complex.getTrueBiomeStream(), currentType);
case BIOME_LAND -> biomeShader(complex.getLandBiomeStream(), currentType);
case BIOME_SEA -> biomeShader(complex.getSeaBiomeStream(), currentType);
case REGION -> regionShader(complex, currentType);
case CAVE_LAND -> biomeShader(complex.getCaveBiomeStream(), currentType);
case HEIGHT -> heightShader(studio ? complex.getNaturalHeightStream() : complex.getHeightStream());
case RIVER -> (double x, double z) -> riverColor(complex, x, z, step);
case CONTINENT -> studio
? continentShader(complex.getBaseBiomeStream())
: this::continentColor;
};
}
private static boolean adaptiveStudioType(RenderType type) {
return switch (type) {
case BIOME, DECORATOR_LOAD, OBJECT_LOAD, LAYER_LOAD, BIOME_LAND, BIOME_SEA, REGION, CAVE_LAND,
CONTINENT -> true;
case HEIGHT, RIVER -> false;
};
}
private static void renderAdaptiveAtlas(
int[] pixels,
int resolution,
double startX,
double startZ,
double step,
PixelShader shader,
BooleanSupplier cancelled
) {
int maximumPixels = Math.max(1, Math.min(16, (int) Math.floor(64D / step)));
int blockPixels = Integer.highestOneBit(maximumPixels);
AdaptiveSampler sampler = new AdaptiveSampler(pixels, resolution, startX, startZ, step, shader, cancelled);
for (int pixelZ = 0; pixelZ < resolution; pixelZ += blockPixels) {
int height = Math.min(blockPixels, resolution - pixelZ);
for (int pixelX = 0; pixelX < resolution; pixelX += blockPixels) {
sampler.render(pixelX, pixelZ, Math.min(blockPixels, resolution - pixelX), height);
}
}
}
private static void renderHeightAtlas(
int[] pixels,
int resolution,
double startX,
double startZ,
double step,
Engine engine,
BooleanSupplier cancelled
) {
IrisComplex complex = engine.getComplex();
IrisDimension dimension = engine.getDimension();
double fluidHeight = dimension == null ? 0D : dimension.getFluidHeight();
int maximumPixels = Math.max(1, Math.min(16, (int) Math.floor(64D / step)));
int blockPixels = Integer.highestOneBit(maximumPixels);
HeightSampler sampler = new HeightSampler(
pixels,
resolution,
startX,
startZ,
step,
complex.getNaturalHeightStream(),
engine.getHeight(),
fluidHeight,
cancelled
);
for (int pixelZ = 0; pixelZ < resolution; pixelZ += blockPixels) {
int height = Math.min(blockPixels, resolution - pixelZ);
for (int pixelX = 0; pixelX < resolution; pixelX += blockPixels) {
sampler.render(pixelX, pixelZ, Math.min(blockPixels, resolution - pixelX), height);
}
}
}
private static void renderRiverAtlas(
int[] pixels,
int resolution,
double startX,
double startZ,
double step,
IrisComplex complex,
BooleanSupplier cancelled,
boolean composite
) {
IrisRiverRuntime runtime = complex.getRiverRuntime();
if (runtime == null) {
return;
}
int maximumPixels = Math.max(1, Math.min(16, (int) Math.floor(64D / step)));
int blockPixels = Integer.highestOneBit(maximumPixels);
for (int pixelZ = 0; pixelZ < resolution; pixelZ += blockPixels) {
int height = Math.min(blockPixels, resolution - pixelZ);
for (int pixelX = 0; pixelX < resolution; pixelX += blockPixels) {
renderRiverBlock(
pixels,
resolution,
startX,
startZ,
step,
runtime,
cancelled,
composite,
pixelX,
pixelZ,
Math.min(blockPixels, resolution - pixelX),
height
);
}
}
}
private static void renderRiverBlock(
int[] pixels,
int resolution,
double startX,
double startZ,
double step,
IrisRiverRuntime runtime,
BooleanSupplier cancelled,
boolean composite,
int pixelX,
int pixelZ,
int width,
int height
) {
checkCancelled(cancelled);
RiverSample sample = runtime.sampleFootprint(
startX + pixelX * step,
startZ + pixelZ * step,
startX + (pixelX + width) * step,
startZ + (pixelZ + height) * step
);
if (!sample.present()) {
return;
}
if (width == 1 && height == 1) {
int index = pixelZ * resolution + pixelX;
int color = riverColor(sample.section());
pixels[index] = composite ? riverCompositeColor(pixels[index], sample.section(), color) : color;
return;
}
int leftWidth = Math.max(1, width / 2);
int rightWidth = width - leftWidth;
int topHeight = Math.max(1, height / 2);
int bottomHeight = height - topHeight;
renderRiverBlock(pixels, resolution, startX, startZ, step, runtime, cancelled, composite,
pixelX, pixelZ, leftWidth, topHeight);
if (rightWidth > 0) {
renderRiverBlock(pixels, resolution, startX, startZ, step, runtime, cancelled, composite,
pixelX + leftWidth, pixelZ, rightWidth, topHeight);
}
if (bottomHeight > 0) {
renderRiverBlock(pixels, resolution, startX, startZ, step, runtime, cancelled, composite,
pixelX, pixelZ + topHeight, leftWidth, bottomHeight);
if (rightWidth > 0) {
renderRiverBlock(pixels, resolution, startX, startZ, step, runtime, cancelled, composite,
pixelX + leftWidth, pixelZ + topHeight, rightWidth, bottomHeight);
}
}
}
private static int riverCompositeColor(int base, RiverSection section, int river) {
return switch (section) {
case CHANNEL, MOUTH -> river;
case BANK -> blend(base, river, 0.68D);
case DRY_CHANNEL -> blend(base, river, 0.88D);
case DRY_BANK -> blend(base, river, 0.58D);
case NONE -> base;
};
}
private PixelShader biomeShader(ProceduralStream<IrisBiome> stream, RenderType currentType) {
IdentityHashMap<IrisBiome, Integer> colors = new IdentityHashMap<>();
return (double x, double z) -> {
IrisBiome biome = stream.get(x, z);
Integer color = colors.get(biome);
if (color == null) {
color = biome.getColor(renderer, currentType).getRGB();
colors.put(biome, color);
}
return color;
};
}
private PixelShader regionShader(IrisComplex complex, RenderType currentType) {
ProceduralStream<IrisRegion> stream = complex.getRegionStream();
IdentityHashMap<IrisRegion, Integer> colors = new IdentityHashMap<>();
return (double x, double z) -> {
IrisRegion region = stream.get(x, z);
Integer color = colors.get(region);
if (color == null) {
color = region.getColor(complex, currentType).getRGB();
colors.put(region, color);
}
return color;
};
}
private PixelShader heightShader(ProceduralStream<Double> stream) {
double maximumHeight = renderer.getHeight();
IrisDimension dimension = renderer.getDimension();
double fluidHeight = dimension == null ? 0D : dimension.getFluidHeight();
return (double x, double z) -> heightColor(stream.getDouble(x, z), maximumHeight, fluidHeight);
}
private int riverColor(IrisComplex complex, double x, double z, double step) {
IrisRiverRuntime runtime = complex.getRiverRuntime();
if (runtime == null) {
return riverColor(RiverSection.NONE);
}
double endX = x + step;
double endZ = z + step;
RiverSample sample = runtime.sampleFootprint(
StrictMath.min(x, endX),
StrictMath.min(z, endZ),
StrictMath.max(x, endX),
StrictMath.max(z, endZ)
);
return riverColor(sample.section());
}
private int continentColor(double x, double z) {
IrisBiome biome = renderer.getBiome(
(int) Math.round(x),
renderer.getMaxHeight() - 1,
(int) Math.round(z)
);
return continentColor(biome);
}
private PixelShader continentShader(ProceduralStream<IrisBiome> stream) {
IdentityHashMap<IrisBiome, Integer> colors = new IdentityHashMap<>();
return (double x, double z) -> {
IrisBiome biome = stream.get(x, z);
Integer color = colors.get(biome);
if (color == null) {
color = continentColor(biome);
colors.put(biome, color);
}
return color;
};
}
private int continentColor(IrisBiome biome) {
if (biome == null) {
return GREEN;
}
List<IrisBiomeGeneratorLink> generators = biome.getGenerators();
if (generators.isEmpty()) {
return GREEN;
}
IrisBiomeGeneratorLink generator = generators.get(0);
if (generator.getMax() <= 0D) {
return BLUE;
}
if (generator.getMin() < 0D) {
return YELLOW;
}
return GREEN;
}
private static int blend(int first, int second, double progress) {
double bounded = clamp(progress, 0D, 1D);
int red = (int) Math.round(((first >> 16) & 0xFF) * (1D - bounded) + ((second >> 16) & 0xFF) * bounded);
int green = (int) Math.round(((first >> 8) & 0xFF) * (1D - bounded) + ((second >> 8) & 0xFF) * bounded);
int blue = (int) Math.round((first & 0xFF) * (1D - bounded) + (second & 0xFF) * bounded);
return (red << 16) | (green << 8) | blue;
}
private static double clamp(double value, double minimum, double maximum) {
return Math.max(minimum, Math.min(maximum, value));
}
private static void checkCancelled(BooleanSupplier cancelled) {
if (cancelled.getAsBoolean() || Thread.currentThread().isInterrupted()) {
throw new CancellationException("Vision render cancelled");
}
}
@FunctionalInterface
private interface PixelShader {
int color(double x, double z);
}
private static final class AdaptiveSampler {
private final int[] pixels;
private final boolean[] sampled;
private final int resolution;
private final double startX;
private final double startZ;
private final double step;
private final PixelShader shader;
private final BooleanSupplier cancelled;
private AdaptiveSampler(
int[] pixels,
int resolution,
double startX,
double startZ,
double step,
PixelShader shader,
BooleanSupplier cancelled
) {
this.pixels = pixels;
this.sampled = new boolean[pixels.length];
this.resolution = resolution;
this.startX = startX;
this.startZ = startZ;
this.step = step;
this.shader = shader;
this.cancelled = cancelled;
}
private void render(int pixelX, int pixelZ, int width, int height) {
checkCancelled(cancelled);
if (width == 1 && height == 1) {
sample(pixelX, pixelZ);
return;
}
int maximumX = pixelX + width - 1;
int maximumZ = pixelZ + height - 1;
int centerX = pixelX + width / 2;
int centerZ = pixelZ + height / 2;
int color = sample(pixelX, pixelZ);
if (sample(maximumX, pixelZ) == color
&& sample(pixelX, maximumZ) == color
&& sample(maximumX, maximumZ) == color
&& sample(centerX, centerZ) == color) {
fill(pixelX, pixelZ, width, height, color);
return;
}
int leftWidth = Math.max(1, width / 2);
int rightWidth = width - leftWidth;
int topHeight = Math.max(1, height / 2);
int bottomHeight = height - topHeight;
render(pixelX, pixelZ, leftWidth, topHeight);
if (rightWidth > 0) {
render(pixelX + leftWidth, pixelZ, rightWidth, topHeight);
}
if (bottomHeight > 0) {
render(pixelX, pixelZ + topHeight, leftWidth, bottomHeight);
if (rightWidth > 0) {
render(pixelX + leftWidth, pixelZ + topHeight, rightWidth, bottomHeight);
}
}
}
private int sample(int pixelX, int pixelZ) {
int index = pixelZ * resolution + pixelX;
if (!sampled[index]) {
pixels[index] = shader.color(startX + pixelX * step, startZ + pixelZ * step);
sampled[index] = true;
}
return pixels[index];
}
private void fill(int pixelX, int pixelZ, int width, int height, int color) {
for (int row = pixelZ; row < pixelZ + height; row++) {
int start = row * resolution + pixelX;
Arrays.fill(pixels, start, start + width, color);
Arrays.fill(sampled, start, start + width, true);
}
}
}
private static final class HeightSampler {
private static final double MAXIMUM_INTERPOLATION_ERROR = 1.5D;
private final int[] pixels;
private final double[] heights;
private final boolean[] sampled;
private final int resolution;
private final double startX;
private final double startZ;
private final double step;
private final ProceduralStream<Double> stream;
private final double maximumHeight;
private final double fluidHeight;
private final BooleanSupplier cancelled;
private HeightSampler(
int[] pixels,
int resolution,
double startX,
double startZ,
double step,
ProceduralStream<Double> stream,
double maximumHeight,
double fluidHeight,
BooleanSupplier cancelled
) {
this.pixels = pixels;
this.heights = new double[pixels.length];
this.sampled = new boolean[pixels.length];
this.resolution = resolution;
this.startX = startX;
this.startZ = startZ;
this.step = step;
this.stream = stream;
this.maximumHeight = maximumHeight;
this.fluidHeight = fluidHeight;
this.cancelled = cancelled;
}
private void render(int pixelX, int pixelZ, int width, int height) {
checkCancelled(cancelled);
if (width == 1 && height == 1) {
pixels[pixelZ * resolution + pixelX] = heightColor(
sample(pixelX, pixelZ), maximumHeight, fluidHeight);
return;
}
int maximumX = pixelX + width - 1;
int maximumZ = pixelZ + height - 1;
int centerX = pixelX + width / 2;
int centerZ = pixelZ + height / 2;
double topLeft = sample(pixelX, pixelZ);
double topRight = sample(maximumX, pixelZ);
double bottomLeft = sample(pixelX, maximumZ);
double bottomRight = sample(maximumX, maximumZ);
if (matchesPlane(pixelX, pixelZ, width, height, topLeft, topRight, bottomLeft, bottomRight,
centerX, pixelZ)
&& matchesPlane(pixelX, pixelZ, width, height, topLeft, topRight, bottomLeft, bottomRight,
pixelX, centerZ)
&& matchesPlane(pixelX, pixelZ, width, height, topLeft, topRight, bottomLeft, bottomRight,
maximumX, centerZ)
&& matchesPlane(pixelX, pixelZ, width, height, topLeft, topRight, bottomLeft, bottomRight,
centerX, maximumZ)
&& matchesPlane(pixelX, pixelZ, width, height, topLeft, topRight, bottomLeft, bottomRight,
centerX, centerZ)) {
fillPlane(pixelX, pixelZ, width, height, topLeft, topRight, bottomLeft, bottomRight);
return;
}
int leftWidth = Math.max(1, width / 2);
int rightWidth = width - leftWidth;
int topHeight = Math.max(1, height / 2);
int bottomHeight = height - topHeight;
render(pixelX, pixelZ, leftWidth, topHeight);
if (rightWidth > 0) {
render(pixelX + leftWidth, pixelZ, rightWidth, topHeight);
}
if (bottomHeight > 0) {
render(pixelX, pixelZ + topHeight, leftWidth, bottomHeight);
if (rightWidth > 0) {
render(pixelX + leftWidth, pixelZ + topHeight, rightWidth, bottomHeight);
}
}
}
private boolean matchesPlane(
int pixelX,
int pixelZ,
int width,
int height,
double topLeft,
double topRight,
double bottomLeft,
double bottomRight,
int sampleX,
int sampleZ
) {
double predicted = interpolate(
pixelX,
pixelZ,
width,
height,
topLeft,
topRight,
bottomLeft,
bottomRight,
sampleX,
sampleZ
);
return Math.abs(sample(sampleX, sampleZ) - predicted) <= MAXIMUM_INTERPOLATION_ERROR;
}
private void fillPlane(
int pixelX,
int pixelZ,
int width,
int height,
double topLeft,
double topRight,
double bottomLeft,
double bottomRight
) {
for (int row = pixelZ; row < pixelZ + height; row++) {
int offset = row * resolution;
for (int column = pixelX; column < pixelX + width; column++) {
double value = interpolate(
pixelX,
pixelZ,
width,
height,
topLeft,
topRight,
bottomLeft,
bottomRight,
column,
row
);
pixels[offset + column] = heightColor(value, maximumHeight, fluidHeight);
}
}
}
private double sample(int pixelX, int pixelZ) {
int index = pixelZ * resolution + pixelX;
if (!sampled[index]) {
heights[index] = stream.getDouble(startX + pixelX * step, startZ + pixelZ * step);
sampled[index] = true;
}
return heights[index];
}
private static double interpolate(
int pixelX,
int pixelZ,
int width,
int height,
double topLeft,
double topRight,
double bottomLeft,
double bottomRight,
int sampleX,
int sampleZ
) {
double x = width <= 1 ? 0D : (sampleX - pixelX) / (double) (width - 1);
double z = height <= 1 ? 0D : (sampleZ - pixelZ) / (double) (height - 1);
double top = topLeft + (topRight - topLeft) * x;
double bottom = bottomLeft + (bottomRight - bottomLeft) * x;
return top + (bottom - top) * z;
}
}
}
@@ -19,5 +19,5 @@
package art.arcane.iris.engine.framework.render;
public enum RenderType {
BIOME, BIOME_LAND, BIOME_SEA, REGION, CAVE_LAND, HEIGHT, OBJECT_LOAD, DECORATOR_LOAD, CONTINENT, LAYER_LOAD
BIOME, BIOME_LAND, BIOME_SEA, REGION, CAVE_LAND, RIVER, HEIGHT, OBJECT_LOAD, DECORATOR_LOAD, CONTINENT, LAYER_LOAD
}
@@ -27,6 +27,8 @@ import art.arcane.iris.engine.framework.EngineTarget;
import art.arcane.iris.engine.framework.TreeBlockMaterial;
import art.arcane.iris.engine.mantle.components.MantleObjectComponent;
import art.arcane.iris.engine.object.IrisDimension;
import art.arcane.iris.engine.river.cave.RiverCaveHydrology;
import art.arcane.iris.engine.river.cave.RiverCaveHydrologyStorage;
import art.arcane.iris.engine.object.IrisPosition;
import art.arcane.volmlib.util.collection.KList;
import art.arcane.iris.util.common.data.B;
@@ -102,7 +104,7 @@ public interface EngineMantle extends MatterGenerator {
}
default int getHighest(int x, int z, IrisData data, boolean ignoreFluid) {
return ignoreFluid ? trueHeight(x, z) : Math.max(trueHeight(x, z), getEngine().getDimension().getFluidHeight());
return ignoreFluid ? trueHeight(x, z) : Math.max(trueHeight(x, z), getFluidHeight(x, z));
}
default int trueHeight(int x, int z) {
@@ -110,6 +112,10 @@ public interface EngineMantle extends MatterGenerator {
}
default boolean isCarved(int x, int h, int z) {
RiverCaveHydrology hydrology = RiverCaveHydrologyStorage.getIfPresent(getMantle(), x, h, z);
if (hydrology != null) {
return hydrology.carves();
}
return getMantle().get(x, h, z, MatterCavern.class) != null;
}
@@ -125,13 +131,17 @@ public interface EngineMantle extends MatterGenerator {
}
default boolean isUnderwater(int x, int z) {
return getHighest(x, z, true) <= getFluidHeight();
return getHighest(x, z, true) < getFluidHeight(x, z);
}
default int getFluidHeight() {
return getEngine().getDimension().getFluidHeight();
}
default int getFluidHeight(int x, int z) {
return (int) Math.round(getComplex().getRiverWaterSurfaceStream().get(x, z));
}
default boolean isDebugSmartBore() {
return getEngine().getDimension().isDebugSmartBore();
}
@@ -34,6 +34,10 @@ public interface MantleComponent extends Comparable<MantleComponent> {
int getRadius();
default int getInputRadius() {
return 0;
}
default IrisData getData() {
return getEngineMantle().getData();
}
@@ -31,6 +31,7 @@ import art.arcane.iris.engine.object.IObjectPlacer;
import art.arcane.iris.engine.object.IrisGeneratorStyle;
import art.arcane.iris.engine.object.IrisPosition;
import art.arcane.iris.engine.object.TileData;
import art.arcane.iris.engine.river.cave.RiverCaveHydrology;
import art.arcane.volmlib.util.collection.KSet;
import art.arcane.volmlib.util.documentation.ChunkCoordinates;
import art.arcane.volmlib.util.function.Function3;
@@ -181,6 +182,10 @@ public class MantleWriter implements IObjectPlacer, AutoCloseable {
if (chunk == null) return;
Matter matter = chunk.getOrCreate(y >> 4);
if ((t instanceof PlatformBlockState || t instanceof MatterCavern)
&& hasProtectedHydrology(matter, x, y, z)) {
return;
}
if (t instanceof PlatformBlockState) {
clearDeferredPlacement(matter, x, y, z);
}
@@ -206,6 +211,9 @@ public class MantleWriter implements IObjectPlacer, AutoCloseable {
}
Matter matter = chunk.getOrCreate(y >> 4);
if (hasProtectedHydrology(matter, x, y, z)) {
return false;
}
MatterCavern existing = matter.<MatterCavern>slice(MatterCavern.class).get(x & 15, y & 15, z & 15);
if (existing != null) {
return false;
@@ -226,6 +234,9 @@ public class MantleWriter implements IObjectPlacer, AutoCloseable {
}
Matter matter = chunk.getOrCreate(y >> 4);
if (hasProtectedHydrology(matter, x, y, z)) {
return false;
}
if (matter.hasSlice(PlatformBlockState.class)) {
matter.<PlatformBlockState>getSlice(PlatformBlockState.class).set(x & 15, y & 15, z & 15, null);
}
@@ -248,6 +259,9 @@ public class MantleWriter implements IObjectPlacer, AutoCloseable {
+ x + "," + y + "," + z);
}
Matter matter = chunk.getOrCreate(y >> 4);
if (hasProtectedHydrology(matter, x, y, z)) {
return;
}
if (matter.hasSlice(PlatformBlockState.class)) {
matter.<PlatformBlockState>getSlice(PlatformBlockState.class).set(x & 15, y & 15, z & 15, null);
}
@@ -271,6 +285,9 @@ public class MantleWriter implements IObjectPlacer, AutoCloseable {
if (matter == null) {
return;
}
if (hasProtectedHydrology(matter, x, y, z)) {
return;
}
if (matter.hasSlice(PlatformBlockState.class)) {
matter.<PlatformBlockState>getSlice(PlatformBlockState.class).set(x & 15, y & 15, z & 15, null);
}
@@ -328,9 +345,22 @@ public class MantleWriter implements IObjectPlacer, AutoCloseable {
if (matter == null || !matter.hasSlice(type)) {
return;
}
if ((type == PlatformBlockState.class || type == MatterCavern.class)
&& hasProtectedHydrology(matter, x, y, z)) {
return;
}
matter.getSlice(type).set(x & 15, y & 15, z & 15, null);
}
private static boolean hasProtectedHydrology(Matter matter, int x, int y, int z) {
if (!matter.hasSlice(RiverCaveHydrology.class)) {
return false;
}
RiverCaveHydrology hydrology = matter.<RiverCaveHydrology>getSlice(RiverCaveHydrology.class)
.get(x & 15, y & 15, z & 15);
return hydrology != null && hydrology.protectsPlacement();
}
private static void clearDeferredPlacement(Matter matter, int x, int y, int z) {
if (matter.hasSlice(Identifier.class)) {
matter.<Identifier>getSlice(Identifier.class).set(x & 15, y & 15, z & 15, null);
@@ -425,6 +455,10 @@ public class MantleWriter implements IObjectPlacer, AutoCloseable {
@Override
public boolean isCarved(int x, int y, int z) {
RiverCaveHydrology hydrology = getDataIfPresent(x, y, z, RiverCaveHydrology.class);
if (hydrology != null) {
return hydrology.carves();
}
return getDataIfPresent(x, y, z, MatterCavern.class) != null;
}
@@ -449,15 +483,28 @@ public class MantleWriter implements IObjectPlacer, AutoCloseable {
}
Matter matter = chunk.get(section);
if (matter == null || !matter.hasSlice(MatterCavern.class)) {
if (matter == null) {
continue;
}
MatterSlice<MatterCavern> slice = matter.getSlice(MatterCavern.class);
MatterSlice<MatterCavern> cavernSlice = matter.hasSlice(MatterCavern.class)
? matter.getSlice(MatterCavern.class)
: null;
MatterSlice<RiverCaveHydrology> hydrologySlice = matter.hasSlice(RiverCaveHydrology.class)
? matter.getSlice(RiverCaveHydrology.class)
: null;
if (cavernSlice == null && hydrologySlice == null) {
continue;
}
int sectionBaseY = section << 4;
int sectionMaxY = Math.min(cappedHeight, sectionBaseY + 16);
for (int y = sectionBaseY; y < sectionMaxY; y++) {
if (slice.get(localX, y & 15, localZ) != null) {
RiverCaveHydrology hydrology = hydrologySlice == null
? null
: hydrologySlice.get(localX, y & 15, localZ);
if (hydrology != null) {
carvedColumn[y] = hydrology.carves() ? (byte) 1 : 0;
} else if (cavernSlice != null && cavernSlice.get(localX, y & 15, localZ) != null) {
carvedColumn[y] = 1;
}
}
@@ -61,15 +61,14 @@ public final class CarveOrphanSweep {
int[] surfaceHeights,
int maxSurfaceBreakDepth,
int worldCeilingY,
int[] surfaceFluidBoundaryStartY,
int fluidHeight
long[] surfaceFluidBoundaries
) {
if (chunk == null) {
return 0;
}
return sweep(surfaceHeights, maxSurfaceBreakDepth, 0, worldCeilingY,
new MantleCarveAccess(chunk, surfaceFluidBoundaryStartY, fluidHeight));
new MantleCarveAccess(chunk, surfaceFluidBoundaries));
}
public static int sweep(int[] surfaceHeights, int maxSurfaceBreakDepth, int worldFloorY, int worldCeilingY, CarveAccess access) {
@@ -229,15 +228,13 @@ public final class CarveOrphanSweep {
private static final class MantleCarveAccess implements CarveAccess {
private final MantleChunk<Matter> chunk;
private final int[] surfaceFluidBoundaryStartY;
private final int fluidHeight;
private final long[] surfaceFluidBoundaries;
private MatterSlice<MatterCavern> cachedSlice;
private int cachedSectionIndex = -1;
private MantleCarveAccess(MantleChunk<Matter> chunk, int[] surfaceFluidBoundaryStartY, int fluidHeight) {
private MantleCarveAccess(MantleChunk<Matter> chunk, long[] surfaceFluidBoundaries) {
this.chunk = chunk;
this.surfaceFluidBoundaryStartY = surfaceFluidBoundaryStartY;
this.fluidHeight = fluidHeight;
this.surfaceFluidBoundaries = surfaceFluidBoundaries;
}
@Override
@@ -261,7 +258,7 @@ public final class CarveOrphanSweep {
@Override
public boolean isProtected(int localX, int y, int localZ) {
int columnIndex = PowerOfTwoCoordinates.packLocal16(localX, localZ);
return SurfaceFluidBoundaryPlan.protects(surfaceFluidBoundaryStartY, columnIndex, y, fluidHeight);
return SurfaceFluidBoundaryPlan.protects(surfaceFluidBoundaries, columnIndex, y);
}
@Override
@@ -23,6 +23,7 @@ import art.arcane.iris.engine.data.cache.Cache;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.object.IObjectPlacer;
import art.arcane.iris.engine.object.TileData;
import art.arcane.iris.engine.river.cave.RiverCaveHydrology;
import art.arcane.iris.spi.PlatformBlockState;
import art.arcane.iris.util.common.data.B;
import art.arcane.volmlib.util.collection.KList;
@@ -71,6 +72,12 @@ final class CaveObjectPlacementTransaction implements IObjectPlacer {
discard();
return CommitResult.REJECTED_BOUNDS;
}
RiverCaveHydrology hydrology = delegate.getData(
mutation.x(), mutation.y(), mutation.z(), RiverCaveHydrology.class);
if (hydrology != null && hydrology.protectsPlacement()) {
discard();
return CommitResult.REJECTED_HYDROLOGY;
}
}
for (BufferedMutation mutation : mutations) {
@@ -245,7 +252,8 @@ final class CaveObjectPlacementTransaction implements IObjectPlacer {
enum CommitResult {
COMMITTED,
EMPTY,
REJECTED_BOUNDS
REJECTED_BOUNDS,
REJECTED_HYDROLOGY
}
private interface BufferedMutation {
@@ -0,0 +1,66 @@
package art.arcane.iris.engine.mantle.components;
import art.arcane.iris.core.loader.IrisData;
import art.arcane.iris.engine.object.IrisGeneratorStyle;
import art.arcane.iris.engine.object.NoiseStyle;
import art.arcane.iris.engine.river.cave.RiverCaveGrottoShape;
import art.arcane.iris.engine.river.cave.RiverCavePlannerSettings;
import art.arcane.iris.engine.river.cave.RiverCaveSource;
import art.arcane.iris.util.project.noise.CNG;
import art.arcane.volmlib.util.math.RNG;
final class ConfiguredRiverGrottoShape implements RiverCaveGrottoShape {
private static final long SHAPE_SALT = 0x3C6EF372FE94F82BL;
private static final long WARP_X_SALT = 0xA54FF53A5F1D36F1L;
private static final long WARP_Y_SALT = 0x510E527FADE682D1L;
private static final long WARP_Z_SALT = 0x9B05688C2B3E6C1FL;
private final CNG shape;
private final CNG warpX;
private final CNG warpY;
private final CNG warpZ;
private final double warpStrength;
ConfiguredRiverGrottoShape(
long seed,
IrisData data,
IrisGeneratorStyle shapeStyle,
IrisGeneratorStyle warpStyle,
double warpStrength
) {
IrisGeneratorStyle resolvedShape = shapeStyle == null
? new IrisGeneratorStyle(NoiseStyle.FLAT)
: shapeStyle;
IrisGeneratorStyle resolvedWarp = warpStyle == null
? new IrisGeneratorStyle(NoiseStyle.FLAT)
: warpStyle;
shape = resolvedShape.createNoCache(new RNG(seed ^ SHAPE_SALT), data);
warpX = resolvedWarp.createNoCache(new RNG(seed ^ WARP_X_SALT), data);
warpY = resolvedWarp.createNoCache(new RNG(seed ^ WARP_Y_SALT), data);
warpZ = resolvedWarp.createNoCache(new RNG(seed ^ WARP_Z_SALT), data);
this.warpStrength = Math.max(0D, warpStrength);
}
@Override
public boolean contains(
RiverCaveSource source,
RiverCavePlannerSettings settings,
int offsetX,
int offsetY,
int offsetZ
) {
double worldX = source.target().x() + offsetX;
double worldY = source.target().y() + offsetY;
double worldZ = source.target().z() + offsetZ;
double warpedX = offsetX + warpX.fitDouble(-warpStrength, warpStrength, worldX, worldY, worldZ);
double warpedY = offsetY + warpY.fitDouble(-warpStrength, warpStrength, worldY, worldZ, worldX);
double warpedZ = offsetZ + warpZ.fitDouble(-warpStrength, warpStrength, worldZ, worldX, worldY);
double horizontalRadius = settings.grottoHorizontalRadius();
double verticalRadius = settings.grottoVerticalRadius();
double normalized = (warpedX * warpedX / (horizontalRadius * horizontalRadius))
+ (warpedY * warpedY / (verticalRadius * verticalRadius))
+ (warpedZ * warpedZ / (horizontalRadius * horizontalRadius));
double boundary = shape.fitDouble(-0.2D, 0.2D, worldX, worldY, worldZ);
return normalized <= 1D + boundary;
}
}
@@ -197,7 +197,7 @@ public class IrisCaveCarver3D {
double thresholdPenalty,
IrisRange worldYRange,
int[] precomputedSurfaceHeights,
int[] surfaceFluidBoundaryStartY,
long[] surfaceFluidBoundaries,
IrisRange overrideVerticalRange,
CaveFluidSupportPlan fluidSupportPlan
) {
@@ -318,7 +318,7 @@ public class IrisCaveCarver3D {
surfaceBreakThresholdBoost,
columnMaxY,
fluidMaxY,
surfaceFluidBoundaryStartY,
surfaceFluidBoundaries,
surfaceBreakFloorY,
surfaceBreakColumn,
columnThreshold,
@@ -341,7 +341,7 @@ public class IrisCaveCarver3D {
surfaceBreakThresholdBoost,
columnMaxY,
fluidMaxY,
surfaceFluidBoundaryStartY,
surfaceFluidBoundaries,
surfaceBreakFloorY,
surfaceBreakColumn,
columnThreshold,
@@ -367,7 +367,7 @@ public class IrisCaveCarver3D {
surfaceBreakThresholdBoost,
columnMaxY,
fluidMaxY,
surfaceFluidBoundaryStartY,
surfaceFluidBoundaries,
surfaceBreakFloorY,
surfaceBreakColumn,
columnThreshold,
@@ -391,7 +391,7 @@ public class IrisCaveCarver3D {
surfaceBreakThresholdBoost,
columnMaxY,
fluidMaxY,
surfaceFluidBoundaryStartY,
surfaceFluidBoundaries,
surfaceBreakFloorY,
surfaceBreakColumn,
columnThreshold,
@@ -422,7 +422,7 @@ public class IrisCaveCarver3D {
double surfaceBreakThresholdBoost,
int[] columnMaxY,
int[] fluidMaxY,
int[] surfaceFluidBoundaryStartY,
long[] surfaceFluidBoundaries,
int[] surfaceBreakFloorY,
boolean[] surfaceBreakColumn,
double[] columnThreshold,
@@ -478,7 +478,7 @@ public class IrisCaveCarver3D {
}
int columnIndex = activeColumnIndices[activeIndex];
if (SurfaceFluidBoundaryPlan.protects(surfaceFluidBoundaryStartY, columnIndex, y, fluidHeight)) {
if (SurfaceFluidBoundaryPlan.protects(surfaceFluidBoundaries, columnIndex, y)) {
continue;
}
planeColumnIndices[planeCount] = columnIndex;
@@ -553,7 +553,7 @@ public class IrisCaveCarver3D {
double surfaceBreakThresholdBoost,
int[] columnMaxY,
int[] fluidMaxY,
int[] surfaceFluidBoundaryStartY,
long[] surfaceFluidBoundaries,
int[] surfaceBreakFloorY,
boolean[] surfaceBreakColumn,
double[] columnThreshold,
@@ -615,7 +615,7 @@ public class IrisCaveCarver3D {
}
int columnIndex = activeColumnIndices[activeIndex];
if (SurfaceFluidBoundaryPlan.protects(surfaceFluidBoundaryStartY, columnIndex, y, fluidHeight)) {
if (SurfaceFluidBoundaryPlan.protects(surfaceFluidBoundaries, columnIndex, y)) {
continue;
}
planeColumnIndices[planeCount] = columnIndex;
@@ -712,7 +712,7 @@ public class IrisCaveCarver3D {
double surfaceBreakThresholdBoost,
int[] columnMaxY,
int[] fluidMaxY,
int[] surfaceFluidBoundaryStartY,
long[] surfaceFluidBoundaries,
int[] surfaceBreakFloorY,
boolean[] surfaceBreakColumn,
double[] columnThreshold,
@@ -814,7 +814,7 @@ public class IrisCaveCarver3D {
}
int index = tileIndices[columnIndex];
if (SurfaceFluidBoundaryPlan.protects(surfaceFluidBoundaryStartY, index, yy, fluidHeight)) {
if (SurfaceFluidBoundaryPlan.protects(surfaceFluidBoundaries, index, yy)) {
continue;
}
double localThreshold = passThreshold[index];
@@ -861,7 +861,7 @@ public class IrisCaveCarver3D {
double surfaceBreakThresholdBoost,
int[] columnMaxY,
int[] fluidMaxY,
int[] surfaceFluidBoundaryStartY,
long[] surfaceFluidBoundaries,
int[] surfaceBreakFloorY,
boolean[] surfaceBreakColumn,
double[] columnThreshold,
@@ -909,7 +909,7 @@ public class IrisCaveCarver3D {
int carveMaxY = Math.min(columnTopY, y + sampleStep - 1);
for (int yy = y; yy <= carveMaxY; yy++) {
if (SurfaceFluidBoundaryPlan.protects(surfaceFluidBoundaryStartY, index, yy, fluidHeight)) {
if (SurfaceFluidBoundaryPlan.protects(surfaceFluidBoundaries, index, yy)) {
continue;
}
MatterCavern verticalMatter = matterByY[yy - minY];
@@ -70,7 +70,7 @@ public class IrisStructureComponent extends IrisMantleComponent {
private static final MatterCavern CARVE_CAVERN = new MatterCavern(true, "", (byte) 3);
public IrisStructureComponent(EngineMantle engineMantle) {
super(engineMantle, ReservedFlag.JIGSAW, 3);
super(engineMantle, ReservedFlag.JIGSAW, 4);
}
@Override
@@ -104,20 +104,19 @@ public class MantleCarvingComponent extends IrisMantleComponent {
PrecisionStopwatch resolveStopwatch = PrecisionStopwatch.start();
List<WeightedProfile> weightedProfiles = resolveWeightedProfiles(x, z, complex, resolverState);
getEngineMantle().getEngine().getMetrics().getCarveResolve().put(resolveStopwatch.getMilliseconds());
int fluidHeight = getDimension().getFluidHeight();
int[] surfaceFluidBoundaryStartY = blendScratch.surfaceFluidBoundaryStartY;
long[] surfaceFluidBoundaries = blendScratch.surfaceFluidBoundaries;
SurfaceFluidBoundaryPlan.fill(
chunkSurfaceHeights,
blendScratch.fieldSurfaceHeights,
blendScratch.fieldHasFluid,
blendScratch.fieldFluidHeights,
FIELD_SIZE,
BLEND_RADIUS,
fluidHeight,
surfaceFluidBoundaryStartY
surfaceFluidBoundaries
);
CaveFluidSupportPlan fluidSupportPlan = new CaveFluidSupportPlan();
for (WeightedProfile weightedProfile : weightedProfiles) {
carveProfile(weightedProfile, writer, x, z, chunkSurfaceHeights, surfaceFluidBoundaryStartY, fluidSupportPlan);
carveProfile(weightedProfile, writer, x, z, chunkSurfaceHeights, surfaceFluidBoundaries, fluidSupportPlan);
}
UpperDimensionContext upperCtx = getEngineMantle().getEngine().getUpperContext();
@@ -132,8 +131,7 @@ public class MantleCarvingComponent extends IrisMantleComponent {
chunkSurfaceHeights,
maxSurfaceBreakDepth(weightedProfiles),
writer.getMantle().getWorldHeight() - 1,
surfaceFluidBoundaryStartY,
fluidHeight
surfaceFluidBoundaries
);
}
}
@@ -148,11 +146,11 @@ public class MantleCarvingComponent extends IrisMantleComponent {
@ChunkCoordinates
private void carveProfile(WeightedProfile weightedProfile, MantleWriter writer, int cx, int cz,
int[] chunkSurfaceHeights, int[] surfaceFluidBoundaryStartY,
int[] chunkSurfaceHeights, long[] surfaceFluidBoundaries,
CaveFluidSupportPlan fluidSupportPlan) {
IrisCaveCarver3D carver = getCarver(weightedProfile.profile);
carver.carve(writer, cx, cz, weightedProfile.columnWeights, MIN_WEIGHT, THRESHOLD_PENALTY,
weightedProfile.worldYRange, chunkSurfaceHeights, surfaceFluidBoundaryStartY, null, fluidSupportPlan);
weightedProfile.worldYRange, chunkSurfaceHeights, surfaceFluidBoundaries, null, fluidSupportPlan);
}
private void carveUpperTerrain(UpperDimensionContext upperCtx, List<WeightedProfile> normalProfiles,
@@ -475,7 +473,9 @@ public class MantleCarvingComponent extends IrisMantleComponent {
private void prefillProfileFieldSamples(int startX, int startZ, IrisComplex complex, BlendScratch blendScratch) {
fillFieldHeights(complex.getHeightStream(), startX, startZ, blendScratch.fieldSurfaceHeights);
fillFieldFluidPresence(complex.getFluidStream(), startX, startZ, blendScratch.fieldHasFluid);
fillFieldHeights(complex.getRiverWaterSurfaceStream(), startX, startZ, blendScratch.fieldFluidHeights);
fillFieldFluidPresence(complex.getFluidStream(), startX, startZ, blendScratch.fieldSurfaceHeights,
blendScratch.fieldFluidHeights, blendScratch.fieldHasFluid);
fillFieldObjects(complex.getRegionStream(), startX, startZ, blendScratch.fieldRegions);
fillFieldObjects(complex.getTrueBiomeStream(), startX, startZ, blendScratch.fieldSurfaceBiomes);
fillFieldObjects(complex.getCaveBiomeStream(), startX, startZ, blendScratch.fieldCaveBiomes);
@@ -499,11 +499,20 @@ public class MantleCarvingComponent extends IrisMantleComponent {
}
}
private void fillFieldFluidPresence(ProceduralStream<PlatformBlockState> stream, int startX, int startZ, boolean[] target) {
private void fillFieldFluidPresence(
ProceduralStream<PlatformBlockState> stream,
int startX,
int startZ,
double[] surfaceHeights,
double[] fluidHeights,
boolean[] target
) {
for (int fieldX = 0; fieldX < FIELD_SIZE; fieldX++) {
int worldX = startX + fieldX;
for (int fieldZ = 0; fieldZ < FIELD_SIZE; fieldZ++) {
target[(fieldX * FIELD_SIZE) + fieldZ] = B.isFluid(stream.get(worldX, startZ + fieldZ));
int fieldIndex = (fieldX * FIELD_SIZE) + fieldZ;
target[fieldIndex] = B.isFluid(stream.get(worldX, startZ + fieldZ))
&& Math.round(surfaceHeights[fieldIndex]) < Math.round(fluidHeights[fieldIndex]);
}
}
}
@@ -697,12 +706,13 @@ public class MantleCarvingComponent extends IrisMantleComponent {
private final IdentityHashMap<IrisCaveProfile, Boolean> activeProfiles = new IdentityHashMap<>();
private final List<IrisCaveProfile> profileOrder = new ArrayList<>();
private final double[] fieldSurfaceHeights = new double[FIELD_SIZE * FIELD_SIZE];
private final double[] fieldFluidHeights = new double[FIELD_SIZE * FIELD_SIZE];
private final boolean[] fieldHasFluid = new boolean[FIELD_SIZE * FIELD_SIZE];
private final long[] surfaceFluidBoundaries = new long[CHUNK_AREA];
private final IrisRegion[] fieldRegions = new IrisRegion[FIELD_SIZE * FIELD_SIZE];
private final IrisBiome[] fieldSurfaceBiomes = new IrisBiome[FIELD_SIZE * FIELD_SIZE];
private final IrisBiome[] fieldCaveBiomes = new IrisBiome[FIELD_SIZE * FIELD_SIZE];
private final int[] chunkSurfaceHeights = new int[CHUNK_AREA];
private final int[] surfaceFluidBoundaryStartY = new int[CHUNK_AREA];
private final double[] chunkSurfaceHeightSamples = new double[CHUNK_AREA];
}
}
@@ -59,7 +59,7 @@ public class MantleFloatingObjectComponent extends IrisMantleComponent {
private static final IrisObjectRotation ROTATION_NONE = IrisObjectRotation.of(0, 0, 0);
public MantleFloatingObjectComponent(EngineMantle engineMantle) {
super(engineMantle, ReservedFlag.FLOATING_OBJECT, 2);
super(engineMantle, ReservedFlag.FLOATING_OBJECT, 3);
}
@Override
@@ -48,6 +48,7 @@ import art.arcane.iris.engine.object.IrisProceduralPlacement;
import art.arcane.iris.engine.object.IrisProceduralTree;
import art.arcane.iris.engine.object.IrisRegion;
import art.arcane.iris.engine.object.ObjectPlaceMode;
import art.arcane.iris.engine.river.cave.RiverCaveHydrology;
import art.arcane.iris.spi.IrisLogging;
import art.arcane.volmlib.util.collection.KList;
import art.arcane.volmlib.util.collection.KMap;
@@ -83,7 +84,7 @@ public class MantleObjectComponent extends IrisMantleComponent {
private static final Set<String> MISSING_LOAD_KEY_WARNED = ConcurrentHashMap.newKeySet();
public MantleObjectComponent(EngineMantle engineMantle) {
super(engineMantle, ReservedFlag.OBJECT, 1);
super(engineMantle, ReservedFlag.OBJECT, 2);
}
private static String placementMarker(IrisObject object, int id, String context) {
@@ -581,11 +582,18 @@ public class MantleObjectComponent extends IrisMantleComponent {
minDepthBelowSurface,
anchorCache
);
RiverCaveHydrology hydrology = candidateY < 0
? null
: writer.getDataIfPresent(candidateX, candidateY, candidateZ, RiverCaveHydrology.class);
MatterCavern cavern = candidateY < 0
? null
: writer.getDataIfPresent(candidateX, candidateY, candidateZ, MatterCavern.class);
if (candidateY < 0
|| caveAnchorBiomeConflicts(candidateX, candidateY, candidateZ, expectedCaveBiomeKey)
|| !acceptsCaveAnchorFluid(
underwater,
writer.getDataIfPresent(candidateX, candidateY, candidateZ, MatterCavern.class),
hydrology == null ? cavern : hydrology.asCavern(),
hydrology,
candidateY,
getDimension().getCaveLavaHeight())) {
continue;
@@ -596,6 +604,19 @@ public class MantleObjectComponent extends IrisMantleComponent {
}
static boolean acceptsCaveAnchorFluid(boolean underwater, MatterCavern cavern, int y, int lavaHeight) {
return acceptsCaveAnchorFluid(underwater, cavern, null, y, lavaHeight);
}
static boolean acceptsCaveAnchorFluid(
boolean underwater,
MatterCavern cavern,
RiverCaveHydrology hydrology,
int y,
int lavaHeight
) {
if (hydrology != null && hydrology.protectsPlacement()) {
return false;
}
if (cavern == null || !cavern.isCavern()) {
return false;
}
@@ -0,0 +1,157 @@
package art.arcane.iris.engine.mantle.components;
import art.arcane.iris.engine.river.cave.CavePosition;
import art.arcane.iris.engine.river.cave.CaveVoxel;
import art.arcane.iris.engine.river.cave.CaveVoxelView;
import art.arcane.iris.engine.river.cave.RiverCaveAction;
import art.arcane.iris.engine.river.cave.RiverCaveHydrology;
import art.arcane.iris.engine.data.cache.Cache;
import art.arcane.iris.engine.object.IrisProceduralBlocks;
import art.arcane.iris.spi.PlatformBlockState;
import art.arcane.volmlib.util.function.Function2;
import art.arcane.volmlib.util.mantle.runtime.Mantle;
import art.arcane.volmlib.util.mantle.runtime.MantleChunk;
import art.arcane.volmlib.util.mantle.runtime.TectonicPlate;
import art.arcane.volmlib.util.matter.Matter;
import art.arcane.volmlib.util.matter.MatterCavern;
import it.unimi.dsi.fastutil.longs.Long2IntOpenHashMap;
import java.util.Objects;
final class MantleRiverCaveVoxelView implements MantleRiverHydrologyComponent.TunnelVoxelView {
private static final int CLOSED_COLUMN = Integer.MAX_VALUE;
private static final int CACHE_MISS = Integer.MIN_VALUE;
private final Mantle<Matter> mantle;
private final int worldHeight;
private final Function2<Integer, Integer, Integer> surfaceHeight;
private final Function2<Integer, Integer, PlatformBlockState> compatibleFluid;
private final Long2IntOpenHashMap openFloorCache;
private final Long2IntOpenHashMap surfaceHeightCache;
MantleRiverCaveVoxelView(
Mantle<Matter> mantle,
int worldHeight,
Function2<Integer, Integer, Integer> surfaceHeight,
Function2<Integer, Integer, PlatformBlockState> compatibleFluid
) {
this.mantle = Objects.requireNonNull(mantle);
this.worldHeight = worldHeight;
this.surfaceHeight = Objects.requireNonNull(surfaceHeight);
this.compatibleFluid = Objects.requireNonNull(compatibleFluid);
openFloorCache = new Long2IntOpenHashMap();
openFloorCache.defaultReturnValue(CACHE_MISS);
surfaceHeightCache = new Long2IntOpenHashMap();
surfaceHeightCache.defaultReturnValue(CACHE_MISS);
}
@Override
public boolean isInWorld(CavePosition position) {
return position.y() > 0 && position.y() < worldHeight - 1;
}
@Override
public CaveVoxel voxelAt(CavePosition position) {
RiverCaveHydrology hydrology = dataIfPresent(position, RiverCaveHydrology.class);
if (hydrology != null && hydrology.carves()) {
return hydrology.isWet() ? CaveVoxel.COMPATIBLE_FLUID : CaveVoxel.CAVE_AIR;
}
MatterCavern cavern = dataIfPresent(position, MatterCavern.class);
if (cavern != null) {
if (cavern.isLava()) {
return CaveVoxel.LAVA;
}
if (cavern.getLiquid() == 1) {
return CaveVoxel.COMPATIBLE_FLUID;
}
return CaveVoxel.CAVE_AIR;
}
PlatformBlockState block = dataIfPresent(position, PlatformBlockState.class);
if (block == null) {
return position.y() > surfaceY(position.x(), position.z())
? CaveVoxel.CAVE_AIR
: CaveVoxel.SOLID;
}
if (!block.isFluid()) {
return CaveVoxel.SOLID;
}
if (IrisProceduralBlocks.materialKey(block).endsWith(":lava")) {
return CaveVoxel.LAVA;
}
PlatformBlockState expected = compatibleFluid.apply(position.x(), position.z());
return expected != null
&& IrisProceduralBlocks.materialKey(expected).equals(IrisProceduralBlocks.materialKey(block))
? CaveVoxel.COMPATIBLE_FLUID
: CaveVoxel.INCOMPATIBLE_FLUID;
}
@Override
public boolean isOpenToSurface(CavePosition position) {
if (!isInWorld(position) || voxelAt(position) == CaveVoxel.SOLID) {
return false;
}
if (position.y() > surfaceY(position.x(), position.z())) {
return true;
}
long key = Cache.key(position.x(), position.z());
int openFloor = openFloorCache.get(key);
if (openFloor == CACHE_MISS) {
openFloor = resolveOpenFloor(position.x(), position.z());
openFloorCache.put(key, openFloor);
}
return openFloor != CLOSED_COLUMN && position.y() >= openFloor;
}
@Override
public RiverCaveAction riverActionAt(CavePosition position) {
RiverCaveHydrology hydrology = dataIfPresent(position, RiverCaveHydrology.class);
return hydrology == null ? null : hydrology.action();
}
private int resolveOpenFloor(int x, int z) {
int top = surfaceY(x, z);
CavePosition surface = new CavePosition(x, top, z);
if (voxelAt(surface) == CaveVoxel.SOLID) {
return CLOSED_COLUMN;
}
int y = top;
while (y > 0 && voxelAt(new CavePosition(x, y - 1, z)) != CaveVoxel.SOLID) {
y--;
}
return y;
}
private int surfaceY(int x, int z) {
long key = Cache.key(x, z);
int cached = surfaceHeightCache.get(key);
if (cached != CACHE_MISS) {
return cached;
}
int resolved = Math.max(1, Math.min(worldHeight - 2, surfaceHeight.apply(x, z)));
surfaceHeightCache.put(key, resolved);
return resolved;
}
private <T> T dataIfPresent(CavePosition position, Class<T> type) {
int chunkX = position.x() >> 4;
int chunkZ = position.z() >> 4;
TectonicPlate<Matter> plate = mantle.getLoadedRegions().get(Mantle.key(chunkX >> 5, chunkZ >> 5));
if (plate == null || plate.isClosed()) {
return null;
}
MantleChunk<Matter> chunk = plate.get(chunkX & 31, chunkZ & 31);
int section = position.y() >> 4;
if (chunk == null || !chunk.exists(section)) {
return null;
}
Matter matter = chunk.get(section);
if (matter == null || !matter.hasSlice(type)) {
return null;
}
return matter.<T>getSlice(type).get(
position.x() & 15,
position.y() & 15,
position.z() & 15
);
}
}
@@ -0,0 +1,707 @@
package art.arcane.iris.engine.mantle.components;
import art.arcane.iris.core.loader.IrisData;
import art.arcane.iris.engine.mantle.ComponentFlag;
import art.arcane.iris.engine.mantle.EngineMantle;
import art.arcane.iris.engine.mantle.IrisMantleComponent;
import art.arcane.iris.engine.mantle.MantleWriter;
import art.arcane.iris.engine.object.IrisRiverCaveFallback;
import art.arcane.iris.engine.object.IrisRiverCaveMode;
import art.arcane.iris.engine.object.IrisRiverCaves;
import art.arcane.iris.engine.object.IrisRiverExistingFluidPolicy;
import art.arcane.iris.engine.object.IrisDimension;
import art.arcane.iris.engine.object.IrisRiverNetwork;
import art.arcane.iris.engine.river.RiverAnchor;
import art.arcane.iris.engine.river.RiverRouteState;
import art.arcane.iris.engine.river.RiverSample;
import art.arcane.iris.engine.river.RiverSection;
import art.arcane.iris.engine.river.cave.CavePosition;
import art.arcane.iris.engine.river.cave.CaveVoxel;
import art.arcane.iris.engine.river.cave.CaveVoxelPrecondition;
import art.arcane.iris.engine.river.cave.CaveVoxelView;
import art.arcane.iris.engine.river.cave.RiverCaveAction;
import art.arcane.iris.engine.river.cave.RiverCaveContainmentPlanner;
import art.arcane.iris.engine.river.cave.RiverCaveFluidPolicy;
import art.arcane.iris.engine.river.cave.RiverCaveHydrology;
import art.arcane.iris.engine.river.cave.RiverCaveMode;
import art.arcane.iris.engine.river.cave.RiverCavePlan;
import art.arcane.iris.engine.river.cave.RiverCavePlannerSettings;
import art.arcane.iris.engine.river.cave.RiverCavePlanningResult;
import art.arcane.iris.engine.river.cave.RiverCaveSource;
import art.arcane.iris.engine.river.runtime.IrisRiverRuntime;
import art.arcane.iris.engine.river.runtime.IrisRiverSurfaceSample;
import art.arcane.iris.engine.river.runtime.IrisRiverTunnelSample;
import art.arcane.iris.util.project.context.ChunkContext;
import art.arcane.volmlib.util.mantle.flag.MantleFlag;
import art.arcane.volmlib.util.mantle.flag.ReservedFlag;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
@ComponentFlag(ReservedFlag.RIVER_HYDROLOGY)
public final class MantleRiverHydrologyComponent extends IrisMantleComponent {
private static final long CANDIDATE_SALT = 0x6A09E667F3BCC909L;
static final int PRIORITY = 1;
private static final int[] FALLBACK_X = {0, 1, -1, 0, 0};
private static final int[] FALLBACK_Z = {0, 0, 0, 1, -1};
private static final MantleFlag[] PREREQUISITES = {ReservedFlag.CARVED};
private static final int[][] NEIGHBORS = {
{1, 0, 0}, {-1, 0, 0},
{0, 1, 0}, {0, -1, 0},
{0, 0, 1}, {0, 0, -1}
};
private static final Comparator<Map.Entry<CavePosition, RiverCaveAction>> ACTION_ORDER = Comparator
.comparingInt((Map.Entry<CavePosition, RiverCaveAction> entry) -> entry.getKey().x())
.thenComparingInt(entry -> entry.getKey().y())
.thenComparingInt(entry -> entry.getKey().z());
private final RiverCaveContainmentPlanner planner;
public MantleRiverHydrologyComponent(EngineMantle engineMantle) {
super(engineMantle, ReservedFlag.RIVER_HYDROLOGY, PRIORITY);
planner = new RiverCaveContainmentPlanner();
}
@Override
public MantleFlag[] getPrerequisiteFlags() {
return PREREQUISITES;
}
@Override
public int getInputRadius() {
if (!getDimension().isCarvingEnabled()
|| getDimension().getRivers() == null
|| !getDimension().getRivers().isEnabled()) {
return 0;
}
IrisRiverRuntime runtime = getComplex().getRiverRuntime();
if (runtime == null) {
return 0;
}
return inputRadius(runtime.caveSettings(), tunnelHalo(runtime));
}
@Override
public void generateLayer(MantleWriter writer, int chunkX, int chunkZ, ChunkContext context) {
IrisRiverRuntime runtime = context.getComplex().getRiverRuntime();
if (runtime == null || !getDimension().isCarvingEnabled()) {
return;
}
publishTunnels(writer, context, runtime, chunkX, chunkZ);
IrisRiverCaves caves = runtime.caveSettings();
if (caves.getMode() == IrisRiverCaveMode.SEALED || caves.getMaximumPerReach() <= 0) {
return;
}
MantleRiverCaveVoxelView view = createView(writer, context);
int candidateHalo = candidateHalo(caves);
int minimumX = (chunkX << 4) - candidateHalo;
int minimumZ = (chunkZ << 4) - candidateHalo;
int maximumX = ((chunkX + 1) << 4) + candidateHalo;
int maximumZ = ((chunkZ + 1) << 4) + candidateHalo;
List<RiverAnchor> anchors = runtime.candidateAnchors(
minimumX,
minimumZ,
maximumX,
maximumZ,
caves.getMinimumSpacing(),
CANDIDATE_SALT
);
if (anchors.isEmpty()) {
return;
}
RiverCavePlannerSettings settings = plannerSettings(caves, seed(), getData());
List<RiverCaveSource> sources = new ArrayList<>();
Map<Long, String> floodedBiomes = new HashMap<>();
for (RiverAnchor anchor : anchors) {
if (!runtime.acceptsCaveAnchor(anchor)) {
continue;
}
SourceCandidate candidate = sourceFor(runtime, view, caves, anchor);
if (candidate == null) {
continue;
}
RiverCaveSource source = candidate.source();
RiverCavePlan initial = planner.plan(view, source, settings);
if (!initial.accepted()
&& caves.getFallback() == IrisRiverCaveFallback.GENERATE_GROTTO) {
source = fallbackSource(view, caves, settings, candidate, source);
}
if (source == null) {
continue;
}
sources.add(source);
floodedBiomes.put(source.sourceId(), runtime.selectFloodedCaveBiome(anchor));
}
if (sources.isEmpty()) {
return;
}
RiverCavePlanningResult result = planner.planAll(view, sources, settings);
MantleRiverCaveVoxelView revalidationView = createView(writer, context);
if (!preconditionsHold(revalidationView, result.baselinePreconditions())) {
return;
}
publishLocal(writer, chunkX, chunkZ, result, floodedBiomes);
}
@Override
protected int computeRadius() {
return 0;
}
public static boolean isEnabledFor(IrisDimension dimension) {
IrisRiverNetwork rivers = dimension.getRivers();
if (!dimension.isUseMantle()
|| !dimension.isCarvingEnabled()
|| dimension.getDisabledComponents().contains(ReservedFlag.CARVED)
|| dimension.getDisabledComponents().contains(ReservedFlag.RIVER_HYDROLOGY)
|| rivers == null
|| !rivers.isEnabled()) {
return false;
}
return true;
}
public static boolean isCaveConnectionsEnabledFor(IrisDimension dimension) {
if (!isEnabledFor(dimension)) {
return false;
}
IrisRiverCaves caves = dimension.getRivers().getCaves();
return caves != null
&& caves.getMode() != IrisRiverCaveMode.SEALED
&& caves.getMaximumPerReach() > 0;
}
static int planningHalo(IrisRiverCaves caves) {
return cavePublicationRadius(caves) * 4;
}
static int inputRadius(IrisRiverCaves caves, int tunnelRadius) {
if (caves.getMode() == IrisRiverCaveMode.SEALED || caves.getMaximumPerReach() <= 0) {
return tunnelRadius;
}
return Math.max(tunnelRadius, planningHalo(caves));
}
static int candidateHalo(IrisRiverCaves caves) {
return cavePublicationRadius(caves) * 3;
}
static int cavePublicationRadius(IrisRiverCaves caves) {
int generatedRadius = generatedGrottoPublicationRadius(caves);
return switch (caves.getMode()) {
case SEALED -> 0;
case GENERATE_GROTTO -> generatedRadius;
case FLOOD_CLOSED_COMPONENT, GROTTO_OR_CLOSED_COMPONENT, WATERFALL_POOL ->
Math.max(closedComponentPublicationRadius(caves), generatedRadius);
};
}
static int closedComponentPublicationRadius(IrisRiverCaves caves) {
return caves.getMaxFloodRadius() + 1;
}
static int generatedGrottoPublicationRadius(IrisRiverCaves caves) {
int targetOffset = caves.getFallback() == IrisRiverCaveFallback.GENERATE_GROTTO
? caves.getThroatRadius() + 2
: 0;
long maximumX = (long) targetOffset + caves.getGrottoHorizontalRadius() + 1L;
long maximumZ = caves.getGrottoHorizontalRadius();
int grottoRadius = (int) StrictMath.ceil(StrictMath.sqrt(
maximumX * maximumX + maximumZ * maximumZ
));
int throatRadius = targetOffset + caves.getThroatRadius();
return Math.max(grottoRadius, throatRadius);
}
static int tunnelHalo(IrisRiverRuntime runtime) {
return Math.max(1, (int) StrictMath.ceil(runtime.maximumChannelWidth() * 0.5D) + 1);
}
static int waterHeadY(IrisRiverSurfaceSample sample, IrisRiverCaves caves) {
return (int) Math.round(sample.waterSurfaceY()) + caves.getWaterLevelOffset();
}
static boolean owns(int chunkX, int chunkZ, CavePosition position) {
return (position.x() >> 4) == chunkX && (position.z() >> 4) == chunkZ;
}
static RiverCaveFluidPolicy fluidPolicy(IrisRiverExistingFluidPolicy policy) {
return switch (policy) {
case REJECT -> RiverCaveFluidPolicy.REJECT_EXISTING;
case ALLOW_SAME -> RiverCaveFluidPolicy.ALLOW_COMPATIBLE;
case REPLACE -> RiverCaveFluidPolicy.REPLACE_CONTAINED;
};
}
static boolean preconditionsHold(
CaveVoxelView view,
Map<CavePosition, CaveVoxelPrecondition> preconditions
) {
for (Map.Entry<CavePosition, CaveVoxelPrecondition> entry : preconditions.entrySet()) {
CaveVoxelPrecondition expected = entry.getValue();
if (view.voxelAt(entry.getKey()) != expected.voxel()
|| view.isOpenToSurface(entry.getKey()) != expected.openToSurface()) {
return false;
}
}
return true;
}
static TunnelPlan planTunnels(
CaveVoxelView view,
int chunkX,
int chunkZ,
int halo,
FootprintSampler footprintSampler,
TunnelSampler tunnelSampler,
SurfaceSampler surfaceSampler
) {
int minimumX = (chunkX << 4) - halo;
int minimumZ = (chunkZ << 4) - halo;
int maximumX = ((chunkX + 1) << 4) + halo;
int maximumZ = ((chunkZ + 1) << 4) + halo;
if (!footprintSampler.sample(minimumX, minimumZ, maximumX, maximumZ).present()) {
return TunnelPlan.empty();
}
ArrayList<TunnelColumn> solidColumns = new ArrayList<>();
for (int x = minimumX; x < maximumX; x++) {
for (int z = minimumZ; z < maximumZ; z++) {
IrisRiverTunnelSample sample = tunnelSampler.sample(x, z);
TunnelColumn column = createTunnelColumn(view, x, z, sample);
if (column != null) {
solidColumns.add(column);
}
}
}
if (solidColumns.isEmpty()) {
return TunnelPlan.empty();
}
Map<CavePosition, RiverCaveAction> candidateActions = mergeActions(solidColumns);
ArrayList<TunnelColumn> containedColumns = new ArrayList<>(solidColumns.size());
for (TunnelColumn column : solidColumns) {
if (isTunnelColumnContained(view, column, candidateActions.keySet(), surfaceSampler)) {
containedColumns.add(column);
}
}
Map<CavePosition, RiverCaveAction> actions = mergeActions(containedColumns);
LinkedHashMap<CavePosition, CaveVoxelPrecondition> preconditions = new LinkedHashMap<>();
for (CavePosition position : actions.keySet()) {
preconditions.put(position, new CaveVoxelPrecondition(
view.voxelAt(position),
view.isOpenToSurface(position)
));
}
for (CavePosition position : List.copyOf(actions.keySet())) {
for (int[] offset : NEIGHBORS) {
CavePosition neighbor = offset(position, offset);
if (actions.containsKey(neighbor)
|| !view.isInWorld(neighbor)
|| view.voxelAt(neighbor) != CaveVoxel.SOLID) {
continue;
}
actions.putIfAbsent(neighbor, RiverCaveAction.SEAL_GUARD);
preconditions.putIfAbsent(neighbor, new CaveVoxelPrecondition(CaveVoxel.SOLID, false));
}
}
return new TunnelPlan(Map.copyOf(actions), Map.copyOf(preconditions));
}
private static TunnelColumn createTunnelColumn(
CaveVoxelView view,
int x,
int z,
IrisRiverTunnelSample sample
) {
if (sample == null) {
return null;
}
LinkedHashMap<CavePosition, RiverCaveAction> actions = new LinkedHashMap<>();
for (int y = sample.bedY() + 1; y <= sample.ceilingY(); y++) {
CavePosition position = new CavePosition(x, y, z);
RiverCaveAction action = y <= sample.waterHeadY()
? RiverCaveAction.WET_SOURCE
: RiverCaveAction.DRY_AIR;
if (!view.isInWorld(position)
|| (view.voxelAt(position) != CaveVoxel.SOLID
&& !matchesPublishedAction(view, position, action))) {
return null;
}
actions.put(position, action);
}
return actions.isEmpty() ? null : new TunnelColumn(actions);
}
private static boolean isTunnelColumnContained(
CaveVoxelView view,
TunnelColumn column,
Set<CavePosition> candidateActions,
SurfaceSampler surfaceSampler
) {
for (CavePosition position : column.actions().keySet()) {
for (int[] offset : NEIGHBORS) {
CavePosition neighbor = offset(position, offset);
if (candidateActions.contains(neighbor)) {
continue;
}
if (!view.isInWorld(neighbor)) {
return false;
}
if (view.voxelAt(neighbor) == CaveVoxel.SOLID || isSurfaceMouth(neighbor, surfaceSampler)) {
continue;
}
return false;
}
}
return true;
}
private static boolean isSurfaceMouth(CavePosition position, SurfaceSampler surfaceSampler) {
IrisRiverSurfaceSample sample = surfaceSampler.sample(position.x(), position.z());
if (!isWetChannelBed(sample) || sample.subterranean()) {
return false;
}
int bedY = (int) Math.round(sample.terrainHeight());
int headY = (int) Math.round(sample.waterSurfaceY());
return position.y() > bedY && position.y() <= headY;
}
private static Map<CavePosition, RiverCaveAction> mergeActions(List<TunnelColumn> columns) {
LinkedHashMap<CavePosition, RiverCaveAction> actions = new LinkedHashMap<>();
for (TunnelColumn column : columns) {
actions.putAll(column.actions());
}
return actions;
}
private static boolean matchesPublishedAction(
CaveVoxelView view,
CavePosition position,
RiverCaveAction action
) {
return view instanceof TunnelVoxelView tunnelView
&& tunnelView.riverActionAt(position) == action;
}
private static CavePosition offset(CavePosition position, int[] offset) {
return new CavePosition(
position.x() + offset[0],
position.y() + offset[1],
position.z() + offset[2]
);
}
private MantleRiverCaveVoxelView createView(MantleWriter writer, ChunkContext context) {
return new MantleRiverCaveVoxelView(
writer.getMantle(),
writer.getMantle().getWorldHeight(),
(x, z) -> context.getComplex().getRoundedHeighteightStream().get(x, z),
(x, z) -> context.getComplex().getFluidStream().get(x, z)
);
}
private void publishTunnels(
MantleWriter writer,
ChunkContext context,
IrisRiverRuntime runtime,
int chunkX,
int chunkZ
) {
for (int attempt = 0; attempt < 2; attempt++) {
MantleRiverCaveVoxelView view = createView(writer, context);
TunnelPlan plan = planTunnels(
view,
chunkX,
chunkZ,
tunnelHalo(runtime),
runtime::sampleFootprint,
runtime::sampleTunnel,
runtime::sample
);
MantleRiverCaveVoxelView revalidationView = createView(writer, context);
if (preconditionsHold(revalidationView, plan.preconditions())) {
publishTunnelLocal(writer, chunkX, chunkZ, plan);
return;
}
}
}
static RiverCavePlannerSettings plannerSettings(IrisRiverCaves caves, long seed, IrisData data) {
int horizontalRadius = generatedGrottoPublicationRadius(caves);
int maximumDepth = caves.getMaxBoreDepth() + caves.getGrottoVerticalRadius() + 1;
int throatLength = caves.getMaxBoreDepth() + horizontalRadius;
return new RiverCavePlannerSettings(
horizontalRadius,
maximumDepth,
caves.getMaxFloodVolume(),
throatLength,
caves.getThroatRadius(),
caves.getGrottoHorizontalRadius(),
caves.getGrottoVerticalRadius(),
caves.getDryHeadroom(),
fluidPolicy(caves.getExistingFluidPolicy()),
new ConfiguredRiverGrottoShape(
seed,
data,
caves.getGrottoShapeStyle(),
caves.getGrottoWarpStyle(),
caves.getGrottoWarpStrength()
),
caves.getMaxFloodRadius(),
caves.getMaxFloodDepth()
);
}
private SourceCandidate sourceFor(
IrisRiverRuntime runtime,
CaveVoxelView view,
IrisRiverCaves caves,
RiverAnchor anchor
) {
int x = (int) StrictMath.floor(anchor.x());
int z = (int) StrictMath.floor(anchor.z());
IrisRiverSurfaceSample sample = runtime.sample(x, z);
if (!isWetChannelBed(sample)) {
return null;
}
int bedY = (int) Math.round(sample.terrainHeight());
int headY = waterHeadY(sample, caves);
int entryY = Math.max(bedY, headY);
CavePosition entry = new CavePosition(x, entryY, z);
if (!view.isInWorld(entry)) {
return null;
}
CavePosition existingTarget = findExistingTarget(view, caves, x, z, bedY, headY);
RiverCaveMode requestedMode = sourceMode(
caves.getMode(),
runtime.isTerminalCaveAnchor(anchor)
);
CavePosition target;
RiverCaveMode sourceMode;
if (requestedMode == RiverCaveMode.GENERATED_GROTTO) {
target = findGeneratedTarget(view, caves, entry, headY, 0, 0);
sourceMode = RiverCaveMode.GENERATED_GROTTO;
} else if (requestedMode == RiverCaveMode.GROTTO_OR_CLOSED_COMPONENT) {
target = existingTarget;
sourceMode = RiverCaveMode.CLOSED_COMPONENT;
if (target == null) {
target = findGeneratedTarget(view, caves, entry, headY, 0, 0);
sourceMode = RiverCaveMode.GENERATED_GROTTO;
}
} else {
target = existingTarget;
sourceMode = requestedMode;
}
if (target == null && caves.getFallback() == IrisRiverCaveFallback.GENERATE_GROTTO) {
target = findGeneratedTarget(view, caves, entry, headY, 0, 0);
sourceMode = RiverCaveMode.GENERATED_GROTTO;
}
if (target == null) {
return null;
}
RiverCaveSource source = new RiverCaveSource(anchor.stableId(), entry, target, headY, sourceMode);
return new SourceCandidate(entry, headY, source);
}
static boolean isWetChannelBed(IrisRiverSurfaceSample sample) {
return sample.river().present()
&& sample.river().state() == RiverRouteState.WET
&& sample.river().section() == RiverSection.CHANNEL
&& sample.surfaceFluid();
}
static CavePosition findExistingTarget(
CaveVoxelView view,
IrisRiverCaves caves,
int x,
int z,
int bedY,
int headY
) {
int maximumY = Math.min(bedY - 1, headY);
int minimumY = Math.max(1, bedY - caves.getMaxBoreDepth());
for (int y = maximumY; y >= minimumY; y--) {
CavePosition position = new CavePosition(x, y, z);
CaveVoxel voxel = view.voxelAt(position);
if (voxel != CaveVoxel.SOLID) {
return position;
}
}
return null;
}
static CavePosition findGeneratedTarget(
CaveVoxelView view,
IrisRiverCaves caves,
CavePosition entry,
int headY,
int offsetX,
int offsetZ
) {
int preferredY = headY + caves.getDryHeadroom() - caves.getGrottoVerticalRadius();
int maximumY = Math.min(Math.min(entry.y() - 1, headY), preferredY);
int minimumY = Math.max(1, entry.y() - caves.getMaxBoreDepth());
for (int y = maximumY; y >= minimumY; y--) {
CavePosition target = new CavePosition(entry.x() + offsetX, y, entry.z() + offsetZ);
if (view.isInWorld(target) && view.voxelAt(target) == CaveVoxel.SOLID) {
return target;
}
}
return null;
}
private RiverCaveSource fallbackSource(
CaveVoxelView view,
IrisRiverCaves caves,
RiverCavePlannerSettings settings,
SourceCandidate candidate,
RiverCaveSource rejected
) {
int fallbackDistance = caves.getThroatRadius() + 2;
for (int index = 0; index < FALLBACK_X.length; index++) {
int offsetX = FALLBACK_X[index] * fallbackDistance;
int offsetZ = FALLBACK_Z[index] * fallbackDistance;
CavePosition target = findGeneratedTarget(
view,
caves,
candidate.entry(),
candidate.waterHeadY(),
offsetX,
offsetZ
);
if (target == null || target.equals(rejected.target())) {
continue;
}
RiverCaveSource fallback = new RiverCaveSource(
rejected.sourceId(),
candidate.entry(),
target,
candidate.waterHeadY(),
RiverCaveMode.GENERATED_GROTTO
);
if (planner.plan(view, fallback, settings).accepted()) {
return fallback;
}
}
return null;
}
static RiverCaveMode sourceMode(IrisRiverCaveMode mode, boolean forcedTerminal) {
if (forcedTerminal) {
return RiverCaveMode.GENERATED_GROTTO;
}
return switch (mode) {
case FLOOD_CLOSED_COMPONENT -> RiverCaveMode.CLOSED_COMPONENT;
case GENERATE_GROTTO -> RiverCaveMode.GENERATED_GROTTO;
case GROTTO_OR_CLOSED_COMPONENT -> RiverCaveMode.GROTTO_OR_CLOSED_COMPONENT;
case WATERFALL_POOL -> RiverCaveMode.WATERFALL_POOL;
case SEALED -> throw new IllegalArgumentException("Sealed river caves do not create sources");
};
}
private void publishLocal(
MantleWriter writer,
int chunkX,
int chunkZ,
RiverCavePlanningResult result,
Map<Long, String> floodedBiomes
) {
Map<CavePosition, RiverCaveSource> owners = actionOwners(result);
ArrayList<Map.Entry<CavePosition, RiverCaveAction>> actions = new ArrayList<>(result.actions().entrySet());
actions.sort(ACTION_ORDER);
for (Map.Entry<CavePosition, RiverCaveAction> entry : actions) {
CavePosition position = entry.getKey();
if (!owns(chunkX, chunkZ, position)) {
continue;
}
RiverCaveSource source = owners.get(position);
String biome = source == null ? "" : floodedBiomes.getOrDefault(source.sourceId(), "");
if (entry.getValue() == RiverCaveAction.SEAL_GUARD) {
biome = "";
}
writer.setData(
position.x(),
position.y(),
position.z(),
new RiverCaveHydrology(entry.getValue(), biome)
);
}
}
private void publishTunnelLocal(
MantleWriter writer,
int chunkX,
int chunkZ,
TunnelPlan plan
) {
ArrayList<Map.Entry<CavePosition, RiverCaveAction>> actions = new ArrayList<>(plan.actions().entrySet());
actions.sort(ACTION_ORDER);
for (Map.Entry<CavePosition, RiverCaveAction> entry : actions) {
CavePosition position = entry.getKey();
if (owns(chunkX, chunkZ, position)) {
writer.setData(position.x(), position.y(), position.z(), RiverCaveHydrology.of(entry.getValue()));
}
}
}
private Map<CavePosition, RiverCaveSource> actionOwners(RiverCavePlanningResult result) {
Map<CavePosition, RiverCaveSource> owners = new LinkedHashMap<>();
for (RiverCavePlan plan : result.plans()) {
if (!plan.accepted()) {
continue;
}
for (CavePosition position : plan.actions().keySet()) {
owners.put(position, plan.source());
}
}
return owners;
}
private record SourceCandidate(
CavePosition entry,
int waterHeadY,
RiverCaveSource source
) {
}
@FunctionalInterface
interface FootprintSampler {
RiverSample sample(double minimumX, double minimumZ, double maximumX, double maximumZ);
}
@FunctionalInterface
interface TunnelSampler {
IrisRiverTunnelSample sample(int x, int z);
}
@FunctionalInterface
interface SurfaceSampler {
IrisRiverSurfaceSample sample(int x, int z);
}
interface TunnelVoxelView extends CaveVoxelView {
RiverCaveAction riverActionAt(CavePosition position);
}
record TunnelPlan(
Map<CavePosition, RiverCaveAction> actions,
Map<CavePosition, CaveVoxelPrecondition> preconditions
) {
static TunnelPlan empty() {
return new TunnelPlan(Map.of(), Map.of());
}
}
private record TunnelColumn(Map<CavePosition, RiverCaveAction> actions) {
}
}
@@ -32,16 +32,17 @@ final class SurfaceFluidBoundaryPlan {
int[] chunkSurfaceHeights,
double[] fieldSurfaceHeights,
boolean[] fieldHasFluid,
double[] fieldFluidHeights,
int fieldSize,
int padding,
int fluidHeight,
int[] boundaryStartY
long[] boundaries
) {
if (chunkSurfaceHeights == null || chunkSurfaceHeights.length < CHUNK_AREA
|| boundaryStartY == null || boundaryStartY.length < CHUNK_AREA
|| boundaries == null || boundaries.length < CHUNK_AREA
|| padding < 1 || fieldSize < CHUNK_SIZE + (padding * 2)
|| fieldSurfaceHeights == null || fieldSurfaceHeights.length < fieldSize * fieldSize
|| fieldHasFluid == null || fieldHasFluid.length < fieldSize * fieldSize) {
|| fieldHasFluid == null || fieldHasFluid.length < fieldSize * fieldSize
|| fieldFluidHeights == null || fieldFluidHeights.length < fieldSize * fieldSize) {
throw new IllegalArgumentException("Surface fluid boundary fields do not cover a padded chunk");
}
@@ -51,44 +52,67 @@ final class SurfaceFluidBoundaryPlan {
int fieldZ = localZ + padding;
int columnIndex = PowerOfTwoCoordinates.packLocal16(localX, localZ);
int boundaryY = NO_BOUNDARY;
int boundaryEndY = Integer.MIN_VALUE;
int surfaceY = chunkSurfaceHeights[columnIndex];
int fieldIndex = (fieldX * fieldSize) + fieldZ;
int fluidHeight = roundedHeight(fieldFluidHeights[fieldIndex]);
if (fieldHasFluid[fieldIndex] && surfaceY < fluidHeight) {
boundaryY = surfaceY;
boundaryEndY = fluidHeight;
}
boundaryY = lowerBoundary(boundaryY, fieldSurfaceHeights, fieldHasFluid,
((fieldX - 1) * fieldSize) + fieldZ, fluidHeight);
boundaryY = lowerBoundary(boundaryY, fieldSurfaceHeights, fieldHasFluid,
((fieldX + 1) * fieldSize) + fieldZ, fluidHeight);
boundaryY = lowerBoundary(boundaryY, fieldSurfaceHeights, fieldHasFluid,
(fieldX * fieldSize) + fieldZ - 1, fluidHeight);
boundaryY = lowerBoundary(boundaryY, fieldSurfaceHeights, fieldHasFluid,
(fieldX * fieldSize) + fieldZ + 1, fluidHeight);
boundaryStartY[columnIndex] = boundaryY;
long boundary = expandBoundary(boundaryY, boundaryEndY, fieldSurfaceHeights,
fieldHasFluid, fieldFluidHeights, ((fieldX - 1) * fieldSize) + fieldZ);
boundary = expandBoundary(startY(boundary), endY(boundary), fieldSurfaceHeights,
fieldHasFluid, fieldFluidHeights, ((fieldX + 1) * fieldSize) + fieldZ);
boundary = expandBoundary(startY(boundary), endY(boundary), fieldSurfaceHeights,
fieldHasFluid, fieldFluidHeights, (fieldX * fieldSize) + fieldZ - 1);
boundaries[columnIndex] = expandBoundary(startY(boundary), endY(boundary), fieldSurfaceHeights,
fieldHasFluid, fieldFluidHeights, (fieldX * fieldSize) + fieldZ + 1);
}
}
}
static boolean protects(int[] boundaryStartY, int columnIndex, int y, int fluidHeight) {
return boundaryStartY != null
&& columnIndex >= 0
&& columnIndex < boundaryStartY.length
&& y >= boundaryStartY[columnIndex]
&& y <= fluidHeight;
static boolean protects(long[] boundaries, int columnIndex, int y) {
if (boundaries == null || columnIndex < 0 || columnIndex >= boundaries.length) {
return false;
}
long boundary = boundaries[columnIndex];
return y >= startY(boundary) && y <= endY(boundary);
}
private static int lowerBoundary(
static int startY(long boundary) {
return (int) (boundary >> 32);
}
static int endY(long boundary) {
return (int) boundary;
}
private static long expandBoundary(
int currentBoundaryY,
int currentBoundaryEndY,
double[] fieldSurfaceHeights,
boolean[] fieldHasFluid,
int fieldIndex,
int fluidHeight
double[] fieldFluidHeights,
int fieldIndex
) {
int fluidHeight = roundedHeight(fieldFluidHeights[fieldIndex]);
int neighborSurfaceY = (int) Math.round(fieldSurfaceHeights[fieldIndex]);
if (!fieldHasFluid[fieldIndex] || neighborSurfaceY >= fluidHeight) {
return currentBoundaryY;
return boundary(currentBoundaryY, currentBoundaryEndY);
}
return Math.min(currentBoundaryY, neighborSurfaceY + 1);
return boundary(
Math.min(currentBoundaryY, neighborSurfaceY + 1),
Math.max(currentBoundaryEndY, fluidHeight)
);
}
private static int roundedHeight(double height) {
return Double.isFinite(height) ? (int) Math.round(height) : Integer.MIN_VALUE;
}
static long boundary(int startY, int endY) {
return ((long) startY << 32) | (endY & 0xffffffffL);
}
}
@@ -28,6 +28,8 @@ import art.arcane.iris.engine.object.IrisDecorationPart;
import art.arcane.iris.engine.object.IrisDecorator;
import art.arcane.iris.engine.object.IrisDimensionCarvingResolver;
import art.arcane.iris.engine.object.IrisProceduralBlocks;
import art.arcane.iris.engine.river.cave.RiverCaveAction;
import art.arcane.iris.engine.river.cave.RiverCaveHydrology;
import art.arcane.iris.util.project.context.ChunkContext;
import art.arcane.iris.util.common.data.B;
import art.arcane.volmlib.util.documentation.ChunkCoordinates;
@@ -105,49 +107,26 @@ public class IrisCarveModifier extends EngineAssignedModifier<PlatformBlockState
PrecisionStopwatch resolveStopwatch = PrecisionStopwatch.start();
int worldHeightSpan = getEngine().getWorld().maxHeight() - getEngine().getWorld().minHeight();
int caveLavaHeight = getEngine().getDimension().getCaveLavaHeight();
mantleChunk.iterate(MatterCavern.class, (xx, yy, zz, cavern) -> {
if (cavern == null) {
return;
}
if (yy >= worldHeightSpan || yy <= 0) {
return;
}
int rx = xx & 15;
int rz = zz & 15;
int columnIndex = PowerOfTwoCoordinates.packLocal16(rx, rz);
if (upperSurfaceHeights != null && yy >= upperSurfaceHeights[columnIndex]) {
return;
}
PlatformBlockState current = output.getRaw(rx, yy, rz);
boolean explicitCarveIntent = hasExplicitCarveIntent(cavern);
if (shouldPreserveExistingFluid(cavern, current)) {
return;
}
columnMasks[columnIndex].add(yy);
if (!cavern.getCustomBiome().isEmpty()) {
scratch.customCaveBiomePresent = true;
}
if (current.isAir() && !explicitCarveIntent) {
return;
}
if (explicitCarveIntent) {
// Only a fluid cavern consumes the fluid sample, and on the maintenance path that
// sample is a full procedural stream evaluation, so never take it per voxel.
PlatformBlockState fluid = isFluidIntent(cavern) ? context.getFluid().get(rx, rz) : null;
output.setRaw(rx, yy, rz, resolveExplicitCarveState(cavern, fluid, LAVA, AIR));
} else if (usesDefaultLava(caveLavaHeight, yy)) {
output.setRaw(rx, yy, rz, LAVA);
} else {
output.setRaw(rx, yy, rz, AIR);
CarveResolutionContext resolutionContext = new CarveResolutionContext(
output,
context,
scratch,
columnMasks,
upperSurfaceHeights,
worldHeightSpan,
caveLavaHeight
);
CarveResolver carveResolver = new CarveResolver(resolutionContext);
mantleChunk.iterate(MatterCavern.class, (xx, yy, zz, cavern) -> carveResolver.apply(
xx,
yy,
zz,
cavern,
dataIfPresent(mantleChunk, xx, yy, zz, RiverCaveHydrology.class)
));
mantleChunk.iterate(RiverCaveHydrology.class, (xx, yy, zz, hydrology) -> {
if (dataIfPresent(mantleChunk, xx, yy, zz, MatterCavern.class) == null) {
carveResolver.apply(xx, yy, zz, null, hydrology);
}
});
if (scratch.customCaveBiomePresent) {
@@ -161,6 +140,11 @@ public class IrisCarveModifier extends EngineAssignedModifier<PlatformBlockState
PrecisionStopwatch applyStopwatch = PrecisionStopwatch.start();
try {
walls.forEach((rx, yy, rz, cavern) -> {
RiverCaveHydrology hydrology = dataIfPresent(
mantleChunk, rx, yy, rz, RiverCaveHydrology.class);
if (hydrology != null && hydrology.protectsPlacement()) {
return;
}
int worldX = rx + chunkBlockX;
int worldZ = rz + chunkBlockZ;
String customBiome = cavern.getCustomBiome();
@@ -179,14 +163,39 @@ public class IrisCarveModifier extends EngineAssignedModifier<PlatformBlockState
});
for (int columnIndex = 0; columnIndex < 256; columnIndex++) {
processColumnFromMask(output, mantleChunk, mantle, columnMasks[columnIndex], columnIndex, x, z, resolverState, caveBiomeCache, customBiomeCache);
int localX = PowerOfTwoCoordinates.unpackLocal16X(columnIndex);
int localZ = columnIndex & 15;
processColumnFromMask(
output,
mantleChunk,
mantle,
columnMasks[columnIndex],
columnIndex,
x,
z,
resolverState,
caveBiomeCache,
customBiomeCache,
context.getFluid().get(localX, localZ)
);
}
for (int columnIndex = 0; columnIndex < 256; columnIndex++) {
if (boundaryMasks[columnIndex].isEmpty() || !columnMasks[columnIndex].isEmpty()) {
continue;
}
processBoundaryColumnFromMask(output, boundaryMasks[columnIndex], walls, columnIndex, x, z, resolverState, caveBiomeCache, customBiomeCache);
processBoundaryColumnFromMask(
output,
mantleChunk,
boundaryMasks[columnIndex],
walls,
columnIndex,
x,
z,
resolverState,
caveBiomeCache,
customBiomeCache
);
}
// Surface-break carving must not leave an ore cap suspended across the opening.
@@ -252,6 +261,141 @@ public class IrisCarveModifier extends EngineAssignedModifier<PlatformBlockState
return cavern.getLiquid() == 3 ? air : null;
}
static MatterCavern composeCavern(MatterCavern baseline, RiverCaveHydrology hydrology) {
return hydrology == null ? baseline : hydrology.asCavern();
}
static PlatformBlockState resolveHydrologyState(
RiverCaveHydrology hydrology,
PlatformBlockState current,
PlatformBlockState fluid,
PlatformBlockState air
) {
if (hydrology == null) {
return null;
}
return switch (hydrology.action()) {
case WET_SOURCE -> fluid;
case FALLING_WATER -> fallingFluidState(fluid);
case DRY_AIR -> air;
case SEAL_GUARD -> normalizeWaterlogging(current, null);
};
}
static PlatformBlockState normalizeWaterlogging(PlatformBlockState state, PlatformBlockState resultingFluid) {
if (state == null || B.isFluid(state) || !IrisProceduralBlocks.hasProperty(state, "waterlogged")) {
return state;
}
String target = resultingFluid != null && resultingFluid.isWater() ? "true" : "false";
if (target.equals(IrisProceduralBlocks.propertyValue(state, "waterlogged"))) {
return state;
}
return state.withProperty("waterlogged", target);
}
static PlatformBlockState normalizeHydrologyWaterlogging(
PlatformBlockState state,
MatterCavern baseline,
RiverCaveHydrology hydrology,
PlatformBlockState columnFluid
) {
if (hydrology == null) {
return state;
}
MatterCavern composed = composeCavern(baseline, hydrology);
PlatformBlockState resultingFluid = isFluidIntent(composed) ? columnFluid : null;
return normalizeWaterlogging(state, resultingFluid);
}
private static PlatformBlockState fallingFluidState(PlatformBlockState fluid) {
if (fluid == null || !IrisProceduralBlocks.hasProperty(fluid, "level")) {
return fluid;
}
if ("8".equals(IrisProceduralBlocks.propertyValue(fluid, "level"))) {
return fluid;
}
return fluid.withProperty("level", "8");
}
private final class CarveResolver {
private final CarveResolutionContext context;
private CarveResolver(CarveResolutionContext context) {
this.context = context;
}
private void apply(
int x,
int y,
int z,
MatterCavern baseline,
RiverCaveHydrology hydrology
) {
if (y >= context.worldHeightSpan() || y <= 0) {
return;
}
int localX = x & 15;
int localZ = z & 15;
int columnIndex = PowerOfTwoCoordinates.packLocal16(localX, localZ);
if (context.upperSurfaceHeights() != null && y >= context.upperSurfaceHeights()[columnIndex]) {
return;
}
PlatformBlockState current = context.output().getRaw(localX, y, localZ);
if (hydrology != null && hydrology.action() == RiverCaveAction.SEAL_GUARD) {
PlatformBlockState normalized = resolveHydrologyState(hydrology, current, null, AIR);
if (normalized != current) {
context.output().setRaw(localX, y, localZ, normalized);
}
return;
}
MatterCavern cavern = composeCavern(baseline, hydrology);
if (cavern == null || shouldPreserveExistingFluid(cavern, current)) {
return;
}
context.columnMasks()[columnIndex].add(y);
if (!cavern.getCustomBiome().isEmpty()) {
context.scratch().customCaveBiomePresent = true;
}
boolean explicitCarveIntent = hasExplicitCarveIntent(cavern);
if (current.isAir() && !explicitCarveIntent) {
return;
}
PlatformBlockState fluid = isFluidIntent(cavern)
? context.chunkContext().getFluid().get(localX, localZ)
: null;
if (hydrology != null) {
context.output().setRaw(localX, y, localZ,
resolveHydrologyState(hydrology, current, fluid, AIR));
return;
}
if (explicitCarveIntent) {
context.output().setRaw(localX, y, localZ,
resolveExplicitCarveState(cavern, fluid, LAVA, AIR));
} else if (usesDefaultLava(context.caveLavaHeight(), y)) {
context.output().setRaw(localX, y, localZ, LAVA);
} else {
context.output().setRaw(localX, y, localZ, AIR);
}
}
}
private record CarveResolutionContext(
Hunk<PlatformBlockState> output,
ChunkContext chunkContext,
IrisCarveScratch scratch,
CarveColumnMask[] columnMasks,
int[] upperSurfaceHeights,
int worldHeightSpan,
int caveLavaHeight
) {
}
private void addInternalWallsFromMasks(CarveWallBuffer walls, CarveColumnMask[] columnMasks) {
for (int columnIndex = 0; columnIndex < 256; columnIndex++) {
CarveColumnMask columnMask = columnMasks[columnIndex];
@@ -291,18 +435,18 @@ public class IrisCarveModifier extends EngineAssignedModifier<PlatformBlockState
int rz = columnIndex & 15;
int yy = columnMask.nextSetBit(0);
while (yy >= 0) {
MatterCavern cavern = mc.get(rx, yy, rz, MatterCavern.class);
MatterCavern cavern = composedCavernAt(mc, rx, yy, rz);
if (cavern != null) {
if (rz < 15 && mc.get(rx, yy, rz + 1, MatterCavern.class) == null) {
if (rz < 15 && composedCavernAt(mc, rx, yy, rz + 1) == null) {
walls.put(rx, yy, rz + 1, cavern);
}
if (rx < 15 && mc.get(rx + 1, yy, rz, MatterCavern.class) == null) {
if (rx < 15 && composedCavernAt(mc, rx + 1, yy, rz) == null) {
walls.put(rx + 1, yy, rz, cavern);
}
if (rz > 0 && mc.get(rx, yy, rz - 1, MatterCavern.class) == null) {
if (rz > 0 && composedCavernAt(mc, rx, yy, rz - 1) == null) {
walls.put(rx, yy, rz - 1, cavern);
}
if (rx > 0 && mc.get(rx - 1, yy, rz, MatterCavern.class) == null) {
if (rx > 0 && composedCavernAt(mc, rx - 1, yy, rz) == null) {
walls.put(rx - 1, yy, rz, cavern);
}
}
@@ -370,11 +514,11 @@ public class IrisCarveModifier extends EngineAssignedModifier<PlatformBlockState
int neighborX,
int neighborZ
) {
if (mc.get(localX, yy, localZ, MatterCavern.class) != null) {
if (composedCavernAt(mc, localX, yy, localZ) != null) {
return;
}
MatterCavern neighbor = neighborChunk.get(neighborX, yy, neighborZ, MatterCavern.class);
MatterCavern neighbor = composedCavernAt(neighborChunk, neighborX, yy, neighborZ);
if (neighbor == null) {
return;
}
@@ -392,6 +536,24 @@ public class IrisCarveModifier extends EngineAssignedModifier<PlatformBlockState
return plate.get(chunkX & 31, chunkZ & 31);
}
private MatterCavern composedCavernAt(MantleChunk<Matter> mantleChunk, int x, int y, int z) {
MatterCavern baseline = dataIfPresent(mantleChunk, x, y, z, MatterCavern.class);
RiverCaveHydrology hydrology = dataIfPresent(mantleChunk, x, y, z, RiverCaveHydrology.class);
return composeCavern(baseline, hydrology);
}
private static <T> T dataIfPresent(MantleChunk<Matter> mantleChunk, int x, int y, int z, Class<T> type) {
int section = y >> 4;
if (y < 0 || !mantleChunk.exists(section)) {
return null;
}
Matter matter = mantleChunk.get(section);
if (matter == null || !matter.hasSlice(type)) {
return null;
}
return matter.<T>getSlice(type).get(x & 15, y & 15, z & 15);
}
private void processColumnFromMask(
Hunk<PlatformBlockState> output,
MantleChunk<Matter> mc,
@@ -402,7 +564,8 @@ public class IrisCarveModifier extends EngineAssignedModifier<PlatformBlockState
int chunkZ,
IrisDimensionCarvingResolver.State resolverState,
Long2ObjectOpenHashMap<IrisBiome> caveBiomeCache,
Map<String, IrisBiome> customBiomeCache
Map<String, IrisBiome> customBiomeCache,
PlatformBlockState columnFluid
) {
if (columnMask == null || columnMask.isEmpty()) {
return;
@@ -429,7 +592,8 @@ public class IrisCarveModifier extends EngineAssignedModifier<PlatformBlockState
zone.ceiling = buf;
} else {
if (zone.isValid(getEngine())) {
processZone(output, mc, mantle, zone, rx, rz, worldX, worldZ, resolverState, caveBiomeCache, customBiomeCache);
processZone(output, mc, mantle, zone, rx, rz, worldX, worldZ, resolverState,
caveBiomeCache, customBiomeCache, columnFluid);
}
zone = new CaveZone();
zone.setFloor(y);
@@ -441,12 +605,14 @@ public class IrisCarveModifier extends EngineAssignedModifier<PlatformBlockState
}
if (zone.isValid(getEngine())) {
processZone(output, mc, mantle, zone, rx, rz, worldX, worldZ, resolverState, caveBiomeCache, customBiomeCache);
processZone(output, mc, mantle, zone, rx, rz, worldX, worldZ, resolverState,
caveBiomeCache, customBiomeCache, columnFluid);
}
}
private void processBoundaryColumnFromMask(
Hunk<PlatformBlockState> output,
MantleChunk<Matter> mantleChunk,
CarveColumnMask boundaryMask,
CarveWallBuffer walls,
int columnIndex,
@@ -473,18 +639,21 @@ public class IrisCarveModifier extends EngineAssignedModifier<PlatformBlockState
if (y == zoneCeiling + 1) {
zoneCeiling = y;
} else {
paintBoundaryZone(output, walls, rx, rz, worldX, worldZ, zoneFloor, zoneCeiling, resolverState, caveBiomeCache, customBiomeCache);
paintBoundaryZone(output, mantleChunk, walls, rx, rz, worldX, worldZ, zoneFloor, zoneCeiling,
resolverState, caveBiomeCache, customBiomeCache);
zoneFloor = y;
zoneCeiling = y;
}
y = boundaryMask.nextSetBit(y + 1);
}
paintBoundaryZone(output, walls, rx, rz, worldX, worldZ, zoneFloor, zoneCeiling, resolverState, caveBiomeCache, customBiomeCache);
paintBoundaryZone(output, mantleChunk, walls, rx, rz, worldX, worldZ, zoneFloor, zoneCeiling,
resolverState, caveBiomeCache, customBiomeCache);
}
private void paintBoundaryZone(
Hunk<PlatformBlockState> output,
MantleChunk<Matter> mantleChunk,
CarveWallBuffer walls,
int rx,
int rz,
@@ -517,6 +686,11 @@ public class IrisCarveModifier extends EngineAssignedModifier<PlatformBlockState
if (floorY < 0) {
break;
}
RiverCaveHydrology hydrology = dataIfPresent(
mantleChunk, rx, floorY, rz, RiverCaveHydrology.class);
if (hydrology != null && hydrology.protectsPlacement()) {
continue;
}
PlatformBlockState existing = output.getRaw(rx, floorY, rz);
PlatformBlockState layer = floorLayers.get(i);
if (!B.isSolid(existing) || !canReplaceCaveFloorLayer(output, rx, floorY, rz, layer)) {
@@ -539,6 +713,11 @@ public class IrisCarveModifier extends EngineAssignedModifier<PlatformBlockState
if (ceilingY >= worldMaxY) {
break;
}
RiverCaveHydrology hydrology = dataIfPresent(
mantleChunk, rx, ceilingY, rz, RiverCaveHydrology.class);
if (hydrology != null && hydrology.protectsPlacement()) {
continue;
}
PlatformBlockState existing = output.getRaw(rx, ceilingY, rz);
if (!B.isSolid(existing)) {
continue;
@@ -565,7 +744,12 @@ public class IrisCarveModifier extends EngineAssignedModifier<PlatformBlockState
return (h & 15L) == 0L;
}
private void processZone(Hunk<PlatformBlockState> output, MantleChunk<Matter> mc, Mantle<Matter> mantle, CaveZone zone, int rx, int rz, int xx, int zz, IrisDimensionCarvingResolver.State resolverState, Long2ObjectOpenHashMap<IrisBiome> caveBiomeCache, Map<String, IrisBiome> customBiomeCache) {
private void processZone(Hunk<PlatformBlockState> output, MantleChunk<Matter> mc, Mantle<Matter> mantle,
CaveZone zone, int rx, int rz, int xx, int zz,
IrisDimensionCarvingResolver.State resolverState,
Long2ObjectOpenHashMap<IrisBiome> caveBiomeCache,
Map<String, IrisBiome> customBiomeCache,
PlatformBlockState columnFluid) {
int maxY = output.getHeight();
if (zone.ceiling + 1 < maxY && B.isDecorant(output.getRaw(rx, zone.ceiling + 1, rz))) {
@@ -590,6 +774,7 @@ public class IrisCarveModifier extends EngineAssignedModifier<PlatformBlockState
IrisBiome floorBiome = resolveCaveBoundaryBiome(mc, rx, zone.floor, rz, xx, zz, resolverState, caveBiomeCache, customBiomeCache);
IrisBiome ceilingBiome = resolveCaveBoundaryBiome(mc, rx, zone.ceiling, rz, xx, zz, resolverState, caveBiomeCache, customBiomeCache);
if (floorBiome == null && ceilingBiome == null) {
normalizeCaveZoneWaterlogging(output, mc, zone, rx, rz, columnFluid);
return;
}
@@ -600,6 +785,10 @@ public class IrisCarveModifier extends EngineAssignedModifier<PlatformBlockState
break;
}
int y = zone.floor - i - 1;
RiverCaveHydrology hydrology = dataIfPresent(mc, rx, y, rz, RiverCaveHydrology.class);
if (hydrology != null && hydrology.protectsPlacement()) {
continue;
}
PlatformBlockState block = floorBlocks.get(i);
PlatformBlockState existing = output.getRaw(rx, y, rz);
if (!B.isSolid(existing) || !canReplaceCaveFloorLayer(output, rx, y, rz, block)) {
@@ -620,6 +809,10 @@ public class IrisCarveModifier extends EngineAssignedModifier<PlatformBlockState
if (cy >= maxY) {
break;
}
RiverCaveHydrology hydrology = dataIfPresent(mc, rx, cy, rz, RiverCaveHydrology.class);
if (hydrology != null && hydrology.protectsPlacement()) {
continue;
}
PlatformBlockState block = ceilingBlocks.get(i);
PlatformBlockState existing = output.getRaw(rx, cy, rz);
if (!B.isSolid(existing)) {
@@ -646,10 +839,43 @@ public class IrisCarveModifier extends EngineAssignedModifier<PlatformBlockState
if (ceilingDecorators.length > 0 && zone.getCeiling() + 1 < maxY && B.isSolid(output.getRaw(rx, zone.getCeiling() + 1, rz))) {
decorant.getCeilingDecorator().decorate(rx, rz, xx, xx, xx, zz, zz, zz, output, ceilingBiome, InferredType.CAVE, zone.getCeiling(), zone.airThickness());
}
normalizeCaveZoneWaterlogging(output, mc, zone, rx, rz, columnFluid);
}
private void normalizeCaveZoneWaterlogging(
Hunk<PlatformBlockState> output,
MantleChunk<Matter> mantleChunk,
CaveZone zone,
int localX,
int localZ,
PlatformBlockState columnFluid
) {
int minimumY = Math.max(0, zone.floor - 1);
int maximumY = Math.min(output.getHeight() - 1, zone.ceiling + 1);
for (int y = minimumY; y <= maximumY; y++) {
RiverCaveHydrology hydrology = dataIfPresent(
mantleChunk, localX, y, localZ, RiverCaveHydrology.class);
if (hydrology == null) {
continue;
}
MatterCavern baseline = dataIfPresent(
mantleChunk, localX, y, localZ, MatterCavern.class);
PlatformBlockState current = output.getRaw(localX, y, localZ);
PlatformBlockState normalized = normalizeHydrologyWaterlogging(
current,
baseline,
hydrology,
columnFluid
);
if (normalized != current) {
output.setRaw(localX, y, localZ, normalized);
}
}
}
IrisBiome resolveCaveBoundaryBiome(MantleChunk<Matter> mantleChunk, int x, int y, int z, int worldX, int worldZ, IrisDimensionCarvingResolver.State resolverState, Long2ObjectOpenHashMap<IrisBiome> caveBiomeCache, Map<String, IrisBiome> customBiomeCache) {
MatterCavern cavern = mantleChunk.get(x, y, z, MatterCavern.class);
MatterCavern cavern = composedCavernAt(mantleChunk, x, y, z);
return resolveCaveBoundaryBiome(
cavern, worldX, y, worldZ, resolverState, caveBiomeCache, customBiomeCache);
}
@@ -57,10 +57,9 @@ public class IrisPostModifier extends EngineAssignedModifier<PlatformBlockState>
IrisDimension dimension = getDimension();
boolean walls = dimension.isPostProcessingWalls();
boolean slabs = dimension.isPostProcessingSlabs();
int fluidHeight = dimension.getFluidHeight();
for (int i = 0; i < width; i++) {
for (int j = 0; j < depth; j++) {
post(i, j, sync, i + x, j + z, context, heights, planeWidth, walls, slabs, fluidHeight);
post(i, j, sync, i + x, j + z, context, heights, planeWidth, walls, slabs);
}
}
@@ -90,7 +89,7 @@ public class IrisPostModifier extends EngineAssignedModifier<PlatformBlockState>
return heights;
}
private void post(int currentPostX, int currentPostZ, Hunk<PlatformBlockState> currentData, int x, int z, ChunkContext context, int[] heights, int planeWidth, boolean walls, boolean slabs, int fluidHeight) {
private void post(int currentPostX, int currentPostZ, Hunk<PlatformBlockState> currentData, int x, int z, ChunkContext context, int[] heights, int planeWidth, boolean walls, boolean slabs) {
// x/z are world coordinates, the hunk is indexed relative to this chunk origin.
int originX = x - currentPostX;
int originZ = z - currentPostZ;
@@ -100,6 +99,7 @@ public class IrisPostModifier extends EngineAssignedModifier<PlatformBlockState>
int hb = heights[center + planeWidth];
int hc = heights[center - 1];
int hd = heights[center - planeWidth];
int fluidHeight = (int) Math.round(getComplex().getRiverWaterSurfaceStream().get(x, z));
// Floating Nibs
int g = 0;
@@ -50,6 +50,17 @@ public interface IObjectPlacer {
int getFluidHeight();
default int getFluidHeight(int x, int z) {
Engine engine = getEngine();
if (engine == null || engine.getComplex() == null) {
return getFluidHeight();
}
int coordinateShift = getFluidHeight() - engine.getDimension().getFluidHeight();
return coordinateShift + (int) Math.round(
engine.getComplex().getRiverWaterSurfaceStream().get(x, z)
);
}
boolean isDebugSmartBore();
void setTile(int xx, int yy, int zz, TileData tile);
@@ -130,6 +130,8 @@ public class IrisBiome extends IrisRegistrant implements IRare {
private int lockLayersMax = 7;
@Desc("Profile-driven 3D cave configuration")
private IrisCaveProfile caveProfile = new IrisCaveProfile();
@Desc("Biome-level river routing, shape, cave-entry, and biome-pool overrides. Omit to inherit region and dimension settings.")
private IrisRiverOverride riverOverride = null;
@MinNumber(1)
@MaxNumber(512)
@Desc("The rarity of this biome (integer)")
@@ -36,7 +36,7 @@ final class IrisBiomeColorRenderer {
static Color getColor(IrisBiome biome, Engine engine, RenderType type) {
switch (type) {
case BIOME, HEIGHT, CAVE_LAND, REGION, BIOME_SEA, BIOME_LAND -> {
case BIOME, HEIGHT, CAVE_LAND, REGION, BIOME_SEA, BIOME_LAND, RIVER -> {
return biome.getCacheColor().aquire(() -> {
if (biome.getColor() == null) {
RandomColor randomColor = new RandomColor(biome.getName().hashCode());
@@ -164,6 +164,8 @@ public class IrisDimension extends IrisRegistrant {
private KList<IrisDimensionCarvingEntry> carving = new KList<>();
@Desc("Profile-driven 3D cave configuration")
private IrisCaveProfile caveProfile = new IrisCaveProfile();
@Desc("Connected surface rivers and contained river cave-water generation.")
private IrisRiverNetwork rivers = new IrisRiverNetwork();
@Desc("Refuse to place surface objects and trees over carved surface openings.")
private boolean requireObjectSurfaceSupport = true;
@MinNumber(0)
@@ -489,6 +491,11 @@ public class IrisDimension extends IrisRegistrant {
}
Deque<String> pending = new ArrayDeque<>();
IrisRiverNetwork riverNetwork = getRivers();
boolean riversEnabled = riverNetwork != null && riverNetwork.isEnabled();
if (riversEnabled && riverNetwork.getBiomes() != null) {
addReachableBiomeKeys(pending, riverNetwork.getBiomes().getAllBiomeIds());
}
KList<String> regionKeys = getRegions();
if (regionKeys != null) {
for (String regionKey : regionKeys) {
@@ -496,7 +503,8 @@ public class IrisDimension extends IrisRegistrant {
if (region == null) {
continue;
}
addReachableBiomeKeys(pending, region.getAllBiomeIds());
addReachableBiomeKeys(pending,
riversEnabled ? region.getAllBiomeIds() : region.getNaturalBiomeIds());
}
}
@@ -531,6 +539,9 @@ public class IrisDimension extends IrisRegistrant {
biomes.put(loadKey, biome);
addReachableBiomeKeys(pending, biome.getChildren());
addReachableBiomeKey(pending, biome.getCarvingBiome());
if (riversEnabled && biome.getRiverOverride() != null) {
addReachableBiomeKeys(pending, biome.getRiverOverride().getAllBiomeIds());
}
KList<IrisFloatingChildBiomes> floatingChildren = biome.getFloatingChildBiomes();
if (floatingChildren == null) {
@@ -28,6 +28,9 @@ public enum IrisEngineStreamType {
@Desc("Represents the given slope at the x, z coordinates")
SLOPE((f) -> f.getComplex().getSlopeStream()),
@Desc("Represents terrain height before river incision and river biome replacement.")
NATURAL_HEIGHT((f) -> f.getComplex().getNaturalHeightStream()),
@Desc("Represents the base generator height at the given position. This includes only the biome generators / interpolation and noise features but does not include carving, caves.")
HEIGHT((f) -> f.getComplex().getHeightStream()),
@@ -41,7 +44,19 @@ public enum IrisEngineStreamType {
REGION_STYLE((f) -> f.getComplex().getRegionStyleStream()),
@Desc("Represents the identity of regions. Each region has a unique number (very large numbers)")
REGION_IDENTITY((f) -> f.getComplex().getRegionIdentityStream());
REGION_IDENTITY((f) -> f.getComplex().getRegionIdentityStream()),
@Desc("Represents block distance from the nearest active river centerline.")
RIVER_DISTANCE((f) -> f.getComplex().getRiverDistanceStream()),
@Desc("Represents the merged upstream flow carried by the active river reach.")
RIVER_FLOW((f) -> f.getComplex().getRiverFlowStream()),
@Desc("Represents the normalized river terrain-incision weight.")
RIVER_CARVE_WEIGHT((f) -> f.getComplex().getRiverCarveWeightStream()),
@Desc("Represents the solved river water-surface height.")
RIVER_WATER_SURFACE((f) -> f.getComplex().getRiverWaterSurfaceStream());
private final Function<Engine, ProceduralStream<Double>> getter;
@@ -332,7 +332,8 @@ final class IrisObjectPlacementRunner {
return -1;
}
if (!config.isForcePlace() && !rawStructurePiece && config.isUnderwater() && y + rty + ty >= placer.getFluidHeight()) {
if (!config.isForcePlace() && !rawStructurePiece && config.isUnderwater()
&& y + rty + ty >= placer.getFluidHeight(x, z)) {
return -1;
}
@@ -118,6 +118,8 @@ public class IrisRegion extends IrisRegistrant implements IRare {
private double caveBiomeZoom = 1;
@Desc("Profile-driven 3D cave configuration")
private IrisCaveProfile caveProfile = new IrisCaveProfile();
@Desc("Region-level river routing, shape, cave-entry, and biome-pool overrides. Omit to inherit dimension settings.")
private IrisRiverOverride riverOverride = null;
@RegistryListResource(IrisBiome.class)
@Required
@ArrayType(min = 1, type = String.class)
@@ -277,18 +279,33 @@ public class IrisRegion extends IrisRegistrant implements IRare {
}
public KSet<String> getAllBiomeIds() {
KSet<String> names = getNaturalBiomeIds();
if (riverOverride != null) {
names.addAll(riverOverride.getAllBiomeIds());
}
return names;
}
public KSet<String> getNaturalBiomeIds() {
KSet<String> names = new KSet<>();
names.addAll(landBiomes);
names.addAll(caveBiomes);
names.addAll(seaBiomes);
names.addAll(shoreBiomes);
return names;
}
public KList<IrisBiome> getAllBiomes(DataProvider g) {
return resolveBiomes(g, getAllBiomeIds());
}
public KList<IrisBiome> getNaturalBiomes(DataProvider g) {
return resolveBiomes(g, getNaturalBiomeIds());
}
private KList<IrisBiome> resolveBiomes(DataProvider g, KSet<String> biomeIds) {
KMap<String, IrisBiome> b = new KMap<>();
KSet<String> names = getAllBiomeIds();
KSet<String> names = biomeIds.copy();
while (!names.isEmpty()) {
for (String i : new KList<>(names)) {
@@ -0,0 +1,61 @@
package art.arcane.iris.engine.object;
import art.arcane.iris.engine.object.annotations.ArrayType;
import art.arcane.iris.engine.object.annotations.Desc;
import art.arcane.iris.engine.object.annotations.RegistryListResource;
import art.arcane.volmlib.util.collection.KList;
import art.arcane.volmlib.util.collection.KSet;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.experimental.Accessors;
@Accessors(chain = true)
@NoArgsConstructor
@Desc("Biome pools used by dimension-level river sections and contained river caves.")
@Data
public class IrisRiverBiomes {
@Desc("Noise used to select a biome inside the active river-section pool.")
private IrisGeneratorStyle selectionStyle = new IrisGeneratorStyle(NoiseStyle.CELLULAR_IRIS_DOUBLE)
.zoomed(512D);
@RegistryListResource(IrisBiome.class)
@ArrayType(type = String.class)
@Desc("Biome pool for wet river channels.")
private KList<String> channel = new KList<>();
@RegistryListResource(IrisBiome.class)
@ArrayType(type = String.class)
@Desc("Biome pool for river banks outside the wet channel.")
private KList<String> bank = new KList<>();
@RegistryListResource(IrisBiome.class)
@ArrayType(type = String.class)
@Desc("Biome pool for river reaches meeting natural sea.")
private KList<String> mouth = new KList<>();
@RegistryListResource(IrisBiome.class)
@ArrayType(type = String.class)
@Desc("Biome pool for dry river channels and terminal tapers.")
private KList<String> dry = new KList<>();
@RegistryListResource(IrisBiome.class)
@ArrayType(type = String.class)
@Desc("Cave biome pool for accepted contained river cave bodies.")
private KList<String> floodedCave = new KList<>();
public KSet<String> getAllBiomeIds() {
KSet<String> biomeIds = new KSet<>();
addAll(biomeIds, channel);
addAll(biomeIds, bank);
addAll(biomeIds, mouth);
addAll(biomeIds, dry);
addAll(biomeIds, floodedCave);
return biomeIds;
}
private static void addAll(KSet<String> destination, KList<String> source) {
if (source != null) {
destination.addAll(source);
}
}
}
@@ -0,0 +1,12 @@
package art.arcane.iris.engine.object;
import art.arcane.iris.engine.object.annotations.Desc;
@Desc("Selects the fallback when a requested river cave connection cannot be proven safe.")
public enum IrisRiverCaveFallback {
@Desc("Keep the rejected connection sealed.")
SEALED,
@Desc("Try a bounded generated grotto instead of the rejected existing cave.")
GENERATE_GROTTO
}
@@ -0,0 +1,21 @@
package art.arcane.iris.engine.object;
import art.arcane.iris.engine.object.annotations.Desc;
@Desc("Selects the contained cave-water behavior available to river entry events.")
public enum IrisRiverCaveMode {
@Desc("Keep the surface reservoir sealed from caves.")
SEALED,
@Desc("Flood only an existing cave component whose complete fluid-reachable boundary is proven closed.")
FLOOD_CLOSED_COMPONENT,
@Desc("Generate a bounded grotto with a guaranteed solid shell.")
GENERATE_GROTTO,
@Desc("Use a proven closed cave component when available, otherwise generate a bounded grotto.")
GROTTO_OR_CLOSED_COMPONENT,
@Desc("Generate a controlled falling column into a proven contained pool.")
WATERFALL_POOL
}
@@ -0,0 +1,95 @@
package art.arcane.iris.engine.object;
import art.arcane.iris.engine.object.annotations.Desc;
import art.arcane.iris.engine.object.annotations.MaxNumber;
import art.arcane.iris.engine.object.annotations.MinNumber;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.experimental.Accessors;
@Accessors(chain = true)
@NoArgsConstructor
@Desc("Controls bounded and transactionally validated river-to-cave connections.")
@Data
public class IrisRiverCaves {
@Desc("The contained cave-water behavior available to river entry events.")
private IrisRiverCaveMode mode = IrisRiverCaveMode.SEALED;
@Desc("Selects cave-entry stations at stable river-reach anchors.")
private IrisRiverNoiseChance entry = new IrisRiverNoiseChance()
.setChance(0.12D)
.setStyle(new IrisGeneratorStyle(NoiseStyle.IRIS).zoomed(1024D))
.setInfluence(0.4D);
@MinNumber(16)
@MaxNumber(4096)
@Desc("The minimum distance in blocks between cave-entry candidates.")
private int minimumSpacing = 128;
@MinNumber(0)
@MaxNumber(16)
@Desc("The maximum entry-noise-eligible cave anchors accepted on one ordinary reach. A forced sinkhole terminal uses its reach exclusively and requires this value above zero.")
private int maximumPerReach = 1;
@MinNumber(1)
@MaxNumber(256)
@Desc("The maximum vertical distance searched while boring from river bed to cave.")
private int maxBoreDepth = 48;
@MinNumber(1)
@MaxNumber(16)
@Desc("The radius in blocks of a generated river-to-cave throat.")
private int throatRadius = 2;
@MinNumber(-64)
@MaxNumber(64)
@Desc("The offset applied to river water height when filling an accepted cave body.")
private int waterLevelOffset = 0;
@MinNumber(0)
@MaxNumber(64)
@Desc("The minimum dry headroom retained above water in generated grottos.")
private int dryHeadroom = 4;
@MinNumber(2)
@MaxNumber(128)
@Desc("The horizontal radius in blocks of a generated sealed grotto.")
private int grottoHorizontalRadius = 12;
@MinNumber(2)
@MaxNumber(128)
@Desc("The vertical radius in blocks of a generated sealed grotto.")
private int grottoVerticalRadius = 7;
@Desc("Noise shaping the boundary of generated sealed grottos.")
private IrisGeneratorStyle grottoShapeStyle = new IrisGeneratorStyle(NoiseStyle.IRIS).zoomed(24D);
@Desc("Noise warping the coordinate field used for generated sealed grottos.")
private IrisGeneratorStyle grottoWarpStyle = new IrisGeneratorStyle(NoiseStyle.IRIS).zoomed(48D);
@MinNumber(0)
@MaxNumber(32)
@Desc("The maximum coordinate warp applied to generated sealed grottos in blocks.")
private double grottoWarpStrength = 2D;
@MinNumber(4)
@MaxNumber(256)
@Desc("The horizontal proof radius for an existing closed cave component.")
private int maxFloodRadius = 48;
@MinNumber(4)
@MaxNumber(256)
@Desc("The vertical proof depth for an existing closed cave component.")
private int maxFloodDepth = 32;
@MinNumber(64)
@MaxNumber(1048576)
@Desc("The greatest cave-component volume that may be fully proven and flooded.")
private int maxFloodVolume = 8192;
@Desc("The behavior used when a requested cave connection cannot be proven safe.")
private IrisRiverCaveFallback fallback = IrisRiverCaveFallback.SEALED;
@Desc("The policy for fluid already present in a candidate contained cave body.")
private IrisRiverExistingFluidPolicy existingFluidPolicy = IrisRiverExistingFluidPolicy.REJECT;
}
@@ -0,0 +1,15 @@
package art.arcane.iris.engine.object;
import art.arcane.iris.engine.object.annotations.Desc;
@Desc("Controls how river cave hydrology treats fluid already present in a candidate cave body.")
public enum IrisRiverExistingFluidPolicy {
@Desc("Reject a candidate containing any existing fluid.")
REJECT,
@Desc("Accept only fluid compatible with the river fluid palette.")
ALLOW_SAME,
@Desc("Replace contained existing fluid with the river fluid palette.")
REPLACE
}
@@ -0,0 +1,30 @@
package art.arcane.iris.engine.object;
import art.arcane.iris.engine.object.annotations.Desc;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.experimental.Accessors;
@Accessors(chain = true)
@NoArgsConstructor
@Desc("Dimension-owned configuration for connected surface rivers and contained river cave water.")
@Data
public class IrisRiverNetwork {
@Desc("Enable the river network for this dimension.")
private boolean enabled = false;
@Desc("Dimension-owned connected graph and source-selection settings.")
private IrisRiverTopology topology = new IrisRiverTopology();
@Desc("Channel geometry, terrain incision, meanders, and terminal behavior.")
private IrisRiverTerrain terrain = new IrisRiverTerrain();
@Desc("River water-surface settings.")
private IrisRiverWater water = new IrisRiverWater();
@Desc("Dimension-level biome pools for river sections and contained river caves.")
private IrisRiverBiomes biomes = new IrisRiverBiomes();
@Desc("Bounded river-to-cave connection settings.")
private IrisRiverCaves caves = new IrisRiverCaves();
}
@@ -0,0 +1,27 @@
package art.arcane.iris.engine.object;
import art.arcane.iris.engine.object.annotations.Desc;
import art.arcane.iris.engine.object.annotations.MaxNumber;
import art.arcane.iris.engine.object.annotations.MinNumber;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.experimental.Accessors;
@Accessors(chain = true)
@NoArgsConstructor
@Desc("A deterministic graph-event chance modulated by configurable noise.")
@Data
public class IrisRiverNoiseChance {
@MinNumber(0)
@MaxNumber(1)
@Desc("The base probability before noise modulation.")
private double chance = 1D;
@Desc("The noise sampled once at the stable graph-event anchor.")
private IrisGeneratorStyle style = new IrisGeneratorStyle(NoiseStyle.FLAT);
@MinNumber(0)
@MaxNumber(1)
@Desc("The maximum centered noise contribution added to the base probability.")
private double influence = 0D;
}
@@ -0,0 +1,103 @@
package art.arcane.iris.engine.object;
import art.arcane.iris.engine.object.annotations.ArrayType;
import art.arcane.iris.engine.object.annotations.Desc;
import art.arcane.iris.engine.object.annotations.MaxNumber;
import art.arcane.iris.engine.object.annotations.MinNumber;
import art.arcane.iris.engine.object.annotations.RegistryListResource;
import art.arcane.volmlib.util.collection.KList;
import art.arcane.volmlib.util.collection.KSet;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.experimental.Accessors;
@Accessors(chain = true)
@NoArgsConstructor
@Desc("Nullable river settings overridden by a region or natural biome without changing graph identity.")
@Data
public class IrisRiverOverride {
@Desc("Whether new river sources may begin in this area. Existing trunks are unaffected.")
private Boolean allowSources = null;
@Desc("How downstream routing treats this area.")
private IrisRiverRoutingPolicy routingPolicy = null;
@MinNumber(0)
@MaxNumber(64)
@Desc("Multiplier applied to downstream routing cost.")
private Double routingCostMultiplier = null;
@MinNumber(0.0001)
@MaxNumber(16)
@Desc("Multiplier applied to wet channel width.")
private Double widthMultiplier = null;
@MinNumber(0)
@MaxNumber(16)
@Desc("Multiplier applied to river bank width.")
private Double bankWidthMultiplier = null;
@MinNumber(0.0001)
@MaxNumber(16)
@Desc("Multiplier applied to river-bed depth.")
private Double depthMultiplier = null;
@MinNumber(0)
@MaxNumber(16)
@Desc("Multiplier applied to maximum terrain incision.")
private Double maxIncisionMultiplier = null;
@MinNumber(0)
@MaxNumber(16)
@Desc("Multiplier applied to reach continuation probability.")
private Double continuationChanceMultiplier = null;
@MinNumber(0)
@MaxNumber(16)
@Desc("Multiplier applied to cave-entry probability.")
private Double caveEntryMultiplier = null;
@Desc("Optional terminal behavior override for failed routes in this area.")
private IrisRiverTerminalMode terminalMode = null;
@RegistryListResource(IrisBiome.class)
@ArrayType(type = String.class)
@Desc("Optional replacement biome pool for wet river channels. Empty explicitly disables this pool.")
private KList<String> channelBiomes = null;
@RegistryListResource(IrisBiome.class)
@ArrayType(type = String.class)
@Desc("Optional replacement biome pool for river banks. Empty explicitly disables this pool.")
private KList<String> bankBiomes = null;
@RegistryListResource(IrisBiome.class)
@ArrayType(type = String.class)
@Desc("Optional replacement biome pool for river mouths. Empty explicitly disables this pool.")
private KList<String> mouthBiomes = null;
@RegistryListResource(IrisBiome.class)
@ArrayType(type = String.class)
@Desc("Optional replacement biome pool for dry river channels. Empty explicitly disables this pool.")
private KList<String> dryBiomes = null;
@RegistryListResource(IrisBiome.class)
@ArrayType(type = String.class)
@Desc("Optional replacement cave biome pool for accepted contained river cave bodies. Empty explicitly disables this pool.")
private KList<String> floodedCaveBiomes = null;
public KSet<String> getAllBiomeIds() {
KSet<String> biomeIds = new KSet<>();
addAll(biomeIds, channelBiomes);
addAll(biomeIds, bankBiomes);
addAll(biomeIds, mouthBiomes);
addAll(biomeIds, dryBiomes);
addAll(biomeIds, floodedCaveBiomes);
return biomeIds;
}
private static void addAll(KSet<String> destination, KList<String> source) {
if (source != null) {
destination.addAll(source);
}
}
}
@@ -0,0 +1,15 @@
package art.arcane.iris.engine.object;
import art.arcane.iris.engine.object.annotations.Desc;
@Desc("Controls how river routing treats a region or biome.")
public enum IrisRiverRoutingPolicy {
@Desc("Allow normal river routing through this area.")
ALLOW,
@Desc("Increase the routing cost while still permitting established river trunks.")
AVOID,
@Desc("Forbid river reaches from crossing this area.")
BLOCK
}
@@ -0,0 +1,15 @@
package art.arcane.iris.engine.object;
import art.arcane.iris.engine.object.annotations.Desc;
@Desc("Selects how a river route ends when it cannot continue to an outlet.")
public enum IrisRiverTerminalMode {
@Desc("Suppress the failed route instead of generating it.")
SUPPRESS,
@Desc("Continue as a dry channel that tapers back into natural terrain.")
DRY_CHANNEL,
@Desc("End in a contained underground grotto when cave hydrology accepts the connection.")
SINKHOLE_GROTTO
}
@@ -0,0 +1,99 @@
package art.arcane.iris.engine.object;
import art.arcane.iris.engine.object.annotations.Desc;
import art.arcane.iris.engine.object.annotations.MaxNumber;
import art.arcane.iris.engine.object.annotations.MinNumber;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.experimental.Accessors;
@Accessors(chain = true)
@NoArgsConstructor
@Desc("Controls river channel geometry, banks, incision, meanders, and terminal tapering.")
@Data
public class IrisRiverTerrain {
@Desc("The wet channel width in blocks before stream-order scaling.")
private IrisStyledRange channelWidth = range(8D, 20D, NoiseStyle.IRIS, 1024D);
@Desc("The bank width outside the wet channel in blocks.")
private IrisStyledRange bankWidth = range(5D, 18D, NoiseStyle.IRIS, 1024D);
@Desc("The wet-bed depth below the local water surface, or dry-channel depth below natural terrain, in blocks.")
private IrisStyledRange depth = range(2D, 7D, NoiseStyle.IRIS, 768D);
@MinNumber(1)
@MaxNumber(2048)
@Desc("The final wet-channel width cap after region, biome, and stream-order scaling.")
private double maxChannelWidth = 10D;
@MinNumber(0)
@MaxNumber(2048)
@Desc("The final bank-width cap on each side after region and biome scaling.")
private double maxBankWidth = 4D;
@MinNumber(1)
@MaxNumber(512)
@Desc("The final river-depth cap after region, biome, and stream-order scaling.")
private double maxDepth = 10D;
@MinNumber(0)
@MaxNumber(8)
@Desc("Additional channel-width fraction applied for each merged upstream flow order.")
private double orderWidthFactor = 0.35D;
@MinNumber(0)
@MaxNumber(8)
@Desc("Additional river-bed depth fraction applied for each merged upstream flow order.")
private double orderDepthFactor = 0.2D;
@Desc("Selects whether a complete graph reach may incise terrain. A rejected reach follows terminal behavior.")
private IrisRiverNoiseChance incision = new IrisRiverNoiseChance();
@MinNumber(0)
@MaxNumber(512)
@Desc("The greatest permitted vertical incision below natural terrain.")
private int maxIncision = 48;
@MinNumber(0.125)
@MaxNumber(16)
@Desc("The exponent shaping the channel-to-bank cross-section transition.")
private double bankExponent = 2D;
@Desc("Modulates perpendicular spline displacement while preserving graph endpoints.")
private IrisGeneratorStyle meanderStyle = new IrisGeneratorStyle(NoiseStyle.IRIS).zoomed(512D);
@MinNumber(0)
@MaxNumber(1024)
@Desc("The maximum perpendicular meander displacement in blocks.")
private double meanderStrength = 72D;
@MinNumber(1)
@MaxNumber(64)
@Desc("The number of straight segments used to flatten each meandering graph reach.")
private int meanderSubdivisions = 8;
@Desc("Modulates small river-bed height variation after the connected channel shape is solved.")
private IrisGeneratorStyle bedRoughnessStyle = new IrisGeneratorStyle(NoiseStyle.IRIS).zoomed(96D);
@MinNumber(0)
@MaxNumber(8)
@Desc("The maximum river-bed roughness in blocks.")
private double bedRoughness = 0.75D;
@Desc("The behavior used when a graph route cannot continue as a wet channel.")
private IrisRiverTerminalMode terminalMode = IrisRiverTerminalMode.DRY_CHANNEL;
@MinNumber(8)
@MaxNumber(1024)
@Desc("The distance in blocks over which a terminal channel returns to natural terrain.")
private int terminalTaper = 64;
@MinNumber(0)
@MaxNumber(1)
@Desc("The probability that a failed wet route continues as a tapered dry channel.")
private double dryContinuationChance = 1D;
private static IrisStyledRange range(double min, double max, NoiseStyle style, double zoom) {
return new IrisStyledRange(min, max, new IrisGeneratorStyle(style).zoomed(zoom));
}
}
@@ -0,0 +1,92 @@
package art.arcane.iris.engine.object;
import art.arcane.iris.engine.object.annotations.Desc;
import art.arcane.iris.engine.object.annotations.MaxNumber;
import art.arcane.iris.engine.object.annotations.MinNumber;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.experimental.Accessors;
@Accessors(chain = true)
@NoArgsConstructor
@Desc("Dimension-owned settings for the deterministic connected river graph.")
@Data
public class IrisRiverTopology {
@MinNumber(64)
@MaxNumber(4096)
@Desc("The routing-cell width in blocks. This controls graph identity and cannot be overridden by regions or biomes.")
private int cellSize = 512;
@MinNumber(1)
@MaxNumber(64)
@Desc("The number of routing cells grouped into one immutable river cache tile.")
private int tileCells = 4;
@MinNumber(0)
@MaxNumber(0.49)
@Desc("The fraction of a routing cell used to jitter its graph node away from the center.")
private double siteJitter = 0.35D;
@MinNumber(1)
@MaxNumber(256)
@Desc("The maximum number of directed graph reaches followed by one source route.")
private int maxRouteReaches = 16;
@MinNumber(0)
@MaxNumber(64)
@Desc("The minimum number of noise-weighted source nodes selected in each routing tile while source chance is above zero.")
private int minimumSourcesPerTile = 0;
@MinNumber(0)
@MaxNumber(7)
@Desc("The number of alternate downstream reaches inspected before declaring a sink.")
private int sinkSearchReaches = 4;
@MinNumber(8)
@MaxNumber(256)
@Desc("The spacing of deterministic drainage-basin sinks in routing cells. Larger values produce longer trunks and wider tributary trees.")
private int routingBasinCells = 64;
@MinNumber(1)
@MaxNumber(64)
@Desc("The horizontal basin-distance span in routing cells per one block of terraced water rise.")
private double routingPlateauHeight = 8D;
@Desc("Selects complete river source routes at stable graph nodes.")
private IrisRiverNoiseChance source = new IrisRiverNoiseChance()
.setChance(0.05D)
.setStyle(new IrisGeneratorStyle(NoiseStyle.IRIS).zoomed(8192D))
.setInfluence(0.035D);
@Desc("Selects complete continuation reaches. A rejected reach terminates or suppresses its route rather than creating a gap.")
private IrisRiverNoiseChance continuation = new IrisRiverNoiseChance()
.setChance(0.99D)
.setStyle(new IrisGeneratorStyle(NoiseStyle.VASCULAR).zoomed(4096D))
.setInfluence(0.01D);
@Desc("Adds deterministic cost variation while choosing downstream graph neighbors.")
private IrisGeneratorStyle routingStyle = new IrisGeneratorStyle(NoiseStyle.VASCULAR).zoomed(8192D);
@MinNumber(0)
@MaxNumber(1024)
@Desc("The maximum routing-cost contribution from routingStyle.")
private double routingNoiseWeight = 24D;
@MinNumber(0)
@MaxNumber(16)
@Desc("The contribution of natural terrain height to downstream routing cost.")
private double terrainHeightWeight = 0.7D;
@MinNumber(0)
@MaxNumber(16)
@Desc("The contribution of natural terrain slope to downstream routing cost.")
private double terrainSlopeWeight = 0.35D;
@MinNumber(0)
@MaxNumber(16)
@Desc("The routing preference toward natural sea outlets.")
private double oceanAttraction = 1D;
@Desc("Require every wet source route to reach natural sea or a proven sea-reaching trunk.")
private boolean requireOcean = false;
}
@@ -0,0 +1,32 @@
package art.arcane.iris.engine.object;
import art.arcane.iris.engine.object.annotations.Desc;
import art.arcane.iris.engine.object.annotations.MaxNumber;
import art.arcane.iris.engine.object.annotations.MinNumber;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.experimental.Accessors;
@Accessors(chain = true)
@NoArgsConstructor
@Desc("Controls the river water-surface solver.")
@Data
public class IrisRiverWater {
@Desc("The strategy used to determine river water-surface height.")
private IrisRiverWaterMode mode = IrisRiverWaterMode.SEA_LEVEL;
@MinNumber(8)
@MaxNumber(4096)
@Desc("The target length of each flat terraced pool in blocks.")
private int poolLength = 96;
@MinNumber(0)
@MaxNumber(64)
@Desc("The greatest river water height permitted above the dimension fluid height.")
private int maximumPoolRise = 4;
@MinNumber(1)
@MaxNumber(32)
@Desc("The vertical height of controlled drops between terraced pools.")
private int dropHeight = 1;
}
@@ -0,0 +1,12 @@
package art.arcane.iris.engine.object;
import art.arcane.iris.engine.object.annotations.Desc;
@Desc("Selects how a river determines its surface fluid height.")
public enum IrisRiverWaterMode {
@Desc("Use the dimension fluid height for every wet river reach.")
SEA_LEVEL,
@Desc("Use flat pools connected by controlled vertical drops.")
TERRACED
}
@@ -32,6 +32,7 @@ import art.arcane.iris.core.IrisWorlds;
import art.arcane.iris.core.gui.PregeneratorJob;
import art.arcane.iris.core.loader.IrisData;
import art.arcane.iris.core.nms.INMS;
import art.arcane.iris.core.runtime.ObjectStudioActivation;
import art.arcane.iris.core.runtime.jigsaw.JigsawStudioActivation;
import art.arcane.iris.core.runtime.jigsaw.JigsawStudioSession;
import art.arcane.iris.core.service.StudioSVC;
@@ -61,6 +62,7 @@ import art.arcane.volmlib.util.collection.KList;
import art.arcane.volmlib.util.math.M;
import art.arcane.iris.util.project.hunk.Hunk;
import art.arcane.iris.util.project.hunk.view.ChunkDataHunkHolder;
import art.arcane.iris.util.project.context.ChunkContext;
import art.arcane.iris.util.project.context.IrisContext;
import art.arcane.volmlib.util.io.ReactiveFolder;
import art.arcane.volmlib.util.scheduling.ChronoLatch;
@@ -83,12 +85,17 @@ import org.jetbrains.annotations.Nullable;
import java.io.File;
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Locale;
import java.util.Objects;
import java.util.Random;
import java.util.UUID;
import java.util.concurrent.CompletionException;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.concurrent.atomic.AtomicBoolean;
@@ -102,6 +109,11 @@ import java.util.function.Supplier;
@Data
public class BukkitChunkGenerator extends ChunkGenerator implements PlatformChunkGenerator, Listener {
private static final int LOAD_LOCKS = Runtime.getRuntime().availableProcessors() * 4;
private static final int STUDIO_ENTRY_PRECOMPUTE_RADIUS = 2;
private static final int STUDIO_ENTRY_PRECOMPUTE_THREADS = Math.max(
1,
Math.min(6, Runtime.getRuntime().availableProcessors() / 2));
private static final AtomicInteger STUDIO_ENTRY_THREAD_SEQUENCE = new AtomicInteger();
private static final long HOTLOAD_LOOP_DELAY_MS = 250L;
private static final long HOTLOAD_MAINTENANCE_DELAY_MS = 4000L;
private final GenerationStageGate loadLock;
@@ -115,6 +127,7 @@ public class BukkitChunkGenerator extends ChunkGenerator implements PlatformChun
private final AtomicBoolean setup;
private final boolean studio;
private final AtomicBoolean studioEntryBootstrapActive;
private final ConcurrentHashMap<Long, PreparedStudioChunk> preparedStudioEntryChunks;
private final AtomicInteger a = new AtomicInteger(0);
private volatile long lastChunkGenTime = 0L;
private final CompletableFuture<Integer> spawnChunks = new CompletableFuture<>();
@@ -143,6 +156,7 @@ public class BukkitChunkGenerator extends ChunkGenerator implements PlatformChun
this.hotloadChecker = new ChronoLatch(1000, false);
this.studio = studio;
this.studioEntryBootstrapActive = new AtomicBoolean(studio);
this.preparedStudioEntryChunks = new ConcurrentHashMap<>();
this.dataLocation = dataLocation;
this.dimensionKey = dimensionKey;
this.folder = new ReactiveFolder(
@@ -475,6 +489,7 @@ public class BukkitChunkGenerator extends ChunkGenerator implements PlatformChun
if (currentEngine != null && !currentEngine.isClosed()) {
currentEngine.close();
}
preparedStudioEntryChunks.clear();
folder.clear();
populators.clear();
});
@@ -525,6 +540,124 @@ public class BukkitChunkGenerator extends ChunkGenerator implements PlatformChun
studioEntryBootstrapActive.set(false);
}
public CompletableFuture<Void> prepareStudioEntryChunks(
World bukkitWorld,
int centerChunkX,
int centerChunkZ
) {
if (!studio || closing) {
return CompletableFuture.completedFuture(null);
}
Engine activeEngine = getEngine(bukkitWorld);
computeStudioGenerator();
if (studioGenerator != null) {
return CompletableFuture.completedFuture(null);
}
long generationSessionId = activeEngine.getGenerationSessionId();
ConcurrentHashMap<Long, PreparedStudioChunk> prepared = new ConcurrentHashMap<>();
ExecutorService executor = createStudioEntryExecutor();
int diameter = STUDIO_ENTRY_PRECOMPUTE_RADIUS * 2 + 1;
ArrayList<CompletableFuture<Void>> tasks = new ArrayList<>(diameter * diameter);
for (int offsetX = -STUDIO_ENTRY_PRECOMPUTE_RADIUS;
offsetX <= STUDIO_ENTRY_PRECOMPUTE_RADIUS;
offsetX++) {
for (int offsetZ = -STUDIO_ENTRY_PRECOMPUTE_RADIUS;
offsetZ <= STUDIO_ENTRY_PRECOMPUTE_RADIUS;
offsetZ++) {
int chunkX = centerChunkX + offsetX;
int chunkZ = centerChunkZ + offsetZ;
CompletableFuture<Void> task = CompletableFuture.runAsync(
() -> prepareStudioEntryChunk(
bukkitWorld,
activeEngine,
generationSessionId,
chunkX,
chunkZ,
prepared),
executor);
tasks.add(task);
}
}
CompletableFuture<Void> completion = CompletableFuture.allOf(
tasks.toArray(new CompletableFuture<?>[0]));
CompletableFuture<Void> publication = completion.thenRun(() -> {
if (closing
|| engine != activeEngine
|| activeEngine.getGenerationSessionId() != generationSessionId) {
throw new IllegalStateException(
"Studio entry precompute finished for a replaced engine runtime.");
}
preparedStudioEntryChunks.clear();
preparedStudioEntryChunks.putAll(prepared);
});
return publication.whenComplete((ignored, failure) -> executor.shutdownNow());
}
private ExecutorService createStudioEntryExecutor() {
return Executors.newFixedThreadPool(STUDIO_ENTRY_PRECOMPUTE_THREADS, runnable -> {
Thread thread = new Thread(
runnable,
"Iris Studio Entry-" + STUDIO_ENTRY_THREAD_SEQUENCE.incrementAndGet());
thread.setDaemon(true);
thread.setPriority(Thread.NORM_PRIORITY);
return thread;
});
}
private void prepareStudioEntryChunk(
World bukkitWorld,
Engine activeEngine,
long generationSessionId,
int chunkX,
int chunkZ,
ConcurrentHashMap<Long, PreparedStudioChunk> prepared
) {
try (GenerationStagePermit ignored = acquireGenerationStage(
"studio_entry_chunk_precompute")) {
TerrainChunk terrainChunk = TerrainChunk.create(bukkitWorld);
ChunkDataHunkHolder blocks = new ChunkDataHunkHolder(terrainChunk.getChunkData());
Hunk<PlatformBiome> biomes = Hunk.viewBiomes(terrainChunk);
ChunkContext context = createStudioEntryContext(
activeEngine,
generationSessionId,
chunkX,
chunkZ);
activeEngine.generateMatter(chunkX, chunkZ, true, context);
try {
activeEngine.generate(chunkX << 4, chunkZ << 4, blocks, biomes, false);
} catch (WrongEngineBroException exception) {
throw new CompletionException(exception);
}
prepared.put(
chunkKey(chunkX, chunkZ),
new PreparedStudioChunk(activeEngine, generationSessionId, blocks)
);
}
}
private ChunkContext createStudioEntryContext(
Engine activeEngine,
long generationSessionId,
int chunkX,
int chunkZ
) {
boolean cacheContext = !activeEngine.getPlatformHooks()
.shouldDisableChunkContextCache(activeEngine);
ChunkContext.PrefillPlan prefillPlan = cacheContext
? ChunkContext.PrefillPlan.NO_CAVE
: ChunkContext.PrefillPlan.NONE;
return new ChunkContext(
chunkX << 4,
chunkZ << 4,
activeEngine.getComplex(),
generationSessionId,
cacheContext,
prefillPlan,
activeEngine.getMetrics());
}
public boolean isStudioEntryBootstrapActive() {
return studioEntryBootstrapActive.get();
}
@@ -783,12 +916,19 @@ public class BukkitChunkGenerator extends ChunkGenerator implements PlatformChun
if (studioGenerator != null) {
studioGenerator.generateChunk(engine, tc, x, z);
} else {
PreparedStudioChunk prepared = preparedStudioEntryChunks.remove(chunkKey(x, z));
if (prepared != null
&& prepared.engine() == engine
&& prepared.generationSessionId() == engine.getGenerationSessionId()) {
prepared.blocks().applyTo(d);
IrisLogging.debug("Applied prepared Studio entry chunk " + x + " " + z);
return;
}
ChunkDataHunkHolder blocks = new ChunkDataHunkHolder(d);
Hunk<PlatformBiome> biomes = Hunk.viewBiomes(tc);
boolean useMulticore = studio && !J.isFolia();
try (GenerationSessionLease lease = engine.acquireGenerationLease("bukkit_terrain_stage");
IrisContext.Scope ignored = IrisContext.open(engine, lease.sessionId(), null)) {
engine.generate(x << 4, z << 4, blocks, biomes, useMulticore);
engine.generate(x << 4, z << 4, blocks, biomes, false);
blocks.apply();
}
}
@@ -829,6 +969,17 @@ public class BukkitChunkGenerator extends ChunkGenerator implements PlatformChun
return isMaintenanceActive();
}
private static long chunkKey(int chunkX, int chunkZ) {
return ((long) chunkX << 32) ^ (chunkZ & 0xFFFFFFFFL);
}
private record PreparedStudioChunk(
Engine engine,
long generationSessionId,
ChunkDataHunkHolder blocks
) {
}
private boolean isMaintenanceActive() {
World realWorld = BukkitWorldBinding.world(this.world);
return realWorld != null && IrisToolbelt.isWorldMaintenanceActive(realWorld);
@@ -910,7 +1061,7 @@ public class BukkitChunkGenerator extends ChunkGenerator implements PlatformChun
StudioMode desired = studio
? java.util.Optional.ofNullable(getEngine().getDimension().getStudioMode()).orElse(StudioMode.NORMAL)
: StudioMode.NORMAL;
if (studio && art.arcane.iris.core.runtime.ObjectStudioActivation.isActive(getEngine().getDimension().getLoadKey())) {
if (studio && ObjectStudioActivation.isActive(getEngine().getDimension().getLoadKey())) {
desired = StudioMode.OBJECT_BUFFET;
}
if (!desired.equals(lastMode)) {

Some files were not shown because too many files have changed in this diff Show More