mirror of
https://github.com/VolmitSoftware/Iris.git
synced 2026-08-27 04:37:47 +00:00
d
d
This commit is contained in:
+129
-24
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
+4
-1
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
+4
-3
@@ -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
|
||||
|
||||
+46
-1
@@ -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"));
|
||||
}
|
||||
}
|
||||
|
||||
+74
-15
@@ -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
|
||||
|
||||
+8
-4
@@ -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;
|
||||
|
||||
+6
-1
@@ -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
|
||||
}
|
||||
|
||||
+64
@@ -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;
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -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
|
||||
}
|
||||
|
||||
+85
-17
@@ -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);
|
||||
}
|
||||
}
|
||||
+7
-2
@@ -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);
|
||||
|
||||
+3
-1
@@ -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
|
||||
|
||||
+14
-14
@@ -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")
|
||||
|
||||
+1
-1
@@ -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();
|
||||
|
||||
+67
-5
@@ -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);
|
||||
|
||||
+31
@@ -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,
|
||||
|
||||
+145
@@ -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
|
||||
);
|
||||
}
|
||||
}
|
||||
+12
@@ -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);
|
||||
|
||||
+139
-4
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+45
@@ -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);
|
||||
}
|
||||
}
|
||||
+15
@@ -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"));
|
||||
}
|
||||
}
|
||||
|
||||
+42
-3
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
+88
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
+2
-5
@@ -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;
|
||||
}
|
||||
|
||||
+1
-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();
|
||||
|
||||
Reference in New Issue
Block a user