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();