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();
|
||||
|
||||
@@ -79,6 +79,11 @@ public final class IrisStartupValidation {
|
||||
return isReady(snapshot);
|
||||
}
|
||||
|
||||
public static boolean isRestartRequired() {
|
||||
Snapshot current = snapshot;
|
||||
return current.enforced() && current.datapacks() == ValidationState.RESTART_REQUIRED;
|
||||
}
|
||||
|
||||
public static Optional<String> denialReason() {
|
||||
Snapshot current = snapshot;
|
||||
if (!current.enforced() || isReady(current)) {
|
||||
|
||||
@@ -124,6 +124,7 @@ public class ServerConfigurator {
|
||||
&& pinLoadedDatapackCompilerInputs()
|
||||
&& pinLoadedDatapackRegistryRequirements();
|
||||
if (result.restartRequired()) {
|
||||
requireDatapackRestart();
|
||||
IrisLogging.warn("Iris datapack changes require another server restart before worlds can use them.");
|
||||
}
|
||||
}
|
||||
@@ -1020,6 +1021,24 @@ public class ServerConfigurator {
|
||||
}));
|
||||
}
|
||||
|
||||
public static void restartAtStartupBoundary(String reason) {
|
||||
String restartReason = reason == null || reason.isBlank()
|
||||
? "Iris startup validation requires a restart."
|
||||
: reason.trim();
|
||||
IrisLogging.warn(restartReason + " Restarting server before default worlds are loaded.");
|
||||
try {
|
||||
Bukkit.restart();
|
||||
} catch (Throwable failure) {
|
||||
IrisLogging.reportError("Unable to restart the server at the Iris startup boundary.", failure);
|
||||
}
|
||||
IrisLogging.error("The immediate Iris startup restart returned unexpectedly; stopping the server instead.");
|
||||
try {
|
||||
Bukkit.shutdown();
|
||||
} catch (Throwable failure) {
|
||||
IrisLogging.reportError("Unable to stop the server after the Iris startup restart returned.", failure);
|
||||
}
|
||||
}
|
||||
|
||||
public static boolean verifyDataPackInstalled(IrisDimension dimension) {
|
||||
KSet<String> keys = new KSet<>();
|
||||
boolean warn = false;
|
||||
|
||||
@@ -24,13 +24,19 @@ import art.arcane.iris.engine.framework.Engine;
|
||||
|
||||
import javax.swing.JFrame;
|
||||
import java.awt.Desktop;
|
||||
import java.awt.EventQueue;
|
||||
import java.awt.GraphicsEnvironment;
|
||||
import java.awt.desktop.QuitResponse;
|
||||
import java.awt.event.WindowAdapter;
|
||||
import java.awt.event.WindowEvent;
|
||||
import java.util.Locale;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
public final class GuiHost {
|
||||
private static final AtomicBoolean DESKTOP_QUIT_GUARD_INSTALLED = new AtomicBoolean(false);
|
||||
private static final Set<JFrame> MANAGED_FRAMES = ConcurrentHashMap.newKeySet();
|
||||
private static volatile Provider provider = new Provider() {
|
||||
};
|
||||
private static volatile boolean desktopSuppressed = false;
|
||||
@@ -78,6 +84,13 @@ public final class GuiHost {
|
||||
|
||||
public static void prepareFrame(JFrame frame) {
|
||||
frame.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);
|
||||
MANAGED_FRAMES.add(frame);
|
||||
frame.addWindowListener(new WindowAdapter() {
|
||||
@Override
|
||||
public void windowClosed(WindowEvent event) {
|
||||
MANAGED_FRAMES.remove(frame);
|
||||
}
|
||||
});
|
||||
prepareServerDesktop();
|
||||
}
|
||||
|
||||
@@ -96,15 +109,20 @@ public final class GuiHost {
|
||||
if (!desktop.isSupported(Desktop.Action.APP_QUIT_HANDLER)) {
|
||||
return;
|
||||
}
|
||||
desktop.setQuitHandler((event, response) -> cancelDesktopQuit(response));
|
||||
desktop.setQuitHandler((event, response) -> closeDesktopWindowsAndCancelQuit(response));
|
||||
} catch (Throwable error) {
|
||||
IrisLogging.reportError(error);
|
||||
IrisLogging.info("Unable to install the Iris desktop quit guard; use the server stop command instead of macOS Quit");
|
||||
}
|
||||
}
|
||||
|
||||
static void cancelDesktopQuit(QuitResponse response) {
|
||||
static void closeDesktopWindowsAndCancelQuit(QuitResponse response) {
|
||||
response.cancelQuit();
|
||||
EventQueue.invokeLater(() -> {
|
||||
for (JFrame frame : MANAGED_FRAMES) {
|
||||
frame.dispose();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,96 @@
|
||||
package art.arcane.iris.core.gui;
|
||||
|
||||
import java.awt.Color;
|
||||
|
||||
enum NoisePalette {
|
||||
TERRAIN("Terrain", 0D, 1D, new int[]{0x071A2F, 0x155E75, 0x2A9D8F, 0xE9C46A, 0xF4F1DE}),
|
||||
SIGNED("Signed", -1D, 1D, new int[]{0x173B66, 0x4F86C6, 0xE7EDF3, 0xE89555, 0x9D3B35}),
|
||||
GRAYSCALE("Grayscale", 0D, 1D, new int[]{0x050505, 0xFFFFFF});
|
||||
|
||||
static final int INVALID_COLOR = 0xFF2DAA;
|
||||
|
||||
private final String label;
|
||||
private final double minimum;
|
||||
private final double maximum;
|
||||
private final int[] lookup;
|
||||
private final Color[] displayColors;
|
||||
|
||||
NoisePalette(String label, double minimum, double maximum, int[] stops) {
|
||||
this.label = label;
|
||||
this.minimum = minimum;
|
||||
this.maximum = maximum;
|
||||
this.lookup = buildLookup(stops);
|
||||
this.displayColors = buildDisplayColors(lookup);
|
||||
}
|
||||
|
||||
int color(double value) {
|
||||
if (!Double.isFinite(value)) {
|
||||
return INVALID_COLOR;
|
||||
}
|
||||
return colorFinite(value);
|
||||
}
|
||||
|
||||
int colorFinite(double value) {
|
||||
double normalized = (value - minimum) / (maximum - minimum);
|
||||
return colorNormalized(normalized);
|
||||
}
|
||||
|
||||
int colorNormalized(double normalized) {
|
||||
double clipped = Math.max(0D, Math.min(1D, normalized));
|
||||
return lookup[(int) Math.round(clipped * (lookup.length - 1))];
|
||||
}
|
||||
|
||||
Color displayColorNormalized(double normalized) {
|
||||
double clipped = Math.max(0D, Math.min(1D, normalized));
|
||||
return displayColors[(int) Math.round(clipped * (displayColors.length - 1))];
|
||||
}
|
||||
|
||||
String label() {
|
||||
return label;
|
||||
}
|
||||
|
||||
double minimum() {
|
||||
return minimum;
|
||||
}
|
||||
|
||||
double maximum() {
|
||||
return maximum;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return label;
|
||||
}
|
||||
|
||||
private static int[] buildLookup(int[] stops) {
|
||||
int[] values = new int[256];
|
||||
for (int index = 0; index < values.length; index++) {
|
||||
double position = (index / 255D) * (stops.length - 1);
|
||||
int lowerIndex = Math.min(stops.length - 2, (int) position);
|
||||
double fraction = position - lowerIndex;
|
||||
values[index] = interpolate(stops[lowerIndex], stops[lowerIndex + 1], fraction);
|
||||
}
|
||||
return values;
|
||||
}
|
||||
|
||||
private static Color[] buildDisplayColors(int[] lookup) {
|
||||
Color[] colors = new Color[lookup.length];
|
||||
for (int index = 0; index < lookup.length; index++) {
|
||||
colors[index] = new Color(lookup[index]);
|
||||
}
|
||||
return colors;
|
||||
}
|
||||
|
||||
private static int interpolate(int from, int to, double fraction) {
|
||||
int red = channel(from, 16, to, fraction);
|
||||
int green = channel(from, 8, to, fraction);
|
||||
int blue = channel(from, 0, to, fraction);
|
||||
return (red << 16) | (green << 8) | blue;
|
||||
}
|
||||
|
||||
private static int channel(int from, int shift, int to, double fraction) {
|
||||
int fromChannel = (from >>> shift) & 0xFF;
|
||||
int toChannel = (to >>> shift) & 0xFF;
|
||||
return (int) Math.round(fromChannel + ((toChannel - fromChannel) * fraction));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,393 @@
|
||||
package art.arcane.iris.core.gui;
|
||||
|
||||
import art.arcane.iris.engine.framework.PreservationRegistry;
|
||||
import art.arcane.iris.spi.IrisServices;
|
||||
import art.arcane.volmlib.util.function.NoiseProvider;
|
||||
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.awt.image.DataBufferInt;
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.ArrayBlockingQueue;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.RejectedExecutionException;
|
||||
import java.util.concurrent.ThreadFactory;
|
||||
import java.util.concurrent.ThreadPoolExecutor;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.concurrent.atomic.AtomicLong;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
final class NoiseRenderCoordinator implements AutoCloseable {
|
||||
private final int workerCount;
|
||||
private final Listener listener;
|
||||
private final ThreadFactory workerThreadFactory;
|
||||
private final ThreadFactory coordinatorThreadFactory;
|
||||
private final AtomicReference<RenderGeneration> latestRequested = new AtomicReference<>();
|
||||
private final AtomicLong latestRevision = new AtomicLong();
|
||||
private final AtomicBoolean closed = new AtomicBoolean();
|
||||
|
||||
NoiseRenderCoordinator(Listener listener) {
|
||||
this(Math.min(4, Math.max(1, Runtime.getRuntime().availableProcessors() / 2)), listener);
|
||||
}
|
||||
|
||||
NoiseRenderCoordinator(int workerCount, Listener listener) {
|
||||
this.workerCount = Math.max(1, workerCount);
|
||||
this.listener = Objects.requireNonNull(listener, "listener");
|
||||
AtomicInteger threadIds = new AtomicInteger();
|
||||
workerThreadFactory = runnable -> {
|
||||
Thread thread = new Thread(runnable, "Iris Noise Renderer " + threadIds.incrementAndGet());
|
||||
thread.setDaemon(true);
|
||||
thread.setPriority(Thread.MIN_PRIORITY);
|
||||
return thread;
|
||||
};
|
||||
coordinatorThreadFactory = runnable -> {
|
||||
Thread thread = new Thread(runnable, "Iris Noise Coordinator " + threadIds.incrementAndGet());
|
||||
thread.setDaemon(true);
|
||||
thread.setPriority(Thread.MIN_PRIORITY);
|
||||
return thread;
|
||||
};
|
||||
}
|
||||
|
||||
void request(Request request) {
|
||||
Objects.requireNonNull(request, "request");
|
||||
if (closed.get()) {
|
||||
return;
|
||||
}
|
||||
latestRevision.accumulateAndGet(request.revision(), Math::max);
|
||||
cancelActiveRender();
|
||||
RenderGeneration generation = createGeneration(request);
|
||||
latestRequested.set(generation);
|
||||
generation.start();
|
||||
}
|
||||
|
||||
void cancel(long revision) {
|
||||
latestRevision.accumulateAndGet(revision, Math::max);
|
||||
cancelActiveRender();
|
||||
}
|
||||
|
||||
boolean isClosed() {
|
||||
return closed.get();
|
||||
}
|
||||
|
||||
static int sampleStepForBudget(int width, int height, long sampleBudget) {
|
||||
if (width < 1 || height < 1 || sampleBudget < 1L) {
|
||||
throw new IllegalArgumentException("Invalid noise sample budget");
|
||||
}
|
||||
long totalSamples = (long) width * height;
|
||||
int sampleStep = Math.max(1, (int) Math.ceil(Math.sqrt(totalSamples / (double) sampleBudget)));
|
||||
while (sampleCount(width, height, sampleStep) > sampleBudget) {
|
||||
sampleStep++;
|
||||
}
|
||||
return sampleStep;
|
||||
}
|
||||
|
||||
static long sampleCount(int width, int height, int sampleStep) {
|
||||
if (width < 1 || height < 1 || sampleStep < 1) {
|
||||
throw new IllegalArgumentException("Invalid noise sample dimensions");
|
||||
}
|
||||
long outputWidth = ((long) width + sampleStep - 1L) / sampleStep;
|
||||
long outputHeight = ((long) height + sampleStep - 1L) / sampleStep;
|
||||
return outputWidth * outputHeight;
|
||||
}
|
||||
|
||||
static int nextRefinementStep(int width, int height, int currentStep, long sampleBudget) {
|
||||
if (currentStep <= 1) {
|
||||
throw new IllegalArgumentException("Noise refinement requires a coarse input");
|
||||
}
|
||||
return Math.min(currentStep, sampleStepForBudget(width, height, sampleBudget));
|
||||
}
|
||||
|
||||
static long timeBoundSampleBudget(long completedSamples, double milliseconds, double targetMilliseconds,
|
||||
long minimumBudget, long maximumBudget) {
|
||||
if (completedSamples < 1L
|
||||
|| !Double.isFinite(milliseconds)
|
||||
|| milliseconds <= 0D
|
||||
|| !Double.isFinite(targetMilliseconds)
|
||||
|| targetMilliseconds <= 0D
|
||||
|| minimumBudget < 1L
|
||||
|| maximumBudget < minimumBudget) {
|
||||
throw new IllegalArgumentException("Invalid timed noise sample budget");
|
||||
}
|
||||
double projectedSamples = Math.ceil((completedSamples / milliseconds) * targetMilliseconds);
|
||||
long timeBoundBudget = (long) Math.min(maximumBudget, projectedSamples);
|
||||
return Math.max(minimumBudget, timeBoundBudget);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
if (!closed.compareAndSet(false, true)) {
|
||||
return;
|
||||
}
|
||||
latestRevision.incrementAndGet();
|
||||
cancelActiveRender();
|
||||
}
|
||||
|
||||
private void renderGeneration(RenderGeneration generation) {
|
||||
Request request = generation.request();
|
||||
if (!isCurrent(request)) {
|
||||
generation.close();
|
||||
return;
|
||||
}
|
||||
listener.onRenderStarted(request);
|
||||
try {
|
||||
Result result = render(generation);
|
||||
if (result != null && isCurrent(request)) {
|
||||
listener.onRenderCompleted(result);
|
||||
}
|
||||
} catch (Throwable error) {
|
||||
if (isCurrent(request)) {
|
||||
listener.onRenderFailed(request, error);
|
||||
}
|
||||
} finally {
|
||||
latestRequested.compareAndSet(generation, null);
|
||||
generation.close();
|
||||
}
|
||||
}
|
||||
|
||||
private Result render(RenderGeneration generation) throws InterruptedException {
|
||||
Request request = generation.request();
|
||||
long started = System.nanoTime();
|
||||
int outputWidth = Math.max(1, (request.width() + request.sampleStep() - 1) / request.sampleStep());
|
||||
int outputHeight = Math.max(1, (request.height() + request.sampleStep() - 1) / request.sampleStep());
|
||||
BufferedImage image = new BufferedImage(outputWidth, outputHeight, BufferedImage.TYPE_INT_RGB);
|
||||
int[] pixels = ((DataBufferInt) image.getRaster().getDataBuffer()).getData();
|
||||
int bandCount = Math.min(workerCount, outputHeight);
|
||||
BandStats[] bandStats = new BandStats[bandCount];
|
||||
CountDownLatch completion = new CountDownLatch(bandCount);
|
||||
AtomicReference<Throwable> failure = new AtomicReference<>();
|
||||
for (int band = 0; band < bandCount; band++) {
|
||||
bandStats[band] = new BandStats();
|
||||
}
|
||||
for (int band = 0; band < bandCount; band++) {
|
||||
int bandIndex = band;
|
||||
Runnable task = () -> {
|
||||
try {
|
||||
renderBand(request, image, pixels, bandIndex, bandCount, bandStats[bandIndex]);
|
||||
} catch (Throwable error) {
|
||||
failure.compareAndSet(null, error);
|
||||
} finally {
|
||||
completion.countDown();
|
||||
}
|
||||
};
|
||||
try {
|
||||
generation.executor().execute(task);
|
||||
} catch (RejectedExecutionException exception) {
|
||||
failure.compareAndSet(null, exception);
|
||||
completion.countDown();
|
||||
}
|
||||
}
|
||||
completion.await();
|
||||
Throwable renderFailure = failure.get();
|
||||
if (renderFailure != null) {
|
||||
if (renderFailure instanceof RuntimeException runtimeException) {
|
||||
throw runtimeException;
|
||||
}
|
||||
if (renderFailure instanceof Error error) {
|
||||
throw error;
|
||||
}
|
||||
throw new IllegalStateException("Noise rendering failed", renderFailure);
|
||||
}
|
||||
if (!isCurrent(request)) {
|
||||
return null;
|
||||
}
|
||||
RenderStats combined = combine(bandStats);
|
||||
double milliseconds = (System.nanoTime() - started) / 1_000_000D;
|
||||
return new Result(request, image, milliseconds, combined.samples, combined.minimum, combined.maximum,
|
||||
combined.centerValue, combined.underflow, combined.overflow, combined.invalid);
|
||||
}
|
||||
|
||||
private void renderBand(Request request, BufferedImage image, int[] pixels, int bandIndex, int bandCount,
|
||||
BandStats stats) {
|
||||
int outputWidth = image.getWidth();
|
||||
int outputHeight = image.getHeight();
|
||||
int fromY = (outputHeight * bandIndex) / bandCount;
|
||||
int toY = (outputHeight * (bandIndex + 1)) / bandCount;
|
||||
double sampleOffset = request.sampleStep() * 0.5D;
|
||||
double startWorldX = request.viewport().worldX(sampleOffset, request.width());
|
||||
double worldStep = request.viewport().blocksPerPixel() * request.sampleStep();
|
||||
NoisePalette palette = request.palette();
|
||||
double paletteMinimum = palette.minimum();
|
||||
double paletteMaximum = palette.maximum();
|
||||
for (int y = fromY; y < toY; y++) {
|
||||
double screenY = (y * (double) request.sampleStep()) + sampleOffset;
|
||||
double worldZ = request.viewport().worldZ(screenY, request.height());
|
||||
double worldX = startWorldX;
|
||||
int pixelIndex = y * outputWidth;
|
||||
boolean centerRow = y == outputHeight / 2;
|
||||
for (int x = 0; x < outputWidth; x++) {
|
||||
if ((x & 31) == 0 && (!isCurrent(request) || Thread.currentThread().isInterrupted())) {
|
||||
return;
|
||||
}
|
||||
double value = request.sampler().noise(worldX, worldZ);
|
||||
boolean center = centerRow && x == outputWidth / 2;
|
||||
if (Double.isFinite(value)) {
|
||||
pixels[pixelIndex++] = palette.colorFinite(value);
|
||||
stats.acceptFinite(value, center, paletteMinimum, paletteMaximum);
|
||||
} else {
|
||||
pixels[pixelIndex++] = NoisePalette.INVALID_COLOR;
|
||||
stats.acceptInvalid(value, center);
|
||||
}
|
||||
worldX += worldStep;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isCurrent(Request request) {
|
||||
return !closed.get()
|
||||
&& latestRequested.get() != null
|
||||
&& latestRequested.get().request() == request
|
||||
&& request.revision() >= latestRevision.get();
|
||||
}
|
||||
|
||||
private void cancelActiveRender() {
|
||||
RenderGeneration generation = latestRequested.getAndSet(null);
|
||||
if (generation != null) {
|
||||
generation.close();
|
||||
}
|
||||
}
|
||||
|
||||
private RenderGeneration createGeneration(Request request) {
|
||||
ThreadPoolExecutor executor = new ThreadPoolExecutor(
|
||||
workerCount,
|
||||
workerCount,
|
||||
0L,
|
||||
TimeUnit.MILLISECONDS,
|
||||
new ArrayBlockingQueue<>(Math.max(1, workerCount)),
|
||||
workerThreadFactory,
|
||||
new ThreadPoolExecutor.AbortPolicy()
|
||||
);
|
||||
PreservationRegistry preservation = IrisServices.getOrNull(PreservationRegistry.class);
|
||||
if (preservation != null) {
|
||||
preservation.register(executor);
|
||||
}
|
||||
RenderGeneration generation = new RenderGeneration(request, executor);
|
||||
Thread actualRunner = coordinatorThreadFactory.newThread(() -> renderGeneration(generation));
|
||||
generation.setRunner(actualRunner);
|
||||
if (preservation != null) {
|
||||
preservation.register(actualRunner);
|
||||
}
|
||||
return generation;
|
||||
}
|
||||
|
||||
private static RenderStats combine(BandStats[] bands) {
|
||||
long samples = 0L;
|
||||
long underflow = 0L;
|
||||
long overflow = 0L;
|
||||
long invalid = 0L;
|
||||
double minimum = Double.POSITIVE_INFINITY;
|
||||
double maximum = Double.NEGATIVE_INFINITY;
|
||||
double centerValue = Double.NaN;
|
||||
for (BandStats band : bands) {
|
||||
samples += band.samples;
|
||||
underflow += band.underflow;
|
||||
overflow += band.overflow;
|
||||
invalid += band.invalid;
|
||||
minimum = Math.min(minimum, band.minimum);
|
||||
maximum = Math.max(maximum, band.maximum);
|
||||
if (!Double.isNaN(band.centerValue)) {
|
||||
centerValue = band.centerValue;
|
||||
}
|
||||
}
|
||||
if (minimum == Double.POSITIVE_INFINITY) {
|
||||
minimum = Double.NaN;
|
||||
maximum = Double.NaN;
|
||||
}
|
||||
return new RenderStats(samples, minimum, maximum, centerValue, underflow, overflow, invalid);
|
||||
}
|
||||
|
||||
interface Listener {
|
||||
void onRenderStarted(Request request);
|
||||
|
||||
void onRenderCompleted(Result result);
|
||||
|
||||
void onRenderFailed(Request request, Throwable error);
|
||||
}
|
||||
|
||||
record Request(long revision, NoiseProvider sampler, NoiseViewport viewport, NoisePalette palette,
|
||||
int width, int height, int sampleStep) {
|
||||
Request {
|
||||
Objects.requireNonNull(sampler, "sampler");
|
||||
Objects.requireNonNull(viewport, "viewport");
|
||||
Objects.requireNonNull(palette, "palette");
|
||||
if (revision < 0L || width < 1 || height < 1 || sampleStep < 1) {
|
||||
throw new IllegalArgumentException("Invalid noise render request");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
record Result(Request request, BufferedImage image, double milliseconds, long samples, double minimum,
|
||||
double maximum, double centerValue, long underflow, long overflow, long invalid) {
|
||||
}
|
||||
|
||||
private static final class RenderGeneration {
|
||||
private final Request request;
|
||||
private final ThreadPoolExecutor executor;
|
||||
private volatile Thread runner;
|
||||
|
||||
private RenderGeneration(Request request, ThreadPoolExecutor executor) {
|
||||
this.request = Objects.requireNonNull(request, "request");
|
||||
this.executor = Objects.requireNonNull(executor, "executor");
|
||||
}
|
||||
|
||||
private Request request() {
|
||||
return request;
|
||||
}
|
||||
|
||||
private ThreadPoolExecutor executor() {
|
||||
return executor;
|
||||
}
|
||||
|
||||
private void setRunner(Thread runner) {
|
||||
this.runner = Objects.requireNonNull(runner, "runner");
|
||||
}
|
||||
|
||||
private void start() {
|
||||
runner.start();
|
||||
}
|
||||
|
||||
private void close() {
|
||||
Thread activeRunner = runner;
|
||||
if (activeRunner != null) {
|
||||
activeRunner.interrupt();
|
||||
}
|
||||
executor.shutdownNow();
|
||||
}
|
||||
}
|
||||
|
||||
private static final class BandStats {
|
||||
private long samples;
|
||||
private long underflow;
|
||||
private long overflow;
|
||||
private long invalid;
|
||||
private double minimum = Double.POSITIVE_INFINITY;
|
||||
private double maximum = Double.NEGATIVE_INFINITY;
|
||||
private double centerValue = Double.NaN;
|
||||
|
||||
private void acceptFinite(double value, boolean center, double paletteMinimum, double paletteMaximum) {
|
||||
samples++;
|
||||
if (center) {
|
||||
centerValue = value;
|
||||
}
|
||||
minimum = Math.min(minimum, value);
|
||||
maximum = Math.max(maximum, value);
|
||||
if (value < paletteMinimum) {
|
||||
underflow++;
|
||||
} else if (value > paletteMaximum) {
|
||||
overflow++;
|
||||
}
|
||||
}
|
||||
|
||||
private void acceptInvalid(double value, boolean center) {
|
||||
samples++;
|
||||
invalid++;
|
||||
if (center) {
|
||||
centerValue = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private record RenderStats(long samples, double minimum, double maximum, double centerValue,
|
||||
long underflow, long overflow, long invalid) {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
package art.arcane.iris.core.gui;
|
||||
|
||||
record NoiseViewport(double centerX, double centerZ, double blocksPerPixel) {
|
||||
static final double MIN_BLOCKS_PER_PIXEL = 0.0001D;
|
||||
static final double MAX_BLOCKS_PER_PIXEL = 1_000_000D;
|
||||
|
||||
NoiseViewport {
|
||||
if (!Double.isFinite(centerX) || !Double.isFinite(centerZ)) {
|
||||
throw new IllegalArgumentException("Viewport center must be finite");
|
||||
}
|
||||
if (!Double.isFinite(blocksPerPixel) || blocksPerPixel <= 0D) {
|
||||
throw new IllegalArgumentException("Viewport scale must be finite and positive");
|
||||
}
|
||||
}
|
||||
|
||||
double worldX(double screenX, int width) {
|
||||
return centerX + ((screenX - (width / 2D)) * blocksPerPixel);
|
||||
}
|
||||
|
||||
double worldZ(double screenZ, int height) {
|
||||
return centerZ + ((screenZ - (height / 2D)) * blocksPerPixel);
|
||||
}
|
||||
|
||||
NoiseViewport panPixels(double deltaX, double deltaZ) {
|
||||
return new NoiseViewport(
|
||||
centerX - (deltaX * blocksPerPixel),
|
||||
centerZ - (deltaZ * blocksPerPixel),
|
||||
blocksPerPixel
|
||||
);
|
||||
}
|
||||
|
||||
NoiseViewport zoomAt(double screenX, double screenZ, int width, int height, double factor) {
|
||||
if (!Double.isFinite(factor) || factor <= 0D) {
|
||||
throw new IllegalArgumentException("Zoom factor must be finite and positive");
|
||||
}
|
||||
double anchorX = worldX(screenX, width);
|
||||
double anchorZ = worldZ(screenZ, height);
|
||||
double nextScale = Math.max(MIN_BLOCKS_PER_PIXEL,
|
||||
Math.min(MAX_BLOCKS_PER_PIXEL, blocksPerPixel * factor));
|
||||
double nextCenterX = anchorX - ((screenX - (width / 2D)) * nextScale);
|
||||
double nextCenterZ = anchorZ - ((screenZ - (height / 2D)) * nextScale);
|
||||
return new NoiseViewport(nextCenterX, nextCenterZ, nextScale);
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,626 @@
|
||||
/*
|
||||
* Iris is a World Generator for Minecraft Servers
|
||||
* Copyright (c) 2026 Arcane Arts (Volmit Software)
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package art.arcane.iris.core.gui;
|
||||
|
||||
import art.arcane.iris.engine.framework.PreservationRegistry;
|
||||
import art.arcane.iris.engine.framework.render.IrisRenderer;
|
||||
import art.arcane.iris.engine.framework.render.RenderType;
|
||||
import art.arcane.iris.spi.IrisLogging;
|
||||
import art.arcane.iris.spi.IrisServices;
|
||||
|
||||
import java.awt.EventQueue;
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Comparator;
|
||||
import java.util.Iterator;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.ArrayBlockingQueue;
|
||||
import java.util.concurrent.Callable;
|
||||
import java.util.concurrent.CancellationException;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.Future;
|
||||
import java.util.concurrent.FutureTask;
|
||||
import java.util.concurrent.RejectedExecutionException;
|
||||
import java.util.concurrent.ThreadFactory;
|
||||
import java.util.concurrent.ThreadPoolExecutor;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.concurrent.atomic.AtomicLong;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
final class VisionRenderController implements AutoCloseable {
|
||||
static final int TILE_PIXELS = 64;
|
||||
static final double MINIMUM_BLOCKS_PER_PIXEL = 1D;
|
||||
static final double MAXIMUM_BLOCKS_PER_PIXEL = 4_096D;
|
||||
|
||||
private static final int MAXIMUM_WORKERS = 3;
|
||||
private static final int MAXIMUM_VISIBLE_TILES = 32_768;
|
||||
private static final int RENDER_QUEUE_CAPACITY = 12;
|
||||
private static final int PROBE_QUEUE_CAPACITY = 1;
|
||||
private static final long CACHE_BYTES = 64L * 1024L * 1024L;
|
||||
|
||||
private final Runnable listener;
|
||||
private final int renderWorkerCount;
|
||||
private final ThreadPoolExecutor renderExecutor;
|
||||
private final ThreadPoolExecutor probeExecutor;
|
||||
private final WeightedTileCache cache;
|
||||
private final AtomicLong viewSequence;
|
||||
private final AtomicLong probeSequence;
|
||||
private final AtomicBoolean closed;
|
||||
private final AtomicBoolean publicationQueued;
|
||||
private final AtomicBoolean publicationDirty;
|
||||
private final Set<Future<?>> activeRenderTasks;
|
||||
private volatile Frame currentFrame;
|
||||
private volatile WorkState currentWork;
|
||||
private volatile CancellationToken currentToken;
|
||||
|
||||
VisionRenderController(Runnable listener) {
|
||||
this(listener, RuntimeOptions.production());
|
||||
}
|
||||
|
||||
VisionRenderController(Runnable listener, RuntimeOptions options) {
|
||||
this.listener = Objects.requireNonNull(listener, "listener");
|
||||
Objects.requireNonNull(options, "options");
|
||||
AtomicInteger threadSequence = new AtomicInteger();
|
||||
this.renderWorkerCount = options.workers();
|
||||
this.renderExecutor = new ThreadPoolExecutor(
|
||||
options.workers(),
|
||||
options.workers(),
|
||||
0L,
|
||||
TimeUnit.MILLISECONDS,
|
||||
new ArrayBlockingQueue<>(options.renderQueueCapacity()),
|
||||
daemonFactory("Iris Vision Render", Thread.NORM_PRIORITY, threadSequence),
|
||||
new ThreadPoolExecutor.AbortPolicy()
|
||||
);
|
||||
this.probeExecutor = new ThreadPoolExecutor(
|
||||
1,
|
||||
1,
|
||||
0L,
|
||||
TimeUnit.MILLISECONDS,
|
||||
new ArrayBlockingQueue<>(PROBE_QUEUE_CAPACITY),
|
||||
daemonFactory("Iris Vision Probe", Thread.MIN_PRIORITY, threadSequence),
|
||||
new ThreadPoolExecutor.DiscardOldestPolicy()
|
||||
);
|
||||
this.cache = new WeightedTileCache(CACHE_BYTES);
|
||||
this.viewSequence = new AtomicLong();
|
||||
this.probeSequence = new AtomicLong();
|
||||
this.closed = new AtomicBoolean();
|
||||
this.publicationQueued = new AtomicBoolean();
|
||||
this.publicationDirty = new AtomicBoolean();
|
||||
this.activeRenderTasks = ConcurrentHashMap.newKeySet();
|
||||
if (options.registerPreservation()) {
|
||||
PreservationRegistry preservation = IrisServices.getOrNull(PreservationRegistry.class);
|
||||
if (preservation != null) {
|
||||
preservation.register(renderExecutor);
|
||||
preservation.register(probeExecutor);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
synchronized Frame request(RenderSpec spec) {
|
||||
Objects.requireNonNull(spec, "spec");
|
||||
if (closed.get()) {
|
||||
throw new IllegalStateException("Vision render controller is closed");
|
||||
}
|
||||
|
||||
CancellationToken previousToken = currentToken;
|
||||
if (previousToken != null) {
|
||||
previousToken.cancel();
|
||||
}
|
||||
renderExecutor.getQueue().clear();
|
||||
cancelActiveRenderTasks();
|
||||
probeSequence.incrementAndGet();
|
||||
probeExecutor.getQueue().clear();
|
||||
|
||||
List<VisibleTile> tiles = visibleTiles(spec);
|
||||
Frame frame = new Frame(viewSequence.incrementAndGet(), spec, tiles);
|
||||
for (VisibleTile tile : tiles) {
|
||||
tile.setImage(cache.get(frame.key(tile)));
|
||||
}
|
||||
CancellationToken token = new CancellationToken();
|
||||
WorkState work = new WorkState(frame, token);
|
||||
currentFrame = frame;
|
||||
currentToken = token;
|
||||
currentWork = work;
|
||||
schedule(work);
|
||||
publish(frame, token);
|
||||
return frame;
|
||||
}
|
||||
|
||||
Frame currentFrame() {
|
||||
return currentFrame;
|
||||
}
|
||||
|
||||
BufferedImage image(Frame frame, VisibleTile tile) {
|
||||
if (frame == null || tile == null) {
|
||||
return null;
|
||||
}
|
||||
return tile.image();
|
||||
}
|
||||
|
||||
Progress progress(Frame frame) {
|
||||
if (frame == null) {
|
||||
return new Progress(0, 0, 0, 0);
|
||||
}
|
||||
int ready = 0;
|
||||
for (VisibleTile tile : frame.tiles()) {
|
||||
if (tile.image() != null) {
|
||||
ready++;
|
||||
}
|
||||
}
|
||||
return new Progress(frame.tiles().size(), ready, renderExecutor.getActiveCount(), renderExecutor.getQueue().size());
|
||||
}
|
||||
|
||||
<T> void submitProbe(Frame frame, Callable<T> probe, Consumer<T> consumer) {
|
||||
Objects.requireNonNull(probe, "probe");
|
||||
Objects.requireNonNull(consumer, "consumer");
|
||||
if (frame == null || closed.get() || currentFrame != frame) {
|
||||
return;
|
||||
}
|
||||
long probeRevision = probeSequence.incrementAndGet();
|
||||
probeExecutor.getQueue().clear();
|
||||
try {
|
||||
probeExecutor.execute(() -> runProbe(frame, probeRevision, probe, consumer));
|
||||
} catch (RejectedExecutionException ignored) {
|
||||
probeExecutor.getQueue().clear();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized void close() {
|
||||
if (!closed.compareAndSet(false, true)) {
|
||||
return;
|
||||
}
|
||||
CancellationToken token = currentToken;
|
||||
if (token != null) {
|
||||
token.cancel();
|
||||
}
|
||||
currentWork = null;
|
||||
currentFrame = null;
|
||||
renderExecutor.getQueue().clear();
|
||||
cancelActiveRenderTasks();
|
||||
probeExecutor.getQueue().clear();
|
||||
renderExecutor.shutdownNow();
|
||||
probeExecutor.shutdownNow();
|
||||
cache.clear();
|
||||
}
|
||||
|
||||
static List<VisibleTile> visibleTiles(RenderSpec spec) {
|
||||
Objects.requireNonNull(spec, "spec");
|
||||
double blocksPerPixel = spec.blocksPerPixel();
|
||||
double tileSpan = TILE_PIXELS * blocksPerPixel;
|
||||
double halfWidth = spec.width() * blocksPerPixel * 0.5D;
|
||||
double halfHeight = spec.height() * blocksPerPixel * 0.5D;
|
||||
long minimumX = floorTile(spec.centerX() - halfWidth, tileSpan);
|
||||
long maximumX = floorTile(Math.nextDown(spec.centerX() + halfWidth), tileSpan);
|
||||
long minimumZ = floorTile(spec.centerZ() - halfHeight, tileSpan);
|
||||
long maximumZ = floorTile(Math.nextDown(spec.centerZ() + halfHeight), tileSpan);
|
||||
long tileCount = Math.multiplyExact(maximumX - minimumX + 1L, maximumZ - minimumZ + 1L);
|
||||
if (tileCount > MAXIMUM_VISIBLE_TILES) {
|
||||
throw new IllegalArgumentException("Vision viewport contains too many tiles");
|
||||
}
|
||||
|
||||
ArrayList<VisibleTile> tiles = new ArrayList<>((int) tileCount);
|
||||
double centerTileX = spec.centerX() / tileSpan;
|
||||
double centerTileZ = spec.centerZ() / tileSpan;
|
||||
for (long tileZ = minimumZ; ; tileZ++) {
|
||||
for (long tileX = minimumX; ; tileX++) {
|
||||
int screenX = (int) Math.round(spec.width() * 0.5D + (tileX * tileSpan - spec.centerX()) / blocksPerPixel);
|
||||
int screenY = (int) Math.round(spec.height() * 0.5D + (tileZ * tileSpan - spec.centerZ()) / blocksPerPixel);
|
||||
double deltaX = tileX + 0.5D - centerTileX;
|
||||
double deltaZ = tileZ + 0.5D - centerTileZ;
|
||||
tiles.add(new VisibleTile(tileX, tileZ, screenX, screenY, deltaX * deltaX + deltaZ * deltaZ));
|
||||
if (tileX == maximumX) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (tileZ == maximumZ) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
tiles.sort(Comparator.comparingDouble(VisibleTile::distanceSquared)
|
||||
.thenComparingLong(VisibleTile::tileZ)
|
||||
.thenComparingLong(VisibleTile::tileX));
|
||||
return List.copyOf(tiles);
|
||||
}
|
||||
|
||||
static long sampleCount(int tileCount) {
|
||||
if (tileCount < 1) {
|
||||
throw new IllegalArgumentException("Vision tile count must be positive");
|
||||
}
|
||||
return Math.multiplyExact((long) tileCount, (long) TILE_PIXELS * TILE_PIXELS);
|
||||
}
|
||||
|
||||
private static long floorTile(double coordinate, double tileSpan) {
|
||||
double tile = Math.floor(coordinate / tileSpan);
|
||||
if (tile < Long.MIN_VALUE || tile > Long.MAX_VALUE) {
|
||||
throw new IllegalArgumentException("Vision viewport exceeds tile coordinate range");
|
||||
}
|
||||
return (long) tile;
|
||||
}
|
||||
|
||||
private static ThreadFactory daemonFactory(String name, int priority, AtomicInteger sequence) {
|
||||
return (Runnable runnable) -> {
|
||||
Thread thread = new Thread(runnable, name + " " + sequence.incrementAndGet());
|
||||
thread.setDaemon(true);
|
||||
thread.setPriority(priority);
|
||||
thread.setUncaughtExceptionHandler((Thread failedThread, Throwable error) -> IrisLogging.reportError(error));
|
||||
return thread;
|
||||
};
|
||||
}
|
||||
|
||||
private void schedule(WorkState work) {
|
||||
synchronized (work) {
|
||||
if (!isCurrent(work)) {
|
||||
return;
|
||||
}
|
||||
int admissionLimit = work.frame().spec().type() == RenderType.RIVER ? 1 : renderWorkerCount;
|
||||
while (work.inFlight() < admissionLimit) {
|
||||
VisibleTile tile = work.nextMissing();
|
||||
if (tile == null) {
|
||||
return;
|
||||
}
|
||||
work.incrementInFlight();
|
||||
TrackedRenderTask task = new TrackedRenderTask(() -> render(work, tile), activeRenderTasks);
|
||||
activeRenderTasks.add(task);
|
||||
try {
|
||||
renderExecutor.execute(task);
|
||||
} catch (RejectedExecutionException ignored) {
|
||||
task.cancel(false);
|
||||
work.decrementInFlight();
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void render(WorkState work, VisibleTile tile) {
|
||||
Frame frame = work.frame();
|
||||
CancellationToken token = work.token();
|
||||
try {
|
||||
if (!isCurrent(frame, token)) {
|
||||
return;
|
||||
}
|
||||
double tileSpan = TILE_PIXELS * frame.spec().blocksPerPixel();
|
||||
BufferedImage image = frame.spec().renderer().renderStudio(
|
||||
tile.tileX() * tileSpan,
|
||||
tile.tileZ() * tileSpan,
|
||||
tileSpan,
|
||||
TILE_PIXELS,
|
||||
frame.spec().type(),
|
||||
() -> !isCurrent(frame, token)
|
||||
);
|
||||
if (!isCurrent(frame, token)) {
|
||||
return;
|
||||
}
|
||||
cache.put(frame.key(tile), image);
|
||||
tile.setImage(image);
|
||||
publish(frame, token);
|
||||
} catch (CancellationException ignored) {
|
||||
} catch (Throwable error) {
|
||||
IrisLogging.debug("Vision tile render failed: " + error.getClass().getSimpleName() + ": " + error.getMessage());
|
||||
} finally {
|
||||
complete(work);
|
||||
}
|
||||
}
|
||||
|
||||
private void complete(WorkState work) {
|
||||
synchronized (work) {
|
||||
work.decrementInFlight();
|
||||
if (!isCurrent(work)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
schedule(work);
|
||||
}
|
||||
|
||||
private <T> void runProbe(Frame frame, long probeRevision, Callable<T> probe, Consumer<T> consumer) {
|
||||
try {
|
||||
if (!isProbeCurrent(frame, probeRevision)) {
|
||||
return;
|
||||
}
|
||||
T result = probe.call();
|
||||
if (!isProbeCurrent(frame, probeRevision)) {
|
||||
return;
|
||||
}
|
||||
EventQueue.invokeLater(() -> {
|
||||
if (isProbeCurrent(frame, probeRevision)) {
|
||||
consumer.accept(result);
|
||||
}
|
||||
});
|
||||
} catch (InterruptedException interrupted) {
|
||||
Thread.currentThread().interrupt();
|
||||
} catch (Throwable error) {
|
||||
IrisLogging.debug("Vision probe failed: " + error.getClass().getSimpleName() + ": " + error.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isProbeCurrent(Frame frame, long probeRevision) {
|
||||
return !closed.get() && currentFrame == frame && probeSequence.get() == probeRevision;
|
||||
}
|
||||
|
||||
private boolean isCurrent(Frame frame, CancellationToken token) {
|
||||
return !closed.get() && !token.cancelled() && currentFrame == frame;
|
||||
}
|
||||
|
||||
private boolean isCurrent(WorkState work) {
|
||||
return currentWork == work && isCurrent(work.frame(), work.token());
|
||||
}
|
||||
|
||||
private void publish(Frame frame, CancellationToken token) {
|
||||
if (!isCurrent(frame, token)) {
|
||||
return;
|
||||
}
|
||||
publicationDirty.set(true);
|
||||
queuePublication();
|
||||
}
|
||||
|
||||
private void queuePublication() {
|
||||
if (!publicationQueued.compareAndSet(false, true)) {
|
||||
return;
|
||||
}
|
||||
EventQueue.invokeLater(() -> {
|
||||
try {
|
||||
if (publicationDirty.getAndSet(false) && !closed.get()) {
|
||||
listener.run();
|
||||
}
|
||||
} finally {
|
||||
publicationQueued.set(false);
|
||||
if (publicationDirty.get() && !closed.get()) {
|
||||
queuePublication();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void cancelActiveRenderTasks() {
|
||||
for (Future<?> task : activeRenderTasks) {
|
||||
task.cancel(true);
|
||||
}
|
||||
activeRenderTasks.clear();
|
||||
}
|
||||
|
||||
record RenderSpec(
|
||||
IrisRenderer renderer,
|
||||
RenderType type,
|
||||
long contentRevision,
|
||||
double centerX,
|
||||
double centerZ,
|
||||
double blocksPerPixel,
|
||||
int width,
|
||||
int height
|
||||
) {
|
||||
RenderSpec {
|
||||
Objects.requireNonNull(renderer, "renderer");
|
||||
Objects.requireNonNull(type, "type");
|
||||
if (!Double.isFinite(centerX) || !Double.isFinite(centerZ)) {
|
||||
throw new IllegalArgumentException("Vision center must be finite");
|
||||
}
|
||||
if (!Double.isFinite(blocksPerPixel)
|
||||
|| blocksPerPixel < MINIMUM_BLOCKS_PER_PIXEL
|
||||
|| blocksPerPixel > MAXIMUM_BLOCKS_PER_PIXEL) {
|
||||
throw new IllegalArgumentException("Vision scale is outside the supported range");
|
||||
}
|
||||
if (width < 1 || height < 1) {
|
||||
throw new IllegalArgumentException("Vision viewport must be positive");
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
record Frame(long viewRevision, RenderSpec spec, List<VisibleTile> tiles) {
|
||||
Frame {
|
||||
Objects.requireNonNull(spec, "spec");
|
||||
tiles = List.copyOf(tiles);
|
||||
}
|
||||
|
||||
TileKey key(VisibleTile tile) {
|
||||
return new TileKey(spec.contentRevision(), spec.type(), spec.blocksPerPixel(), tile.tileX(), tile.tileZ());
|
||||
}
|
||||
}
|
||||
|
||||
static final class VisibleTile {
|
||||
private final long tileX;
|
||||
private final long tileZ;
|
||||
private final int screenX;
|
||||
private final int screenY;
|
||||
private final double distanceSquared;
|
||||
private volatile BufferedImage image;
|
||||
|
||||
private VisibleTile(long tileX, long tileZ, int screenX, int screenY, double distanceSquared) {
|
||||
this.tileX = tileX;
|
||||
this.tileZ = tileZ;
|
||||
this.screenX = screenX;
|
||||
this.screenY = screenY;
|
||||
this.distanceSquared = distanceSquared;
|
||||
}
|
||||
|
||||
long tileX() {
|
||||
return tileX;
|
||||
}
|
||||
|
||||
long tileZ() {
|
||||
return tileZ;
|
||||
}
|
||||
|
||||
int screenX() {
|
||||
return screenX;
|
||||
}
|
||||
|
||||
int screenY() {
|
||||
return screenY;
|
||||
}
|
||||
|
||||
double distanceSquared() {
|
||||
return distanceSquared;
|
||||
}
|
||||
|
||||
BufferedImage image() {
|
||||
return image;
|
||||
}
|
||||
|
||||
void setImage(BufferedImage image) {
|
||||
this.image = image;
|
||||
}
|
||||
}
|
||||
|
||||
record TileKey(long contentRevision, RenderType type, double blocksPerPixel, long tileX, long tileZ) {
|
||||
TileKey {
|
||||
Objects.requireNonNull(type, "type");
|
||||
if (!Double.isFinite(blocksPerPixel) || blocksPerPixel <= 0D) {
|
||||
throw new IllegalArgumentException("Vision cache scale must be finite and positive");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
record Progress(int total, int ready, int active, int queued) {
|
||||
double completion() {
|
||||
if (total == 0) {
|
||||
return 1D;
|
||||
}
|
||||
return Math.min(1D, ready / (double) total);
|
||||
}
|
||||
}
|
||||
|
||||
record RuntimeOptions(int workers, int renderQueueCapacity, boolean registerPreservation) {
|
||||
RuntimeOptions {
|
||||
if (workers < 1 || renderQueueCapacity < 1) {
|
||||
throw new IllegalArgumentException("Vision render runtime options are invalid");
|
||||
}
|
||||
}
|
||||
|
||||
static RuntimeOptions production() {
|
||||
int processors = Math.max(1, Runtime.getRuntime().availableProcessors());
|
||||
int workers = Math.max(1, Math.min(MAXIMUM_WORKERS, processors));
|
||||
return new RuntimeOptions(workers, RENDER_QUEUE_CAPACITY, true);
|
||||
}
|
||||
}
|
||||
|
||||
private static final class CancellationToken {
|
||||
private final AtomicBoolean cancelled = new AtomicBoolean();
|
||||
|
||||
void cancel() {
|
||||
cancelled.set(true);
|
||||
}
|
||||
|
||||
boolean cancelled() {
|
||||
return cancelled.get();
|
||||
}
|
||||
}
|
||||
|
||||
private static final class TrackedRenderTask extends FutureTask<Void> {
|
||||
private final Set<Future<?>> tasks;
|
||||
|
||||
private TrackedRenderTask(Runnable task, Set<Future<?>> tasks) {
|
||||
super(task, null);
|
||||
this.tasks = tasks;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void done() {
|
||||
tasks.remove(this);
|
||||
}
|
||||
}
|
||||
|
||||
private static final class WorkState {
|
||||
private final Frame frame;
|
||||
private final CancellationToken token;
|
||||
private int index;
|
||||
private int inFlight;
|
||||
|
||||
private WorkState(Frame frame, CancellationToken token) {
|
||||
this.frame = frame;
|
||||
this.token = token;
|
||||
}
|
||||
|
||||
Frame frame() {
|
||||
return frame;
|
||||
}
|
||||
|
||||
CancellationToken token() {
|
||||
return token;
|
||||
}
|
||||
|
||||
VisibleTile nextMissing() {
|
||||
while (index < frame.tiles().size()) {
|
||||
VisibleTile tile = frame.tiles().get(index++);
|
||||
if (tile.image() == null) {
|
||||
return tile;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
int inFlight() {
|
||||
return inFlight;
|
||||
}
|
||||
|
||||
void incrementInFlight() {
|
||||
inFlight++;
|
||||
}
|
||||
|
||||
void decrementInFlight() {
|
||||
inFlight--;
|
||||
}
|
||||
}
|
||||
|
||||
private static final class WeightedTileCache {
|
||||
private final long maximumBytes;
|
||||
private final LinkedHashMap<TileKey, CacheEntry> entries;
|
||||
private long bytes;
|
||||
|
||||
private WeightedTileCache(long maximumBytes) {
|
||||
this.maximumBytes = maximumBytes;
|
||||
this.entries = new LinkedHashMap<>(128, 0.75F, true);
|
||||
}
|
||||
|
||||
synchronized BufferedImage get(TileKey key) {
|
||||
CacheEntry entry = entries.get(key);
|
||||
return entry == null ? null : entry.image();
|
||||
}
|
||||
|
||||
synchronized void put(TileKey key, BufferedImage image) {
|
||||
long imageBytes = (long) image.getWidth() * image.getHeight() * Integer.BYTES;
|
||||
CacheEntry previous = entries.put(key, new CacheEntry(image, imageBytes));
|
||||
if (previous != null) {
|
||||
bytes -= previous.bytes();
|
||||
}
|
||||
bytes += imageBytes;
|
||||
while (bytes > maximumBytes && !entries.isEmpty()) {
|
||||
Iterator<Map.Entry<TileKey, CacheEntry>> iterator = entries.entrySet().iterator();
|
||||
Map.Entry<TileKey, CacheEntry> eldest = iterator.next();
|
||||
bytes -= eldest.getValue().bytes();
|
||||
iterator.remove();
|
||||
}
|
||||
}
|
||||
|
||||
synchronized void clear() {
|
||||
entries.clear();
|
||||
bytes = 0L;
|
||||
}
|
||||
}
|
||||
|
||||
private record CacheEntry(BufferedImage image, long bytes) {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
package art.arcane.iris.core.gui;
|
||||
|
||||
record VisionViewport(double centerX, double centerZ, double blocksPerPixel) {
|
||||
VisionViewport {
|
||||
if (!Double.isFinite(centerX) || !Double.isFinite(centerZ)) {
|
||||
throw new IllegalArgumentException("Vision viewport center must be finite");
|
||||
}
|
||||
if (!Double.isFinite(blocksPerPixel) || blocksPerPixel <= 0D) {
|
||||
throw new IllegalArgumentException("Vision viewport scale must be finite and positive");
|
||||
}
|
||||
}
|
||||
|
||||
double worldX(double screenX, int width) {
|
||||
return centerX + (screenX - width * 0.5D) * blocksPerPixel;
|
||||
}
|
||||
|
||||
double worldZ(double screenZ, int height) {
|
||||
return centerZ + (screenZ - height * 0.5D) * blocksPerPixel;
|
||||
}
|
||||
|
||||
VisionViewport zoomAt(
|
||||
double screenX,
|
||||
double screenZ,
|
||||
int width,
|
||||
int height,
|
||||
double factor,
|
||||
double minimumScale,
|
||||
double maximumScale
|
||||
) {
|
||||
if (!Double.isFinite(factor) || factor <= 0D) {
|
||||
throw new IllegalArgumentException("Vision zoom factor must be finite and positive");
|
||||
}
|
||||
double anchorX = worldX(screenX, width);
|
||||
double anchorZ = worldZ(screenZ, height);
|
||||
double nextScale = Math.max(minimumScale, Math.min(maximumScale, blocksPerPixel * factor));
|
||||
return new VisionViewport(
|
||||
anchorX - (screenX - width * 0.5D) * nextScale,
|
||||
anchorZ - (screenZ - height * 0.5D) * nextScale,
|
||||
nextScale
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -10,7 +10,6 @@ public final class DesktopUiMessages {
|
||||
public static final TextKey VISION_VIEW = TextKey.of("iris.desktop.vision.view", "View:");
|
||||
public static final TextKey VISION_GRID = TextKey.of("iris.desktop.vision.grid", "Grid");
|
||||
public static final TextKey VISION_FOLLOW = TextKey.of("iris.desktop.vision.follow", "Follow");
|
||||
public static final TextKey VISION_LOW_QUALITY_SHORT = TextKey.of("iris.desktop.vision.low_quality_short", "LQ");
|
||||
public static final TextKey VISION_REFRESHING = TextKey.of("iris.desktop.vision.refreshing", "Refreshing");
|
||||
public static final TextKey VISION_FPS = TextKey.of("iris.desktop.vision.fps", "{fps} FPS");
|
||||
public static final TextKey VISION_ZOOM_RESET = TextKey.of("iris.desktop.vision.zoom_reset", "Zoom reset");
|
||||
@@ -19,8 +18,6 @@ public final class DesktopUiMessages {
|
||||
public static final TextKey VISION_FOLLOWING = TextKey.of("iris.desktop.vision.following", "Following {player}");
|
||||
public static final TextKey VISION_NO_PLAYER = TextKey.of("iris.desktop.vision.no_player", "No player in world");
|
||||
public static final TextKey VISION_FOLLOW_DISABLED = TextKey.of("iris.desktop.vision.follow_disabled", "Follow disabled");
|
||||
public static final TextKey VISION_LOW_QUALITY = TextKey.of("iris.desktop.vision.low_quality", "Low quality");
|
||||
public static final TextKey VISION_HIGH_QUALITY = TextKey.of("iris.desktop.vision.high_quality", "High quality");
|
||||
public static final TextKey VISION_STATUS_LEFT = TextKey.of("iris.desktop.vision.status_left", "{mode} | {bpp} bpp | {width} x {height} blocks");
|
||||
public static final TextKey VISION_STATUS_RIGHT = TextKey.of("iris.desktop.vision.status_right", "X: {x} Z: {z} | {fps} FPS");
|
||||
public static final TextKey VISION_ENTITY_POSITION = TextKey.of("iris.desktop.vision.entity_position", "Position: {x}, {y}, {z}");
|
||||
@@ -31,8 +28,8 @@ public final class DesktopUiMessages {
|
||||
public static final TextKey VISION_BIOME_KEY = TextKey.of("iris.desktop.vision.biome_key", "Key: {key}");
|
||||
public static final TextKey VISION_BIOME_FILE = TextKey.of("iris.desktop.vision.biome_file", "File: {file}");
|
||||
public static final TextKey VISION_VELOCITY = TextKey.of("iris.desktop.vision.velocity", "Velocity: {velocity}");
|
||||
public static final TextKey VISION_TILES = TextKey.of("iris.desktop.vision.tiles", "Tiles: {high} HD / {low} LQ");
|
||||
public static final TextKey VISION_WORKERS = TextKey.of("iris.desktop.vision.workers", "Workers: {high} HD / {low} LQ");
|
||||
public static final TextKey VISION_TILES = TextKey.of("iris.desktop.vision.tiles", "Atlas pages: {ready} / {total} exact");
|
||||
public static final TextKey VISION_WORKERS = TextKey.of("iris.desktop.vision.workers", "Workers: {active} active / {queued} queued");
|
||||
public static final TextKey VISION_CENTER = TextKey.of("iris.desktop.vision.center", "Center: {x}, {z}");
|
||||
public static final TextKey VISION_HELP_TOGGLE = TextKey.of("iris.desktop.vision.help.toggle", "Toggle help");
|
||||
public static final TextKey VISION_HELP_REFRESH = TextKey.of("iris.desktop.vision.help.refresh", "Refresh tiles");
|
||||
@@ -40,7 +37,6 @@ public final class DesktopUiMessages {
|
||||
public static final TextKey VISION_HELP_ZOOM = TextKey.of("iris.desktop.vision.help.zoom", "Zoom in/out");
|
||||
public static final TextKey VISION_HELP_RESET_ZOOM = TextKey.of("iris.desktop.vision.help.reset_zoom", "Reset zoom");
|
||||
public static final TextKey VISION_HELP_CYCLE_MODE = TextKey.of("iris.desktop.vision.help.cycle_mode", "Cycle render mode");
|
||||
public static final TextKey VISION_HELP_QUALITY = TextKey.of("iris.desktop.vision.help.quality", "Toggle tile quality");
|
||||
public static final TextKey VISION_HELP_FPS = TextKey.of("iris.desktop.vision.help.fps", "Toggle 30/60 FPS");
|
||||
public static final TextKey VISION_HELP_GRID = TextKey.of("iris.desktop.vision.help.grid", "Toggle grid");
|
||||
public static final TextKey VISION_HELP_BIOME = TextKey.of("iris.desktop.vision.help.biome", "Detailed biome info");
|
||||
@@ -53,6 +49,7 @@ public final class DesktopUiMessages {
|
||||
public static final TextKey VISION_MODE_BIOME_SEA = TextKey.of("iris.desktop.vision.mode.biome_sea", "Biome sea");
|
||||
public static final TextKey VISION_MODE_REGION = TextKey.of("iris.desktop.vision.mode.region", "Region");
|
||||
public static final TextKey VISION_MODE_CAVE_LAND = TextKey.of("iris.desktop.vision.mode.cave_land", "Cave land");
|
||||
public static final TextKey VISION_MODE_RIVER = TextKey.of("iris.desktop.vision.mode.river", "River network");
|
||||
public static final TextKey VISION_MODE_HEIGHT = TextKey.of("iris.desktop.vision.mode.height", "Height");
|
||||
public static final TextKey VISION_MODE_OBJECT_LOAD = TextKey.of("iris.desktop.vision.mode.object_load", "Object load");
|
||||
public static final TextKey VISION_MODE_DECORATOR_LOAD = TextKey.of("iris.desktop.vision.mode.decorator_load", "Decorator load");
|
||||
@@ -95,17 +92,17 @@ public final class DesktopUiMessages {
|
||||
public static final TextKey PREGEN_MEMORY = TextKey.of("iris.desktop.pregen.memory", "Memory: {used} ({usage}) Pressure: {pressure}/s");
|
||||
|
||||
private static final List<MessageKey> KEYS = List.of(
|
||||
VISION_TITLE, VISION_VIEW, VISION_GRID, VISION_FOLLOW, VISION_LOW_QUALITY_SHORT,
|
||||
VISION_TITLE, VISION_VIEW, VISION_GRID, VISION_FOLLOW,
|
||||
VISION_REFRESHING, VISION_FPS, VISION_ZOOM_RESET, VISION_GRID_ENABLED, VISION_GRID_DISABLED,
|
||||
VISION_FOLLOWING, VISION_NO_PLAYER, VISION_FOLLOW_DISABLED, VISION_LOW_QUALITY, VISION_HIGH_QUALITY,
|
||||
VISION_FOLLOWING, VISION_NO_PLAYER, VISION_FOLLOW_DISABLED,
|
||||
VISION_STATUS_LEFT, VISION_STATUS_RIGHT, VISION_ENTITY_POSITION, VISION_ENTITY_HEALTH,
|
||||
VISION_BLOCK_POSITION, VISION_CHUNK_POSITION, VISION_REGION_POSITION, VISION_BIOME_KEY,
|
||||
VISION_BIOME_FILE, VISION_VELOCITY, VISION_TILES, VISION_WORKERS, VISION_CENTER,
|
||||
VISION_HELP_TOGGLE, VISION_HELP_REFRESH, VISION_HELP_FOLLOW, VISION_HELP_ZOOM,
|
||||
VISION_HELP_RESET_ZOOM, VISION_HELP_CYCLE_MODE, VISION_HELP_QUALITY, VISION_HELP_FPS,
|
||||
VISION_HELP_RESET_ZOOM, VISION_HELP_CYCLE_MODE, VISION_HELP_FPS,
|
||||
VISION_HELP_GRID, VISION_HELP_BIOME, VISION_HELP_TELEPORT, VISION_HELP_EDITOR, VISION_OPENED,
|
||||
VISION_TELEPORTING, VISION_MODE_BIOME, VISION_MODE_BIOME_LAND, VISION_MODE_BIOME_SEA,
|
||||
VISION_MODE_REGION, VISION_MODE_CAVE_LAND, VISION_MODE_HEIGHT, VISION_MODE_OBJECT_LOAD,
|
||||
VISION_MODE_REGION, VISION_MODE_CAVE_LAND, VISION_MODE_RIVER, VISION_MODE_HEIGHT, VISION_MODE_OBJECT_LOAD,
|
||||
VISION_MODE_DECORATOR_LOAD, VISION_MODE_CONTINENT, VISION_MODE_LAYER_LOAD, NOISE_TITLE,
|
||||
NOISE_TITLE_GENERATOR, NOISE_SEARCH, NOISE_STATUS, NOISE_CATEGORY_CUSTOM,
|
||||
NOISE_CATEGORY_PACK_GENERATORS, NOISE_CATEGORY_SIMPLEX, NOISE_CATEGORY_PERLIN,
|
||||
|
||||
@@ -239,7 +239,7 @@ public interface INMSBinding {
|
||||
IrisImportedStructureControl importedStructures
|
||||
) throws NoSuchFieldException, IllegalAccessException;
|
||||
|
||||
void completeStudioStructureBootstrap(World world) throws NoSuchFieldException, IllegalAccessException;
|
||||
CompletableFuture<Void> completeStudioStructureBootstrap(World world) throws NoSuchFieldException, IllegalAccessException;
|
||||
|
||||
void abandonStudioStructureBootstrap(World world);
|
||||
|
||||
|
||||
@@ -49,6 +49,7 @@ import org.bukkit.inventory.ItemStack;
|
||||
import java.awt.Color;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.stream.StreamSupport;
|
||||
|
||||
public class NMSBinding1X implements INMSBinding {
|
||||
@@ -117,7 +118,8 @@ public class NMSBinding1X implements INMSBinding {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void completeStudioStructureBootstrap(World world) {
|
||||
public CompletableFuture<Void> completeStudioStructureBootstrap(World world) {
|
||||
return CompletableFuture.completedFuture(null);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -0,0 +1,876 @@
|
||||
package art.arcane.iris.core.pack;
|
||||
|
||||
import art.arcane.iris.engine.object.NoiseStyle;
|
||||
import art.arcane.iris.engine.river.RiverTopologyComplexity;
|
||||
import art.arcane.volmlib.util.json.JSONArray;
|
||||
import art.arcane.volmlib.util.json.JSONObject;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Comparator;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
final class PackRiverValidator {
|
||||
private static final Set<String> WATER_MODES = Set.of("SEA_LEVEL", "TERRACED");
|
||||
private static final Set<String> TERMINAL_MODES = Set.of("SUPPRESS", "DRY_CHANNEL", "SINKHOLE_GROTTO");
|
||||
private static final Set<String> ROUTING_POLICIES = Set.of("ALLOW", "AVOID", "BLOCK");
|
||||
private static final Set<String> CAVE_MODES = Set.of(
|
||||
"SEALED",
|
||||
"FLOOD_CLOSED_COMPONENT",
|
||||
"GENERATE_GROTTO",
|
||||
"GROTTO_OR_CLOSED_COMPONENT",
|
||||
"WATERFALL_POOL"
|
||||
);
|
||||
private static final Set<String> CAVE_FALLBACKS = Set.of("SEALED", "GENERATE_GROTTO");
|
||||
private static final Set<String> EXISTING_FLUID_POLICIES = Set.of("REJECT", "ALLOW_SAME", "REPLACE");
|
||||
private static final Set<String> UNSAFE_RIVER_STREAMS = Set.of("HEIGHT", "HEIGHT_OR_FLUID", "SLOPE");
|
||||
private static final Set<String> NOISE_STYLES = noiseStyles();
|
||||
|
||||
private PackRiverValidator() {
|
||||
}
|
||||
|
||||
static Validation validate(File packFolder, File[] dimensionFiles) {
|
||||
List<String> errors = new ArrayList<>();
|
||||
List<String> warnings = new ArrayList<>();
|
||||
if (packFolder == null || !packFolder.isDirectory() || dimensionFiles == null) {
|
||||
return new Validation(errors, warnings);
|
||||
}
|
||||
|
||||
boolean enabled = false;
|
||||
List<DimensionRiverContext> contexts = new ArrayList<>();
|
||||
List<File> sortedDimensions = new ArrayList<>(List.of(dimensionFiles));
|
||||
sortedDimensions.sort(Comparator.comparing(File::getPath));
|
||||
for (File dimensionFile : sortedDimensions) {
|
||||
JSONObject dimension = PackValidationIo.readJson(dimensionFile);
|
||||
if (dimension == null || !dimension.has("rivers")) {
|
||||
continue;
|
||||
}
|
||||
String dimensionKey = PackValidationIo.stripExtension(dimensionFile.getName());
|
||||
String path = "Dimension '" + dimensionKey + "' rivers";
|
||||
JSONObject rivers = requireObject(dimension, "rivers", path, errors);
|
||||
if (rivers == null) {
|
||||
continue;
|
||||
}
|
||||
PackJsonFieldChecks.validateOptionalBoolean(path, rivers, "enabled", errors);
|
||||
if (!booleanValue(rivers, "enabled", false)) {
|
||||
continue;
|
||||
}
|
||||
enabled = true;
|
||||
DimensionRiverContext context = new DimensionRiverContext(
|
||||
dimensionKey,
|
||||
dimension,
|
||||
rivers,
|
||||
referencedKeys(dimension.optJSONArray("regions"))
|
||||
);
|
||||
contexts.add(context);
|
||||
validateNetwork(packFolder, path, context, errors, warnings);
|
||||
}
|
||||
|
||||
if (enabled) {
|
||||
validateOverrides(packFolder, new File(packFolder, "regions"), "Region", contexts, errors, warnings);
|
||||
validateOverrides(packFolder, new File(packFolder, "biomes"), "Biome", contexts, errors, warnings);
|
||||
}
|
||||
return new Validation(errors, warnings);
|
||||
}
|
||||
|
||||
private static void validateNetwork(File packFolder, String path, DimensionRiverContext context,
|
||||
List<String> errors, List<String> warnings) {
|
||||
JSONObject rivers = context.rivers();
|
||||
JSONObject topology = nestedObject(rivers, "topology", path, errors);
|
||||
JSONObject terrain = nestedObject(rivers, "terrain", path, errors);
|
||||
JSONObject water = nestedObject(rivers, "water", path, errors);
|
||||
JSONObject biomes = nestedObject(rivers, "biomes", path, errors);
|
||||
JSONObject caves = nestedObject(rivers, "caves", path, errors);
|
||||
|
||||
if (topology != null) {
|
||||
validateTopology(packFolder, path + ".topology", topology, errors, warnings);
|
||||
}
|
||||
if (terrain != null) {
|
||||
validateTerrain(packFolder, path + ".terrain", terrain, errors, warnings);
|
||||
}
|
||||
if (water != null) {
|
||||
validateWater(path + ".water", water, errors);
|
||||
}
|
||||
if (biomes != null) {
|
||||
validateBiomePools(packFolder, path + ".biomes", biomes, false, errors, warnings);
|
||||
}
|
||||
boolean sinkholeTerminal = terrain != null
|
||||
&& "SINKHOLE_GROTTO".equals(stringValue(terrain, "terminalMode", "DRY_CHANNEL"));
|
||||
if (caves != null) {
|
||||
validateCaves(packFolder, path + ".caves", caves, sinkholeTerminal, errors, warnings);
|
||||
}
|
||||
|
||||
if (topology != null && terrain != null) {
|
||||
double meanderStrength = doubleValue(terrain, "meanderStrength", 72D);
|
||||
int cellSize = integerValue(topology, "cellSize", 512);
|
||||
if (Double.isFinite(meanderStrength) && meanderStrength > cellSize) {
|
||||
warnings.add(path + ".terrain.meanderStrength exceeds topology.cellSize; reaches may require large cache halos.");
|
||||
}
|
||||
validateTopologyComplexity(path, topology, terrain, errors);
|
||||
}
|
||||
if (sinkholeTerminal && caves != null) {
|
||||
validateSinkholeCapability(
|
||||
path + ".terrain.terminalMode",
|
||||
context,
|
||||
caves,
|
||||
path + ".caves",
|
||||
errors
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private static void validateTopology(File packFolder, String path, JSONObject topology,
|
||||
List<String> errors, List<String> warnings) {
|
||||
PackJsonFieldChecks.validateOptionalIntegerRange(path, topology, "cellSize", 64, 4096, errors);
|
||||
PackJsonFieldChecks.validateOptionalIntegerRange(path, topology, "tileCells", 1, 64, errors);
|
||||
PackJsonFieldChecks.validateOptionalDoubleRange(path, topology, "siteJitter", 0D, 0.49D, errors);
|
||||
PackJsonFieldChecks.validateOptionalIntegerRange(path, topology, "maxRouteReaches", 1, 256, errors);
|
||||
PackJsonFieldChecks.validateOptionalIntegerRange(path, topology, "minimumSourcesPerTile", 0, 64, errors);
|
||||
PackJsonFieldChecks.validateOptionalIntegerRange(path, topology, "sinkSearchReaches", 0, 7, errors);
|
||||
PackJsonFieldChecks.validateOptionalIntegerRange(path, topology, "routingBasinCells", 8, 256, errors);
|
||||
PackJsonFieldChecks.validateOptionalDoubleRange(path, topology, "routingPlateauHeight", 1D, 64D, errors);
|
||||
PackJsonFieldChecks.validateOptionalDoubleRange(path, topology, "routingNoiseWeight", 0D, 1024D, errors);
|
||||
PackJsonFieldChecks.validateOptionalDoubleRange(path, topology, "terrainHeightWeight", 0D, 16D, errors);
|
||||
PackJsonFieldChecks.validateOptionalDoubleRange(path, topology, "terrainSlopeWeight", 0D, 16D, errors);
|
||||
PackJsonFieldChecks.validateOptionalDoubleRange(path, topology, "oceanAttraction", 0D, 16D, errors);
|
||||
PackJsonFieldChecks.validateOptionalBoolean(path, topology, "requireOcean", errors);
|
||||
validateNoiseChance(packFolder, topology, "source", path, errors);
|
||||
validateNoiseChance(packFolder, topology, "continuation", path, errors);
|
||||
validateStyle(packFolder, topology, "routingStyle", path, errors);
|
||||
|
||||
int tileCells = integerValue(topology, "tileCells", 4);
|
||||
int minimumSourcesPerTile = integerValue(topology, "minimumSourcesPerTile", 0);
|
||||
if (tileCells >= 1 && tileCells <= 64
|
||||
&& minimumSourcesPerTile >= 0
|
||||
&& minimumSourcesPerTile > tileCells * tileCells) {
|
||||
errors.add(path + ".minimumSourcesPerTile must not exceed tileCells squared.");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private static void validateTerrain(File packFolder, String path, JSONObject terrain,
|
||||
List<String> errors, List<String> warnings) {
|
||||
validateStyledRange(packFolder, terrain, "channelWidth", path, 1D, 2048D, errors, warnings);
|
||||
validateStyledRange(packFolder, terrain, "bankWidth", path, 0D, 2048D, errors, warnings);
|
||||
validateStyledRange(packFolder, terrain, "depth", path, 1D, 512D, errors, warnings);
|
||||
PackJsonFieldChecks.validateOptionalDoubleRange(path, terrain, "maxChannelWidth", 1D, 2048D, errors);
|
||||
PackJsonFieldChecks.validateOptionalDoubleRange(path, terrain, "maxBankWidth", 0D, 2048D, errors);
|
||||
PackJsonFieldChecks.validateOptionalDoubleRange(path, terrain, "maxDepth", 1D, 512D, errors);
|
||||
PackJsonFieldChecks.validateOptionalDoubleRange(path, terrain, "orderWidthFactor", 0D, 8D, errors);
|
||||
PackJsonFieldChecks.validateOptionalDoubleRange(path, terrain, "orderDepthFactor", 0D, 8D, errors);
|
||||
PackJsonFieldChecks.validateOptionalIntegerRange(path, terrain, "maxIncision", 0, 512, errors);
|
||||
PackJsonFieldChecks.validateOptionalDoubleRange(path, terrain, "bankExponent", 0.125D, 16D, errors);
|
||||
PackJsonFieldChecks.validateOptionalDoubleRange(path, terrain, "meanderStrength", 0D, 1024D, errors);
|
||||
PackJsonFieldChecks.validateOptionalIntegerRange(path, terrain, "meanderSubdivisions", 1, 64, errors);
|
||||
PackJsonFieldChecks.validateOptionalDoubleRange(path, terrain, "bedRoughness", 0D, 8D, errors);
|
||||
PackJsonFieldChecks.validateOptionalEnum(path, terrain, "terminalMode", TERMINAL_MODES, errors);
|
||||
PackJsonFieldChecks.validateOptionalIntegerRange(path, terrain, "terminalTaper", 8, 1024, errors);
|
||||
PackJsonFieldChecks.validateOptionalDoubleRange(path, terrain, "dryContinuationChance", 0D, 1D, errors);
|
||||
validateNoiseChance(packFolder, terrain, "incision", path, errors);
|
||||
validateStyle(packFolder, terrain, "meanderStyle", path, errors);
|
||||
validateStyle(packFolder, terrain, "bedRoughnessStyle", path, errors);
|
||||
}
|
||||
|
||||
private static void validateWater(String path, JSONObject water, List<String> errors) {
|
||||
PackJsonFieldChecks.validateOptionalEnum(path, water, "mode", WATER_MODES, errors);
|
||||
PackJsonFieldChecks.validateOptionalIntegerRange(path, water, "poolLength", 8, 4096, errors);
|
||||
PackJsonFieldChecks.validateOptionalIntegerRange(path, water, "maximumPoolRise", 0, 64, errors);
|
||||
PackJsonFieldChecks.validateOptionalIntegerRange(path, water, "dropHeight", 1, 32, errors);
|
||||
|
||||
String mode = stringValue(water, "mode", "SEA_LEVEL");
|
||||
int maximumPoolRise = integerValue(water, "maximumPoolRise", 4);
|
||||
int dropHeight = integerValue(water, "dropHeight", 1);
|
||||
if ("TERRACED".equals(mode) && dropHeight > maximumPoolRise) {
|
||||
errors.add(path + ".dropHeight must not exceed maximumPoolRise in TERRACED mode.");
|
||||
}
|
||||
}
|
||||
|
||||
private static void validateTopologyComplexity(
|
||||
String path,
|
||||
JSONObject topology,
|
||||
JSONObject terrain,
|
||||
List<String> errors
|
||||
) {
|
||||
int cellSize = integerValue(topology, "cellSize", 512);
|
||||
int tileCells = integerValue(topology, "tileCells", 4);
|
||||
double siteJitter = doubleValue(topology, "siteJitter", 0.35D);
|
||||
int maxRouteReaches = integerValue(topology, "maxRouteReaches", 16);
|
||||
double meanderStrength = doubleValue(terrain, "meanderStrength", 72D);
|
||||
int meanderSubdivisions = integerValue(terrain, "meanderSubdivisions", 8);
|
||||
double maximumChannelWidth = doubleValue(terrain, "maxChannelWidth", 10D);
|
||||
double maximumBankWidth = doubleValue(terrain, "maxBankWidth", 4D);
|
||||
if (cellSize < 64 || cellSize > 4096
|
||||
|| tileCells < 1 || tileCells > 64
|
||||
|| !Double.isFinite(siteJitter) || siteJitter < 0D || siteJitter > 0.49D
|
||||
|| maxRouteReaches < 1 || maxRouteReaches > 256
|
||||
|| !Double.isFinite(meanderStrength) || meanderStrength < 0D || meanderStrength > 1024D
|
||||
|| meanderSubdivisions < 1 || meanderSubdivisions > 64
|
||||
|| !Double.isFinite(maximumChannelWidth) || maximumChannelWidth < 1D || maximumChannelWidth > 2048D
|
||||
|| !Double.isFinite(maximumBankWidth) || maximumBankWidth < 0D || maximumBankWidth > 2048D) {
|
||||
return;
|
||||
}
|
||||
double maximumReachRadius = maximumChannelWidth * 0.5D + maximumBankWidth;
|
||||
RiverTopologyComplexity.Estimate estimate = RiverTopologyComplexity.estimate(
|
||||
cellSize,
|
||||
tileCells,
|
||||
siteJitter,
|
||||
maxRouteReaches,
|
||||
maximumReachRadius,
|
||||
meanderStrength,
|
||||
meanderSubdivisions
|
||||
);
|
||||
for (String violation : estimate.violations()) {
|
||||
errors.add(path + " exceeds the safe derived complexity budget. " + violation);
|
||||
}
|
||||
}
|
||||
|
||||
private static void validateCaves(File packFolder, String path, JSONObject caves,
|
||||
boolean forceGeneratedGrotto,
|
||||
List<String> errors, List<String> warnings) {
|
||||
PackJsonFieldChecks.validateOptionalEnum(path, caves, "mode", CAVE_MODES, errors);
|
||||
PackJsonFieldChecks.validateOptionalIntegerRange(path, caves, "minimumSpacing", 16, 4096, errors);
|
||||
PackJsonFieldChecks.validateOptionalIntegerRange(path, caves, "maximumPerReach", 0, 16, errors);
|
||||
PackJsonFieldChecks.validateOptionalIntegerRange(path, caves, "maxBoreDepth", 1, 256, errors);
|
||||
PackJsonFieldChecks.validateOptionalIntegerRange(path, caves, "throatRadius", 1, 16, errors);
|
||||
PackJsonFieldChecks.validateOptionalIntegerRange(path, caves, "waterLevelOffset", -64, 64, errors);
|
||||
PackJsonFieldChecks.validateOptionalIntegerRange(path, caves, "dryHeadroom", 0, 64, errors);
|
||||
PackJsonFieldChecks.validateOptionalIntegerRange(path, caves, "grottoHorizontalRadius", 2, 128, errors);
|
||||
PackJsonFieldChecks.validateOptionalIntegerRange(path, caves, "grottoVerticalRadius", 2, 128, errors);
|
||||
PackJsonFieldChecks.validateOptionalDoubleRange(path, caves, "grottoWarpStrength", 0D, 32D, errors);
|
||||
PackJsonFieldChecks.validateOptionalIntegerRange(path, caves, "maxFloodRadius", 4, 256, errors);
|
||||
PackJsonFieldChecks.validateOptionalIntegerRange(path, caves, "maxFloodDepth", 4, 256, errors);
|
||||
PackJsonFieldChecks.validateOptionalIntegerRange(path, caves, "maxFloodVolume", 64, 1048576, errors);
|
||||
PackJsonFieldChecks.validateOptionalEnum(path, caves, "fallback", CAVE_FALLBACKS, errors);
|
||||
PackJsonFieldChecks.validateOptionalEnum(path, caves, "existingFluidPolicy", EXISTING_FLUID_POLICIES, errors);
|
||||
validateNoiseChance(packFolder, caves, "entry", path, errors);
|
||||
validateStyle(packFolder, caves, "grottoShapeStyle", path, errors);
|
||||
validateStyle(packFolder, caves, "grottoWarpStyle", path, errors);
|
||||
|
||||
String mode = stringValue(caves, "mode", "SEALED");
|
||||
if ("SEALED".equals(mode) && !forceGeneratedGrotto) {
|
||||
return;
|
||||
}
|
||||
|
||||
int maximumPerReach = integerValue(caves, "maximumPerReach", 1);
|
||||
double entryChance = noiseChanceValue(caves, "entry", 0.12D);
|
||||
if (maximumPerReach == 0 || (!forceGeneratedGrotto && entryChance == 0D)) {
|
||||
warnings.add(path + " enables cave hydrology but its entry gate cannot accept any connections.");
|
||||
}
|
||||
|
||||
int maxBoreDepth = integerValue(caves, "maxBoreDepth", 48);
|
||||
int throatRadius = integerValue(caves, "throatRadius", 2);
|
||||
int maxFloodRadius = integerValue(caves, "maxFloodRadius", 48);
|
||||
int maxFloodDepth = integerValue(caves, "maxFloodDepth", 32);
|
||||
if (throatRadius >= maxFloodRadius) {
|
||||
errors.add(path + ".throatRadius must be smaller than maxFloodRadius so the proof boundary can contain the throat.");
|
||||
}
|
||||
if (throatRadius >= maxFloodDepth) {
|
||||
errors.add(path + ".throatRadius must be smaller than maxFloodDepth so the proof boundary can contain the throat.");
|
||||
}
|
||||
if (maxBoreDepth > maxFloodDepth) {
|
||||
warnings.add(path + ".maxBoreDepth exceeds maxFloodDepth; deeper cave targets found by the bore search will be rejected by containment proof.");
|
||||
}
|
||||
|
||||
String fallback = stringValue(caves, "fallback", "SEALED");
|
||||
if (forceGeneratedGrotto || usesGeneratedGrotto(mode, fallback)) {
|
||||
validateGrotto(path, caves, errors);
|
||||
}
|
||||
}
|
||||
|
||||
private static void validateGrotto(String path, JSONObject caves, List<String> errors) {
|
||||
int throatRadius = integerValue(caves, "throatRadius", 2);
|
||||
int dryHeadroom = integerValue(caves, "dryHeadroom", 4);
|
||||
int horizontalRadius = integerValue(caves, "grottoHorizontalRadius", 12);
|
||||
int verticalRadius = integerValue(caves, "grottoVerticalRadius", 7);
|
||||
double warpStrength = doubleValue(caves, "grottoWarpStrength", 2D);
|
||||
int maxFloodRadius = integerValue(caves, "maxFloodRadius", 48);
|
||||
int maxFloodDepth = integerValue(caves, "maxFloodDepth", 32);
|
||||
int maxFloodVolume = integerValue(caves, "maxFloodVolume", 8192);
|
||||
if (throatRadius >= horizontalRadius || throatRadius >= verticalRadius) {
|
||||
addDistinct(errors, path + ".throatRadius must be smaller than both grotto radii so a sealed chamber can surround the inlet.");
|
||||
}
|
||||
if (dryHeadroom >= (verticalRadius * 2) + 1) {
|
||||
addDistinct(errors, path + ".dryHeadroom must fit inside the generated grotto height.");
|
||||
}
|
||||
|
||||
int warpEnvelope = Double.isFinite(warpStrength) ? (int) Math.ceil(warpStrength) : 0;
|
||||
int requiredRadius = horizontalRadius + warpEnvelope + 1;
|
||||
int requiredDepth = verticalRadius + warpEnvelope + 1;
|
||||
if (maxFloodRadius < requiredRadius) {
|
||||
addDistinct(errors, path + ".maxFloodRadius must be at least " + requiredRadius
|
||||
+ " to prove the configured grotto and its sealed shell.");
|
||||
}
|
||||
if (maxFloodDepth < requiredDepth) {
|
||||
addDistinct(errors, path + ".maxFloodDepth must be at least " + requiredDepth
|
||||
+ " to prove the configured grotto and its sealed shell.");
|
||||
}
|
||||
|
||||
long volume = grottoVolume(horizontalRadius, verticalRadius);
|
||||
if (volume > maxFloodVolume) {
|
||||
addDistinct(errors, path + ".maxFloodVolume must be at least " + volume
|
||||
+ " to contain the configured grotto before its throat and shell are considered.");
|
||||
}
|
||||
}
|
||||
|
||||
private static long grottoVolume(int horizontalRadius, int verticalRadius) {
|
||||
long volume = 0L;
|
||||
double horizontalSquared = (double) horizontalRadius * horizontalRadius;
|
||||
double verticalSquared = (double) verticalRadius * verticalRadius;
|
||||
for (int dx = -horizontalRadius; dx <= horizontalRadius; dx++) {
|
||||
for (int dy = -verticalRadius; dy <= verticalRadius; dy++) {
|
||||
double remaining = 1D - ((double) dx * dx / horizontalSquared)
|
||||
- ((double) dy * dy / verticalSquared);
|
||||
if (remaining < 0D) {
|
||||
continue;
|
||||
}
|
||||
int maximumZ = (int) Math.floor(horizontalRadius * Math.sqrt(remaining));
|
||||
volume += (maximumZ * 2L) + 1L;
|
||||
}
|
||||
}
|
||||
return volume;
|
||||
}
|
||||
|
||||
private static boolean usesGeneratedGrotto(String mode, String fallback) {
|
||||
return "GENERATE_GROTTO".equals(mode)
|
||||
|| "GROTTO_OR_CLOSED_COMPONENT".equals(mode)
|
||||
|| "WATERFALL_POOL".equals(mode)
|
||||
|| "GENERATE_GROTTO".equals(fallback);
|
||||
}
|
||||
|
||||
private static void validateSinkholeCapability(
|
||||
String terminalPath,
|
||||
DimensionRiverContext context,
|
||||
JSONObject caves,
|
||||
String cavesPath,
|
||||
List<String> errors
|
||||
) {
|
||||
String suffix = " in Dimension '" + context.dimensionKey() + "'.";
|
||||
if (!booleanValue(context.dimension(), "carvingEnabled", true)) {
|
||||
addDistinct(errors, terminalPath + " SINKHOLE_GROTTO requires carvingEnabled to be true" + suffix);
|
||||
}
|
||||
if (!booleanValue(context.dimension(), "useMantle", true)) {
|
||||
addDistinct(errors, terminalPath + " SINKHOLE_GROTTO requires useMantle to be true" + suffix);
|
||||
}
|
||||
if (disabled(context.dimension(), "CARVED")) {
|
||||
addDistinct(errors, terminalPath + " SINKHOLE_GROTTO requires CARVED to remain enabled" + suffix);
|
||||
}
|
||||
if (disabled(context.dimension(), "RIVER_HYDROLOGY")) {
|
||||
addDistinct(errors, terminalPath + " SINKHOLE_GROTTO requires RIVER_HYDROLOGY to remain enabled" + suffix);
|
||||
}
|
||||
if ("SEALED".equals(stringValue(caves, "mode", "SEALED"))) {
|
||||
addDistinct(errors, terminalPath + " SINKHOLE_GROTTO requires a non-SEALED caves.mode" + suffix);
|
||||
}
|
||||
if (integerValue(caves, "maximumPerReach", 1) <= 0) {
|
||||
addDistinct(errors, terminalPath + " SINKHOLE_GROTTO requires caves.maximumPerReach above zero" + suffix);
|
||||
}
|
||||
validateGrotto(cavesPath, caves, errors);
|
||||
}
|
||||
|
||||
private static void validateOverrideSinkhole(
|
||||
File packFolder,
|
||||
String terminalPath,
|
||||
String resourceKey,
|
||||
String resourceType,
|
||||
List<DimensionRiverContext> contexts,
|
||||
List<String> errors,
|
||||
List<String> warnings
|
||||
) {
|
||||
boolean referenced = false;
|
||||
for (DimensionRiverContext context : contexts) {
|
||||
boolean reachable = "Region".equals(resourceType)
|
||||
? context.regionKeys().contains(resourceKey)
|
||||
: referencedSurfaceBiomes(packFolder, context.regionKeys()).contains(resourceKey);
|
||||
if (!reachable) {
|
||||
continue;
|
||||
}
|
||||
referenced = true;
|
||||
JSONObject caves = nestedObject(context.rivers(), "caves", "Dimension '"
|
||||
+ context.dimensionKey() + "' rivers", errors);
|
||||
if (caves != null) {
|
||||
validateSinkholeCapability(
|
||||
terminalPath,
|
||||
context,
|
||||
caves,
|
||||
"Dimension '" + context.dimensionKey() + "' rivers.caves",
|
||||
errors
|
||||
);
|
||||
}
|
||||
}
|
||||
if (!referenced) {
|
||||
addDistinct(warnings, terminalPath
|
||||
+ " selects SINKHOLE_GROTTO but no enabled river dimension reaches this "
|
||||
+ resourceType.toLowerCase() + ".");
|
||||
}
|
||||
}
|
||||
|
||||
private static Set<String> referencedSurfaceBiomes(File packFolder, Set<String> regionKeys) {
|
||||
Set<String> biomes = new HashSet<>();
|
||||
File regionsFolder = new File(packFolder, "regions");
|
||||
for (String regionKey : regionKeys) {
|
||||
JSONObject region = PackValidationIo.readJson(new File(regionsFolder, regionKey + ".json"));
|
||||
if (region == null) {
|
||||
continue;
|
||||
}
|
||||
collectBiomeKeys(region.optJSONArray("landBiomes"), biomes);
|
||||
collectBiomeKeys(region.optJSONArray("seaBiomes"), biomes);
|
||||
collectBiomeKeys(region.optJSONArray("shoreBiomes"), biomes);
|
||||
}
|
||||
Set<String> roots = Set.copyOf(biomes);
|
||||
for (String biomeKey : roots) {
|
||||
collectBiomeChildren(packFolder, biomeKey, 0, biomes);
|
||||
}
|
||||
return biomes;
|
||||
}
|
||||
|
||||
private static void collectBiomeChildren(File packFolder, String biomeKey, int depth, Set<String> biomes) {
|
||||
if (depth >= 4) {
|
||||
return;
|
||||
}
|
||||
JSONObject biome = PackValidationIo.readJson(new File(packFolder, "biomes/" + biomeKey + ".json"));
|
||||
if (biome == null) {
|
||||
return;
|
||||
}
|
||||
JSONArray children = biome.optJSONArray("children");
|
||||
if (children == null) {
|
||||
return;
|
||||
}
|
||||
for (int index = 0; index < children.length(); index++) {
|
||||
String child = children.optString(index, null);
|
||||
if (child == null || child.isBlank()) {
|
||||
continue;
|
||||
}
|
||||
boolean added = biomes.add(child);
|
||||
if (added) {
|
||||
collectBiomeChildren(packFolder, child, depth + 1, biomes);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static Set<String> referencedKeys(JSONArray keys) {
|
||||
Set<String> referenced = new HashSet<>();
|
||||
collectBiomeKeys(keys, referenced);
|
||||
return Set.copyOf(referenced);
|
||||
}
|
||||
|
||||
private static void collectBiomeKeys(JSONArray keys, Set<String> destination) {
|
||||
if (keys == null) {
|
||||
return;
|
||||
}
|
||||
for (int index = 0; index < keys.length(); index++) {
|
||||
String key = keys.optString(index, null);
|
||||
if (key != null && !key.isBlank()) {
|
||||
destination.add(key);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean disabled(JSONObject dimension, String flag) {
|
||||
JSONArray disabled = dimension.optJSONArray("disabledComponents");
|
||||
if (disabled == null) {
|
||||
return false;
|
||||
}
|
||||
for (int index = 0; index < disabled.length(); index++) {
|
||||
if (flag.equals(disabled.optString(index, null))) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private static void addDistinct(List<String> destination, String value) {
|
||||
if (!destination.contains(value)) {
|
||||
destination.add(value);
|
||||
}
|
||||
}
|
||||
|
||||
private static void validateOverrides(File packFolder, File resourceFolder, String resourceType,
|
||||
List<DimensionRiverContext> contexts,
|
||||
List<String> errors, List<String> warnings) {
|
||||
if (!resourceFolder.isDirectory()) {
|
||||
return;
|
||||
}
|
||||
List<File> files = PackValidationIo.listJsonRecursive(resourceFolder);
|
||||
files.sort(Comparator.comparing(File::getPath));
|
||||
for (File file : files) {
|
||||
JSONObject resource = PackValidationIo.readJson(file);
|
||||
if (resource == null || !resource.has("riverOverride")) {
|
||||
continue;
|
||||
}
|
||||
String key = PackValidationIo.deriveKey(resourceFolder, file);
|
||||
String path = resourceType + " '" + key + "' riverOverride";
|
||||
Object rawOverride = resource.opt("riverOverride");
|
||||
if (rawOverride == JSONObject.NULL) {
|
||||
continue;
|
||||
}
|
||||
if (!(rawOverride instanceof JSONObject override)) {
|
||||
errors.add(path + " must be an object or null.");
|
||||
continue;
|
||||
}
|
||||
validateOverride(packFolder, path, key, resourceType, override, contexts, errors, warnings);
|
||||
}
|
||||
}
|
||||
|
||||
private static void validateOverride(File packFolder, String path, String resourceKey, String resourceType,
|
||||
JSONObject override, List<DimensionRiverContext> contexts,
|
||||
List<String> errors, List<String> warnings) {
|
||||
PackJsonFieldChecks.validateOptionalBoolean(path, override, "allowSources", errors);
|
||||
PackJsonFieldChecks.validateOptionalEnum(path, override, "routingPolicy", ROUTING_POLICIES, errors);
|
||||
PackJsonFieldChecks.validateOptionalDoubleRange(path, override, "routingCostMultiplier", 0D, 64D, errors);
|
||||
PackJsonFieldChecks.validateOptionalDoubleRange(path, override, "widthMultiplier", 0.0001D, 16D, errors);
|
||||
PackJsonFieldChecks.validateOptionalDoubleRange(path, override, "bankWidthMultiplier", 0D, 16D, errors);
|
||||
PackJsonFieldChecks.validateOptionalDoubleRange(path, override, "depthMultiplier", 0.0001D, 16D, errors);
|
||||
PackJsonFieldChecks.validateOptionalDoubleRange(path, override, "maxIncisionMultiplier", 0D, 16D, errors);
|
||||
PackJsonFieldChecks.validateOptionalDoubleRange(path, override, "continuationChanceMultiplier", 0D, 16D, errors);
|
||||
PackJsonFieldChecks.validateOptionalDoubleRange(path, override, "caveEntryMultiplier", 0D, 16D, errors);
|
||||
PackJsonFieldChecks.validateOptionalEnum(path, override, "terminalMode", TERMINAL_MODES, errors);
|
||||
validateBiomePool(packFolder, path, override, "channelBiomes", RiverBiomeRole.CHANNEL, true, errors, warnings);
|
||||
validateBiomePool(packFolder, path, override, "bankBiomes", RiverBiomeRole.BANK, true, errors, warnings);
|
||||
validateBiomePool(packFolder, path, override, "mouthBiomes", RiverBiomeRole.MOUTH, true, errors, warnings);
|
||||
validateBiomePool(packFolder, path, override, "dryBiomes", RiverBiomeRole.DRY, true, errors, warnings);
|
||||
validateBiomePool(packFolder, path, override, "floodedCaveBiomes", RiverBiomeRole.FLOODED_CAVE, true,
|
||||
errors, warnings);
|
||||
if ("SINKHOLE_GROTTO".equals(stringValue(override, "terminalMode", null))) {
|
||||
validateOverrideSinkhole(
|
||||
packFolder,
|
||||
path + ".terminalMode",
|
||||
resourceKey,
|
||||
resourceType,
|
||||
contexts,
|
||||
errors,
|
||||
warnings
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private static void validateBiomePools(File packFolder, String path, JSONObject biomes, boolean allowNull,
|
||||
List<String> errors, List<String> warnings) {
|
||||
validateStyle(packFolder, biomes, "selectionStyle", path, errors);
|
||||
validateBiomePool(packFolder, path, biomes, "channel", RiverBiomeRole.CHANNEL, allowNull, errors, warnings);
|
||||
validateBiomePool(packFolder, path, biomes, "bank", RiverBiomeRole.BANK, allowNull, errors, warnings);
|
||||
validateBiomePool(packFolder, path, biomes, "mouth", RiverBiomeRole.MOUTH, allowNull, errors, warnings);
|
||||
validateBiomePool(packFolder, path, biomes, "dry", RiverBiomeRole.DRY, allowNull, errors, warnings);
|
||||
validateBiomePool(packFolder, path, biomes, "floodedCave", RiverBiomeRole.FLOODED_CAVE, allowNull,
|
||||
errors, warnings);
|
||||
}
|
||||
|
||||
private static void validateBiomePool(File packFolder, String path, JSONObject owner, String field,
|
||||
RiverBiomeRole role, boolean allowNull,
|
||||
List<String> errors, List<String> warnings) {
|
||||
if (!owner.has(field)) {
|
||||
return;
|
||||
}
|
||||
Object rawPool = owner.opt(field);
|
||||
if (rawPool == JSONObject.NULL && allowNull) {
|
||||
return;
|
||||
}
|
||||
if (!(rawPool instanceof JSONArray pool)) {
|
||||
errors.add(path + "." + field + " must be an array" + (allowNull ? " or null" : "") + ".");
|
||||
return;
|
||||
}
|
||||
|
||||
Set<String> seen = new HashSet<>();
|
||||
File biomesFolder = new File(packFolder, "biomes");
|
||||
for (int index = 0; index < pool.length(); index++) {
|
||||
Object rawKey = pool.opt(index);
|
||||
String entryPath = path + "." + field + "[" + index + "]";
|
||||
if (!(rawKey instanceof String key) || key.isBlank()) {
|
||||
errors.add(entryPath + " must name a biome resource.");
|
||||
continue;
|
||||
}
|
||||
if (!seen.add(key)) {
|
||||
warnings.add(entryPath + " duplicates biome '" + key + "' in the same river pool.");
|
||||
continue;
|
||||
}
|
||||
File biomeFile = new File(biomesFolder, key + ".json");
|
||||
if (!biomeFile.isFile()) {
|
||||
errors.add(entryPath + " references missing biome '" + key + "'.");
|
||||
continue;
|
||||
}
|
||||
validateBiomeSuitability(entryPath, key, role, PackValidationIo.readJson(biomeFile), warnings);
|
||||
}
|
||||
}
|
||||
|
||||
private static void validateBiomeSuitability(String path, String biomeKey, RiverBiomeRole role,
|
||||
JSONObject biome, List<String> warnings) {
|
||||
if (biome == null || role == RiverBiomeRole.DRY || role == RiverBiomeRole.FLOODED_CAVE) {
|
||||
return;
|
||||
}
|
||||
String derivative = stringValue(biome, "vanillaDerivative", null);
|
||||
if (derivative == null || derivative.isBlank()) {
|
||||
derivative = stringValue(biome, "derivative", "minecraft:the_void");
|
||||
}
|
||||
String normalized = derivative.indexOf(':') >= 0 ? derivative : "minecraft:" + derivative;
|
||||
if (!normalized.startsWith("minecraft:")) {
|
||||
return;
|
||||
}
|
||||
boolean suitable = switch (role) {
|
||||
case CHANNEL, MOUTH -> normalized.contains("ocean") || normalized.endsWith("river");
|
||||
case BANK -> normalized.endsWith("beach") || normalized.endsWith("shore");
|
||||
default -> true;
|
||||
};
|
||||
if (!suitable) {
|
||||
warnings.add(path + " assigns biome '" + biomeKey + "' the inferred river role " + role.label
|
||||
+ " but its vanilla derivative '" + normalized
|
||||
+ "' does not match that role; native structure selection will use Iris's safe role fallback.");
|
||||
}
|
||||
}
|
||||
|
||||
private static void validateNoiseChance(File packFolder, JSONObject owner, String field, String path,
|
||||
List<String> errors) {
|
||||
if (!owner.has(field)) {
|
||||
return;
|
||||
}
|
||||
JSONObject chance = requireObject(owner, field, path + "." + field, errors);
|
||||
if (chance == null) {
|
||||
return;
|
||||
}
|
||||
String chancePath = path + "." + field;
|
||||
PackJsonFieldChecks.validateOptionalDoubleRange(chancePath, chance, "chance", 0D, 1D, errors);
|
||||
PackJsonFieldChecks.validateOptionalDoubleRange(chancePath, chance, "influence", 0D, 1D, errors);
|
||||
validateStyle(packFolder, chance, "style", chancePath, errors);
|
||||
}
|
||||
|
||||
private static void validateStyledRange(File packFolder, JSONObject owner, String field, String path,
|
||||
double minimum, double maximum,
|
||||
List<String> errors, List<String> warnings) {
|
||||
if (!owner.has(field)) {
|
||||
return;
|
||||
}
|
||||
String rangePath = path + "." + field;
|
||||
JSONObject range = resolveObject(packFolder, owner.opt(field), "snippet/style-range/", rangePath, errors);
|
||||
if (range == null) {
|
||||
return;
|
||||
}
|
||||
boolean hasMinimum = range.has("min") && range.opt("min") != JSONObject.NULL;
|
||||
boolean hasMaximum = range.has("max") && range.opt("max") != JSONObject.NULL;
|
||||
if (!hasMinimum && !hasMaximum) {
|
||||
errors.add(rangePath + " must set min and max explicitly.");
|
||||
} else if (!hasMinimum || !hasMaximum) {
|
||||
warnings.add(rangePath
|
||||
+ " should set both min and max explicitly; the omitted bound uses the shared style-range default.");
|
||||
}
|
||||
PackJsonFieldChecks.validateOptionalDoubleRange(rangePath, range, "min", minimum, maximum, errors);
|
||||
PackJsonFieldChecks.validateOptionalDoubleRange(rangePath, range, "max", minimum, maximum, errors);
|
||||
double minimumValue = doubleValue(range, "min", 16D);
|
||||
double maximumValue = doubleValue(range, "max", 32D);
|
||||
if (Double.isFinite(minimumValue) && Double.isFinite(maximumValue) && minimumValue > maximumValue) {
|
||||
errors.add(rangePath + ".min must not exceed " + rangePath + ".max.");
|
||||
}
|
||||
validateStyle(packFolder, range, "style", rangePath, errors);
|
||||
}
|
||||
|
||||
private static void validateStyle(File packFolder, JSONObject owner, String field, String path,
|
||||
List<String> errors) {
|
||||
if (!owner.has(field)) {
|
||||
return;
|
||||
}
|
||||
validateStyle(packFolder, owner.opt(field), path + "." + field, errors, new HashSet<>());
|
||||
}
|
||||
|
||||
private static void validateStyle(File packFolder, Object rawStyle, String path,
|
||||
List<String> errors, Set<String> dependencyStack) {
|
||||
String styleMarker = rawStyle instanceof String reference ? "style:" + reference : null;
|
||||
if (styleMarker != null && !dependencyStack.add(styleMarker)) {
|
||||
errors.add(path + " has a cyclic river-noise style snippet dependency.");
|
||||
return;
|
||||
}
|
||||
try {
|
||||
JSONObject style = resolveObject(packFolder, rawStyle, "snippet/style/", path, errors);
|
||||
if (style != null) {
|
||||
validateResolvedStyle(packFolder, style, path, errors, dependencyStack);
|
||||
}
|
||||
} finally {
|
||||
if (styleMarker != null) {
|
||||
dependencyStack.remove(styleMarker);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void validateResolvedStyle(File packFolder, JSONObject style, String path,
|
||||
List<String> errors, Set<String> dependencyStack) {
|
||||
PackJsonFieldChecks.validateOptionalEnum(path, style, "style", NOISE_STYLES, errors);
|
||||
PackJsonFieldChecks.validateOptionalDoubleRange(path, style, "cellularFrequency", 0D, Double.MAX_VALUE, errors);
|
||||
PackJsonFieldChecks.validateOptionalDoubleRange(path, style, "cellularZoom", 0.00001D, Double.MAX_VALUE, errors);
|
||||
PackJsonFieldChecks.validateOptionalDoubleRange(path, style, "zoom", 0.00001D, Double.MAX_VALUE, errors);
|
||||
PackJsonFieldChecks.validateOptionalDoubleRange(path, style, "multiplier",
|
||||
-Double.MAX_VALUE, Double.MAX_VALUE, errors);
|
||||
PackJsonFieldChecks.validateOptionalDoubleRange(path, style, "exponent", 0.01562D, 64D, errors);
|
||||
PackJsonFieldChecks.validateOptionalIntegerRange(path, style, "cacheSize", 0, 8192, errors);
|
||||
|
||||
if (style.has("expression") && style.opt("expression") != JSONObject.NULL) {
|
||||
Object rawExpression = style.opt("expression");
|
||||
if (!(rawExpression instanceof String expressionKey) || expressionKey.isBlank()) {
|
||||
errors.add(path + ".expression must name an expression resource.");
|
||||
} else {
|
||||
validateExpression(packFolder, expressionKey, path, errors, dependencyStack);
|
||||
}
|
||||
}
|
||||
if (style.has("fracture") && style.opt("fracture") != JSONObject.NULL) {
|
||||
validateStyle(packFolder, style.opt("fracture"), path + ".fracture", errors, dependencyStack);
|
||||
}
|
||||
}
|
||||
|
||||
private static void validateExpression(File packFolder, String expressionKey, String usePath,
|
||||
List<String> errors, Set<String> dependencyStack) {
|
||||
String expressionMarker = "expression:" + expressionKey;
|
||||
if (!dependencyStack.add(expressionMarker)) {
|
||||
errors.add(usePath + " has a cyclic river-noise expression dependency through '" + expressionKey + "'.");
|
||||
return;
|
||||
}
|
||||
try {
|
||||
File expressionFile = new File(new File(packFolder, "expressions"), expressionKey + ".json");
|
||||
JSONObject expression = PackValidationIo.readJson(expressionFile);
|
||||
if (expression == null) {
|
||||
return;
|
||||
}
|
||||
scanExpressionEntries(packFolder, expression, "variables", expressionKey, usePath, errors, dependencyStack);
|
||||
scanExpressionEntries(packFolder, expression, "functions", expressionKey, usePath, errors, dependencyStack);
|
||||
} finally {
|
||||
dependencyStack.remove(expressionMarker);
|
||||
}
|
||||
}
|
||||
|
||||
private static void scanExpressionEntries(File packFolder, JSONObject expression, String field,
|
||||
String expressionKey, String usePath,
|
||||
List<String> errors, Set<String> dependencyStack) {
|
||||
JSONArray entries = expression.optJSONArray(field);
|
||||
if (entries == null) {
|
||||
return;
|
||||
}
|
||||
for (int index = 0; index < entries.length(); index++) {
|
||||
Object rawEntry = entries.opt(index);
|
||||
String snippetFolder = "variables".equals(field)
|
||||
? "snippet/expression-load/"
|
||||
: "snippet/expression-function/";
|
||||
JSONObject entry = resolveExpressionEntry(packFolder, rawEntry, snippetFolder);
|
||||
if (entry == null) {
|
||||
continue;
|
||||
}
|
||||
String stream = stringValue(entry, "engineStreamValue", null);
|
||||
if (stream != null && isUnsafeRiverStream(stream)) {
|
||||
errors.add(usePath + " uses expression '" + expressionKey + "' " + field + "[" + index
|
||||
+ "].engineStreamValue '" + stream
|
||||
+ "', which depends on final river-shaped terrain and would recurse during river generation.");
|
||||
}
|
||||
if (entry.has("styleValue")) {
|
||||
validateStyle(packFolder, entry.opt("styleValue"), usePath + " -> expression '" + expressionKey
|
||||
+ "' " + field + "[" + index + "].styleValue", errors, dependencyStack);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static JSONObject resolveExpressionEntry(File packFolder, Object rawEntry, String snippetFolder) {
|
||||
if (rawEntry instanceof JSONObject entry) {
|
||||
return entry;
|
||||
}
|
||||
if (!(rawEntry instanceof String reference) || !reference.startsWith("snippet/")) {
|
||||
return null;
|
||||
}
|
||||
String resolved = reference.startsWith(snippetFolder)
|
||||
? reference
|
||||
: snippetFolder + reference.substring("snippet/".length());
|
||||
return PackValidationIo.readJson(new File(packFolder, resolved + ".json"));
|
||||
}
|
||||
|
||||
private static boolean isUnsafeRiverStream(String stream) {
|
||||
return UNSAFE_RIVER_STREAMS.contains(stream) || stream.startsWith("RIVER_");
|
||||
}
|
||||
|
||||
private static Set<String> noiseStyles() {
|
||||
Set<String> styles = new HashSet<>();
|
||||
for (NoiseStyle style : NoiseStyle.values()) {
|
||||
styles.add(style.name());
|
||||
}
|
||||
return Set.copyOf(styles);
|
||||
}
|
||||
|
||||
private static JSONObject nestedObject(JSONObject owner, String field, String path, List<String> errors) {
|
||||
if (!owner.has(field)) {
|
||||
return new JSONObject();
|
||||
}
|
||||
return requireObject(owner, field, path + "." + field, errors);
|
||||
}
|
||||
|
||||
private static JSONObject requireObject(JSONObject owner, String field, String path, List<String> errors) {
|
||||
Object raw = owner.opt(field);
|
||||
if (!(raw instanceof JSONObject object)) {
|
||||
errors.add(path + " must be an object.");
|
||||
return null;
|
||||
}
|
||||
return object;
|
||||
}
|
||||
|
||||
private static JSONObject resolveObject(File packFolder, Object raw, String snippetFolder,
|
||||
String path, List<String> errors) {
|
||||
if (raw instanceof JSONObject object) {
|
||||
return object;
|
||||
}
|
||||
if (raw instanceof String reference && reference.startsWith("snippet/")) {
|
||||
String resolved = reference.startsWith(snippetFolder)
|
||||
? reference
|
||||
: snippetFolder + reference.substring("snippet/".length());
|
||||
return PackValidationIo.readJson(new File(packFolder, resolved + ".json"));
|
||||
}
|
||||
errors.add(path + " must be an object or snippet reference.");
|
||||
return null;
|
||||
}
|
||||
|
||||
private static boolean booleanValue(JSONObject object, String field, boolean defaultValue) {
|
||||
Object raw = object.opt(field);
|
||||
return raw instanceof Boolean value ? value : defaultValue;
|
||||
}
|
||||
|
||||
private static int integerValue(JSONObject object, String field, int defaultValue) {
|
||||
Object raw = object.opt(field);
|
||||
if (!(raw instanceof Number number) || !Double.isFinite(number.doubleValue())) {
|
||||
return defaultValue;
|
||||
}
|
||||
return number.intValue();
|
||||
}
|
||||
|
||||
private static double doubleValue(JSONObject object, String field, double defaultValue) {
|
||||
Object raw = object.opt(field);
|
||||
return raw instanceof Number number ? number.doubleValue() : defaultValue;
|
||||
}
|
||||
|
||||
private static String stringValue(JSONObject object, String field, String defaultValue) {
|
||||
Object raw = object.opt(field);
|
||||
return raw instanceof String value ? value : defaultValue;
|
||||
}
|
||||
|
||||
private static double noiseChanceValue(JSONObject owner, String field, double defaultValue) {
|
||||
JSONObject chance = owner.optJSONObject(field);
|
||||
return chance == null ? defaultValue : doubleValue(chance, "chance", defaultValue);
|
||||
}
|
||||
|
||||
record Validation(List<String> errors, List<String> warnings) {
|
||||
Validation {
|
||||
errors = List.copyOf(errors);
|
||||
warnings = List.copyOf(warnings);
|
||||
}
|
||||
}
|
||||
|
||||
private record DimensionRiverContext(
|
||||
String dimensionKey,
|
||||
JSONObject dimension,
|
||||
JSONObject rivers,
|
||||
Set<String> regionKeys
|
||||
) {
|
||||
}
|
||||
|
||||
private enum RiverBiomeRole {
|
||||
CHANNEL("SEA"),
|
||||
BANK("SHORE"),
|
||||
MOUTH("SEA"),
|
||||
DRY("LAND"),
|
||||
FLOODED_CAVE("CAVE");
|
||||
|
||||
private final String label;
|
||||
|
||||
RiverBiomeRole(String label) {
|
||||
this.label = label;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -76,6 +76,9 @@ public final class PackValidator {
|
||||
}
|
||||
|
||||
PackDimensionValidator.validateDimensions(packFolder, dimensionFiles, blockingErrors, warnings);
|
||||
PackRiverValidator.Validation riverValidation = PackRiverValidator.validate(packFolder, dimensionFiles);
|
||||
addDistinct(blockingErrors, riverValidation.errors());
|
||||
addDistinct(warnings, riverValidation.warnings());
|
||||
blockingErrors.addAll(PackCaveProfileValidator.validateLegacyFields(packFolder));
|
||||
PackLootValidator.LootGraphIssues lootIssues = PackLootValidator.validateLootGraph(packFolder);
|
||||
addDistinct(blockingErrors, lootIssues.errors());
|
||||
|
||||
@@ -16,7 +16,6 @@ import art.arcane.iris.core.tools.IrisCreator;
|
||||
import art.arcane.iris.core.tools.IrisToolbelt;
|
||||
import art.arcane.iris.engine.platform.BukkitChunkGenerator;
|
||||
import art.arcane.iris.engine.platform.PlatformChunkGenerator;
|
||||
import art.arcane.iris.platform.bukkit.BukkitPlatform;
|
||||
import art.arcane.iris.util.common.plugin.VolmitSender;
|
||||
import art.arcane.iris.util.common.scheduling.J;
|
||||
import art.arcane.volmlib.util.exceptions.IrisException;
|
||||
@@ -55,6 +54,7 @@ import java.util.function.Supplier;
|
||||
public final class StudioOpenCoordinator {
|
||||
private static final long STUDIO_CLOSE_TIMEOUT_SECONDS = 120L;
|
||||
private static final long STUDIO_ENTRY_LOAD_TIMEOUT_SECONDS = 30L;
|
||||
private static final long STUDIO_ENTRY_RELEASE_TIMEOUT_SECONDS = 15L;
|
||||
private static final long STUDIO_STRUCTURE_ACTIVATION_TIMEOUT_SECONDS = 30L;
|
||||
private static final long STUDIO_ENTRY_CLEANUP_BOUNDARY_SECONDS = 120L;
|
||||
private static volatile StudioOpenCoordinator instance;
|
||||
@@ -103,10 +103,115 @@ public final class StudioOpenCoordinator {
|
||||
return closeWorldCoordinated(provider, worldName, world, true, project);
|
||||
}
|
||||
|
||||
public CompletableFuture<Boolean> teleportPlayerToProject(
|
||||
IrisProject project,
|
||||
Player player,
|
||||
AtomicBoolean admission,
|
||||
long deadlineNanos
|
||||
) {
|
||||
if (project == null || player == null) {
|
||||
return CompletableFuture.completedFuture(false);
|
||||
}
|
||||
AtomicBoolean activeAdmission = Objects.requireNonNull(
|
||||
admission,
|
||||
"Studio teleport admission");
|
||||
if (!isStudioTeleportAdmitted(activeAdmission, deadlineNanos)) {
|
||||
return studioTeleportDeadlineFailure("entry loading");
|
||||
}
|
||||
PlatformChunkGenerator provider = project.getActiveProvider();
|
||||
if (provider == null) {
|
||||
return CompletableFuture.failedFuture(new IllegalStateException(
|
||||
"Studio runtime provider is unavailable."));
|
||||
}
|
||||
World world = BukkitWorldBinding.world(provider.getTarget().getWorld());
|
||||
if (world == null) {
|
||||
return CompletableFuture.failedFuture(new IllegalStateException(
|
||||
"Studio world is not loaded."));
|
||||
}
|
||||
Location entryAnchor = WorldRuntimeControlService.get().resolveEntryAnchor(world);
|
||||
if (entryAnchor == null) {
|
||||
return CompletableFuture.failedFuture(new IllegalStateException(
|
||||
"Studio entry anchor could not be resolved."));
|
||||
}
|
||||
EntryChunkResolution entryResolution = loadEntryChunk(world, entryAnchor);
|
||||
CompletableFuture<Boolean> teleportOperation = beforeStudioTeleportDeadline(
|
||||
entryResolution.chunk(),
|
||||
activeAdmission,
|
||||
deadlineNanos,
|
||||
"entry loading")
|
||||
.thenCompose(ignored -> {
|
||||
if (!isStudioTeleportAdmitted(activeAdmission, deadlineNanos)) {
|
||||
return studioTeleportDeadlineFailure("safe-entry resolution");
|
||||
}
|
||||
return beforeStudioTeleportDeadline(
|
||||
entryResolution.safeEntry(),
|
||||
activeAdmission,
|
||||
deadlineNanos,
|
||||
"safe-entry resolution");
|
||||
})
|
||||
.thenCompose(entry -> {
|
||||
if (entry == null) {
|
||||
return CompletableFuture.failedFuture(new IllegalStateException(
|
||||
"Studio entry point could not be resolved."));
|
||||
}
|
||||
if (System.nanoTime() >= deadlineNanos
|
||||
|| !activeAdmission.compareAndSet(true, false)) {
|
||||
return studioTeleportDeadlineFailure("native teleport delegation");
|
||||
}
|
||||
CompletableFuture<Boolean> teleport =
|
||||
WorldRuntimeControlService.get().teleport(player, entry);
|
||||
if (teleport == null) {
|
||||
return CompletableFuture.failedFuture(new IllegalStateException(
|
||||
"Studio native teleport returned no completion future."));
|
||||
}
|
||||
return teleport;
|
||||
});
|
||||
return teleportOperation;
|
||||
}
|
||||
|
||||
private <T> CompletableFuture<T> beforeStudioTeleportDeadline(
|
||||
CompletableFuture<T> stage,
|
||||
AtomicBoolean admission,
|
||||
long deadlineNanos,
|
||||
String stageName
|
||||
) {
|
||||
if (stage == null) {
|
||||
return CompletableFuture.failedFuture(new IllegalStateException(
|
||||
"Studio " + stageName + " returned no completion future."));
|
||||
}
|
||||
long remainingNanos = deadlineNanos - System.nanoTime();
|
||||
if (!admission.get() || remainingNanos <= 0L) {
|
||||
return studioTeleportDeadlineFailure(stageName);
|
||||
}
|
||||
CompletableFuture<T> bounded = new CompletableFuture<>();
|
||||
stage.whenComplete((value, failure) -> {
|
||||
if (failure == null) {
|
||||
bounded.complete(value);
|
||||
} else {
|
||||
bounded.completeExceptionally(failure);
|
||||
}
|
||||
});
|
||||
CompletableFuture.delayedExecutor(remainingNanos, TimeUnit.NANOSECONDS)
|
||||
.execute(() -> bounded.completeExceptionally(new TimeoutException(
|
||||
"Studio teleport deadline expired during " + stageName + ".")));
|
||||
return bounded;
|
||||
}
|
||||
|
||||
private boolean isStudioTeleportAdmitted(AtomicBoolean admission, long deadlineNanos) {
|
||||
return admission.get() && System.nanoTime() < deadlineNanos;
|
||||
}
|
||||
|
||||
private <T> CompletableFuture<T> studioTeleportDeadlineFailure(String stageName) {
|
||||
return CompletableFuture.failedFuture(new TimeoutException(
|
||||
"Studio teleport deadline expired before " + stageName + "."));
|
||||
}
|
||||
|
||||
private void executeOpen(StudioOpenRequest request, CompletableFuture<StudioOpenResult> future) {
|
||||
World world = null;
|
||||
PlatformChunkGenerator provider = null;
|
||||
CompletableFuture<Void> entryLoadFuture = null;
|
||||
CompletableFuture<Void> entryUseFuture = null;
|
||||
CompletableFuture<Boolean> nativeTeleportFuture = null;
|
||||
try {
|
||||
long openStart = System.nanoTime();
|
||||
long t = openStart;
|
||||
@@ -158,35 +263,75 @@ public final class StudioOpenCoordinator {
|
||||
}
|
||||
t = logStudioPhase(request, "resolve_entry_anchor", t, openStart);
|
||||
|
||||
updateStage(request, "load_entry_chunk", 0.80D);
|
||||
int entryChunkX = entryAnchor.getBlockX() >> 4;
|
||||
int entryChunkZ = entryAnchor.getBlockZ() >> 4;
|
||||
try {
|
||||
entryLoadFuture = loadEntryChunk(world, entryChunkX, entryChunkZ);
|
||||
entryLoads.register(request.worldName(), entryLoadFuture);
|
||||
entryLoadFuture.get(STUDIO_ENTRY_LOAD_TIMEOUT_SECONDS, TimeUnit.SECONDS);
|
||||
} catch (TimeoutException e) {
|
||||
throw new IllegalStateException("Studio entry chunk did not load in time at "
|
||||
+ entryChunkX + "," + entryChunkZ + " — chunk system may be stalled.", e);
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
throw new IllegalStateException("Studio entry chunk load was interrupted at "
|
||||
+ entryChunkX + "," + entryChunkZ + ".", e);
|
||||
long entryPrecomputeStartedAt = System.nanoTime();
|
||||
CompletableFuture<Void> preparedEntryChunks = CompletableFuture.completedFuture(null);
|
||||
if (requiresLoadedEntry(request)) {
|
||||
if (!(provider instanceof BukkitChunkGenerator bukkitGenerator)) {
|
||||
throw new IllegalStateException(
|
||||
"Studio runtime provider cannot prepare its entry chunks.");
|
||||
}
|
||||
preparedEntryChunks = bukkitGenerator.prepareStudioEntryChunks(
|
||||
world,
|
||||
entryAnchor.getBlockX() >> 4,
|
||||
entryAnchor.getBlockZ() >> 4
|
||||
);
|
||||
}
|
||||
t = logStudioPhase(request, "load_entry_chunk", t, openStart);
|
||||
|
||||
updateStage(request, "resolve_safe_entry", 0.84D);
|
||||
Location safeEntry;
|
||||
try {
|
||||
safeEntry = WorldRuntimeControlService.get().resolveSafeEntry(world, entryAnchor)
|
||||
.get(5L, TimeUnit.SECONDS);
|
||||
} catch (TimeoutException e) {
|
||||
throw new IllegalStateException("Studio entry point resolution timed out — region thread may be stalled.");
|
||||
updateStage(request, "prepare_structure_rings", 0.79D);
|
||||
endStudioEntryBootstrap(world, provider);
|
||||
t = logStudioPhase(request, "prepare_structure_rings", t, openStart);
|
||||
|
||||
if (requiresLoadedEntry(request)) {
|
||||
try {
|
||||
preparedEntryChunks.get(
|
||||
STUDIO_ENTRY_LOAD_TIMEOUT_SECONDS,
|
||||
TimeUnit.SECONDS
|
||||
);
|
||||
} catch (TimeoutException e) {
|
||||
throw new IllegalStateException(
|
||||
"Studio entry chunk precompute did not finish in time.", e);
|
||||
}
|
||||
t = logOverlappedStudioPhase(
|
||||
request,
|
||||
"prepare_entry_chunks",
|
||||
entryPrecomputeStartedAt,
|
||||
openStart
|
||||
);
|
||||
}
|
||||
if (safeEntry == null) {
|
||||
throw new IllegalStateException("Studio entry point could not be resolved for world \"" + request.worldName() + "\".");
|
||||
|
||||
Location safeEntry = entryAnchor;
|
||||
if (requiresLoadedEntry(request)) {
|
||||
updateStage(request, "load_entry_chunk", 0.80D);
|
||||
int entryChunkX = entryAnchor.getBlockX() >> 4;
|
||||
int entryChunkZ = entryAnchor.getBlockZ() >> 4;
|
||||
EntryChunkResolution entryResolution = loadEntryChunk(world, entryAnchor);
|
||||
CompletableFuture<Void> useSettlement = new CompletableFuture<>();
|
||||
entryUseFuture = useSettlement;
|
||||
entryLoadFuture = entryResolution.safeEntry().thenCompose(ignored -> useSettlement);
|
||||
entryLoads.register(request.worldName(), entryLoadFuture);
|
||||
try {
|
||||
entryResolution.chunk().get(STUDIO_ENTRY_LOAD_TIMEOUT_SECONDS, TimeUnit.SECONDS);
|
||||
} catch (TimeoutException e) {
|
||||
throw new IllegalStateException("Studio entry chunk did not load in time at "
|
||||
+ entryChunkX + "," + entryChunkZ + " — chunk system may be stalled.", e);
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
throw new IllegalStateException("Studio entry chunk load was interrupted at "
|
||||
+ entryChunkX + "," + entryChunkZ + ".", e);
|
||||
}
|
||||
t = logStudioPhase(request, "load_entry_chunk", t, openStart);
|
||||
|
||||
updateStage(request, "resolve_safe_entry", 0.84D);
|
||||
try {
|
||||
safeEntry = entryResolution.safeEntry().get(5L, TimeUnit.SECONDS);
|
||||
} catch (TimeoutException e) {
|
||||
throw new IllegalStateException("Studio entry point resolution timed out — region thread may be stalled.");
|
||||
}
|
||||
if (safeEntry == null) {
|
||||
throw new IllegalStateException("Studio entry point could not be resolved for world \"" + request.worldName() + "\".");
|
||||
}
|
||||
t = logStudioPhase(request, "resolve_safe_entry", t, openStart);
|
||||
}
|
||||
t = logStudioPhase(request, "resolve_safe_entry", t, openStart);
|
||||
|
||||
if (request.openKind().teleportThroughStandardEntry()
|
||||
&& request.playerName() != null
|
||||
@@ -199,7 +344,12 @@ public final class StudioOpenCoordinator {
|
||||
|
||||
Boolean teleported;
|
||||
try {
|
||||
teleported = WorldRuntimeControlService.get().teleport(player, safeEntry).get(60L, TimeUnit.SECONDS);
|
||||
nativeTeleportFuture = WorldRuntimeControlService.get().teleport(player, safeEntry);
|
||||
if (nativeTeleportFuture == null) {
|
||||
throw new IllegalStateException(
|
||||
"Studio native teleport returned no completion future.");
|
||||
}
|
||||
teleported = nativeTeleportFuture.get(60L, TimeUnit.SECONDS);
|
||||
} catch (TimeoutException e) {
|
||||
throw new IllegalStateException("Studio teleport timed out — destination region may still be generating.");
|
||||
}
|
||||
@@ -209,8 +359,6 @@ public final class StudioOpenCoordinator {
|
||||
t = logStudioPhase(request, "teleport_standard_entry", t, openStart);
|
||||
}
|
||||
|
||||
endStudioEntryBootstrap(world, provider);
|
||||
|
||||
updateStage(request, "finalize_open", 1.00D);
|
||||
if (request.project() != null) {
|
||||
request.project().setActiveProvider(provider);
|
||||
@@ -221,11 +369,17 @@ public final class StudioOpenCoordinator {
|
||||
runOpenFinalizer(request.onDone(), world);
|
||||
t = logStudioPhase(request, "finalize_open", t, openStart);
|
||||
|
||||
settleEntryUseAfterOperation(entryUseFuture, nativeTeleportFuture);
|
||||
if (entryLoadFuture != null) {
|
||||
entryLoadFuture.get(STUDIO_ENTRY_RELEASE_TIMEOUT_SECONDS, TimeUnit.SECONDS);
|
||||
}
|
||||
|
||||
IrisLogging.info("Studio open: " + world.getName() + " ready in "
|
||||
+ elapsedMillis(openStart) + "ms");
|
||||
entryLoads.release(request.worldName(), entryLoadFuture);
|
||||
future.complete(new StudioOpenResult(world, safeEntry));
|
||||
} catch (Throwable e) {
|
||||
settleEntryUseAfterOperation(entryUseFuture, nativeTeleportFuture);
|
||||
abandonStudioEntryBootstrap(world, e);
|
||||
IrisLogging.reportError("Studio open failed for world \"" + request.worldName() + "\".", e);
|
||||
if (!request.retainOnFailure()) {
|
||||
@@ -262,11 +416,23 @@ public final class StudioOpenCoordinator {
|
||||
+ request.worldName() + "\".", unwrapFailure(cleanupError));
|
||||
}
|
||||
}
|
||||
} else if (entryLoadFuture != null) {
|
||||
entryLoads.releaseAfterSuccessfulCompletion(
|
||||
request.worldName(),
|
||||
entryLoadFuture,
|
||||
entryLoadFuture);
|
||||
}
|
||||
future.completeExceptionally(e);
|
||||
}
|
||||
}
|
||||
|
||||
static boolean requiresLoadedEntry(StudioOpenRequest request) {
|
||||
Objects.requireNonNull(request, "Studio open request");
|
||||
return request.openKind().teleportThroughStandardEntry()
|
||||
&& request.playerName() != null
|
||||
&& !request.playerName().isBlank();
|
||||
}
|
||||
|
||||
private void deferFailedOpenCleanupToRestart(
|
||||
PlatformChunkGenerator provider,
|
||||
String worldName,
|
||||
@@ -278,7 +444,12 @@ public final class StudioOpenCoordinator {
|
||||
worldName,
|
||||
new IllegalStateException("Studio cleanup deferred across the queued server restart."));
|
||||
}
|
||||
entryLoads.release(worldName, entryLoadFuture);
|
||||
if (entryLoadFuture != null) {
|
||||
entryLoads.releaseAfterSuccessfulCompletion(
|
||||
worldName,
|
||||
entryLoadFuture,
|
||||
entryLoadFuture);
|
||||
}
|
||||
}
|
||||
|
||||
private boolean transientWorldStorageExists(String worldName) {
|
||||
@@ -305,6 +476,22 @@ public final class StudioOpenCoordinator {
|
||||
return now;
|
||||
}
|
||||
|
||||
private long logOverlappedStudioPhase(
|
||||
StudioOpenRequest request,
|
||||
String phase,
|
||||
long phaseStart,
|
||||
long openStart
|
||||
) {
|
||||
long now = System.nanoTime();
|
||||
IrisLogging.info("[Studio timing] world=%s kind=%s phase=%s duration=%dms cumulative=%dms",
|
||||
request.worldName(),
|
||||
request.openKind().name().toLowerCase(Locale.ROOT),
|
||||
phase,
|
||||
TimeUnit.NANOSECONDS.toMillis(now - phaseStart),
|
||||
TimeUnit.NANOSECONDS.toMillis(now - openStart));
|
||||
return now;
|
||||
}
|
||||
|
||||
private void runOpenFinalizer(Consumer<World> finalizer, World world)
|
||||
throws InterruptedException, ExecutionException, TimeoutException {
|
||||
if (finalizer == null) {
|
||||
@@ -331,14 +518,12 @@ public final class StudioOpenCoordinator {
|
||||
duration);
|
||||
}
|
||||
|
||||
private CompletableFuture<Void> loadEntryChunk(World world, int chunkX, int chunkZ) {
|
||||
if (!J.isFolia()) {
|
||||
return loadEntryChunkAsync(world, chunkX, chunkZ);
|
||||
}
|
||||
return scheduleEntryChunkRetention(world, chunkX, chunkZ);
|
||||
}
|
||||
|
||||
private CompletableFuture<Void> loadEntryChunkAsync(World world, int chunkX, int chunkZ) {
|
||||
private EntryChunkResolution loadEntryChunk(World world, Location entryAnchor) {
|
||||
int chunkX = entryAnchor.getBlockX() >> 4;
|
||||
int chunkZ = entryAnchor.getBlockZ() >> 4;
|
||||
CompletableFuture<Chunk> chunkFuture = new CompletableFuture<>();
|
||||
CompletableFuture<Location> safeEntryFuture = new CompletableFuture<>();
|
||||
EntryChunkResolution resolution = new EntryChunkResolution(chunkFuture, safeEntryFuture);
|
||||
CompletableFuture<Chunk> requested;
|
||||
try {
|
||||
requested = WorldRuntimeControlService.get().requestChunkAsync(
|
||||
@@ -348,56 +533,67 @@ public final class StudioOpenCoordinator {
|
||||
true,
|
||||
true);
|
||||
} catch (Throwable throwable) {
|
||||
return CompletableFuture.failedFuture(throwable);
|
||||
failEntryResolution(resolution, throwable);
|
||||
return resolution;
|
||||
}
|
||||
if (requested == null) {
|
||||
return CompletableFuture.failedFuture(new IllegalStateException(
|
||||
failEntryResolution(resolution, new IllegalStateException(
|
||||
"Entry-chunk async request did not return a future at " + chunkX + "," + chunkZ + "."));
|
||||
return resolution;
|
||||
}
|
||||
return requested.thenCompose(chunk -> {
|
||||
if (chunk == null) {
|
||||
return CompletableFuture.failedFuture(new IllegalStateException(
|
||||
"Entry-chunk async request returned no chunk at " + chunkX + "," + chunkZ + "."));
|
||||
requested.whenComplete((chunk, failure) -> {
|
||||
if (failure != null) {
|
||||
failEntryResolution(resolution, failure);
|
||||
return;
|
||||
}
|
||||
if (chunk == null) {
|
||||
failEntryResolution(resolution, new IllegalStateException(
|
||||
"Entry-chunk async request returned no chunk at " + chunkX + "," + chunkZ + "."));
|
||||
return;
|
||||
}
|
||||
Runnable resolve = () -> {
|
||||
chunkFuture.complete(chunk);
|
||||
try {
|
||||
Location safeEntry = WorldRuntimeControlService.findTopSafeStudioLocation(world, entryAnchor);
|
||||
safeEntryFuture.complete(safeEntry);
|
||||
} catch (Throwable resolutionFailure) {
|
||||
failEntryResolution(resolution, resolutionFailure);
|
||||
}
|
||||
};
|
||||
try {
|
||||
if (J.isOwnedByCurrentRegion(world, chunkX, chunkZ)) {
|
||||
resolve.run();
|
||||
return;
|
||||
}
|
||||
if (!J.runRegion(world, chunkX, chunkZ, resolve)) {
|
||||
failEntryResolution(resolution, new IllegalStateException(
|
||||
"Failed to resolve the entry chunk on its owning region at "
|
||||
+ chunkX + "," + chunkZ + "."));
|
||||
}
|
||||
} catch (Throwable schedulingFailure) {
|
||||
failEntryResolution(resolution, schedulingFailure);
|
||||
}
|
||||
return scheduleEntryChunkRetention(world, chunkX, chunkZ);
|
||||
});
|
||||
return resolution;
|
||||
}
|
||||
|
||||
private CompletableFuture<Void> scheduleEntryChunkRetention(World world, int chunkX, int chunkZ) {
|
||||
CompletableFuture<Void> loaded = new CompletableFuture<>();
|
||||
try {
|
||||
J.s(() -> retainAndConfirmEntryChunk(world, chunkX, chunkZ)
|
||||
.whenComplete((ignored, throwable) -> complete(loaded, throwable)));
|
||||
} catch (Throwable throwable) {
|
||||
loaded.completeExceptionally(throwable);
|
||||
}
|
||||
return loaded;
|
||||
private void failEntryResolution(EntryChunkResolution resolution, Throwable failure) {
|
||||
resolution.chunk().completeExceptionally(failure);
|
||||
resolution.safeEntry().completeExceptionally(failure);
|
||||
}
|
||||
|
||||
private CompletableFuture<Void> retainAndConfirmEntryChunk(World world, int chunkX, int chunkZ) {
|
||||
CompletableFuture<Void> confirmed = new CompletableFuture<>();
|
||||
try {
|
||||
world.addPluginChunkTicket(
|
||||
chunkX,
|
||||
chunkZ,
|
||||
BukkitPlatform.plugin());
|
||||
} catch (Throwable throwable) {
|
||||
confirmed.completeExceptionally(throwable);
|
||||
return confirmed;
|
||||
}
|
||||
if (!J.runRegion(world, chunkX, chunkZ, () -> confirmed.complete(null))) {
|
||||
confirmed.completeExceptionally(new IllegalStateException(
|
||||
"Failed to confirm entry-chunk region at " + chunkX + "," + chunkZ + "."));
|
||||
}
|
||||
return confirmed;
|
||||
}
|
||||
|
||||
private void complete(CompletableFuture<Void> target, Throwable throwable) {
|
||||
if (throwable == null) {
|
||||
target.complete(null);
|
||||
private void settleEntryUseAfterOperation(
|
||||
CompletableFuture<Void> entryUseFuture,
|
||||
CompletableFuture<?> operation
|
||||
) {
|
||||
if (entryUseFuture == null) {
|
||||
return;
|
||||
}
|
||||
target.completeExceptionally(throwable);
|
||||
if (operation == null) {
|
||||
entryUseFuture.complete(null);
|
||||
return;
|
||||
}
|
||||
operation.whenComplete((ignored, failure) -> entryUseFuture.complete(null));
|
||||
}
|
||||
|
||||
private void endStudioEntryBootstrap(World world, PlatformChunkGenerator provider) {
|
||||
@@ -405,19 +601,28 @@ public final class StudioOpenCoordinator {
|
||||
throw new IllegalStateException("Studio runtime provider cannot finish its entry bootstrap.");
|
||||
}
|
||||
AtomicBoolean activationClaim = new AtomicBoolean(true);
|
||||
CompletableFuture<Void> activation = J.sfut(() -> {
|
||||
CompletableFuture<CompletableFuture<Void>> scheduledActivation = J.sfut(() -> {
|
||||
if (!activationClaim.compareAndSet(true, false)) {
|
||||
INMS.get().abandonStudioStructureBootstrap(world);
|
||||
return;
|
||||
return CompletableFuture.failedFuture(new IllegalStateException(
|
||||
"Studio native structure activation was cancelled before it began."));
|
||||
}
|
||||
try {
|
||||
INMS.get().completeStudioStructureBootstrap(world);
|
||||
bukkitGenerator.endStudioEntryBootstrap();
|
||||
CompletableFuture<Void> nativeActivation =
|
||||
INMS.get().completeStudioStructureBootstrap(world);
|
||||
if (nativeActivation == null) {
|
||||
return CompletableFuture.failedFuture(new IllegalStateException(
|
||||
"Studio native structure activation returned no completion future."));
|
||||
}
|
||||
return nativeActivation;
|
||||
} catch (ReflectiveOperationException e) {
|
||||
throw new IllegalStateException(
|
||||
"Studio native structure state could not be activated after entry bootstrap.", e);
|
||||
return CompletableFuture.failedFuture(new IllegalStateException(
|
||||
"Studio native structure state could not be activated after entry bootstrap.", e));
|
||||
}
|
||||
});
|
||||
CompletableFuture<Void> activation = scheduledActivation
|
||||
.thenCompose(nativeActivation -> nativeActivation)
|
||||
.thenCompose(ignored -> J.sfut(bukkitGenerator::endStudioEntryBootstrap));
|
||||
try {
|
||||
activation.get(STUDIO_STRUCTURE_ACTIVATION_TIMEOUT_SECONDS, TimeUnit.SECONDS);
|
||||
} catch (InterruptedException e) {
|
||||
@@ -910,7 +1115,7 @@ public final class StudioOpenCoordinator {
|
||||
};
|
||||
}
|
||||
|
||||
private Throwable unwrapFailure(Throwable throwable) {
|
||||
private static Throwable unwrapFailure(Throwable throwable) {
|
||||
Throwable cursor = throwable;
|
||||
while (cursor instanceof CompletionException || cursor instanceof ExecutionException) {
|
||||
if (cursor.getCause() == null) {
|
||||
@@ -1053,6 +1258,12 @@ public final class StudioOpenCoordinator {
|
||||
}
|
||||
}
|
||||
|
||||
private record EntryChunkResolution(
|
||||
CompletableFuture<Chunk> chunk,
|
||||
CompletableFuture<Location> safeEntry
|
||||
) {
|
||||
}
|
||||
|
||||
static final class EntryLoadRegistry {
|
||||
private final ConcurrentHashMap<String, CompletableFuture<?>> entryLoads;
|
||||
|
||||
|
||||
@@ -21,6 +21,7 @@ import org.bukkit.Material;
|
||||
import org.bukkit.World;
|
||||
import org.bukkit.block.Block;
|
||||
import org.bukkit.block.data.BlockData;
|
||||
import org.bukkit.block.data.Levelled;
|
||||
import org.bukkit.block.data.Waterlogged;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.event.world.TimeSkipEvent;
|
||||
@@ -430,6 +431,62 @@ public final class WorldRuntimeControlService {
|
||||
return null;
|
||||
}
|
||||
|
||||
static Location findTopSafeStudioLocation(World world, Location source) {
|
||||
Location dryLocation = findTopSafeLocation(world, source);
|
||||
if (dryLocation != null) {
|
||||
return dryLocation;
|
||||
}
|
||||
|
||||
int sourceX = source.getBlockX();
|
||||
int sourceZ = source.getBlockZ();
|
||||
int chunkX = sourceX >> 4;
|
||||
int chunkZ = sourceZ >> 4;
|
||||
if (!world.isChunkLoaded(chunkX, chunkZ)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
int minimumFloorY = world.getMinHeight();
|
||||
int maximumFloorY = world.getMaxHeight() - 3;
|
||||
if (minimumFloorY > maximumFloorY) {
|
||||
return null;
|
||||
}
|
||||
|
||||
int minimumX = chunkX << 4;
|
||||
int minimumZ = chunkZ << 4;
|
||||
int maximumX = minimumX + 15;
|
||||
int maximumZ = minimumZ + 15;
|
||||
for (int radius = 0; radius <= MAX_SAFE_ENTRY_HORIZONTAL_RADIUS; radius++) {
|
||||
for (int offsetX = -radius; offsetX <= radius; offsetX++) {
|
||||
for (int offsetZ = -radius; offsetZ <= radius; offsetZ++) {
|
||||
if (Math.max(Math.abs(offsetX), Math.abs(offsetZ)) != radius) {
|
||||
continue;
|
||||
}
|
||||
|
||||
int x = sourceX + offsetX;
|
||||
int z = sourceZ + offsetZ;
|
||||
if (x < minimumX || x > maximumX || z < minimumZ || z > maximumZ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
Location waterLocation = findSafeWaterSurfaceLocationInColumn(
|
||||
world,
|
||||
x,
|
||||
z,
|
||||
minimumFloorY,
|
||||
maximumFloorY,
|
||||
source.getYaw(),
|
||||
source.getPitch()
|
||||
);
|
||||
if (waterLocation != null) {
|
||||
return waterLocation;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private static Location findSafeLocationInColumn(
|
||||
World world,
|
||||
int x,
|
||||
@@ -458,6 +515,48 @@ public final class WorldRuntimeControlService {
|
||||
return null;
|
||||
}
|
||||
|
||||
private static Location findSafeWaterSurfaceLocationInColumn(
|
||||
World world,
|
||||
int x,
|
||||
int z,
|
||||
int minimumFloorY,
|
||||
int maximumFloorY,
|
||||
float yaw,
|
||||
float pitch
|
||||
) {
|
||||
int surfaceY = world.getHighestBlockYAt(x, z, HeightMap.MOTION_BLOCKING_NO_LEAVES);
|
||||
if (surfaceY <= minimumFloorY || surfaceY > maximumFloorY) {
|
||||
return null;
|
||||
}
|
||||
|
||||
Block surface = world.getBlockAt(x, surfaceY, z);
|
||||
if (!isStableWater(surface)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
Block support = world.getBlockAt(x, surfaceY - 1, z);
|
||||
if (!isStableWater(support) && !isSafeFloor(support)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
Block feet = world.getBlockAt(x, surfaceY + 1, z);
|
||||
Block head = world.getBlockAt(x, surfaceY + 2, z);
|
||||
if (!isClearEntryBlock(feet) || !isClearEntryBlock(head)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return new Location(world, x + BLOCK_CENTER, surfaceY + 1D, z + BLOCK_CENTER, yaw, pitch);
|
||||
}
|
||||
|
||||
private static boolean isStableWater(Block block) {
|
||||
if (block.getType() != Material.WATER || !block.isLiquid()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
BlockData blockData = block.getBlockData();
|
||||
return blockData instanceof Levelled levelled && levelled.getLevel() == 0;
|
||||
}
|
||||
|
||||
private static boolean isSafeFloor(Block block) {
|
||||
Material material = block.getType();
|
||||
if (material == null
|
||||
|
||||
@@ -59,6 +59,7 @@ import art.arcane.iris.util.common.parallel.MultiBurst;
|
||||
import art.arcane.iris.util.common.scheduling.J;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.World;
|
||||
import org.bukkit.entity.Player;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
@@ -82,6 +83,7 @@ import java.util.UUID;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.Future;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.function.Supplier;
|
||||
import java.util.regex.Pattern;
|
||||
@@ -93,6 +95,7 @@ import art.arcane.volmlib.util.localization.MessageArgument;
|
||||
public class StudioSVC implements IrisService {
|
||||
public static final String WORKSPACE_NAME = "packs";
|
||||
private static final long DOWNLOAD_SHUTDOWN_POLL_SECONDS = 15L;
|
||||
private static final long STUDIO_PLAYER_TELEPORT_TIMEOUT_SECONDS = 10L;
|
||||
private static final Pattern PROJECT_NAME = Pattern.compile("[a-z0-9_-]+");
|
||||
private static final AtomicCache<Integer> counter = new AtomicCache<>();
|
||||
private final StudioTransitionQueue studioTransitions = new StudioTransitionQueue();
|
||||
@@ -474,6 +477,28 @@ public class StudioSVC implements IrisService {
|
||||
return activeProject != null && activeProject.isOpen();
|
||||
}
|
||||
|
||||
public CompletableFuture<Boolean> teleportToActiveProject(Player player) {
|
||||
Player target = Objects.requireNonNull(player, "Studio teleport player");
|
||||
AtomicBoolean admission = new AtomicBoolean(true);
|
||||
long deadlineNanos = System.nanoTime()
|
||||
+ TimeUnit.SECONDS.toNanos(STUDIO_PLAYER_TELEPORT_TIMEOUT_SECONDS);
|
||||
CompletableFuture<Boolean> transition = studioTransitions.submit(() -> {
|
||||
IrisProject project = activeProject;
|
||||
if (project == null || !project.isOpen()) {
|
||||
return CompletableFuture.failedFuture(new IllegalStateException(
|
||||
"No active Studio project is available for teleport."));
|
||||
}
|
||||
return StudioOpenCoordinator.get().teleportPlayerToProject(
|
||||
project,
|
||||
target,
|
||||
admission,
|
||||
deadlineNanos);
|
||||
});
|
||||
transition.orTimeout(STUDIO_PLAYER_TELEPORT_TIMEOUT_SECONDS, TimeUnit.SECONDS);
|
||||
transition.whenComplete((ignored, failure) -> admission.set(false));
|
||||
return transition;
|
||||
}
|
||||
|
||||
public void open(VolmitSender sender, String dimm) {
|
||||
open(sender, 1337, dimm);
|
||||
}
|
||||
|
||||
@@ -207,7 +207,7 @@ public class TreeSVC implements IrisService {
|
||||
|
||||
@Override
|
||||
public int getFluidHeight() {
|
||||
return worldAccess.getEngine().getDimension().getFluidHeight();
|
||||
return worldFluidHeight(engine);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -290,6 +290,10 @@ public class TreeSVC implements IrisService {
|
||||
}
|
||||
}
|
||||
|
||||
static int worldFluidHeight(Engine engine) {
|
||||
return engine.getMinHeight() + engine.getDimension().getFluidHeight();
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds a single object placement (which may contain more than one object) for the requirements species, location &
|
||||
* size
|
||||
|
||||
@@ -29,7 +29,16 @@ import art.arcane.iris.engine.object.IrisDecorator;
|
||||
import art.arcane.iris.engine.object.IrisGenerator;
|
||||
import art.arcane.iris.engine.object.IrisInterpolator;
|
||||
import art.arcane.iris.engine.object.IrisRegion;
|
||||
import art.arcane.iris.engine.object.IrisRiverOverride;
|
||||
import art.arcane.iris.engine.object.IrisRiverRoutingPolicy;
|
||||
import art.arcane.iris.engine.object.IrisRiverWaterMode;
|
||||
import art.arcane.iris.engine.object.IrisShapedGeneratorStyle;
|
||||
import art.arcane.iris.engine.river.runtime.IrisRiverRuntime;
|
||||
import art.arcane.iris.engine.river.runtime.IrisRiverRuntimeContext;
|
||||
import art.arcane.iris.engine.river.runtime.IrisRiverSurfaceSample;
|
||||
import art.arcane.iris.engine.river.RiverRouteState;
|
||||
import art.arcane.iris.engine.river.RiverSample;
|
||||
import art.arcane.iris.engine.river.RiverSection;
|
||||
import art.arcane.iris.spi.IrisPlatforms;
|
||||
import art.arcane.iris.spi.IrisLogging;
|
||||
import art.arcane.iris.spi.PlatformBiome;
|
||||
@@ -61,8 +70,8 @@ import java.util.UUID;
|
||||
import java.util.concurrent.atomic.AtomicLong;
|
||||
|
||||
@Data
|
||||
@EqualsAndHashCode(exclude = {"data", "gridBoundsCache", "frozenInterpolators", "frozenGenerators"})
|
||||
@ToString(exclude = {"data", "gridBoundsCache", "frozenInterpolators", "frozenGenerators"})
|
||||
@EqualsAndHashCode(exclude = {"data", "gridBoundsCache", "frozenInterpolators", "frozenGenerators", "riverRuntime"})
|
||||
@ToString(exclude = {"data", "gridBoundsCache", "frozenInterpolators", "frozenGenerators", "riverRuntime"})
|
||||
public class IrisComplex implements DataProvider {
|
||||
private static final NoiseBounds ZERO_NOISE_BOUNDS = new NoiseBounds(0D, 0D);
|
||||
private static final AtomicLong lastBoundsFailureLog = new AtomicLong(0L);
|
||||
@@ -96,14 +105,22 @@ public class IrisComplex implements DataProvider {
|
||||
private ProceduralStream<IrisBiome> shoreBiomeStream;
|
||||
private ProceduralStream<IrisBiome> baseBiomeStream;
|
||||
private ProceduralStream<UUID> baseBiomeIDStream;
|
||||
private ProceduralStream<IrisBiome> naturalTrueBiomeStream;
|
||||
private ProceduralStream<IrisBiome> trueBiomeStream;
|
||||
private ProceduralStream<PlatformBiome> trueBiomeDerivativeStream;
|
||||
private ProceduralStream<Double> naturalHeightStream;
|
||||
private ProceduralStream<Double> heightStream;
|
||||
private ProceduralStream<Integer> roundedHeighteightStream;
|
||||
private ProceduralStream<Double> maxHeightStream;
|
||||
private ProceduralStream<Double> overlayStream;
|
||||
private ProceduralStream<Double> heightFluidStream;
|
||||
private ProceduralStream<Double> naturalSlopeStream;
|
||||
private ProceduralStream<Double> slopeStream;
|
||||
private ProceduralStream<IrisRiverSurfaceSample> riverSurfaceStream;
|
||||
private ProceduralStream<Double> riverDistanceStream;
|
||||
private ProceduralStream<Double> riverFlowStream;
|
||||
private ProceduralStream<Double> riverCarveWeightStream;
|
||||
private ProceduralStream<Double> riverWaterSurfaceStream;
|
||||
private ProceduralStream<Integer> topSurfaceStream;
|
||||
private ProceduralStream<IrisDecorator> terrainSurfaceDecoration;
|
||||
private ProceduralStream<IrisDecorator> terrainCeilingDecoration;
|
||||
@@ -118,6 +135,7 @@ public class IrisComplex implements DataProvider {
|
||||
private IrisRegion focusRegion;
|
||||
private Map<IrisInterpolator, IdentityHashMap<IrisBiome, GeneratorBounds>> generatorBounds;
|
||||
private Set<IrisBiome> generatorBiomes;
|
||||
private IrisRiverRuntime riverRuntime;
|
||||
// Copy-on-write: reads happen per column on every burst thread; the synchronizedMap
|
||||
// monitor was taken on every HIT. Writes are once per biome and bounded, so a fresh map
|
||||
// per insert is cheap. Identity keying is load-bearing (IrisBiome is mutable/value-hashed).
|
||||
@@ -150,7 +168,7 @@ public class IrisComplex implements DataProvider {
|
||||
//@builder
|
||||
if (focusRegion != null) {
|
||||
prepareInferredBiomes(focusRegion);
|
||||
focusRegion.getAllBiomes(this).forEach(this::registerGenerators);
|
||||
focusRegion.getNaturalBiomes(this).forEach(this::registerGenerators);
|
||||
} else {
|
||||
engine.getDimension().getRegions().forEach(regionKey -> {
|
||||
IrisRegion region = data.getRegionLoader().load(regionKey);
|
||||
@@ -158,7 +176,7 @@ public class IrisComplex implements DataProvider {
|
||||
return;
|
||||
}
|
||||
prepareInferredBiomes(region);
|
||||
region.getAllBiomes(this).forEach(this::registerGenerators);
|
||||
region.getNaturalBiomes(this).forEach(this::registerGenerators);
|
||||
});
|
||||
}
|
||||
int interpolatorCount = generators.size();
|
||||
@@ -246,25 +264,85 @@ public class IrisComplex implements DataProvider {
|
||||
bridgeStream.convertAware2D((t, x, z) -> inferredStreams.get(t).get(x, z))
|
||||
.convertAware2D(this::implode)
|
||||
.cache2D("baseBiomeStream", engine, cacheSize);
|
||||
heightStream = ProceduralStream.of((x, z) -> {
|
||||
naturalHeightStream = ProceduralStream.of((x, z) -> {
|
||||
IrisBiome b = focusBiome != null ? focusBiome : baseBiomeStream.get(x, z);
|
||||
return getHeight(engine, b, x, z, engine.getSeedManager().getHeight());
|
||||
}, Interpolated.DOUBLE).cache2DDouble("heightStream", engine, cacheSize);
|
||||
}, Interpolated.DOUBLE).cache2DDouble("naturalHeightStream", engine, cacheSize);
|
||||
naturalSlopeStream = naturalHeightStream.slope(3)
|
||||
.cache2DDouble("naturalSlopeStream", engine, cacheSize);
|
||||
naturalTrueBiomeStream = focusBiome != null ? ProceduralStream.of((x, y) -> focusBiome, Interpolated.of(a -> 0D,
|
||||
b -> focusBiome))
|
||||
.cache2D("naturalTrueBiomeStream-focus", engine, cacheSize) : naturalHeightStream
|
||||
.convertAware2D((h, x, z) ->
|
||||
fixBiomeType(h, baseBiomeStream.get(x, z), regionStream.get(x, z), x, z, fluidHeight))
|
||||
.cache2D("naturalTrueBiomeStream", engine, cacheSize);
|
||||
if (engine.getDimension().getRivers() != null && engine.getDimension().getRivers().isEnabled()) {
|
||||
ProceduralStream<Boolean> naturalOceanStream = createNaturalOceanStream(
|
||||
naturalHeightStream,
|
||||
bridgeStream,
|
||||
focusBiome,
|
||||
fluidHeight,
|
||||
engine.getDimension().getRivers().getWater().getMode()
|
||||
).cache2D("naturalOceanStream", engine, cacheSize);
|
||||
riverRuntime = new IrisRiverRuntime(new IrisRiverRuntimeContext(
|
||||
engine.getSeedManager().getBodies(),
|
||||
engine.getDimension().getRivers(),
|
||||
data,
|
||||
(int) Math.round(fluidHeight),
|
||||
IrisEngineMantle.isRiverHydrologyEnabled(engine.getDimension()),
|
||||
IrisEngineMantle.isRiverCaveHydrologyEnabled(engine.getDimension()),
|
||||
blockingRiverRoutingPossible(engine),
|
||||
variableMaxIncisionPossible(engine),
|
||||
biomeRiverOverridesPossible(focusBiome, generatorBiomes),
|
||||
(blockX, blockZ) -> naturalHeightBounds(engine, overlayNoise, blockX, blockZ),
|
||||
naturalHeightStream,
|
||||
naturalSlopeStream,
|
||||
naturalOceanStream,
|
||||
naturalTrueBiomeStream,
|
||||
regionStream
|
||||
));
|
||||
riverSurfaceStream = ProceduralStream.of(
|
||||
(x, z) -> riverRuntime.sample(x, z),
|
||||
Interpolated.of(
|
||||
IrisRiverSurfaceSample::terrainHeight,
|
||||
value -> IrisRiverSurfaceSample.none(value, fluidHeight)
|
||||
)
|
||||
)
|
||||
.cache2D("riverSurfaceStream", engine, cacheSize);
|
||||
} else {
|
||||
riverSurfaceStream = naturalHeightStream.convert(
|
||||
value -> IrisRiverSurfaceSample.none(value, fluidHeight)
|
||||
)
|
||||
.cache2D("riverSurfaceStream-disabled", engine, cacheSize);
|
||||
}
|
||||
heightStream = riverSurfaceStream.convert(IrisRiverSurfaceSample::terrainHeight)
|
||||
.cache2DDouble("heightStream", engine, cacheSize);
|
||||
roundedHeighteightStream = heightStream.contextInjecting(engine, (c, x, z) -> c.getHeight().getDouble(x, z))
|
||||
.round();
|
||||
slopeStream = heightStream.contextInjecting(engine, (c, x, z) -> c.getHeight().getDouble(x, z))
|
||||
.slope(3).cache2DDouble("slopeStream", engine, cacheSize);
|
||||
trueBiomeStream = focusBiome != null ? ProceduralStream.of((x, y) -> focusBiome, Interpolated.of(a -> 0D,
|
||||
b -> focusBiome))
|
||||
.cache2D("trueBiomeStream-focus", engine, cacheSize) : heightStream
|
||||
.convertAware2D((h, x, z) ->
|
||||
fixBiomeType(h, baseBiomeStream.get(x, z),
|
||||
regionStream.contextInjecting(engine, (c, xx, zz) -> c.getRegion().get(xx, zz)).get(x, z), x, z, fluidHeight))
|
||||
.cache2D("trueBiomeStream-focus", engine, cacheSize) : riverSurfaceStream
|
||||
.convertAware2D((sample, x, z) -> resolveRiverSurfaceBiome(sample, x, z))
|
||||
.cache2D("trueBiomeStream", engine, cacheSize);
|
||||
trueBiomeDerivativeStream = trueBiomeStream.contextInjecting(engine, (c, x, z) -> c.getBiome().get(x, z))
|
||||
.convert((b) -> IrisPlatforms.get().registries().biome(b.getDerivativeKey())).cache2D("trueBiomeDerivativeStream", engine, cacheSize);
|
||||
heightFluidStream = heightStream.contextInjecting(engine, (c, x, z) -> c.getHeight().getDouble(x, z))
|
||||
.max(fluidHeight).cache2DDouble("heightFluidStream", engine, cacheSize);
|
||||
riverDistanceStream = riverSurfaceStream.convert(sample -> sample.river().present()
|
||||
? sample.river().distance()
|
||||
: Double.MAX_VALUE)
|
||||
.cache2DDouble("riverDistanceStream", engine, cacheSize);
|
||||
riverFlowStream = riverSurfaceStream.convert(sample -> (double) sample.river().flow())
|
||||
.cache2DDouble("riverFlowStream", engine, cacheSize);
|
||||
riverCarveWeightStream = riverSurfaceStream.convert(sample -> sample.river().carveWeight())
|
||||
.cache2DDouble("riverCarveWeightStream", engine, cacheSize);
|
||||
riverWaterSurfaceStream = riverSurfaceStream.convert(IrisRiverSurfaceSample::waterSurfaceY)
|
||||
.cache2DDouble("riverWaterSurfaceStream", engine, cacheSize);
|
||||
heightFluidStream = ProceduralStream.ofDouble((x, z) -> Math.max(
|
||||
heightStream.get(x, z),
|
||||
riverWaterSurfaceStream.get(x, z)
|
||||
))
|
||||
.cache2DDouble("heightFluidStream", engine, cacheSize);
|
||||
maxHeightStream = ProceduralStream.ofDouble((x, z) -> height);
|
||||
terrainSurfaceDecoration = trueBiomeStream.contextInjecting(engine, (c, x, z) -> c.getBiome().get(x, z))
|
||||
.convertAware2D((b, xx, zz) -> decorateFor(b, xx, zz, IrisDecorationPart.NONE)).cache2D("terrainSurfaceDecoration", engine, cacheSize);
|
||||
@@ -290,6 +368,90 @@ public class IrisComplex implements DataProvider {
|
||||
//@done
|
||||
}
|
||||
|
||||
private boolean blockingRiverRoutingPossible(Engine engine) {
|
||||
if (blocksRiverRouting(focusRegion == null ? null : focusRegion.getRiverOverride())
|
||||
|| blocksRiverRouting(focusBiome == null ? null : focusBiome.getRiverOverride())) {
|
||||
return true;
|
||||
}
|
||||
KList<IrisRegion> regions = engine.getDimension().getAllRegions(engine);
|
||||
for (IrisRegion loadedRegion : regions) {
|
||||
if (loadedRegion != null && blocksRiverRouting(loadedRegion.getRiverOverride())) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
KList<IrisBiome> biomes = engine.getDimension().getReachableBiomes(engine);
|
||||
for (IrisBiome biome : biomes) {
|
||||
if (biome != null && blocksRiverRouting(biome.getRiverOverride())) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private static boolean blocksRiverRouting(IrisRiverOverride override) {
|
||||
return override != null && override.getRoutingPolicy() == IrisRiverRoutingPolicy.BLOCK;
|
||||
}
|
||||
|
||||
private boolean variableMaxIncisionPossible(Engine engine) {
|
||||
if (changesMaxIncision(focusRegion == null ? null : focusRegion.getRiverOverride())
|
||||
|| changesMaxIncision(focusBiome == null ? null : focusBiome.getRiverOverride())) {
|
||||
return true;
|
||||
}
|
||||
KList<IrisRegion> regions = engine.getDimension().getAllRegions(engine);
|
||||
for (IrisRegion loadedRegion : regions) {
|
||||
if (loadedRegion != null && changesMaxIncision(loadedRegion.getRiverOverride())) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
KList<IrisBiome> biomes = engine.getDimension().getReachableBiomes(engine);
|
||||
for (IrisBiome biome : biomes) {
|
||||
if (biome != null && changesMaxIncision(biome.getRiverOverride())) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
static boolean changesMaxIncision(IrisRiverOverride override) {
|
||||
if (override == null || override.getMaxIncisionMultiplier() == null) {
|
||||
return false;
|
||||
}
|
||||
double multiplier = override.getMaxIncisionMultiplier();
|
||||
return Double.isFinite(multiplier) && Double.compare(Math.max(0D, multiplier), 1D) != 0;
|
||||
}
|
||||
|
||||
static boolean biomeRiverOverridesPossible(IrisBiome focusBiome, Iterable<IrisBiome> naturalBiomes) {
|
||||
if (focusBiome != null) {
|
||||
return focusBiome.getRiverOverride() != null;
|
||||
}
|
||||
for (IrisBiome biome : naturalBiomes) {
|
||||
if (biome != null && biome.getRiverOverride() != null) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
static ProceduralStream<Boolean> createNaturalOceanStream(
|
||||
ProceduralStream<Double> naturalHeightStream,
|
||||
ProceduralStream<InferredType> bridgeStream,
|
||||
IrisBiome focusBiome,
|
||||
double fluidHeight,
|
||||
IrisRiverWaterMode waterMode
|
||||
) {
|
||||
if (focusBiome != null) {
|
||||
boolean ocean = focusBiome.getInferredType() == InferredType.SEA;
|
||||
return ProceduralStream.of((x, z) -> ocean, Interpolated.BOOLEAN);
|
||||
}
|
||||
if (waterMode == IrisRiverWaterMode.SEA_LEVEL) {
|
||||
return bridgeStream.convert(type -> type == InferredType.SEA);
|
||||
}
|
||||
return ProceduralStream.of(
|
||||
(x, z) -> naturalHeightStream.getDouble(x, z) < fluidHeight - 1D,
|
||||
Interpolated.BOOLEAN
|
||||
);
|
||||
}
|
||||
|
||||
public ProceduralStream<IrisBiome> getBiomeStream(InferredType type) {
|
||||
switch (type) {
|
||||
case CAVE:
|
||||
@@ -343,6 +505,63 @@ public class IrisComplex implements DataProvider {
|
||||
return null;
|
||||
}
|
||||
|
||||
private IrisBiome resolveRiverSurfaceBiome(IrisRiverSurfaceSample sample, double x, double z) {
|
||||
if (riverRuntime != null) {
|
||||
if (sample.subterranean()) {
|
||||
return fixBiomeType(
|
||||
sample.naturalHeight(),
|
||||
baseBiomeStream.get(x, z),
|
||||
regionStream.get(x, z),
|
||||
x,
|
||||
z,
|
||||
fluidHeight
|
||||
);
|
||||
}
|
||||
IrisBiome riverBiome = riverRuntime.selectSurfaceBiome(sample, x, z);
|
||||
if (riverBiome != null) {
|
||||
return implode(riverBiome, x, z);
|
||||
}
|
||||
InferredType directFallback = directRiverFallback(sample.river());
|
||||
if (directFallback != null) {
|
||||
IrisBiome baseBiome = baseBiomeStream.get(x, z);
|
||||
return implode(baseBiome.withInferredType(directFallback), x, z);
|
||||
}
|
||||
if (sample.river().present()
|
||||
&& sample.river().state() == RiverRouteState.WET
|
||||
&& sample.river().section() == RiverSection.BANK) {
|
||||
return fixBiomeType(
|
||||
sample.terrainHeight(),
|
||||
baseBiomeStream.get(x, z),
|
||||
regionStream.get(x, z),
|
||||
x,
|
||||
z,
|
||||
sample.waterSurfaceY()
|
||||
);
|
||||
}
|
||||
}
|
||||
return fixBiomeType(
|
||||
sample.terrainHeight(),
|
||||
baseBiomeStream.get(x, z),
|
||||
regionStream.get(x, z),
|
||||
x,
|
||||
z,
|
||||
fluidHeight
|
||||
);
|
||||
}
|
||||
|
||||
static InferredType directRiverFallback(RiverSample river) {
|
||||
if (!river.present()) {
|
||||
return null;
|
||||
}
|
||||
if (river.state() == RiverRouteState.DRY) {
|
||||
return InferredType.LAND;
|
||||
}
|
||||
return switch (river.section()) {
|
||||
case CHANNEL, MOUTH -> InferredType.SEA;
|
||||
default -> null;
|
||||
};
|
||||
}
|
||||
|
||||
private IrisBiome fixBiomeType(Double height, IrisBiome biome, IrisRegion region, Double x, Double z, double fluidHeight) {
|
||||
IrisBiome resolved = resolveSurfaceBiome(
|
||||
height,
|
||||
@@ -491,6 +710,36 @@ public class IrisComplex implements DataProvider {
|
||||
return h;
|
||||
}
|
||||
|
||||
private NoiseBounds naturalHeightBounds(
|
||||
Engine engine,
|
||||
KList<IrisShapedGeneratorStyle> overlayNoise,
|
||||
double x,
|
||||
double z
|
||||
) {
|
||||
double minimum = fluidHeight;
|
||||
double maximum = fluidHeight;
|
||||
for (int interpolatorIndex = 0; interpolatorIndex < frozenInterpolators.length; interpolatorIndex++) {
|
||||
NoiseBounds bounds = gridSampleBounds(
|
||||
engine,
|
||||
frozenInterpolators[interpolatorIndex],
|
||||
interpolatorIndex,
|
||||
frozenGenerators[interpolatorIndex],
|
||||
x,
|
||||
z
|
||||
);
|
||||
minimum += Math.min(bounds.min(), bounds.max());
|
||||
maximum += Math.max(bounds.min(), bounds.max());
|
||||
}
|
||||
for (IrisShapedGeneratorStyle style : overlayNoise) {
|
||||
minimum += Math.min(style.getMin(), style.getMax());
|
||||
maximum += Math.max(style.getMin(), style.getMax());
|
||||
}
|
||||
return new NoiseBounds(
|
||||
Math.max(0D, Math.min(engine.getHeight(), minimum)),
|
||||
Math.max(0D, Math.min(engine.getHeight(), maximum))
|
||||
);
|
||||
}
|
||||
|
||||
private double getHeight(Engine engine, IrisBiome b, double x, double z, long seed) {
|
||||
return Math.max(Math.min(getInterpolatedHeight(engine, x, z, seed) + fluidHeight + overlayStream.get(x, z), engine.getHeight()), 0);
|
||||
}
|
||||
@@ -922,6 +1171,8 @@ public class IrisComplex implements DataProvider {
|
||||
}
|
||||
|
||||
public void close() {
|
||||
|
||||
if (riverRuntime != null) {
|
||||
riverRuntime.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,7 +30,9 @@ import art.arcane.iris.engine.mantle.MantlePass;
|
||||
import art.arcane.iris.engine.mantle.components.MantleCarvingComponent;
|
||||
import art.arcane.iris.engine.mantle.components.MantleFloatingObjectComponent;
|
||||
import art.arcane.iris.engine.mantle.components.MantleObjectComponent;
|
||||
import art.arcane.iris.engine.mantle.components.MantleRiverHydrologyComponent;
|
||||
import art.arcane.iris.engine.mantle.components.IrisStructureComponent;
|
||||
import art.arcane.iris.engine.object.IrisDimension;
|
||||
import art.arcane.iris.spi.IrisLogging;
|
||||
import art.arcane.iris.spi.PlatformBlockState;
|
||||
import art.arcane.iris.util.project.matter.IrisMatterContext;
|
||||
@@ -88,6 +90,7 @@ public class IrisEngineMantle implements EngineMantle {
|
||||
this.mantle = createMantle(engine);
|
||||
components = new KMap<>();
|
||||
registerComponent(new MantleCarvingComponent(this));
|
||||
registerComponent(new MantleRiverHydrologyComponent(this));
|
||||
object = new MantleObjectComponent(this);
|
||||
registerComponent(object);
|
||||
registerComponent(new MantleFloatingObjectComponent(this));
|
||||
@@ -126,9 +129,14 @@ public class IrisEngineMantle implements EngineMantle {
|
||||
.mapToInt(MantleComponent::getRadius)
|
||||
.max()
|
||||
.orElse(0);
|
||||
int cumulative = downstreamBlockRadius + passBlockRadius;
|
||||
built[i] = new MantlePass(pass, Math.ceilDiv(cumulative, 16), downstreamBlockRadius);
|
||||
downstreamBlockRadius = cumulative;
|
||||
int passInputRadius = pass.stream()
|
||||
.filter(MantleComponent::isEnabled)
|
||||
.mapToInt(MantleComponent::getInputRadius)
|
||||
.max()
|
||||
.orElse(0);
|
||||
int invocationRadius = downstreamBlockRadius + passBlockRadius;
|
||||
built[i] = new MantlePass(pass, Math.ceilDiv(invocationRadius, 16), downstreamBlockRadius);
|
||||
downstreamBlockRadius = invocationRadius + passInputRadius;
|
||||
}
|
||||
|
||||
return List.of(built);
|
||||
@@ -157,7 +165,7 @@ public class IrisEngineMantle implements EngineMantle {
|
||||
@Override
|
||||
public void hotload() {
|
||||
disabledFlags.reset();
|
||||
for (var component : registeredComponents.values()) {
|
||||
for (MantleComponent component : registeredComponents.values()) {
|
||||
component.hotload();
|
||||
component.setEnabled(!getDisabledFlags().contains(component.getFlag()));
|
||||
}
|
||||
@@ -171,10 +179,22 @@ public class IrisEngineMantle implements EngineMantle {
|
||||
if (!getDimension().isCarvingEnabled()) {
|
||||
disabled.addIfMissing(ReservedFlag.CARVED);
|
||||
}
|
||||
if (disabled.contains(ReservedFlag.CARVED)
|
||||
|| !isRiverHydrologyEnabled(getDimension())) {
|
||||
disabled.addIfMissing(ReservedFlag.RIVER_HYDROLOGY);
|
||||
}
|
||||
return Set.copyOf(disabled);
|
||||
});
|
||||
}
|
||||
|
||||
static boolean isRiverHydrologyEnabled(IrisDimension dimension) {
|
||||
return MantleRiverHydrologyComponent.isEnabledFor(dimension);
|
||||
}
|
||||
|
||||
static boolean isRiverCaveHydrologyEnabled(IrisDimension dimension) {
|
||||
return MantleRiverHydrologyComponent.isCaveConnectionsEnabledFor(dimension);
|
||||
}
|
||||
|
||||
@Override
|
||||
public MantleObjectComponent getObjectComponent() {
|
||||
return object;
|
||||
|
||||
@@ -68,8 +68,8 @@ public class UpperDimensionContext implements DataProvider {
|
||||
engine.getDimension(),
|
||||
engine.getData(),
|
||||
chunkHeight,
|
||||
complex.getHeightStream(),
|
||||
complex.getTrueBiomeStream(),
|
||||
complex.getNaturalHeightStream(),
|
||||
complex.getNaturalTrueBiomeStream(),
|
||||
complex.getRegionStream(),
|
||||
complex.getRockStream(),
|
||||
true
|
||||
@@ -94,7 +94,7 @@ public class UpperDimensionContext implements DataProvider {
|
||||
upperDim.getRegions().forEach(regionKey -> {
|
||||
IrisRegion region = upperData.getRegionLoader().load(regionKey);
|
||||
if (region != null) {
|
||||
region.getAllBiomes(dataProvider).forEach(biome -> {
|
||||
region.getNaturalBiomes(dataProvider).forEach(biome -> {
|
||||
allBiomes.add(biome);
|
||||
biome.getGenerators().forEach(link -> {
|
||||
IrisGenerator gen = link.getCachedGenerator(dataProvider);
|
||||
|
||||
@@ -27,6 +27,8 @@ import art.arcane.iris.engine.framework.Engine;
|
||||
import art.arcane.iris.engine.framework.EngineAssignedActuator;
|
||||
import art.arcane.iris.engine.framework.EngineDecorator;
|
||||
import art.arcane.iris.engine.object.IrisBiome;
|
||||
import art.arcane.iris.engine.river.RiverRouteState;
|
||||
import art.arcane.iris.engine.river.runtime.IrisRiverSurfaceSample;
|
||||
import art.arcane.iris.util.common.data.B;
|
||||
import art.arcane.iris.util.project.context.ChunkContext;
|
||||
import art.arcane.volmlib.util.documentation.BlockCoordinates;
|
||||
@@ -64,6 +66,12 @@ public class IrisDecorantActuator extends EngineAssignedActuator<PlatformBlockSt
|
||||
seaFloorDecorator = new IrisSeaFloorDecorator(getEngine());
|
||||
}
|
||||
|
||||
static boolean shouldDecorateShoreline(IrisRiverSurfaceSample sample, int height) {
|
||||
return !sample.subterranean()
|
||||
&& height == Math.round(sample.waterSurfaceY())
|
||||
&& (!sample.river().present() || sample.river().state() != RiverRouteState.DRY);
|
||||
}
|
||||
|
||||
@BlockCoordinates
|
||||
@Override
|
||||
public void onActuate(int x, int z, Hunk<PlatformBlockState> output, boolean multicore, ChunkContext context) {
|
||||
@@ -86,23 +94,25 @@ public class IrisDecorantActuator extends EngineAssignedActuator<PlatformBlockSt
|
||||
height = context.getRoundedHeight(i, j);
|
||||
biome = context.getBiome().get(i, j);
|
||||
cave = shouldRay ? context.getCave().get(i, j) : null;
|
||||
IrisRiverSurfaceSample riverSurface = getComplex().getRiverSurfaceStream().get(realX, realZ);
|
||||
int surfaceFluidHeight = (int) Math.round(riverSurface.waterSurfaceY());
|
||||
|
||||
if (biome.getDecorators().isEmpty() && (cave == null || cave.getDecorators().isEmpty())) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (height < getDimension().getFluidHeight() && PREDICATE_SOLID.test(output.get(i, height, j))
|
||||
if (height < surfaceFluidHeight && PREDICATE_SOLID.test(output.get(i, height, j))
|
||||
&& height + 1 < output.getHeight() && B.isWater(output.get(i, height + 1, j))) {
|
||||
getSeaSurfaceDecorator().decorate(i, j,
|
||||
realX, Math.round(i + 1), Math.round(x + i - 1),
|
||||
realZ, Math.round(z + j + 1), Math.round(z + j - 1),
|
||||
output, biome, getDimension().getFluidHeight(), getEngine().getHeight());
|
||||
output, biome, surfaceFluidHeight, getEngine().getHeight());
|
||||
getSeaFloorDecorator().decorate(i, j,
|
||||
realX, realZ, output, biome, height + 1,
|
||||
getDimension().getFluidHeight() + 1);
|
||||
surfaceFluidHeight + 1);
|
||||
}
|
||||
|
||||
if (height == getDimension().getFluidHeight()) {
|
||||
if (shouldDecorateShoreline(riverSurface, height)) {
|
||||
getShoreLineDecorator().decorate(i, j,
|
||||
realX, Math.round(x + i + 1), Math.round(x + i - 1),
|
||||
realZ, Math.round(z + j + 1), Math.round(z + j - 1),
|
||||
|
||||
@@ -67,10 +67,6 @@ public class IrisTerrainNormalActuator extends EngineAssignedActuator<PlatformBl
|
||||
getEngine().getMetrics().getTerrain().put(p.getMilliseconds());
|
||||
}
|
||||
|
||||
private int fluidOrHeight(int height) {
|
||||
return Math.max(getDimension().getFluidHeight(), height);
|
||||
}
|
||||
|
||||
/**
|
||||
* This is calling 1/16th of a chunk x/z slice. It is a plane from sky to bedrock 1 thick in the x direction.
|
||||
*
|
||||
@@ -92,8 +88,6 @@ public class IrisTerrainNormalActuator extends EngineAssignedActuator<PlatformBl
|
||||
IrisData data = getData();
|
||||
IrisComplex complex = getComplex();
|
||||
RNG localRng = rng;
|
||||
int fluidHeight = dimension.getFluidHeight();
|
||||
int clampedFluidHeight = Math.min(chunkHeight, fluidHeight);
|
||||
boolean bedrockEnabled = dimension.isBedrock();
|
||||
boolean hideOres = dimension.isHideOresForHiddenOre();
|
||||
ChunkedDataCache<IrisBiome> biomeCache = context.getBiome();
|
||||
@@ -114,7 +108,11 @@ public class IrisTerrainNormalActuator extends EngineAssignedActuator<PlatformBl
|
||||
IrisBiome biome = biomeCache.get(xf, zf);
|
||||
IrisRegion region = regionCache.get(xf, zf);
|
||||
int he = Math.min(chunkHeight, context.getRoundedHeight(xf, zf));
|
||||
int hf = Math.max(clampedFluidHeight, he);
|
||||
int surfaceFluidHeight = Math.min(
|
||||
chunkHeight,
|
||||
(int) Math.round(complex.getRiverWaterSurfaceStream().get(realX, realZ))
|
||||
);
|
||||
int hf = Math.max(surfaceFluidHeight, he);
|
||||
if (hf < 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -40,16 +40,17 @@ public class IrisShoreLineDecorator extends IrisEngineDecorator {
|
||||
@Override
|
||||
public void decorate(int x, int z, int realX, int realX1, int realX_1, int realZ, int realZ1, int realZ_1,
|
||||
Hunk<PlatformBlockState> data, IrisBiome biome, int height, int max) {
|
||||
if (height != getDimension().getFluidHeight()) {
|
||||
double localFluidHeight = getComplex().getRiverWaterSurfaceStream().get(realX, realZ);
|
||||
if (height != Math.round(localFluidHeight)) {
|
||||
return;
|
||||
}
|
||||
|
||||
double complexFluidHeight = getComplex().getFluidHeight();
|
||||
ProceduralStream<Double> heightStream = getComplex().getHeightStream();
|
||||
if (Math.round(heightStream.get(realX1, realZ)) >= complexFluidHeight
|
||||
&& Math.round(heightStream.get(realX_1, realZ)) >= complexFluidHeight
|
||||
&& Math.round(heightStream.get(realX, realZ1)) >= complexFluidHeight
|
||||
&& Math.round(heightStream.get(realX, realZ_1)) >= complexFluidHeight) {
|
||||
ProceduralStream<Double> fluidStream = getComplex().getRiverWaterSurfaceStream();
|
||||
if (Math.round(heightStream.get(realX1, realZ)) >= Math.round(fluidStream.get(realX1, realZ))
|
||||
&& Math.round(heightStream.get(realX_1, realZ)) >= Math.round(fluidStream.get(realX_1, realZ))
|
||||
&& Math.round(heightStream.get(realX, realZ1)) >= Math.round(fluidStream.get(realX, realZ1))
|
||||
&& Math.round(heightStream.get(realX, realZ_1)) >= Math.round(fluidStream.get(realX, realZ_1))) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -58,7 +58,7 @@ public class IrisSurfaceDecorator extends IrisEngineDecorator {
|
||||
@BlockCoordinates
|
||||
public void decorate(int x, int z, int realX, int realX1, int realX_1, int realZ, int realZ1, int realZ_1,
|
||||
Hunk<PlatformBlockState> data, IrisBiome biome, InferredType inferredType, int height, int max) {
|
||||
int fluidHeight = getDimension().getFluidHeight();
|
||||
int fluidHeight = (int) Math.round(getComplex().getRiverWaterSurfaceStream().get(realX, realZ));
|
||||
if (inferredType == InferredType.SHORE && height < fluidHeight) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -42,6 +42,8 @@ import art.arcane.iris.engine.object.IrisPosition;
|
||||
import art.arcane.iris.engine.object.IrisRegion;
|
||||
import art.arcane.iris.engine.object.IrisStructure;
|
||||
import art.arcane.iris.engine.object.IrisWorld;
|
||||
import art.arcane.iris.engine.river.cave.RiverCaveHydrology;
|
||||
import art.arcane.iris.engine.river.cave.RiverCaveHydrologyStorage;
|
||||
import art.arcane.iris.spi.IrisLogging;
|
||||
import art.arcane.iris.spi.PlatformBiome;
|
||||
import art.arcane.iris.spi.PlatformBlockState;
|
||||
@@ -244,6 +246,14 @@ public interface Engine extends DataProvider, Fallible, BlockUpdater, Renderer,
|
||||
|
||||
@BlockCoordinates
|
||||
default IrisBiome getCaveOrMantleBiome(int x, int y, int z) {
|
||||
RiverCaveHydrology hydrology = RiverCaveHydrologyStorage.getIfPresent(
|
||||
getMantle().getMantle(), x, y, z);
|
||||
if (hydrology != null && !hydrology.floodedBiomeKey().isEmpty()) {
|
||||
IrisBiome biome = getData().getBiomeLoader().load(hydrology.floodedBiomeKey());
|
||||
if (biome != null) {
|
||||
return biome;
|
||||
}
|
||||
}
|
||||
MatterCavern m = getMantle().getMantle().get(x, y, z, MatterCavern.class);
|
||||
|
||||
if (m != null && m.getCustomBiome() != null && !m.getCustomBiome().isEmpty()) {
|
||||
|
||||
+4
-1
@@ -157,7 +157,10 @@ public final class NativeStructurePlacementPlanner {
|
||||
}
|
||||
|
||||
static boolean isSubmerged(Engine engine, int blockX, int blockZ) {
|
||||
return engine.getHeight(blockX, blockZ, true) < engine.getDimension().getFluidHeight();
|
||||
int localFluidHeight = engine.getComplex() == null
|
||||
? engine.getDimension().getFluidHeight()
|
||||
: (int) Math.round(engine.getComplex().getRiverWaterSurfaceStream().get(blockX, blockZ));
|
||||
return engine.getHeight(blockX, blockZ, true) < localFluidHeight;
|
||||
}
|
||||
|
||||
private static int comparePlacementPriority(IrisStructurePlacement left, IrisStructurePlacement right) {
|
||||
|
||||
+26
-1
@@ -3,6 +3,8 @@ package art.arcane.iris.engine.framework;
|
||||
import art.arcane.iris.engine.object.IrisBiome;
|
||||
import art.arcane.iris.engine.object.IrisStructureAnchorMode;
|
||||
import art.arcane.iris.engine.object.IrisStructurePlacement;
|
||||
import art.arcane.iris.engine.river.cave.RiverCaveHydrology;
|
||||
import art.arcane.iris.engine.river.cave.RiverCaveHydrologyStorage;
|
||||
import art.arcane.volmlib.util.math.RNG;
|
||||
import art.arcane.volmlib.util.matter.MatterCavern;
|
||||
|
||||
@@ -286,10 +288,12 @@ public final class StructureCaveAnchorResolver {
|
||||
int mantleY,
|
||||
int blockZ
|
||||
) {
|
||||
RiverCaveHydrology hydrology = hydrologyAt(engine, blockX, mantleY, blockZ);
|
||||
MatterCavern cavern = cavernAt(engine, blockX, mantleY, blockZ);
|
||||
return acceptsAnchorFluid(
|
||||
placement.isUnderwater(),
|
||||
cavern,
|
||||
hydrology,
|
||||
mantleY,
|
||||
engine.getDimension().getCaveLavaHeight());
|
||||
}
|
||||
@@ -300,6 +304,19 @@ public final class StructureCaveAnchorResolver {
|
||||
int mantleY,
|
||||
int defaultLavaHeight
|
||||
) {
|
||||
return acceptsAnchorFluid(underwater, cavern, null, mantleY, defaultLavaHeight);
|
||||
}
|
||||
|
||||
static boolean acceptsAnchorFluid(
|
||||
boolean underwater,
|
||||
MatterCavern cavern,
|
||||
RiverCaveHydrology hydrology,
|
||||
int mantleY,
|
||||
int defaultLavaHeight
|
||||
) {
|
||||
if (hydrology != null && hydrology.protectsPlacement()) {
|
||||
return false;
|
||||
}
|
||||
if (cavern == null || !cavern.isCavern()) {
|
||||
return false;
|
||||
}
|
||||
@@ -313,7 +330,15 @@ public final class StructureCaveAnchorResolver {
|
||||
}
|
||||
|
||||
private static MatterCavern cavernAt(Engine engine, int blockX, int mantleY, int blockZ) {
|
||||
return engine.getMantle().getMantle().get(blockX, mantleY, blockZ, MatterCavern.class);
|
||||
MatterCavern baseline = engine.getMantle().getMantle()
|
||||
.get(blockX, mantleY, blockZ, MatterCavern.class);
|
||||
RiverCaveHydrology hydrology = hydrologyAt(engine, blockX, mantleY, blockZ);
|
||||
return hydrology == null ? baseline : hydrology.asCavern();
|
||||
}
|
||||
|
||||
private static RiverCaveHydrology hydrologyAt(Engine engine, int blockX, int mantleY, int blockZ) {
|
||||
return RiverCaveHydrologyStorage.getIfPresent(
|
||||
engine.getMantle().getMantle(), blockX, mantleY, blockZ);
|
||||
}
|
||||
|
||||
static int toMantleY(int worldY, int worldMinHeight) {
|
||||
|
||||
@@ -6,12 +6,17 @@ import art.arcane.iris.engine.object.IrisRegion;
|
||||
import art.arcane.iris.engine.object.IrisStructurePlacement;
|
||||
import art.arcane.volmlib.util.collection.KList;
|
||||
|
||||
import java.lang.ref.WeakReference;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.IdentityHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
|
||||
public final class StructurePlacementScope {
|
||||
private static final List<CachedScopeIndex> SCOPE_INDEXES = new ArrayList<>();
|
||||
|
||||
private StructurePlacementScope() {
|
||||
}
|
||||
|
||||
@@ -22,20 +27,93 @@ public final class StructurePlacementScope {
|
||||
int blockZ = (chunkZ << 4) + 8;
|
||||
KList<IrisStructurePlacement> placements = new KList<>();
|
||||
Set<IrisStructurePlacement> seen = Collections.newSetFromMap(new IdentityHashMap<>());
|
||||
ScopeIndex index = scopeIndex(activeEngine);
|
||||
if (complex != null) {
|
||||
IrisBiome biome = complex.getTrueBiomeStream().get(blockX, blockZ);
|
||||
IrisBiome caveBiome = complex.getCaveBiomeStream().get(blockX, blockZ);
|
||||
IrisRegion region = complex.getRegionStream().get(blockX, blockZ);
|
||||
addUnique(placements, seen, biome == null ? null : biome.getStructures());
|
||||
addCaveUnique(placements, seen, caveBiome == null ? null : caveBiome.getStructures());
|
||||
addUnique(placements, seen, region == null ? null : region.getStructures());
|
||||
if (index.biome()) {
|
||||
IrisBiome biome = complex.getTrueBiomeStream().get(blockX, blockZ);
|
||||
addUnique(placements, seen, biome == null ? null : biome.getStructures());
|
||||
}
|
||||
if (index.caveBiome()) {
|
||||
IrisBiome caveBiome = complex.getCaveBiomeStream().get(blockX, blockZ);
|
||||
addCaveUnique(placements, seen, caveBiome == null ? null : caveBiome.getStructures());
|
||||
}
|
||||
if (index.region()) {
|
||||
IrisRegion region = complex.getRegionStream().get(blockX, blockZ);
|
||||
addUnique(placements, seen, region == null ? null : region.getStructures());
|
||||
}
|
||||
}
|
||||
if (activeEngine.getDimension() != null) {
|
||||
if (index.dimension()) {
|
||||
addUnique(placements, seen, activeEngine.getDimension().getStructures());
|
||||
}
|
||||
return placements;
|
||||
}
|
||||
|
||||
private static ScopeIndex scopeIndex(Engine engine) {
|
||||
int revision = engine.getCacheID();
|
||||
synchronized (SCOPE_INDEXES) {
|
||||
for (int i = SCOPE_INDEXES.size() - 1; i >= 0; i--) {
|
||||
CachedScopeIndex cached = SCOPE_INDEXES.get(i);
|
||||
Engine indexedEngine = cached.engine().get();
|
||||
if (indexedEngine == null) {
|
||||
SCOPE_INDEXES.remove(i);
|
||||
continue;
|
||||
}
|
||||
if (indexedEngine == engine) {
|
||||
if (cached.revision() == revision) {
|
||||
return cached.index();
|
||||
}
|
||||
SCOPE_INDEXES.remove(i);
|
||||
break;
|
||||
}
|
||||
}
|
||||
ScopeIndex index = buildScopeIndex(engine);
|
||||
SCOPE_INDEXES.add(new CachedScopeIndex(new WeakReference<>(engine), revision, index));
|
||||
return index;
|
||||
}
|
||||
}
|
||||
|
||||
private static ScopeIndex buildScopeIndex(Engine engine) {
|
||||
boolean biome = false;
|
||||
boolean caveBiome = false;
|
||||
KList<IrisBiome> allBiomes = engine.getAllBiomes();
|
||||
if (allBiomes == null) {
|
||||
biome = true;
|
||||
caveBiome = true;
|
||||
} else {
|
||||
for (IrisBiome candidate : allBiomes) {
|
||||
KList<IrisStructurePlacement> structures = candidate.getStructures();
|
||||
if (structures == null || structures.isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
biome = true;
|
||||
for (IrisStructurePlacement placement : structures) {
|
||||
if (placement != null && placement.resolvedAnchor().isCave()) {
|
||||
caveBiome = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
boolean region = false;
|
||||
if (engine.getDimension() != null) {
|
||||
KList<IrisRegion> allRegions = engine.getDimension().getAllRegions(engine);
|
||||
if (allRegions == null) {
|
||||
region = true;
|
||||
} else {
|
||||
for (IrisRegion candidate : allRegions) {
|
||||
if (candidate.getStructures() != null && !candidate.getStructures().isEmpty()) {
|
||||
region = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
boolean dimension = engine.getDimension() != null
|
||||
&& engine.getDimension().getStructures() != null
|
||||
&& !engine.getDimension().getStructures().isEmpty();
|
||||
return new ScopeIndex(biome, caveBiome, region, dimension);
|
||||
}
|
||||
|
||||
private static void addUnique(KList<IrisStructurePlacement> destination,
|
||||
Set<IrisStructurePlacement> seen,
|
||||
KList<IrisStructurePlacement> source) {
|
||||
@@ -61,4 +139,10 @@ public final class StructurePlacementScope {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private record CachedScopeIndex(WeakReference<Engine> engine, int revision, ScopeIndex index) {
|
||||
}
|
||||
|
||||
private record ScopeIndex(boolean biome, boolean caveBiome, boolean region, boolean dimension) {
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,7 +21,6 @@ import art.arcane.iris.util.common.scheduling.J;
|
||||
import art.arcane.volmlib.util.collection.KList;
|
||||
import art.arcane.iris.util.common.data.IrisCustomData;
|
||||
import art.arcane.volmlib.util.math.RNG;
|
||||
import art.arcane.volmlib.util.matter.MatterCavern;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.Getter;
|
||||
import org.bukkit.Bukkit;
|
||||
@@ -112,7 +111,7 @@ public class WorldObjectPlacer implements IObjectPlacer {
|
||||
|
||||
@Override
|
||||
public boolean isCarved(int x, int y, int z) {
|
||||
return mantle.getMantle().get(x, y, z, MatterCavern.class) != null;
|
||||
return mantle.isCarved(x, y, z);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -18,63 +18,712 @@
|
||||
|
||||
package art.arcane.iris.engine.framework.render;
|
||||
|
||||
import art.arcane.iris.engine.IrisComplex;
|
||||
import art.arcane.iris.engine.framework.Engine;
|
||||
import art.arcane.iris.engine.object.IrisBiome;
|
||||
import art.arcane.iris.engine.object.IrisBiomeGeneratorLink;
|
||||
import art.arcane.iris.util.project.interpolation.IrisInterpolation;
|
||||
import art.arcane.iris.engine.object.IrisDimension;
|
||||
import art.arcane.iris.engine.object.IrisRegion;
|
||||
import art.arcane.iris.engine.river.RiverSample;
|
||||
import art.arcane.iris.engine.river.RiverSection;
|
||||
import art.arcane.iris.engine.river.runtime.IrisRiverRuntime;
|
||||
import art.arcane.iris.util.project.stream.ProceduralStream;
|
||||
|
||||
import java.awt.Color;
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.awt.image.DataBufferInt;
|
||||
import java.util.function.BiFunction;
|
||||
import java.util.Arrays;
|
||||
import java.util.IdentityHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.CancellationException;
|
||||
import java.util.function.BooleanSupplier;
|
||||
|
||||
public class IrisRenderer {
|
||||
private static final int BLUE = Color.BLUE.getRGB();
|
||||
private static final int YELLOW = Color.YELLOW.getRGB();
|
||||
private static final int GREEN = Color.GREEN.getRGB();
|
||||
public final class IrisRenderer {
|
||||
private static final int BLUE = new Color(45, 91, 156).getRGB();
|
||||
private static final int YELLOW = new Color(211, 164, 67).getRGB();
|
||||
private static final int GREEN = new Color(78, 137, 83).getRGB();
|
||||
private static final int RIVER_CHANNEL = new Color(48, 112, 190).getRGB();
|
||||
private static final int RIVER_MOUTH = new Color(54, 164, 205).getRGB();
|
||||
private static final int RIVER_BANK = new Color(92, 146, 78).getRGB();
|
||||
private static final int DRY_CHANNEL = new Color(171, 128, 68).getRGB();
|
||||
private static final int DRY_BANK = new Color(132, 105, 62).getRGB();
|
||||
private static final int NO_RIVER = new Color(28, 31, 38).getRGB();
|
||||
private static final int DEEP_WATER = new Color(20, 48, 92).getRGB();
|
||||
private static final int SHALLOW_WATER = new Color(50, 112, 154).getRGB();
|
||||
private static final int LOWLAND = new Color(78, 128, 76).getRGB();
|
||||
private static final int HIGHLAND = new Color(151, 139, 92).getRGB();
|
||||
private static final int ROCK = new Color(126, 119, 112).getRGB();
|
||||
private static final int SNOW = new Color(226, 230, 232).getRGB();
|
||||
|
||||
private final Engine renderer;
|
||||
|
||||
public IrisRenderer(Engine renderer) {
|
||||
this.renderer = renderer;
|
||||
this.renderer = Objects.requireNonNull(renderer, "renderer");
|
||||
}
|
||||
|
||||
public BufferedImage render(double sx, double sz, double size, int resolution, RenderType currentType) {
|
||||
return render(sx, sz, size, resolution, currentType, () -> false);
|
||||
}
|
||||
|
||||
public BufferedImage render(
|
||||
double sx,
|
||||
double sz,
|
||||
double size,
|
||||
int resolution,
|
||||
RenderType currentType,
|
||||
BooleanSupplier cancelled
|
||||
) {
|
||||
return render(sx, sz, size, resolution, currentType, cancelled, false);
|
||||
}
|
||||
|
||||
public BufferedImage renderStudio(
|
||||
double sx,
|
||||
double sz,
|
||||
double size,
|
||||
int resolution,
|
||||
RenderType currentType,
|
||||
BooleanSupplier cancelled
|
||||
) {
|
||||
return render(sx, sz, size, resolution, currentType, cancelled, true);
|
||||
}
|
||||
|
||||
private BufferedImage render(
|
||||
double sx,
|
||||
double sz,
|
||||
double size,
|
||||
int resolution,
|
||||
RenderType currentType,
|
||||
BooleanSupplier cancelled,
|
||||
boolean studio
|
||||
) {
|
||||
if (!Double.isFinite(sx) || !Double.isFinite(sz) || !Double.isFinite(size) || size <= 0D) {
|
||||
throw new IllegalArgumentException("Vision render coordinates and size must be finite");
|
||||
}
|
||||
if (resolution < 1) {
|
||||
throw new IllegalArgumentException("Vision render resolution must be positive");
|
||||
}
|
||||
Objects.requireNonNull(currentType, "currentType");
|
||||
Objects.requireNonNull(cancelled, "cancelled");
|
||||
checkCancelled(cancelled);
|
||||
|
||||
BufferedImage image = new BufferedImage(resolution, resolution, BufferedImage.TYPE_INT_RGB);
|
||||
int[] pixels = ((DataBufferInt) image.getRaster().getDataBuffer()).getData();
|
||||
BiFunction<Double, Double, Integer> colorFunction = (d, dx) -> 0;
|
||||
|
||||
switch (currentType) {
|
||||
case BIOME, DECORATOR_LOAD, OBJECT_LOAD, LAYER_LOAD ->
|
||||
colorFunction = (x, z) -> renderer.getComplex().getTrueBiomeStream().get(x, z).getColor(renderer, currentType).getRGB();
|
||||
case BIOME_LAND ->
|
||||
colorFunction = (x, z) -> renderer.getComplex().getLandBiomeStream().get(x, z).getColor(renderer, currentType).getRGB();
|
||||
case BIOME_SEA ->
|
||||
colorFunction = (x, z) -> renderer.getComplex().getSeaBiomeStream().get(x, z).getColor(renderer, currentType).getRGB();
|
||||
case REGION ->
|
||||
colorFunction = (x, z) -> renderer.getComplex().getRegionStream().get(x, z).getColor(renderer.getComplex(), currentType).getRGB();
|
||||
case CAVE_LAND ->
|
||||
colorFunction = (x, z) -> renderer.getComplex().getCaveBiomeStream().get(x, z).getColor(renderer, currentType).getRGB();
|
||||
case HEIGHT ->
|
||||
colorFunction = (x, z) -> Color.getHSBColor(renderer.getComplex().getHeightStream().get(x, z).floatValue(), 1f, 1f).getRGB();
|
||||
case CONTINENT -> colorFunction = (x, z) -> {
|
||||
IrisBiome b = renderer.getBiome((int) Math.round(x), renderer.getMaxHeight() - 1, (int) Math.round(z));
|
||||
IrisBiomeGeneratorLink g = b.getGenerators().get(0);
|
||||
if (g.getMax() <= 0) return BLUE;
|
||||
if (g.getMin() < 0) return YELLOW;
|
||||
return GREEN;
|
||||
};
|
||||
double step = size / resolution;
|
||||
if (studio && currentType == RenderType.HEIGHT) {
|
||||
renderHeightAtlas(pixels, resolution, sx, sz, step, renderer, cancelled);
|
||||
return image;
|
||||
}
|
||||
PixelShader shader = shader(currentType, step, studio);
|
||||
if (studio && currentType == RenderType.RIVER) {
|
||||
Arrays.fill(pixels, NO_RIVER);
|
||||
renderRiverAtlas(pixels, resolution, sx, sz, step, renderer.getComplex(), cancelled, false);
|
||||
return image;
|
||||
}
|
||||
if (studio && adaptiveStudioType(currentType)) {
|
||||
renderAdaptiveAtlas(pixels, resolution, sx, sz, step, shader, cancelled);
|
||||
if (currentType == RenderType.BIOME) {
|
||||
renderRiverAtlas(pixels, resolution, sx, sz, step, renderer.getComplex(), cancelled, true);
|
||||
}
|
||||
return image;
|
||||
}
|
||||
int groupSize = sampleGroup(step, resolution);
|
||||
|
||||
double x, z;
|
||||
for (int i = 0; i < resolution; i++) {
|
||||
x = IrisInterpolation.lerp(sx, sx + size, (double) i / (double) resolution);
|
||||
for (int j = 0; j < resolution; j++) {
|
||||
z = IrisInterpolation.lerp(sz, sz + size, (double) j / (double) resolution);
|
||||
pixels[j * resolution + i] = colorFunction.apply(x, z);
|
||||
for (int groupZ = 0; groupZ < resolution; groupZ += groupSize) {
|
||||
checkCancelled(cancelled);
|
||||
int maximumZ = Math.min(resolution, groupZ + groupSize);
|
||||
for (int groupX = 0; groupX < resolution; groupX += groupSize) {
|
||||
checkCancelled(cancelled);
|
||||
int maximumX = Math.min(resolution, groupX + groupSize);
|
||||
for (int pixelZ = groupZ; pixelZ < maximumZ; pixelZ++) {
|
||||
double z = sz + step * pixelZ;
|
||||
int row = pixelZ * resolution;
|
||||
for (int pixelX = groupX; pixelX < maximumX; pixelX++) {
|
||||
checkCancelled(cancelled);
|
||||
double x = sx + step * pixelX;
|
||||
pixels[row + pixelX] = shader.color(x, z);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return image;
|
||||
}
|
||||
|
||||
public static int riverColor(RiverSection section) {
|
||||
Objects.requireNonNull(section, "section");
|
||||
return switch (section) {
|
||||
case CHANNEL -> RIVER_CHANNEL;
|
||||
case MOUTH -> RIVER_MOUTH;
|
||||
case BANK -> RIVER_BANK;
|
||||
case DRY_CHANNEL -> DRY_CHANNEL;
|
||||
case DRY_BANK -> DRY_BANK;
|
||||
case NONE -> NO_RIVER;
|
||||
};
|
||||
}
|
||||
|
||||
public static int heightColor(double height, double maximumHeight, double fluidHeight) {
|
||||
double boundedMaximum = Math.max(1D, maximumHeight);
|
||||
double boundedHeight = clamp(height, 0D, boundedMaximum);
|
||||
double boundedFluid = clamp(fluidHeight, 0D, boundedMaximum);
|
||||
if (boundedHeight <= boundedFluid && boundedFluid > 0D) {
|
||||
return blend(DEEP_WATER, SHALLOW_WATER, boundedHeight / boundedFluid);
|
||||
}
|
||||
|
||||
double landRange = Math.max(1D, boundedMaximum - boundedFluid);
|
||||
double land = clamp((boundedHeight - boundedFluid) / landRange, 0D, 1D);
|
||||
if (land < 0.45D) {
|
||||
return blend(LOWLAND, HIGHLAND, land / 0.45D);
|
||||
}
|
||||
if (land < 0.78D) {
|
||||
return blend(HIGHLAND, ROCK, (land - 0.45D) / 0.33D);
|
||||
}
|
||||
return blend(ROCK, SNOW, (land - 0.78D) / 0.22D);
|
||||
}
|
||||
|
||||
static int sampleGroup(double step, int resolution) {
|
||||
double absoluteStep = Math.abs(step);
|
||||
if (!Double.isFinite(absoluteStep) || absoluteStep <= 0D) {
|
||||
return 1;
|
||||
}
|
||||
return Math.max(1, Math.min(resolution, (int) Math.floor(16D / absoluteStep)));
|
||||
}
|
||||
|
||||
private PixelShader shader(RenderType currentType, double step, boolean studio) {
|
||||
IrisComplex complex = renderer.getComplex();
|
||||
return switch (currentType) {
|
||||
case BIOME, DECORATOR_LOAD, OBJECT_LOAD, LAYER_LOAD -> biomeShader(
|
||||
studio ? complex.getBaseBiomeStream() : complex.getTrueBiomeStream(), currentType);
|
||||
case BIOME_LAND -> biomeShader(complex.getLandBiomeStream(), currentType);
|
||||
case BIOME_SEA -> biomeShader(complex.getSeaBiomeStream(), currentType);
|
||||
case REGION -> regionShader(complex, currentType);
|
||||
case CAVE_LAND -> biomeShader(complex.getCaveBiomeStream(), currentType);
|
||||
case HEIGHT -> heightShader(studio ? complex.getNaturalHeightStream() : complex.getHeightStream());
|
||||
case RIVER -> (double x, double z) -> riverColor(complex, x, z, step);
|
||||
case CONTINENT -> studio
|
||||
? continentShader(complex.getBaseBiomeStream())
|
||||
: this::continentColor;
|
||||
};
|
||||
}
|
||||
|
||||
private static boolean adaptiveStudioType(RenderType type) {
|
||||
return switch (type) {
|
||||
case BIOME, DECORATOR_LOAD, OBJECT_LOAD, LAYER_LOAD, BIOME_LAND, BIOME_SEA, REGION, CAVE_LAND,
|
||||
CONTINENT -> true;
|
||||
case HEIGHT, RIVER -> false;
|
||||
};
|
||||
}
|
||||
|
||||
private static void renderAdaptiveAtlas(
|
||||
int[] pixels,
|
||||
int resolution,
|
||||
double startX,
|
||||
double startZ,
|
||||
double step,
|
||||
PixelShader shader,
|
||||
BooleanSupplier cancelled
|
||||
) {
|
||||
int maximumPixels = Math.max(1, Math.min(16, (int) Math.floor(64D / step)));
|
||||
int blockPixels = Integer.highestOneBit(maximumPixels);
|
||||
AdaptiveSampler sampler = new AdaptiveSampler(pixels, resolution, startX, startZ, step, shader, cancelled);
|
||||
for (int pixelZ = 0; pixelZ < resolution; pixelZ += blockPixels) {
|
||||
int height = Math.min(blockPixels, resolution - pixelZ);
|
||||
for (int pixelX = 0; pixelX < resolution; pixelX += blockPixels) {
|
||||
sampler.render(pixelX, pixelZ, Math.min(blockPixels, resolution - pixelX), height);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void renderHeightAtlas(
|
||||
int[] pixels,
|
||||
int resolution,
|
||||
double startX,
|
||||
double startZ,
|
||||
double step,
|
||||
Engine engine,
|
||||
BooleanSupplier cancelled
|
||||
) {
|
||||
IrisComplex complex = engine.getComplex();
|
||||
IrisDimension dimension = engine.getDimension();
|
||||
double fluidHeight = dimension == null ? 0D : dimension.getFluidHeight();
|
||||
int maximumPixels = Math.max(1, Math.min(16, (int) Math.floor(64D / step)));
|
||||
int blockPixels = Integer.highestOneBit(maximumPixels);
|
||||
HeightSampler sampler = new HeightSampler(
|
||||
pixels,
|
||||
resolution,
|
||||
startX,
|
||||
startZ,
|
||||
step,
|
||||
complex.getNaturalHeightStream(),
|
||||
engine.getHeight(),
|
||||
fluidHeight,
|
||||
cancelled
|
||||
);
|
||||
for (int pixelZ = 0; pixelZ < resolution; pixelZ += blockPixels) {
|
||||
int height = Math.min(blockPixels, resolution - pixelZ);
|
||||
for (int pixelX = 0; pixelX < resolution; pixelX += blockPixels) {
|
||||
sampler.render(pixelX, pixelZ, Math.min(blockPixels, resolution - pixelX), height);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void renderRiverAtlas(
|
||||
int[] pixels,
|
||||
int resolution,
|
||||
double startX,
|
||||
double startZ,
|
||||
double step,
|
||||
IrisComplex complex,
|
||||
BooleanSupplier cancelled,
|
||||
boolean composite
|
||||
) {
|
||||
IrisRiverRuntime runtime = complex.getRiverRuntime();
|
||||
if (runtime == null) {
|
||||
return;
|
||||
}
|
||||
int maximumPixels = Math.max(1, Math.min(16, (int) Math.floor(64D / step)));
|
||||
int blockPixels = Integer.highestOneBit(maximumPixels);
|
||||
for (int pixelZ = 0; pixelZ < resolution; pixelZ += blockPixels) {
|
||||
int height = Math.min(blockPixels, resolution - pixelZ);
|
||||
for (int pixelX = 0; pixelX < resolution; pixelX += blockPixels) {
|
||||
renderRiverBlock(
|
||||
pixels,
|
||||
resolution,
|
||||
startX,
|
||||
startZ,
|
||||
step,
|
||||
runtime,
|
||||
cancelled,
|
||||
composite,
|
||||
pixelX,
|
||||
pixelZ,
|
||||
Math.min(blockPixels, resolution - pixelX),
|
||||
height
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void renderRiverBlock(
|
||||
int[] pixels,
|
||||
int resolution,
|
||||
double startX,
|
||||
double startZ,
|
||||
double step,
|
||||
IrisRiverRuntime runtime,
|
||||
BooleanSupplier cancelled,
|
||||
boolean composite,
|
||||
int pixelX,
|
||||
int pixelZ,
|
||||
int width,
|
||||
int height
|
||||
) {
|
||||
checkCancelled(cancelled);
|
||||
RiverSample sample = runtime.sampleFootprint(
|
||||
startX + pixelX * step,
|
||||
startZ + pixelZ * step,
|
||||
startX + (pixelX + width) * step,
|
||||
startZ + (pixelZ + height) * step
|
||||
);
|
||||
if (!sample.present()) {
|
||||
return;
|
||||
}
|
||||
if (width == 1 && height == 1) {
|
||||
int index = pixelZ * resolution + pixelX;
|
||||
int color = riverColor(sample.section());
|
||||
pixels[index] = composite ? riverCompositeColor(pixels[index], sample.section(), color) : color;
|
||||
return;
|
||||
}
|
||||
int leftWidth = Math.max(1, width / 2);
|
||||
int rightWidth = width - leftWidth;
|
||||
int topHeight = Math.max(1, height / 2);
|
||||
int bottomHeight = height - topHeight;
|
||||
renderRiverBlock(pixels, resolution, startX, startZ, step, runtime, cancelled, composite,
|
||||
pixelX, pixelZ, leftWidth, topHeight);
|
||||
if (rightWidth > 0) {
|
||||
renderRiverBlock(pixels, resolution, startX, startZ, step, runtime, cancelled, composite,
|
||||
pixelX + leftWidth, pixelZ, rightWidth, topHeight);
|
||||
}
|
||||
if (bottomHeight > 0) {
|
||||
renderRiverBlock(pixels, resolution, startX, startZ, step, runtime, cancelled, composite,
|
||||
pixelX, pixelZ + topHeight, leftWidth, bottomHeight);
|
||||
if (rightWidth > 0) {
|
||||
renderRiverBlock(pixels, resolution, startX, startZ, step, runtime, cancelled, composite,
|
||||
pixelX + leftWidth, pixelZ + topHeight, rightWidth, bottomHeight);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static int riverCompositeColor(int base, RiverSection section, int river) {
|
||||
return switch (section) {
|
||||
case CHANNEL, MOUTH -> river;
|
||||
case BANK -> blend(base, river, 0.68D);
|
||||
case DRY_CHANNEL -> blend(base, river, 0.88D);
|
||||
case DRY_BANK -> blend(base, river, 0.58D);
|
||||
case NONE -> base;
|
||||
};
|
||||
}
|
||||
|
||||
private PixelShader biomeShader(ProceduralStream<IrisBiome> stream, RenderType currentType) {
|
||||
IdentityHashMap<IrisBiome, Integer> colors = new IdentityHashMap<>();
|
||||
return (double x, double z) -> {
|
||||
IrisBiome biome = stream.get(x, z);
|
||||
Integer color = colors.get(biome);
|
||||
if (color == null) {
|
||||
color = biome.getColor(renderer, currentType).getRGB();
|
||||
colors.put(biome, color);
|
||||
}
|
||||
return color;
|
||||
};
|
||||
}
|
||||
|
||||
private PixelShader regionShader(IrisComplex complex, RenderType currentType) {
|
||||
ProceduralStream<IrisRegion> stream = complex.getRegionStream();
|
||||
IdentityHashMap<IrisRegion, Integer> colors = new IdentityHashMap<>();
|
||||
return (double x, double z) -> {
|
||||
IrisRegion region = stream.get(x, z);
|
||||
Integer color = colors.get(region);
|
||||
if (color == null) {
|
||||
color = region.getColor(complex, currentType).getRGB();
|
||||
colors.put(region, color);
|
||||
}
|
||||
return color;
|
||||
};
|
||||
}
|
||||
|
||||
private PixelShader heightShader(ProceduralStream<Double> stream) {
|
||||
double maximumHeight = renderer.getHeight();
|
||||
IrisDimension dimension = renderer.getDimension();
|
||||
double fluidHeight = dimension == null ? 0D : dimension.getFluidHeight();
|
||||
return (double x, double z) -> heightColor(stream.getDouble(x, z), maximumHeight, fluidHeight);
|
||||
}
|
||||
|
||||
private int riverColor(IrisComplex complex, double x, double z, double step) {
|
||||
IrisRiverRuntime runtime = complex.getRiverRuntime();
|
||||
if (runtime == null) {
|
||||
return riverColor(RiverSection.NONE);
|
||||
}
|
||||
double endX = x + step;
|
||||
double endZ = z + step;
|
||||
RiverSample sample = runtime.sampleFootprint(
|
||||
StrictMath.min(x, endX),
|
||||
StrictMath.min(z, endZ),
|
||||
StrictMath.max(x, endX),
|
||||
StrictMath.max(z, endZ)
|
||||
);
|
||||
return riverColor(sample.section());
|
||||
}
|
||||
|
||||
private int continentColor(double x, double z) {
|
||||
IrisBiome biome = renderer.getBiome(
|
||||
(int) Math.round(x),
|
||||
renderer.getMaxHeight() - 1,
|
||||
(int) Math.round(z)
|
||||
);
|
||||
return continentColor(biome);
|
||||
}
|
||||
|
||||
private PixelShader continentShader(ProceduralStream<IrisBiome> stream) {
|
||||
IdentityHashMap<IrisBiome, Integer> colors = new IdentityHashMap<>();
|
||||
return (double x, double z) -> {
|
||||
IrisBiome biome = stream.get(x, z);
|
||||
Integer color = colors.get(biome);
|
||||
if (color == null) {
|
||||
color = continentColor(biome);
|
||||
colors.put(biome, color);
|
||||
}
|
||||
return color;
|
||||
};
|
||||
}
|
||||
|
||||
private int continentColor(IrisBiome biome) {
|
||||
if (biome == null) {
|
||||
return GREEN;
|
||||
}
|
||||
List<IrisBiomeGeneratorLink> generators = biome.getGenerators();
|
||||
if (generators.isEmpty()) {
|
||||
return GREEN;
|
||||
}
|
||||
IrisBiomeGeneratorLink generator = generators.get(0);
|
||||
if (generator.getMax() <= 0D) {
|
||||
return BLUE;
|
||||
}
|
||||
if (generator.getMin() < 0D) {
|
||||
return YELLOW;
|
||||
}
|
||||
return GREEN;
|
||||
}
|
||||
|
||||
private static int blend(int first, int second, double progress) {
|
||||
double bounded = clamp(progress, 0D, 1D);
|
||||
int red = (int) Math.round(((first >> 16) & 0xFF) * (1D - bounded) + ((second >> 16) & 0xFF) * bounded);
|
||||
int green = (int) Math.round(((first >> 8) & 0xFF) * (1D - bounded) + ((second >> 8) & 0xFF) * bounded);
|
||||
int blue = (int) Math.round((first & 0xFF) * (1D - bounded) + (second & 0xFF) * bounded);
|
||||
return (red << 16) | (green << 8) | blue;
|
||||
}
|
||||
|
||||
private static double clamp(double value, double minimum, double maximum) {
|
||||
return Math.max(minimum, Math.min(maximum, value));
|
||||
}
|
||||
|
||||
private static void checkCancelled(BooleanSupplier cancelled) {
|
||||
if (cancelled.getAsBoolean() || Thread.currentThread().isInterrupted()) {
|
||||
throw new CancellationException("Vision render cancelled");
|
||||
}
|
||||
}
|
||||
|
||||
@FunctionalInterface
|
||||
private interface PixelShader {
|
||||
int color(double x, double z);
|
||||
}
|
||||
|
||||
private static final class AdaptiveSampler {
|
||||
private final int[] pixels;
|
||||
private final boolean[] sampled;
|
||||
private final int resolution;
|
||||
private final double startX;
|
||||
private final double startZ;
|
||||
private final double step;
|
||||
private final PixelShader shader;
|
||||
private final BooleanSupplier cancelled;
|
||||
|
||||
private AdaptiveSampler(
|
||||
int[] pixels,
|
||||
int resolution,
|
||||
double startX,
|
||||
double startZ,
|
||||
double step,
|
||||
PixelShader shader,
|
||||
BooleanSupplier cancelled
|
||||
) {
|
||||
this.pixels = pixels;
|
||||
this.sampled = new boolean[pixels.length];
|
||||
this.resolution = resolution;
|
||||
this.startX = startX;
|
||||
this.startZ = startZ;
|
||||
this.step = step;
|
||||
this.shader = shader;
|
||||
this.cancelled = cancelled;
|
||||
}
|
||||
|
||||
private void render(int pixelX, int pixelZ, int width, int height) {
|
||||
checkCancelled(cancelled);
|
||||
if (width == 1 && height == 1) {
|
||||
sample(pixelX, pixelZ);
|
||||
return;
|
||||
}
|
||||
int maximumX = pixelX + width - 1;
|
||||
int maximumZ = pixelZ + height - 1;
|
||||
int centerX = pixelX + width / 2;
|
||||
int centerZ = pixelZ + height / 2;
|
||||
int color = sample(pixelX, pixelZ);
|
||||
if (sample(maximumX, pixelZ) == color
|
||||
&& sample(pixelX, maximumZ) == color
|
||||
&& sample(maximumX, maximumZ) == color
|
||||
&& sample(centerX, centerZ) == color) {
|
||||
fill(pixelX, pixelZ, width, height, color);
|
||||
return;
|
||||
}
|
||||
int leftWidth = Math.max(1, width / 2);
|
||||
int rightWidth = width - leftWidth;
|
||||
int topHeight = Math.max(1, height / 2);
|
||||
int bottomHeight = height - topHeight;
|
||||
render(pixelX, pixelZ, leftWidth, topHeight);
|
||||
if (rightWidth > 0) {
|
||||
render(pixelX + leftWidth, pixelZ, rightWidth, topHeight);
|
||||
}
|
||||
if (bottomHeight > 0) {
|
||||
render(pixelX, pixelZ + topHeight, leftWidth, bottomHeight);
|
||||
if (rightWidth > 0) {
|
||||
render(pixelX + leftWidth, pixelZ + topHeight, rightWidth, bottomHeight);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private int sample(int pixelX, int pixelZ) {
|
||||
int index = pixelZ * resolution + pixelX;
|
||||
if (!sampled[index]) {
|
||||
pixels[index] = shader.color(startX + pixelX * step, startZ + pixelZ * step);
|
||||
sampled[index] = true;
|
||||
}
|
||||
return pixels[index];
|
||||
}
|
||||
|
||||
private void fill(int pixelX, int pixelZ, int width, int height, int color) {
|
||||
for (int row = pixelZ; row < pixelZ + height; row++) {
|
||||
int start = row * resolution + pixelX;
|
||||
Arrays.fill(pixels, start, start + width, color);
|
||||
Arrays.fill(sampled, start, start + width, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static final class HeightSampler {
|
||||
private static final double MAXIMUM_INTERPOLATION_ERROR = 1.5D;
|
||||
|
||||
private final int[] pixels;
|
||||
private final double[] heights;
|
||||
private final boolean[] sampled;
|
||||
private final int resolution;
|
||||
private final double startX;
|
||||
private final double startZ;
|
||||
private final double step;
|
||||
private final ProceduralStream<Double> stream;
|
||||
private final double maximumHeight;
|
||||
private final double fluidHeight;
|
||||
private final BooleanSupplier cancelled;
|
||||
|
||||
private HeightSampler(
|
||||
int[] pixels,
|
||||
int resolution,
|
||||
double startX,
|
||||
double startZ,
|
||||
double step,
|
||||
ProceduralStream<Double> stream,
|
||||
double maximumHeight,
|
||||
double fluidHeight,
|
||||
BooleanSupplier cancelled
|
||||
) {
|
||||
this.pixels = pixels;
|
||||
this.heights = new double[pixels.length];
|
||||
this.sampled = new boolean[pixels.length];
|
||||
this.resolution = resolution;
|
||||
this.startX = startX;
|
||||
this.startZ = startZ;
|
||||
this.step = step;
|
||||
this.stream = stream;
|
||||
this.maximumHeight = maximumHeight;
|
||||
this.fluidHeight = fluidHeight;
|
||||
this.cancelled = cancelled;
|
||||
}
|
||||
|
||||
private void render(int pixelX, int pixelZ, int width, int height) {
|
||||
checkCancelled(cancelled);
|
||||
if (width == 1 && height == 1) {
|
||||
pixels[pixelZ * resolution + pixelX] = heightColor(
|
||||
sample(pixelX, pixelZ), maximumHeight, fluidHeight);
|
||||
return;
|
||||
}
|
||||
int maximumX = pixelX + width - 1;
|
||||
int maximumZ = pixelZ + height - 1;
|
||||
int centerX = pixelX + width / 2;
|
||||
int centerZ = pixelZ + height / 2;
|
||||
double topLeft = sample(pixelX, pixelZ);
|
||||
double topRight = sample(maximumX, pixelZ);
|
||||
double bottomLeft = sample(pixelX, maximumZ);
|
||||
double bottomRight = sample(maximumX, maximumZ);
|
||||
if (matchesPlane(pixelX, pixelZ, width, height, topLeft, topRight, bottomLeft, bottomRight,
|
||||
centerX, pixelZ)
|
||||
&& matchesPlane(pixelX, pixelZ, width, height, topLeft, topRight, bottomLeft, bottomRight,
|
||||
pixelX, centerZ)
|
||||
&& matchesPlane(pixelX, pixelZ, width, height, topLeft, topRight, bottomLeft, bottomRight,
|
||||
maximumX, centerZ)
|
||||
&& matchesPlane(pixelX, pixelZ, width, height, topLeft, topRight, bottomLeft, bottomRight,
|
||||
centerX, maximumZ)
|
||||
&& matchesPlane(pixelX, pixelZ, width, height, topLeft, topRight, bottomLeft, bottomRight,
|
||||
centerX, centerZ)) {
|
||||
fillPlane(pixelX, pixelZ, width, height, topLeft, topRight, bottomLeft, bottomRight);
|
||||
return;
|
||||
}
|
||||
int leftWidth = Math.max(1, width / 2);
|
||||
int rightWidth = width - leftWidth;
|
||||
int topHeight = Math.max(1, height / 2);
|
||||
int bottomHeight = height - topHeight;
|
||||
render(pixelX, pixelZ, leftWidth, topHeight);
|
||||
if (rightWidth > 0) {
|
||||
render(pixelX + leftWidth, pixelZ, rightWidth, topHeight);
|
||||
}
|
||||
if (bottomHeight > 0) {
|
||||
render(pixelX, pixelZ + topHeight, leftWidth, bottomHeight);
|
||||
if (rightWidth > 0) {
|
||||
render(pixelX + leftWidth, pixelZ + topHeight, rightWidth, bottomHeight);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private boolean matchesPlane(
|
||||
int pixelX,
|
||||
int pixelZ,
|
||||
int width,
|
||||
int height,
|
||||
double topLeft,
|
||||
double topRight,
|
||||
double bottomLeft,
|
||||
double bottomRight,
|
||||
int sampleX,
|
||||
int sampleZ
|
||||
) {
|
||||
double predicted = interpolate(
|
||||
pixelX,
|
||||
pixelZ,
|
||||
width,
|
||||
height,
|
||||
topLeft,
|
||||
topRight,
|
||||
bottomLeft,
|
||||
bottomRight,
|
||||
sampleX,
|
||||
sampleZ
|
||||
);
|
||||
return Math.abs(sample(sampleX, sampleZ) - predicted) <= MAXIMUM_INTERPOLATION_ERROR;
|
||||
}
|
||||
|
||||
private void fillPlane(
|
||||
int pixelX,
|
||||
int pixelZ,
|
||||
int width,
|
||||
int height,
|
||||
double topLeft,
|
||||
double topRight,
|
||||
double bottomLeft,
|
||||
double bottomRight
|
||||
) {
|
||||
for (int row = pixelZ; row < pixelZ + height; row++) {
|
||||
int offset = row * resolution;
|
||||
for (int column = pixelX; column < pixelX + width; column++) {
|
||||
double value = interpolate(
|
||||
pixelX,
|
||||
pixelZ,
|
||||
width,
|
||||
height,
|
||||
topLeft,
|
||||
topRight,
|
||||
bottomLeft,
|
||||
bottomRight,
|
||||
column,
|
||||
row
|
||||
);
|
||||
pixels[offset + column] = heightColor(value, maximumHeight, fluidHeight);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private double sample(int pixelX, int pixelZ) {
|
||||
int index = pixelZ * resolution + pixelX;
|
||||
if (!sampled[index]) {
|
||||
heights[index] = stream.getDouble(startX + pixelX * step, startZ + pixelZ * step);
|
||||
sampled[index] = true;
|
||||
}
|
||||
return heights[index];
|
||||
}
|
||||
|
||||
private static double interpolate(
|
||||
int pixelX,
|
||||
int pixelZ,
|
||||
int width,
|
||||
int height,
|
||||
double topLeft,
|
||||
double topRight,
|
||||
double bottomLeft,
|
||||
double bottomRight,
|
||||
int sampleX,
|
||||
int sampleZ
|
||||
) {
|
||||
double x = width <= 1 ? 0D : (sampleX - pixelX) / (double) (width - 1);
|
||||
double z = height <= 1 ? 0D : (sampleZ - pixelZ) / (double) (height - 1);
|
||||
double top = topLeft + (topRight - topLeft) * x;
|
||||
double bottom = bottomLeft + (bottomRight - bottomLeft) * x;
|
||||
return top + (bottom - top) * z;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,5 +19,5 @@
|
||||
package art.arcane.iris.engine.framework.render;
|
||||
|
||||
public enum RenderType {
|
||||
BIOME, BIOME_LAND, BIOME_SEA, REGION, CAVE_LAND, HEIGHT, OBJECT_LOAD, DECORATOR_LOAD, CONTINENT, LAYER_LOAD
|
||||
BIOME, BIOME_LAND, BIOME_SEA, REGION, CAVE_LAND, RIVER, HEIGHT, OBJECT_LOAD, DECORATOR_LOAD, CONTINENT, LAYER_LOAD
|
||||
}
|
||||
|
||||
@@ -27,6 +27,8 @@ import art.arcane.iris.engine.framework.EngineTarget;
|
||||
import art.arcane.iris.engine.framework.TreeBlockMaterial;
|
||||
import art.arcane.iris.engine.mantle.components.MantleObjectComponent;
|
||||
import art.arcane.iris.engine.object.IrisDimension;
|
||||
import art.arcane.iris.engine.river.cave.RiverCaveHydrology;
|
||||
import art.arcane.iris.engine.river.cave.RiverCaveHydrologyStorage;
|
||||
import art.arcane.iris.engine.object.IrisPosition;
|
||||
import art.arcane.volmlib.util.collection.KList;
|
||||
import art.arcane.iris.util.common.data.B;
|
||||
@@ -102,7 +104,7 @@ public interface EngineMantle extends MatterGenerator {
|
||||
}
|
||||
|
||||
default int getHighest(int x, int z, IrisData data, boolean ignoreFluid) {
|
||||
return ignoreFluid ? trueHeight(x, z) : Math.max(trueHeight(x, z), getEngine().getDimension().getFluidHeight());
|
||||
return ignoreFluid ? trueHeight(x, z) : Math.max(trueHeight(x, z), getFluidHeight(x, z));
|
||||
}
|
||||
|
||||
default int trueHeight(int x, int z) {
|
||||
@@ -110,6 +112,10 @@ public interface EngineMantle extends MatterGenerator {
|
||||
}
|
||||
|
||||
default boolean isCarved(int x, int h, int z) {
|
||||
RiverCaveHydrology hydrology = RiverCaveHydrologyStorage.getIfPresent(getMantle(), x, h, z);
|
||||
if (hydrology != null) {
|
||||
return hydrology.carves();
|
||||
}
|
||||
return getMantle().get(x, h, z, MatterCavern.class) != null;
|
||||
}
|
||||
|
||||
@@ -125,13 +131,17 @@ public interface EngineMantle extends MatterGenerator {
|
||||
}
|
||||
|
||||
default boolean isUnderwater(int x, int z) {
|
||||
return getHighest(x, z, true) <= getFluidHeight();
|
||||
return getHighest(x, z, true) < getFluidHeight(x, z);
|
||||
}
|
||||
|
||||
default int getFluidHeight() {
|
||||
return getEngine().getDimension().getFluidHeight();
|
||||
}
|
||||
|
||||
default int getFluidHeight(int x, int z) {
|
||||
return (int) Math.round(getComplex().getRiverWaterSurfaceStream().get(x, z));
|
||||
}
|
||||
|
||||
default boolean isDebugSmartBore() {
|
||||
return getEngine().getDimension().isDebugSmartBore();
|
||||
}
|
||||
|
||||
@@ -34,6 +34,10 @@ public interface MantleComponent extends Comparable<MantleComponent> {
|
||||
|
||||
int getRadius();
|
||||
|
||||
default int getInputRadius() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
default IrisData getData() {
|
||||
return getEngineMantle().getData();
|
||||
}
|
||||
|
||||
@@ -31,6 +31,7 @@ import art.arcane.iris.engine.object.IObjectPlacer;
|
||||
import art.arcane.iris.engine.object.IrisGeneratorStyle;
|
||||
import art.arcane.iris.engine.object.IrisPosition;
|
||||
import art.arcane.iris.engine.object.TileData;
|
||||
import art.arcane.iris.engine.river.cave.RiverCaveHydrology;
|
||||
import art.arcane.volmlib.util.collection.KSet;
|
||||
import art.arcane.volmlib.util.documentation.ChunkCoordinates;
|
||||
import art.arcane.volmlib.util.function.Function3;
|
||||
@@ -181,6 +182,10 @@ public class MantleWriter implements IObjectPlacer, AutoCloseable {
|
||||
if (chunk == null) return;
|
||||
|
||||
Matter matter = chunk.getOrCreate(y >> 4);
|
||||
if ((t instanceof PlatformBlockState || t instanceof MatterCavern)
|
||||
&& hasProtectedHydrology(matter, x, y, z)) {
|
||||
return;
|
||||
}
|
||||
if (t instanceof PlatformBlockState) {
|
||||
clearDeferredPlacement(matter, x, y, z);
|
||||
}
|
||||
@@ -206,6 +211,9 @@ public class MantleWriter implements IObjectPlacer, AutoCloseable {
|
||||
}
|
||||
|
||||
Matter matter = chunk.getOrCreate(y >> 4);
|
||||
if (hasProtectedHydrology(matter, x, y, z)) {
|
||||
return false;
|
||||
}
|
||||
MatterCavern existing = matter.<MatterCavern>slice(MatterCavern.class).get(x & 15, y & 15, z & 15);
|
||||
if (existing != null) {
|
||||
return false;
|
||||
@@ -226,6 +234,9 @@ public class MantleWriter implements IObjectPlacer, AutoCloseable {
|
||||
}
|
||||
|
||||
Matter matter = chunk.getOrCreate(y >> 4);
|
||||
if (hasProtectedHydrology(matter, x, y, z)) {
|
||||
return false;
|
||||
}
|
||||
if (matter.hasSlice(PlatformBlockState.class)) {
|
||||
matter.<PlatformBlockState>getSlice(PlatformBlockState.class).set(x & 15, y & 15, z & 15, null);
|
||||
}
|
||||
@@ -248,6 +259,9 @@ public class MantleWriter implements IObjectPlacer, AutoCloseable {
|
||||
+ x + "," + y + "," + z);
|
||||
}
|
||||
Matter matter = chunk.getOrCreate(y >> 4);
|
||||
if (hasProtectedHydrology(matter, x, y, z)) {
|
||||
return;
|
||||
}
|
||||
if (matter.hasSlice(PlatformBlockState.class)) {
|
||||
matter.<PlatformBlockState>getSlice(PlatformBlockState.class).set(x & 15, y & 15, z & 15, null);
|
||||
}
|
||||
@@ -271,6 +285,9 @@ public class MantleWriter implements IObjectPlacer, AutoCloseable {
|
||||
if (matter == null) {
|
||||
return;
|
||||
}
|
||||
if (hasProtectedHydrology(matter, x, y, z)) {
|
||||
return;
|
||||
}
|
||||
if (matter.hasSlice(PlatformBlockState.class)) {
|
||||
matter.<PlatformBlockState>getSlice(PlatformBlockState.class).set(x & 15, y & 15, z & 15, null);
|
||||
}
|
||||
@@ -328,9 +345,22 @@ public class MantleWriter implements IObjectPlacer, AutoCloseable {
|
||||
if (matter == null || !matter.hasSlice(type)) {
|
||||
return;
|
||||
}
|
||||
if ((type == PlatformBlockState.class || type == MatterCavern.class)
|
||||
&& hasProtectedHydrology(matter, x, y, z)) {
|
||||
return;
|
||||
}
|
||||
matter.getSlice(type).set(x & 15, y & 15, z & 15, null);
|
||||
}
|
||||
|
||||
private static boolean hasProtectedHydrology(Matter matter, int x, int y, int z) {
|
||||
if (!matter.hasSlice(RiverCaveHydrology.class)) {
|
||||
return false;
|
||||
}
|
||||
RiverCaveHydrology hydrology = matter.<RiverCaveHydrology>getSlice(RiverCaveHydrology.class)
|
||||
.get(x & 15, y & 15, z & 15);
|
||||
return hydrology != null && hydrology.protectsPlacement();
|
||||
}
|
||||
|
||||
private static void clearDeferredPlacement(Matter matter, int x, int y, int z) {
|
||||
if (matter.hasSlice(Identifier.class)) {
|
||||
matter.<Identifier>getSlice(Identifier.class).set(x & 15, y & 15, z & 15, null);
|
||||
@@ -425,6 +455,10 @@ public class MantleWriter implements IObjectPlacer, AutoCloseable {
|
||||
|
||||
@Override
|
||||
public boolean isCarved(int x, int y, int z) {
|
||||
RiverCaveHydrology hydrology = getDataIfPresent(x, y, z, RiverCaveHydrology.class);
|
||||
if (hydrology != null) {
|
||||
return hydrology.carves();
|
||||
}
|
||||
return getDataIfPresent(x, y, z, MatterCavern.class) != null;
|
||||
}
|
||||
|
||||
@@ -449,15 +483,28 @@ public class MantleWriter implements IObjectPlacer, AutoCloseable {
|
||||
}
|
||||
|
||||
Matter matter = chunk.get(section);
|
||||
if (matter == null || !matter.hasSlice(MatterCavern.class)) {
|
||||
if (matter == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
MatterSlice<MatterCavern> slice = matter.getSlice(MatterCavern.class);
|
||||
MatterSlice<MatterCavern> cavernSlice = matter.hasSlice(MatterCavern.class)
|
||||
? matter.getSlice(MatterCavern.class)
|
||||
: null;
|
||||
MatterSlice<RiverCaveHydrology> hydrologySlice = matter.hasSlice(RiverCaveHydrology.class)
|
||||
? matter.getSlice(RiverCaveHydrology.class)
|
||||
: null;
|
||||
if (cavernSlice == null && hydrologySlice == null) {
|
||||
continue;
|
||||
}
|
||||
int sectionBaseY = section << 4;
|
||||
int sectionMaxY = Math.min(cappedHeight, sectionBaseY + 16);
|
||||
for (int y = sectionBaseY; y < sectionMaxY; y++) {
|
||||
if (slice.get(localX, y & 15, localZ) != null) {
|
||||
RiverCaveHydrology hydrology = hydrologySlice == null
|
||||
? null
|
||||
: hydrologySlice.get(localX, y & 15, localZ);
|
||||
if (hydrology != null) {
|
||||
carvedColumn[y] = hydrology.carves() ? (byte) 1 : 0;
|
||||
} else if (cavernSlice != null && cavernSlice.get(localX, y & 15, localZ) != null) {
|
||||
carvedColumn[y] = 1;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -61,15 +61,14 @@ public final class CarveOrphanSweep {
|
||||
int[] surfaceHeights,
|
||||
int maxSurfaceBreakDepth,
|
||||
int worldCeilingY,
|
||||
int[] surfaceFluidBoundaryStartY,
|
||||
int fluidHeight
|
||||
long[] surfaceFluidBoundaries
|
||||
) {
|
||||
if (chunk == null) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return sweep(surfaceHeights, maxSurfaceBreakDepth, 0, worldCeilingY,
|
||||
new MantleCarveAccess(chunk, surfaceFluidBoundaryStartY, fluidHeight));
|
||||
new MantleCarveAccess(chunk, surfaceFluidBoundaries));
|
||||
}
|
||||
|
||||
public static int sweep(int[] surfaceHeights, int maxSurfaceBreakDepth, int worldFloorY, int worldCeilingY, CarveAccess access) {
|
||||
@@ -229,15 +228,13 @@ public final class CarveOrphanSweep {
|
||||
|
||||
private static final class MantleCarveAccess implements CarveAccess {
|
||||
private final MantleChunk<Matter> chunk;
|
||||
private final int[] surfaceFluidBoundaryStartY;
|
||||
private final int fluidHeight;
|
||||
private final long[] surfaceFluidBoundaries;
|
||||
private MatterSlice<MatterCavern> cachedSlice;
|
||||
private int cachedSectionIndex = -1;
|
||||
|
||||
private MantleCarveAccess(MantleChunk<Matter> chunk, int[] surfaceFluidBoundaryStartY, int fluidHeight) {
|
||||
private MantleCarveAccess(MantleChunk<Matter> chunk, long[] surfaceFluidBoundaries) {
|
||||
this.chunk = chunk;
|
||||
this.surfaceFluidBoundaryStartY = surfaceFluidBoundaryStartY;
|
||||
this.fluidHeight = fluidHeight;
|
||||
this.surfaceFluidBoundaries = surfaceFluidBoundaries;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -261,7 +258,7 @@ public final class CarveOrphanSweep {
|
||||
@Override
|
||||
public boolean isProtected(int localX, int y, int localZ) {
|
||||
int columnIndex = PowerOfTwoCoordinates.packLocal16(localX, localZ);
|
||||
return SurfaceFluidBoundaryPlan.protects(surfaceFluidBoundaryStartY, columnIndex, y, fluidHeight);
|
||||
return SurfaceFluidBoundaryPlan.protects(surfaceFluidBoundaries, columnIndex, y);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
+9
-1
@@ -23,6 +23,7 @@ import art.arcane.iris.engine.data.cache.Cache;
|
||||
import art.arcane.iris.engine.framework.Engine;
|
||||
import art.arcane.iris.engine.object.IObjectPlacer;
|
||||
import art.arcane.iris.engine.object.TileData;
|
||||
import art.arcane.iris.engine.river.cave.RiverCaveHydrology;
|
||||
import art.arcane.iris.spi.PlatformBlockState;
|
||||
import art.arcane.iris.util.common.data.B;
|
||||
import art.arcane.volmlib.util.collection.KList;
|
||||
@@ -71,6 +72,12 @@ final class CaveObjectPlacementTransaction implements IObjectPlacer {
|
||||
discard();
|
||||
return CommitResult.REJECTED_BOUNDS;
|
||||
}
|
||||
RiverCaveHydrology hydrology = delegate.getData(
|
||||
mutation.x(), mutation.y(), mutation.z(), RiverCaveHydrology.class);
|
||||
if (hydrology != null && hydrology.protectsPlacement()) {
|
||||
discard();
|
||||
return CommitResult.REJECTED_HYDROLOGY;
|
||||
}
|
||||
}
|
||||
|
||||
for (BufferedMutation mutation : mutations) {
|
||||
@@ -245,7 +252,8 @@ final class CaveObjectPlacementTransaction implements IObjectPlacer {
|
||||
enum CommitResult {
|
||||
COMMITTED,
|
||||
EMPTY,
|
||||
REJECTED_BOUNDS
|
||||
REJECTED_BOUNDS,
|
||||
REJECTED_HYDROLOGY
|
||||
}
|
||||
|
||||
private interface BufferedMutation {
|
||||
|
||||
+66
@@ -0,0 +1,66 @@
|
||||
package art.arcane.iris.engine.mantle.components;
|
||||
|
||||
import art.arcane.iris.core.loader.IrisData;
|
||||
import art.arcane.iris.engine.object.IrisGeneratorStyle;
|
||||
import art.arcane.iris.engine.object.NoiseStyle;
|
||||
import art.arcane.iris.engine.river.cave.RiverCaveGrottoShape;
|
||||
import art.arcane.iris.engine.river.cave.RiverCavePlannerSettings;
|
||||
import art.arcane.iris.engine.river.cave.RiverCaveSource;
|
||||
import art.arcane.iris.util.project.noise.CNG;
|
||||
import art.arcane.volmlib.util.math.RNG;
|
||||
|
||||
final class ConfiguredRiverGrottoShape implements RiverCaveGrottoShape {
|
||||
private static final long SHAPE_SALT = 0x3C6EF372FE94F82BL;
|
||||
private static final long WARP_X_SALT = 0xA54FF53A5F1D36F1L;
|
||||
private static final long WARP_Y_SALT = 0x510E527FADE682D1L;
|
||||
private static final long WARP_Z_SALT = 0x9B05688C2B3E6C1FL;
|
||||
|
||||
private final CNG shape;
|
||||
private final CNG warpX;
|
||||
private final CNG warpY;
|
||||
private final CNG warpZ;
|
||||
private final double warpStrength;
|
||||
|
||||
ConfiguredRiverGrottoShape(
|
||||
long seed,
|
||||
IrisData data,
|
||||
IrisGeneratorStyle shapeStyle,
|
||||
IrisGeneratorStyle warpStyle,
|
||||
double warpStrength
|
||||
) {
|
||||
IrisGeneratorStyle resolvedShape = shapeStyle == null
|
||||
? new IrisGeneratorStyle(NoiseStyle.FLAT)
|
||||
: shapeStyle;
|
||||
IrisGeneratorStyle resolvedWarp = warpStyle == null
|
||||
? new IrisGeneratorStyle(NoiseStyle.FLAT)
|
||||
: warpStyle;
|
||||
shape = resolvedShape.createNoCache(new RNG(seed ^ SHAPE_SALT), data);
|
||||
warpX = resolvedWarp.createNoCache(new RNG(seed ^ WARP_X_SALT), data);
|
||||
warpY = resolvedWarp.createNoCache(new RNG(seed ^ WARP_Y_SALT), data);
|
||||
warpZ = resolvedWarp.createNoCache(new RNG(seed ^ WARP_Z_SALT), data);
|
||||
this.warpStrength = Math.max(0D, warpStrength);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean contains(
|
||||
RiverCaveSource source,
|
||||
RiverCavePlannerSettings settings,
|
||||
int offsetX,
|
||||
int offsetY,
|
||||
int offsetZ
|
||||
) {
|
||||
double worldX = source.target().x() + offsetX;
|
||||
double worldY = source.target().y() + offsetY;
|
||||
double worldZ = source.target().z() + offsetZ;
|
||||
double warpedX = offsetX + warpX.fitDouble(-warpStrength, warpStrength, worldX, worldY, worldZ);
|
||||
double warpedY = offsetY + warpY.fitDouble(-warpStrength, warpStrength, worldY, worldZ, worldX);
|
||||
double warpedZ = offsetZ + warpZ.fitDouble(-warpStrength, warpStrength, worldZ, worldX, worldY);
|
||||
double horizontalRadius = settings.grottoHorizontalRadius();
|
||||
double verticalRadius = settings.grottoVerticalRadius();
|
||||
double normalized = (warpedX * warpedX / (horizontalRadius * horizontalRadius))
|
||||
+ (warpedY * warpedY / (verticalRadius * verticalRadius))
|
||||
+ (warpedZ * warpedZ / (horizontalRadius * horizontalRadius));
|
||||
double boundary = shape.fitDouble(-0.2D, 0.2D, worldX, worldY, worldZ);
|
||||
return normalized <= 1D + boundary;
|
||||
}
|
||||
}
|
||||
@@ -197,7 +197,7 @@ public class IrisCaveCarver3D {
|
||||
double thresholdPenalty,
|
||||
IrisRange worldYRange,
|
||||
int[] precomputedSurfaceHeights,
|
||||
int[] surfaceFluidBoundaryStartY,
|
||||
long[] surfaceFluidBoundaries,
|
||||
IrisRange overrideVerticalRange,
|
||||
CaveFluidSupportPlan fluidSupportPlan
|
||||
) {
|
||||
@@ -318,7 +318,7 @@ public class IrisCaveCarver3D {
|
||||
surfaceBreakThresholdBoost,
|
||||
columnMaxY,
|
||||
fluidMaxY,
|
||||
surfaceFluidBoundaryStartY,
|
||||
surfaceFluidBoundaries,
|
||||
surfaceBreakFloorY,
|
||||
surfaceBreakColumn,
|
||||
columnThreshold,
|
||||
@@ -341,7 +341,7 @@ public class IrisCaveCarver3D {
|
||||
surfaceBreakThresholdBoost,
|
||||
columnMaxY,
|
||||
fluidMaxY,
|
||||
surfaceFluidBoundaryStartY,
|
||||
surfaceFluidBoundaries,
|
||||
surfaceBreakFloorY,
|
||||
surfaceBreakColumn,
|
||||
columnThreshold,
|
||||
@@ -367,7 +367,7 @@ public class IrisCaveCarver3D {
|
||||
surfaceBreakThresholdBoost,
|
||||
columnMaxY,
|
||||
fluidMaxY,
|
||||
surfaceFluidBoundaryStartY,
|
||||
surfaceFluidBoundaries,
|
||||
surfaceBreakFloorY,
|
||||
surfaceBreakColumn,
|
||||
columnThreshold,
|
||||
@@ -391,7 +391,7 @@ public class IrisCaveCarver3D {
|
||||
surfaceBreakThresholdBoost,
|
||||
columnMaxY,
|
||||
fluidMaxY,
|
||||
surfaceFluidBoundaryStartY,
|
||||
surfaceFluidBoundaries,
|
||||
surfaceBreakFloorY,
|
||||
surfaceBreakColumn,
|
||||
columnThreshold,
|
||||
@@ -422,7 +422,7 @@ public class IrisCaveCarver3D {
|
||||
double surfaceBreakThresholdBoost,
|
||||
int[] columnMaxY,
|
||||
int[] fluidMaxY,
|
||||
int[] surfaceFluidBoundaryStartY,
|
||||
long[] surfaceFluidBoundaries,
|
||||
int[] surfaceBreakFloorY,
|
||||
boolean[] surfaceBreakColumn,
|
||||
double[] columnThreshold,
|
||||
@@ -478,7 +478,7 @@ public class IrisCaveCarver3D {
|
||||
}
|
||||
|
||||
int columnIndex = activeColumnIndices[activeIndex];
|
||||
if (SurfaceFluidBoundaryPlan.protects(surfaceFluidBoundaryStartY, columnIndex, y, fluidHeight)) {
|
||||
if (SurfaceFluidBoundaryPlan.protects(surfaceFluidBoundaries, columnIndex, y)) {
|
||||
continue;
|
||||
}
|
||||
planeColumnIndices[planeCount] = columnIndex;
|
||||
@@ -553,7 +553,7 @@ public class IrisCaveCarver3D {
|
||||
double surfaceBreakThresholdBoost,
|
||||
int[] columnMaxY,
|
||||
int[] fluidMaxY,
|
||||
int[] surfaceFluidBoundaryStartY,
|
||||
long[] surfaceFluidBoundaries,
|
||||
int[] surfaceBreakFloorY,
|
||||
boolean[] surfaceBreakColumn,
|
||||
double[] columnThreshold,
|
||||
@@ -615,7 +615,7 @@ public class IrisCaveCarver3D {
|
||||
}
|
||||
|
||||
int columnIndex = activeColumnIndices[activeIndex];
|
||||
if (SurfaceFluidBoundaryPlan.protects(surfaceFluidBoundaryStartY, columnIndex, y, fluidHeight)) {
|
||||
if (SurfaceFluidBoundaryPlan.protects(surfaceFluidBoundaries, columnIndex, y)) {
|
||||
continue;
|
||||
}
|
||||
planeColumnIndices[planeCount] = columnIndex;
|
||||
@@ -712,7 +712,7 @@ public class IrisCaveCarver3D {
|
||||
double surfaceBreakThresholdBoost,
|
||||
int[] columnMaxY,
|
||||
int[] fluidMaxY,
|
||||
int[] surfaceFluidBoundaryStartY,
|
||||
long[] surfaceFluidBoundaries,
|
||||
int[] surfaceBreakFloorY,
|
||||
boolean[] surfaceBreakColumn,
|
||||
double[] columnThreshold,
|
||||
@@ -814,7 +814,7 @@ public class IrisCaveCarver3D {
|
||||
}
|
||||
|
||||
int index = tileIndices[columnIndex];
|
||||
if (SurfaceFluidBoundaryPlan.protects(surfaceFluidBoundaryStartY, index, yy, fluidHeight)) {
|
||||
if (SurfaceFluidBoundaryPlan.protects(surfaceFluidBoundaries, index, yy)) {
|
||||
continue;
|
||||
}
|
||||
double localThreshold = passThreshold[index];
|
||||
@@ -861,7 +861,7 @@ public class IrisCaveCarver3D {
|
||||
double surfaceBreakThresholdBoost,
|
||||
int[] columnMaxY,
|
||||
int[] fluidMaxY,
|
||||
int[] surfaceFluidBoundaryStartY,
|
||||
long[] surfaceFluidBoundaries,
|
||||
int[] surfaceBreakFloorY,
|
||||
boolean[] surfaceBreakColumn,
|
||||
double[] columnThreshold,
|
||||
@@ -909,7 +909,7 @@ public class IrisCaveCarver3D {
|
||||
|
||||
int carveMaxY = Math.min(columnTopY, y + sampleStep - 1);
|
||||
for (int yy = y; yy <= carveMaxY; yy++) {
|
||||
if (SurfaceFluidBoundaryPlan.protects(surfaceFluidBoundaryStartY, index, yy, fluidHeight)) {
|
||||
if (SurfaceFluidBoundaryPlan.protects(surfaceFluidBoundaries, index, yy)) {
|
||||
continue;
|
||||
}
|
||||
MatterCavern verticalMatter = matterByY[yy - minY];
|
||||
|
||||
+1
-1
@@ -70,7 +70,7 @@ public class IrisStructureComponent extends IrisMantleComponent {
|
||||
private static final MatterCavern CARVE_CAVERN = new MatterCavern(true, "", (byte) 3);
|
||||
|
||||
public IrisStructureComponent(EngineMantle engineMantle) {
|
||||
super(engineMantle, ReservedFlag.JIGSAW, 3);
|
||||
super(engineMantle, ReservedFlag.JIGSAW, 4);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
+23
-13
@@ -104,20 +104,19 @@ public class MantleCarvingComponent extends IrisMantleComponent {
|
||||
PrecisionStopwatch resolveStopwatch = PrecisionStopwatch.start();
|
||||
List<WeightedProfile> weightedProfiles = resolveWeightedProfiles(x, z, complex, resolverState);
|
||||
getEngineMantle().getEngine().getMetrics().getCarveResolve().put(resolveStopwatch.getMilliseconds());
|
||||
int fluidHeight = getDimension().getFluidHeight();
|
||||
int[] surfaceFluidBoundaryStartY = blendScratch.surfaceFluidBoundaryStartY;
|
||||
long[] surfaceFluidBoundaries = blendScratch.surfaceFluidBoundaries;
|
||||
SurfaceFluidBoundaryPlan.fill(
|
||||
chunkSurfaceHeights,
|
||||
blendScratch.fieldSurfaceHeights,
|
||||
blendScratch.fieldHasFluid,
|
||||
blendScratch.fieldFluidHeights,
|
||||
FIELD_SIZE,
|
||||
BLEND_RADIUS,
|
||||
fluidHeight,
|
||||
surfaceFluidBoundaryStartY
|
||||
surfaceFluidBoundaries
|
||||
);
|
||||
CaveFluidSupportPlan fluidSupportPlan = new CaveFluidSupportPlan();
|
||||
for (WeightedProfile weightedProfile : weightedProfiles) {
|
||||
carveProfile(weightedProfile, writer, x, z, chunkSurfaceHeights, surfaceFluidBoundaryStartY, fluidSupportPlan);
|
||||
carveProfile(weightedProfile, writer, x, z, chunkSurfaceHeights, surfaceFluidBoundaries, fluidSupportPlan);
|
||||
}
|
||||
|
||||
UpperDimensionContext upperCtx = getEngineMantle().getEngine().getUpperContext();
|
||||
@@ -132,8 +131,7 @@ public class MantleCarvingComponent extends IrisMantleComponent {
|
||||
chunkSurfaceHeights,
|
||||
maxSurfaceBreakDepth(weightedProfiles),
|
||||
writer.getMantle().getWorldHeight() - 1,
|
||||
surfaceFluidBoundaryStartY,
|
||||
fluidHeight
|
||||
surfaceFluidBoundaries
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -148,11 +146,11 @@ public class MantleCarvingComponent extends IrisMantleComponent {
|
||||
|
||||
@ChunkCoordinates
|
||||
private void carveProfile(WeightedProfile weightedProfile, MantleWriter writer, int cx, int cz,
|
||||
int[] chunkSurfaceHeights, int[] surfaceFluidBoundaryStartY,
|
||||
int[] chunkSurfaceHeights, long[] surfaceFluidBoundaries,
|
||||
CaveFluidSupportPlan fluidSupportPlan) {
|
||||
IrisCaveCarver3D carver = getCarver(weightedProfile.profile);
|
||||
carver.carve(writer, cx, cz, weightedProfile.columnWeights, MIN_WEIGHT, THRESHOLD_PENALTY,
|
||||
weightedProfile.worldYRange, chunkSurfaceHeights, surfaceFluidBoundaryStartY, null, fluidSupportPlan);
|
||||
weightedProfile.worldYRange, chunkSurfaceHeights, surfaceFluidBoundaries, null, fluidSupportPlan);
|
||||
}
|
||||
|
||||
private void carveUpperTerrain(UpperDimensionContext upperCtx, List<WeightedProfile> normalProfiles,
|
||||
@@ -475,7 +473,9 @@ public class MantleCarvingComponent extends IrisMantleComponent {
|
||||
|
||||
private void prefillProfileFieldSamples(int startX, int startZ, IrisComplex complex, BlendScratch blendScratch) {
|
||||
fillFieldHeights(complex.getHeightStream(), startX, startZ, blendScratch.fieldSurfaceHeights);
|
||||
fillFieldFluidPresence(complex.getFluidStream(), startX, startZ, blendScratch.fieldHasFluid);
|
||||
fillFieldHeights(complex.getRiverWaterSurfaceStream(), startX, startZ, blendScratch.fieldFluidHeights);
|
||||
fillFieldFluidPresence(complex.getFluidStream(), startX, startZ, blendScratch.fieldSurfaceHeights,
|
||||
blendScratch.fieldFluidHeights, blendScratch.fieldHasFluid);
|
||||
fillFieldObjects(complex.getRegionStream(), startX, startZ, blendScratch.fieldRegions);
|
||||
fillFieldObjects(complex.getTrueBiomeStream(), startX, startZ, blendScratch.fieldSurfaceBiomes);
|
||||
fillFieldObjects(complex.getCaveBiomeStream(), startX, startZ, blendScratch.fieldCaveBiomes);
|
||||
@@ -499,11 +499,20 @@ public class MantleCarvingComponent extends IrisMantleComponent {
|
||||
}
|
||||
}
|
||||
|
||||
private void fillFieldFluidPresence(ProceduralStream<PlatformBlockState> stream, int startX, int startZ, boolean[] target) {
|
||||
private void fillFieldFluidPresence(
|
||||
ProceduralStream<PlatformBlockState> stream,
|
||||
int startX,
|
||||
int startZ,
|
||||
double[] surfaceHeights,
|
||||
double[] fluidHeights,
|
||||
boolean[] target
|
||||
) {
|
||||
for (int fieldX = 0; fieldX < FIELD_SIZE; fieldX++) {
|
||||
int worldX = startX + fieldX;
|
||||
for (int fieldZ = 0; fieldZ < FIELD_SIZE; fieldZ++) {
|
||||
target[(fieldX * FIELD_SIZE) + fieldZ] = B.isFluid(stream.get(worldX, startZ + fieldZ));
|
||||
int fieldIndex = (fieldX * FIELD_SIZE) + fieldZ;
|
||||
target[fieldIndex] = B.isFluid(stream.get(worldX, startZ + fieldZ))
|
||||
&& Math.round(surfaceHeights[fieldIndex]) < Math.round(fluidHeights[fieldIndex]);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -697,12 +706,13 @@ public class MantleCarvingComponent extends IrisMantleComponent {
|
||||
private final IdentityHashMap<IrisCaveProfile, Boolean> activeProfiles = new IdentityHashMap<>();
|
||||
private final List<IrisCaveProfile> profileOrder = new ArrayList<>();
|
||||
private final double[] fieldSurfaceHeights = new double[FIELD_SIZE * FIELD_SIZE];
|
||||
private final double[] fieldFluidHeights = new double[FIELD_SIZE * FIELD_SIZE];
|
||||
private final boolean[] fieldHasFluid = new boolean[FIELD_SIZE * FIELD_SIZE];
|
||||
private final long[] surfaceFluidBoundaries = new long[CHUNK_AREA];
|
||||
private final IrisRegion[] fieldRegions = new IrisRegion[FIELD_SIZE * FIELD_SIZE];
|
||||
private final IrisBiome[] fieldSurfaceBiomes = new IrisBiome[FIELD_SIZE * FIELD_SIZE];
|
||||
private final IrisBiome[] fieldCaveBiomes = new IrisBiome[FIELD_SIZE * FIELD_SIZE];
|
||||
private final int[] chunkSurfaceHeights = new int[CHUNK_AREA];
|
||||
private final int[] surfaceFluidBoundaryStartY = new int[CHUNK_AREA];
|
||||
private final double[] chunkSurfaceHeightSamples = new double[CHUNK_AREA];
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -59,7 +59,7 @@ public class MantleFloatingObjectComponent extends IrisMantleComponent {
|
||||
private static final IrisObjectRotation ROTATION_NONE = IrisObjectRotation.of(0, 0, 0);
|
||||
|
||||
public MantleFloatingObjectComponent(EngineMantle engineMantle) {
|
||||
super(engineMantle, ReservedFlag.FLOATING_OBJECT, 2);
|
||||
super(engineMantle, ReservedFlag.FLOATING_OBJECT, 3);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
+23
-2
@@ -48,6 +48,7 @@ import art.arcane.iris.engine.object.IrisProceduralPlacement;
|
||||
import art.arcane.iris.engine.object.IrisProceduralTree;
|
||||
import art.arcane.iris.engine.object.IrisRegion;
|
||||
import art.arcane.iris.engine.object.ObjectPlaceMode;
|
||||
import art.arcane.iris.engine.river.cave.RiverCaveHydrology;
|
||||
import art.arcane.iris.spi.IrisLogging;
|
||||
import art.arcane.volmlib.util.collection.KList;
|
||||
import art.arcane.volmlib.util.collection.KMap;
|
||||
@@ -83,7 +84,7 @@ public class MantleObjectComponent extends IrisMantleComponent {
|
||||
private static final Set<String> MISSING_LOAD_KEY_WARNED = ConcurrentHashMap.newKeySet();
|
||||
|
||||
public MantleObjectComponent(EngineMantle engineMantle) {
|
||||
super(engineMantle, ReservedFlag.OBJECT, 1);
|
||||
super(engineMantle, ReservedFlag.OBJECT, 2);
|
||||
}
|
||||
|
||||
private static String placementMarker(IrisObject object, int id, String context) {
|
||||
@@ -581,11 +582,18 @@ public class MantleObjectComponent extends IrisMantleComponent {
|
||||
minDepthBelowSurface,
|
||||
anchorCache
|
||||
);
|
||||
RiverCaveHydrology hydrology = candidateY < 0
|
||||
? null
|
||||
: writer.getDataIfPresent(candidateX, candidateY, candidateZ, RiverCaveHydrology.class);
|
||||
MatterCavern cavern = candidateY < 0
|
||||
? null
|
||||
: writer.getDataIfPresent(candidateX, candidateY, candidateZ, MatterCavern.class);
|
||||
if (candidateY < 0
|
||||
|| caveAnchorBiomeConflicts(candidateX, candidateY, candidateZ, expectedCaveBiomeKey)
|
||||
|| !acceptsCaveAnchorFluid(
|
||||
underwater,
|
||||
writer.getDataIfPresent(candidateX, candidateY, candidateZ, MatterCavern.class),
|
||||
hydrology == null ? cavern : hydrology.asCavern(),
|
||||
hydrology,
|
||||
candidateY,
|
||||
getDimension().getCaveLavaHeight())) {
|
||||
continue;
|
||||
@@ -596,6 +604,19 @@ public class MantleObjectComponent extends IrisMantleComponent {
|
||||
}
|
||||
|
||||
static boolean acceptsCaveAnchorFluid(boolean underwater, MatterCavern cavern, int y, int lavaHeight) {
|
||||
return acceptsCaveAnchorFluid(underwater, cavern, null, y, lavaHeight);
|
||||
}
|
||||
|
||||
static boolean acceptsCaveAnchorFluid(
|
||||
boolean underwater,
|
||||
MatterCavern cavern,
|
||||
RiverCaveHydrology hydrology,
|
||||
int y,
|
||||
int lavaHeight
|
||||
) {
|
||||
if (hydrology != null && hydrology.protectsPlacement()) {
|
||||
return false;
|
||||
}
|
||||
if (cavern == null || !cavern.isCavern()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
+157
@@ -0,0 +1,157 @@
|
||||
package art.arcane.iris.engine.mantle.components;
|
||||
|
||||
import art.arcane.iris.engine.river.cave.CavePosition;
|
||||
import art.arcane.iris.engine.river.cave.CaveVoxel;
|
||||
import art.arcane.iris.engine.river.cave.CaveVoxelView;
|
||||
import art.arcane.iris.engine.river.cave.RiverCaveAction;
|
||||
import art.arcane.iris.engine.river.cave.RiverCaveHydrology;
|
||||
import art.arcane.iris.engine.data.cache.Cache;
|
||||
import art.arcane.iris.engine.object.IrisProceduralBlocks;
|
||||
import art.arcane.iris.spi.PlatformBlockState;
|
||||
import art.arcane.volmlib.util.function.Function2;
|
||||
import art.arcane.volmlib.util.mantle.runtime.Mantle;
|
||||
import art.arcane.volmlib.util.mantle.runtime.MantleChunk;
|
||||
import art.arcane.volmlib.util.mantle.runtime.TectonicPlate;
|
||||
import art.arcane.volmlib.util.matter.Matter;
|
||||
import art.arcane.volmlib.util.matter.MatterCavern;
|
||||
import it.unimi.dsi.fastutil.longs.Long2IntOpenHashMap;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
final class MantleRiverCaveVoxelView implements MantleRiverHydrologyComponent.TunnelVoxelView {
|
||||
private static final int CLOSED_COLUMN = Integer.MAX_VALUE;
|
||||
private static final int CACHE_MISS = Integer.MIN_VALUE;
|
||||
|
||||
private final Mantle<Matter> mantle;
|
||||
private final int worldHeight;
|
||||
private final Function2<Integer, Integer, Integer> surfaceHeight;
|
||||
private final Function2<Integer, Integer, PlatformBlockState> compatibleFluid;
|
||||
private final Long2IntOpenHashMap openFloorCache;
|
||||
private final Long2IntOpenHashMap surfaceHeightCache;
|
||||
|
||||
MantleRiverCaveVoxelView(
|
||||
Mantle<Matter> mantle,
|
||||
int worldHeight,
|
||||
Function2<Integer, Integer, Integer> surfaceHeight,
|
||||
Function2<Integer, Integer, PlatformBlockState> compatibleFluid
|
||||
) {
|
||||
this.mantle = Objects.requireNonNull(mantle);
|
||||
this.worldHeight = worldHeight;
|
||||
this.surfaceHeight = Objects.requireNonNull(surfaceHeight);
|
||||
this.compatibleFluid = Objects.requireNonNull(compatibleFluid);
|
||||
openFloorCache = new Long2IntOpenHashMap();
|
||||
openFloorCache.defaultReturnValue(CACHE_MISS);
|
||||
surfaceHeightCache = new Long2IntOpenHashMap();
|
||||
surfaceHeightCache.defaultReturnValue(CACHE_MISS);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isInWorld(CavePosition position) {
|
||||
return position.y() > 0 && position.y() < worldHeight - 1;
|
||||
}
|
||||
|
||||
@Override
|
||||
public CaveVoxel voxelAt(CavePosition position) {
|
||||
RiverCaveHydrology hydrology = dataIfPresent(position, RiverCaveHydrology.class);
|
||||
if (hydrology != null && hydrology.carves()) {
|
||||
return hydrology.isWet() ? CaveVoxel.COMPATIBLE_FLUID : CaveVoxel.CAVE_AIR;
|
||||
}
|
||||
MatterCavern cavern = dataIfPresent(position, MatterCavern.class);
|
||||
if (cavern != null) {
|
||||
if (cavern.isLava()) {
|
||||
return CaveVoxel.LAVA;
|
||||
}
|
||||
if (cavern.getLiquid() == 1) {
|
||||
return CaveVoxel.COMPATIBLE_FLUID;
|
||||
}
|
||||
return CaveVoxel.CAVE_AIR;
|
||||
}
|
||||
PlatformBlockState block = dataIfPresent(position, PlatformBlockState.class);
|
||||
if (block == null) {
|
||||
return position.y() > surfaceY(position.x(), position.z())
|
||||
? CaveVoxel.CAVE_AIR
|
||||
: CaveVoxel.SOLID;
|
||||
}
|
||||
if (!block.isFluid()) {
|
||||
return CaveVoxel.SOLID;
|
||||
}
|
||||
if (IrisProceduralBlocks.materialKey(block).endsWith(":lava")) {
|
||||
return CaveVoxel.LAVA;
|
||||
}
|
||||
PlatformBlockState expected = compatibleFluid.apply(position.x(), position.z());
|
||||
return expected != null
|
||||
&& IrisProceduralBlocks.materialKey(expected).equals(IrisProceduralBlocks.materialKey(block))
|
||||
? CaveVoxel.COMPATIBLE_FLUID
|
||||
: CaveVoxel.INCOMPATIBLE_FLUID;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isOpenToSurface(CavePosition position) {
|
||||
if (!isInWorld(position) || voxelAt(position) == CaveVoxel.SOLID) {
|
||||
return false;
|
||||
}
|
||||
if (position.y() > surfaceY(position.x(), position.z())) {
|
||||
return true;
|
||||
}
|
||||
long key = Cache.key(position.x(), position.z());
|
||||
int openFloor = openFloorCache.get(key);
|
||||
if (openFloor == CACHE_MISS) {
|
||||
openFloor = resolveOpenFloor(position.x(), position.z());
|
||||
openFloorCache.put(key, openFloor);
|
||||
}
|
||||
return openFloor != CLOSED_COLUMN && position.y() >= openFloor;
|
||||
}
|
||||
|
||||
@Override
|
||||
public RiverCaveAction riverActionAt(CavePosition position) {
|
||||
RiverCaveHydrology hydrology = dataIfPresent(position, RiverCaveHydrology.class);
|
||||
return hydrology == null ? null : hydrology.action();
|
||||
}
|
||||
|
||||
private int resolveOpenFloor(int x, int z) {
|
||||
int top = surfaceY(x, z);
|
||||
CavePosition surface = new CavePosition(x, top, z);
|
||||
if (voxelAt(surface) == CaveVoxel.SOLID) {
|
||||
return CLOSED_COLUMN;
|
||||
}
|
||||
int y = top;
|
||||
while (y > 0 && voxelAt(new CavePosition(x, y - 1, z)) != CaveVoxel.SOLID) {
|
||||
y--;
|
||||
}
|
||||
return y;
|
||||
}
|
||||
|
||||
private int surfaceY(int x, int z) {
|
||||
long key = Cache.key(x, z);
|
||||
int cached = surfaceHeightCache.get(key);
|
||||
if (cached != CACHE_MISS) {
|
||||
return cached;
|
||||
}
|
||||
int resolved = Math.max(1, Math.min(worldHeight - 2, surfaceHeight.apply(x, z)));
|
||||
surfaceHeightCache.put(key, resolved);
|
||||
return resolved;
|
||||
}
|
||||
|
||||
private <T> T dataIfPresent(CavePosition position, Class<T> type) {
|
||||
int chunkX = position.x() >> 4;
|
||||
int chunkZ = position.z() >> 4;
|
||||
TectonicPlate<Matter> plate = mantle.getLoadedRegions().get(Mantle.key(chunkX >> 5, chunkZ >> 5));
|
||||
if (plate == null || plate.isClosed()) {
|
||||
return null;
|
||||
}
|
||||
MantleChunk<Matter> chunk = plate.get(chunkX & 31, chunkZ & 31);
|
||||
int section = position.y() >> 4;
|
||||
if (chunk == null || !chunk.exists(section)) {
|
||||
return null;
|
||||
}
|
||||
Matter matter = chunk.get(section);
|
||||
if (matter == null || !matter.hasSlice(type)) {
|
||||
return null;
|
||||
}
|
||||
return matter.<T>getSlice(type).get(
|
||||
position.x() & 15,
|
||||
position.y() & 15,
|
||||
position.z() & 15
|
||||
);
|
||||
}
|
||||
}
|
||||
+707
@@ -0,0 +1,707 @@
|
||||
package art.arcane.iris.engine.mantle.components;
|
||||
|
||||
import art.arcane.iris.core.loader.IrisData;
|
||||
import art.arcane.iris.engine.mantle.ComponentFlag;
|
||||
import art.arcane.iris.engine.mantle.EngineMantle;
|
||||
import art.arcane.iris.engine.mantle.IrisMantleComponent;
|
||||
import art.arcane.iris.engine.mantle.MantleWriter;
|
||||
import art.arcane.iris.engine.object.IrisRiverCaveFallback;
|
||||
import art.arcane.iris.engine.object.IrisRiverCaveMode;
|
||||
import art.arcane.iris.engine.object.IrisRiverCaves;
|
||||
import art.arcane.iris.engine.object.IrisRiverExistingFluidPolicy;
|
||||
import art.arcane.iris.engine.object.IrisDimension;
|
||||
import art.arcane.iris.engine.object.IrisRiverNetwork;
|
||||
import art.arcane.iris.engine.river.RiverAnchor;
|
||||
import art.arcane.iris.engine.river.RiverRouteState;
|
||||
import art.arcane.iris.engine.river.RiverSample;
|
||||
import art.arcane.iris.engine.river.RiverSection;
|
||||
import art.arcane.iris.engine.river.cave.CavePosition;
|
||||
import art.arcane.iris.engine.river.cave.CaveVoxel;
|
||||
import art.arcane.iris.engine.river.cave.CaveVoxelPrecondition;
|
||||
import art.arcane.iris.engine.river.cave.CaveVoxelView;
|
||||
import art.arcane.iris.engine.river.cave.RiverCaveAction;
|
||||
import art.arcane.iris.engine.river.cave.RiverCaveContainmentPlanner;
|
||||
import art.arcane.iris.engine.river.cave.RiverCaveFluidPolicy;
|
||||
import art.arcane.iris.engine.river.cave.RiverCaveHydrology;
|
||||
import art.arcane.iris.engine.river.cave.RiverCaveMode;
|
||||
import art.arcane.iris.engine.river.cave.RiverCavePlan;
|
||||
import art.arcane.iris.engine.river.cave.RiverCavePlannerSettings;
|
||||
import art.arcane.iris.engine.river.cave.RiverCavePlanningResult;
|
||||
import art.arcane.iris.engine.river.cave.RiverCaveSource;
|
||||
import art.arcane.iris.engine.river.runtime.IrisRiverRuntime;
|
||||
import art.arcane.iris.engine.river.runtime.IrisRiverSurfaceSample;
|
||||
import art.arcane.iris.engine.river.runtime.IrisRiverTunnelSample;
|
||||
import art.arcane.iris.util.project.context.ChunkContext;
|
||||
import art.arcane.volmlib.util.mantle.flag.MantleFlag;
|
||||
import art.arcane.volmlib.util.mantle.flag.ReservedFlag;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Comparator;
|
||||
import java.util.HashMap;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
@ComponentFlag(ReservedFlag.RIVER_HYDROLOGY)
|
||||
public final class MantleRiverHydrologyComponent extends IrisMantleComponent {
|
||||
private static final long CANDIDATE_SALT = 0x6A09E667F3BCC909L;
|
||||
static final int PRIORITY = 1;
|
||||
private static final int[] FALLBACK_X = {0, 1, -1, 0, 0};
|
||||
private static final int[] FALLBACK_Z = {0, 0, 0, 1, -1};
|
||||
private static final MantleFlag[] PREREQUISITES = {ReservedFlag.CARVED};
|
||||
private static final int[][] NEIGHBORS = {
|
||||
{1, 0, 0}, {-1, 0, 0},
|
||||
{0, 1, 0}, {0, -1, 0},
|
||||
{0, 0, 1}, {0, 0, -1}
|
||||
};
|
||||
private static final Comparator<Map.Entry<CavePosition, RiverCaveAction>> ACTION_ORDER = Comparator
|
||||
.comparingInt((Map.Entry<CavePosition, RiverCaveAction> entry) -> entry.getKey().x())
|
||||
.thenComparingInt(entry -> entry.getKey().y())
|
||||
.thenComparingInt(entry -> entry.getKey().z());
|
||||
|
||||
private final RiverCaveContainmentPlanner planner;
|
||||
|
||||
public MantleRiverHydrologyComponent(EngineMantle engineMantle) {
|
||||
super(engineMantle, ReservedFlag.RIVER_HYDROLOGY, PRIORITY);
|
||||
planner = new RiverCaveContainmentPlanner();
|
||||
}
|
||||
|
||||
@Override
|
||||
public MantleFlag[] getPrerequisiteFlags() {
|
||||
return PREREQUISITES;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getInputRadius() {
|
||||
if (!getDimension().isCarvingEnabled()
|
||||
|| getDimension().getRivers() == null
|
||||
|| !getDimension().getRivers().isEnabled()) {
|
||||
return 0;
|
||||
}
|
||||
IrisRiverRuntime runtime = getComplex().getRiverRuntime();
|
||||
if (runtime == null) {
|
||||
return 0;
|
||||
}
|
||||
return inputRadius(runtime.caveSettings(), tunnelHalo(runtime));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void generateLayer(MantleWriter writer, int chunkX, int chunkZ, ChunkContext context) {
|
||||
IrisRiverRuntime runtime = context.getComplex().getRiverRuntime();
|
||||
if (runtime == null || !getDimension().isCarvingEnabled()) {
|
||||
return;
|
||||
}
|
||||
publishTunnels(writer, context, runtime, chunkX, chunkZ);
|
||||
|
||||
IrisRiverCaves caves = runtime.caveSettings();
|
||||
if (caves.getMode() == IrisRiverCaveMode.SEALED || caves.getMaximumPerReach() <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
MantleRiverCaveVoxelView view = createView(writer, context);
|
||||
int candidateHalo = candidateHalo(caves);
|
||||
int minimumX = (chunkX << 4) - candidateHalo;
|
||||
int minimumZ = (chunkZ << 4) - candidateHalo;
|
||||
int maximumX = ((chunkX + 1) << 4) + candidateHalo;
|
||||
int maximumZ = ((chunkZ + 1) << 4) + candidateHalo;
|
||||
List<RiverAnchor> anchors = runtime.candidateAnchors(
|
||||
minimumX,
|
||||
minimumZ,
|
||||
maximumX,
|
||||
maximumZ,
|
||||
caves.getMinimumSpacing(),
|
||||
CANDIDATE_SALT
|
||||
);
|
||||
if (anchors.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
RiverCavePlannerSettings settings = plannerSettings(caves, seed(), getData());
|
||||
List<RiverCaveSource> sources = new ArrayList<>();
|
||||
Map<Long, String> floodedBiomes = new HashMap<>();
|
||||
for (RiverAnchor anchor : anchors) {
|
||||
if (!runtime.acceptsCaveAnchor(anchor)) {
|
||||
continue;
|
||||
}
|
||||
SourceCandidate candidate = sourceFor(runtime, view, caves, anchor);
|
||||
if (candidate == null) {
|
||||
continue;
|
||||
}
|
||||
RiverCaveSource source = candidate.source();
|
||||
RiverCavePlan initial = planner.plan(view, source, settings);
|
||||
if (!initial.accepted()
|
||||
&& caves.getFallback() == IrisRiverCaveFallback.GENERATE_GROTTO) {
|
||||
source = fallbackSource(view, caves, settings, candidate, source);
|
||||
}
|
||||
if (source == null) {
|
||||
continue;
|
||||
}
|
||||
sources.add(source);
|
||||
floodedBiomes.put(source.sourceId(), runtime.selectFloodedCaveBiome(anchor));
|
||||
}
|
||||
if (sources.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
RiverCavePlanningResult result = planner.planAll(view, sources, settings);
|
||||
MantleRiverCaveVoxelView revalidationView = createView(writer, context);
|
||||
if (!preconditionsHold(revalidationView, result.baselinePreconditions())) {
|
||||
return;
|
||||
}
|
||||
publishLocal(writer, chunkX, chunkZ, result, floodedBiomes);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected int computeRadius() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
public static boolean isEnabledFor(IrisDimension dimension) {
|
||||
IrisRiverNetwork rivers = dimension.getRivers();
|
||||
if (!dimension.isUseMantle()
|
||||
|| !dimension.isCarvingEnabled()
|
||||
|| dimension.getDisabledComponents().contains(ReservedFlag.CARVED)
|
||||
|| dimension.getDisabledComponents().contains(ReservedFlag.RIVER_HYDROLOGY)
|
||||
|| rivers == null
|
||||
|| !rivers.isEnabled()) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public static boolean isCaveConnectionsEnabledFor(IrisDimension dimension) {
|
||||
if (!isEnabledFor(dimension)) {
|
||||
return false;
|
||||
}
|
||||
IrisRiverCaves caves = dimension.getRivers().getCaves();
|
||||
return caves != null
|
||||
&& caves.getMode() != IrisRiverCaveMode.SEALED
|
||||
&& caves.getMaximumPerReach() > 0;
|
||||
}
|
||||
|
||||
static int planningHalo(IrisRiverCaves caves) {
|
||||
return cavePublicationRadius(caves) * 4;
|
||||
}
|
||||
|
||||
static int inputRadius(IrisRiverCaves caves, int tunnelRadius) {
|
||||
if (caves.getMode() == IrisRiverCaveMode.SEALED || caves.getMaximumPerReach() <= 0) {
|
||||
return tunnelRadius;
|
||||
}
|
||||
return Math.max(tunnelRadius, planningHalo(caves));
|
||||
}
|
||||
|
||||
static int candidateHalo(IrisRiverCaves caves) {
|
||||
return cavePublicationRadius(caves) * 3;
|
||||
}
|
||||
|
||||
static int cavePublicationRadius(IrisRiverCaves caves) {
|
||||
int generatedRadius = generatedGrottoPublicationRadius(caves);
|
||||
return switch (caves.getMode()) {
|
||||
case SEALED -> 0;
|
||||
case GENERATE_GROTTO -> generatedRadius;
|
||||
case FLOOD_CLOSED_COMPONENT, GROTTO_OR_CLOSED_COMPONENT, WATERFALL_POOL ->
|
||||
Math.max(closedComponentPublicationRadius(caves), generatedRadius);
|
||||
};
|
||||
}
|
||||
|
||||
static int closedComponentPublicationRadius(IrisRiverCaves caves) {
|
||||
return caves.getMaxFloodRadius() + 1;
|
||||
}
|
||||
|
||||
static int generatedGrottoPublicationRadius(IrisRiverCaves caves) {
|
||||
int targetOffset = caves.getFallback() == IrisRiverCaveFallback.GENERATE_GROTTO
|
||||
? caves.getThroatRadius() + 2
|
||||
: 0;
|
||||
long maximumX = (long) targetOffset + caves.getGrottoHorizontalRadius() + 1L;
|
||||
long maximumZ = caves.getGrottoHorizontalRadius();
|
||||
int grottoRadius = (int) StrictMath.ceil(StrictMath.sqrt(
|
||||
maximumX * maximumX + maximumZ * maximumZ
|
||||
));
|
||||
int throatRadius = targetOffset + caves.getThroatRadius();
|
||||
return Math.max(grottoRadius, throatRadius);
|
||||
}
|
||||
|
||||
static int tunnelHalo(IrisRiverRuntime runtime) {
|
||||
return Math.max(1, (int) StrictMath.ceil(runtime.maximumChannelWidth() * 0.5D) + 1);
|
||||
}
|
||||
|
||||
static int waterHeadY(IrisRiverSurfaceSample sample, IrisRiverCaves caves) {
|
||||
return (int) Math.round(sample.waterSurfaceY()) + caves.getWaterLevelOffset();
|
||||
}
|
||||
|
||||
static boolean owns(int chunkX, int chunkZ, CavePosition position) {
|
||||
return (position.x() >> 4) == chunkX && (position.z() >> 4) == chunkZ;
|
||||
}
|
||||
|
||||
static RiverCaveFluidPolicy fluidPolicy(IrisRiverExistingFluidPolicy policy) {
|
||||
return switch (policy) {
|
||||
case REJECT -> RiverCaveFluidPolicy.REJECT_EXISTING;
|
||||
case ALLOW_SAME -> RiverCaveFluidPolicy.ALLOW_COMPATIBLE;
|
||||
case REPLACE -> RiverCaveFluidPolicy.REPLACE_CONTAINED;
|
||||
};
|
||||
}
|
||||
|
||||
static boolean preconditionsHold(
|
||||
CaveVoxelView view,
|
||||
Map<CavePosition, CaveVoxelPrecondition> preconditions
|
||||
) {
|
||||
for (Map.Entry<CavePosition, CaveVoxelPrecondition> entry : preconditions.entrySet()) {
|
||||
CaveVoxelPrecondition expected = entry.getValue();
|
||||
if (view.voxelAt(entry.getKey()) != expected.voxel()
|
||||
|| view.isOpenToSurface(entry.getKey()) != expected.openToSurface()) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
static TunnelPlan planTunnels(
|
||||
CaveVoxelView view,
|
||||
int chunkX,
|
||||
int chunkZ,
|
||||
int halo,
|
||||
FootprintSampler footprintSampler,
|
||||
TunnelSampler tunnelSampler,
|
||||
SurfaceSampler surfaceSampler
|
||||
) {
|
||||
int minimumX = (chunkX << 4) - halo;
|
||||
int minimumZ = (chunkZ << 4) - halo;
|
||||
int maximumX = ((chunkX + 1) << 4) + halo;
|
||||
int maximumZ = ((chunkZ + 1) << 4) + halo;
|
||||
if (!footprintSampler.sample(minimumX, minimumZ, maximumX, maximumZ).present()) {
|
||||
return TunnelPlan.empty();
|
||||
}
|
||||
ArrayList<TunnelColumn> solidColumns = new ArrayList<>();
|
||||
for (int x = minimumX; x < maximumX; x++) {
|
||||
for (int z = minimumZ; z < maximumZ; z++) {
|
||||
IrisRiverTunnelSample sample = tunnelSampler.sample(x, z);
|
||||
TunnelColumn column = createTunnelColumn(view, x, z, sample);
|
||||
if (column != null) {
|
||||
solidColumns.add(column);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (solidColumns.isEmpty()) {
|
||||
return TunnelPlan.empty();
|
||||
}
|
||||
|
||||
Map<CavePosition, RiverCaveAction> candidateActions = mergeActions(solidColumns);
|
||||
ArrayList<TunnelColumn> containedColumns = new ArrayList<>(solidColumns.size());
|
||||
for (TunnelColumn column : solidColumns) {
|
||||
if (isTunnelColumnContained(view, column, candidateActions.keySet(), surfaceSampler)) {
|
||||
containedColumns.add(column);
|
||||
}
|
||||
}
|
||||
Map<CavePosition, RiverCaveAction> actions = mergeActions(containedColumns);
|
||||
LinkedHashMap<CavePosition, CaveVoxelPrecondition> preconditions = new LinkedHashMap<>();
|
||||
for (CavePosition position : actions.keySet()) {
|
||||
preconditions.put(position, new CaveVoxelPrecondition(
|
||||
view.voxelAt(position),
|
||||
view.isOpenToSurface(position)
|
||||
));
|
||||
}
|
||||
for (CavePosition position : List.copyOf(actions.keySet())) {
|
||||
for (int[] offset : NEIGHBORS) {
|
||||
CavePosition neighbor = offset(position, offset);
|
||||
if (actions.containsKey(neighbor)
|
||||
|| !view.isInWorld(neighbor)
|
||||
|| view.voxelAt(neighbor) != CaveVoxel.SOLID) {
|
||||
continue;
|
||||
}
|
||||
actions.putIfAbsent(neighbor, RiverCaveAction.SEAL_GUARD);
|
||||
preconditions.putIfAbsent(neighbor, new CaveVoxelPrecondition(CaveVoxel.SOLID, false));
|
||||
}
|
||||
}
|
||||
return new TunnelPlan(Map.copyOf(actions), Map.copyOf(preconditions));
|
||||
}
|
||||
|
||||
private static TunnelColumn createTunnelColumn(
|
||||
CaveVoxelView view,
|
||||
int x,
|
||||
int z,
|
||||
IrisRiverTunnelSample sample
|
||||
) {
|
||||
if (sample == null) {
|
||||
return null;
|
||||
}
|
||||
LinkedHashMap<CavePosition, RiverCaveAction> actions = new LinkedHashMap<>();
|
||||
for (int y = sample.bedY() + 1; y <= sample.ceilingY(); y++) {
|
||||
CavePosition position = new CavePosition(x, y, z);
|
||||
RiverCaveAction action = y <= sample.waterHeadY()
|
||||
? RiverCaveAction.WET_SOURCE
|
||||
: RiverCaveAction.DRY_AIR;
|
||||
if (!view.isInWorld(position)
|
||||
|| (view.voxelAt(position) != CaveVoxel.SOLID
|
||||
&& !matchesPublishedAction(view, position, action))) {
|
||||
return null;
|
||||
}
|
||||
actions.put(position, action);
|
||||
}
|
||||
return actions.isEmpty() ? null : new TunnelColumn(actions);
|
||||
}
|
||||
|
||||
private static boolean isTunnelColumnContained(
|
||||
CaveVoxelView view,
|
||||
TunnelColumn column,
|
||||
Set<CavePosition> candidateActions,
|
||||
SurfaceSampler surfaceSampler
|
||||
) {
|
||||
for (CavePosition position : column.actions().keySet()) {
|
||||
for (int[] offset : NEIGHBORS) {
|
||||
CavePosition neighbor = offset(position, offset);
|
||||
if (candidateActions.contains(neighbor)) {
|
||||
continue;
|
||||
}
|
||||
if (!view.isInWorld(neighbor)) {
|
||||
return false;
|
||||
}
|
||||
if (view.voxelAt(neighbor) == CaveVoxel.SOLID || isSurfaceMouth(neighbor, surfaceSampler)) {
|
||||
continue;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private static boolean isSurfaceMouth(CavePosition position, SurfaceSampler surfaceSampler) {
|
||||
IrisRiverSurfaceSample sample = surfaceSampler.sample(position.x(), position.z());
|
||||
if (!isWetChannelBed(sample) || sample.subterranean()) {
|
||||
return false;
|
||||
}
|
||||
int bedY = (int) Math.round(sample.terrainHeight());
|
||||
int headY = (int) Math.round(sample.waterSurfaceY());
|
||||
return position.y() > bedY && position.y() <= headY;
|
||||
}
|
||||
|
||||
private static Map<CavePosition, RiverCaveAction> mergeActions(List<TunnelColumn> columns) {
|
||||
LinkedHashMap<CavePosition, RiverCaveAction> actions = new LinkedHashMap<>();
|
||||
for (TunnelColumn column : columns) {
|
||||
actions.putAll(column.actions());
|
||||
}
|
||||
return actions;
|
||||
}
|
||||
|
||||
private static boolean matchesPublishedAction(
|
||||
CaveVoxelView view,
|
||||
CavePosition position,
|
||||
RiverCaveAction action
|
||||
) {
|
||||
return view instanceof TunnelVoxelView tunnelView
|
||||
&& tunnelView.riverActionAt(position) == action;
|
||||
}
|
||||
|
||||
private static CavePosition offset(CavePosition position, int[] offset) {
|
||||
return new CavePosition(
|
||||
position.x() + offset[0],
|
||||
position.y() + offset[1],
|
||||
position.z() + offset[2]
|
||||
);
|
||||
}
|
||||
|
||||
private MantleRiverCaveVoxelView createView(MantleWriter writer, ChunkContext context) {
|
||||
return new MantleRiverCaveVoxelView(
|
||||
writer.getMantle(),
|
||||
writer.getMantle().getWorldHeight(),
|
||||
(x, z) -> context.getComplex().getRoundedHeighteightStream().get(x, z),
|
||||
(x, z) -> context.getComplex().getFluidStream().get(x, z)
|
||||
);
|
||||
}
|
||||
|
||||
private void publishTunnels(
|
||||
MantleWriter writer,
|
||||
ChunkContext context,
|
||||
IrisRiverRuntime runtime,
|
||||
int chunkX,
|
||||
int chunkZ
|
||||
) {
|
||||
for (int attempt = 0; attempt < 2; attempt++) {
|
||||
MantleRiverCaveVoxelView view = createView(writer, context);
|
||||
TunnelPlan plan = planTunnels(
|
||||
view,
|
||||
chunkX,
|
||||
chunkZ,
|
||||
tunnelHalo(runtime),
|
||||
runtime::sampleFootprint,
|
||||
runtime::sampleTunnel,
|
||||
runtime::sample
|
||||
);
|
||||
MantleRiverCaveVoxelView revalidationView = createView(writer, context);
|
||||
if (preconditionsHold(revalidationView, plan.preconditions())) {
|
||||
publishTunnelLocal(writer, chunkX, chunkZ, plan);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static RiverCavePlannerSettings plannerSettings(IrisRiverCaves caves, long seed, IrisData data) {
|
||||
int horizontalRadius = generatedGrottoPublicationRadius(caves);
|
||||
int maximumDepth = caves.getMaxBoreDepth() + caves.getGrottoVerticalRadius() + 1;
|
||||
int throatLength = caves.getMaxBoreDepth() + horizontalRadius;
|
||||
return new RiverCavePlannerSettings(
|
||||
horizontalRadius,
|
||||
maximumDepth,
|
||||
caves.getMaxFloodVolume(),
|
||||
throatLength,
|
||||
caves.getThroatRadius(),
|
||||
caves.getGrottoHorizontalRadius(),
|
||||
caves.getGrottoVerticalRadius(),
|
||||
caves.getDryHeadroom(),
|
||||
fluidPolicy(caves.getExistingFluidPolicy()),
|
||||
new ConfiguredRiverGrottoShape(
|
||||
seed,
|
||||
data,
|
||||
caves.getGrottoShapeStyle(),
|
||||
caves.getGrottoWarpStyle(),
|
||||
caves.getGrottoWarpStrength()
|
||||
),
|
||||
caves.getMaxFloodRadius(),
|
||||
caves.getMaxFloodDepth()
|
||||
);
|
||||
}
|
||||
|
||||
private SourceCandidate sourceFor(
|
||||
IrisRiverRuntime runtime,
|
||||
CaveVoxelView view,
|
||||
IrisRiverCaves caves,
|
||||
RiverAnchor anchor
|
||||
) {
|
||||
int x = (int) StrictMath.floor(anchor.x());
|
||||
int z = (int) StrictMath.floor(anchor.z());
|
||||
IrisRiverSurfaceSample sample = runtime.sample(x, z);
|
||||
if (!isWetChannelBed(sample)) {
|
||||
return null;
|
||||
}
|
||||
int bedY = (int) Math.round(sample.terrainHeight());
|
||||
int headY = waterHeadY(sample, caves);
|
||||
int entryY = Math.max(bedY, headY);
|
||||
CavePosition entry = new CavePosition(x, entryY, z);
|
||||
if (!view.isInWorld(entry)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
CavePosition existingTarget = findExistingTarget(view, caves, x, z, bedY, headY);
|
||||
RiverCaveMode requestedMode = sourceMode(
|
||||
caves.getMode(),
|
||||
runtime.isTerminalCaveAnchor(anchor)
|
||||
);
|
||||
CavePosition target;
|
||||
RiverCaveMode sourceMode;
|
||||
if (requestedMode == RiverCaveMode.GENERATED_GROTTO) {
|
||||
target = findGeneratedTarget(view, caves, entry, headY, 0, 0);
|
||||
sourceMode = RiverCaveMode.GENERATED_GROTTO;
|
||||
} else if (requestedMode == RiverCaveMode.GROTTO_OR_CLOSED_COMPONENT) {
|
||||
target = existingTarget;
|
||||
sourceMode = RiverCaveMode.CLOSED_COMPONENT;
|
||||
if (target == null) {
|
||||
target = findGeneratedTarget(view, caves, entry, headY, 0, 0);
|
||||
sourceMode = RiverCaveMode.GENERATED_GROTTO;
|
||||
}
|
||||
} else {
|
||||
target = existingTarget;
|
||||
sourceMode = requestedMode;
|
||||
}
|
||||
if (target == null && caves.getFallback() == IrisRiverCaveFallback.GENERATE_GROTTO) {
|
||||
target = findGeneratedTarget(view, caves, entry, headY, 0, 0);
|
||||
sourceMode = RiverCaveMode.GENERATED_GROTTO;
|
||||
}
|
||||
if (target == null) {
|
||||
return null;
|
||||
}
|
||||
RiverCaveSource source = new RiverCaveSource(anchor.stableId(), entry, target, headY, sourceMode);
|
||||
return new SourceCandidate(entry, headY, source);
|
||||
}
|
||||
|
||||
static boolean isWetChannelBed(IrisRiverSurfaceSample sample) {
|
||||
return sample.river().present()
|
||||
&& sample.river().state() == RiverRouteState.WET
|
||||
&& sample.river().section() == RiverSection.CHANNEL
|
||||
&& sample.surfaceFluid();
|
||||
}
|
||||
|
||||
static CavePosition findExistingTarget(
|
||||
CaveVoxelView view,
|
||||
IrisRiverCaves caves,
|
||||
int x,
|
||||
int z,
|
||||
int bedY,
|
||||
int headY
|
||||
) {
|
||||
int maximumY = Math.min(bedY - 1, headY);
|
||||
int minimumY = Math.max(1, bedY - caves.getMaxBoreDepth());
|
||||
for (int y = maximumY; y >= minimumY; y--) {
|
||||
CavePosition position = new CavePosition(x, y, z);
|
||||
CaveVoxel voxel = view.voxelAt(position);
|
||||
if (voxel != CaveVoxel.SOLID) {
|
||||
return position;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
static CavePosition findGeneratedTarget(
|
||||
CaveVoxelView view,
|
||||
IrisRiverCaves caves,
|
||||
CavePosition entry,
|
||||
int headY,
|
||||
int offsetX,
|
||||
int offsetZ
|
||||
) {
|
||||
int preferredY = headY + caves.getDryHeadroom() - caves.getGrottoVerticalRadius();
|
||||
int maximumY = Math.min(Math.min(entry.y() - 1, headY), preferredY);
|
||||
int minimumY = Math.max(1, entry.y() - caves.getMaxBoreDepth());
|
||||
for (int y = maximumY; y >= minimumY; y--) {
|
||||
CavePosition target = new CavePosition(entry.x() + offsetX, y, entry.z() + offsetZ);
|
||||
if (view.isInWorld(target) && view.voxelAt(target) == CaveVoxel.SOLID) {
|
||||
return target;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private RiverCaveSource fallbackSource(
|
||||
CaveVoxelView view,
|
||||
IrisRiverCaves caves,
|
||||
RiverCavePlannerSettings settings,
|
||||
SourceCandidate candidate,
|
||||
RiverCaveSource rejected
|
||||
) {
|
||||
int fallbackDistance = caves.getThroatRadius() + 2;
|
||||
for (int index = 0; index < FALLBACK_X.length; index++) {
|
||||
int offsetX = FALLBACK_X[index] * fallbackDistance;
|
||||
int offsetZ = FALLBACK_Z[index] * fallbackDistance;
|
||||
CavePosition target = findGeneratedTarget(
|
||||
view,
|
||||
caves,
|
||||
candidate.entry(),
|
||||
candidate.waterHeadY(),
|
||||
offsetX,
|
||||
offsetZ
|
||||
);
|
||||
if (target == null || target.equals(rejected.target())) {
|
||||
continue;
|
||||
}
|
||||
RiverCaveSource fallback = new RiverCaveSource(
|
||||
rejected.sourceId(),
|
||||
candidate.entry(),
|
||||
target,
|
||||
candidate.waterHeadY(),
|
||||
RiverCaveMode.GENERATED_GROTTO
|
||||
);
|
||||
if (planner.plan(view, fallback, settings).accepted()) {
|
||||
return fallback;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
static RiverCaveMode sourceMode(IrisRiverCaveMode mode, boolean forcedTerminal) {
|
||||
if (forcedTerminal) {
|
||||
return RiverCaveMode.GENERATED_GROTTO;
|
||||
}
|
||||
return switch (mode) {
|
||||
case FLOOD_CLOSED_COMPONENT -> RiverCaveMode.CLOSED_COMPONENT;
|
||||
case GENERATE_GROTTO -> RiverCaveMode.GENERATED_GROTTO;
|
||||
case GROTTO_OR_CLOSED_COMPONENT -> RiverCaveMode.GROTTO_OR_CLOSED_COMPONENT;
|
||||
case WATERFALL_POOL -> RiverCaveMode.WATERFALL_POOL;
|
||||
case SEALED -> throw new IllegalArgumentException("Sealed river caves do not create sources");
|
||||
};
|
||||
}
|
||||
|
||||
private void publishLocal(
|
||||
MantleWriter writer,
|
||||
int chunkX,
|
||||
int chunkZ,
|
||||
RiverCavePlanningResult result,
|
||||
Map<Long, String> floodedBiomes
|
||||
) {
|
||||
Map<CavePosition, RiverCaveSource> owners = actionOwners(result);
|
||||
ArrayList<Map.Entry<CavePosition, RiverCaveAction>> actions = new ArrayList<>(result.actions().entrySet());
|
||||
actions.sort(ACTION_ORDER);
|
||||
for (Map.Entry<CavePosition, RiverCaveAction> entry : actions) {
|
||||
CavePosition position = entry.getKey();
|
||||
if (!owns(chunkX, chunkZ, position)) {
|
||||
continue;
|
||||
}
|
||||
RiverCaveSource source = owners.get(position);
|
||||
String biome = source == null ? "" : floodedBiomes.getOrDefault(source.sourceId(), "");
|
||||
if (entry.getValue() == RiverCaveAction.SEAL_GUARD) {
|
||||
biome = "";
|
||||
}
|
||||
writer.setData(
|
||||
position.x(),
|
||||
position.y(),
|
||||
position.z(),
|
||||
new RiverCaveHydrology(entry.getValue(), biome)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private void publishTunnelLocal(
|
||||
MantleWriter writer,
|
||||
int chunkX,
|
||||
int chunkZ,
|
||||
TunnelPlan plan
|
||||
) {
|
||||
ArrayList<Map.Entry<CavePosition, RiverCaveAction>> actions = new ArrayList<>(plan.actions().entrySet());
|
||||
actions.sort(ACTION_ORDER);
|
||||
for (Map.Entry<CavePosition, RiverCaveAction> entry : actions) {
|
||||
CavePosition position = entry.getKey();
|
||||
if (owns(chunkX, chunkZ, position)) {
|
||||
writer.setData(position.x(), position.y(), position.z(), RiverCaveHydrology.of(entry.getValue()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private Map<CavePosition, RiverCaveSource> actionOwners(RiverCavePlanningResult result) {
|
||||
Map<CavePosition, RiverCaveSource> owners = new LinkedHashMap<>();
|
||||
for (RiverCavePlan plan : result.plans()) {
|
||||
if (!plan.accepted()) {
|
||||
continue;
|
||||
}
|
||||
for (CavePosition position : plan.actions().keySet()) {
|
||||
owners.put(position, plan.source());
|
||||
}
|
||||
}
|
||||
return owners;
|
||||
}
|
||||
|
||||
private record SourceCandidate(
|
||||
CavePosition entry,
|
||||
int waterHeadY,
|
||||
RiverCaveSource source
|
||||
) {
|
||||
}
|
||||
|
||||
@FunctionalInterface
|
||||
interface FootprintSampler {
|
||||
RiverSample sample(double minimumX, double minimumZ, double maximumX, double maximumZ);
|
||||
}
|
||||
|
||||
@FunctionalInterface
|
||||
interface TunnelSampler {
|
||||
IrisRiverTunnelSample sample(int x, int z);
|
||||
}
|
||||
|
||||
@FunctionalInterface
|
||||
interface SurfaceSampler {
|
||||
IrisRiverSurfaceSample sample(int x, int z);
|
||||
}
|
||||
|
||||
interface TunnelVoxelView extends CaveVoxelView {
|
||||
RiverCaveAction riverActionAt(CavePosition position);
|
||||
}
|
||||
|
||||
record TunnelPlan(
|
||||
Map<CavePosition, RiverCaveAction> actions,
|
||||
Map<CavePosition, CaveVoxelPrecondition> preconditions
|
||||
) {
|
||||
static TunnelPlan empty() {
|
||||
return new TunnelPlan(Map.of(), Map.of());
|
||||
}
|
||||
}
|
||||
|
||||
private record TunnelColumn(Map<CavePosition, RiverCaveAction> actions) {
|
||||
}
|
||||
}
|
||||
+48
-24
@@ -32,16 +32,17 @@ final class SurfaceFluidBoundaryPlan {
|
||||
int[] chunkSurfaceHeights,
|
||||
double[] fieldSurfaceHeights,
|
||||
boolean[] fieldHasFluid,
|
||||
double[] fieldFluidHeights,
|
||||
int fieldSize,
|
||||
int padding,
|
||||
int fluidHeight,
|
||||
int[] boundaryStartY
|
||||
long[] boundaries
|
||||
) {
|
||||
if (chunkSurfaceHeights == null || chunkSurfaceHeights.length < CHUNK_AREA
|
||||
|| boundaryStartY == null || boundaryStartY.length < CHUNK_AREA
|
||||
|| boundaries == null || boundaries.length < CHUNK_AREA
|
||||
|| padding < 1 || fieldSize < CHUNK_SIZE + (padding * 2)
|
||||
|| fieldSurfaceHeights == null || fieldSurfaceHeights.length < fieldSize * fieldSize
|
||||
|| fieldHasFluid == null || fieldHasFluid.length < fieldSize * fieldSize) {
|
||||
|| fieldHasFluid == null || fieldHasFluid.length < fieldSize * fieldSize
|
||||
|| fieldFluidHeights == null || fieldFluidHeights.length < fieldSize * fieldSize) {
|
||||
throw new IllegalArgumentException("Surface fluid boundary fields do not cover a padded chunk");
|
||||
}
|
||||
|
||||
@@ -51,44 +52,67 @@ final class SurfaceFluidBoundaryPlan {
|
||||
int fieldZ = localZ + padding;
|
||||
int columnIndex = PowerOfTwoCoordinates.packLocal16(localX, localZ);
|
||||
int boundaryY = NO_BOUNDARY;
|
||||
int boundaryEndY = Integer.MIN_VALUE;
|
||||
int surfaceY = chunkSurfaceHeights[columnIndex];
|
||||
int fieldIndex = (fieldX * fieldSize) + fieldZ;
|
||||
int fluidHeight = roundedHeight(fieldFluidHeights[fieldIndex]);
|
||||
if (fieldHasFluid[fieldIndex] && surfaceY < fluidHeight) {
|
||||
boundaryY = surfaceY;
|
||||
boundaryEndY = fluidHeight;
|
||||
}
|
||||
|
||||
boundaryY = lowerBoundary(boundaryY, fieldSurfaceHeights, fieldHasFluid,
|
||||
((fieldX - 1) * fieldSize) + fieldZ, fluidHeight);
|
||||
boundaryY = lowerBoundary(boundaryY, fieldSurfaceHeights, fieldHasFluid,
|
||||
((fieldX + 1) * fieldSize) + fieldZ, fluidHeight);
|
||||
boundaryY = lowerBoundary(boundaryY, fieldSurfaceHeights, fieldHasFluid,
|
||||
(fieldX * fieldSize) + fieldZ - 1, fluidHeight);
|
||||
boundaryY = lowerBoundary(boundaryY, fieldSurfaceHeights, fieldHasFluid,
|
||||
(fieldX * fieldSize) + fieldZ + 1, fluidHeight);
|
||||
boundaryStartY[columnIndex] = boundaryY;
|
||||
long boundary = expandBoundary(boundaryY, boundaryEndY, fieldSurfaceHeights,
|
||||
fieldHasFluid, fieldFluidHeights, ((fieldX - 1) * fieldSize) + fieldZ);
|
||||
boundary = expandBoundary(startY(boundary), endY(boundary), fieldSurfaceHeights,
|
||||
fieldHasFluid, fieldFluidHeights, ((fieldX + 1) * fieldSize) + fieldZ);
|
||||
boundary = expandBoundary(startY(boundary), endY(boundary), fieldSurfaceHeights,
|
||||
fieldHasFluid, fieldFluidHeights, (fieldX * fieldSize) + fieldZ - 1);
|
||||
boundaries[columnIndex] = expandBoundary(startY(boundary), endY(boundary), fieldSurfaceHeights,
|
||||
fieldHasFluid, fieldFluidHeights, (fieldX * fieldSize) + fieldZ + 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static boolean protects(int[] boundaryStartY, int columnIndex, int y, int fluidHeight) {
|
||||
return boundaryStartY != null
|
||||
&& columnIndex >= 0
|
||||
&& columnIndex < boundaryStartY.length
|
||||
&& y >= boundaryStartY[columnIndex]
|
||||
&& y <= fluidHeight;
|
||||
static boolean protects(long[] boundaries, int columnIndex, int y) {
|
||||
if (boundaries == null || columnIndex < 0 || columnIndex >= boundaries.length) {
|
||||
return false;
|
||||
}
|
||||
long boundary = boundaries[columnIndex];
|
||||
return y >= startY(boundary) && y <= endY(boundary);
|
||||
}
|
||||
|
||||
private static int lowerBoundary(
|
||||
static int startY(long boundary) {
|
||||
return (int) (boundary >> 32);
|
||||
}
|
||||
|
||||
static int endY(long boundary) {
|
||||
return (int) boundary;
|
||||
}
|
||||
|
||||
private static long expandBoundary(
|
||||
int currentBoundaryY,
|
||||
int currentBoundaryEndY,
|
||||
double[] fieldSurfaceHeights,
|
||||
boolean[] fieldHasFluid,
|
||||
int fieldIndex,
|
||||
int fluidHeight
|
||||
double[] fieldFluidHeights,
|
||||
int fieldIndex
|
||||
) {
|
||||
int fluidHeight = roundedHeight(fieldFluidHeights[fieldIndex]);
|
||||
int neighborSurfaceY = (int) Math.round(fieldSurfaceHeights[fieldIndex]);
|
||||
if (!fieldHasFluid[fieldIndex] || neighborSurfaceY >= fluidHeight) {
|
||||
return currentBoundaryY;
|
||||
return boundary(currentBoundaryY, currentBoundaryEndY);
|
||||
}
|
||||
return Math.min(currentBoundaryY, neighborSurfaceY + 1);
|
||||
return boundary(
|
||||
Math.min(currentBoundaryY, neighborSurfaceY + 1),
|
||||
Math.max(currentBoundaryEndY, fluidHeight)
|
||||
);
|
||||
}
|
||||
|
||||
private static int roundedHeight(double height) {
|
||||
return Double.isFinite(height) ? (int) Math.round(height) : Integer.MIN_VALUE;
|
||||
}
|
||||
|
||||
static long boundary(int startY, int endY) {
|
||||
return ((long) startY << 32) | (endY & 0xffffffffL);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,6 +28,8 @@ import art.arcane.iris.engine.object.IrisDecorationPart;
|
||||
import art.arcane.iris.engine.object.IrisDecorator;
|
||||
import art.arcane.iris.engine.object.IrisDimensionCarvingResolver;
|
||||
import art.arcane.iris.engine.object.IrisProceduralBlocks;
|
||||
import art.arcane.iris.engine.river.cave.RiverCaveAction;
|
||||
import art.arcane.iris.engine.river.cave.RiverCaveHydrology;
|
||||
import art.arcane.iris.util.project.context.ChunkContext;
|
||||
import art.arcane.iris.util.common.data.B;
|
||||
import art.arcane.volmlib.util.documentation.ChunkCoordinates;
|
||||
@@ -105,49 +107,26 @@ public class IrisCarveModifier extends EngineAssignedModifier<PlatformBlockState
|
||||
PrecisionStopwatch resolveStopwatch = PrecisionStopwatch.start();
|
||||
int worldHeightSpan = getEngine().getWorld().maxHeight() - getEngine().getWorld().minHeight();
|
||||
int caveLavaHeight = getEngine().getDimension().getCaveLavaHeight();
|
||||
mantleChunk.iterate(MatterCavern.class, (xx, yy, zz, cavern) -> {
|
||||
if (cavern == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (yy >= worldHeightSpan || yy <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
int rx = xx & 15;
|
||||
int rz = zz & 15;
|
||||
int columnIndex = PowerOfTwoCoordinates.packLocal16(rx, rz);
|
||||
|
||||
if (upperSurfaceHeights != null && yy >= upperSurfaceHeights[columnIndex]) {
|
||||
return;
|
||||
}
|
||||
|
||||
PlatformBlockState current = output.getRaw(rx, yy, rz);
|
||||
boolean explicitCarveIntent = hasExplicitCarveIntent(cavern);
|
||||
|
||||
if (shouldPreserveExistingFluid(cavern, current)) {
|
||||
return;
|
||||
}
|
||||
|
||||
columnMasks[columnIndex].add(yy);
|
||||
|
||||
if (!cavern.getCustomBiome().isEmpty()) {
|
||||
scratch.customCaveBiomePresent = true;
|
||||
}
|
||||
|
||||
if (current.isAir() && !explicitCarveIntent) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (explicitCarveIntent) {
|
||||
// Only a fluid cavern consumes the fluid sample, and on the maintenance path that
|
||||
// sample is a full procedural stream evaluation, so never take it per voxel.
|
||||
PlatformBlockState fluid = isFluidIntent(cavern) ? context.getFluid().get(rx, rz) : null;
|
||||
output.setRaw(rx, yy, rz, resolveExplicitCarveState(cavern, fluid, LAVA, AIR));
|
||||
} else if (usesDefaultLava(caveLavaHeight, yy)) {
|
||||
output.setRaw(rx, yy, rz, LAVA);
|
||||
} else {
|
||||
output.setRaw(rx, yy, rz, AIR);
|
||||
CarveResolutionContext resolutionContext = new CarveResolutionContext(
|
||||
output,
|
||||
context,
|
||||
scratch,
|
||||
columnMasks,
|
||||
upperSurfaceHeights,
|
||||
worldHeightSpan,
|
||||
caveLavaHeight
|
||||
);
|
||||
CarveResolver carveResolver = new CarveResolver(resolutionContext);
|
||||
mantleChunk.iterate(MatterCavern.class, (xx, yy, zz, cavern) -> carveResolver.apply(
|
||||
xx,
|
||||
yy,
|
||||
zz,
|
||||
cavern,
|
||||
dataIfPresent(mantleChunk, xx, yy, zz, RiverCaveHydrology.class)
|
||||
));
|
||||
mantleChunk.iterate(RiverCaveHydrology.class, (xx, yy, zz, hydrology) -> {
|
||||
if (dataIfPresent(mantleChunk, xx, yy, zz, MatterCavern.class) == null) {
|
||||
carveResolver.apply(xx, yy, zz, null, hydrology);
|
||||
}
|
||||
});
|
||||
if (scratch.customCaveBiomePresent) {
|
||||
@@ -161,6 +140,11 @@ public class IrisCarveModifier extends EngineAssignedModifier<PlatformBlockState
|
||||
PrecisionStopwatch applyStopwatch = PrecisionStopwatch.start();
|
||||
try {
|
||||
walls.forEach((rx, yy, rz, cavern) -> {
|
||||
RiverCaveHydrology hydrology = dataIfPresent(
|
||||
mantleChunk, rx, yy, rz, RiverCaveHydrology.class);
|
||||
if (hydrology != null && hydrology.protectsPlacement()) {
|
||||
return;
|
||||
}
|
||||
int worldX = rx + chunkBlockX;
|
||||
int worldZ = rz + chunkBlockZ;
|
||||
String customBiome = cavern.getCustomBiome();
|
||||
@@ -179,14 +163,39 @@ public class IrisCarveModifier extends EngineAssignedModifier<PlatformBlockState
|
||||
});
|
||||
|
||||
for (int columnIndex = 0; columnIndex < 256; columnIndex++) {
|
||||
processColumnFromMask(output, mantleChunk, mantle, columnMasks[columnIndex], columnIndex, x, z, resolverState, caveBiomeCache, customBiomeCache);
|
||||
int localX = PowerOfTwoCoordinates.unpackLocal16X(columnIndex);
|
||||
int localZ = columnIndex & 15;
|
||||
processColumnFromMask(
|
||||
output,
|
||||
mantleChunk,
|
||||
mantle,
|
||||
columnMasks[columnIndex],
|
||||
columnIndex,
|
||||
x,
|
||||
z,
|
||||
resolverState,
|
||||
caveBiomeCache,
|
||||
customBiomeCache,
|
||||
context.getFluid().get(localX, localZ)
|
||||
);
|
||||
}
|
||||
|
||||
for (int columnIndex = 0; columnIndex < 256; columnIndex++) {
|
||||
if (boundaryMasks[columnIndex].isEmpty() || !columnMasks[columnIndex].isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
processBoundaryColumnFromMask(output, boundaryMasks[columnIndex], walls, columnIndex, x, z, resolverState, caveBiomeCache, customBiomeCache);
|
||||
processBoundaryColumnFromMask(
|
||||
output,
|
||||
mantleChunk,
|
||||
boundaryMasks[columnIndex],
|
||||
walls,
|
||||
columnIndex,
|
||||
x,
|
||||
z,
|
||||
resolverState,
|
||||
caveBiomeCache,
|
||||
customBiomeCache
|
||||
);
|
||||
}
|
||||
|
||||
// Surface-break carving must not leave an ore cap suspended across the opening.
|
||||
@@ -252,6 +261,141 @@ public class IrisCarveModifier extends EngineAssignedModifier<PlatformBlockState
|
||||
return cavern.getLiquid() == 3 ? air : null;
|
||||
}
|
||||
|
||||
static MatterCavern composeCavern(MatterCavern baseline, RiverCaveHydrology hydrology) {
|
||||
return hydrology == null ? baseline : hydrology.asCavern();
|
||||
}
|
||||
|
||||
static PlatformBlockState resolveHydrologyState(
|
||||
RiverCaveHydrology hydrology,
|
||||
PlatformBlockState current,
|
||||
PlatformBlockState fluid,
|
||||
PlatformBlockState air
|
||||
) {
|
||||
if (hydrology == null) {
|
||||
return null;
|
||||
}
|
||||
return switch (hydrology.action()) {
|
||||
case WET_SOURCE -> fluid;
|
||||
case FALLING_WATER -> fallingFluidState(fluid);
|
||||
case DRY_AIR -> air;
|
||||
case SEAL_GUARD -> normalizeWaterlogging(current, null);
|
||||
};
|
||||
}
|
||||
|
||||
static PlatformBlockState normalizeWaterlogging(PlatformBlockState state, PlatformBlockState resultingFluid) {
|
||||
if (state == null || B.isFluid(state) || !IrisProceduralBlocks.hasProperty(state, "waterlogged")) {
|
||||
return state;
|
||||
}
|
||||
String target = resultingFluid != null && resultingFluid.isWater() ? "true" : "false";
|
||||
if (target.equals(IrisProceduralBlocks.propertyValue(state, "waterlogged"))) {
|
||||
return state;
|
||||
}
|
||||
return state.withProperty("waterlogged", target);
|
||||
}
|
||||
|
||||
static PlatformBlockState normalizeHydrologyWaterlogging(
|
||||
PlatformBlockState state,
|
||||
MatterCavern baseline,
|
||||
RiverCaveHydrology hydrology,
|
||||
PlatformBlockState columnFluid
|
||||
) {
|
||||
if (hydrology == null) {
|
||||
return state;
|
||||
}
|
||||
MatterCavern composed = composeCavern(baseline, hydrology);
|
||||
PlatformBlockState resultingFluid = isFluidIntent(composed) ? columnFluid : null;
|
||||
return normalizeWaterlogging(state, resultingFluid);
|
||||
}
|
||||
|
||||
private static PlatformBlockState fallingFluidState(PlatformBlockState fluid) {
|
||||
if (fluid == null || !IrisProceduralBlocks.hasProperty(fluid, "level")) {
|
||||
return fluid;
|
||||
}
|
||||
if ("8".equals(IrisProceduralBlocks.propertyValue(fluid, "level"))) {
|
||||
return fluid;
|
||||
}
|
||||
return fluid.withProperty("level", "8");
|
||||
}
|
||||
|
||||
private final class CarveResolver {
|
||||
private final CarveResolutionContext context;
|
||||
|
||||
private CarveResolver(CarveResolutionContext context) {
|
||||
this.context = context;
|
||||
}
|
||||
|
||||
private void apply(
|
||||
int x,
|
||||
int y,
|
||||
int z,
|
||||
MatterCavern baseline,
|
||||
RiverCaveHydrology hydrology
|
||||
) {
|
||||
if (y >= context.worldHeightSpan() || y <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
int localX = x & 15;
|
||||
int localZ = z & 15;
|
||||
int columnIndex = PowerOfTwoCoordinates.packLocal16(localX, localZ);
|
||||
if (context.upperSurfaceHeights() != null && y >= context.upperSurfaceHeights()[columnIndex]) {
|
||||
return;
|
||||
}
|
||||
|
||||
PlatformBlockState current = context.output().getRaw(localX, y, localZ);
|
||||
if (hydrology != null && hydrology.action() == RiverCaveAction.SEAL_GUARD) {
|
||||
PlatformBlockState normalized = resolveHydrologyState(hydrology, current, null, AIR);
|
||||
if (normalized != current) {
|
||||
context.output().setRaw(localX, y, localZ, normalized);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
MatterCavern cavern = composeCavern(baseline, hydrology);
|
||||
if (cavern == null || shouldPreserveExistingFluid(cavern, current)) {
|
||||
return;
|
||||
}
|
||||
|
||||
context.columnMasks()[columnIndex].add(y);
|
||||
if (!cavern.getCustomBiome().isEmpty()) {
|
||||
context.scratch().customCaveBiomePresent = true;
|
||||
}
|
||||
|
||||
boolean explicitCarveIntent = hasExplicitCarveIntent(cavern);
|
||||
if (current.isAir() && !explicitCarveIntent) {
|
||||
return;
|
||||
}
|
||||
|
||||
PlatformBlockState fluid = isFluidIntent(cavern)
|
||||
? context.chunkContext().getFluid().get(localX, localZ)
|
||||
: null;
|
||||
if (hydrology != null) {
|
||||
context.output().setRaw(localX, y, localZ,
|
||||
resolveHydrologyState(hydrology, current, fluid, AIR));
|
||||
return;
|
||||
}
|
||||
if (explicitCarveIntent) {
|
||||
context.output().setRaw(localX, y, localZ,
|
||||
resolveExplicitCarveState(cavern, fluid, LAVA, AIR));
|
||||
} else if (usesDefaultLava(context.caveLavaHeight(), y)) {
|
||||
context.output().setRaw(localX, y, localZ, LAVA);
|
||||
} else {
|
||||
context.output().setRaw(localX, y, localZ, AIR);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private record CarveResolutionContext(
|
||||
Hunk<PlatformBlockState> output,
|
||||
ChunkContext chunkContext,
|
||||
IrisCarveScratch scratch,
|
||||
CarveColumnMask[] columnMasks,
|
||||
int[] upperSurfaceHeights,
|
||||
int worldHeightSpan,
|
||||
int caveLavaHeight
|
||||
) {
|
||||
}
|
||||
|
||||
private void addInternalWallsFromMasks(CarveWallBuffer walls, CarveColumnMask[] columnMasks) {
|
||||
for (int columnIndex = 0; columnIndex < 256; columnIndex++) {
|
||||
CarveColumnMask columnMask = columnMasks[columnIndex];
|
||||
@@ -291,18 +435,18 @@ public class IrisCarveModifier extends EngineAssignedModifier<PlatformBlockState
|
||||
int rz = columnIndex & 15;
|
||||
int yy = columnMask.nextSetBit(0);
|
||||
while (yy >= 0) {
|
||||
MatterCavern cavern = mc.get(rx, yy, rz, MatterCavern.class);
|
||||
MatterCavern cavern = composedCavernAt(mc, rx, yy, rz);
|
||||
if (cavern != null) {
|
||||
if (rz < 15 && mc.get(rx, yy, rz + 1, MatterCavern.class) == null) {
|
||||
if (rz < 15 && composedCavernAt(mc, rx, yy, rz + 1) == null) {
|
||||
walls.put(rx, yy, rz + 1, cavern);
|
||||
}
|
||||
if (rx < 15 && mc.get(rx + 1, yy, rz, MatterCavern.class) == null) {
|
||||
if (rx < 15 && composedCavernAt(mc, rx + 1, yy, rz) == null) {
|
||||
walls.put(rx + 1, yy, rz, cavern);
|
||||
}
|
||||
if (rz > 0 && mc.get(rx, yy, rz - 1, MatterCavern.class) == null) {
|
||||
if (rz > 0 && composedCavernAt(mc, rx, yy, rz - 1) == null) {
|
||||
walls.put(rx, yy, rz - 1, cavern);
|
||||
}
|
||||
if (rx > 0 && mc.get(rx - 1, yy, rz, MatterCavern.class) == null) {
|
||||
if (rx > 0 && composedCavernAt(mc, rx - 1, yy, rz) == null) {
|
||||
walls.put(rx - 1, yy, rz, cavern);
|
||||
}
|
||||
}
|
||||
@@ -370,11 +514,11 @@ public class IrisCarveModifier extends EngineAssignedModifier<PlatformBlockState
|
||||
int neighborX,
|
||||
int neighborZ
|
||||
) {
|
||||
if (mc.get(localX, yy, localZ, MatterCavern.class) != null) {
|
||||
if (composedCavernAt(mc, localX, yy, localZ) != null) {
|
||||
return;
|
||||
}
|
||||
|
||||
MatterCavern neighbor = neighborChunk.get(neighborX, yy, neighborZ, MatterCavern.class);
|
||||
MatterCavern neighbor = composedCavernAt(neighborChunk, neighborX, yy, neighborZ);
|
||||
if (neighbor == null) {
|
||||
return;
|
||||
}
|
||||
@@ -392,6 +536,24 @@ public class IrisCarveModifier extends EngineAssignedModifier<PlatformBlockState
|
||||
return plate.get(chunkX & 31, chunkZ & 31);
|
||||
}
|
||||
|
||||
private MatterCavern composedCavernAt(MantleChunk<Matter> mantleChunk, int x, int y, int z) {
|
||||
MatterCavern baseline = dataIfPresent(mantleChunk, x, y, z, MatterCavern.class);
|
||||
RiverCaveHydrology hydrology = dataIfPresent(mantleChunk, x, y, z, RiverCaveHydrology.class);
|
||||
return composeCavern(baseline, hydrology);
|
||||
}
|
||||
|
||||
private static <T> T dataIfPresent(MantleChunk<Matter> mantleChunk, int x, int y, int z, Class<T> type) {
|
||||
int section = y >> 4;
|
||||
if (y < 0 || !mantleChunk.exists(section)) {
|
||||
return null;
|
||||
}
|
||||
Matter matter = mantleChunk.get(section);
|
||||
if (matter == null || !matter.hasSlice(type)) {
|
||||
return null;
|
||||
}
|
||||
return matter.<T>getSlice(type).get(x & 15, y & 15, z & 15);
|
||||
}
|
||||
|
||||
private void processColumnFromMask(
|
||||
Hunk<PlatformBlockState> output,
|
||||
MantleChunk<Matter> mc,
|
||||
@@ -402,7 +564,8 @@ public class IrisCarveModifier extends EngineAssignedModifier<PlatformBlockState
|
||||
int chunkZ,
|
||||
IrisDimensionCarvingResolver.State resolverState,
|
||||
Long2ObjectOpenHashMap<IrisBiome> caveBiomeCache,
|
||||
Map<String, IrisBiome> customBiomeCache
|
||||
Map<String, IrisBiome> customBiomeCache,
|
||||
PlatformBlockState columnFluid
|
||||
) {
|
||||
if (columnMask == null || columnMask.isEmpty()) {
|
||||
return;
|
||||
@@ -429,7 +592,8 @@ public class IrisCarveModifier extends EngineAssignedModifier<PlatformBlockState
|
||||
zone.ceiling = buf;
|
||||
} else {
|
||||
if (zone.isValid(getEngine())) {
|
||||
processZone(output, mc, mantle, zone, rx, rz, worldX, worldZ, resolverState, caveBiomeCache, customBiomeCache);
|
||||
processZone(output, mc, mantle, zone, rx, rz, worldX, worldZ, resolverState,
|
||||
caveBiomeCache, customBiomeCache, columnFluid);
|
||||
}
|
||||
zone = new CaveZone();
|
||||
zone.setFloor(y);
|
||||
@@ -441,12 +605,14 @@ public class IrisCarveModifier extends EngineAssignedModifier<PlatformBlockState
|
||||
}
|
||||
|
||||
if (zone.isValid(getEngine())) {
|
||||
processZone(output, mc, mantle, zone, rx, rz, worldX, worldZ, resolverState, caveBiomeCache, customBiomeCache);
|
||||
processZone(output, mc, mantle, zone, rx, rz, worldX, worldZ, resolverState,
|
||||
caveBiomeCache, customBiomeCache, columnFluid);
|
||||
}
|
||||
}
|
||||
|
||||
private void processBoundaryColumnFromMask(
|
||||
Hunk<PlatformBlockState> output,
|
||||
MantleChunk<Matter> mantleChunk,
|
||||
CarveColumnMask boundaryMask,
|
||||
CarveWallBuffer walls,
|
||||
int columnIndex,
|
||||
@@ -473,18 +639,21 @@ public class IrisCarveModifier extends EngineAssignedModifier<PlatformBlockState
|
||||
if (y == zoneCeiling + 1) {
|
||||
zoneCeiling = y;
|
||||
} else {
|
||||
paintBoundaryZone(output, walls, rx, rz, worldX, worldZ, zoneFloor, zoneCeiling, resolverState, caveBiomeCache, customBiomeCache);
|
||||
paintBoundaryZone(output, mantleChunk, walls, rx, rz, worldX, worldZ, zoneFloor, zoneCeiling,
|
||||
resolverState, caveBiomeCache, customBiomeCache);
|
||||
zoneFloor = y;
|
||||
zoneCeiling = y;
|
||||
}
|
||||
y = boundaryMask.nextSetBit(y + 1);
|
||||
}
|
||||
|
||||
paintBoundaryZone(output, walls, rx, rz, worldX, worldZ, zoneFloor, zoneCeiling, resolverState, caveBiomeCache, customBiomeCache);
|
||||
paintBoundaryZone(output, mantleChunk, walls, rx, rz, worldX, worldZ, zoneFloor, zoneCeiling,
|
||||
resolverState, caveBiomeCache, customBiomeCache);
|
||||
}
|
||||
|
||||
private void paintBoundaryZone(
|
||||
Hunk<PlatformBlockState> output,
|
||||
MantleChunk<Matter> mantleChunk,
|
||||
CarveWallBuffer walls,
|
||||
int rx,
|
||||
int rz,
|
||||
@@ -517,6 +686,11 @@ public class IrisCarveModifier extends EngineAssignedModifier<PlatformBlockState
|
||||
if (floorY < 0) {
|
||||
break;
|
||||
}
|
||||
RiverCaveHydrology hydrology = dataIfPresent(
|
||||
mantleChunk, rx, floorY, rz, RiverCaveHydrology.class);
|
||||
if (hydrology != null && hydrology.protectsPlacement()) {
|
||||
continue;
|
||||
}
|
||||
PlatformBlockState existing = output.getRaw(rx, floorY, rz);
|
||||
PlatformBlockState layer = floorLayers.get(i);
|
||||
if (!B.isSolid(existing) || !canReplaceCaveFloorLayer(output, rx, floorY, rz, layer)) {
|
||||
@@ -539,6 +713,11 @@ public class IrisCarveModifier extends EngineAssignedModifier<PlatformBlockState
|
||||
if (ceilingY >= worldMaxY) {
|
||||
break;
|
||||
}
|
||||
RiverCaveHydrology hydrology = dataIfPresent(
|
||||
mantleChunk, rx, ceilingY, rz, RiverCaveHydrology.class);
|
||||
if (hydrology != null && hydrology.protectsPlacement()) {
|
||||
continue;
|
||||
}
|
||||
PlatformBlockState existing = output.getRaw(rx, ceilingY, rz);
|
||||
if (!B.isSolid(existing)) {
|
||||
continue;
|
||||
@@ -565,7 +744,12 @@ public class IrisCarveModifier extends EngineAssignedModifier<PlatformBlockState
|
||||
return (h & 15L) == 0L;
|
||||
}
|
||||
|
||||
private void processZone(Hunk<PlatformBlockState> output, MantleChunk<Matter> mc, Mantle<Matter> mantle, CaveZone zone, int rx, int rz, int xx, int zz, IrisDimensionCarvingResolver.State resolverState, Long2ObjectOpenHashMap<IrisBiome> caveBiomeCache, Map<String, IrisBiome> customBiomeCache) {
|
||||
private void processZone(Hunk<PlatformBlockState> output, MantleChunk<Matter> mc, Mantle<Matter> mantle,
|
||||
CaveZone zone, int rx, int rz, int xx, int zz,
|
||||
IrisDimensionCarvingResolver.State resolverState,
|
||||
Long2ObjectOpenHashMap<IrisBiome> caveBiomeCache,
|
||||
Map<String, IrisBiome> customBiomeCache,
|
||||
PlatformBlockState columnFluid) {
|
||||
int maxY = output.getHeight();
|
||||
|
||||
if (zone.ceiling + 1 < maxY && B.isDecorant(output.getRaw(rx, zone.ceiling + 1, rz))) {
|
||||
@@ -590,6 +774,7 @@ public class IrisCarveModifier extends EngineAssignedModifier<PlatformBlockState
|
||||
IrisBiome floorBiome = resolveCaveBoundaryBiome(mc, rx, zone.floor, rz, xx, zz, resolverState, caveBiomeCache, customBiomeCache);
|
||||
IrisBiome ceilingBiome = resolveCaveBoundaryBiome(mc, rx, zone.ceiling, rz, xx, zz, resolverState, caveBiomeCache, customBiomeCache);
|
||||
if (floorBiome == null && ceilingBiome == null) {
|
||||
normalizeCaveZoneWaterlogging(output, mc, zone, rx, rz, columnFluid);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -600,6 +785,10 @@ public class IrisCarveModifier extends EngineAssignedModifier<PlatformBlockState
|
||||
break;
|
||||
}
|
||||
int y = zone.floor - i - 1;
|
||||
RiverCaveHydrology hydrology = dataIfPresent(mc, rx, y, rz, RiverCaveHydrology.class);
|
||||
if (hydrology != null && hydrology.protectsPlacement()) {
|
||||
continue;
|
||||
}
|
||||
PlatformBlockState block = floorBlocks.get(i);
|
||||
PlatformBlockState existing = output.getRaw(rx, y, rz);
|
||||
if (!B.isSolid(existing) || !canReplaceCaveFloorLayer(output, rx, y, rz, block)) {
|
||||
@@ -620,6 +809,10 @@ public class IrisCarveModifier extends EngineAssignedModifier<PlatformBlockState
|
||||
if (cy >= maxY) {
|
||||
break;
|
||||
}
|
||||
RiverCaveHydrology hydrology = dataIfPresent(mc, rx, cy, rz, RiverCaveHydrology.class);
|
||||
if (hydrology != null && hydrology.protectsPlacement()) {
|
||||
continue;
|
||||
}
|
||||
PlatformBlockState block = ceilingBlocks.get(i);
|
||||
PlatformBlockState existing = output.getRaw(rx, cy, rz);
|
||||
if (!B.isSolid(existing)) {
|
||||
@@ -646,10 +839,43 @@ public class IrisCarveModifier extends EngineAssignedModifier<PlatformBlockState
|
||||
if (ceilingDecorators.length > 0 && zone.getCeiling() + 1 < maxY && B.isSolid(output.getRaw(rx, zone.getCeiling() + 1, rz))) {
|
||||
decorant.getCeilingDecorator().decorate(rx, rz, xx, xx, xx, zz, zz, zz, output, ceilingBiome, InferredType.CAVE, zone.getCeiling(), zone.airThickness());
|
||||
}
|
||||
|
||||
normalizeCaveZoneWaterlogging(output, mc, zone, rx, rz, columnFluid);
|
||||
}
|
||||
|
||||
private void normalizeCaveZoneWaterlogging(
|
||||
Hunk<PlatformBlockState> output,
|
||||
MantleChunk<Matter> mantleChunk,
|
||||
CaveZone zone,
|
||||
int localX,
|
||||
int localZ,
|
||||
PlatformBlockState columnFluid
|
||||
) {
|
||||
int minimumY = Math.max(0, zone.floor - 1);
|
||||
int maximumY = Math.min(output.getHeight() - 1, zone.ceiling + 1);
|
||||
for (int y = minimumY; y <= maximumY; y++) {
|
||||
RiverCaveHydrology hydrology = dataIfPresent(
|
||||
mantleChunk, localX, y, localZ, RiverCaveHydrology.class);
|
||||
if (hydrology == null) {
|
||||
continue;
|
||||
}
|
||||
MatterCavern baseline = dataIfPresent(
|
||||
mantleChunk, localX, y, localZ, MatterCavern.class);
|
||||
PlatformBlockState current = output.getRaw(localX, y, localZ);
|
||||
PlatformBlockState normalized = normalizeHydrologyWaterlogging(
|
||||
current,
|
||||
baseline,
|
||||
hydrology,
|
||||
columnFluid
|
||||
);
|
||||
if (normalized != current) {
|
||||
output.setRaw(localX, y, localZ, normalized);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
IrisBiome resolveCaveBoundaryBiome(MantleChunk<Matter> mantleChunk, int x, int y, int z, int worldX, int worldZ, IrisDimensionCarvingResolver.State resolverState, Long2ObjectOpenHashMap<IrisBiome> caveBiomeCache, Map<String, IrisBiome> customBiomeCache) {
|
||||
MatterCavern cavern = mantleChunk.get(x, y, z, MatterCavern.class);
|
||||
MatterCavern cavern = composedCavernAt(mantleChunk, x, y, z);
|
||||
return resolveCaveBoundaryBiome(
|
||||
cavern, worldX, y, worldZ, resolverState, caveBiomeCache, customBiomeCache);
|
||||
}
|
||||
|
||||
@@ -57,10 +57,9 @@ public class IrisPostModifier extends EngineAssignedModifier<PlatformBlockState>
|
||||
IrisDimension dimension = getDimension();
|
||||
boolean walls = dimension.isPostProcessingWalls();
|
||||
boolean slabs = dimension.isPostProcessingSlabs();
|
||||
int fluidHeight = dimension.getFluidHeight();
|
||||
for (int i = 0; i < width; i++) {
|
||||
for (int j = 0; j < depth; j++) {
|
||||
post(i, j, sync, i + x, j + z, context, heights, planeWidth, walls, slabs, fluidHeight);
|
||||
post(i, j, sync, i + x, j + z, context, heights, planeWidth, walls, slabs);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -90,7 +89,7 @@ public class IrisPostModifier extends EngineAssignedModifier<PlatformBlockState>
|
||||
return heights;
|
||||
}
|
||||
|
||||
private void post(int currentPostX, int currentPostZ, Hunk<PlatformBlockState> currentData, int x, int z, ChunkContext context, int[] heights, int planeWidth, boolean walls, boolean slabs, int fluidHeight) {
|
||||
private void post(int currentPostX, int currentPostZ, Hunk<PlatformBlockState> currentData, int x, int z, ChunkContext context, int[] heights, int planeWidth, boolean walls, boolean slabs) {
|
||||
// x/z are world coordinates, the hunk is indexed relative to this chunk origin.
|
||||
int originX = x - currentPostX;
|
||||
int originZ = z - currentPostZ;
|
||||
@@ -100,6 +99,7 @@ public class IrisPostModifier extends EngineAssignedModifier<PlatformBlockState>
|
||||
int hb = heights[center + planeWidth];
|
||||
int hc = heights[center - 1];
|
||||
int hd = heights[center - planeWidth];
|
||||
int fluidHeight = (int) Math.round(getComplex().getRiverWaterSurfaceStream().get(x, z));
|
||||
|
||||
// Floating Nibs
|
||||
int g = 0;
|
||||
|
||||
@@ -50,6 +50,17 @@ public interface IObjectPlacer {
|
||||
|
||||
int getFluidHeight();
|
||||
|
||||
default int getFluidHeight(int x, int z) {
|
||||
Engine engine = getEngine();
|
||||
if (engine == null || engine.getComplex() == null) {
|
||||
return getFluidHeight();
|
||||
}
|
||||
int coordinateShift = getFluidHeight() - engine.getDimension().getFluidHeight();
|
||||
return coordinateShift + (int) Math.round(
|
||||
engine.getComplex().getRiverWaterSurfaceStream().get(x, z)
|
||||
);
|
||||
}
|
||||
|
||||
boolean isDebugSmartBore();
|
||||
|
||||
void setTile(int xx, int yy, int zz, TileData tile);
|
||||
|
||||
@@ -130,6 +130,8 @@ public class IrisBiome extends IrisRegistrant implements IRare {
|
||||
private int lockLayersMax = 7;
|
||||
@Desc("Profile-driven 3D cave configuration")
|
||||
private IrisCaveProfile caveProfile = new IrisCaveProfile();
|
||||
@Desc("Biome-level river routing, shape, cave-entry, and biome-pool overrides. Omit to inherit region and dimension settings.")
|
||||
private IrisRiverOverride riverOverride = null;
|
||||
@MinNumber(1)
|
||||
@MaxNumber(512)
|
||||
@Desc("The rarity of this biome (integer)")
|
||||
|
||||
@@ -36,7 +36,7 @@ final class IrisBiomeColorRenderer {
|
||||
|
||||
static Color getColor(IrisBiome biome, Engine engine, RenderType type) {
|
||||
switch (type) {
|
||||
case BIOME, HEIGHT, CAVE_LAND, REGION, BIOME_SEA, BIOME_LAND -> {
|
||||
case BIOME, HEIGHT, CAVE_LAND, REGION, BIOME_SEA, BIOME_LAND, RIVER -> {
|
||||
return biome.getCacheColor().aquire(() -> {
|
||||
if (biome.getColor() == null) {
|
||||
RandomColor randomColor = new RandomColor(biome.getName().hashCode());
|
||||
|
||||
@@ -164,6 +164,8 @@ public class IrisDimension extends IrisRegistrant {
|
||||
private KList<IrisDimensionCarvingEntry> carving = new KList<>();
|
||||
@Desc("Profile-driven 3D cave configuration")
|
||||
private IrisCaveProfile caveProfile = new IrisCaveProfile();
|
||||
@Desc("Connected surface rivers and contained river cave-water generation.")
|
||||
private IrisRiverNetwork rivers = new IrisRiverNetwork();
|
||||
@Desc("Refuse to place surface objects and trees over carved surface openings.")
|
||||
private boolean requireObjectSurfaceSupport = true;
|
||||
@MinNumber(0)
|
||||
@@ -489,6 +491,11 @@ public class IrisDimension extends IrisRegistrant {
|
||||
}
|
||||
|
||||
Deque<String> pending = new ArrayDeque<>();
|
||||
IrisRiverNetwork riverNetwork = getRivers();
|
||||
boolean riversEnabled = riverNetwork != null && riverNetwork.isEnabled();
|
||||
if (riversEnabled && riverNetwork.getBiomes() != null) {
|
||||
addReachableBiomeKeys(pending, riverNetwork.getBiomes().getAllBiomeIds());
|
||||
}
|
||||
KList<String> regionKeys = getRegions();
|
||||
if (regionKeys != null) {
|
||||
for (String regionKey : regionKeys) {
|
||||
@@ -496,7 +503,8 @@ public class IrisDimension extends IrisRegistrant {
|
||||
if (region == null) {
|
||||
continue;
|
||||
}
|
||||
addReachableBiomeKeys(pending, region.getAllBiomeIds());
|
||||
addReachableBiomeKeys(pending,
|
||||
riversEnabled ? region.getAllBiomeIds() : region.getNaturalBiomeIds());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -531,6 +539,9 @@ public class IrisDimension extends IrisRegistrant {
|
||||
biomes.put(loadKey, biome);
|
||||
addReachableBiomeKeys(pending, biome.getChildren());
|
||||
addReachableBiomeKey(pending, biome.getCarvingBiome());
|
||||
if (riversEnabled && biome.getRiverOverride() != null) {
|
||||
addReachableBiomeKeys(pending, biome.getRiverOverride().getAllBiomeIds());
|
||||
}
|
||||
|
||||
KList<IrisFloatingChildBiomes> floatingChildren = biome.getFloatingChildBiomes();
|
||||
if (floatingChildren == null) {
|
||||
|
||||
@@ -28,6 +28,9 @@ public enum IrisEngineStreamType {
|
||||
@Desc("Represents the given slope at the x, z coordinates")
|
||||
SLOPE((f) -> f.getComplex().getSlopeStream()),
|
||||
|
||||
@Desc("Represents terrain height before river incision and river biome replacement.")
|
||||
NATURAL_HEIGHT((f) -> f.getComplex().getNaturalHeightStream()),
|
||||
|
||||
@Desc("Represents the base generator height at the given position. This includes only the biome generators / interpolation and noise features but does not include carving, caves.")
|
||||
HEIGHT((f) -> f.getComplex().getHeightStream()),
|
||||
|
||||
@@ -41,7 +44,19 @@ public enum IrisEngineStreamType {
|
||||
REGION_STYLE((f) -> f.getComplex().getRegionStyleStream()),
|
||||
|
||||
@Desc("Represents the identity of regions. Each region has a unique number (very large numbers)")
|
||||
REGION_IDENTITY((f) -> f.getComplex().getRegionIdentityStream());
|
||||
REGION_IDENTITY((f) -> f.getComplex().getRegionIdentityStream()),
|
||||
|
||||
@Desc("Represents block distance from the nearest active river centerline.")
|
||||
RIVER_DISTANCE((f) -> f.getComplex().getRiverDistanceStream()),
|
||||
|
||||
@Desc("Represents the merged upstream flow carried by the active river reach.")
|
||||
RIVER_FLOW((f) -> f.getComplex().getRiverFlowStream()),
|
||||
|
||||
@Desc("Represents the normalized river terrain-incision weight.")
|
||||
RIVER_CARVE_WEIGHT((f) -> f.getComplex().getRiverCarveWeightStream()),
|
||||
|
||||
@Desc("Represents the solved river water-surface height.")
|
||||
RIVER_WATER_SURFACE((f) -> f.getComplex().getRiverWaterSurfaceStream());
|
||||
|
||||
private final Function<Engine, ProceduralStream<Double>> getter;
|
||||
|
||||
|
||||
@@ -332,7 +332,8 @@ final class IrisObjectPlacementRunner {
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (!config.isForcePlace() && !rawStructurePiece && config.isUnderwater() && y + rty + ty >= placer.getFluidHeight()) {
|
||||
if (!config.isForcePlace() && !rawStructurePiece && config.isUnderwater()
|
||||
&& y + rty + ty >= placer.getFluidHeight(x, z)) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
|
||||
@@ -118,6 +118,8 @@ public class IrisRegion extends IrisRegistrant implements IRare {
|
||||
private double caveBiomeZoom = 1;
|
||||
@Desc("Profile-driven 3D cave configuration")
|
||||
private IrisCaveProfile caveProfile = new IrisCaveProfile();
|
||||
@Desc("Region-level river routing, shape, cave-entry, and biome-pool overrides. Omit to inherit dimension settings.")
|
||||
private IrisRiverOverride riverOverride = null;
|
||||
@RegistryListResource(IrisBiome.class)
|
||||
@Required
|
||||
@ArrayType(min = 1, type = String.class)
|
||||
@@ -277,18 +279,33 @@ public class IrisRegion extends IrisRegistrant implements IRare {
|
||||
}
|
||||
|
||||
public KSet<String> getAllBiomeIds() {
|
||||
KSet<String> names = getNaturalBiomeIds();
|
||||
if (riverOverride != null) {
|
||||
names.addAll(riverOverride.getAllBiomeIds());
|
||||
}
|
||||
return names;
|
||||
}
|
||||
|
||||
public KSet<String> getNaturalBiomeIds() {
|
||||
KSet<String> names = new KSet<>();
|
||||
names.addAll(landBiomes);
|
||||
names.addAll(caveBiomes);
|
||||
names.addAll(seaBiomes);
|
||||
names.addAll(shoreBiomes);
|
||||
|
||||
return names;
|
||||
}
|
||||
|
||||
public KList<IrisBiome> getAllBiomes(DataProvider g) {
|
||||
return resolveBiomes(g, getAllBiomeIds());
|
||||
}
|
||||
|
||||
public KList<IrisBiome> getNaturalBiomes(DataProvider g) {
|
||||
return resolveBiomes(g, getNaturalBiomeIds());
|
||||
}
|
||||
|
||||
private KList<IrisBiome> resolveBiomes(DataProvider g, KSet<String> biomeIds) {
|
||||
KMap<String, IrisBiome> b = new KMap<>();
|
||||
KSet<String> names = getAllBiomeIds();
|
||||
KSet<String> names = biomeIds.copy();
|
||||
|
||||
while (!names.isEmpty()) {
|
||||
for (String i : new KList<>(names)) {
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
package art.arcane.iris.engine.object;
|
||||
|
||||
import art.arcane.iris.engine.object.annotations.ArrayType;
|
||||
import art.arcane.iris.engine.object.annotations.Desc;
|
||||
import art.arcane.iris.engine.object.annotations.RegistryListResource;
|
||||
import art.arcane.volmlib.util.collection.KList;
|
||||
import art.arcane.volmlib.util.collection.KSet;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.experimental.Accessors;
|
||||
|
||||
@Accessors(chain = true)
|
||||
@NoArgsConstructor
|
||||
@Desc("Biome pools used by dimension-level river sections and contained river caves.")
|
||||
@Data
|
||||
public class IrisRiverBiomes {
|
||||
@Desc("Noise used to select a biome inside the active river-section pool.")
|
||||
private IrisGeneratorStyle selectionStyle = new IrisGeneratorStyle(NoiseStyle.CELLULAR_IRIS_DOUBLE)
|
||||
.zoomed(512D);
|
||||
|
||||
@RegistryListResource(IrisBiome.class)
|
||||
@ArrayType(type = String.class)
|
||||
@Desc("Biome pool for wet river channels.")
|
||||
private KList<String> channel = new KList<>();
|
||||
|
||||
@RegistryListResource(IrisBiome.class)
|
||||
@ArrayType(type = String.class)
|
||||
@Desc("Biome pool for river banks outside the wet channel.")
|
||||
private KList<String> bank = new KList<>();
|
||||
|
||||
@RegistryListResource(IrisBiome.class)
|
||||
@ArrayType(type = String.class)
|
||||
@Desc("Biome pool for river reaches meeting natural sea.")
|
||||
private KList<String> mouth = new KList<>();
|
||||
|
||||
@RegistryListResource(IrisBiome.class)
|
||||
@ArrayType(type = String.class)
|
||||
@Desc("Biome pool for dry river channels and terminal tapers.")
|
||||
private KList<String> dry = new KList<>();
|
||||
|
||||
@RegistryListResource(IrisBiome.class)
|
||||
@ArrayType(type = String.class)
|
||||
@Desc("Cave biome pool for accepted contained river cave bodies.")
|
||||
private KList<String> floodedCave = new KList<>();
|
||||
|
||||
public KSet<String> getAllBiomeIds() {
|
||||
KSet<String> biomeIds = new KSet<>();
|
||||
addAll(biomeIds, channel);
|
||||
addAll(biomeIds, bank);
|
||||
addAll(biomeIds, mouth);
|
||||
addAll(biomeIds, dry);
|
||||
addAll(biomeIds, floodedCave);
|
||||
return biomeIds;
|
||||
}
|
||||
|
||||
private static void addAll(KSet<String> destination, KList<String> source) {
|
||||
if (source != null) {
|
||||
destination.addAll(source);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package art.arcane.iris.engine.object;
|
||||
|
||||
import art.arcane.iris.engine.object.annotations.Desc;
|
||||
|
||||
@Desc("Selects the fallback when a requested river cave connection cannot be proven safe.")
|
||||
public enum IrisRiverCaveFallback {
|
||||
@Desc("Keep the rejected connection sealed.")
|
||||
SEALED,
|
||||
|
||||
@Desc("Try a bounded generated grotto instead of the rejected existing cave.")
|
||||
GENERATE_GROTTO
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package art.arcane.iris.engine.object;
|
||||
|
||||
import art.arcane.iris.engine.object.annotations.Desc;
|
||||
|
||||
@Desc("Selects the contained cave-water behavior available to river entry events.")
|
||||
public enum IrisRiverCaveMode {
|
||||
@Desc("Keep the surface reservoir sealed from caves.")
|
||||
SEALED,
|
||||
|
||||
@Desc("Flood only an existing cave component whose complete fluid-reachable boundary is proven closed.")
|
||||
FLOOD_CLOSED_COMPONENT,
|
||||
|
||||
@Desc("Generate a bounded grotto with a guaranteed solid shell.")
|
||||
GENERATE_GROTTO,
|
||||
|
||||
@Desc("Use a proven closed cave component when available, otherwise generate a bounded grotto.")
|
||||
GROTTO_OR_CLOSED_COMPONENT,
|
||||
|
||||
@Desc("Generate a controlled falling column into a proven contained pool.")
|
||||
WATERFALL_POOL
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
package art.arcane.iris.engine.object;
|
||||
|
||||
import art.arcane.iris.engine.object.annotations.Desc;
|
||||
import art.arcane.iris.engine.object.annotations.MaxNumber;
|
||||
import art.arcane.iris.engine.object.annotations.MinNumber;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.experimental.Accessors;
|
||||
|
||||
@Accessors(chain = true)
|
||||
@NoArgsConstructor
|
||||
@Desc("Controls bounded and transactionally validated river-to-cave connections.")
|
||||
@Data
|
||||
public class IrisRiverCaves {
|
||||
@Desc("The contained cave-water behavior available to river entry events.")
|
||||
private IrisRiverCaveMode mode = IrisRiverCaveMode.SEALED;
|
||||
|
||||
@Desc("Selects cave-entry stations at stable river-reach anchors.")
|
||||
private IrisRiverNoiseChance entry = new IrisRiverNoiseChance()
|
||||
.setChance(0.12D)
|
||||
.setStyle(new IrisGeneratorStyle(NoiseStyle.IRIS).zoomed(1024D))
|
||||
.setInfluence(0.4D);
|
||||
|
||||
@MinNumber(16)
|
||||
@MaxNumber(4096)
|
||||
@Desc("The minimum distance in blocks between cave-entry candidates.")
|
||||
private int minimumSpacing = 128;
|
||||
|
||||
@MinNumber(0)
|
||||
@MaxNumber(16)
|
||||
@Desc("The maximum entry-noise-eligible cave anchors accepted on one ordinary reach. A forced sinkhole terminal uses its reach exclusively and requires this value above zero.")
|
||||
private int maximumPerReach = 1;
|
||||
|
||||
@MinNumber(1)
|
||||
@MaxNumber(256)
|
||||
@Desc("The maximum vertical distance searched while boring from river bed to cave.")
|
||||
private int maxBoreDepth = 48;
|
||||
|
||||
@MinNumber(1)
|
||||
@MaxNumber(16)
|
||||
@Desc("The radius in blocks of a generated river-to-cave throat.")
|
||||
private int throatRadius = 2;
|
||||
|
||||
@MinNumber(-64)
|
||||
@MaxNumber(64)
|
||||
@Desc("The offset applied to river water height when filling an accepted cave body.")
|
||||
private int waterLevelOffset = 0;
|
||||
|
||||
@MinNumber(0)
|
||||
@MaxNumber(64)
|
||||
@Desc("The minimum dry headroom retained above water in generated grottos.")
|
||||
private int dryHeadroom = 4;
|
||||
|
||||
@MinNumber(2)
|
||||
@MaxNumber(128)
|
||||
@Desc("The horizontal radius in blocks of a generated sealed grotto.")
|
||||
private int grottoHorizontalRadius = 12;
|
||||
|
||||
@MinNumber(2)
|
||||
@MaxNumber(128)
|
||||
@Desc("The vertical radius in blocks of a generated sealed grotto.")
|
||||
private int grottoVerticalRadius = 7;
|
||||
|
||||
@Desc("Noise shaping the boundary of generated sealed grottos.")
|
||||
private IrisGeneratorStyle grottoShapeStyle = new IrisGeneratorStyle(NoiseStyle.IRIS).zoomed(24D);
|
||||
|
||||
@Desc("Noise warping the coordinate field used for generated sealed grottos.")
|
||||
private IrisGeneratorStyle grottoWarpStyle = new IrisGeneratorStyle(NoiseStyle.IRIS).zoomed(48D);
|
||||
|
||||
@MinNumber(0)
|
||||
@MaxNumber(32)
|
||||
@Desc("The maximum coordinate warp applied to generated sealed grottos in blocks.")
|
||||
private double grottoWarpStrength = 2D;
|
||||
|
||||
@MinNumber(4)
|
||||
@MaxNumber(256)
|
||||
@Desc("The horizontal proof radius for an existing closed cave component.")
|
||||
private int maxFloodRadius = 48;
|
||||
|
||||
@MinNumber(4)
|
||||
@MaxNumber(256)
|
||||
@Desc("The vertical proof depth for an existing closed cave component.")
|
||||
private int maxFloodDepth = 32;
|
||||
|
||||
@MinNumber(64)
|
||||
@MaxNumber(1048576)
|
||||
@Desc("The greatest cave-component volume that may be fully proven and flooded.")
|
||||
private int maxFloodVolume = 8192;
|
||||
|
||||
@Desc("The behavior used when a requested cave connection cannot be proven safe.")
|
||||
private IrisRiverCaveFallback fallback = IrisRiverCaveFallback.SEALED;
|
||||
|
||||
@Desc("The policy for fluid already present in a candidate contained cave body.")
|
||||
private IrisRiverExistingFluidPolicy existingFluidPolicy = IrisRiverExistingFluidPolicy.REJECT;
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package art.arcane.iris.engine.object;
|
||||
|
||||
import art.arcane.iris.engine.object.annotations.Desc;
|
||||
|
||||
@Desc("Controls how river cave hydrology treats fluid already present in a candidate cave body.")
|
||||
public enum IrisRiverExistingFluidPolicy {
|
||||
@Desc("Reject a candidate containing any existing fluid.")
|
||||
REJECT,
|
||||
|
||||
@Desc("Accept only fluid compatible with the river fluid palette.")
|
||||
ALLOW_SAME,
|
||||
|
||||
@Desc("Replace contained existing fluid with the river fluid palette.")
|
||||
REPLACE
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
package art.arcane.iris.engine.object;
|
||||
|
||||
import art.arcane.iris.engine.object.annotations.Desc;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.experimental.Accessors;
|
||||
|
||||
@Accessors(chain = true)
|
||||
@NoArgsConstructor
|
||||
@Desc("Dimension-owned configuration for connected surface rivers and contained river cave water.")
|
||||
@Data
|
||||
public class IrisRiverNetwork {
|
||||
@Desc("Enable the river network for this dimension.")
|
||||
private boolean enabled = false;
|
||||
|
||||
@Desc("Dimension-owned connected graph and source-selection settings.")
|
||||
private IrisRiverTopology topology = new IrisRiverTopology();
|
||||
|
||||
@Desc("Channel geometry, terrain incision, meanders, and terminal behavior.")
|
||||
private IrisRiverTerrain terrain = new IrisRiverTerrain();
|
||||
|
||||
@Desc("River water-surface settings.")
|
||||
private IrisRiverWater water = new IrisRiverWater();
|
||||
|
||||
@Desc("Dimension-level biome pools for river sections and contained river caves.")
|
||||
private IrisRiverBiomes biomes = new IrisRiverBiomes();
|
||||
|
||||
@Desc("Bounded river-to-cave connection settings.")
|
||||
private IrisRiverCaves caves = new IrisRiverCaves();
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
package art.arcane.iris.engine.object;
|
||||
|
||||
import art.arcane.iris.engine.object.annotations.Desc;
|
||||
import art.arcane.iris.engine.object.annotations.MaxNumber;
|
||||
import art.arcane.iris.engine.object.annotations.MinNumber;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.experimental.Accessors;
|
||||
|
||||
@Accessors(chain = true)
|
||||
@NoArgsConstructor
|
||||
@Desc("A deterministic graph-event chance modulated by configurable noise.")
|
||||
@Data
|
||||
public class IrisRiverNoiseChance {
|
||||
@MinNumber(0)
|
||||
@MaxNumber(1)
|
||||
@Desc("The base probability before noise modulation.")
|
||||
private double chance = 1D;
|
||||
|
||||
@Desc("The noise sampled once at the stable graph-event anchor.")
|
||||
private IrisGeneratorStyle style = new IrisGeneratorStyle(NoiseStyle.FLAT);
|
||||
|
||||
@MinNumber(0)
|
||||
@MaxNumber(1)
|
||||
@Desc("The maximum centered noise contribution added to the base probability.")
|
||||
private double influence = 0D;
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
package art.arcane.iris.engine.object;
|
||||
|
||||
import art.arcane.iris.engine.object.annotations.ArrayType;
|
||||
import art.arcane.iris.engine.object.annotations.Desc;
|
||||
import art.arcane.iris.engine.object.annotations.MaxNumber;
|
||||
import art.arcane.iris.engine.object.annotations.MinNumber;
|
||||
import art.arcane.iris.engine.object.annotations.RegistryListResource;
|
||||
import art.arcane.volmlib.util.collection.KList;
|
||||
import art.arcane.volmlib.util.collection.KSet;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.experimental.Accessors;
|
||||
|
||||
@Accessors(chain = true)
|
||||
@NoArgsConstructor
|
||||
@Desc("Nullable river settings overridden by a region or natural biome without changing graph identity.")
|
||||
@Data
|
||||
public class IrisRiverOverride {
|
||||
@Desc("Whether new river sources may begin in this area. Existing trunks are unaffected.")
|
||||
private Boolean allowSources = null;
|
||||
|
||||
@Desc("How downstream routing treats this area.")
|
||||
private IrisRiverRoutingPolicy routingPolicy = null;
|
||||
|
||||
@MinNumber(0)
|
||||
@MaxNumber(64)
|
||||
@Desc("Multiplier applied to downstream routing cost.")
|
||||
private Double routingCostMultiplier = null;
|
||||
|
||||
@MinNumber(0.0001)
|
||||
@MaxNumber(16)
|
||||
@Desc("Multiplier applied to wet channel width.")
|
||||
private Double widthMultiplier = null;
|
||||
|
||||
@MinNumber(0)
|
||||
@MaxNumber(16)
|
||||
@Desc("Multiplier applied to river bank width.")
|
||||
private Double bankWidthMultiplier = null;
|
||||
|
||||
@MinNumber(0.0001)
|
||||
@MaxNumber(16)
|
||||
@Desc("Multiplier applied to river-bed depth.")
|
||||
private Double depthMultiplier = null;
|
||||
|
||||
@MinNumber(0)
|
||||
@MaxNumber(16)
|
||||
@Desc("Multiplier applied to maximum terrain incision.")
|
||||
private Double maxIncisionMultiplier = null;
|
||||
|
||||
@MinNumber(0)
|
||||
@MaxNumber(16)
|
||||
@Desc("Multiplier applied to reach continuation probability.")
|
||||
private Double continuationChanceMultiplier = null;
|
||||
|
||||
@MinNumber(0)
|
||||
@MaxNumber(16)
|
||||
@Desc("Multiplier applied to cave-entry probability.")
|
||||
private Double caveEntryMultiplier = null;
|
||||
|
||||
@Desc("Optional terminal behavior override for failed routes in this area.")
|
||||
private IrisRiverTerminalMode terminalMode = null;
|
||||
|
||||
@RegistryListResource(IrisBiome.class)
|
||||
@ArrayType(type = String.class)
|
||||
@Desc("Optional replacement biome pool for wet river channels. Empty explicitly disables this pool.")
|
||||
private KList<String> channelBiomes = null;
|
||||
|
||||
@RegistryListResource(IrisBiome.class)
|
||||
@ArrayType(type = String.class)
|
||||
@Desc("Optional replacement biome pool for river banks. Empty explicitly disables this pool.")
|
||||
private KList<String> bankBiomes = null;
|
||||
|
||||
@RegistryListResource(IrisBiome.class)
|
||||
@ArrayType(type = String.class)
|
||||
@Desc("Optional replacement biome pool for river mouths. Empty explicitly disables this pool.")
|
||||
private KList<String> mouthBiomes = null;
|
||||
|
||||
@RegistryListResource(IrisBiome.class)
|
||||
@ArrayType(type = String.class)
|
||||
@Desc("Optional replacement biome pool for dry river channels. Empty explicitly disables this pool.")
|
||||
private KList<String> dryBiomes = null;
|
||||
|
||||
@RegistryListResource(IrisBiome.class)
|
||||
@ArrayType(type = String.class)
|
||||
@Desc("Optional replacement cave biome pool for accepted contained river cave bodies. Empty explicitly disables this pool.")
|
||||
private KList<String> floodedCaveBiomes = null;
|
||||
|
||||
public KSet<String> getAllBiomeIds() {
|
||||
KSet<String> biomeIds = new KSet<>();
|
||||
addAll(biomeIds, channelBiomes);
|
||||
addAll(biomeIds, bankBiomes);
|
||||
addAll(biomeIds, mouthBiomes);
|
||||
addAll(biomeIds, dryBiomes);
|
||||
addAll(biomeIds, floodedCaveBiomes);
|
||||
return biomeIds;
|
||||
}
|
||||
|
||||
private static void addAll(KSet<String> destination, KList<String> source) {
|
||||
if (source != null) {
|
||||
destination.addAll(source);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package art.arcane.iris.engine.object;
|
||||
|
||||
import art.arcane.iris.engine.object.annotations.Desc;
|
||||
|
||||
@Desc("Controls how river routing treats a region or biome.")
|
||||
public enum IrisRiverRoutingPolicy {
|
||||
@Desc("Allow normal river routing through this area.")
|
||||
ALLOW,
|
||||
|
||||
@Desc("Increase the routing cost while still permitting established river trunks.")
|
||||
AVOID,
|
||||
|
||||
@Desc("Forbid river reaches from crossing this area.")
|
||||
BLOCK
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package art.arcane.iris.engine.object;
|
||||
|
||||
import art.arcane.iris.engine.object.annotations.Desc;
|
||||
|
||||
@Desc("Selects how a river route ends when it cannot continue to an outlet.")
|
||||
public enum IrisRiverTerminalMode {
|
||||
@Desc("Suppress the failed route instead of generating it.")
|
||||
SUPPRESS,
|
||||
|
||||
@Desc("Continue as a dry channel that tapers back into natural terrain.")
|
||||
DRY_CHANNEL,
|
||||
|
||||
@Desc("End in a contained underground grotto when cave hydrology accepts the connection.")
|
||||
SINKHOLE_GROTTO
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
package art.arcane.iris.engine.object;
|
||||
|
||||
import art.arcane.iris.engine.object.annotations.Desc;
|
||||
import art.arcane.iris.engine.object.annotations.MaxNumber;
|
||||
import art.arcane.iris.engine.object.annotations.MinNumber;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.experimental.Accessors;
|
||||
|
||||
@Accessors(chain = true)
|
||||
@NoArgsConstructor
|
||||
@Desc("Controls river channel geometry, banks, incision, meanders, and terminal tapering.")
|
||||
@Data
|
||||
public class IrisRiverTerrain {
|
||||
@Desc("The wet channel width in blocks before stream-order scaling.")
|
||||
private IrisStyledRange channelWidth = range(8D, 20D, NoiseStyle.IRIS, 1024D);
|
||||
|
||||
@Desc("The bank width outside the wet channel in blocks.")
|
||||
private IrisStyledRange bankWidth = range(5D, 18D, NoiseStyle.IRIS, 1024D);
|
||||
|
||||
@Desc("The wet-bed depth below the local water surface, or dry-channel depth below natural terrain, in blocks.")
|
||||
private IrisStyledRange depth = range(2D, 7D, NoiseStyle.IRIS, 768D);
|
||||
|
||||
@MinNumber(1)
|
||||
@MaxNumber(2048)
|
||||
@Desc("The final wet-channel width cap after region, biome, and stream-order scaling.")
|
||||
private double maxChannelWidth = 10D;
|
||||
|
||||
@MinNumber(0)
|
||||
@MaxNumber(2048)
|
||||
@Desc("The final bank-width cap on each side after region and biome scaling.")
|
||||
private double maxBankWidth = 4D;
|
||||
|
||||
@MinNumber(1)
|
||||
@MaxNumber(512)
|
||||
@Desc("The final river-depth cap after region, biome, and stream-order scaling.")
|
||||
private double maxDepth = 10D;
|
||||
|
||||
@MinNumber(0)
|
||||
@MaxNumber(8)
|
||||
@Desc("Additional channel-width fraction applied for each merged upstream flow order.")
|
||||
private double orderWidthFactor = 0.35D;
|
||||
|
||||
@MinNumber(0)
|
||||
@MaxNumber(8)
|
||||
@Desc("Additional river-bed depth fraction applied for each merged upstream flow order.")
|
||||
private double orderDepthFactor = 0.2D;
|
||||
|
||||
@Desc("Selects whether a complete graph reach may incise terrain. A rejected reach follows terminal behavior.")
|
||||
private IrisRiverNoiseChance incision = new IrisRiverNoiseChance();
|
||||
|
||||
@MinNumber(0)
|
||||
@MaxNumber(512)
|
||||
@Desc("The greatest permitted vertical incision below natural terrain.")
|
||||
private int maxIncision = 48;
|
||||
|
||||
@MinNumber(0.125)
|
||||
@MaxNumber(16)
|
||||
@Desc("The exponent shaping the channel-to-bank cross-section transition.")
|
||||
private double bankExponent = 2D;
|
||||
|
||||
@Desc("Modulates perpendicular spline displacement while preserving graph endpoints.")
|
||||
private IrisGeneratorStyle meanderStyle = new IrisGeneratorStyle(NoiseStyle.IRIS).zoomed(512D);
|
||||
|
||||
@MinNumber(0)
|
||||
@MaxNumber(1024)
|
||||
@Desc("The maximum perpendicular meander displacement in blocks.")
|
||||
private double meanderStrength = 72D;
|
||||
|
||||
@MinNumber(1)
|
||||
@MaxNumber(64)
|
||||
@Desc("The number of straight segments used to flatten each meandering graph reach.")
|
||||
private int meanderSubdivisions = 8;
|
||||
|
||||
@Desc("Modulates small river-bed height variation after the connected channel shape is solved.")
|
||||
private IrisGeneratorStyle bedRoughnessStyle = new IrisGeneratorStyle(NoiseStyle.IRIS).zoomed(96D);
|
||||
|
||||
@MinNumber(0)
|
||||
@MaxNumber(8)
|
||||
@Desc("The maximum river-bed roughness in blocks.")
|
||||
private double bedRoughness = 0.75D;
|
||||
|
||||
@Desc("The behavior used when a graph route cannot continue as a wet channel.")
|
||||
private IrisRiverTerminalMode terminalMode = IrisRiverTerminalMode.DRY_CHANNEL;
|
||||
|
||||
@MinNumber(8)
|
||||
@MaxNumber(1024)
|
||||
@Desc("The distance in blocks over which a terminal channel returns to natural terrain.")
|
||||
private int terminalTaper = 64;
|
||||
|
||||
@MinNumber(0)
|
||||
@MaxNumber(1)
|
||||
@Desc("The probability that a failed wet route continues as a tapered dry channel.")
|
||||
private double dryContinuationChance = 1D;
|
||||
|
||||
private static IrisStyledRange range(double min, double max, NoiseStyle style, double zoom) {
|
||||
return new IrisStyledRange(min, max, new IrisGeneratorStyle(style).zoomed(zoom));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
package art.arcane.iris.engine.object;
|
||||
|
||||
import art.arcane.iris.engine.object.annotations.Desc;
|
||||
import art.arcane.iris.engine.object.annotations.MaxNumber;
|
||||
import art.arcane.iris.engine.object.annotations.MinNumber;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.experimental.Accessors;
|
||||
|
||||
@Accessors(chain = true)
|
||||
@NoArgsConstructor
|
||||
@Desc("Dimension-owned settings for the deterministic connected river graph.")
|
||||
@Data
|
||||
public class IrisRiverTopology {
|
||||
@MinNumber(64)
|
||||
@MaxNumber(4096)
|
||||
@Desc("The routing-cell width in blocks. This controls graph identity and cannot be overridden by regions or biomes.")
|
||||
private int cellSize = 512;
|
||||
|
||||
@MinNumber(1)
|
||||
@MaxNumber(64)
|
||||
@Desc("The number of routing cells grouped into one immutable river cache tile.")
|
||||
private int tileCells = 4;
|
||||
|
||||
@MinNumber(0)
|
||||
@MaxNumber(0.49)
|
||||
@Desc("The fraction of a routing cell used to jitter its graph node away from the center.")
|
||||
private double siteJitter = 0.35D;
|
||||
|
||||
@MinNumber(1)
|
||||
@MaxNumber(256)
|
||||
@Desc("The maximum number of directed graph reaches followed by one source route.")
|
||||
private int maxRouteReaches = 16;
|
||||
|
||||
@MinNumber(0)
|
||||
@MaxNumber(64)
|
||||
@Desc("The minimum number of noise-weighted source nodes selected in each routing tile while source chance is above zero.")
|
||||
private int minimumSourcesPerTile = 0;
|
||||
|
||||
@MinNumber(0)
|
||||
@MaxNumber(7)
|
||||
@Desc("The number of alternate downstream reaches inspected before declaring a sink.")
|
||||
private int sinkSearchReaches = 4;
|
||||
|
||||
@MinNumber(8)
|
||||
@MaxNumber(256)
|
||||
@Desc("The spacing of deterministic drainage-basin sinks in routing cells. Larger values produce longer trunks and wider tributary trees.")
|
||||
private int routingBasinCells = 64;
|
||||
|
||||
@MinNumber(1)
|
||||
@MaxNumber(64)
|
||||
@Desc("The horizontal basin-distance span in routing cells per one block of terraced water rise.")
|
||||
private double routingPlateauHeight = 8D;
|
||||
|
||||
@Desc("Selects complete river source routes at stable graph nodes.")
|
||||
private IrisRiverNoiseChance source = new IrisRiverNoiseChance()
|
||||
.setChance(0.05D)
|
||||
.setStyle(new IrisGeneratorStyle(NoiseStyle.IRIS).zoomed(8192D))
|
||||
.setInfluence(0.035D);
|
||||
|
||||
@Desc("Selects complete continuation reaches. A rejected reach terminates or suppresses its route rather than creating a gap.")
|
||||
private IrisRiverNoiseChance continuation = new IrisRiverNoiseChance()
|
||||
.setChance(0.99D)
|
||||
.setStyle(new IrisGeneratorStyle(NoiseStyle.VASCULAR).zoomed(4096D))
|
||||
.setInfluence(0.01D);
|
||||
|
||||
@Desc("Adds deterministic cost variation while choosing downstream graph neighbors.")
|
||||
private IrisGeneratorStyle routingStyle = new IrisGeneratorStyle(NoiseStyle.VASCULAR).zoomed(8192D);
|
||||
|
||||
@MinNumber(0)
|
||||
@MaxNumber(1024)
|
||||
@Desc("The maximum routing-cost contribution from routingStyle.")
|
||||
private double routingNoiseWeight = 24D;
|
||||
|
||||
@MinNumber(0)
|
||||
@MaxNumber(16)
|
||||
@Desc("The contribution of natural terrain height to downstream routing cost.")
|
||||
private double terrainHeightWeight = 0.7D;
|
||||
|
||||
@MinNumber(0)
|
||||
@MaxNumber(16)
|
||||
@Desc("The contribution of natural terrain slope to downstream routing cost.")
|
||||
private double terrainSlopeWeight = 0.35D;
|
||||
|
||||
@MinNumber(0)
|
||||
@MaxNumber(16)
|
||||
@Desc("The routing preference toward natural sea outlets.")
|
||||
private double oceanAttraction = 1D;
|
||||
|
||||
@Desc("Require every wet source route to reach natural sea or a proven sea-reaching trunk.")
|
||||
private boolean requireOcean = false;
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
package art.arcane.iris.engine.object;
|
||||
|
||||
import art.arcane.iris.engine.object.annotations.Desc;
|
||||
import art.arcane.iris.engine.object.annotations.MaxNumber;
|
||||
import art.arcane.iris.engine.object.annotations.MinNumber;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.experimental.Accessors;
|
||||
|
||||
@Accessors(chain = true)
|
||||
@NoArgsConstructor
|
||||
@Desc("Controls the river water-surface solver.")
|
||||
@Data
|
||||
public class IrisRiverWater {
|
||||
@Desc("The strategy used to determine river water-surface height.")
|
||||
private IrisRiverWaterMode mode = IrisRiverWaterMode.SEA_LEVEL;
|
||||
|
||||
@MinNumber(8)
|
||||
@MaxNumber(4096)
|
||||
@Desc("The target length of each flat terraced pool in blocks.")
|
||||
private int poolLength = 96;
|
||||
|
||||
@MinNumber(0)
|
||||
@MaxNumber(64)
|
||||
@Desc("The greatest river water height permitted above the dimension fluid height.")
|
||||
private int maximumPoolRise = 4;
|
||||
|
||||
@MinNumber(1)
|
||||
@MaxNumber(32)
|
||||
@Desc("The vertical height of controlled drops between terraced pools.")
|
||||
private int dropHeight = 1;
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package art.arcane.iris.engine.object;
|
||||
|
||||
import art.arcane.iris.engine.object.annotations.Desc;
|
||||
|
||||
@Desc("Selects how a river determines its surface fluid height.")
|
||||
public enum IrisRiverWaterMode {
|
||||
@Desc("Use the dimension fluid height for every wet river reach.")
|
||||
SEA_LEVEL,
|
||||
|
||||
@Desc("Use flat pools connected by controlled vertical drops.")
|
||||
TERRACED
|
||||
}
|
||||
@@ -32,6 +32,7 @@ import art.arcane.iris.core.IrisWorlds;
|
||||
import art.arcane.iris.core.gui.PregeneratorJob;
|
||||
import art.arcane.iris.core.loader.IrisData;
|
||||
import art.arcane.iris.core.nms.INMS;
|
||||
import art.arcane.iris.core.runtime.ObjectStudioActivation;
|
||||
import art.arcane.iris.core.runtime.jigsaw.JigsawStudioActivation;
|
||||
import art.arcane.iris.core.runtime.jigsaw.JigsawStudioSession;
|
||||
import art.arcane.iris.core.service.StudioSVC;
|
||||
@@ -61,6 +62,7 @@ import art.arcane.volmlib.util.collection.KList;
|
||||
import art.arcane.volmlib.util.math.M;
|
||||
import art.arcane.iris.util.project.hunk.Hunk;
|
||||
import art.arcane.iris.util.project.hunk.view.ChunkDataHunkHolder;
|
||||
import art.arcane.iris.util.project.context.ChunkContext;
|
||||
import art.arcane.iris.util.project.context.IrisContext;
|
||||
import art.arcane.volmlib.util.io.ReactiveFolder;
|
||||
import art.arcane.volmlib.util.scheduling.ChronoLatch;
|
||||
@@ -83,12 +85,17 @@ import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.PrintWriter;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Objects;
|
||||
import java.util.Random;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.CompletionException;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.TimeoutException;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
@@ -102,6 +109,11 @@ import java.util.function.Supplier;
|
||||
@Data
|
||||
public class BukkitChunkGenerator extends ChunkGenerator implements PlatformChunkGenerator, Listener {
|
||||
private static final int LOAD_LOCKS = Runtime.getRuntime().availableProcessors() * 4;
|
||||
private static final int STUDIO_ENTRY_PRECOMPUTE_RADIUS = 2;
|
||||
private static final int STUDIO_ENTRY_PRECOMPUTE_THREADS = Math.max(
|
||||
1,
|
||||
Math.min(6, Runtime.getRuntime().availableProcessors() / 2));
|
||||
private static final AtomicInteger STUDIO_ENTRY_THREAD_SEQUENCE = new AtomicInteger();
|
||||
private static final long HOTLOAD_LOOP_DELAY_MS = 250L;
|
||||
private static final long HOTLOAD_MAINTENANCE_DELAY_MS = 4000L;
|
||||
private final GenerationStageGate loadLock;
|
||||
@@ -115,6 +127,7 @@ public class BukkitChunkGenerator extends ChunkGenerator implements PlatformChun
|
||||
private final AtomicBoolean setup;
|
||||
private final boolean studio;
|
||||
private final AtomicBoolean studioEntryBootstrapActive;
|
||||
private final ConcurrentHashMap<Long, PreparedStudioChunk> preparedStudioEntryChunks;
|
||||
private final AtomicInteger a = new AtomicInteger(0);
|
||||
private volatile long lastChunkGenTime = 0L;
|
||||
private final CompletableFuture<Integer> spawnChunks = new CompletableFuture<>();
|
||||
@@ -143,6 +156,7 @@ public class BukkitChunkGenerator extends ChunkGenerator implements PlatformChun
|
||||
this.hotloadChecker = new ChronoLatch(1000, false);
|
||||
this.studio = studio;
|
||||
this.studioEntryBootstrapActive = new AtomicBoolean(studio);
|
||||
this.preparedStudioEntryChunks = new ConcurrentHashMap<>();
|
||||
this.dataLocation = dataLocation;
|
||||
this.dimensionKey = dimensionKey;
|
||||
this.folder = new ReactiveFolder(
|
||||
@@ -475,6 +489,7 @@ public class BukkitChunkGenerator extends ChunkGenerator implements PlatformChun
|
||||
if (currentEngine != null && !currentEngine.isClosed()) {
|
||||
currentEngine.close();
|
||||
}
|
||||
preparedStudioEntryChunks.clear();
|
||||
folder.clear();
|
||||
populators.clear();
|
||||
});
|
||||
@@ -525,6 +540,124 @@ public class BukkitChunkGenerator extends ChunkGenerator implements PlatformChun
|
||||
studioEntryBootstrapActive.set(false);
|
||||
}
|
||||
|
||||
public CompletableFuture<Void> prepareStudioEntryChunks(
|
||||
World bukkitWorld,
|
||||
int centerChunkX,
|
||||
int centerChunkZ
|
||||
) {
|
||||
if (!studio || closing) {
|
||||
return CompletableFuture.completedFuture(null);
|
||||
}
|
||||
Engine activeEngine = getEngine(bukkitWorld);
|
||||
computeStudioGenerator();
|
||||
if (studioGenerator != null) {
|
||||
return CompletableFuture.completedFuture(null);
|
||||
}
|
||||
|
||||
long generationSessionId = activeEngine.getGenerationSessionId();
|
||||
ConcurrentHashMap<Long, PreparedStudioChunk> prepared = new ConcurrentHashMap<>();
|
||||
ExecutorService executor = createStudioEntryExecutor();
|
||||
int diameter = STUDIO_ENTRY_PRECOMPUTE_RADIUS * 2 + 1;
|
||||
ArrayList<CompletableFuture<Void>> tasks = new ArrayList<>(diameter * diameter);
|
||||
for (int offsetX = -STUDIO_ENTRY_PRECOMPUTE_RADIUS;
|
||||
offsetX <= STUDIO_ENTRY_PRECOMPUTE_RADIUS;
|
||||
offsetX++) {
|
||||
for (int offsetZ = -STUDIO_ENTRY_PRECOMPUTE_RADIUS;
|
||||
offsetZ <= STUDIO_ENTRY_PRECOMPUTE_RADIUS;
|
||||
offsetZ++) {
|
||||
int chunkX = centerChunkX + offsetX;
|
||||
int chunkZ = centerChunkZ + offsetZ;
|
||||
CompletableFuture<Void> task = CompletableFuture.runAsync(
|
||||
() -> prepareStudioEntryChunk(
|
||||
bukkitWorld,
|
||||
activeEngine,
|
||||
generationSessionId,
|
||||
chunkX,
|
||||
chunkZ,
|
||||
prepared),
|
||||
executor);
|
||||
tasks.add(task);
|
||||
}
|
||||
}
|
||||
|
||||
CompletableFuture<Void> completion = CompletableFuture.allOf(
|
||||
tasks.toArray(new CompletableFuture<?>[0]));
|
||||
CompletableFuture<Void> publication = completion.thenRun(() -> {
|
||||
if (closing
|
||||
|| engine != activeEngine
|
||||
|| activeEngine.getGenerationSessionId() != generationSessionId) {
|
||||
throw new IllegalStateException(
|
||||
"Studio entry precompute finished for a replaced engine runtime.");
|
||||
}
|
||||
preparedStudioEntryChunks.clear();
|
||||
preparedStudioEntryChunks.putAll(prepared);
|
||||
});
|
||||
return publication.whenComplete((ignored, failure) -> executor.shutdownNow());
|
||||
}
|
||||
|
||||
private ExecutorService createStudioEntryExecutor() {
|
||||
return Executors.newFixedThreadPool(STUDIO_ENTRY_PRECOMPUTE_THREADS, runnable -> {
|
||||
Thread thread = new Thread(
|
||||
runnable,
|
||||
"Iris Studio Entry-" + STUDIO_ENTRY_THREAD_SEQUENCE.incrementAndGet());
|
||||
thread.setDaemon(true);
|
||||
thread.setPriority(Thread.NORM_PRIORITY);
|
||||
return thread;
|
||||
});
|
||||
}
|
||||
|
||||
private void prepareStudioEntryChunk(
|
||||
World bukkitWorld,
|
||||
Engine activeEngine,
|
||||
long generationSessionId,
|
||||
int chunkX,
|
||||
int chunkZ,
|
||||
ConcurrentHashMap<Long, PreparedStudioChunk> prepared
|
||||
) {
|
||||
try (GenerationStagePermit ignored = acquireGenerationStage(
|
||||
"studio_entry_chunk_precompute")) {
|
||||
TerrainChunk terrainChunk = TerrainChunk.create(bukkitWorld);
|
||||
ChunkDataHunkHolder blocks = new ChunkDataHunkHolder(terrainChunk.getChunkData());
|
||||
Hunk<PlatformBiome> biomes = Hunk.viewBiomes(terrainChunk);
|
||||
ChunkContext context = createStudioEntryContext(
|
||||
activeEngine,
|
||||
generationSessionId,
|
||||
chunkX,
|
||||
chunkZ);
|
||||
activeEngine.generateMatter(chunkX, chunkZ, true, context);
|
||||
try {
|
||||
activeEngine.generate(chunkX << 4, chunkZ << 4, blocks, biomes, false);
|
||||
} catch (WrongEngineBroException exception) {
|
||||
throw new CompletionException(exception);
|
||||
}
|
||||
prepared.put(
|
||||
chunkKey(chunkX, chunkZ),
|
||||
new PreparedStudioChunk(activeEngine, generationSessionId, blocks)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private ChunkContext createStudioEntryContext(
|
||||
Engine activeEngine,
|
||||
long generationSessionId,
|
||||
int chunkX,
|
||||
int chunkZ
|
||||
) {
|
||||
boolean cacheContext = !activeEngine.getPlatformHooks()
|
||||
.shouldDisableChunkContextCache(activeEngine);
|
||||
ChunkContext.PrefillPlan prefillPlan = cacheContext
|
||||
? ChunkContext.PrefillPlan.NO_CAVE
|
||||
: ChunkContext.PrefillPlan.NONE;
|
||||
return new ChunkContext(
|
||||
chunkX << 4,
|
||||
chunkZ << 4,
|
||||
activeEngine.getComplex(),
|
||||
generationSessionId,
|
||||
cacheContext,
|
||||
prefillPlan,
|
||||
activeEngine.getMetrics());
|
||||
}
|
||||
|
||||
public boolean isStudioEntryBootstrapActive() {
|
||||
return studioEntryBootstrapActive.get();
|
||||
}
|
||||
@@ -783,12 +916,19 @@ public class BukkitChunkGenerator extends ChunkGenerator implements PlatformChun
|
||||
if (studioGenerator != null) {
|
||||
studioGenerator.generateChunk(engine, tc, x, z);
|
||||
} else {
|
||||
PreparedStudioChunk prepared = preparedStudioEntryChunks.remove(chunkKey(x, z));
|
||||
if (prepared != null
|
||||
&& prepared.engine() == engine
|
||||
&& prepared.generationSessionId() == engine.getGenerationSessionId()) {
|
||||
prepared.blocks().applyTo(d);
|
||||
IrisLogging.debug("Applied prepared Studio entry chunk " + x + " " + z);
|
||||
return;
|
||||
}
|
||||
ChunkDataHunkHolder blocks = new ChunkDataHunkHolder(d);
|
||||
Hunk<PlatformBiome> biomes = Hunk.viewBiomes(tc);
|
||||
boolean useMulticore = studio && !J.isFolia();
|
||||
try (GenerationSessionLease lease = engine.acquireGenerationLease("bukkit_terrain_stage");
|
||||
IrisContext.Scope ignored = IrisContext.open(engine, lease.sessionId(), null)) {
|
||||
engine.generate(x << 4, z << 4, blocks, biomes, useMulticore);
|
||||
engine.generate(x << 4, z << 4, blocks, biomes, false);
|
||||
blocks.apply();
|
||||
}
|
||||
}
|
||||
@@ -829,6 +969,17 @@ public class BukkitChunkGenerator extends ChunkGenerator implements PlatformChun
|
||||
return isMaintenanceActive();
|
||||
}
|
||||
|
||||
private static long chunkKey(int chunkX, int chunkZ) {
|
||||
return ((long) chunkX << 32) ^ (chunkZ & 0xFFFFFFFFL);
|
||||
}
|
||||
|
||||
private record PreparedStudioChunk(
|
||||
Engine engine,
|
||||
long generationSessionId,
|
||||
ChunkDataHunkHolder blocks
|
||||
) {
|
||||
}
|
||||
|
||||
private boolean isMaintenanceActive() {
|
||||
World realWorld = BukkitWorldBinding.world(this.world);
|
||||
return realWorld != null && IrisToolbelt.isWorldMaintenanceActive(realWorld);
|
||||
@@ -910,7 +1061,7 @@ public class BukkitChunkGenerator extends ChunkGenerator implements PlatformChun
|
||||
StudioMode desired = studio
|
||||
? java.util.Optional.ofNullable(getEngine().getDimension().getStudioMode()).orElse(StudioMode.NORMAL)
|
||||
: StudioMode.NORMAL;
|
||||
if (studio && art.arcane.iris.core.runtime.ObjectStudioActivation.isActive(getEngine().getDimension().getLoadKey())) {
|
||||
if (studio && ObjectStudioActivation.isActive(getEngine().getDimension().getLoadKey())) {
|
||||
desired = StudioMode.OBJECT_BUFFET;
|
||||
}
|
||||
if (!desired.equals(lastMode)) {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user