This commit is contained in:
Brian Neumann-Fopiano
2026-07-21 12:00:17 -04:00
parent d76dadec30
commit f6f06c1ab9
141 changed files with 10717 additions and 1725 deletions
@@ -5,9 +5,12 @@ import com.mojang.serialization.MapCodec;
import art.arcane.iris.spi.IrisLogging;
import art.arcane.iris.engine.data.cache.AtomicCache;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.framework.GenerationSessionException;
import art.arcane.iris.engine.framework.GenerationSessionLease;
import art.arcane.iris.engine.object.IrisBiome;
import art.arcane.iris.engine.object.IrisBiomeCustom;
import art.arcane.iris.engine.object.IrisDimension;
import art.arcane.iris.util.project.context.IrisContext;
import art.arcane.volmlib.util.collection.KMap;
import art.arcane.volmlib.util.math.RNG;
import net.minecraft.core.Holder;
@@ -148,17 +151,26 @@ public class CustomBiomeSource extends BiomeSource {
}
Set<Holder<Biome>> possibleStructureBiomes() {
ensureCachesCurrent();
World world = BukkitWorldBinding.world(engine.getWorld());
if (world == null) {
throw new IllegalStateException("Iris biome source has no bound Bukkit world");
GenerationSessionLease lease = tryAcquireGenerationLease("bukkit_possible_biomes");
if (lease == null) {
throw new IllegalStateException("Iris possible 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 possible biome lookup has no active engine runtime");
}
ensureCachesCurrent();
World world = BukkitWorldBinding.world(engine.getWorld());
if (world == null) {
throw new IllegalStateException("Iris biome source has no bound Bukkit world");
}
Registry<Biome> customRegistry = ((RegistryAccess) getFor(
RegistryAccess.Frozen.class, ((CraftServer) Bukkit.getServer()).getHandle().getServer()))
.lookup(Registries.BIOME).orElse(null);
Registry<Biome> worldRegistry = ((CraftWorld) world).getHandle().registryAccess()
.lookup(Registries.BIOME).orElse(null);
return Set.copyOf(getAllBiomes(customRegistry, worldRegistry, engine));
}
Registry<Biome> customRegistry = ((RegistryAccess) getFor(
RegistryAccess.Frozen.class, ((CraftServer) Bukkit.getServer()).getHandle().getServer()))
.lookup(Registries.BIOME).orElse(null);
Registry<Biome> worldRegistry = ((CraftWorld) world).getHandle().registryAccess()
.lookup(Registries.BIOME).orElse(null);
return Set.copyOf(getAllBiomes(customRegistry, worldRegistry, engine));
}
private KMap<String, Holder<Biome>> fillCustomBiomes(Registry<Biome> customRegistry, Engine engine, Holder<Biome> fallback) {
@@ -212,11 +224,20 @@ public class CustomBiomeSource extends BiomeSource {
}
Holder<Biome> getVanillaSpawnBiome(Holder<Biome> biome) {
ensureCachesCurrent();
if (biome == null) {
return null;
}
return vanillaSpawnBiomes.get(biome.value());
GenerationSessionLease lease = tryAcquireGenerationLease("bukkit_spawn_biome");
if (lease == null) {
throw new IllegalStateException("Iris spawn 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 spawn biome lookup has no active engine runtime");
}
ensureCachesCurrent();
return vanillaSpawnBiomes.get(biome.value());
}
}
private RegistryAccess registry() {
@@ -230,51 +251,69 @@ public class CustomBiomeSource extends BiomeSource {
@Override
public Holder<Biome> getNoiseBiome(int x, int y, int z, Climate.Sampler sampler) {
ensureCachesCurrent();
if (isGuaranteedSurfaceBiome(y)) {
return getSurfaceStructureBiomeHolder(x, z);
GenerationSessionLease lease = tryAcquireGenerationLease("bukkit_structure_biome");
if (lease == null) {
throw new IllegalStateException("Iris structure 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 biome lookup has no active engine runtime");
}
ensureCachesCurrent();
if (isGuaranteedSurfaceBiome(y)) {
return getSurfaceStructureBiomeHolder(x, z);
}
long cacheKey = packNoiseKey(x, y, z);
Holder<Biome> cachedHolder = structureBiomeCache.get(cacheKey);
if (cachedHolder != null) {
return cachedHolder;
long cacheKey = packNoiseKey(x, y, z);
Holder<Biome> cachedHolder = structureBiomeCache.get(cacheKey);
if (cachedHolder != null) {
return cachedHolder;
}
Holder<Biome> resolvedHolder = resolveStructureBiomeHolder(x, y, z);
Holder<Biome> existingHolder = structureBiomeCache.putIfAbsent(cacheKey, resolvedHolder);
if (existingHolder != null) {
return existingHolder;
}
if (structureBiomeCache.size() > NOISE_BIOME_CACHE_MAX) {
structureBiomeCache.clear();
}
return resolvedHolder;
}
Holder<Biome> resolvedHolder = resolveStructureBiomeHolder(x, y, z);
Holder<Biome> existingHolder = structureBiomeCache.putIfAbsent(cacheKey, resolvedHolder);
if (existingHolder != null) {
return existingHolder;
}
if (structureBiomeCache.size() > NOISE_BIOME_CACHE_MAX) {
structureBiomeCache.clear();
}
return resolvedHolder;
}
@Override
public Set<Holder<Biome>> getBiomesWithin(int x, int y, int z, int radius, Climate.Sampler sampler) {
ensureCachesCurrent();
int minQuartY = QuartPos.fromBlock(y - radius);
boolean monumentQuery = radius == 29
&& y == engine.getMinHeight() + engine.getDimension().getFluidHeight();
if (!monumentQuery && !isGuaranteedSurfaceBiome(minQuartY)) {
return super.getBiomesWithin(x, y, z, radius, sampler);
GenerationSessionLease lease = tryAcquireGenerationLease("bukkit_biomes_within");
if (lease == null) {
throw new IllegalStateException("Iris biome radius lookup was rejected during an engine transition");
}
int minQuartX = QuartPos.fromBlock(x - radius);
int maxQuartX = QuartPos.fromBlock(x + radius);
int minQuartZ = QuartPos.fromBlock(z - radius);
int maxQuartZ = QuartPos.fromBlock(z + radius);
int columns = (maxQuartX - minQuartX + 1) * (maxQuartZ - minQuartZ + 1);
Set<Holder<Biome>> biomes = new HashSet<>(columns);
for (int quartZ = minQuartZ; quartZ <= maxQuartZ; quartZ++) {
for (int quartX = minQuartX; quartX <= maxQuartX; quartX++) {
biomes.add(getSurfaceStructureBiomeHolder(quartX, quartZ));
try (lease; IrisContext.Scope ignored = IrisContext.open(engine, lease.sessionId(), null)) {
if (!isRuntimeAvailable()) {
throw new IllegalStateException("Iris biome radius lookup has no active engine runtime");
}
ensureCachesCurrent();
int minQuartY = QuartPos.fromBlock(y - radius);
boolean monumentQuery = radius == 29
&& y == engine.getMinHeight() + engine.getDimension().getFluidHeight();
if (!monumentQuery && !isGuaranteedSurfaceBiome(minQuartY)) {
return super.getBiomesWithin(x, y, z, radius, sampler);
}
int minQuartX = QuartPos.fromBlock(x - radius);
int maxQuartX = QuartPos.fromBlock(x + radius);
int minQuartZ = QuartPos.fromBlock(z - radius);
int maxQuartZ = QuartPos.fromBlock(z + radius);
int columns = (maxQuartX - minQuartX + 1) * (maxQuartZ - minQuartZ + 1);
Set<Holder<Biome>> biomes = new HashSet<>(columns);
for (int quartZ = minQuartZ; quartZ <= maxQuartZ; quartZ++) {
for (int quartX = minQuartX; quartX <= maxQuartX; quartX++) {
biomes.add(getSurfaceStructureBiomeHolder(quartX, quartZ));
}
}
return biomes;
}
return biomes;
}
private Holder<Biome> getSurfaceStructureBiomeHolder(int x, int z) {
@@ -322,24 +361,52 @@ public class CustomBiomeSource extends BiomeSource {
}
public Holder<Biome> getVisibleNoiseBiome(int x, int y, int z, Climate.Sampler sampler) {
ensureCachesCurrent();
long cacheKey = packNoiseKey(x, y, z);
Holder<Biome> cachedHolder = noiseBiomeCache.get(cacheKey);
if (cachedHolder != null) {
return cachedHolder;
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;
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;
}
}
if (noiseBiomeCache.size() > NOISE_BIOME_CACHE_MAX) {
noiseBiomeCache.clear();
private GenerationSessionLease tryAcquireGenerationLease(String operation) {
if (engine.isClosed()) {
return null;
}
try {
return engine.acquireGenerationLease(operation);
} catch (GenerationSessionException e) {
if (engine.isClosing() || e.isExpectedTeardown()) {
return null;
}
throw new IllegalStateException("Iris biome source could not acquire generation session for "
+ operation + ".", e);
}
}
return resolvedHolder;
private boolean isRuntimeAvailable() {
return !engine.isClosed() && engine.getComplex() != null;
}
private void ensureCachesCurrent() {
@@ -1,6 +1,8 @@
package art.arcane.iris.core.nms.v26_2_R1;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.framework.GenerationSessionException;
import art.arcane.iris.engine.framework.GenerationSessionLease;
import art.arcane.iris.engine.framework.IrisStructureLocator;
import art.arcane.iris.engine.framework.NativeStructureGenerationPolicy;
import art.arcane.iris.engine.object.IrisDimension;
@@ -14,6 +16,7 @@ import art.arcane.iris.spi.PlatformBlockState;
import art.arcane.iris.util.common.data.IrisCustomData;
import art.arcane.iris.util.common.reflect.WrappedField;
import art.arcane.iris.util.common.reflect.WrappedReturningMethod;
import art.arcane.iris.util.project.context.IrisContext;
import art.arcane.volmlib.util.math.RNG;
import com.mojang.datafixers.util.Pair;
import com.mojang.serialization.MapCodec;
@@ -90,6 +93,7 @@ public class IrisChunkGenerator extends CustomChunkGenerator {
private final CustomBiomeSource customBiomeSource;
private final int runtimeMinY;
private final int runtimeHeight;
private final int runtimeSeaLevel;
private final ConcurrentHashMap<SpawnTableKey, WeightedList<MobSpawnSettings.SpawnerData>> mergedSpawnTables = new ConcurrentHashMap<>();
private volatile ReachableStructureCache reachableStructureCache;
private volatile StructureStepCache structureStepCache;
@@ -106,17 +110,21 @@ public class IrisChunkGenerator extends CustomChunkGenerator {
ServerLevel level = ((CraftWorld) world).getHandle();
this.runtimeMinY = level.getMinY();
this.runtimeHeight = level.getHeight();
this.runtimeSeaLevel = runtimeMinY + engine.getDimension().getFluidHeight();
}
@Override
public @Nullable Pair<BlockPos, Holder<Structure>> findNearestMapStructure(ServerLevel level, HolderSet<Structure> holders, BlockPos pos, int radius, boolean findUnexplored) {
Pair<BlockPos, Holder<Structure>> irisPlaced = findNearestIrisStructure(
level, holders, pos, Math.max(1, radius), findUnexplored);
HolderSet<Structure> reachable = filterReachableStructures(level, holders);
Pair<BlockPos, Holder<Structure>> nativeLocated = reachable == null || reachable.size() == 0
? null
: delegate.findNearestMapStructure(level, reachable, pos, radius, findUnexplored);
return NativeStructureLocateResults.nearest(pos, irisPlaced, nativeLocated);
try (GenerationSessionLease lease = requireGenerationLease("bukkit_nms_structure_locate");
IrisContext.Scope ignored = IrisContext.open(engine, lease.sessionId(), null)) {
Pair<BlockPos, Holder<Structure>> irisPlaced = findNearestIrisStructure(
level, holders, pos, Math.max(1, radius), findUnexplored);
HolderSet<Structure> reachable = filterReachableStructures(level, holders);
Pair<BlockPos, Holder<Structure>> nativeLocated = reachable == null || reachable.size() == 0
? null
: delegate.findNearestMapStructure(level, reachable, pos, radius, findUnexplored);
return NativeStructureLocateResults.nearest(pos, irisPlaced, nativeLocated);
}
}
private Pair<BlockPos, Holder<Structure>> findNearestIrisStructure(ServerLevel level,
@@ -231,14 +239,17 @@ public class IrisChunkGenerator extends CustomChunkGenerator {
@Override
public int getSeaLevel() {
return runtimeMinY + engine.getDimension().getFluidHeight();
return runtimeSeaLevel;
}
@Override
public void createStructures(RegistryAccess registryAccess, ChunkGeneratorStructureState structureState, StructureManager structureManager, ChunkAccess access, StructureTemplateManager templateManager, ResourceKey<Level> levelKey) {
Map<Structure, StructureStart> previousStarts = new HashMap<>(access.getAllStarts());
super.createStructures(registryAccess, structureState, structureManager, access, templateManager, levelKey);
adjustGeneratedStructures(registryAccess, access, previousStarts);
try (GenerationSessionLease lease = requireGenerationLease("bukkit_nms_create_structures");
IrisContext.Scope ignored = IrisContext.open(engine, lease.sessionId(), null)) {
Map<Structure, StructureStart> previousStarts = new HashMap<>(access.getAllStarts());
super.createStructures(registryAccess, structureState, structureManager, access, templateManager, levelKey);
adjustGeneratedStructures(registryAccess, access, previousStarts);
}
}
private void adjustGeneratedStructures(RegistryAccess registryAccess, ChunkAccess access, Map<Structure, StructureStart> previousStarts) {
@@ -298,8 +309,11 @@ public class IrisChunkGenerator extends CustomChunkGenerator {
@Override
public CompletableFuture<ChunkAccess> createBiomes(RandomState randomstate, Blender blender, StructureManager structuremanager, ChunkAccess ichunkaccess) {
ichunkaccess.fillBiomesFromNoise(customBiomeSource::getVisibleNoiseBiome, randomstate.sampler());
return CompletableFuture.completedFuture(ichunkaccess);
try (GenerationSessionLease lease = requireGenerationLease("bukkit_nms_create_biomes");
IrisContext.Scope ignored = IrisContext.open(engine, lease.sessionId(), null)) {
ichunkaccess.fillBiomesFromNoise(customBiomeSource::getVisibleNoiseBiome, randomstate.sampler());
return CompletableFuture.completedFuture(ichunkaccess);
}
}
@Override
@@ -355,9 +369,12 @@ public class IrisChunkGenerator extends CustomChunkGenerator {
@Override
public void applyBiomeDecoration(WorldGenLevel generatoraccessseed, ChunkAccess ichunkaccess, StructureManager structuremanager, boolean vanilla) {
addVanillaDecorations(generatoraccessseed, ichunkaccess, structuremanager);
placeVanillaStructures(generatoraccessseed, ichunkaccess, structuremanager);
delegate.applyBiomeDecoration(generatoraccessseed, ichunkaccess, structuremanager, false);
try (GenerationSessionLease lease = requireGenerationLease("bukkit_nms_biome_decoration");
IrisContext.Scope ignored = IrisContext.open(engine, lease.sessionId(), null)) {
addVanillaDecorations(generatoraccessseed, ichunkaccess, structuremanager);
placeVanillaStructures(generatoraccessseed, ichunkaccess, structuremanager);
delegate.applyBiomeDecoration(generatoraccessseed, ichunkaccess, structuremanager, false);
}
}
private void placeVanillaStructures(WorldGenLevel world, ChunkAccess chunk, StructureManager structureManager) {
@@ -525,29 +542,35 @@ public class IrisChunkGenerator extends CustomChunkGenerator {
@Override
public void addVanillaDecorations(WorldGenLevel level, ChunkAccess chunkAccess, StructureManager structureManager) {
SectionPos sectionPos = SectionPos.of(chunkAccess.getPos(), level.getMinSectionY());
BlockPos blockPos = sectionPos.origin();
try (GenerationSessionLease lease = engine.acquireGenerationLease("bukkit_nms_heightmaps");
IrisContext.Scope ignored = IrisContext.open(engine, lease.sessionId(), null)) {
SectionPos sectionPos = SectionPos.of(chunkAccess.getPos(), level.getMinSectionY());
BlockPos blockPos = sectionPos.origin();
Heightmap surface = chunkAccess.getOrCreateHeightmapUnprimed(Heightmap.Types.WORLD_SURFACE_WG);
Heightmap ocean = chunkAccess.getOrCreateHeightmapUnprimed(Heightmap.Types.OCEAN_FLOOR_WG);
Heightmap motion = chunkAccess.getOrCreateHeightmapUnprimed(Heightmap.Types.MOTION_BLOCKING);
Heightmap motionNoLeaves = chunkAccess.getOrCreateHeightmapUnprimed(Heightmap.Types.MOTION_BLOCKING_NO_LEAVES);
Heightmap surface = chunkAccess.getOrCreateHeightmapUnprimed(Heightmap.Types.WORLD_SURFACE_WG);
Heightmap ocean = chunkAccess.getOrCreateHeightmapUnprimed(Heightmap.Types.OCEAN_FLOOR_WG);
Heightmap motion = chunkAccess.getOrCreateHeightmapUnprimed(Heightmap.Types.MOTION_BLOCKING);
Heightmap motionNoLeaves = chunkAccess.getOrCreateHeightmapUnprimed(Heightmap.Types.MOTION_BLOCKING_NO_LEAVES);
int minHeight = engine.getMinHeight();
for (int x = 0; x < 16; x++) {
for (int z = 0; z < 16; z++) {
int wX = x + blockPos.getX();
int wZ = z + blockPos.getZ();
for (int x = 0; x < 16; x++) {
for (int z = 0; z < 16; z++) {
int wX = x + blockPos.getX();
int wZ = z + blockPos.getZ();
int terrainTop = engine.getHeight(wX, wZ, false) + engine.getMinHeight() + 1;
int terrainNoFluid = engine.getHeight(wX, wZ, true) + engine.getMinHeight() + 1;
SET_HEIGHT.invoke(ocean, x, z, terrainNoFluid);
SET_HEIGHT.invoke(surface, x, z, terrainTop);
SET_HEIGHT.invoke(motion, x, z, terrainTop);
SET_HEIGHT.invoke(motionNoLeaves, x, z, terrainTop);
int terrainTop = engine.getHeight(wX, wZ, false) + minHeight + 1;
int terrainNoFluid = engine.getHeight(wX, wZ, true) + minHeight + 1;
SET_HEIGHT.invoke(ocean, x, z, terrainNoFluid);
SET_HEIGHT.invoke(surface, x, z, terrainTop);
SET_HEIGHT.invoke(motion, x, z, terrainTop);
SET_HEIGHT.invoke(motionNoLeaves, x, z, terrainTop);
}
}
}
Heightmap.primeHeightmaps(chunkAccess, ChunkStatus.FINAL_HEIGHTMAPS);
Heightmap.primeHeightmaps(chunkAccess, ChunkStatus.FINAL_HEIGHTMAPS);
} catch (GenerationSessionException e) {
throw new IllegalStateException("Iris heightmap generation could not acquire its engine runtime.", e);
}
}
@Override
@@ -585,20 +608,44 @@ public class IrisChunkGenerator extends CustomChunkGenerator {
@Override
public int getBaseHeight(int i, int j, Heightmap.Types heightmap_type, LevelHeightAccessor levelheightaccessor, RandomState randomstate) {
return levelheightaccessor.getMinY() + engine.getHeight(i, j, !heightmap_type.isOpaque().test(Blocks.WATER.defaultBlockState())) + 1;
try (GenerationSessionLease lease = engine.acquireGenerationLease("bukkit_nms_base_height");
IrisContext.Scope ignored = IrisContext.open(engine, lease.sessionId(), null)) {
return levelheightaccessor.getMinY()
+ engine.getHeight(i, j, !heightmap_type.isOpaque().test(Blocks.WATER.defaultBlockState()))
+ 1;
} catch (GenerationSessionException e) {
throw new IllegalStateException("Iris base height query could not acquire its engine runtime.", e);
}
}
@Override
public NoiseColumn getBaseColumn(int i, int j, LevelHeightAccessor levelheightaccessor, RandomState randomstate) {
int block = engine.getHeight(i, j, true);
int water = engine.getHeight(i, j, false);
BlockState[] column = new BlockState[levelheightaccessor.getHeight()];
for (int k = 0; k < column.length; k++) {
if (k <= block) column[k] = Blocks.STONE.defaultBlockState();
else if (k <= water) column[k] = Blocks.WATER.defaultBlockState();
else column[k] = Blocks.AIR.defaultBlockState();
try (GenerationSessionLease lease = engine.acquireGenerationLease("bukkit_nms_base_column");
IrisContext.Scope ignored = IrisContext.open(engine, lease.sessionId(), null)) {
int block = engine.getHeight(i, j, true);
int water = engine.getHeight(i, j, false);
BlockState[] column = new BlockState[levelheightaccessor.getHeight()];
for (int k = 0; k < column.length; k++) {
if (k <= block) {
column[k] = Blocks.STONE.defaultBlockState();
} else if (k <= water) {
column[k] = Blocks.WATER.defaultBlockState();
} else {
column[k] = Blocks.AIR.defaultBlockState();
}
}
return new NoiseColumn(levelheightaccessor.getMinY(), column);
} catch (GenerationSessionException e) {
throw new IllegalStateException("Iris base column query could not acquire its engine runtime.", e);
}
}
private GenerationSessionLease requireGenerationLease(String operation) {
try {
return engine.acquireGenerationLease(operation);
} catch (GenerationSessionException exception) {
throw new IllegalStateException("Iris " + operation + " could not acquire its engine runtime.", exception);
}
return new NoiseColumn(levelheightaccessor.getMinY(), column);
}
@Override
@@ -19,4 +19,21 @@ public class CustomBiomeSourceStructureContractTest {
assertTrue(source.contains("resolution.irisBiome.getStructureDerivativeKey()"));
assertFalse(source.contains("resolution.irisBiome.getVanillaDerivative()"));
}
@Test
public void biomeRuntimeReadsAreGenerationLeased() throws IOException {
String source = Files.readString(Path.of(System.getProperty("iris.customBiomeSource")));
assertTrue(source.contains("tryAcquireGenerationLease(\"bukkit_possible_biomes\")"));
assertTrue(source.contains("tryAcquireGenerationLease(\"bukkit_spawn_biome\")"));
assertTrue(source.contains("Iris spawn biome lookup was rejected during an engine transition"));
assertTrue(source.contains("Iris spawn biome lookup has no active engine runtime"));
assertTrue(source.contains("tryAcquireGenerationLease(\"bukkit_structure_biome\")"));
assertTrue(source.contains("tryAcquireGenerationLease(\"bukkit_biomes_within\")"));
assertTrue(source.contains("tryAcquireGenerationLease(\"bukkit_visible_biome\")"));
assertTrue(source.contains("if (engine.isClosed())"));
assertTrue(source.contains("engine.isClosing() || e.isExpectedTeardown()"));
assertTrue(source.contains("catch (GenerationSessionException e)"));
assertTrue(source.contains("e.isExpectedTeardown()"));
}
}
@@ -54,4 +54,14 @@ public class IrisChunkGeneratorFailureContractTest {
< placement.indexOf("for (NativePlacementGroup group"));
assertFalse(placement.contains("IrisLogging.reportError"));
}
@Test
public void heightmapRuntimeReadsAreGenerationLeased() throws IOException {
String source = Files.readString(Path.of(System.getProperty("iris.nmsChunkGeneratorSource")));
assertTrue(source.contains("engine.acquireGenerationLease(\"bukkit_nms_heightmaps\")"));
assertTrue(source.contains("engine.acquireGenerationLease(\"bukkit_nms_base_height\")"));
assertTrue(source.contains("engine.acquireGenerationLease(\"bukkit_nms_base_column\")"));
assertTrue(source.contains("catch (GenerationSessionException e)"));
}
}
+4
View File
@@ -46,4 +46,8 @@ tasks.named('jar', Jar).configure {
tasks.named('test').configure {
systemProperty('iris.commandFindSource', file('src/main/java/art/arcane/iris/core/commands/CommandFind.java').absolutePath)
systemProperty('iris.commandStructureSource', file('src/main/java/art/arcane/iris/core/commands/CommandStructure.java').absolutePath)
systemProperty('iris.bukkitChunkGeneratorSource', rootProject.file('core/src/main/java/art/arcane/iris/engine/platform/BukkitChunkGenerator.java').absolutePath)
systemProperty('iris.pregeneratorJobSource', rootProject.file('core/src/main/java/art/arcane/iris/core/gui/PregeneratorJob.java').absolutePath)
systemProperty('iris.bukkitEnginePlatformHooksSource', file('src/main/java/art/arcane/iris/core/runtime/BukkitEnginePlatformHooks.java').absolutePath)
systemProperty('iris.engineSvcSource', file('src/main/java/art/arcane/iris/core/service/IrisEngineSVC.java').absolutePath)
}
@@ -55,6 +55,7 @@ import art.arcane.iris.engine.EnginePanic;
import art.arcane.iris.engine.framework.BlockEditAccess;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.framework.PreservationRegistry;
import art.arcane.iris.engine.framework.TreeBlockMaterial;
import art.arcane.iris.engine.object.IrisCompat;
import art.arcane.iris.engine.object.IrisDimension;
import art.arcane.iris.engine.object.IrisWorld;
@@ -634,11 +635,8 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
IrisServices.register(EngineComponentCleanup.class, (EngineComponentCleanup) BukkitPlatform::unregisterListener);
IrisServices.register(EngineEffectsProvider.class, (EngineEffectsProvider) IrisEngineEffects::new);
IrisServices.register(EnginePlatformHooks.class, new BukkitEnginePlatformHooks());
IrisServices.register(EngineWorldManagerProvider.class, (EngineWorldManagerProvider) (Engine engine) -> {
IrisWorldManager manager = new IrisWorldManager(engine);
manager.startManager();
return manager;
});
IrisServices.register(EngineWorldManagerProvider.class,
(EngineWorldManagerProvider) IrisWorldManager::new);
IrisServices.register(art.arcane.iris.core.runtime.WorldDeletionQueue.class, (art.arcane.iris.core.runtime.WorldDeletionQueue) Iris::queueWorldDeletionOnStartup);
settingsFile = getDataFile("settings.json");
configHotloadEngine = new ConfigHotloadEngine(
@@ -673,6 +671,7 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
}
IrisToolbelt.retainMantleDataForSlice(String.class.getCanonicalName());
IrisToolbelt.retainMantleDataForSlice(BlockData.class.getCanonicalName());
IrisToolbelt.retainMantleDataForSlice(TreeBlockMaterial.class.getCanonicalName());
});
}
@@ -0,0 +1,12 @@
package art.arcane.iris.api.tree;
import org.bukkit.block.Block;
import org.bukkit.event.block.BlockBreakEvent;
public interface IrisTreeFellerService {
boolean tryFell(BlockBreakEvent event, TreeFellerOptions options);
boolean isManagedBreak(BlockBreakEvent event);
boolean isTreeBlock(Block block);
}
@@ -0,0 +1,6 @@
package art.arcane.iris.api.tree;
public enum TreeFellerAccess {
STANDALONE,
INTEGRATION_OVERRIDE
}
@@ -0,0 +1,32 @@
package art.arcane.iris.api.tree;
import java.util.Objects;
public record TreeFellerOptions(
TreeFellerAccess access,
int durabilityPreservationChance,
TreeFellerRunHooks runHooks
) {
public TreeFellerOptions {
Objects.requireNonNull(access, "access");
Objects.requireNonNull(runHooks, "runHooks");
if (durabilityPreservationChance < 0 || durabilityPreservationChance > 100) {
throw new IllegalArgumentException("durabilityPreservationChance must be between 0 and 100");
}
}
public static TreeFellerOptions standalone() {
return new TreeFellerOptions(TreeFellerAccess.STANDALONE, 0, TreeFellerRunHooks.NONE);
}
public static TreeFellerOptions integrationOverride(
int durabilityPreservationChance,
TreeFellerRunHooks runHooks
) {
return new TreeFellerOptions(
TreeFellerAccess.INTEGRATION_OVERRIDE,
durabilityPreservationChance,
runHooks
);
}
}
@@ -0,0 +1,30 @@
package art.arcane.iris.api.tree;
public interface TreeFellerRunHooks {
TreeFellerRunHooks NONE = new TreeFellerRunHooks() {
@Override
public void onActivationAccepted() {
}
@Override
public boolean reserveLogCost() {
return true;
}
@Override
public void commitLogCost() {
}
@Override
public void refundLogCost() {
}
};
void onActivationAccepted();
boolean reserveLogCost();
void commitLogCost();
void refundLogCost();
}
@@ -80,7 +80,10 @@ public final class BukkitEnginePlatformHooks implements EnginePlatformHooks {
@Override
public void shutdownPregenerator(Engine engine) {
PregeneratorJob.shutdownInstance();
IrisWorld world = engine.getWorld();
if (world != null) {
PregeneratorJob.shutdownInstanceForWorld(world.identity());
}
}
@Override
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,91 @@
package art.arcane.iris.core.service;
import art.arcane.iris.core.loader.ResourceLoader;
import art.arcane.iris.engine.framework.MeteredCache;
import art.arcane.iris.spi.IrisServices;
import art.arcane.iris.util.common.format.C;
import art.arcane.iris.util.common.plugin.VolmitSender;
import art.arcane.iris.util.project.stream.utility.CachedDoubleStream2D;
import art.arcane.iris.util.project.stream.utility.CachedStream2D;
import art.arcane.iris.util.project.stream.utility.CachedStream3D;
import art.arcane.volmlib.util.format.Form;
import java.util.List;
final class IrisEngineStatus {
private IrisEngineStatus() {
}
static void send(VolmitSender sender, Snapshot snapshot) {
CacheSummary caches = summarizeCaches();
MaintenanceMetrics metrics = snapshot.metrics();
sender.sendMessage(C.DARK_PURPLE + "-------------------------");
sender.sendMessage(C.DARK_PURPLE + "Status:");
sender.sendMessage(C.DARK_PURPLE + "- Service: " + C.LIGHT_PURPLE + (snapshot.serviceRunning() ? "Running" : "Stopped"));
sender.sendMessage(C.DARK_PURPLE + "- Metrics: " + C.LIGHT_PURPLE + (snapshot.metricsRunning() ? "Running" : "Stopped"));
sender.sendMessage(C.DARK_PURPLE + "- Maintenance Period: " + C.LIGHT_PURPLE + Form.duration(snapshot.maintenancePeriodMillis()));
sender.sendMessage(C.DARK_PURPLE + "- Worker Parallelism: " + C.LIGHT_PURPLE + snapshot.workerParallelism());
sender.sendMessage(C.DARK_PURPLE + "- Active World Tasks: " + C.LIGHT_PURPLE + metrics.activeTasks());
sender.sendMessage(C.DARK_PURPLE + "Tectonic Plates:");
sender.sendMessage(C.DARK_PURPLE + "- Configured Retention: " + C.LIGHT_PURPLE + Form.duration(snapshot.retentionMillis()));
sender.sendMessage(C.DARK_PURPLE + "- Heap Usage: " + C.LIGHT_PURPLE + Form.pc(snapshot.heapUsage()));
sender.sendMessage(C.DARK_PURPLE + "- Resident: " + C.LIGHT_PURPLE + metrics.residentTectonicPlates());
sender.sendMessage(C.DARK_PURPLE + "- Queued: " + C.LIGHT_PURPLE + metrics.queuedTectonicPlates());
sender.sendMessage(C.DARK_PURPLE + "- Average Idle Duration: " + C.LIGHT_PURPLE + Form.duration(metrics.averageIdleDuration(), 2));
sender.sendMessage(C.DARK_PURPLE + "- Max Idle Duration: " + C.LIGHT_PURPLE + Form.duration(metrics.maxIdleDuration(), 2));
sender.sendMessage(C.DARK_PURPLE + "- Min Idle Duration: " + C.LIGHT_PURPLE + Form.duration(metrics.minIdleDuration(), 2));
sender.sendMessage(C.DARK_PURPLE + "Caches:");
sender.sendMessage(C.DARK_PURPLE + "- Resource: " + C.LIGHT_PURPLE + caches.sizes()[0] + " (" + caches.counts()[0] + ")");
sender.sendMessage(C.DARK_PURPLE + "- 2D Stream: " + C.LIGHT_PURPLE + caches.sizes()[1] + " (" + caches.counts()[1] + ")");
sender.sendMessage(C.DARK_PURPLE + "- 3D Stream: " + C.LIGHT_PURPLE + caches.sizes()[2] + " (" + caches.counts()[2] + ")");
sender.sendMessage(C.DARK_PURPLE + "- Other: " + C.LIGHT_PURPLE + caches.sizes()[3] + " (" + caches.counts()[3] + ")");
sender.sendMessage(C.DARK_PURPLE + "Other:");
sender.sendMessage(C.DARK_PURPLE + "- Iris Worlds: " + C.LIGHT_PURPLE + metrics.worlds());
sender.sendMessage(C.DARK_PURPLE + "- Loaded Chunks: " + C.LIGHT_PURPLE + metrics.loadedChunks());
sender.sendMessage(C.DARK_PURPLE + "-------------------------");
}
private static CacheSummary summarizeCaches() {
long[] sizes = new long[4];
long[] counts = new long[4];
PreservationSVC preservation = IrisServices.get(PreservationSVC.class);
List<MeteredCache> caches = preservation == null ? List.of() : preservation.getCaches();
for (MeteredCache cache : caches) {
int type = switch (cache) {
case ResourceLoader<?> ignored -> 0;
case CachedStream2D<?> ignored -> 1;
case CachedDoubleStream2D ignored -> 1;
case CachedStream3D<?> ignored -> 2;
default -> 3;
};
sizes[type] += cache.getSize();
counts[type]++;
}
return new CacheSummary(sizes, counts);
}
record Snapshot(boolean serviceRunning,
boolean metricsRunning,
long maintenancePeriodMillis,
int workerParallelism,
long retentionMillis,
double heapUsage,
MaintenanceMetrics metrics) {
}
record MaintenanceMetrics(long residentTectonicPlates,
long queuedTectonicPlates,
long loadedChunks,
int worlds,
int activeTasks,
double averageIdleDuration,
double maxIdleDuration,
double minIdleDuration) {
static final MaintenanceMetrics EMPTY = new MaintenanceMetrics(0L, 0L, 0L, 0, 0, 0D, 0D, 0D);
}
private record CacheSummary(long[] sizes, long[] counts) {
}
}
@@ -1,12 +1,14 @@
package art.arcane.iris.core.service;
import art.arcane.iris.Iris;
import art.arcane.iris.core.gui.PregeneratorJob;
import art.arcane.iris.engine.framework.EngineTelemetrySnapshot;
import art.arcane.iris.spi.IrisServices;
import art.arcane.iris.util.common.plugin.IrisService;
import art.arcane.volmlib.integration.IntegrationHandshakeRequest;
import art.arcane.volmlib.integration.IntegrationHandshakeResponse;
import art.arcane.volmlib.integration.IntegrationHeartbeat;
import art.arcane.volmlib.integration.IntegrationMetricDescriptor;
import art.arcane.volmlib.integration.IntegrationMetricGroup;
import art.arcane.volmlib.integration.IntegrationMetricSample;
import art.arcane.volmlib.integration.IntegrationMetricSchema;
import art.arcane.volmlib.integration.IntegrationProtocolNegotiator;
@@ -15,25 +17,59 @@ import art.arcane.volmlib.integration.IntegrationServiceContract;
import org.bukkit.Bukkit;
import org.bukkit.plugin.ServicePriority;
import java.util.HashMap;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.function.Supplier;
public class IrisIntegrationService implements IrisService, IntegrationServiceContract {
private static final IntegrationProtocolVersion CURRENT_PROTOCOL = new IntegrationProtocolVersion(1, 2);
private static final Set<IntegrationProtocolVersion> SUPPORTED_PROTOCOLS = Set.of(
new IntegrationProtocolVersion(1, 0),
new IntegrationProtocolVersion(1, 1)
new IntegrationProtocolVersion(1, 1),
CURRENT_PROTOCOL
);
private static final Set<String> CAPABILITIES = Set.of(
"handshake",
"heartbeat",
"metrics",
"iris-engine-metrics"
"metric-groups",
"iris-engine-metrics",
"iris-world-metrics"
);
private static final Map<String, String> TIMING_KEYS = Map.ofEntries(
Map.entry("total", IntegrationMetricSchema.IRIS_GENERATION_TOTAL_MS),
Map.entry("updates", IntegrationMetricSchema.IRIS_GENERATION_UPDATES_MS),
Map.entry("terrain", IntegrationMetricSchema.IRIS_GENERATION_TERRAIN_MS),
Map.entry("biome", IntegrationMetricSchema.IRIS_GENERATION_BIOME_MS),
Map.entry("post", IntegrationMetricSchema.IRIS_GENERATION_POST_MS),
Map.entry("perfection", IntegrationMetricSchema.IRIS_GENERATION_PERFECTION_MS),
Map.entry("decoration", IntegrationMetricSchema.IRIS_GENERATION_DECORATION_MS),
Map.entry("cave", IntegrationMetricSchema.IRIS_GENERATION_CAVE_MS),
Map.entry("deposit", IntegrationMetricSchema.IRIS_GENERATION_DEPOSIT_MS),
Map.entry("carve.resolve", IntegrationMetricSchema.IRIS_GENERATION_CARVE_RESOLVE_MS),
Map.entry("carve.apply", IntegrationMetricSchema.IRIS_GENERATION_CARVE_APPLY_MS),
Map.entry("context.prefill", IntegrationMetricSchema.IRIS_GENERATION_CONTEXT_PREFILL_MS),
Map.entry("pregen.wait.permit", IntegrationMetricSchema.IRIS_PREGEN_WAIT_PERMIT_MS),
Map.entry("pregen.wait.adaptive", IntegrationMetricSchema.IRIS_PREGEN_WAIT_ADAPTIVE_MS)
);
private volatile IntegrationProtocolVersion negotiatedProtocol = new IntegrationProtocolVersion(1, 1);
private final Supplier<IrisTelemetrySnapshot> telemetrySupplier;
public IrisIntegrationService() {
this(IrisIntegrationService::currentTelemetry);
}
IrisIntegrationService(Supplier<IrisTelemetrySnapshot> telemetrySupplier) {
this.telemetrySupplier = telemetrySupplier == null
? () -> IrisTelemetrySnapshot.EMPTY
: telemetrySupplier;
}
@Override
public void onEnable() {
@@ -68,9 +104,11 @@ public class IrisIntegrationService implements IrisService, IntegrationServiceCo
@Override
public Set<IntegrationMetricDescriptor> metricDescriptors() {
return IntegrationMetricSchema.descriptors().stream()
.filter(descriptor -> descriptor.key().startsWith("iris."))
.collect(java.util.stream.Collectors.toSet());
Set<IntegrationMetricDescriptor> descriptors = new LinkedHashSet<>();
for (String key : IntegrationMetricSchema.irisKeys()) {
descriptors.add(IntegrationMetricSchema.descriptor(key));
}
return Set.copyOf(descriptors);
}
@Override
@@ -106,12 +144,11 @@ public class IrisIntegrationService implements IrisService, IntegrationServiceCo
);
}
negotiatedProtocol = negotiated.get();
return new IntegrationHandshakeResponse(
pluginId(),
pluginVersion(),
true,
negotiatedProtocol,
negotiated.get(),
SUPPORTED_PROTOCOLS,
CAPABILITIES,
"ok",
@@ -121,90 +158,228 @@ public class IrisIntegrationService implements IrisService, IntegrationServiceCo
@Override
public IntegrationHeartbeat heartbeat() {
long now = System.currentTimeMillis();
return new IntegrationHeartbeat(negotiatedProtocol, true, now, "ok");
return new IntegrationHeartbeat(CURRENT_PROTOCOL, true, System.currentTimeMillis(), "ok");
}
@Override
public Map<String, IntegrationMetricSample> sampleMetrics(Set<String> metricKeys) {
Set<String> requested = metricKeys == null || metricKeys.isEmpty()
? IntegrationMetricSchema.irisKeys()
: metricKeys;
long now = System.currentTimeMillis();
Map<String, IntegrationMetricSample> out = new HashMap<>();
: Set.copyOf(metricKeys);
IrisTelemetrySnapshot snapshot = safeSnapshot();
long sampledAtMs = snapshot.sampledAtMs() > 0L
? snapshot.sampledAtMs()
: System.currentTimeMillis();
if (snapshot.sampledAtMs() <= 0L) {
return unavailableMetrics(requested, "telemetry-not-ready", sampledAtMs);
}
Map<String, IntegrationMetricSample> available = globalSamples(snapshot);
Map<String, IntegrationMetricSample> selected = new LinkedHashMap<>(requested.size());
for (String key : requested) {
switch (key) {
case IntegrationMetricSchema.IRIS_CHUNK_STREAM_MS -> out.put(key, sampleChunkStreamMetric(now));
case IntegrationMetricSchema.IRIS_PREGEN_QUEUE -> out.put(key, samplePregenQueueMetric(now));
case IntegrationMetricSchema.IRIS_BIOME_CACHE_HIT_RATE -> out.put(key, sampleBiomeCacheHitRateMetric(now));
default -> out.put(key, IntegrationMetricSample.unavailable(
IntegrationMetricSchema.descriptor(key),
"unsupported-key",
now
));
IntegrationMetricSample sample = available.get(key);
selected.put(key, sample == null
? unavailable(key, "unsupported-key", sampledAtMs)
: sample);
}
return Map.copyOf(selected);
}
@Override
public List<IntegrationMetricGroup> metricGroups() {
IrisTelemetrySnapshot snapshot = safeSnapshot();
if (snapshot.sampledAtMs() <= 0L || snapshot.worlds().isEmpty()) {
return List.of();
}
List<EngineTelemetrySnapshot> worlds = new ArrayList<>(snapshot.worlds());
worlds.sort(Comparator.comparing(EngineTelemetrySnapshot::worldIdentity));
List<IntegrationMetricGroup> groups = new ArrayList<>(worlds.size());
for (EngineTelemetrySnapshot world : worlds) {
groups.add(new IntegrationMetricGroup(
"world",
world.worldIdentity(),
world.worldName(),
Map.of(
"plugin", "iris",
"dimension", world.dimensionKey()
),
worldSamples(world, snapshot.pregenerator())
));
}
return List.copyOf(groups);
}
private Map<String, IntegrationMetricSample> globalSamples(IrisTelemetrySnapshot snapshot) {
long sampledAtMs = snapshot.sampledAtMs();
EngineTelemetrySnapshot.Aggregate aggregate = snapshot.aggregate();
Map<String, IntegrationMetricSample> samples = new LinkedHashMap<>();
put(samples, IntegrationMetricSchema.IRIS_WORLD_COUNT, aggregate.worldCount(), sampledAtMs);
put(samples, IntegrationMetricSchema.IRIS_ENGINE_ACTIVE, aggregate.active(), sampledAtMs);
put(samples, IntegrationMetricSchema.IRIS_ENGINE_CLOSING, aggregate.closing() + snapshot.closingEngines(), sampledAtMs);
put(samples, IntegrationMetricSchema.IRIS_ENGINE_FAILED, aggregate.failed(), sampledAtMs);
put(samples, IntegrationMetricSchema.IRIS_ENGINE_STUDIO, aggregate.studio(), sampledAtMs);
put(samples, IntegrationMetricSchema.IRIS_ENGINE_PENDING_REGISTRATIONS, snapshot.pendingRegistrations(), sampledAtMs);
put(samples, IntegrationMetricSchema.IRIS_LOADED_CHUNKS, aggregate.loadedChunks(), sampledAtMs);
put(samples, IntegrationMetricSchema.IRIS_LOADED_ENTITIES, aggregate.loadedEntities(), sampledAtMs);
put(samples, IntegrationMetricSchema.IRIS_ENTITY_SATURATION, aggregate.entitySaturationMax(), sampledAtMs);
put(samples, IntegrationMetricSchema.IRIS_CHUNKS_GENERATED_SESSION, aggregate.generatedSession(), sampledAtMs);
put(samples, IntegrationMetricSchema.IRIS_CHUNKS_GENERATED_TOTAL, aggregate.generatedTotal(), sampledAtMs);
put(samples, IntegrationMetricSchema.IRIS_CHUNKS_PER_SECOND, aggregate.chunksPerSecond(), sampledAtMs);
put(samples, IntegrationMetricSchema.IRIS_BLOCK_UPDATES_PER_SECOND, aggregate.blockUpdatesPerSecond(), sampledAtMs);
put(samples, IntegrationMetricSchema.IRIS_ENGINE_PARALLELISM, aggregate.parallelism(), sampledAtMs);
put(samples, IntegrationMetricSchema.IRIS_GENERATION_ACTIVE_LEASES, aggregate.activeGenerationLeases(), sampledAtMs);
put(samples, IntegrationMetricSchema.IRIS_HOTLOADS_TOTAL, aggregate.hotloadsTotal(), sampledAtMs);
put(samples, IntegrationMetricSchema.IRIS_MAINTENANCE_ACTIVE_TASKS, snapshot.maintenanceActiveTasks(), sampledAtMs);
put(samples, IntegrationMetricSchema.IRIS_MAINTENANCE_WORKERS, snapshot.maintenanceWorkers(), sampledAtMs);
put(samples, IntegrationMetricSchema.IRIS_MANTLE_RESIDENT_PLATES, aggregate.mantleResidentPlates(), sampledAtMs);
put(samples, IntegrationMetricSchema.IRIS_MANTLE_QUEUED_PLATES, aggregate.mantleQueuedPlates(), sampledAtMs);
put(samples, IntegrationMetricSchema.IRIS_MANTLE_IDLE_AVERAGE_MS, aggregate.mantleIdleAverageMs(), sampledAtMs);
put(samples, IntegrationMetricSchema.IRIS_MANTLE_IDLE_MAX_MS, aggregate.mantleIdleMaxMs(), sampledAtMs);
put(samples, IntegrationMetricSchema.IRIS_MANTLE_IDLE_MIN_MS, aggregate.mantleIdleMinMs(), sampledAtMs);
put(samples, IntegrationMetricSchema.IRIS_MANTLE_HEAP_USAGE, snapshot.heapUsage(), sampledAtMs);
put(samples, IntegrationMetricSchema.IRIS_MANTLE_RECLAIM_URGENCY, snapshot.reclaimUrgency(), sampledAtMs);
putCacheSamples(samples, snapshot.caches(), sampledAtMs);
putPregenSamples(samples, snapshot.pregenerator(), sampledAtMs);
putTimingSamples(samples, aggregate.generationTimingMaximaMs(), sampledAtMs);
return Map.copyOf(samples);
}
private Map<String, IntegrationMetricSample> worldSamples(
EngineTelemetrySnapshot world,
IrisTelemetrySnapshot.PregenSnapshot pregenerator
) {
long sampledAtMs = world.sampledAtMs();
Map<String, IntegrationMetricSample> samples = new LinkedHashMap<>();
put(samples, IntegrationMetricSchema.IRIS_ENGINE_ACTIVE, world.active() ? 1 : 0, sampledAtMs);
put(samples, IntegrationMetricSchema.IRIS_ENGINE_CLOSING, world.closing() ? 1 : 0, sampledAtMs);
put(samples, IntegrationMetricSchema.IRIS_ENGINE_FAILED, world.failed() ? 1 : 0, sampledAtMs);
put(samples, IntegrationMetricSchema.IRIS_ENGINE_STUDIO, world.studio() ? 1 : 0, sampledAtMs);
put(samples, IntegrationMetricSchema.IRIS_LOADED_CHUNKS, world.loadedChunks(), sampledAtMs);
put(samples, IntegrationMetricSchema.IRIS_LOADED_ENTITIES, world.loadedEntities(), sampledAtMs);
put(samples, IntegrationMetricSchema.IRIS_ENTITY_SATURATION, world.entitySaturation(), sampledAtMs);
put(samples, IntegrationMetricSchema.IRIS_CHUNKS_GENERATED_SESSION, world.generatedSession(), sampledAtMs);
put(samples, IntegrationMetricSchema.IRIS_CHUNKS_GENERATED_TOTAL, world.generatedTotal(), sampledAtMs);
put(samples, IntegrationMetricSchema.IRIS_CHUNKS_PER_SECOND, world.chunksPerSecond(), sampledAtMs);
put(samples, IntegrationMetricSchema.IRIS_BLOCK_UPDATES_PER_SECOND, world.blockUpdatesPerSecond(), sampledAtMs);
put(samples, IntegrationMetricSchema.IRIS_ENGINE_PARALLELISM, world.parallelism(), sampledAtMs);
put(samples, IntegrationMetricSchema.IRIS_GENERATION_ACTIVE_LEASES, world.activeGenerationLeases(), sampledAtMs);
put(samples, IntegrationMetricSchema.IRIS_HOTLOADS_TOTAL, world.hotloadsTotal(), sampledAtMs);
put(samples, IntegrationMetricSchema.IRIS_MANTLE_RESIDENT_PLATES, world.mantleResidentPlates(), sampledAtMs);
put(samples, IntegrationMetricSchema.IRIS_MANTLE_QUEUED_PLATES, world.mantleQueuedPlates(), sampledAtMs);
put(samples, IntegrationMetricSchema.IRIS_MANTLE_IDLE_AVERAGE_MS, world.mantleIdleMs(), sampledAtMs);
boolean targetsWorld = pregenerator.active()
&& world.worldIdentity().equals(pregenerator.worldIdentity());
putPregenSamples(samples, targetsWorld ? pregenerator : IrisTelemetrySnapshot.PregenSnapshot.INACTIVE, sampledAtMs);
putTimingSamples(samples, world.generationTimingsMs(), sampledAtMs);
samples.keySet().retainAll(IntegrationMetricSchema.irisWorldKeys());
return Map.copyOf(samples);
}
private void putCacheSamples(
Map<String, IntegrationMetricSample> samples,
IrisTelemetrySnapshot.CacheSnapshot caches,
long sampledAtMs
) {
putCacheBucket(samples, caches.total(), IntegrationMetricSchema.IRIS_CACHE_COUNT, IntegrationMetricSchema.IRIS_CACHE_ENTRIES, IntegrationMetricSchema.IRIS_CACHE_CAPACITY, IntegrationMetricSchema.IRIS_CACHE_USAGE, sampledAtMs);
putCacheBucket(samples, caches.resource(), IntegrationMetricSchema.IRIS_CACHE_RESOURCE_COUNT, IntegrationMetricSchema.IRIS_CACHE_RESOURCE_ENTRIES, IntegrationMetricSchema.IRIS_CACHE_RESOURCE_CAPACITY, IntegrationMetricSchema.IRIS_CACHE_RESOURCE_USAGE, sampledAtMs);
putCacheBucket(samples, caches.stream2d(), IntegrationMetricSchema.IRIS_CACHE_STREAM_2D_COUNT, IntegrationMetricSchema.IRIS_CACHE_STREAM_2D_ENTRIES, IntegrationMetricSchema.IRIS_CACHE_STREAM_2D_CAPACITY, IntegrationMetricSchema.IRIS_CACHE_STREAM_2D_USAGE, sampledAtMs);
putCacheBucket(samples, caches.stream3d(), IntegrationMetricSchema.IRIS_CACHE_STREAM_3D_COUNT, IntegrationMetricSchema.IRIS_CACHE_STREAM_3D_ENTRIES, IntegrationMetricSchema.IRIS_CACHE_STREAM_3D_CAPACITY, IntegrationMetricSchema.IRIS_CACHE_STREAM_3D_USAGE, sampledAtMs);
putCacheBucket(samples, caches.other(), IntegrationMetricSchema.IRIS_CACHE_OTHER_COUNT, IntegrationMetricSchema.IRIS_CACHE_OTHER_ENTRIES, IntegrationMetricSchema.IRIS_CACHE_OTHER_CAPACITY, IntegrationMetricSchema.IRIS_CACHE_OTHER_USAGE, sampledAtMs);
}
private void putCacheBucket(
Map<String, IntegrationMetricSample> samples,
IrisTelemetrySnapshot.CacheBucket bucket,
String countKey,
String entriesKey,
String capacityKey,
String usageKey,
long sampledAtMs
) {
put(samples, countKey, bucket.count(), sampledAtMs);
put(samples, entriesKey, bucket.entries(), sampledAtMs);
put(samples, capacityKey, bucket.capacity(), sampledAtMs);
put(samples, usageKey, bucket.usage(), sampledAtMs);
}
private void putPregenSamples(
Map<String, IntegrationMetricSample> samples,
IrisTelemetrySnapshot.PregenSnapshot pregenerator,
long sampledAtMs
) {
put(samples, IntegrationMetricSchema.IRIS_PREGEN_ACTIVE, pregenerator.active() ? 1 : 0, sampledAtMs);
if (!pregenerator.active()) {
for (String key : Set.of(
IntegrationMetricSchema.IRIS_PREGEN_PAUSED,
IntegrationMetricSchema.IRIS_PREGEN_PROGRESS,
IntegrationMetricSchema.IRIS_PREGEN_GENERATED,
IntegrationMetricSchema.IRIS_PREGEN_TOTAL,
IntegrationMetricSchema.IRIS_PREGEN_QUEUE,
IntegrationMetricSchema.IRIS_PREGEN_THROUGHPUT,
IntegrationMetricSchema.IRIS_PREGEN_ETA_MS,
IntegrationMetricSchema.IRIS_PREGEN_ELAPSED_MS,
IntegrationMetricSchema.IRIS_PREGEN_FAILED)) {
samples.put(key, unavailable(key, "pregen-inactive", sampledAtMs));
}
return;
}
return out;
put(samples, IntegrationMetricSchema.IRIS_PREGEN_PAUSED, pregenerator.paused() ? 1 : 0, sampledAtMs);
put(samples, IntegrationMetricSchema.IRIS_PREGEN_PROGRESS, pregenerator.progressPercent(), sampledAtMs);
put(samples, IntegrationMetricSchema.IRIS_PREGEN_GENERATED, pregenerator.generated(), sampledAtMs);
put(samples, IntegrationMetricSchema.IRIS_PREGEN_TOTAL, pregenerator.total(), sampledAtMs);
put(samples, IntegrationMetricSchema.IRIS_PREGEN_QUEUE, pregenerator.remaining(), sampledAtMs);
put(samples, IntegrationMetricSchema.IRIS_PREGEN_THROUGHPUT, pregenerator.chunksPerSecond(), sampledAtMs);
put(samples, IntegrationMetricSchema.IRIS_PREGEN_ETA_MS, pregenerator.etaMs(), sampledAtMs);
put(samples, IntegrationMetricSchema.IRIS_PREGEN_ELAPSED_MS, pregenerator.elapsedMs(), sampledAtMs);
put(samples, IntegrationMetricSchema.IRIS_PREGEN_FAILED, pregenerator.failed(), sampledAtMs);
}
private IntegrationMetricSample sampleChunkStreamMetric(long now) {
IntegrationMetricDescriptor descriptor = IntegrationMetricSchema.descriptor(IntegrationMetricSchema.IRIS_CHUNK_STREAM_MS);
double chunksPerSecond = PregeneratorJob.chunksPerSecond();
if (chunksPerSecond > 0D) {
return IntegrationMetricSample.available(descriptor, 1000D / chunksPerSecond, now);
private void putTimingSamples(
Map<String, IntegrationMetricSample> samples,
Map<String, Double> timings,
long sampledAtMs
) {
for (Map.Entry<String, String> entry : TIMING_KEYS.entrySet()) {
Double value = timings.get(entry.getKey());
samples.put(entry.getValue(), value == null
? unavailable(entry.getValue(), "timing-not-available", sampledAtMs)
: available(entry.getValue(), value, sampledAtMs));
}
IrisEngineSVC engineService = Iris.service(IrisEngineSVC.class);
if (engineService != null) {
double idle = engineService.getAverageIdleDuration();
if (idle > 0D && Double.isFinite(idle)) {
return IntegrationMetricSample.available(descriptor, idle, now);
}
}
return IntegrationMetricSample.available(descriptor, 0D, now);
}
private IntegrationMetricSample samplePregenQueueMetric(long now) {
IntegrationMetricDescriptor descriptor = IntegrationMetricSchema.descriptor(IntegrationMetricSchema.IRIS_PREGEN_QUEUE);
long totalQueue = 0L;
boolean hasAnySource = false;
long pregenRemaining = PregeneratorJob.chunksRemaining();
if (pregenRemaining >= 0L) {
totalQueue += pregenRemaining;
hasAnySource = true;
}
IrisEngineSVC engineService = Iris.service(IrisEngineSVC.class);
if (engineService != null) {
totalQueue += Math.max(0, engineService.getQueuedTectonicPlateCount());
hasAnySource = true;
}
if (!hasAnySource) {
return IntegrationMetricSample.unavailable(descriptor, "queue-not-available", now);
}
return IntegrationMetricSample.available(descriptor, totalQueue, now);
private void put(Map<String, IntegrationMetricSample> samples, String key, double value, long sampledAtMs) {
samples.put(key, available(key, value, sampledAtMs));
}
private IntegrationMetricSample sampleBiomeCacheHitRateMetric(long now) {
IntegrationMetricDescriptor descriptor = IntegrationMetricSchema.descriptor(IntegrationMetricSchema.IRIS_BIOME_CACHE_HIT_RATE);
IrisEngineSVC engineService = Iris.service(IrisEngineSVC.class);
if (engineService == null) {
return IntegrationMetricSample.unavailable(descriptor, "engine-service-unavailable", now);
}
private IntegrationMetricSample available(String key, double value, long sampledAtMs) {
return IntegrationMetricSample.available(IntegrationMetricSchema.descriptor(key), value, sampledAtMs);
}
double ratio = engineService.getBiomeCacheUsageRatio();
if (!Double.isFinite(ratio)) {
return IntegrationMetricSample.unavailable(descriptor, "biome-cache-ratio-invalid", now);
}
private IntegrationMetricSample unavailable(String key, String reason, long sampledAtMs) {
return IntegrationMetricSample.unavailable(IntegrationMetricSchema.descriptor(key), reason, sampledAtMs);
}
return IntegrationMetricSample.available(descriptor, Math.max(0D, Math.min(1D, ratio)), now);
private Map<String, IntegrationMetricSample> unavailableMetrics(
Set<String> keys,
String reason,
long sampledAtMs
) {
Map<String, IntegrationMetricSample> samples = new LinkedHashMap<>(keys.size());
for (String key : keys) {
samples.put(key, unavailable(key, reason, sampledAtMs));
}
return Map.copyOf(samples);
}
private IrisTelemetrySnapshot safeSnapshot() {
IrisTelemetrySnapshot snapshot = telemetrySupplier.get();
return snapshot == null ? IrisTelemetrySnapshot.EMPTY : snapshot;
}
private static IrisTelemetrySnapshot currentTelemetry() {
IrisEngineSVC service = IrisServices.get(IrisEngineSVC.class);
return service == null ? IrisTelemetrySnapshot.EMPTY : service.telemetrySnapshot();
}
}
@@ -0,0 +1,140 @@
package art.arcane.iris.core.service;
import art.arcane.iris.engine.framework.EngineTelemetrySnapshot;
import java.util.List;
record IrisTelemetrySnapshot(
long sampledAtMs,
List<EngineTelemetrySnapshot> worlds,
EngineTelemetrySnapshot.Aggregate aggregate,
int maintenanceActiveTasks,
int maintenanceWorkers,
int closingEngines,
int pendingRegistrations,
double heapUsage,
double reclaimUrgency,
CacheSnapshot caches,
PregenSnapshot pregenerator
) {
static final IrisTelemetrySnapshot EMPTY = new IrisTelemetrySnapshot(
0L,
List.of(),
EngineTelemetrySnapshot.aggregate(List.of()),
0,
0,
0,
0,
0D,
0D,
CacheSnapshot.EMPTY,
PregenSnapshot.INACTIVE
);
IrisTelemetrySnapshot {
worlds = worlds == null ? List.of() : List.copyOf(worlds);
aggregate = aggregate == null ? EngineTelemetrySnapshot.aggregate(worlds) : aggregate;
maintenanceActiveTasks = Math.max(0, maintenanceActiveTasks);
maintenanceWorkers = Math.max(0, maintenanceWorkers);
closingEngines = Math.max(0, closingEngines);
pendingRegistrations = Math.max(0, pendingRegistrations);
heapUsage = finiteRatio(heapUsage);
reclaimUrgency = finiteRatio(reclaimUrgency);
caches = caches == null ? CacheSnapshot.EMPTY : caches;
pregenerator = pregenerator == null ? PregenSnapshot.INACTIVE : pregenerator;
}
private static double finiteRatio(double value) {
return Double.isFinite(value) ? Math.max(0D, Math.min(1D, value)) : 0D;
}
record CacheSnapshot(
CacheBucket total,
CacheBucket resource,
CacheBucket stream2d,
CacheBucket stream3d,
CacheBucket other
) {
static final CacheSnapshot EMPTY = new CacheSnapshot(
CacheBucket.EMPTY,
CacheBucket.EMPTY,
CacheBucket.EMPTY,
CacheBucket.EMPTY,
CacheBucket.EMPTY
);
CacheSnapshot {
total = total == null ? CacheBucket.EMPTY : total;
resource = resource == null ? CacheBucket.EMPTY : resource;
stream2d = stream2d == null ? CacheBucket.EMPTY : stream2d;
stream3d = stream3d == null ? CacheBucket.EMPTY : stream3d;
other = other == null ? CacheBucket.EMPTY : other;
}
}
record CacheBucket(int count, long entries, long capacity) {
static final CacheBucket EMPTY = new CacheBucket(0, 0L, 0L);
CacheBucket {
count = Math.max(0, count);
entries = Math.max(0L, entries);
capacity = Math.max(0L, capacity);
}
double usage() {
return capacity <= 0L ? 0D : Math.min(1D, entries / (double) capacity);
}
CacheBucket plus(CacheBucket other) {
if (other == null) {
return this;
}
return new CacheBucket(count + other.count, entries + other.entries, capacity + other.capacity);
}
}
record PregenSnapshot(
boolean active,
String worldIdentity,
String worldName,
boolean paused,
double progressPercent,
long generated,
long total,
long remaining,
double chunksPerSecond,
long etaMs,
long elapsedMs,
long failed
) {
static final PregenSnapshot INACTIVE = new PregenSnapshot(
false,
"",
"",
false,
0D,
0L,
0L,
0L,
0D,
0L,
0L,
0L
);
PregenSnapshot {
worldIdentity = worldIdentity == null ? "" : worldIdentity.trim();
worldName = worldName == null ? "" : worldName.trim();
progressPercent = Double.isFinite(progressPercent)
? Math.max(0D, Math.min(100D, progressPercent))
: 0D;
generated = Math.max(0L, generated);
total = Math.max(0L, total);
remaining = Math.max(0L, remaining);
chunksPerSecond = Double.isFinite(chunksPerSecond) ? Math.max(0D, chunksPerSecond) : 0D;
etaMs = Math.max(0L, etaMs);
elapsedMs = Math.max(0L, elapsedMs);
failed = Math.max(0L, failed);
}
}
}
@@ -0,0 +1,308 @@
package art.arcane.iris.core.service;
import art.arcane.iris.spi.IrisLogging;
import art.arcane.iris.util.common.scheduling.J;
import org.bukkit.Location;
import org.bukkit.Material;
import org.bukkit.Particle;
import org.bukkit.Sound;
import org.bukkit.World;
import org.bukkit.block.Block;
import org.bukkit.block.data.BlockData;
import org.bukkit.entity.ExperienceOrb;
import org.bukkit.entity.Item;
import org.bukkit.entity.Player;
import org.bukkit.inventory.ItemStack;
import org.bukkit.util.Vector;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Queue;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
final class TreeFellerPresentation {
private static final int MIN_BLOCKS_PER_PULSE = 4;
private static final int MAX_BLOCKS_PER_PULSE = 64;
private static final int TARGET_EROSION_PULSES = 60;
private static final int MAX_EFFECT_ORIGINS_PER_PULSE = 16;
private final Player player;
private final World sourceWorld;
private final Queue<PendingDrop> pendingDrops = new ConcurrentLinkedQueue<>();
private final AtomicBoolean flushScheduled = new AtomicBoolean();
private final AtomicBoolean fallbackScheduled = new AtomicBoolean();
private final AtomicBoolean completed = new AtomicBoolean();
private final AtomicBoolean effectFailureReported = new AtomicBoolean();
private final AtomicBoolean deliveryFailureReported = new AtomicBoolean();
private final AtomicInteger deliveryPulses = new AtomicInteger();
private volatile Location fallbackLocation;
TreeFellerPresentation(Player player, World sourceWorld, Location fallbackLocation) {
this.player = player;
this.sourceWorld = sourceWorld;
this.fallbackLocation = fallbackLocation.clone();
}
static int blocksPerPulse(int blockCount) {
int requested = Math.max(1, (blockCount + TARGET_EROSION_PULSES - 1) / TARGET_EROSION_PULSES);
return Math.max(MIN_BLOCKS_PER_PULSE, Math.min(requested, MAX_BLOCKS_PER_PULSE));
}
static int effectStride(int blocksPerPulse) {
return Math.max(
1,
(blocksPerPulse + MAX_EFFECT_ORIGINS_PER_PULSE - 1) / MAX_EFFECT_ORIGINS_PER_PULSE
);
}
static List<ItemStack> consolidateDrops(Collection<ItemStack> drops) {
List<ItemStack> consolidated = new ArrayList<>();
for (ItemStack drop : drops) {
mergeDrop(consolidated, drop);
}
return List.copyOf(consolidated);
}
void activate(Block block) {
try {
Location center = block.getLocation().clone().add(0.5D, 0.5D, 0.5D);
World world = block.getWorld();
world.spawnParticle(Particle.ENCHANT, center, 24, 0.45D, 0.45D, 0.45D, 0.18D);
world.spawnParticle(Particle.END_ROD, center, 8, 0.25D, 0.25D, 0.25D, 0.035D);
world.playSound(center, Sound.BLOCK_ENCHANTMENT_TABLE_USE, 0.55F, 1.35F);
world.playSound(center, Sound.BLOCK_AMETHYST_BLOCK_CHIME, 0.4F, 0.8F);
} catch (Throwable error) {
reportEffectFailure(error);
}
}
void erode(
Location source,
BlockData visualData,
int erosionOrder,
int processed,
int blocksPerPulse,
int effectStride,
int totalBlocks
) {
if (processed % effectStride != 0 || source.getWorld() == null) {
return;
}
try {
World world = source.getWorld();
world.spawnParticle(Particle.BLOCK, source, 5, 0.3D, 0.3D, 0.3D, 0.04D, visualData);
world.spawnParticle(Particle.ENCHANT, source, 3, 0.28D, 0.28D, 0.28D, 0.12D);
if (processed % blocksPerPulse == 0) {
double progress = totalBlocks <= 1 ? 1D : (double) erosionOrder / (double) (totalBlocks - 1);
float pitch = (float) Math.min(1.95D, 0.65D + (progress * 1.25D));
world.playSound(source, Sound.BLOCK_AMETHYST_BLOCK_CHIME, 0.22F, pitch);
}
} catch (Throwable error) {
reportEffectFailure(error);
}
}
boolean routeDrop(Object drop) {
if (!(drop instanceof ItemStack item)) {
return false;
}
if (item.getType() == Material.AIR || item.getAmount() <= 0) {
return true;
}
return enqueue(new PendingDrop(item.clone(), 0));
}
boolean routeExperience(int experience) {
return experience <= 0 || enqueue(new PendingDrop(null, experience));
}
void finish() {
completed.set(true);
if (!pendingDrops.isEmpty()) {
scheduleFlush();
}
}
private boolean enqueue(PendingDrop drop) {
pendingDrops.add(drop);
if (scheduleFlush()) {
return true;
}
return !pendingDrops.remove(drop);
}
private boolean scheduleFlush() {
if (pendingDrops.isEmpty() || !flushScheduled.compareAndSet(false, true)) {
return true;
}
fallbackScheduled.set(false);
Runnable retired = () -> {
if (!scheduleFallbackFlush()) {
flushScheduled.set(false);
IrisLogging.error("Unable to deliver queued Iris tree-feller drops after the player retired.");
}
};
if (J.runEntity(player, this::flushAtPlayer, 1, retired)) {
return true;
}
return scheduleFallbackFlush();
}
private boolean scheduleFallbackFlush() {
if (!fallbackScheduled.compareAndSet(false, true)) {
return true;
}
Location fallback = fallbackLocation.clone();
if (J.runAt(fallback, () -> flushAtFallback(fallback), 1)) {
return true;
}
fallbackScheduled.set(false);
flushScheduled.set(false);
return false;
}
private void flushAtPlayer() {
if (!player.isOnline() || !player.getWorld().equals(sourceWorld)) {
if (!scheduleFallbackFlush()) {
IrisLogging.error("Unable to deliver queued Iris tree-feller drops at their fallback location.");
}
return;
}
Location target = player.getLocation().clone().add(0D, 0.15D, 0D);
fallbackLocation = target.clone();
deliverBatch(target, drainPendingDrops(), true);
completeFlush();
}
private void flushAtFallback(Location fallback) {
deliverBatch(fallback, drainPendingDrops(), false);
completeFlush();
}
private DropBatch drainPendingDrops() {
List<ItemStack> items = new ArrayList<>();
long experience = 0L;
PendingDrop pending;
while ((pending = pendingDrops.poll()) != null) {
if (pending.item() != null) {
items.add(pending.item());
}
experience = Math.min(Integer.MAX_VALUE, experience + pending.experience());
}
return new DropBatch(consolidateDrops(items), (int) experience);
}
private void deliverBatch(Location target, DropBatch batch, boolean atPlayer) {
World world = target.getWorld();
if (world == null) {
requeueBatch(batch);
return;
}
int deliveredItems = 0;
for (int index = 0; index < batch.items().size(); index++) {
ItemStack item = batch.items().get(index);
Item dropped;
try {
dropped = world.dropItem(target, item);
} catch (Throwable error) {
requeueUndelivered(batch, index);
reportDeliveryFailure(error);
return;
}
deliveredItems++;
try {
dropped.setVelocity(new Vector(0D, 0.08D, 0D));
} catch (Throwable error) {
reportEffectFailure(error);
}
}
if (batch.experience() > 0) {
try {
ExperienceOrb orb = world.spawn(target.clone().add(0D, 0.35D, 0D), ExperienceOrb.class);
orb.setExperience(batch.experience());
} catch (Throwable error) {
pendingDrops.add(new PendingDrop(null, batch.experience()));
reportDeliveryFailure(error);
}
}
try {
int enchantParticles = Math.min(32, 6 + (deliveredItems * 2));
Location effectLocation = target.clone().add(0D, 0.35D, 0D);
world.spawnParticle(Particle.ENCHANT, effectLocation, enchantParticles, 0.35D, 0.25D, 0.35D, 0.15D);
world.spawnParticle(Particle.END_ROD, effectLocation, Math.min(8, 2 + deliveredItems), 0.2D, 0.2D, 0.2D, 0.025D);
int deliveryPulse = deliveryPulses.incrementAndGet();
if (atPlayer && (deliveryPulse == 1 || deliveryPulse % 4 == 0 || completed.get())) {
world.playSound(target, Sound.BLOCK_AMETHYST_BLOCK_CHIME, 0.2F, 1.65F);
}
} catch (Throwable error) {
reportEffectFailure(error);
}
}
private void completeFlush() {
fallbackScheduled.set(false);
flushScheduled.set(false);
if (!pendingDrops.isEmpty() && !scheduleFlush()) {
IrisLogging.error("Unable to schedule the remaining Iris tree-feller drops.");
}
}
private void requeueBatch(DropBatch batch) {
requeueUndelivered(batch, 0);
}
private void requeueUndelivered(DropBatch batch, int firstUndeliveredItem) {
for (int index = firstUndeliveredItem; index < batch.items().size(); index++) {
pendingDrops.add(new PendingDrop(batch.items().get(index), 0));
}
if (batch.experience() > 0) {
pendingDrops.add(new PendingDrop(null, batch.experience()));
}
}
private void reportEffectFailure(Throwable error) {
if (effectFailureReported.compareAndSet(false, true)) {
IrisLogging.reportError("Failed to render an Iris tree-feller effect.", error);
}
}
private void reportDeliveryFailure(Throwable error) {
if (deliveryFailureReported.compareAndSet(false, true)) {
IrisLogging.reportError("Failed to deliver an Iris tree-feller drop; it remains queued for retry.", error);
}
}
private static void mergeDrop(List<ItemStack> consolidated, ItemStack source) {
if (source == null || source.getType() == Material.AIR || source.getAmount() <= 0) {
return;
}
int remaining = source.getAmount();
for (ItemStack existing : consolidated) {
if (!existing.isSimilar(source) || existing.getAmount() >= existing.getMaxStackSize()) {
continue;
}
int transferable = Math.min(remaining, existing.getMaxStackSize() - existing.getAmount());
existing.setAmount(existing.getAmount() + transferable);
remaining -= transferable;
if (remaining == 0) {
return;
}
}
int maximumStackSize = Math.max(1, source.getMaxStackSize());
while (remaining > 0) {
ItemStack stack = source.clone();
stack.setAmount(Math.min(remaining, maximumStackSize));
consolidated.add(stack);
remaining -= stack.getAmount();
}
}
private record PendingDrop(ItemStack item, int experience) {
}
private record DropBatch(List<ItemStack> items, int experience) {
}
}
@@ -8,6 +8,10 @@ load: STARTUP
authors: [ cyberpwn, NextdoorPsycho, Vatuu ]
website: volmit.com
description: More than a Dimension!
permissions:
iris.treefeller:
description: Allows survival players to fell Iris-managed trees with an axe.
default: op
dependencies:
server:
PlaceholderAPI:
@@ -22,7 +22,11 @@ loadbefore: [ Multiverse-Core ]
authors: [ cyberpwn, NextdoorPsycho, Vatuu ]
website: volmit.com
description: More than a Dimension!
commands:
iris:
aliases: [ ir, irs ]
commands:
iris:
aliases: [ ir, irs ]
permissions:
iris.treefeller:
description: Allows survival players to fell Iris-managed trees with an axe.
default: op
api-version: '${apiVersion}'
@@ -2,6 +2,8 @@ package art.arcane.iris;
import org.bukkit.plugin.PluginDescriptionFile;
import org.bukkit.plugin.PluginLoadOrder;
import org.bukkit.permissions.Permission;
import org.bukkit.permissions.PermissionDefault;
import org.junit.Test;
import java.io.InputStream;
@@ -54,6 +56,9 @@ public class PaperPluginMetadataTest {
assertTrue(metadata.contains("folia-supported: true"));
assertTrue(metadata.contains("load: STARTUP"));
assertFalse(metadata.contains("commands:"));
assertTrue(metadata.contains("permissions:\n iris.treefeller:\n"
+ " description: Allows survival players to fell Iris-managed trees with an axe.\n"
+ " default: op"));
for (String pluginId : JOINED_PLUGIN_IDS) {
assertTrue(metadata.contains(optionalDependencyBlock(pluginId, "BEFORE", true)));
}
@@ -76,6 +81,13 @@ public class PaperPluginMetadataTest {
assertEquals(List.of("ir", "irs"), commands.get("iris").get("aliases"));
assertEquals(BUKKIT_SOFT_DEPEND_IDS, metadata.getSoftDepend());
assertEquals(List.of("Multiverse-Core"), metadata.getLoadBeforePlugins());
Permission treeFeller = metadata.getPermissions().stream()
.filter(permission -> "iris.treefeller".equals(permission.getName()))
.findFirst()
.orElse(null);
assertNotNull(treeFeller);
assertEquals("Allows survival players to fell Iris-managed trees with an axe.", treeFeller.getDescription());
assertEquals(PermissionDefault.OP, treeFeller.getDefault());
}
@Test
@@ -0,0 +1,28 @@
package art.arcane.iris.api.tree;
import org.junit.Test;
import static org.junit.Assert.assertEquals;
public class TreeFellerOptionsTest {
@Test
public void factoriesExposeStableAccessModes() {
TreeFellerOptions standalone = TreeFellerOptions.standalone();
TreeFellerOptions override = TreeFellerOptions.integrationOverride(75, TreeFellerRunHooks.NONE);
assertEquals(TreeFellerAccess.STANDALONE, standalone.access());
assertEquals(0, standalone.durabilityPreservationChance());
assertEquals(TreeFellerAccess.INTEGRATION_OVERRIDE, override.access());
assertEquals(75, override.durabilityPreservationChance());
}
@Test(expected = IllegalArgumentException.class)
public void chanceBelowRangeIsRejected() {
TreeFellerOptions.integrationOverride(-1, TreeFellerRunHooks.NONE);
}
@Test(expected = IllegalArgumentException.class)
public void chanceAboveRangeIsRejected() {
TreeFellerOptions.integrationOverride(101, TreeFellerRunHooks.NONE);
}
}
@@ -0,0 +1,83 @@
package art.arcane.iris.core.runtime;
import org.junit.Test;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
public class BukkitEngineLifecycleContractTest {
@Test
public void closeFutureIsPublishedBeforeCloseWorkIsScheduled() throws IOException {
String source = Files.readString(Path.of(System.getProperty("iris.bukkitChunkGeneratorSource")));
String closeAsync = method(source, "public CompletableFuture<Void> closeAsync()");
assertBefore(closeAsync, "closeFuture.compareAndSet(null, future)", "withExclusiveControlFuture(");
assertTrue(closeAsync.contains("while (!closeFuture.compareAndSet(null, future))"));
assertTrue(closeAsync.contains("operation.whenComplete("));
assertFalse(closeAsync.contains("!existing.isDone()"));
String baseHeight = method(source, "public int getBaseHeight(");
assertTrue(baseHeight.contains("currentEngine.acquireGenerationLease(\"bukkit_base_height\")"));
assertTrue(baseHeight.contains("IrisContext.open(currentEngine, lease.sessionId(), null)"));
assertTrue(baseHeight.contains("catch (GenerationSessionException e)"));
String generation = method(source, "public void generateNoise(");
assertTrue(generation.contains("throw new IllegalStateException"));
assertTrue(generation.contains("engine.acquireGenerationLease(\"bukkit_terrain_stage\")"));
assertTrue(generation.contains("IrisContext.open(engine, lease.sessionId(), null)"));
assertBefore(generation, "engine.acquireGenerationLease(\"bukkit_terrain_stage\")", "blocks.apply()");
assertFalse(generation.contains("RED_GLAZED_TERRACOTTA"));
}
@Test
public void targetedPregeneratorShutdownChecksWorldIdentity() throws IOException {
String jobSource = Files.readString(Path.of(System.getProperty("iris.pregeneratorJobSource")));
String targetedShutdown = method(jobSource,
"public static boolean shutdownInstanceForWorld(String worldIdentity)");
assertBefore(targetedShutdown, "inst.targetsWorldIdentity(worldIdentity)",
"shutdownAndWait(inst, WORLD_SHUTDOWN_TIMEOUT_MILLIS)");
String waitedShutdown = method(jobSource,
"private static boolean shutdownAndWait(PregeneratorJob inst, long timeoutMs)");
assertBefore(waitedShutdown, "inst.worker.interrupt()", "inst.worker.join(");
assertTrue(waitedShutdown.contains("inst.worker.isAlive()"));
String hooksSource = Files.readString(Path.of(System.getProperty("iris.bukkitEnginePlatformHooksSource")));
String hookShutdown = method(hooksSource, "public void shutdownPregenerator(Engine engine)");
assertTrue(hookShutdown.contains("PregeneratorJob.shutdownInstanceForWorld(world.identity());"));
assertFalse(hookShutdown.contains("PregeneratorJob.shutdownInstance();"));
}
private static void assertBefore(String source, String first, String second) {
int firstIndex = source.indexOf(first);
int secondIndex = source.indexOf(second);
assertTrue("Missing source contract token: " + first, firstIndex >= 0);
assertTrue("Missing source contract token: " + second, secondIndex >= 0);
assertTrue(first + " must occur before " + second, firstIndex < secondIndex);
}
private static String method(String source, String signature) {
int start = source.indexOf(signature);
assertTrue("Missing source contract signature: " + signature, start >= 0);
int openBrace = source.indexOf('{', start);
assertTrue("Missing source contract method body: " + signature, openBrace >= 0);
int depth = 0;
for (int index = openBrace; index < source.length(); index++) {
char current = source.charAt(index);
if (current == '{') {
depth++;
} else if (current == '}') {
depth--;
if (depth == 0) {
return source.substring(start, index + 1);
}
}
}
throw new IllegalArgumentException("Unclosed source contract method: " + signature);
}
}
@@ -0,0 +1,105 @@
package art.arcane.iris.core.service;
import art.arcane.iris.engine.framework.EngineAssignedWorldManager;
import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority;
import org.bukkit.event.world.WorldUnloadEvent;
import org.junit.Test;
import java.io.IOException;
import java.lang.reflect.Method;
import java.nio.file.Files;
import java.nio.file.Path;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
public class IrisEngineLifecycleContractTest {
@Test
public void engineServiceExclusivelyOwnsWorldUnload() throws NoSuchMethodException {
assertMonitorUnloadHandler(IrisEngineSVC.class.getMethod("onWorldUnload", WorldUnloadEvent.class));
for (Method method : EngineAssignedWorldManager.class.getDeclaredMethods()) {
boolean handlesWorldUnload = method.getParameterCount() == 1
&& method.getParameterTypes()[0] == WorldUnloadEvent.class
&& method.isAnnotationPresent(EventHandler.class);
assertFalse("EngineAssignedWorldManager must defer world unload ownership to IrisEngineSVC.", handlesWorldUnload);
}
}
@Test
public void registrationIdentityConflictsByWorldIdentityOrFolder() {
Path sharedFolder = Path.of("build", "worlds", "shared");
IrisEngineSVC.RegistrationIdentity first =
new IrisEngineSVC.RegistrationIdentity("minecraft:first", sharedFolder);
IrisEngineSVC.RegistrationIdentity sameIdentity =
new IrisEngineSVC.RegistrationIdentity("minecraft:first", Path.of("build", "worlds", "other"));
IrisEngineSVC.RegistrationIdentity sameFolder =
new IrisEngineSVC.RegistrationIdentity("minecraft:other", sharedFolder);
IrisEngineSVC.RegistrationIdentity distinct =
new IrisEngineSVC.RegistrationIdentity("minecraft:distinct", Path.of("build", "worlds", "distinct"));
assertTrue(first.conflictsWith(sameIdentity));
assertTrue(first.conflictsWith(sameFolder));
assertFalse(first.conflictsWith(distinct));
}
@Test
public void registrationRetryWaitsAsynchronouslyForClose() throws IOException {
String source = Files.readString(Path.of(System.getProperty("iris.engineSvcSource")));
String add = method(source, "private void add(World world)");
String remove = method(source, "private void remove(World world)");
String completeClose = method(source, "private void completeClose(");
String retry = method(source, "private void retryRegistrationAfterClose(");
String invokeClose = method(source, "private CompletableFuture<Void> invokeGeneratorClose()");
assertBefore(add, "findClosingGenerator(registrationIdentity)", "new Registered(");
assertBefore(remove, "reserveClose(registered)", "startClose(registered, closing)");
assertBefore(completeClose, "if (failure == null)", "closingGenerators.remove(closing)");
assertBefore(completeClose, "closingGenerators.remove(closing)", "closing.completion().complete(null)");
assertBefore(completeClose, "} else {", "closing.completion().completeExceptionally(failure)");
assertTrue(retry.contains("completion.whenComplete("));
assertTrue(retry.contains("J.s(() -> add(world), 1);"));
assertBefore(retry, "if (failure != null)", "J.s(() -> add(world), 1);");
assertFalse(retry.contains("completion.get("));
assertFalse(retry.contains("completion.join("));
assertTrue(invokeClose.contains("CompletableFuture.failedFuture("));
assertTrue(invokeClose.contains("future.whenComplete("));
}
private static void assertMonitorUnloadHandler(Method method) {
EventHandler eventHandler = method.getAnnotation(EventHandler.class);
assertNotNull(eventHandler);
assertEquals(EventPriority.MONITOR, eventHandler.priority());
assertTrue(eventHandler.ignoreCancelled());
}
private static void assertBefore(String source, String first, String second) {
int firstIndex = source.indexOf(first);
int secondIndex = source.indexOf(second);
assertTrue("Missing source contract token: " + first, firstIndex >= 0);
assertTrue("Missing source contract token: " + second, secondIndex >= 0);
assertTrue(first + " must occur before " + second, firstIndex < secondIndex);
}
private static String method(String source, String signature) {
int start = source.indexOf(signature);
assertTrue("Missing source contract signature: " + signature, start >= 0);
int openBrace = source.indexOf('{', start);
assertTrue("Missing source contract method body: " + signature, openBrace >= 0);
int depth = 0;
for (int index = openBrace; index < source.length(); index++) {
char current = source.charAt(index);
if (current == '{') {
depth++;
} else if (current == '}') {
depth--;
if (depth == 0) {
return source.substring(start, index + 1);
}
}
}
throw new IllegalArgumentException("Unclosed source contract method: " + signature);
}
}
@@ -1,24 +0,0 @@
package art.arcane.iris.core.service;
import org.junit.Test;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
public class IrisEngineSVCTest {
@Test
public void maintenanceSkipsReductionWhenPregenDoesNotTargetWorld() {
assertTrue(IrisEngineSVC.shouldSkipMantleReductionForMaintenance(true, false));
}
@Test
public void maintenanceDoesNotSkipReductionForActivePregenWorld() {
assertFalse(IrisEngineSVC.shouldSkipMantleReductionForMaintenance(true, true));
}
@Test
public void noMaintenanceNeverSkipsReduction() {
assertFalse(IrisEngineSVC.shouldSkipMantleReductionForMaintenance(false, false));
assertFalse(IrisEngineSVC.shouldSkipMantleReductionForMaintenance(false, true));
}
}
@@ -0,0 +1,174 @@
package art.arcane.iris.core.service;
import art.arcane.iris.engine.framework.EngineTelemetrySnapshot;
import art.arcane.volmlib.integration.IntegrationMetricGroup;
import art.arcane.volmlib.integration.IntegrationMetricSample;
import art.arcane.volmlib.integration.IntegrationMetricSchema;
import org.junit.Test;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
public class IrisIntegrationServiceTest {
@Test
public void unavailableTelemetryDoesNotPublishFalseZeroes() {
IrisIntegrationService service = new IrisIntegrationService(() -> IrisTelemetrySnapshot.EMPTY);
Map<String, IntegrationMetricSample> samples = service.sampleMetrics(Set.of(
IntegrationMetricSchema.IRIS_WORLD_COUNT,
IntegrationMetricSchema.IRIS_PREGEN_QUEUE,
IntegrationMetricSchema.IRIS_GENERATION_TOTAL_MS));
for (IntegrationMetricSample sample : samples.values()) {
assertFalse(sample.available());
assertEquals("telemetry-not-ready", sample.message());
}
assertTrue(service.metricGroups().isEmpty());
}
@Test
public void publishesAggregateAndPerWorldMetricsFromOneImmutableSnapshot() {
long now = System.currentTimeMillis();
EngineTelemetrySnapshot first = world("minecraft:overworld", "world", 12L, 20L, 4D, now);
EngineTelemetrySnapshot second = world("minecraft:the_nether", "world_nether", 8L, 30L, 2D, now);
IrisTelemetrySnapshot snapshot = telemetry(
List.of(first, second),
new IrisTelemetrySnapshot.PregenSnapshot(
true,
first.worldIdentity(),
first.worldName(),
false,
50D,
100L,
200L,
100L,
8D,
2_000L,
5_000L,
1L
),
now
);
IrisIntegrationService service = new IrisIntegrationService(() -> snapshot);
Map<String, IntegrationMetricSample> samples = service.sampleMetrics(Set.of(
IntegrationMetricSchema.IRIS_WORLD_COUNT,
IntegrationMetricSchema.IRIS_ENGINE_PENDING_REGISTRATIONS,
IntegrationMetricSchema.IRIS_LOADED_CHUNKS,
IntegrationMetricSchema.IRIS_CHUNKS_GENERATED_TOTAL,
IntegrationMetricSchema.IRIS_PREGEN_THROUGHPUT,
IntegrationMetricSchema.IRIS_GENERATION_TOTAL_MS,
"iris.unsupported"
));
assertEquals(2D, value(samples, IntegrationMetricSchema.IRIS_WORLD_COUNT), 0D);
assertEquals(3D, value(samples, IntegrationMetricSchema.IRIS_ENGINE_PENDING_REGISTRATIONS), 0D);
assertEquals(20D, value(samples, IntegrationMetricSchema.IRIS_LOADED_CHUNKS), 0D);
assertEquals(50D, value(samples, IntegrationMetricSchema.IRIS_CHUNKS_GENERATED_TOTAL), 0D);
assertEquals(8D, value(samples, IntegrationMetricSchema.IRIS_PREGEN_THROUGHPUT), 0D);
assertEquals(4D, value(samples, IntegrationMetricSchema.IRIS_GENERATION_TOTAL_MS), 0D);
assertFalse(samples.get("iris.unsupported").available());
assertEquals("unsupported-key", samples.get("iris.unsupported").message());
List<IntegrationMetricGroup> groups = service.metricGroups();
assertEquals(2, groups.size());
IntegrationMetricGroup overworld = groups.get(0);
IntegrationMetricGroup nether = groups.get(1);
assertEquals("minecraft:overworld", overworld.scopeId());
assertEquals(1D, value(overworld.samples(), IntegrationMetricSchema.IRIS_PREGEN_ACTIVE), 0D);
assertEquals(8D, value(overworld.samples(), IntegrationMetricSchema.IRIS_PREGEN_THROUGHPUT), 0D);
assertEquals("minecraft:the_nether", nether.scopeId());
assertEquals(0D, value(nether.samples(), IntegrationMetricSchema.IRIS_PREGEN_ACTIVE), 0D);
assertFalse(nether.samples().get(IntegrationMetricSchema.IRIS_PREGEN_QUEUE).available());
}
@Test
public void publishesEveryManagedWorldWithoutAWorldCountCap() {
long now = System.currentTimeMillis();
List<EngineTelemetrySnapshot> worlds = new ArrayList<>();
for (int index = 0; index < 48; index++) {
worlds.add(world("iris:world_" + index, "world_" + index, index, index, index, now));
}
IrisIntegrationService service = new IrisIntegrationService(
() -> telemetry(worlds, IrisTelemetrySnapshot.PregenSnapshot.INACTIVE, now)
);
List<IntegrationMetricGroup> groups = service.metricGroups();
assertEquals(48, groups.size());
assertEquals(48, groups.stream().map(IntegrationMetricGroup::scopeId).distinct().count());
}
private static IrisTelemetrySnapshot telemetry(
List<EngineTelemetrySnapshot> worlds,
IrisTelemetrySnapshot.PregenSnapshot pregenerator,
long now
) {
IrisTelemetrySnapshot.CacheBucket resource = new IrisTelemetrySnapshot.CacheBucket(2, 10L, 100L);
IrisTelemetrySnapshot.CacheSnapshot caches = new IrisTelemetrySnapshot.CacheSnapshot(
resource,
resource,
IrisTelemetrySnapshot.CacheBucket.EMPTY,
IrisTelemetrySnapshot.CacheBucket.EMPTY,
IrisTelemetrySnapshot.CacheBucket.EMPTY
);
return new IrisTelemetrySnapshot(
now,
worlds,
EngineTelemetrySnapshot.aggregate(worlds),
1,
4,
0,
3,
0.5D,
0.1D,
caches,
pregenerator
);
}
private static EngineTelemetrySnapshot world(
String identity,
String name,
long loadedChunks,
long generatedTotal,
double generationMs,
long now
) {
return new EngineTelemetrySnapshot(
now,
identity,
name,
"dimension",
true,
false,
false,
false,
loadedChunks,
3L,
0.25D,
generatedTotal,
generatedTotal,
2D,
4L,
2,
1,
1L,
4L,
1L,
10D,
Map.of("total", generationMs)
);
}
private static double value(Map<String, IntegrationMetricSample> samples, String key) {
IntegrationMetricSample sample = samples.get(key);
assertTrue(sample.available());
return sample.valueOr(-1D);
}
}
@@ -0,0 +1,60 @@
package art.arcane.iris.core.service;
import art.arcane.iris.core.service.tree.TreeMarkerTraversal;
import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority;
import org.bukkit.event.player.PlayerItemHeldEvent;
import org.bukkit.event.player.PlayerSwapHandItemsEvent;
import org.bukkit.event.player.PlayerToggleSneakEvent;
import org.junit.Test;
import java.lang.reflect.Method;
import java.util.List;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
public class TreeFellerEventOrderTest {
@Test
public void standaloneIntentPrecedesMonitorFinalization() throws Exception {
Method request = TreeFellerSVC.class.getMethod("requestStandalone", org.bukkit.event.block.BlockBreakEvent.class);
Method finalize = TreeFellerSVC.class.getMethod("finalizeBreak", org.bukkit.event.block.BlockBreakEvent.class);
EventHandler requestHandler = request.getAnnotation(EventHandler.class);
EventHandler finalizeHandler = finalize.getAnnotation(EventHandler.class);
assertEquals(EventPriority.HIGHEST, requestHandler.priority());
assertTrue(requestHandler.ignoreCancelled());
assertEquals(EventPriority.MONITOR, finalizeHandler.priority());
assertFalse(finalizeHandler.ignoreCancelled());
}
@Test
public void incompleteDiscoveryFallsOnlyTheTrigger() {
TreeMarkerTraversal.Position trigger = new TreeMarkerTraversal.Position(4, 80, -2);
TreeMarkerTraversal.Discovery incomplete = new TreeMarkerTraversal.Discovery(
List.of(trigger, new TreeMarkerTraversal.Position(4, 81, -2)),
false
);
assertEquals(List.of(trigger), TreeFellerSVC.positionsForFelling(incomplete, trigger));
}
@Test
public void playerControlChangesHaltRunsAtMonitor() throws Exception {
Method sneak = TreeFellerSVC.class.getMethod("haltWhenSneakingStops", PlayerToggleSneakEvent.class);
Method held = TreeFellerSVC.class.getMethod("haltWhenHeldSlotChanges", PlayerItemHeldEvent.class);
Method hands = TreeFellerSVC.class.getMethod("haltWhenHandsSwap", PlayerSwapHandItemsEvent.class);
assertMonitorCancellationHandler(sneak);
assertMonitorCancellationHandler(held);
assertMonitorCancellationHandler(hands);
}
private void assertMonitorCancellationHandler(Method method) {
EventHandler handler = method.getAnnotation(EventHandler.class);
assertEquals(EventPriority.MONITOR, handler.priority());
assertTrue(handler.ignoreCancelled());
}
}
@@ -0,0 +1,27 @@
package art.arcane.iris.core.service;
import org.junit.Test;
import static org.junit.Assert.assertEquals;
public class TreeFellerPresentationTest {
@Test
public void pulseSizeKeepsSmallTreesReadableAndLargeTreesBounded() {
assertEquals(4, TreeFellerPresentation.blocksPerPulse(1));
assertEquals(4, TreeFellerPresentation.blocksPerPulse(240));
assertEquals(10, TreeFellerPresentation.blocksPerPulse(600));
assertEquals(64, TreeFellerPresentation.blocksPerPulse(100_000));
}
@Test
public void effectSamplingStaysBoundedPerPulse() {
assertEquals(1, TreeFellerPresentation.effectStride(4));
assertEquals(1, TreeFellerPresentation.effectStride(16));
assertEquals(2, TreeFellerPresentation.effectStride(17));
assertEquals(2, TreeFellerPresentation.effectStride(31));
assertEquals(2, TreeFellerPresentation.effectStride(32));
assertEquals(3, TreeFellerPresentation.effectStride(33));
assertEquals(4, TreeFellerPresentation.effectStride(64));
}
}
+2 -1
View File
@@ -120,7 +120,8 @@ dependencies {
libs.fabricApi.eventsInteraction,
libs.fabricApi.networking,
libs.fabricApi.rendering,
libs.fabricApi.keyMapping
libs.fabricApi.keyMapping,
libs.fabricApi.permission
]
fabricApi.each { Object notation ->
add('jij', notation)
Binary file not shown.
Binary file not shown.
Binary file not shown.
+98 -8
View File
@@ -1,5 +1,95 @@
[17:42:11] [Test worker/INFO]: Iris registered custom content provider 'iris_deferred_test'
[17:42:11] [Test worker/ERROR]: Iris service rollback failed for art.arcane.iris.modded.ModdedServiceManagerTest$SecondService
[19:42:28] [Test worker/INFO]: Iris registered custom content provider 'iris_deferred_test'
[19:42:28] [Test worker/ERROR]: Iris service onDisable failed for art.arcane.iris.modded.ModdedServiceManagerTest$SecondService
java.lang.RuntimeException: second disable failed
at art.arcane.iris.modded.ModdedServiceManagerTest.disableAttemptsEveryServiceInReverseOrderAndAggregatesFailures(ModdedServiceManagerTest.java:55)
at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:104)
at java.base/java.lang.reflect.Method.invoke(Method.java:565)
at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:59)
at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12)
at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:56)
at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17)
at org.junit.runners.ParentRunner$3.evaluate(ParentRunner.java:306)
at org.junit.runners.BlockJUnit4ClassRunner$1.evaluate(BlockJUnit4ClassRunner.java:100)
at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:366)
at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:103)
at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:63)
at org.junit.runners.ParentRunner$4.run(ParentRunner.java:331)
at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:79)
at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:329)
at org.junit.runners.ParentRunner.access$100(ParentRunner.java:66)
at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:293)
at org.junit.runners.ParentRunner$3.evaluate(ParentRunner.java:306)
at org.junit.runners.ParentRunner.run(ParentRunner.java:413)
at org.junit.runner.JUnitCore.run(JUnitCore.java:137)
at org.junit.runner.JUnitCore.run(JUnitCore.java:115)
at org.gradle.api.internal.tasks.testing.junit.JUnitTestExecutor.runRequest(JUnitTestExecutor.java:175)
at org.gradle.api.internal.tasks.testing.junit.JUnitTestExecutor.accept(JUnitTestExecutor.java:84)
at org.gradle.api.internal.tasks.testing.junit.JUnitTestExecutor.accept(JUnitTestExecutor.java:47)
at org.gradle.api.internal.tasks.testing.junit.AbstractJUnitTestDefinitionProcessor.processTestDefinition(AbstractJUnitTestDefinitionProcessor.java:65)
at org.gradle.api.internal.tasks.testing.SuiteTestDefinitionProcessor.processTestDefinition(SuiteTestDefinitionProcessor.java:53)
at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:104)
at java.base/java.lang.reflect.Method.invoke(Method.java:565)
at org.gradle.internal.dispatch.MethodInvocation.invokeOn(MethodInvocation.java:77)
at org.gradle.internal.dispatch.ReflectionDispatch.dispatch(ReflectionDispatch.java:28)
at org.gradle.internal.dispatch.ReflectionDispatch.dispatch(ReflectionDispatch.java:19)
at org.gradle.internal.dispatch.ContextClassLoaderDispatch.dispatch(ContextClassLoaderDispatch.java:33)
at org.gradle.internal.dispatch.ProxyDispatchAdapter$DispatchingInvocationHandler.invoke(ProxyDispatchAdapter.java:88)
at jdk.proxy1/jdk.proxy1.$Proxy4.processTestDefinition(Unknown Source)
at org.gradle.api.internal.tasks.testing.worker.TestWorker$2.run(TestWorker.java:178)
at org.gradle.api.internal.tasks.testing.worker.TestWorker.executeAndMaintainThreadName(TestWorker.java:126)
at org.gradle.api.internal.tasks.testing.worker.TestWorker.execute(TestWorker.java:103)
at org.gradle.api.internal.tasks.testing.worker.TestWorker.execute(TestWorker.java:63)
at org.gradle.process.internal.worker.child.ActionExecutionWorker.execute(ActionExecutionWorker.java:56)
at org.gradle.process.internal.worker.child.SystemApplicationClassLoaderWorker.call(SystemApplicationClassLoaderWorker.java:122)
at org.gradle.process.internal.worker.child.SystemApplicationClassLoaderWorker.call(SystemApplicationClassLoaderWorker.java:72)
at worker.org.gradle.process.internal.worker.GradleWorkerMain.run(GradleWorkerMain.java:69)
at worker.org.gradle.process.internal.worker.GradleWorkerMain.main(GradleWorkerMain.java:74)
[19:42:28] [Test worker/ERROR]: Iris service onDisable failed for art.arcane.iris.modded.ModdedServiceManagerTest$FirstService
java.lang.RuntimeException: first disable failed
at art.arcane.iris.modded.ModdedServiceManagerTest.disableAttemptsEveryServiceInReverseOrderAndAggregatesFailures(ModdedServiceManagerTest.java:54)
at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:104)
at java.base/java.lang.reflect.Method.invoke(Method.java:565)
at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:59)
at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12)
at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:56)
at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17)
at org.junit.runners.ParentRunner$3.evaluate(ParentRunner.java:306)
at org.junit.runners.BlockJUnit4ClassRunner$1.evaluate(BlockJUnit4ClassRunner.java:100)
at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:366)
at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:103)
at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:63)
at org.junit.runners.ParentRunner$4.run(ParentRunner.java:331)
at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:79)
at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:329)
at org.junit.runners.ParentRunner.access$100(ParentRunner.java:66)
at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:293)
at org.junit.runners.ParentRunner$3.evaluate(ParentRunner.java:306)
at org.junit.runners.ParentRunner.run(ParentRunner.java:413)
at org.junit.runner.JUnitCore.run(JUnitCore.java:137)
at org.junit.runner.JUnitCore.run(JUnitCore.java:115)
at org.gradle.api.internal.tasks.testing.junit.JUnitTestExecutor.runRequest(JUnitTestExecutor.java:175)
at org.gradle.api.internal.tasks.testing.junit.JUnitTestExecutor.accept(JUnitTestExecutor.java:84)
at org.gradle.api.internal.tasks.testing.junit.JUnitTestExecutor.accept(JUnitTestExecutor.java:47)
at org.gradle.api.internal.tasks.testing.junit.AbstractJUnitTestDefinitionProcessor.processTestDefinition(AbstractJUnitTestDefinitionProcessor.java:65)
at org.gradle.api.internal.tasks.testing.SuiteTestDefinitionProcessor.processTestDefinition(SuiteTestDefinitionProcessor.java:53)
at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:104)
at java.base/java.lang.reflect.Method.invoke(Method.java:565)
at org.gradle.internal.dispatch.MethodInvocation.invokeOn(MethodInvocation.java:77)
at org.gradle.internal.dispatch.ReflectionDispatch.dispatch(ReflectionDispatch.java:28)
at org.gradle.internal.dispatch.ReflectionDispatch.dispatch(ReflectionDispatch.java:19)
at org.gradle.internal.dispatch.ContextClassLoaderDispatch.dispatch(ContextClassLoaderDispatch.java:33)
at org.gradle.internal.dispatch.ProxyDispatchAdapter$DispatchingInvocationHandler.invoke(ProxyDispatchAdapter.java:88)
at jdk.proxy1/jdk.proxy1.$Proxy4.processTestDefinition(Unknown Source)
at org.gradle.api.internal.tasks.testing.worker.TestWorker$2.run(TestWorker.java:178)
at org.gradle.api.internal.tasks.testing.worker.TestWorker.executeAndMaintainThreadName(TestWorker.java:126)
at org.gradle.api.internal.tasks.testing.worker.TestWorker.execute(TestWorker.java:103)
at org.gradle.api.internal.tasks.testing.worker.TestWorker.execute(TestWorker.java:63)
at org.gradle.process.internal.worker.child.ActionExecutionWorker.execute(ActionExecutionWorker.java:56)
at org.gradle.process.internal.worker.child.SystemApplicationClassLoaderWorker.call(SystemApplicationClassLoaderWorker.java:122)
at org.gradle.process.internal.worker.child.SystemApplicationClassLoaderWorker.call(SystemApplicationClassLoaderWorker.java:72)
at worker.org.gradle.process.internal.worker.GradleWorkerMain.run(GradleWorkerMain.java:69)
at worker.org.gradle.process.internal.worker.GradleWorkerMain.main(GradleWorkerMain.java:74)
[19:42:28] [Test worker/ERROR]: Iris service rollback failed for art.arcane.iris.modded.ModdedServiceManagerTest$SecondService
java.lang.RuntimeException: cleanup failed
at art.arcane.iris.modded.ModdedServiceManagerTest.rollsBackEnabledServicesAndPreservesCleanupFailures(ModdedServiceManagerTest.java:32)
at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:104)
@@ -44,7 +134,7 @@ java.lang.RuntimeException: cleanup failed
at org.gradle.process.internal.worker.child.SystemApplicationClassLoaderWorker.call(SystemApplicationClassLoaderWorker.java:72)
at worker.org.gradle.process.internal.worker.GradleWorkerMain.run(GradleWorkerMain.java:69)
at worker.org.gradle.process.internal.worker.GradleWorkerMain.main(GradleWorkerMain.java:74)
[17:42:11] [Test worker/ERROR]: Iris service onEnable failed for art.arcane.iris.modded.ModdedServiceManagerTest$SecondService
[19:42:28] [Test worker/ERROR]: Iris service onEnable failed for art.arcane.iris.modded.ModdedServiceManagerTest$SecondService
java.lang.RuntimeException: enable failed
at art.arcane.iris.modded.ModdedServiceManagerTest.rollsBackEnabledServicesAndPreservesCleanupFailures(ModdedServiceManagerTest.java:31)
at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:104)
@@ -92,7 +182,7 @@ java.lang.RuntimeException: enable failed
Suppressed: java.lang.RuntimeException: cleanup failed
at art.arcane.iris.modded.ModdedServiceManagerTest.rollsBackEnabledServicesAndPreservesCleanupFailures(ModdedServiceManagerTest.java:32)
... 42 more
[17:42:11] [Test worker/ERROR]: [worldcheck] server stop request failed
[19:42:28] [Test worker/ERROR]: [worldcheck] server stop request failed
java.lang.IllegalStateException: stop request failed
at art.arcane.iris.modded.ModdedWorldCheckTest.lambda$stopRequestFailureForcesNonzeroResult$1(ModdedWorldCheckTest.java:235)
at art.arcane.iris.modded.ModdedWorldCheck.runAndRequestStop(ModdedWorldCheck.java:179)
@@ -139,7 +229,7 @@ java.lang.IllegalStateException: stop request failed
at org.gradle.process.internal.worker.child.SystemApplicationClassLoaderWorker.call(SystemApplicationClassLoaderWorker.java:72)
at worker.org.gradle.process.internal.worker.GradleWorkerMain.run(GradleWorkerMain.java:69)
at worker.org.gradle.process.internal.worker.GradleWorkerMain.main(GradleWorkerMain.java:74)
[17:42:11] [Test worker/ERROR]: [worldcheck] waiting for server shutdown failed
[19:42:28] [Test worker/ERROR]: [worldcheck] waiting for server shutdown failed
java.lang.IllegalStateException: shutdown wait failed
at art.arcane.iris.modded.ModdedWorldCheckTest.lambda$shutdownWaitFailureForcesNonzeroExit$0(ModdedWorldCheckTest.java:261)
at art.arcane.iris.modded.ModdedWorldCheck.awaitStopAndExit(ModdedWorldCheck.java:194)
@@ -186,7 +276,7 @@ java.lang.IllegalStateException: shutdown wait failed
at org.gradle.process.internal.worker.child.SystemApplicationClassLoaderWorker.call(SystemApplicationClassLoaderWorker.java:72)
at worker.org.gradle.process.internal.worker.GradleWorkerMain.run(GradleWorkerMain.java:69)
at worker.org.gradle.process.internal.worker.GradleWorkerMain.main(GradleWorkerMain.java:74)
[17:42:11] [Test worker/ERROR]: [worldcheck] check failed
[19:42:28] [Test worker/ERROR]: [worldcheck] check failed
java.lang.IllegalStateException: check failed
at art.arcane.iris.modded.ModdedWorldCheckTest.lambda$thrownCheckStillRequestsStop$0(ModdedWorldCheckTest.java:221)
at art.arcane.iris.modded.ModdedWorldCheck.runAndRequestStop(ModdedWorldCheck.java:174)
@@ -233,8 +323,8 @@ java.lang.IllegalStateException: check failed
at org.gradle.process.internal.worker.child.SystemApplicationClassLoaderWorker.call(SystemApplicationClassLoaderWorker.java:72)
at worker.org.gradle.process.internal.worker.GradleWorkerMain.run(GradleWorkerMain.java:69)
at worker.org.gradle.process.internal.worker.GradleWorkerMain.main(GradleWorkerMain.java:74)
[17:42:11] [Test worker/INFO]: Iris registered custom content provider 'iris_discovery_success'
[17:42:11] [Test worker/ERROR]: Iris custom content provider discovery failed
[19:42:28] [Test worker/INFO]: Iris registered custom content provider 'iris_discovery_success'
[19:42:28] [Test worker/ERROR]: Iris custom content provider discovery failed
java.lang.RuntimeException: provider init failed
at art.arcane.iris.modded.api.ModdedCustomContentRegistryTest.failedDiscoveryPublishesNothingAndPreservesTheCause(ModdedCustomContentRegistryTest.java:39)
at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:104)
@@ -19,15 +19,27 @@
package art.arcane.iris.fabric;
import art.arcane.iris.modded.ModdedLoader;
import art.arcane.iris.modded.service.ModdedTreeFellerService;
import net.fabricmc.api.EnvType;
import net.fabricmc.fabric.api.event.player.PlayerBlockBreakEvents;
import net.fabricmc.fabric.api.permission.v1.PermissionContextOwner;
import net.fabricmc.loader.api.FabricLoader;
import net.fabricmc.loader.api.ModContainer;
import net.minecraft.core.BlockPos;
import net.minecraft.resources.Identifier;
import net.minecraft.server.MinecraftServer;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.server.permissions.PermissionLevel;
import net.minecraft.world.level.block.entity.BlockEntity;
import net.minecraft.world.level.block.state.BlockState;
import java.io.File;
import java.nio.file.Path;
public final class FabricModdedLoader implements ModdedLoader {
private static final Identifier TREE_FELLER_PERMISSION = Identifier.fromNamespaceAndPath("iris", "treefeller");
@Override
public String platformName() {
return "fabric";
@@ -74,4 +86,24 @@ public final class FabricModdedLoader implements ModdedLoader {
.map((Path p) -> p.toFile())
.orElse(null);
}
@Override
public boolean hasTreeFellerPermission(ServerPlayer player) {
return ((PermissionContextOwner) player).checkPermission(
TREE_FELLER_PERMISSION,
PermissionLevel.GAMEMASTERS
);
}
@Override
public boolean canTreeFellerBreak(
ServerLevel level,
ServerPlayer player,
BlockPos position,
BlockState state
) {
BlockEntity blockEntity = state.hasBlockEntity() ? level.getBlockEntity(position) : null;
return ModdedTreeFellerService.runBreakProbe(() -> PlayerBlockBreakEvents.BEFORE.invoker()
.beforeBlockBreak(level, player, position, state, blockEntity));
}
}
@@ -42,6 +42,7 @@ import net.minecraft.core.registries.BuiltInRegistries;
import net.minecraft.resources.Identifier;
import net.minecraft.server.MinecraftServer;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.world.InteractionHand;
import net.minecraft.world.InteractionResult;
import net.minecraft.world.entity.player.Player;
@@ -60,13 +61,19 @@ public final class IrisFabricBootstrap implements ModInitializer {
ServerLifecycleEvents.SERVER_STARTED.register((MinecraftServer server) -> ModdedEngineBootstrap.serverStarted(server));
ServerLifecycleEvents.SERVER_STOPPING.register((MinecraftServer server) -> ModdedEngineBootstrap.stop());
ServerLevelEvents.LOAD.register((MinecraftServer server, ServerLevel level) -> ModdedEngineBootstrap.levelLoaded(level));
ServerLevelEvents.UNLOAD.register((MinecraftServer server, ServerLevel level) -> ModdedEngineBootstrap.levelUnloaded(level));
CommandRegistrationCallback.EVENT.register((CommandDispatcher<CommandSourceStack> dispatcher, CommandBuildContext buildContext, Commands.CommandSelection selection) -> IrisModdedCommands.register(dispatcher));
PlayerBlockBreakEvents.BEFORE.register((Level level, Player player, BlockPos pos, BlockState state, BlockEntity blockEntity) -> {
if (level instanceof ServerLevel serverLevel) {
ModdedBlockBreakHandler.prepare(serverLevel, pos, state);
if (level instanceof ServerLevel serverLevel && player instanceof ServerPlayer serverPlayer) {
ModdedBlockBreakHandler.prepare(serverLevel, serverPlayer, pos, state);
}
return true;
});
PlayerBlockBreakEvents.CANCELED.register((Level level, Player player, BlockPos pos, BlockState state, BlockEntity blockEntity) -> {
if (level instanceof ServerLevel serverLevel) {
ModdedBlockBreakHandler.cancel(serverLevel, pos);
}
});
AttackBlockCallback.EVENT.register((Player player, Level level, InteractionHand hand, BlockPos pos, Direction direction) ->
ModdedWandService.attackBlock(player, level, hand, pos) ? InteractionResult.SUCCESS : InteractionResult.PASS);
UseBlockCallback.EVENT.register((Player player, Level level, InteractionHand hand, BlockHitResult hit) ->
@@ -0,0 +1,25 @@
package art.arcane.iris.fabric.mixin;
import art.arcane.iris.modded.ModdedBlockBreakHandler;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.world.item.BlockItem;
import net.minecraft.world.item.context.BlockPlaceContext;
import net.minecraft.world.level.block.state.BlockState;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
@Mixin(BlockItem.class)
public final class BlockItemMixin {
@Inject(method = "placeBlock", at = @At("RETURN"))
private void iris$clearTreeProvenance(
BlockPlaceContext context,
BlockState state,
CallbackInfoReturnable<Boolean> info
) {
if (info.getReturnValue() && context.getLevel() instanceof ServerLevel level) {
ModdedBlockBreakHandler.clearPlacedProvenance(level, context.getClickedPos());
}
}
}
@@ -48,6 +48,10 @@ public class BlockMixin {
if (result == null) {
return;
}
if (result.routeCombinedDrops(info.getReturnValue())) {
info.setReturnValue(List.of());
return;
}
List<ItemStack> drops = result.replaceVanillaDrops()
? new ArrayList<>()
: new ArrayList<>(info.getReturnValue());
@@ -4,6 +4,7 @@
"package": "art.arcane.iris.fabric.mixin",
"compatibilityLevel": "JAVA_25",
"mixins": [
"BlockItemMixin",
"BlockMixin",
"ServerPacksSourceMixin"
],
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+101 -11
View File
@@ -1,8 +1,98 @@
[18Jul2026 17:42:16.031] [Test worker/DEBUG] [io.netty.util.internal.logging.InternalLoggerFactory/]: Using SLF4J as the default logging framework
[18Jul2026 17:42:16.033] [Test worker/DEBUG] [io.netty.util.ResourceLeakDetector/]: -Dio.netty.leakDetection.level: simple
[18Jul2026 17:42:16.033] [Test worker/DEBUG] [io.netty.util.ResourceLeakDetector/]: -Dio.netty.leakDetection.targetRecords: 4
[18Jul2026 17:42:18.043] [Test worker/INFO] [Iris/]: Iris registered custom content provider 'iris_deferred_test'
[18Jul2026 17:42:18.104] [Test worker/ERROR] [Iris/]: Iris service rollback failed for art.arcane.iris.modded.ModdedServiceManagerTest$SecondService
[20Jul2026 19:42:36.918] [Test worker/DEBUG] [io.netty.util.internal.logging.InternalLoggerFactory/]: Using SLF4J as the default logging framework
[20Jul2026 19:42:36.919] [Test worker/DEBUG] [io.netty.util.ResourceLeakDetector/]: -Dio.netty.leakDetection.level: simple
[20Jul2026 19:42:36.919] [Test worker/DEBUG] [io.netty.util.ResourceLeakDetector/]: -Dio.netty.leakDetection.targetRecords: 4
[20Jul2026 19:42:38.772] [Test worker/INFO] [Iris/]: Iris registered custom content provider 'iris_deferred_test'
[20Jul2026 19:42:38.831] [Test worker/ERROR] [Iris/]: Iris service onDisable failed for art.arcane.iris.modded.ModdedServiceManagerTest$SecondService
java.lang.RuntimeException: second disable failed
at art.arcane.iris.modded.ModdedServiceManagerTest.disableAttemptsEveryServiceInReverseOrderAndAggregatesFailures(ModdedServiceManagerTest.java:55) ~[test/:?]
at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:104) ~[?:?]
at java.base/java.lang.reflect.Method.invoke(Method.java:565) ~[?:?]
at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:59) ~[junit-4.13.2.jar:4.13.2]
at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12) ~[junit-4.13.2.jar:4.13.2]
at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:56) ~[junit-4.13.2.jar:4.13.2]
at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17) ~[junit-4.13.2.jar:4.13.2]
at org.junit.runners.ParentRunner$3.evaluate(ParentRunner.java:306) ~[junit-4.13.2.jar:4.13.2]
at org.junit.runners.BlockJUnit4ClassRunner$1.evaluate(BlockJUnit4ClassRunner.java:100) ~[junit-4.13.2.jar:4.13.2]
at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:366) ~[junit-4.13.2.jar:4.13.2]
at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:103) ~[junit-4.13.2.jar:4.13.2]
at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:63) ~[junit-4.13.2.jar:4.13.2]
at org.junit.runners.ParentRunner$4.run(ParentRunner.java:331) ~[junit-4.13.2.jar:4.13.2]
at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:79) ~[junit-4.13.2.jar:4.13.2]
at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:329) ~[junit-4.13.2.jar:4.13.2]
at org.junit.runners.ParentRunner.access$100(ParentRunner.java:66) ~[junit-4.13.2.jar:4.13.2]
at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:293) ~[junit-4.13.2.jar:4.13.2]
at org.junit.runners.ParentRunner$3.evaluate(ParentRunner.java:306) ~[junit-4.13.2.jar:4.13.2]
at org.junit.runners.ParentRunner.run(ParentRunner.java:413) ~[junit-4.13.2.jar:4.13.2]
at org.junit.runner.JUnitCore.run(JUnitCore.java:137) ~[junit-4.13.2.jar:4.13.2]
at org.junit.runner.JUnitCore.run(JUnitCore.java:115) ~[junit-4.13.2.jar:4.13.2]
at org.gradle.api.internal.tasks.testing.junit.JUnitTestExecutor.runRequest(JUnitTestExecutor.java:175) ~[?:?]
at org.gradle.api.internal.tasks.testing.junit.JUnitTestExecutor.accept(JUnitTestExecutor.java:84) ~[?:?]
at org.gradle.api.internal.tasks.testing.junit.JUnitTestExecutor.accept(JUnitTestExecutor.java:47) ~[?:?]
at org.gradle.api.internal.tasks.testing.junit.AbstractJUnitTestDefinitionProcessor.processTestDefinition(AbstractJUnitTestDefinitionProcessor.java:65) ~[?:?]
at org.gradle.api.internal.tasks.testing.SuiteTestDefinitionProcessor.processTestDefinition(SuiteTestDefinitionProcessor.java:53) ~[?:?]
at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:104) ~[?:?]
at java.base/java.lang.reflect.Method.invoke(Method.java:565) ~[?:?]
at org.gradle.internal.dispatch.MethodInvocation.invokeOn(MethodInvocation.java:77) ~[?:?]
at org.gradle.internal.dispatch.ReflectionDispatch.dispatch(ReflectionDispatch.java:28) ~[?:?]
at org.gradle.internal.dispatch.ReflectionDispatch.dispatch(ReflectionDispatch.java:19) ~[?:?]
at org.gradle.internal.dispatch.ContextClassLoaderDispatch.dispatch(ContextClassLoaderDispatch.java:33) ~[?:?]
at org.gradle.internal.dispatch.ProxyDispatchAdapter$DispatchingInvocationHandler.invoke(ProxyDispatchAdapter.java:88) ~[?:?]
at jdk.proxy1/jdk.proxy1.$Proxy4.processTestDefinition(Unknown Source) ~[?:?]
at org.gradle.api.internal.tasks.testing.worker.TestWorker$2.run(TestWorker.java:178) ~[?:?]
at org.gradle.api.internal.tasks.testing.worker.TestWorker.executeAndMaintainThreadName(TestWorker.java:126) ~[?:?]
at org.gradle.api.internal.tasks.testing.worker.TestWorker.execute(TestWorker.java:103) ~[?:?]
at org.gradle.api.internal.tasks.testing.worker.TestWorker.execute(TestWorker.java:63) ~[?:?]
at org.gradle.process.internal.worker.child.ActionExecutionWorker.execute(ActionExecutionWorker.java:56) ~[?:?]
at org.gradle.process.internal.worker.child.SystemApplicationClassLoaderWorker.call(SystemApplicationClassLoaderWorker.java:122) ~[?:?]
at org.gradle.process.internal.worker.child.SystemApplicationClassLoaderWorker.call(SystemApplicationClassLoaderWorker.java:72) ~[?:?]
at worker.org.gradle.process.internal.worker.GradleWorkerMain.run(GradleWorkerMain.java:69) [gradle-worker.jar:?]
at worker.org.gradle.process.internal.worker.GradleWorkerMain.main(GradleWorkerMain.java:74) [gradle-worker.jar:?]
[20Jul2026 19:42:38.837] [Test worker/ERROR] [Iris/]: Iris service onDisable failed for art.arcane.iris.modded.ModdedServiceManagerTest$FirstService
java.lang.RuntimeException: first disable failed
at art.arcane.iris.modded.ModdedServiceManagerTest.disableAttemptsEveryServiceInReverseOrderAndAggregatesFailures(ModdedServiceManagerTest.java:54) ~[test/:?]
at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:104) ~[?:?]
at java.base/java.lang.reflect.Method.invoke(Method.java:565) ~[?:?]
at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:59) ~[junit-4.13.2.jar:4.13.2]
at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12) ~[junit-4.13.2.jar:4.13.2]
at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:56) ~[junit-4.13.2.jar:4.13.2]
at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17) ~[junit-4.13.2.jar:4.13.2]
at org.junit.runners.ParentRunner$3.evaluate(ParentRunner.java:306) ~[junit-4.13.2.jar:4.13.2]
at org.junit.runners.BlockJUnit4ClassRunner$1.evaluate(BlockJUnit4ClassRunner.java:100) ~[junit-4.13.2.jar:4.13.2]
at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:366) ~[junit-4.13.2.jar:4.13.2]
at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:103) ~[junit-4.13.2.jar:4.13.2]
at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:63) ~[junit-4.13.2.jar:4.13.2]
at org.junit.runners.ParentRunner$4.run(ParentRunner.java:331) ~[junit-4.13.2.jar:4.13.2]
at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:79) ~[junit-4.13.2.jar:4.13.2]
at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:329) ~[junit-4.13.2.jar:4.13.2]
at org.junit.runners.ParentRunner.access$100(ParentRunner.java:66) ~[junit-4.13.2.jar:4.13.2]
at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:293) ~[junit-4.13.2.jar:4.13.2]
at org.junit.runners.ParentRunner$3.evaluate(ParentRunner.java:306) ~[junit-4.13.2.jar:4.13.2]
at org.junit.runners.ParentRunner.run(ParentRunner.java:413) ~[junit-4.13.2.jar:4.13.2]
at org.junit.runner.JUnitCore.run(JUnitCore.java:137) ~[junit-4.13.2.jar:4.13.2]
at org.junit.runner.JUnitCore.run(JUnitCore.java:115) ~[junit-4.13.2.jar:4.13.2]
at org.gradle.api.internal.tasks.testing.junit.JUnitTestExecutor.runRequest(JUnitTestExecutor.java:175) ~[?:?]
at org.gradle.api.internal.tasks.testing.junit.JUnitTestExecutor.accept(JUnitTestExecutor.java:84) ~[?:?]
at org.gradle.api.internal.tasks.testing.junit.JUnitTestExecutor.accept(JUnitTestExecutor.java:47) ~[?:?]
at org.gradle.api.internal.tasks.testing.junit.AbstractJUnitTestDefinitionProcessor.processTestDefinition(AbstractJUnitTestDefinitionProcessor.java:65) ~[?:?]
at org.gradle.api.internal.tasks.testing.SuiteTestDefinitionProcessor.processTestDefinition(SuiteTestDefinitionProcessor.java:53) ~[?:?]
at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:104) ~[?:?]
at java.base/java.lang.reflect.Method.invoke(Method.java:565) ~[?:?]
at org.gradle.internal.dispatch.MethodInvocation.invokeOn(MethodInvocation.java:77) ~[?:?]
at org.gradle.internal.dispatch.ReflectionDispatch.dispatch(ReflectionDispatch.java:28) ~[?:?]
at org.gradle.internal.dispatch.ReflectionDispatch.dispatch(ReflectionDispatch.java:19) ~[?:?]
at org.gradle.internal.dispatch.ContextClassLoaderDispatch.dispatch(ContextClassLoaderDispatch.java:33) ~[?:?]
at org.gradle.internal.dispatch.ProxyDispatchAdapter$DispatchingInvocationHandler.invoke(ProxyDispatchAdapter.java:88) ~[?:?]
at jdk.proxy1/jdk.proxy1.$Proxy4.processTestDefinition(Unknown Source) ~[?:?]
at org.gradle.api.internal.tasks.testing.worker.TestWorker$2.run(TestWorker.java:178) ~[?:?]
at org.gradle.api.internal.tasks.testing.worker.TestWorker.executeAndMaintainThreadName(TestWorker.java:126) ~[?:?]
at org.gradle.api.internal.tasks.testing.worker.TestWorker.execute(TestWorker.java:103) ~[?:?]
at org.gradle.api.internal.tasks.testing.worker.TestWorker.execute(TestWorker.java:63) ~[?:?]
at org.gradle.process.internal.worker.child.ActionExecutionWorker.execute(ActionExecutionWorker.java:56) ~[?:?]
at org.gradle.process.internal.worker.child.SystemApplicationClassLoaderWorker.call(SystemApplicationClassLoaderWorker.java:122) ~[?:?]
at org.gradle.process.internal.worker.child.SystemApplicationClassLoaderWorker.call(SystemApplicationClassLoaderWorker.java:72) ~[?:?]
at worker.org.gradle.process.internal.worker.GradleWorkerMain.run(GradleWorkerMain.java:69) [gradle-worker.jar:?]
at worker.org.gradle.process.internal.worker.GradleWorkerMain.main(GradleWorkerMain.java:74) [gradle-worker.jar:?]
[20Jul2026 19:42:38.840] [Test worker/ERROR] [Iris/]: Iris service rollback failed for art.arcane.iris.modded.ModdedServiceManagerTest$SecondService
java.lang.RuntimeException: cleanup failed
at art.arcane.iris.modded.ModdedServiceManagerTest.rollsBackEnabledServicesAndPreservesCleanupFailures(ModdedServiceManagerTest.java:32) ~[test/:?]
at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:104) ~[?:?]
@@ -47,7 +137,7 @@ java.lang.RuntimeException: cleanup failed
at org.gradle.process.internal.worker.child.SystemApplicationClassLoaderWorker.call(SystemApplicationClassLoaderWorker.java:72) ~[?:?]
at worker.org.gradle.process.internal.worker.GradleWorkerMain.run(GradleWorkerMain.java:69) [gradle-worker.jar:?]
at worker.org.gradle.process.internal.worker.GradleWorkerMain.main(GradleWorkerMain.java:74) [gradle-worker.jar:?]
[18Jul2026 17:42:18.110] [Test worker/ERROR] [Iris/]: Iris service onEnable failed for art.arcane.iris.modded.ModdedServiceManagerTest$SecondService
[20Jul2026 19:42:38.843] [Test worker/ERROR] [Iris/]: Iris service onEnable failed for art.arcane.iris.modded.ModdedServiceManagerTest$SecondService
java.lang.RuntimeException: enable failed
at art.arcane.iris.modded.ModdedServiceManagerTest.rollsBackEnabledServicesAndPreservesCleanupFailures(ModdedServiceManagerTest.java:31) ~[test/:?]
at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:104) ~[?:?]
@@ -95,7 +185,7 @@ java.lang.RuntimeException: enable failed
Suppressed: java.lang.RuntimeException: cleanup failed
at art.arcane.iris.modded.ModdedServiceManagerTest.rollsBackEnabledServicesAndPreservesCleanupFailures(ModdedServiceManagerTest.java:32) ~[test/:?]
... 42 more
[18Jul2026 17:42:18.144] [Test worker/ERROR] [Iris/]: [worldcheck] server stop request failed
[20Jul2026 19:42:38.875] [Test worker/ERROR] [Iris/]: [worldcheck] server stop request failed
java.lang.IllegalStateException: stop request failed
at art.arcane.iris.modded.ModdedWorldCheckTest.lambda$stopRequestFailureForcesNonzeroResult$1(ModdedWorldCheckTest.java:235) ~[test/:?]
at art.arcane.iris.modded.ModdedWorldCheck.runAndRequestStop(ModdedWorldCheck.java:179) ~[main/:?]
@@ -142,7 +232,7 @@ java.lang.IllegalStateException: stop request failed
at org.gradle.process.internal.worker.child.SystemApplicationClassLoaderWorker.call(SystemApplicationClassLoaderWorker.java:72) ~[?:?]
at worker.org.gradle.process.internal.worker.GradleWorkerMain.run(GradleWorkerMain.java:69) [gradle-worker.jar:?]
at worker.org.gradle.process.internal.worker.GradleWorkerMain.main(GradleWorkerMain.java:74) [gradle-worker.jar:?]
[18Jul2026 17:42:18.147] [Test worker/ERROR] [Iris/]: [worldcheck] waiting for server shutdown failed
[20Jul2026 19:42:38.878] [Test worker/ERROR] [Iris/]: [worldcheck] waiting for server shutdown failed
java.lang.IllegalStateException: shutdown wait failed
at art.arcane.iris.modded.ModdedWorldCheckTest.lambda$shutdownWaitFailureForcesNonzeroExit$0(ModdedWorldCheckTest.java:261) ~[test/:?]
at art.arcane.iris.modded.ModdedWorldCheck.awaitStopAndExit(ModdedWorldCheck.java:194) ~[main/:?]
@@ -189,7 +279,7 @@ java.lang.IllegalStateException: shutdown wait failed
at org.gradle.process.internal.worker.child.SystemApplicationClassLoaderWorker.call(SystemApplicationClassLoaderWorker.java:72) ~[?:?]
at worker.org.gradle.process.internal.worker.GradleWorkerMain.run(GradleWorkerMain.java:69) [gradle-worker.jar:?]
at worker.org.gradle.process.internal.worker.GradleWorkerMain.main(GradleWorkerMain.java:74) [gradle-worker.jar:?]
[18Jul2026 17:42:18.154] [Test worker/ERROR] [Iris/]: [worldcheck] check failed
[20Jul2026 19:42:38.883] [Test worker/ERROR] [Iris/]: [worldcheck] check failed
java.lang.IllegalStateException: check failed
at art.arcane.iris.modded.ModdedWorldCheckTest.lambda$thrownCheckStillRequestsStop$0(ModdedWorldCheckTest.java:221) ~[test/:?]
at art.arcane.iris.modded.ModdedWorldCheck.runAndRequestStop(ModdedWorldCheck.java:174) ~[main/:?]
@@ -236,8 +326,8 @@ java.lang.IllegalStateException: check failed
at org.gradle.process.internal.worker.child.SystemApplicationClassLoaderWorker.call(SystemApplicationClassLoaderWorker.java:72) ~[?:?]
at worker.org.gradle.process.internal.worker.GradleWorkerMain.run(GradleWorkerMain.java:69) [gradle-worker.jar:?]
at worker.org.gradle.process.internal.worker.GradleWorkerMain.main(GradleWorkerMain.java:74) [gradle-worker.jar:?]
[18Jul2026 17:42:18.160] [Test worker/INFO] [Iris/]: Iris registered custom content provider 'iris_discovery_success'
[18Jul2026 17:42:18.161] [Test worker/ERROR] [Iris/]: Iris custom content provider discovery failed
[20Jul2026 19:42:38.889] [Test worker/INFO] [Iris/]: Iris registered custom content provider 'iris_discovery_success'
[20Jul2026 19:42:38.889] [Test worker/ERROR] [Iris/]: Iris custom content provider discovery failed
java.lang.RuntimeException: provider init failed
at art.arcane.iris.modded.api.ModdedCustomContentRegistryTest.failedDiscoveryPublishesNothingAndPreservesTheCause(ModdedCustomContentRegistryTest.java:39) ~[test/:?]
at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:104) ~[?:?]
+98 -8
View File
@@ -1,5 +1,95 @@
[18Jul2026 17:42:18.043] [Test worker/INFO] [Iris/]: Iris registered custom content provider 'iris_deferred_test'
[18Jul2026 17:42:18.104] [Test worker/ERROR] [Iris/]: Iris service rollback failed for art.arcane.iris.modded.ModdedServiceManagerTest$SecondService
[20Jul2026 19:42:38.772] [Test worker/INFO] [Iris/]: Iris registered custom content provider 'iris_deferred_test'
[20Jul2026 19:42:38.831] [Test worker/ERROR] [Iris/]: Iris service onDisable failed for art.arcane.iris.modded.ModdedServiceManagerTest$SecondService
java.lang.RuntimeException: second disable failed
at art.arcane.iris.modded.ModdedServiceManagerTest.disableAttemptsEveryServiceInReverseOrderAndAggregatesFailures(ModdedServiceManagerTest.java:55) ~[test/:?]
at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:104) ~[?:?]
at java.base/java.lang.reflect.Method.invoke(Method.java:565) ~[?:?]
at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:59) ~[junit-4.13.2.jar:4.13.2]
at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12) ~[junit-4.13.2.jar:4.13.2]
at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:56) ~[junit-4.13.2.jar:4.13.2]
at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17) ~[junit-4.13.2.jar:4.13.2]
at org.junit.runners.ParentRunner$3.evaluate(ParentRunner.java:306) ~[junit-4.13.2.jar:4.13.2]
at org.junit.runners.BlockJUnit4ClassRunner$1.evaluate(BlockJUnit4ClassRunner.java:100) ~[junit-4.13.2.jar:4.13.2]
at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:366) ~[junit-4.13.2.jar:4.13.2]
at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:103) ~[junit-4.13.2.jar:4.13.2]
at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:63) ~[junit-4.13.2.jar:4.13.2]
at org.junit.runners.ParentRunner$4.run(ParentRunner.java:331) ~[junit-4.13.2.jar:4.13.2]
at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:79) ~[junit-4.13.2.jar:4.13.2]
at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:329) ~[junit-4.13.2.jar:4.13.2]
at org.junit.runners.ParentRunner.access$100(ParentRunner.java:66) ~[junit-4.13.2.jar:4.13.2]
at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:293) ~[junit-4.13.2.jar:4.13.2]
at org.junit.runners.ParentRunner$3.evaluate(ParentRunner.java:306) ~[junit-4.13.2.jar:4.13.2]
at org.junit.runners.ParentRunner.run(ParentRunner.java:413) ~[junit-4.13.2.jar:4.13.2]
at org.junit.runner.JUnitCore.run(JUnitCore.java:137) ~[junit-4.13.2.jar:4.13.2]
at org.junit.runner.JUnitCore.run(JUnitCore.java:115) ~[junit-4.13.2.jar:4.13.2]
at org.gradle.api.internal.tasks.testing.junit.JUnitTestExecutor.runRequest(JUnitTestExecutor.java:175) ~[?:?]
at org.gradle.api.internal.tasks.testing.junit.JUnitTestExecutor.accept(JUnitTestExecutor.java:84) ~[?:?]
at org.gradle.api.internal.tasks.testing.junit.JUnitTestExecutor.accept(JUnitTestExecutor.java:47) ~[?:?]
at org.gradle.api.internal.tasks.testing.junit.AbstractJUnitTestDefinitionProcessor.processTestDefinition(AbstractJUnitTestDefinitionProcessor.java:65) ~[?:?]
at org.gradle.api.internal.tasks.testing.SuiteTestDefinitionProcessor.processTestDefinition(SuiteTestDefinitionProcessor.java:53) ~[?:?]
at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:104) ~[?:?]
at java.base/java.lang.reflect.Method.invoke(Method.java:565) ~[?:?]
at org.gradle.internal.dispatch.MethodInvocation.invokeOn(MethodInvocation.java:77) ~[?:?]
at org.gradle.internal.dispatch.ReflectionDispatch.dispatch(ReflectionDispatch.java:28) ~[?:?]
at org.gradle.internal.dispatch.ReflectionDispatch.dispatch(ReflectionDispatch.java:19) ~[?:?]
at org.gradle.internal.dispatch.ContextClassLoaderDispatch.dispatch(ContextClassLoaderDispatch.java:33) ~[?:?]
at org.gradle.internal.dispatch.ProxyDispatchAdapter$DispatchingInvocationHandler.invoke(ProxyDispatchAdapter.java:88) ~[?:?]
at jdk.proxy1/jdk.proxy1.$Proxy4.processTestDefinition(Unknown Source) ~[?:?]
at org.gradle.api.internal.tasks.testing.worker.TestWorker$2.run(TestWorker.java:178) ~[?:?]
at org.gradle.api.internal.tasks.testing.worker.TestWorker.executeAndMaintainThreadName(TestWorker.java:126) ~[?:?]
at org.gradle.api.internal.tasks.testing.worker.TestWorker.execute(TestWorker.java:103) ~[?:?]
at org.gradle.api.internal.tasks.testing.worker.TestWorker.execute(TestWorker.java:63) ~[?:?]
at org.gradle.process.internal.worker.child.ActionExecutionWorker.execute(ActionExecutionWorker.java:56) ~[?:?]
at org.gradle.process.internal.worker.child.SystemApplicationClassLoaderWorker.call(SystemApplicationClassLoaderWorker.java:122) ~[?:?]
at org.gradle.process.internal.worker.child.SystemApplicationClassLoaderWorker.call(SystemApplicationClassLoaderWorker.java:72) ~[?:?]
at worker.org.gradle.process.internal.worker.GradleWorkerMain.run(GradleWorkerMain.java:69) [gradle-worker.jar:?]
at worker.org.gradle.process.internal.worker.GradleWorkerMain.main(GradleWorkerMain.java:74) [gradle-worker.jar:?]
[20Jul2026 19:42:38.837] [Test worker/ERROR] [Iris/]: Iris service onDisable failed for art.arcane.iris.modded.ModdedServiceManagerTest$FirstService
java.lang.RuntimeException: first disable failed
at art.arcane.iris.modded.ModdedServiceManagerTest.disableAttemptsEveryServiceInReverseOrderAndAggregatesFailures(ModdedServiceManagerTest.java:54) ~[test/:?]
at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:104) ~[?:?]
at java.base/java.lang.reflect.Method.invoke(Method.java:565) ~[?:?]
at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:59) ~[junit-4.13.2.jar:4.13.2]
at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12) ~[junit-4.13.2.jar:4.13.2]
at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:56) ~[junit-4.13.2.jar:4.13.2]
at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17) ~[junit-4.13.2.jar:4.13.2]
at org.junit.runners.ParentRunner$3.evaluate(ParentRunner.java:306) ~[junit-4.13.2.jar:4.13.2]
at org.junit.runners.BlockJUnit4ClassRunner$1.evaluate(BlockJUnit4ClassRunner.java:100) ~[junit-4.13.2.jar:4.13.2]
at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:366) ~[junit-4.13.2.jar:4.13.2]
at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:103) ~[junit-4.13.2.jar:4.13.2]
at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:63) ~[junit-4.13.2.jar:4.13.2]
at org.junit.runners.ParentRunner$4.run(ParentRunner.java:331) ~[junit-4.13.2.jar:4.13.2]
at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:79) ~[junit-4.13.2.jar:4.13.2]
at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:329) ~[junit-4.13.2.jar:4.13.2]
at org.junit.runners.ParentRunner.access$100(ParentRunner.java:66) ~[junit-4.13.2.jar:4.13.2]
at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:293) ~[junit-4.13.2.jar:4.13.2]
at org.junit.runners.ParentRunner$3.evaluate(ParentRunner.java:306) ~[junit-4.13.2.jar:4.13.2]
at org.junit.runners.ParentRunner.run(ParentRunner.java:413) ~[junit-4.13.2.jar:4.13.2]
at org.junit.runner.JUnitCore.run(JUnitCore.java:137) ~[junit-4.13.2.jar:4.13.2]
at org.junit.runner.JUnitCore.run(JUnitCore.java:115) ~[junit-4.13.2.jar:4.13.2]
at org.gradle.api.internal.tasks.testing.junit.JUnitTestExecutor.runRequest(JUnitTestExecutor.java:175) ~[?:?]
at org.gradle.api.internal.tasks.testing.junit.JUnitTestExecutor.accept(JUnitTestExecutor.java:84) ~[?:?]
at org.gradle.api.internal.tasks.testing.junit.JUnitTestExecutor.accept(JUnitTestExecutor.java:47) ~[?:?]
at org.gradle.api.internal.tasks.testing.junit.AbstractJUnitTestDefinitionProcessor.processTestDefinition(AbstractJUnitTestDefinitionProcessor.java:65) ~[?:?]
at org.gradle.api.internal.tasks.testing.SuiteTestDefinitionProcessor.processTestDefinition(SuiteTestDefinitionProcessor.java:53) ~[?:?]
at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:104) ~[?:?]
at java.base/java.lang.reflect.Method.invoke(Method.java:565) ~[?:?]
at org.gradle.internal.dispatch.MethodInvocation.invokeOn(MethodInvocation.java:77) ~[?:?]
at org.gradle.internal.dispatch.ReflectionDispatch.dispatch(ReflectionDispatch.java:28) ~[?:?]
at org.gradle.internal.dispatch.ReflectionDispatch.dispatch(ReflectionDispatch.java:19) ~[?:?]
at org.gradle.internal.dispatch.ContextClassLoaderDispatch.dispatch(ContextClassLoaderDispatch.java:33) ~[?:?]
at org.gradle.internal.dispatch.ProxyDispatchAdapter$DispatchingInvocationHandler.invoke(ProxyDispatchAdapter.java:88) ~[?:?]
at jdk.proxy1/jdk.proxy1.$Proxy4.processTestDefinition(Unknown Source) ~[?:?]
at org.gradle.api.internal.tasks.testing.worker.TestWorker$2.run(TestWorker.java:178) ~[?:?]
at org.gradle.api.internal.tasks.testing.worker.TestWorker.executeAndMaintainThreadName(TestWorker.java:126) ~[?:?]
at org.gradle.api.internal.tasks.testing.worker.TestWorker.execute(TestWorker.java:103) ~[?:?]
at org.gradle.api.internal.tasks.testing.worker.TestWorker.execute(TestWorker.java:63) ~[?:?]
at org.gradle.process.internal.worker.child.ActionExecutionWorker.execute(ActionExecutionWorker.java:56) ~[?:?]
at org.gradle.process.internal.worker.child.SystemApplicationClassLoaderWorker.call(SystemApplicationClassLoaderWorker.java:122) ~[?:?]
at org.gradle.process.internal.worker.child.SystemApplicationClassLoaderWorker.call(SystemApplicationClassLoaderWorker.java:72) ~[?:?]
at worker.org.gradle.process.internal.worker.GradleWorkerMain.run(GradleWorkerMain.java:69) [gradle-worker.jar:?]
at worker.org.gradle.process.internal.worker.GradleWorkerMain.main(GradleWorkerMain.java:74) [gradle-worker.jar:?]
[20Jul2026 19:42:38.840] [Test worker/ERROR] [Iris/]: Iris service rollback failed for art.arcane.iris.modded.ModdedServiceManagerTest$SecondService
java.lang.RuntimeException: cleanup failed
at art.arcane.iris.modded.ModdedServiceManagerTest.rollsBackEnabledServicesAndPreservesCleanupFailures(ModdedServiceManagerTest.java:32) ~[test/:?]
at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:104) ~[?:?]
@@ -44,7 +134,7 @@ java.lang.RuntimeException: cleanup failed
at org.gradle.process.internal.worker.child.SystemApplicationClassLoaderWorker.call(SystemApplicationClassLoaderWorker.java:72) ~[?:?]
at worker.org.gradle.process.internal.worker.GradleWorkerMain.run(GradleWorkerMain.java:69) [gradle-worker.jar:?]
at worker.org.gradle.process.internal.worker.GradleWorkerMain.main(GradleWorkerMain.java:74) [gradle-worker.jar:?]
[18Jul2026 17:42:18.110] [Test worker/ERROR] [Iris/]: Iris service onEnable failed for art.arcane.iris.modded.ModdedServiceManagerTest$SecondService
[20Jul2026 19:42:38.843] [Test worker/ERROR] [Iris/]: Iris service onEnable failed for art.arcane.iris.modded.ModdedServiceManagerTest$SecondService
java.lang.RuntimeException: enable failed
at art.arcane.iris.modded.ModdedServiceManagerTest.rollsBackEnabledServicesAndPreservesCleanupFailures(ModdedServiceManagerTest.java:31) ~[test/:?]
at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:104) ~[?:?]
@@ -92,7 +182,7 @@ java.lang.RuntimeException: enable failed
Suppressed: java.lang.RuntimeException: cleanup failed
at art.arcane.iris.modded.ModdedServiceManagerTest.rollsBackEnabledServicesAndPreservesCleanupFailures(ModdedServiceManagerTest.java:32) ~[test/:?]
... 42 more
[18Jul2026 17:42:18.144] [Test worker/ERROR] [Iris/]: [worldcheck] server stop request failed
[20Jul2026 19:42:38.875] [Test worker/ERROR] [Iris/]: [worldcheck] server stop request failed
java.lang.IllegalStateException: stop request failed
at art.arcane.iris.modded.ModdedWorldCheckTest.lambda$stopRequestFailureForcesNonzeroResult$1(ModdedWorldCheckTest.java:235) ~[test/:?]
at art.arcane.iris.modded.ModdedWorldCheck.runAndRequestStop(ModdedWorldCheck.java:179) ~[main/:?]
@@ -139,7 +229,7 @@ java.lang.IllegalStateException: stop request failed
at org.gradle.process.internal.worker.child.SystemApplicationClassLoaderWorker.call(SystemApplicationClassLoaderWorker.java:72) ~[?:?]
at worker.org.gradle.process.internal.worker.GradleWorkerMain.run(GradleWorkerMain.java:69) [gradle-worker.jar:?]
at worker.org.gradle.process.internal.worker.GradleWorkerMain.main(GradleWorkerMain.java:74) [gradle-worker.jar:?]
[18Jul2026 17:42:18.147] [Test worker/ERROR] [Iris/]: [worldcheck] waiting for server shutdown failed
[20Jul2026 19:42:38.878] [Test worker/ERROR] [Iris/]: [worldcheck] waiting for server shutdown failed
java.lang.IllegalStateException: shutdown wait failed
at art.arcane.iris.modded.ModdedWorldCheckTest.lambda$shutdownWaitFailureForcesNonzeroExit$0(ModdedWorldCheckTest.java:261) ~[test/:?]
at art.arcane.iris.modded.ModdedWorldCheck.awaitStopAndExit(ModdedWorldCheck.java:194) ~[main/:?]
@@ -186,7 +276,7 @@ java.lang.IllegalStateException: shutdown wait failed
at org.gradle.process.internal.worker.child.SystemApplicationClassLoaderWorker.call(SystemApplicationClassLoaderWorker.java:72) ~[?:?]
at worker.org.gradle.process.internal.worker.GradleWorkerMain.run(GradleWorkerMain.java:69) [gradle-worker.jar:?]
at worker.org.gradle.process.internal.worker.GradleWorkerMain.main(GradleWorkerMain.java:74) [gradle-worker.jar:?]
[18Jul2026 17:42:18.154] [Test worker/ERROR] [Iris/]: [worldcheck] check failed
[20Jul2026 19:42:38.883] [Test worker/ERROR] [Iris/]: [worldcheck] check failed
java.lang.IllegalStateException: check failed
at art.arcane.iris.modded.ModdedWorldCheckTest.lambda$thrownCheckStillRequestsStop$0(ModdedWorldCheckTest.java:221) ~[test/:?]
at art.arcane.iris.modded.ModdedWorldCheck.runAndRequestStop(ModdedWorldCheck.java:174) ~[main/:?]
@@ -233,8 +323,8 @@ java.lang.IllegalStateException: check failed
at org.gradle.process.internal.worker.child.SystemApplicationClassLoaderWorker.call(SystemApplicationClassLoaderWorker.java:72) ~[?:?]
at worker.org.gradle.process.internal.worker.GradleWorkerMain.run(GradleWorkerMain.java:69) [gradle-worker.jar:?]
at worker.org.gradle.process.internal.worker.GradleWorkerMain.main(GradleWorkerMain.java:74) [gradle-worker.jar:?]
[18Jul2026 17:42:18.160] [Test worker/INFO] [Iris/]: Iris registered custom content provider 'iris_discovery_success'
[18Jul2026 17:42:18.161] [Test worker/ERROR] [Iris/]: Iris custom content provider discovery failed
[20Jul2026 19:42:38.889] [Test worker/INFO] [Iris/]: Iris registered custom content provider 'iris_discovery_success'
[20Jul2026 19:42:38.889] [Test worker/ERROR] [Iris/]: Iris custom content provider discovery failed
java.lang.RuntimeException: provider init failed
at art.arcane.iris.modded.api.ModdedCustomContentRegistryTest.failedDiscoveryPublishesNothingAndPreservesTheCause(ModdedCustomContentRegistryTest.java:39) ~[test/:?]
at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:104) ~[?:?]
@@ -19,18 +19,37 @@
package art.arcane.iris.forge;
import art.arcane.iris.modded.ModdedLoader;
import art.arcane.iris.modded.service.ModdedTreeFellerService;
import net.minecraft.commands.Commands;
import net.minecraft.core.BlockPos;
import net.minecraft.server.MinecraftServer;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraftforge.common.util.Result;
import net.minecraftforge.event.level.BlockEvent;
import net.minecraftforge.fml.ModList;
import net.minecraftforge.fml.loading.FMLEnvironment;
import net.minecraftforge.fml.loading.FMLLoader;
import net.minecraftforge.fml.loading.FMLPaths;
import net.minecraftforge.forgespi.language.IModFileInfo;
import net.minecraftforge.server.ServerLifecycleHooks;
import net.minecraftforge.server.permission.PermissionAPI;
import net.minecraftforge.server.permission.nodes.PermissionNode;
import net.minecraftforge.server.permission.nodes.PermissionTypes;
import java.io.File;
import java.nio.file.Path;
public final class ForgeModdedLoader implements ModdedLoader {
public static final PermissionNode<Boolean> TREE_FELLER_PERMISSION = new PermissionNode<>(
"iris",
"treefeller",
PermissionTypes.BOOLEAN,
(player, playerId, contexts) ->
player != null && Commands.LEVEL_GAMEMASTERS.check(player.permissions())
);
@Override
public String platformName() {
return "forge";
@@ -73,4 +92,29 @@ public final class ForgeModdedLoader implements ModdedLoader {
IModFileInfo info = ModList.getModFileById("irisworldgen");
return info == null ? null : info.getFile().getFilePath().toFile();
}
@Override
public boolean hasTreeFellerPermission(ServerPlayer player) {
return PermissionAPI.getPermission(player, TREE_FELLER_PERMISSION);
}
@Override
public boolean canTreeFellerBreak(
ServerLevel level,
ServerPlayer player,
BlockPos position,
BlockState state
) {
return ModdedTreeFellerService.runBreakProbe(() -> {
BlockEvent.BreakEvent event = new BlockEvent.BreakEvent(
level,
position,
state,
player,
Result.DEFAULT
);
BlockEvent.BreakEvent.BUS.post(event);
return !event.getResult().isDenied();
});
}
}
@@ -65,6 +65,10 @@ public final class IrisForgeBlockLootModifier extends LootModifier {
if (result == null) {
return generatedLoot;
}
if (result.routeCombinedDrops(generatedLoot)) {
generatedLoot.clear();
return generatedLoot;
}
if (result.replaceVanillaDrops()) {
generatedLoot.clear();
}
@@ -44,12 +44,15 @@ import net.minecraftforge.event.server.ServerStartingEvent;
import net.minecraftforge.event.server.ServerStoppingEvent;
import net.minecraftforge.api.distmarker.Dist;
import net.minecraftforge.common.loot.IGlobalLootModifier;
import net.minecraftforge.common.util.BlockSnapshot;
import net.minecraftforge.eventbus.api.listener.Priority;
import net.minecraftforge.fml.common.Mod;
import net.minecraftforge.fml.javafmlmod.FMLJavaModLoadingContext;
import net.minecraftforge.fml.loading.FMLEnvironment;
import net.minecraftforge.fml.loading.FMLLoader;
import net.minecraftforge.registries.DeferredRegister;
import net.minecraftforge.registries.ForgeRegistries;
import net.minecraftforge.server.permission.events.PermissionGatherEvent;
import java.util.function.Predicate;
@@ -80,6 +83,11 @@ public final class IrisForgeBootstrap {
ModdedEngineBootstrap.levelLoaded(level);
}
});
LevelEvent.Unload.BUS.addListener((LevelEvent.Unload event) -> {
if (event.getLevel() instanceof ServerLevel level) {
ModdedEngineBootstrap.levelUnloaded(level);
}
});
PlayerEvent.PlayerLoggedInEvent.BUS.addListener((PlayerEvent.PlayerLoggedInEvent event) -> {
if (event.getEntity() instanceof ServerPlayer player) {
ModdedProtocolHandler.onPlayerJoin(player);
@@ -96,11 +104,32 @@ public final class IrisForgeBootstrap {
}
});
RegisterCommandsEvent.BUS.addListener((RegisterCommandsEvent event) -> IrisModdedCommands.register(event.getDispatcher()));
PermissionGatherEvent.Nodes.BUS.addListener((PermissionGatherEvent.Nodes event) ->
event.addNodes(ForgeModdedLoader.TREE_FELLER_PERMISSION));
BlockEvent.BreakEvent.BUS.addListener((BlockEvent.BreakEvent event) -> {
if (event.getLevel() instanceof ServerLevel level && !event.getResult().isDenied()) {
ModdedBlockBreakHandler.prepare(level, event.getPos(), event.getState());
if (event.getLevel() instanceof ServerLevel level
&& event.getPlayer() instanceof ServerPlayer player
&& !event.getResult().isDenied()) {
ModdedBlockBreakHandler.prepare(level, player, event.getPos(), event.getState());
}
});
BlockEvent.EntityPlaceEvent.BUS.addListener(Priority.MONITOR, false, (BlockEvent.EntityPlaceEvent event) -> {
if (!(event instanceof BlockEvent.EntityMultiPlaceEvent)
&& event.getLevel() instanceof ServerLevel level) {
ModdedBlockBreakHandler.clearPlacedProvenance(level, event.getPos());
}
});
BlockEvent.EntityMultiPlaceEvent.BUS.addListener(
Priority.MONITOR,
false,
(BlockEvent.EntityMultiPlaceEvent event) -> {
for (BlockSnapshot snapshot : event.getReplacedBlockSnapshots()) {
if (snapshot.getLevel() instanceof ServerLevel level) {
ModdedBlockBreakHandler.clearPlacedProvenance(level, snapshot.getPos());
}
}
}
);
PlayerInteractEvent.LeftClickBlock.BUS.addListener((Predicate<PlayerInteractEvent.LeftClickBlock>) (PlayerInteractEvent.LeftClickBlock event) ->
ModdedWandService.attackBlock(event.getEntity(), event.getLevel(), event.getHand(), event.getPos()));
PlayerInteractEvent.RightClickBlock.BUS.addListener((Predicate<PlayerInteractEvent.RightClickBlock>) (PlayerInteractEvent.RightClickBlock event) ->
@@ -19,8 +19,11 @@
package art.arcane.iris.modded;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.framework.GenerationSessionException;
import art.arcane.iris.engine.framework.GenerationSessionLease;
import art.arcane.iris.engine.object.IrisBiome;
import art.arcane.iris.engine.object.IrisBiomeCustom;
import art.arcane.iris.util.project.context.IrisContext;
import art.arcane.volmlib.util.math.RNG;
import com.mojang.serialization.MapCodec;
import net.minecraft.core.Holder;
@@ -51,6 +54,7 @@ final class IrisModdedBiomeSource extends BiomeSource {
private final ConcurrentHashMap<Long, Holder<Biome>> visibleBiomeCache = new ConcurrentHashMap<>();
private final ConcurrentHashMap<Long, Holder<Biome>> structureBiomeCache = new ConcurrentHashMap<>();
private final ConcurrentHashMap<Long, Holder<Biome>> surfaceStructureBiomeCache = new ConcurrentHashMap<>();
private final Set<StructureStateBiomeSource> structureStateSources = ConcurrentHashMap.newKeySet();
private volatile IrisModdedChunkGenerator generator;
private volatile Set<String> possibleStructureBiomeKeys;
@@ -67,6 +71,9 @@ final class IrisModdedBiomeSource extends BiomeSource {
structureBiomeCache.clear();
surfaceStructureBiomeCache.clear();
possibleStructureBiomeKeys = null;
for (StructureStateBiomeSource source : structureStateSources) {
source.clearCache();
}
}
BiomeSource forStructureState(HolderLookup<StructureSet> structureSets) {
@@ -91,7 +98,9 @@ final class IrisModdedBiomeSource extends BiomeSource {
}
}
});
return new StructureStateBiomeSource(this, Set.copyOf(possible));
StructureStateBiomeSource source = new StructureStateBiomeSource(this, Set.copyOf(possible));
structureStateSources.add(source);
return source;
}
@Override
@@ -134,10 +143,19 @@ final class IrisModdedBiomeSource extends BiomeSource {
@Override
public Holder<Biome> getNoiseBiome(int quartX, int quartY, int quartZ, Climate.Sampler sampler) {
Engine engine = engineOrNull();
if (!isReady(engine)) {
if (engine == null) {
return serializedSource.getNoiseBiome(quartX, quartY, quartZ, sampler);
}
return getNoiseBiome(engine, quartX, quartY, quartZ, sampler);
GenerationSessionLease lease = tryAcquireGenerationLease(engine, "modded_structure_biome");
if (lease == null) {
throw new IllegalStateException("Iris structure biome lookup was rejected during an engine transition");
}
try (lease; IrisContext.Scope ignored = IrisContext.open(engine, lease.sessionId(), null)) {
if (!isReady(engine)) {
throw new IllegalStateException("Iris structure biome lookup has no active engine runtime");
}
return getNoiseBiome(engine, quartX, quartY, quartZ, sampler);
}
}
private Holder<Biome> getNoiseBiome(Engine engine, int quartX, int quartY, int quartZ,
@@ -160,24 +178,46 @@ final class IrisModdedBiomeSource extends BiomeSource {
Holder<Biome> getVisibleNoiseBiome(int quartX, int quartY, int quartZ, Climate.Sampler sampler) {
Engine engine = engineOrNull();
if (!isReady(engine)) {
if (engine == null) {
return serializedSource.getNoiseBiome(quartX, quartY, quartZ, sampler);
}
long key = packNoiseKey(quartX, quartY, quartZ);
Holder<Biome> cached = visibleBiomeCache.get(key);
if (cached != null) {
return cached;
GenerationSessionLease lease = tryAcquireGenerationLease(engine, "modded_visible_biome");
if (lease == null) {
throw new IllegalStateException("Iris visible biome lookup was rejected during an engine transition");
}
Holder<Biome> resolved = resolveVisibleBiome(engine, quartX, quartY, quartZ, sampler);
Holder<Biome> existing = visibleBiomeCache.putIfAbsent(key, resolved);
if (visibleBiomeCache.size() > BIOME_CACHE_MAX) {
visibleBiomeCache.clear();
try (lease; IrisContext.Scope ignored = IrisContext.open(engine, lease.sessionId(), null)) {
if (!isReady(engine)) {
throw new IllegalStateException("Iris visible biome lookup has no active engine runtime");
}
long key = packNoiseKey(quartX, quartY, quartZ);
Holder<Biome> cached = visibleBiomeCache.get(key);
if (cached != null) {
return cached;
}
Holder<Biome> resolved = resolveVisibleBiome(engine, quartX, quartY, quartZ, sampler);
Holder<Biome> existing = visibleBiomeCache.putIfAbsent(key, resolved);
if (visibleBiomeCache.size() > BIOME_CACHE_MAX) {
visibleBiomeCache.clear();
}
return existing == null ? resolved : existing;
}
return existing == null ? resolved : existing;
}
boolean isStructureReachable(Holder<Structure> structure) {
Set<String> possible = possibleStructureBiomeKeys();
Engine engine = engineOrNull();
if (engine == null) {
return isStructureReachable(structure, possibleStructureBiomeKeys());
}
GenerationSessionLease lease = tryAcquireGenerationLease(engine, "modded_structure_reachability");
if (lease == null) {
throw new IllegalStateException("Iris structure reachability was rejected during an engine transition");
}
try (lease; IrisContext.Scope ignored = IrisContext.open(engine, lease.sessionId(), null)) {
return isStructureReachable(structure, possibleStructureBiomeKeys());
}
}
private boolean isStructureReachable(Holder<Structure> structure, Set<String> possible) {
for (Holder<Biome> biome : structure.value().biomes()) {
String key = holderKey(biome);
if (isGeneratedBiomeKey(key, possible)) {
@@ -191,26 +231,35 @@ final class IrisModdedBiomeSource extends BiomeSource {
public Set<Holder<Biome>> getBiomesWithin(int x, int y, int z, int radius, Climate.Sampler sampler) {
int minQuartY = QuartPos.fromBlock(y - radius);
Engine engine = engineOrNull();
if (!isReady(engine)) {
if (engine == null) {
return super.getBiomesWithin(x, y, z, radius, sampler);
}
boolean monumentQuery = isMonumentSurfaceBiomeQuery(
y, radius, engine.getMinHeight(), engine.getDimension().getFluidHeight());
if (!monumentQuery && !isGuaranteedSurfaceBiome(minQuartY, engine.getMinHeight())) {
return super.getBiomesWithin(x, y, z, radius, sampler);
GenerationSessionLease lease = tryAcquireGenerationLease(engine, "modded_biomes_within");
if (lease == null) {
throw new IllegalStateException("Iris biome radius lookup was rejected during an engine transition");
}
int minQuartX = QuartPos.fromBlock(x - radius);
int maxQuartX = QuartPos.fromBlock(x + radius);
int minQuartZ = QuartPos.fromBlock(z - radius);
int maxQuartZ = QuartPos.fromBlock(z + radius);
int columns = (maxQuartX - minQuartX + 1) * (maxQuartZ - minQuartZ + 1);
Set<Holder<Biome>> biomes = new HashSet<>(columns);
for (int quartZ = minQuartZ; quartZ <= maxQuartZ; quartZ++) {
for (int quartX = minQuartX; quartX <= maxQuartX; quartX++) {
biomes.add(getSurfaceStructureBiome(engine, quartX, quartZ, sampler));
try (lease; IrisContext.Scope ignored = IrisContext.open(engine, lease.sessionId(), null)) {
if (!isReady(engine)) {
throw new IllegalStateException("Iris biome radius lookup has no active engine runtime");
}
boolean monumentQuery = isMonumentSurfaceBiomeQuery(
y, radius, engine.getMinHeight(), engine.getDimension().getFluidHeight());
if (!monumentQuery && !isGuaranteedSurfaceBiome(minQuartY, engine.getMinHeight())) {
return super.getBiomesWithin(x, y, z, radius, sampler);
}
int minQuartX = QuartPos.fromBlock(x - radius);
int maxQuartX = QuartPos.fromBlock(x + radius);
int minQuartZ = QuartPos.fromBlock(z - radius);
int maxQuartZ = QuartPos.fromBlock(z + radius);
int columns = (maxQuartX - minQuartX + 1) * (maxQuartZ - minQuartZ + 1);
Set<Holder<Biome>> biomes = new HashSet<>(columns);
for (int quartZ = minQuartZ; quartZ <= maxQuartZ; quartZ++) {
for (int quartX = minQuartX; quartX <= maxQuartX; quartX++) {
biomes.add(getSurfaceStructureBiome(engine, quartX, quartZ, sampler));
}
}
return biomes;
}
return biomes;
}
private Holder<Biome> getSurfaceStructureBiome(Engine engine, int quartX, int quartZ,
@@ -338,28 +387,37 @@ final class IrisModdedBiomeSource extends BiomeSource {
throw new IllegalStateException("Iris structure biome source is not bound to its generator");
}
Engine engine = current.awaitStructureEngine();
Registry<Biome> registry = biomeRegistry();
if (registry == null) {
throw new IllegalStateException("Iris structure biome lookup has no biome registry");
GenerationSessionLease lease = tryAcquireGenerationLease(engine, "modded_structure_state_biome");
if (lease == null) {
throw new IllegalStateException("Iris structure biome lookup was rejected during an engine transition");
}
IrisBiome irisBiome;
if (isGuaranteedSurfaceBiome(quartY, engine.getMinHeight())) {
irisBiome = engine.getComplex().getTrueBiomeStream().get(quartX << 2, quartZ << 2);
} else {
BiomeResolution resolution = resolveBiomeResolution(engine, quartX, quartY, quartZ);
irisBiome = resolution == null ? null : resolution.irisBiome();
try (lease; IrisContext.Scope ignored = IrisContext.open(engine, lease.sessionId(), null)) {
if (!isReady(engine)) {
throw new IllegalStateException("Iris structure biome lookup has no active engine runtime");
}
Registry<Biome> registry = biomeRegistry();
if (registry == null) {
throw new IllegalStateException("Iris structure biome lookup has no biome registry");
}
IrisBiome irisBiome;
if (isGuaranteedSurfaceBiome(quartY, engine.getMinHeight())) {
irisBiome = engine.getComplex().getTrueBiomeStream().get(quartX << 2, quartZ << 2);
} else {
BiomeResolution resolution = resolveBiomeResolution(engine, quartX, quartY, quartZ);
irisBiome = resolution == null ? null : resolution.irisBiome();
}
if (irisBiome == null) {
throw new IllegalStateException("Iris returned no structure biome at quart "
+ quartX + "," + quartY + "," + quartZ);
}
String derivativeKey = irisBiome.getStructureDerivativeKey();
Holder<Biome> resolved = resolveHolder(registry, derivativeKey);
if (resolved == null) {
throw new IllegalStateException("Iris structure biome derivative '" + derivativeKey
+ "' is not registered at quart " + quartX + "," + quartY + "," + quartZ);
}
return resolved;
}
if (irisBiome == null) {
throw new IllegalStateException("Iris returned no structure biome at quart "
+ quartX + "," + quartY + "," + quartZ);
}
String derivativeKey = irisBiome.getStructureDerivativeKey();
Holder<Biome> resolved = resolveHolder(registry, derivativeKey);
if (resolved == null) {
throw new IllegalStateException("Iris structure biome derivative '" + derivativeKey
+ "' is not registered at quart " + quartX + "," + quartY + "," + quartZ);
}
return resolved;
}
static long biomeResolutionSeed(long worldSeed, int blockX, int blockY, int blockZ) {
@@ -403,6 +461,21 @@ final class IrisModdedBiomeSource extends BiomeSource {
return current == null ? null : current.structureEngineOrNull();
}
private GenerationSessionLease tryAcquireGenerationLease(Engine engine, String operation) {
if (engine == null || engine.isClosed()) {
return null;
}
try {
return engine.acquireGenerationLease(operation);
} catch (GenerationSessionException e) {
if (engine.isClosing() || e.isExpectedTeardown()) {
return null;
}
throw new IllegalStateException("Iris biome source could not acquire generation session for "
+ operation + ".", e);
}
}
private Set<String> possibleStructureBiomeKeys() {
Set<String> cached = possibleStructureBiomeKeys;
if (cached != null) {
@@ -429,24 +502,33 @@ final class IrisModdedBiomeSource extends BiomeSource {
return Set.of();
}
Engine engine = current.structureEngineOrNull();
if (!isReady(engine)) {
if (engine == null) {
return current.configuredStructureBiomeKeys();
}
LinkedHashSet<String> possible = new LinkedHashSet<>();
String namespace = engine.getDimension().getLoadKey().toLowerCase(Locale.ROOT);
for (IrisBiome irisBiome : engine.getAllBiomes()) {
String derivative = normalizeKey(irisBiome.getStructureDerivativeKey());
if (derivative != null) {
possible.add(derivative);
}
if (!irisBiome.isCustom()) {
continue;
}
for (IrisBiomeCustom customBiome : irisBiome.getCustomDerivitives()) {
possible.add(namespace + ":" + customBiome.getId().toLowerCase(Locale.ROOT));
}
GenerationSessionLease lease = tryAcquireGenerationLease(engine, "modded_structure_biome_keys");
if (lease == null) {
throw new IllegalStateException("Iris structure biome key lookup was rejected during an engine transition");
}
try (lease; IrisContext.Scope ignored = IrisContext.open(engine, lease.sessionId(), null)) {
if (!isReady(engine)) {
throw new IllegalStateException("Iris structure biome key lookup has no active engine runtime");
}
LinkedHashSet<String> possible = new LinkedHashSet<>();
String namespace = engine.getDimension().getLoadKey().toLowerCase(Locale.ROOT);
for (IrisBiome irisBiome : engine.getAllBiomes()) {
String derivative = normalizeKey(irisBiome.getStructureDerivativeKey());
if (derivative != null) {
possible.add(derivative);
}
if (!irisBiome.isCustom()) {
continue;
}
for (IrisBiomeCustom customBiome : irisBiome.getCustomDerivitives()) {
possible.add(namespace + ":" + customBiome.getId().toLowerCase(Locale.ROOT));
}
}
return Set.copyOf(possible);
}
return Set.copyOf(possible);
}
private static Set<String> registeredBiomeKeys(Registry<Biome> registry) {
@@ -517,6 +599,10 @@ final class IrisModdedBiomeSource extends BiomeSource {
this.possibleBiomes = possibleBiomes;
}
private void clearCache() {
resolvedBiomes.clear();
}
@Override
protected MapCodec<? extends BiomeSource> codec() {
throw new UnsupportedOperationException("Structure state biome sources are not serializable");
@@ -22,6 +22,7 @@ import art.arcane.iris.core.loader.IrisData;
import art.arcane.iris.core.pack.PackValidationRegistry;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.framework.GenerationSessionException;
import art.arcane.iris.engine.framework.GenerationSessionLease;
import art.arcane.iris.engine.framework.IrisStructureLocator;
import art.arcane.iris.engine.framework.NativeStructureGenerationPolicy;
import art.arcane.iris.engine.object.IrisBiome;
@@ -37,6 +38,7 @@ import art.arcane.iris.nativegen.NativeStructurePostProcessor;
import art.arcane.iris.spi.IrisPlatforms;
import art.arcane.iris.spi.PlatformBiome;
import art.arcane.iris.spi.PlatformBlockState;
import art.arcane.iris.util.project.context.IrisContext;
import art.arcane.iris.util.project.hunk.Hunk;
import art.arcane.volmlib.util.math.RNG;
import com.mojang.datafixers.util.Pair;
@@ -185,6 +187,7 @@ public final class IrisModdedChunkGenerator extends ChunkGenerator {
private final ConcurrentHashMap<NativeStructureStartKey, Integer> worldCheckStructureShifts = new ConcurrentHashMap<>();
private final AtomicBoolean announced = new AtomicBoolean(false);
private volatile boolean vanillaSpawnBiomesInitialized;
private volatile boolean unloading;
private volatile Engine engine;
private volatile String activePack;
private volatile String activeDimensionKey;
@@ -221,6 +224,7 @@ public final class IrisModdedChunkGenerator extends ChunkGenerator {
}
synchronized void repointAndBind(ServerLevel level, String pack, String packDimensionKey, long seed) {
requireBindingAllowed();
if (level.getChunkSource().getGenerator() != this) {
throw new IllegalArgumentException("ServerLevel does not use Iris generator '" + dimensionKey + "'");
}
@@ -258,11 +262,13 @@ public final class IrisModdedChunkGenerator extends ChunkGenerator {
}
public synchronized void unbindEngine() {
unloading = true;
ServerLevel level = boundLevel();
unbindEngine(level);
}
synchronized void unbindEngine(ServerLevel level) {
unloading = true;
if (level != null) {
ModdedWorldEngines.evictOrThrow(level);
}
@@ -322,13 +328,16 @@ public final class IrisModdedChunkGenerator extends ChunkGenerator {
BlockPos pos, int radius,
boolean findUnexplored) {
Engine current = engine();
Pair<BlockPos, Holder<Structure>> irisPlaced = findNearestIrisStructure(
level, holders, pos, Math.max(1, radius), findUnexplored, current);
HolderSet<Structure> reachable = filterReachableNativeStructures(level, holders, current);
Pair<BlockPos, Holder<Structure>> nativeLocated = reachable.size() == 0
? null
: super.findNearestMapStructure(level, reachable, pos, radius, findUnexplored);
return NativeStructureLocateResults.nearest(pos, irisPlaced, nativeLocated);
try (GenerationSessionLease lease = requireGenerationLease(current, "modded_structure_locate");
IrisContext.Scope ignored = IrisContext.open(current, lease.sessionId(), null)) {
Pair<BlockPos, Holder<Structure>> irisPlaced = findNearestIrisStructure(
level, holders, pos, Math.max(1, radius), findUnexplored, current);
HolderSet<Structure> reachable = filterReachableNativeStructures(level, holders, current);
Pair<BlockPos, Holder<Structure>> nativeLocated = reachable.size() == 0
? null
: super.findNearestMapStructure(level, reachable, pos, radius, findUnexplored);
return NativeStructureLocateResults.nearest(pos, irisPlaced, nativeLocated);
}
}
public boolean isNativeStructureReachable(Holder<Structure> structure) {
@@ -412,8 +421,10 @@ public final class IrisModdedChunkGenerator extends ChunkGenerator {
}
private Engine engine() {
requireBindingAllowed();
Engine cached = engine;
if (cached != null) {
requireCompletedShutdown(cached);
if (cached != null && !cached.isClosed()) {
return cached;
}
ServerLevel level = boundLevel();
@@ -423,14 +434,17 @@ public final class IrisModdedChunkGenerator extends ChunkGenerator {
return bindEngine(level);
}
void bindLevel(ServerLevel level) {
synchronized void bindLevel(ServerLevel level) {
if (level.getChunkSource().getGenerator() != this) {
throw new IllegalArgumentException("ServerLevel does not use Iris generator '" + dimensionKey + "'");
}
requireCompletedShutdown(engine);
unloading = false;
bindEngine(level);
}
private Engine bindEngine(ServerLevel level) {
requireBindingAllowed();
try {
requireGlobalStructureGeneration(
level.getServer().getWorldGenSettings().options().generateStructures(), dimensionKey);
@@ -439,18 +453,22 @@ public final class IrisModdedChunkGenerator extends ChunkGenerator {
throw error;
}
Engine cached = engine;
requireCompletedShutdown(cached);
if (cached != null && !cached.isClosed() && cached.getComplex() != null) {
engineBinding.complete(cached);
return cached;
}
synchronized (this) {
requireBindingAllowed();
Engine existing = engine;
requireCompletedShutdown(existing);
if (existing != null && !existing.isClosed() && existing.getComplex() != null) {
engineBinding.complete(existing);
return existing;
}
try {
Engine created = ModdedWorldEngines.get(level, activePack, activeDimensionKey, seedOverride);
requireCompletedShutdown(created);
if (created.isClosed() || created.getComplex() == null) {
throw new IllegalStateException("Iris generator '" + dimensionKey
+ "' created an engine without a ready biome complex");
@@ -473,6 +491,19 @@ public final class IrisModdedChunkGenerator extends ChunkGenerator {
}
}
private void requireCompletedShutdown(Engine current) {
if (current != null && current.isClosing() && !current.isClosed()) {
throw new IllegalStateException("Iris generator '" + dimensionKey
+ "' cannot bind while its previous engine shutdown remains incomplete");
}
}
private void requireBindingAllowed() {
if (unloading) {
throw new IllegalStateException("Iris generator '" + dimensionKey + "' is unloading and cannot bind an engine");
}
}
static void requireGlobalStructureGeneration(boolean enabled, String dimensionKey) {
if (enabled) {
return;
@@ -483,8 +514,10 @@ public final class IrisModdedChunkGenerator extends ChunkGenerator {
}
private Engine engineOrNull() {
requireBindingAllowed();
Engine cached = engine;
if (cached != null) {
requireCompletedShutdown(cached);
if (cached != null && !cached.isClosed()) {
return cached;
}
try {
@@ -495,8 +528,10 @@ public final class IrisModdedChunkGenerator extends ChunkGenerator {
}
Engine structureEngineOrNull() {
requireBindingAllowed();
engineBinding.throwIfFailed(dimensionKey);
Engine cached = engine;
requireCompletedShutdown(cached);
if (cached != null && !cached.isClosed() && cached.getComplex() != null) {
return cached;
}
@@ -505,11 +540,14 @@ public final class IrisModdedChunkGenerator extends ChunkGenerator {
}
Engine awaitStructureEngine() {
requireBindingAllowed();
Engine current = engine;
requireCompletedShutdown(current);
if (current != null && !current.isClosed() && current.getComplex() != null) {
return current;
}
Engine bound = engineBinding.await(dimensionKey);
requireCompletedShutdown(bound);
if (bound.isClosed() || bound.getComplex() == null) {
throw new IllegalStateException("Iris generator '" + dimensionKey
+ "' completed bootstrap without a ready biome complex");
@@ -518,7 +556,9 @@ public final class IrisModdedChunkGenerator extends ChunkGenerator {
}
private Engine requireDataQueryEngine(String operation) {
requireBindingAllowed();
Engine current = engine;
requireCompletedShutdown(current);
if (current != null && !current.isClosed() && current.getComplex() != null) {
return current;
}
@@ -561,18 +601,21 @@ public final class IrisModdedChunkGenerator extends ChunkGenerator {
return configuredStructureBiomeKeys;
}
Engine current = engine;
Iterable<IrisBiome> biomes;
String namespace;
if (current != null && !current.isClosed()) {
biomes = current.getAllBiomes();
namespace = current.getDimension().getLoadKey();
} else {
ConfiguredPack configured = configuredPack();
Set<String> resolved = collectConfiguredBiomeKeys(configured.dimension(), configured.data());
configuredStructureBiomeKeys = resolved;
return resolved;
if (current != null && !current.isClosed() && !current.isClosing()) {
try (GenerationSessionLease lease = current.acquireGenerationLease("modded_configured_biome_keys");
IrisContext.Scope ignored = IrisContext.open(current, lease.sessionId(), null)) {
Set<String> resolved = collectConfiguredBiomeKeys(
current.getAllBiomes(), current.getDimension().getLoadKey());
configuredStructureBiomeKeys = resolved;
return resolved;
} catch (GenerationSessionException e) {
if (!current.isClosing() && !e.isExpectedTeardown()) {
throw new IllegalStateException("Iris configured biome lookup could not acquire its engine runtime.", e);
}
}
}
Set<String> resolved = collectConfiguredBiomeKeys(biomes, namespace);
ConfiguredPack configured = configuredPack();
Set<String> resolved = collectConfiguredBiomeKeys(configured.dimension(), configured.data());
configuredStructureBiomeKeys = resolved;
return resolved;
}
@@ -654,7 +697,8 @@ public final class IrisModdedChunkGenerator extends ChunkGenerator {
}
public Engine engineIfBound() {
return engine;
Engine current = engine;
return unloading || current == null || current.isClosing() || current.isClosed() ? null : current;
}
public Engine commandEngine() {
@@ -710,23 +754,26 @@ public final class IrisModdedChunkGenerator extends ChunkGenerator {
return;
}
String namespace = current.getDimension().getLoadKey().toLowerCase(Locale.ROOT);
for (IrisBiome irisBiome : current.getDimension().getReachableBiomes(current)) {
if (irisBiome == null || !irisBiome.isCustom()) {
continue;
}
Holder<Biome> vanillaHolder = resolveBiomeHolder(registry, irisBiome.getVanillaDerivativeKey());
if (vanillaHolder == null) {
continue;
}
for (IrisBiomeCustom customBiome : irisBiome.getCustomDerivitives()) {
Holder<Biome> customHolder = resolveBiomeHolder(registry, namespace + ":" + customBiome.getId());
if (customHolder != null) {
vanillaSpawnBiomes.putIfAbsent(customHolder.value(), vanillaHolder);
try (GenerationSessionLease lease = requireGenerationLease(current, "modded_spawn_biomes");
IrisContext.Scope ignored = IrisContext.open(current, lease.sessionId(), null)) {
String namespace = current.getDimension().getLoadKey().toLowerCase(Locale.ROOT);
for (IrisBiome irisBiome : current.getDimension().getReachableBiomes(current)) {
if (irisBiome == null || !irisBiome.isCustom()) {
continue;
}
Holder<Biome> vanillaHolder = resolveBiomeHolder(registry, irisBiome.getVanillaDerivativeKey());
if (vanillaHolder == null) {
continue;
}
for (IrisBiomeCustom customBiome : irisBiome.getCustomDerivitives()) {
Holder<Biome> customHolder = resolveBiomeHolder(registry, namespace + ":" + customBiome.getId());
if (customHolder != null) {
vanillaSpawnBiomes.putIfAbsent(customHolder.value(), vanillaHolder);
}
}
}
vanillaSpawnBiomesInitialized = true;
}
vanillaSpawnBiomesInitialized = true;
}
private Holder<Biome> resolveBiomeHolder(Registry<Biome> registry, String key) {
@@ -750,8 +797,12 @@ public final class IrisModdedChunkGenerator extends ChunkGenerator {
@Override
public CompletableFuture<ChunkAccess> createBiomes(RandomState randomState, Blender blender,
StructureManager structureManager, ChunkAccess chunk) {
chunk.fillBiomesFromNoise(structureBiomeSource::getVisibleNoiseBiome, randomState.sampler());
return CompletableFuture.completedFuture(chunk);
Engine current = engine();
try (GenerationSessionLease lease = requireGenerationLease(current, "modded_create_biomes");
IrisContext.Scope ignored = IrisContext.open(current, lease.sessionId(), null)) {
chunk.fillBiomesFromNoise(structureBiomeSource::getVisibleNoiseBiome, randomState.sampler());
return CompletableFuture.completedFuture(chunk);
}
}
@Override
@@ -759,36 +810,46 @@ public final class IrisModdedChunkGenerator extends ChunkGenerator {
Engine generationEngine = engine();
ChunkPos pos = chunk.getPos();
lastChunkGenAt = System.currentTimeMillis();
if (announced.compareAndSet(false, true)) {
LOGGER.info("Iris generating {} through IrisModdedChunkGenerator (dim={} first chunk {},{})",
dimensionKey, generationEngine.getDimension().getLoadKey(), pos.x(), pos.z());
}
LOGGER.debug("Iris generating chunk {},{}", pos.x(), pos.z());
int dimMinY = generationEngine.getMinHeight();
int dimMaxY = generationEngine.getMaxHeight();
int height = dimMaxY - dimMinY;
PlatformBlockState air = IrisPlatforms.get().registries().air();
if (PARALLEL_CHUNK_SYSTEM) {
return CompletableFuture.completedFuture(
generateTerrain(chunk, generationEngine, pos, dimMinY, height, air));
generateTerrain(chunk, generationEngine, pos, air));
}
return CompletableFuture.supplyAsync(
() -> generateTerrain(chunk, generationEngine, pos, dimMinY, height, air),
() -> generateTerrain(chunk, generationEngine, pos, air),
genPool);
}
private ChunkAccess generateTerrain(ChunkAccess chunk, Engine generationEngine, ChunkPos pos,
int dimMinY, int height, PlatformBlockState air) {
ModdedBlockBuffer blocks = new ModdedBlockBuffer(height, air);
Hunk<PlatformBiome> biomes = Hunk.newArrayHunk(16, height, 16);
try {
PlatformBlockState air) {
try (GenerationSessionLease lease = generationEngine.acquireGenerationLease("modded_chunk_pipeline");
IrisContext.Scope ignored = IrisContext.open(generationEngine, lease.sessionId(), null)) {
if (announced.compareAndSet(false, true)) {
LOGGER.info("Iris generating {} through IrisModdedChunkGenerator (dim={} first chunk {},{})",
dimensionKey, generationEngine.getDimension().getLoadKey(), pos.x(), pos.z());
}
int dimMinY = generationEngine.getMinHeight();
int dimMaxY = generationEngine.getMaxHeight();
int height = dimMaxY - dimMinY;
ModdedBlockBuffer blocks = new ModdedBlockBuffer(height, air);
Hunk<PlatformBiome> biomes = Hunk.newArrayHunk(16, height, 16);
generationEngine.generate(pos.getMinBlockX(), pos.getMinBlockZ(), blocks, biomes, false);
writeBlocks(chunk, blocks, dimMinY, height);
writeTerrainHeightmaps(chunk, generationEngine, pos, height);
Heightmap.primeHeightmaps(chunk, EnumSet.of(
Heightmap.Types.MOTION_BLOCKING,
Heightmap.Types.MOTION_BLOCKING_NO_LEAVES));
ModdedWorldManager.enqueueGenerated(generationEngine, pos.x(), pos.z());
return chunk;
} catch (GenerationSessionException e) {
if (e.isExpectedTeardown()) {
if (generationEngine.isClosing() || e.isExpectedTeardown()) {
LOGGER.debug("Iris chunk {},{} skipped: engine sealed for hotload/teardown", pos.x(), pos.z());
return chunk;
throw new IllegalStateException(
"Iris chunk generation was rejected during an engine transition.", e);
}
LOGGER.error("Iris failed to generate chunk {},{}", pos.x(), pos.z(), e);
throw new IllegalStateException("Iris generation failed for chunk " + pos.x() + "," + pos.z(), e);
@@ -796,14 +857,6 @@ public final class IrisModdedChunkGenerator extends ChunkGenerator {
LOGGER.error("Iris failed to generate chunk {},{}", pos.x(), pos.z(), e);
throw new IllegalStateException("Iris generation failed for chunk " + pos.x() + "," + pos.z(), e);
}
writeBlocks(chunk, blocks, dimMinY, height);
writeTerrainHeightmaps(chunk, generationEngine, pos, height);
Heightmap.primeHeightmaps(chunk, EnumSet.of(
Heightmap.Types.MOTION_BLOCKING,
Heightmap.Types.MOTION_BLOCKING_NO_LEAVES));
ModdedWorldManager.enqueueGenerated(generationEngine, pos.x(), pos.z());
return chunk;
}
private void writeTerrainHeightmaps(ChunkAccess chunk, Engine generationEngine, ChunkPos pos, int height) {
@@ -885,20 +938,31 @@ public final class IrisModdedChunkGenerator extends ChunkGenerator {
@Override
public void applyBiomeDecoration(WorldGenLevel level, ChunkAccess chunk, StructureManager structureManager) {
placeVanillaStructures(level, chunk, structureManager);
Engine current = engine();
try (GenerationSessionLease lease = requireGenerationLease(current, "modded_biome_decoration");
IrisContext.Scope ignored = IrisContext.open(current, lease.sessionId(), null)) {
placeVanillaStructures(level, chunk, structureManager);
}
}
@Override
public void createStructures(RegistryAccess registryAccess, ChunkGeneratorStructureState structureState, StructureManager structureManager, ChunkAccess chunk, StructureTemplateManager templateManager, ResourceKey<Level> levelKey) {
Engine current = engine();
Map<Structure, StructureStart> previousStarts = new HashMap<>(chunk.getAllStarts());
super.createStructures(registryAccess, structureState, structureManager, chunk, templateManager, levelKey);
adjustGeneratedStructures(registryAccess, chunk, previousStarts, current);
try (GenerationSessionLease lease = requireGenerationLease(current, "modded_create_structures");
IrisContext.Scope ignored = IrisContext.open(current, lease.sessionId(), null)) {
Map<Structure, StructureStart> previousStarts = new HashMap<>(chunk.getAllStarts());
super.createStructures(registryAccess, structureState, structureManager, chunk, templateManager, levelKey);
adjustGeneratedStructures(registryAccess, chunk, previousStarts, current);
}
}
@Override
public void createReferences(WorldGenLevel level, StructureManager structureManager, ChunkAccess chunk) {
super.createReferences(level, structureManager, chunk);
Engine current = engine();
try (GenerationSessionLease lease = requireGenerationLease(current, "modded_create_references");
IrisContext.Scope ignored = IrisContext.open(current, lease.sessionId(), null)) {
super.createReferences(level, structureManager, chunk);
}
}
private void adjustGeneratedStructures(RegistryAccess registryAccess, ChunkAccess chunk,
@@ -1168,7 +1232,12 @@ public final class IrisModdedChunkGenerator extends ChunkGenerator {
public int getBaseHeight(int x, int z, Heightmap.Types type, LevelHeightAccessor heightAccessor, RandomState randomState) {
Engine current = requireDataQueryEngine("base height");
boolean ignoreFluid = !type.isOpaque().test(Blocks.WATER.defaultBlockState());
return heightAccessor.getMinY() + current.getHeight(x, z, ignoreFluid) + 1;
try (GenerationSessionLease lease = current.acquireGenerationLease("modded_base_height");
IrisContext.Scope ignored = IrisContext.open(current, lease.sessionId(), null)) {
return heightAccessor.getMinY() + current.getHeight(x, z, ignoreFluid) + 1;
} catch (GenerationSessionException e) {
throw new IllegalStateException("Iris base height query could not acquire its engine runtime.", e);
}
}
@Override
@@ -1176,17 +1245,30 @@ public final class IrisModdedChunkGenerator extends ChunkGenerator {
int minY = heightAccessor.getMinY();
BlockState[] states = new BlockState[heightAccessor.getHeight()];
Engine current = requireDataQueryEngine("base column");
BlockState airState = Blocks.AIR.defaultBlockState();
int surface = current.getHeight(x, z, true);
int fluid = current.getHeight(x, z, false);
BlockState stone = Blocks.STONE.defaultBlockState();
BlockState water = Blocks.WATER.defaultBlockState();
int solidEnd = Math.max(0, Math.min(states.length, surface + 1));
int fluidEnd = Math.max(solidEnd, Math.max(0, Math.min(states.length, fluid + 1)));
Arrays.fill(states, 0, solidEnd, stone);
Arrays.fill(states, solidEnd, fluidEnd, water);
Arrays.fill(states, fluidEnd, states.length, airState);
return new NoiseColumn(minY, states);
try (GenerationSessionLease lease = current.acquireGenerationLease("modded_base_column");
IrisContext.Scope ignored = IrisContext.open(current, lease.sessionId(), null)) {
BlockState airState = Blocks.AIR.defaultBlockState();
int surface = current.getHeight(x, z, true);
int fluid = current.getHeight(x, z, false);
BlockState stone = Blocks.STONE.defaultBlockState();
BlockState water = Blocks.WATER.defaultBlockState();
int solidEnd = Math.max(0, Math.min(states.length, surface + 1));
int fluidEnd = Math.max(solidEnd, Math.max(0, Math.min(states.length, fluid + 1)));
Arrays.fill(states, 0, solidEnd, stone);
Arrays.fill(states, solidEnd, fluidEnd, water);
Arrays.fill(states, fluidEnd, states.length, airState);
return new NoiseColumn(minY, states);
} catch (GenerationSessionException e) {
throw new IllegalStateException("Iris base column query could not acquire its engine runtime.", e);
}
}
private GenerationSessionLease requireGenerationLease(Engine current, String operation) {
try {
return current.acquireGenerationLease(operation);
} catch (GenerationSessionException exception) {
throw new IllegalStateException("Iris " + operation + " could not acquire its engine runtime.", exception);
}
}
@Override
@@ -21,6 +21,7 @@ package art.arcane.iris.modded;
import art.arcane.iris.core.loader.IrisData;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.framework.LootResolver;
import art.arcane.iris.engine.framework.TreeBlockMaterial;
import art.arcane.iris.engine.object.IrisBiome;
import art.arcane.iris.engine.object.IrisBlockData;
import art.arcane.iris.engine.object.IrisBlockDrops;
@@ -28,11 +29,13 @@ import art.arcane.iris.engine.object.IrisLoot;
import art.arcane.iris.engine.object.IrisMarker;
import art.arcane.iris.engine.object.IrisRegion;
import art.arcane.iris.spi.PlatformBlockState;
import art.arcane.iris.modded.service.ModdedTreeFellerService;
import art.arcane.volmlib.util.collection.KList;
import art.arcane.volmlib.util.math.RNG;
import art.arcane.volmlib.util.matter.MatterMarker;
import net.minecraft.core.BlockPos;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.util.Mth;
import net.minecraft.util.RandomSource;
import net.minecraft.world.entity.EntityTypes;
@@ -43,6 +46,7 @@ import net.minecraft.world.level.chunk.ChunkGenerator;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.List;
import java.util.concurrent.ConcurrentHashMap;
public final class ModdedBlockBreakHandler {
@@ -52,12 +56,24 @@ public final class ModdedBlockBreakHandler {
private ModdedBlockBreakHandler() {
}
public static void prepare(ServerLevel level, BlockPos position, BlockState brokenState) {
public static void prepare(
ServerLevel level,
ServerPlayer player,
BlockPos position,
BlockState brokenState
) {
if (ModdedTreeFellerService.isBreakProbe()) {
return;
}
if (engineFor(level) == null) {
return;
}
BreakKey key = new BreakKey(level, position.asLong());
PendingBreak pending = new PendingBreak(brokenState);
ModdedTreeFellerService treeFeller = treeFellerService();
ModdedTreeFellerService.PreparedOrigin preparedOrigin = treeFeller == null
? null
: treeFeller.prepare(level, player, position, brokenState);
PendingBreak pending = new PendingBreak(brokenState, preparedOrigin);
PENDING.put(key, pending);
ModdedScheduler scheduler = ModdedEngineBootstrap.schedulerOrNull();
if (scheduler != null) {
@@ -69,11 +85,15 @@ public final class ModdedBlockBreakHandler {
PENDING.clear();
}
public static void cancel(ServerLevel level, BlockPos position) {
PENDING.remove(new BreakKey(level, position.asLong()));
}
public static Result complete(ServerLevel level, BlockPos position, BlockState fallbackState) {
BreakKey key = new BreakKey(level, position.asLong());
PendingBreak pending = PENDING.remove(key);
BlockState brokenState = pending == null ? fallbackState : pending.brokenState();
return evaluateSafely(level, position, brokenState);
PendingBreak resolved = pending == null ? new PendingBreak(fallbackState, null) : pending;
return evaluateSafely(level, position, resolved);
}
public static Result completePrepared(ServerLevel level, BlockPos position) {
@@ -82,7 +102,7 @@ public final class ModdedBlockBreakHandler {
if (pending == null || level.getBlockState(position).equals(pending.brokenState())) {
return null;
}
return PENDING.remove(key, pending) ? evaluateSafely(level, position, pending.brokenState()) : null;
return PENDING.remove(key, pending) ? evaluateSafely(level, position, pending) : null;
}
public static void spawn(ServerLevel level, BlockPos position, KList<ItemStack> drops) {
@@ -115,13 +135,15 @@ public final class ModdedBlockBreakHandler {
if (level.getBlockState(position).equals(pending.brokenState())) {
return;
}
Result result = evaluateSafely(level, position, pending.brokenState());
spawn(level, position, result.drops());
Result result = evaluateSafely(level, position, pending);
if (!result.routeCombinedDrops(List.of())) {
spawn(level, position, result.drops());
}
}
private static Result evaluateSafely(ServerLevel level, BlockPos position, BlockState brokenState) {
private static Result evaluateSafely(ServerLevel level, BlockPos position, PendingBreak pending) {
try {
return evaluate(level, position, brokenState);
return evaluate(level, position, pending);
} catch (Throwable error) {
LOGGER.error("Iris block-break processing failed at {},{},{} in {}", position.getX(), position.getY(), position.getZ(),
level.dimension().identifier(), error);
@@ -129,13 +151,56 @@ public final class ModdedBlockBreakHandler {
}
}
private static Result evaluate(ServerLevel level, BlockPos position, BlockState brokenState) {
private static Result evaluate(ServerLevel level, BlockPos position, PendingBreak pending) {
Engine engine = engineFor(level);
if (engine == null || engine.isClosed()) {
return Result.empty();
}
removeMarker(engine, position);
Result drops = evaluateDrops(level, position, pending.brokenState(), engine);
ModdedTreeFellerService treeFeller = treeFellerService();
ModdedTreeFellerService.OriginDropRoute route = treeFeller == null || pending.preparedOrigin() == null
? null
: treeFeller.completeOrigin(pending.preparedOrigin());
if (route == null) {
clearTreeProvenance(engine, position);
}
return drops.withRoute(route);
}
public static Result evaluateManagedDrops(ServerLevel level, BlockPos position, BlockState brokenState) {
Engine engine = engineFor(level);
if (engine == null || engine.isClosed()) {
return Result.empty();
}
try {
return evaluateDrops(level, position, brokenState, engine);
} catch (Throwable error) {
LOGGER.error("Iris managed block-drop processing failed at {},{},{} in {}", position.getX(), position.getY(), position.getZ(),
level.dimension().identifier(), error);
return Result.empty();
}
}
public static void completeManagedBreak(ServerLevel level, BlockPos position) {
Engine engine = engineFor(level);
if (engine != null && !engine.isClosed()) {
removeMarker(engine, position);
clearTreeProvenance(engine, position);
}
}
public static void clearPlacedProvenance(ServerLevel level, BlockPos position) {
completeManagedBreak(level, position);
}
private static Result evaluateDrops(
ServerLevel level,
BlockPos position,
BlockState brokenState,
Engine engine
) {
KList<IrisBlockDrops> providers = providers(engine, position, brokenState);
if (providers.isEmpty()) {
return Result.empty();
@@ -155,7 +220,7 @@ public final class ModdedBlockBreakHandler {
}
}
}
return new Result(drops, replaceVanillaDrops);
return new Result(drops, replaceVanillaDrops, null);
}
private static void removeMarker(Engine engine, BlockPos position) {
@@ -174,6 +239,12 @@ public final class ModdedBlockBreakHandler {
}
}
private static void clearTreeProvenance(Engine engine, BlockPos position) {
int mantleY = position.getY() - engine.getMinHeight();
engine.getMantle().getMantle().remove(position.getX(), mantleY, position.getZ(), String.class);
engine.getMantle().getMantle().remove(position.getX(), mantleY, position.getZ(), TreeBlockMaterial.class);
}
private static KList<IrisBlockDrops> providers(Engine engine, BlockPos position, BlockState brokenState) {
KList<IrisBlockDrops> providers = new KList<>();
IrisData data = engine.getData();
@@ -227,7 +298,7 @@ public final class ModdedBlockBreakHandler {
return exact ? configuredState.equals(brokenState) : configuredState.getBlock() == brokenState.getBlock();
}
private static Engine engineFor(ServerLevel level) {
public static Engine engineFor(ServerLevel level) {
ChunkGenerator generator = level.getChunkSource().getGenerator();
if (!(generator instanceof IrisModdedChunkGenerator irisGenerator)) {
return null;
@@ -244,15 +315,72 @@ public final class ModdedBlockBreakHandler {
}
}
public record Result(KList<ItemStack> drops, boolean replaceVanillaDrops) {
private static ModdedTreeFellerService treeFellerService() {
return ModdedEngineBootstrap.services().service(ModdedTreeFellerService.class);
}
public static final class Result {
private final KList<ItemStack> drops;
private final boolean replaceVanillaDrops;
private final ModdedTreeFellerService.OriginDropRoute route;
private Result(
KList<ItemStack> drops,
boolean replaceVanillaDrops,
ModdedTreeFellerService.OriginDropRoute route
) {
this.drops = drops;
this.replaceVanillaDrops = replaceVanillaDrops;
this.route = route;
}
public KList<ItemStack> drops() {
return drops;
}
public boolean replaceVanillaDrops() {
return replaceVanillaDrops;
}
public boolean routeCombinedDrops(Iterable<ItemStack> vanillaDrops) {
if (route == null) {
return false;
}
return route.route(combinedDrops(vanillaDrops));
}
public KList<ItemStack> combinedDrops(Iterable<ItemStack> vanillaDrops) {
KList<ItemStack> combined = new KList<>();
if (!replaceVanillaDrops) {
for (ItemStack stack : vanillaDrops) {
if (stack != null && !stack.isEmpty()) {
combined.add(stack.copy());
}
}
}
for (ItemStack stack : drops) {
if (stack != null && !stack.isEmpty()) {
combined.add(stack.copy());
}
}
return combined;
}
private Result withRoute(ModdedTreeFellerService.OriginDropRoute route) {
return new Result(drops, replaceVanillaDrops, route);
}
private static Result empty() {
return new Result(new KList<>(), false);
return new Result(new KList<>(), false, null);
}
}
private record BreakKey(ServerLevel level, long position) {
}
private record PendingBreak(BlockState brokenState) {
private record PendingBreak(
BlockState brokenState,
ModdedTreeFellerService.PreparedOrigin preparedOrigin
) {
}
}
@@ -175,12 +175,18 @@ public final class ModdedDimensionManager {
}
return false;
}
IrisModdedChunkGenerator generator = level.getChunkSource().getGenerator()
instanceof IrisModdedChunkGenerator irisGenerator
? irisGenerator
: null;
boolean generatorUnbound = false;
try {
evacuate(server, level);
if (level.getChunkSource().getGenerator() instanceof IrisModdedChunkGenerator generator) {
generator.unbindEngine();
if (generator != null) {
generator.unbindEngine(level);
generatorUnbound = true;
}
ModdedWorldEngines.evict(level);
ModdedWorldEngines.evictOrThrow(level);
level.save(null, true, false);
serverAccess.removeLevel(server, key);
level.close();
@@ -191,12 +197,31 @@ public final class ModdedDimensionManager {
LOGGER.info("Iris removed runtime dimension '{}'", dimensionId);
return true;
} catch (Throwable e) {
rollbackRemoval(server, serverAccess, key, level, generator, generatorUnbound, e);
LOGGER.error("Iris failed to remove runtime dimension '{}'", dimensionId, e);
throw new IllegalStateException("Iris runtime dimension removal failed for " + dimensionId, e);
}
}
}
private static void rollbackRemoval(MinecraftServer server, ModdedServerAccess serverAccess,
ResourceKey<Level> key, ServerLevel level,
IrisModdedChunkGenerator generator, boolean generatorUnbound,
Throwable failure) {
try {
if (!generatorUnbound || generator == null || !serverAccess.hasLevel(server, key)) {
return;
}
generator.bindLevel(level);
} catch (Throwable rollbackFailure) {
if (rollbackFailure != failure) {
failure.addSuppressed(rollbackFailure);
}
LOGGER.error("Iris failed to restore the engine for retained runtime dimension '{}'",
key.identifier(), rollbackFailure);
}
}
public static boolean teleport(ServerPlayer player, MinecraftServer server, String dimensionId, double x, double y, double z) {
ServerLevel level = level(server, dimensionId);
if (level == null) {
@@ -42,6 +42,7 @@ import art.arcane.iris.modded.service.ModdedLogFilterService;
import art.arcane.iris.modded.service.ModdedPreservationService;
import art.arcane.iris.modded.service.ModdedSettingsHotloadService;
import art.arcane.iris.modded.service.ModdedStudioHotloadService;
import art.arcane.iris.modded.service.ModdedTreeFellerService;
import art.arcane.iris.spi.IrisPlatform;
import art.arcane.iris.spi.IrisPlatforms;
import art.arcane.iris.spi.IrisServices;
@@ -135,29 +136,82 @@ public final class ModdedEngineBootstrap {
}
}
public static void levelUnloaded(ServerLevel level) {
if (!(level.getChunkSource().getGenerator() instanceof IrisModdedChunkGenerator generator)) {
return;
}
try {
generator.unbindEngine(level);
} catch (Throwable exception) {
LOGGER.error("Iris engine unload failed for {}", level.dimension().identifier(), exception);
if (exception instanceof RuntimeException runtimeException) {
throw runtimeException;
}
if (exception instanceof Error fatalError) {
throw fatalError;
}
throw new IllegalStateException("Iris engine unload failed for "
+ level.dimension().identifier(), exception);
}
}
public static void stop() {
MinecraftServer stoppingServer = currentServer;
ModdedWorldCheck.serverStopped(stoppingServer);
ModdedProtocolHandler.stop();
ModdedPregenJob.shutdown();
ModdedObjectUndo.clearAll();
ModdedWandService.clearAll();
ModdedBlockBreakHandler.clear();
ModdedStudioCommands.clear();
ModdedWorldEngines.shutdown();
ModdedPrimaryWorldRouter.clear();
services().disableAll();
ModdedDimensionManager.clear();
Throwable failure = null;
failure = runStopStage(failure, "world check", () -> ModdedWorldCheck.serverStopped(stoppingServer));
failure = runStopStage(failure, "protocol", ModdedProtocolHandler::stop);
failure = runStopStage(failure, "pregenerator", ModdedPregenJob::shutdown);
failure = runStopStage(failure, "object undo", ModdedObjectUndo::clearAll);
failure = runStopStage(failure, "wand service", ModdedWandService::clearAll);
failure = runStopStage(failure, "block break handler", ModdedBlockBreakHandler::clear);
failure = runStopStage(failure, "studio commands", ModdedStudioCommands::clear);
failure = runStopStage(failure, "services", () -> services().disableAll());
failure = runStopStage(failure, "world engines", ModdedWorldEngines::shutdown);
failure = runStopStage(failure, "primary world router", ModdedPrimaryWorldRouter::clear);
failure = runStopStage(failure, "dimension manager", ModdedDimensionManager::clear);
ModdedScheduler scheduler = schedulerOrNull();
if (scheduler != null) {
scheduler.shutdown();
failure = runStopStage(failure, "scheduler", scheduler::shutdown);
}
IrisModdedChunkGenerator.shutdownGenPool();
ModdedSentry.flush();
ModdedStartup.reset();
currentServer = null;
spawnCaptureServer = null;
initialSpawnWasDefault = false;
failure = runStopStage(failure, "generation pool", IrisModdedChunkGenerator::shutdownGenPool);
failure = runStopStage(failure, "sentry", ModdedSentry::flush);
failure = runStopStage(failure, "startup state", ModdedStartup::reset);
failure = runStopStage(failure, "server state", () -> {
currentServer = null;
spawnCaptureServer = null;
initialSpawnWasDefault = false;
});
if (failure != null) {
LOGGER.error("Iris modded shutdown completed with failures", failure);
throw propagateStopFailure(failure);
}
}
private static Throwable runStopStage(Throwable failure, String stage, StopAction action) {
try {
action.run();
return failure;
} catch (Throwable stageFailure) {
LOGGER.error("Iris modded shutdown stage '{}' failed", stage, stageFailure);
if (failure == null) {
return stageFailure;
}
if (stageFailure != failure) {
failure.addSuppressed(stageFailure);
}
return failure;
}
}
private static RuntimeException propagateStopFailure(Throwable failure) {
if (failure instanceof RuntimeException runtimeException) {
return runtimeException;
}
if (failure instanceof Error fatalError) {
throw fatalError;
}
return new IllegalStateException("Iris modded shutdown completed with failures", failure);
}
private static void captureInitialSpawn(MinecraftServer server) {
@@ -317,6 +371,7 @@ public final class ModdedEngineBootstrap {
ModdedStudioHotloadService.class, new ModdedStudioHotloadService());
createdServices.register(ModdedChunkUpdateService.class, new ModdedChunkUpdateService());
createdServices.register(ModdedEntitySpawnService.class, new ModdedEntitySpawnService());
createdServices.register(ModdedTreeFellerService.class, new ModdedTreeFellerService());
bindService(PreservationRegistry.class, preservation, rollback);
bindService(EngineEffectsProvider.class,
@@ -377,6 +432,11 @@ public final class ModdedEngineBootstrap {
void restore() throws Throwable;
}
@FunctionalInterface
private interface StopAction {
void run() throws Throwable;
}
private static final class BindRollback {
private final ArrayDeque<RollbackAction> actions = new ArrayDeque<>();
@@ -18,7 +18,11 @@
package art.arcane.iris.modded;
import net.minecraft.core.BlockPos;
import net.minecraft.server.MinecraftServer;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.world.level.block.state.BlockState;
import java.io.File;
import java.nio.file.Path;
@@ -39,4 +43,8 @@ public interface ModdedLoader {
Path configDir();
File modJar();
boolean hasTreeFellerPermission(ServerPlayer player);
boolean canTreeFellerBreak(ServerLevel level, ServerPlayer player, BlockPos position, BlockState state);
}
@@ -26,7 +26,6 @@ import org.slf4j.LoggerFactory;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.function.Consumer;
public final class ModdedServiceManager {
private static final Logger LOGGER = LoggerFactory.getLogger("Iris");
@@ -92,7 +91,24 @@ public final class ModdedServiceManager {
return;
}
enabled = false;
forEachReversed(this::disableService);
Throwable failure = null;
ModdedService[] ordered = services.values().toArray(new ModdedService[0]);
for (int i = ordered.length - 1; i >= 0; i--) {
ModdedService service = ordered[i];
try {
service.onDisable();
} catch (Throwable serviceFailure) {
LOGGER.error("Iris service onDisable failed for {}", service.getClass().getName(), serviceFailure);
if (failure == null) {
failure = serviceFailure;
} else if (serviceFailure != failure) {
failure.addSuppressed(serviceFailure);
}
}
}
if (failure != null) {
throw new IllegalStateException("One or more Iris services failed to disable", failure);
}
}
synchronized void rollback(Throwable failure) {
@@ -106,14 +122,6 @@ public final class ModdedServiceManager {
services.clear();
}
private void disableService(ModdedService service) {
try {
service.onDisable();
} catch (Throwable error) {
LOGGER.error("Iris service onDisable failed for {}", service.getClass().getName(), error);
}
}
private void tickService(ModdedTickableService service, MinecraftServer server) {
try {
service.onServerTick(server);
@@ -144,10 +152,4 @@ public final class ModdedServiceManager {
return new IllegalStateException("Iris service failed to enable: " + service.getClass().getName(), failure);
}
private void forEachReversed(Consumer<ModdedService> action) {
ModdedService[] ordered = services.values().toArray(new ModdedService[0]);
for (int i = ordered.length - 1; i >= 0; i--) {
action.accept(ordered[i]);
}
}
}
@@ -102,7 +102,7 @@ public final class ModdedWorldEngines {
ModdedEngineBootstrap.bind();
PackValidationRegistry.requireLoadable(pack);
File packDir = resolvePack(pack, dimensionKey);
IrisData data = IrisData.get(packDir);
IrisData data = IrisData.openRuntime(packDir);
IrisDimension dimension = data.getDimensionLoader().load(dimensionKey);
if (dimension == null) {
LOGGER.error("Iris pack '{}' at {} does not contain dimension '{}' (expected dimensions/{}.json). Install a matching Iris pack and restart.",
@@ -184,17 +184,28 @@ public final class ModdedWorldEngines {
}
public static void shutdown() {
for (Map.Entry<ServerLevel, Engine> entry : ENGINES.entrySet()) {
Throwable failure = null;
for (Map.Entry<ServerLevel, Engine> entry : new ArrayList<>(ENGINES.entrySet())) {
ServerLevel level = entry.getKey();
Engine engine = entry.getValue();
try {
if (!engine.isClosed()) {
engine.close();
close(engine);
if (!ENGINES.remove(level, engine) && ENGINES.containsKey(level)) {
throw new IllegalStateException("Iris engine mapping changed during shutdown for "
+ level.dimension().identifier());
}
LOGGER.info("Iris engine closed for {}", entry.getKey().dimension().identifier());
LOGGER.info("Iris engine closed for {}", level.dimension().identifier());
} catch (Throwable e) {
LOGGER.error("Iris engine close failed for {}", entry.getKey().dimension().identifier(), e);
LOGGER.error("Iris engine close failed for {}", level.dimension().identifier(), e);
if (failure == null) {
failure = e;
} else if (e != failure) {
failure.addSuppressed(e);
}
}
}
ENGINES.clear();
if (failure != null) {
throw new IllegalStateException("One or more Iris engines failed to close; failed mappings were retained", failure);
}
}
}
@@ -22,6 +22,7 @@ import art.arcane.iris.core.IrisSettings;
import art.arcane.iris.core.gui.PregeneratorJob;
import art.arcane.iris.engine.IrisComplex;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.framework.EngineLifecycleTasks;
import art.arcane.iris.engine.framework.EngineWorldManager;
import art.arcane.iris.engine.framework.LootResolver;
import art.arcane.iris.engine.object.IRare;
@@ -79,6 +80,9 @@ public final class ModdedWorldManager implements EngineWorldManager {
private long lastAmbientAt;
private long lastCountAt;
private long lastInitialRecoveryAt;
private boolean initialSpawnQueueClosed;
private boolean mantleWarmupExecutorStopped;
private boolean mantleWarmupsCleared;
private volatile boolean closed;
private volatile int cachedEntityCount;
private volatile int cachedConsideredChunks;
@@ -116,6 +120,10 @@ public final class ModdedWorldManager implements EngineWorldManager {
}
public void serverTick(ServerLevel level) {
EngineLifecycleTasks.run(engine, "modded_world_manager_tick", () -> runServerTick(level));
}
private void runServerTick(ServerLevel level) {
if (closed || engine.isClosed() || engine.getMantle().getMantle().isClosed()) {
return;
}
@@ -239,7 +247,12 @@ public final class ModdedWorldManager implements EngineWorldManager {
IrisLogging.error("Iris could not schedule the initial entity-spawn follow-up because the modded scheduler is unavailable.");
return;
}
scheduler.laterGlobal(() -> runInitialFollowUp(level, chunkX, chunkZ), RNG.r.i(5, 200));
scheduler.laterGlobal(
() -> EngineLifecycleTasks.run(
engine,
"modded_world_manager_initial_spawn_followup",
() -> runInitialFollowUp(level, chunkX, chunkZ)),
RNG.r.i(5, 200));
}
private void runInitialFollowUp(ServerLevel level, int chunkX, int chunkZ) {
@@ -272,7 +285,14 @@ public final class ModdedWorldManager implements EngineWorldManager {
return;
}
try {
mantleWarmupExecutor.execute(() -> warmupMantleChunk(key, chunkX, chunkZ));
mantleWarmupExecutor.execute(() -> {
if (!EngineLifecycleTasks.run(
engine,
"modded_world_manager_mantle_warmup",
() -> warmupMantleChunk(key, chunkX, chunkZ))) {
mantleWarmups.remove(key);
}
});
} catch (RejectedExecutionException e) {
mantleWarmups.remove(key);
}
@@ -727,11 +747,36 @@ public final class ModdedWorldManager implements EngineWorldManager {
}
@Override
public void close() {
public synchronized void close() {
closed = true;
initialSpawnQueue.close();
mantleWarmupExecutor.shutdownNow();
mantleWarmups.clear();
Throwable failure = null;
if (!initialSpawnQueueClosed) {
try {
initialSpawnQueue.close();
initialSpawnQueueClosed = true;
} catch (Throwable e) {
failure = e;
}
}
if (!mantleWarmupExecutorStopped) {
try {
mantleWarmupExecutor.shutdownNow();
mantleWarmupExecutorStopped = true;
} catch (Throwable e) {
failure = appendCloseFailure(failure, e);
}
}
if (!mantleWarmupsCleared) {
try {
mantleWarmups.clear();
mantleWarmupsCleared = true;
} catch (Throwable e) {
failure = appendCloseFailure(failure, e);
}
}
if (failure != null) {
throw new IllegalStateException("Failed to completely stop the modded Iris world manager.", failure);
}
}
@Override
@@ -757,4 +802,14 @@ public final class ModdedWorldManager implements EngineWorldManager {
public void onSave() {
engine.getMantle().save();
}
private static Throwable appendCloseFailure(Throwable failure, Throwable next) {
if (failure == null) {
return next;
}
if (failure != next) {
failure.addSuppressed(next);
}
return failure;
}
}
@@ -23,6 +23,8 @@ import art.arcane.iris.core.gui.GuiHost;
import art.arcane.iris.core.loader.IrisRegistrant;
import art.arcane.iris.core.pack.PackDownloader;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.framework.GenerationSessionException;
import art.arcane.iris.engine.framework.GenerationSessionLease;
import art.arcane.iris.engine.framework.IrisStructureLocator;
import art.arcane.iris.engine.framework.Locator;
import art.arcane.iris.engine.framework.NativeStructureGenerationPolicy;
@@ -40,6 +42,7 @@ import art.arcane.iris.modded.ModdedLoader;
import art.arcane.iris.modded.ModdedPackInstaller;
import art.arcane.iris.spi.IrisLogging;
import art.arcane.iris.spi.PlatformBlockState;
import art.arcane.iris.util.project.context.IrisContext;
import art.arcane.volmlib.util.collection.KMap;
import art.arcane.volmlib.util.math.Position2;
import art.arcane.volmlib.util.matter.MatterMarker;
@@ -95,7 +98,11 @@ import java.util.Optional;
import java.util.Set;
import java.util.TreeMap;
import java.util.TreeSet;
import java.util.UUID;
import java.util.concurrent.CancellationException;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionException;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutionException;
import java.util.function.Predicate;
@@ -104,6 +111,7 @@ public final class IrisModdedCommands {
private static final Predicate<CommandSourceStack> GATE = Commands.hasPermission(Commands.LEVEL_GAMEMASTERS);
private static final long LOCATE_TIMEOUT_MS = 120000L;
private static final int NATIVE_STRUCTURE_LOCATE_RADIUS = 100;
private static final ConcurrentHashMap<UUID, CompletableFuture<Position2>> ACTIVE_LOCATE_REQUESTS = new ConcurrentHashMap<>();
private static final SuggestionProvider<CommandSourceStack> BIOME_KEYS = (CommandContext<CommandSourceStack> context, SuggestionsBuilder builder) -> suggestBiomeKeys(context, builder);
private static final SuggestionProvider<CommandSourceStack> REGION_KEYS = (CommandContext<CommandSourceStack> context, SuggestionsBuilder builder) -> suggestRegionKeys(context, builder);
@@ -1172,32 +1180,79 @@ public final class IrisModdedCommands {
int chunkX = player.blockPosition().getX() >> 4;
int chunkZ = player.blockPosition().getZ() >> 4;
ok(source, "Searching for " + label + "...");
Thread thread = new Thread(() -> {
try {
Position2 at = locator.find(engine, new Position2(chunkX, chunkZ), LOCATE_TIMEOUT_MS, (Integer checks) -> {
}).get();
if (at == null) {
server.execute(() -> fail(source, "Could not find " + label + " within the search timeout."));
return;
CompletableFuture<Position2> search;
try {
search = locator.find(engine, new Position2(chunkX, chunkZ), LOCATE_TIMEOUT_MS, (Integer checks) -> {
});
} catch (WrongEngineBroException e) {
fail(source, "The engine for this world has been closed; rejoin the dimension and try again.");
return;
}
UUID playerId = player.getUUID();
CompletableFuture<Position2> previous = ACTIVE_LOCATE_REQUESTS.put(playerId, search);
if (previous != null && previous != search) {
previous.cancel(true);
}
search.whenComplete((Position2 at, Throwable error) -> completeLocate(
source, level, engine, player, label, server, playerId, search, at, error));
}
private static void completeLocate(CommandSourceStack source, ServerLevel level, Engine engine,
ServerPlayer player, String label, MinecraftServer server, UUID playerId,
CompletableFuture<Position2> search, Position2 at, Throwable error) {
if (ACTIVE_LOCATE_REQUESTS.get(playerId) != search) {
return;
}
Throwable failure = unwrapCompletionFailure(error);
if (failure instanceof CancellationException) {
ACTIVE_LOCATE_REQUESTS.remove(playerId, search);
return;
}
if (failure != null) {
LOGGER.error("Iris locate failed for {}", label, failure);
server.execute(() -> {
if (ACTIVE_LOCATE_REQUESTS.remove(playerId, search)) {
fail(source, "Search failed: " + failure);
}
int blockX = (at.getX() << 4) + 8;
int blockZ = (at.getZ() << 4) + 8;
int blockY = engine.getMinHeight() + engine.getHeight(blockX, blockZ, false) + 2;
server.execute(() -> {
player.teleportTo(level, blockX + 0.5D, blockY, blockZ + 0.5D, Set.<Relative>of(), player.getYRot(), player.getXRot(), false);
ok(source, "Teleported to " + label + " at " + blockX + " " + blockY + " " + blockZ);
});
} catch (WrongEngineBroException e) {
server.execute(() -> fail(source, "The engine for this world has been closed; rejoin the dimension and try again."));
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} catch (ExecutionException e) {
LOGGER.error("Iris locate failed for {}", label, e);
server.execute(() -> fail(source, "Search failed: " + e.getCause()));
});
return;
}
if (at == null) {
server.execute(() -> {
if (ACTIVE_LOCATE_REQUESTS.remove(playerId, search)) {
fail(source, "Could not find " + label + " within the search timeout.");
}
});
return;
}
server.execute(() -> {
if (ACTIVE_LOCATE_REQUESTS.remove(playerId, search)) {
teleportToLocateResult(source, level, engine, player, label, at);
}
}, "Iris Locator");
thread.setDaemon(true);
thread.start();
});
}
private static void teleportToLocateResult(CommandSourceStack source, ServerLevel level, Engine engine,
ServerPlayer player, String label, Position2 at) {
int blockX = (at.getX() << 4) + 8;
int blockZ = (at.getZ() << 4) + 8;
try (GenerationSessionLease lease = engine.acquireGenerationLease("modded_locator_teleport");
IrisContext.Scope ignored = IrisContext.open(engine, lease.sessionId(), null)) {
int blockY = engine.getMinHeight() + engine.getHeight(blockX, blockZ, false) + 2;
player.teleportTo(level, blockX + 0.5D, blockY, blockZ + 0.5D, Set.<Relative>of(), player.getYRot(), player.getXRot(), false);
ok(source, "Teleported to " + label + " at " + blockX + " " + blockY + " " + blockZ);
} catch (GenerationSessionException e) {
fail(source, "The engine changed while locating " + label + "; try again.");
}
}
private static Throwable unwrapCompletionFailure(Throwable error) {
Throwable failure = error;
while ((failure instanceof CompletionException || failure instanceof ExecutionException)
&& failure.getCause() != null) {
failure = failure.getCause();
}
return failure;
}
private static int seed(CommandSourceStack source) {
@@ -35,8 +35,13 @@ import net.minecraft.world.level.dimension.DimensionType;
import net.minecraft.world.level.storage.LevelResource;
import java.io.File;
import java.util.Objects;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.BooleanSupplier;
public final class ModdedPregenJob {
private static final long SHUTDOWN_TIMEOUT_MILLIS = 10_000L;
private static final AtomicReference<ActivePregen> ACTIVE = new AtomicReference<>();
private static volatile String dimension = "?";
private ModdedPregenJob() {
@@ -54,13 +59,26 @@ public final class ModdedPregenJob {
.radiusX(radiusBlocks)
.radiusZ(radiusBlocks)
.build();
PregeneratorMethod method = new ModdedPregenMethod(level, engine, sync);
ModdedPregenMethod moddedMethod = new ModdedPregenMethod(level, engine, sync);
PregeneratorMethod method = moddedMethod;
if (cached) {
method = new CachedPregenMethod(method, PregenCache.create(cacheDirectory(level)).sync(), task);
}
ActivePregen active = new ActivePregen(engine, moddedMethod);
ACTIVE.set(active);
dimension = level.dimension().identifier().toString();
new PregeneratorJob(task, method, engine);
return true;
try {
PregeneratorJob job = new PregeneratorJob(task, method, engine);
job.whenDone(() -> {
if (!moddedMethod.hasPendingFinalSave()) {
ACTIVE.compareAndSet(active, null);
}
});
return true;
} catch (Throwable failure) {
ACTIVE.compareAndSet(active, null);
throw propagate(failure);
}
}
private static File cacheDirectory(ServerLevel level) {
@@ -73,7 +91,17 @@ public final class ModdedPregenJob {
}
public static void shutdown() {
PregeneratorJob.shutdownAndWait(10_000L);
ActivePregen active = ACTIVE.get();
shutdownAndSave(active, () -> PregeneratorJob.shutdownAndWait(SHUTDOWN_TIMEOUT_MILLIS));
}
public static boolean shutdownForWorld(String worldIdentity) {
PregeneratorJob job = PregeneratorJob.getInstance();
if (job == null || !job.targetsWorldIdentity(worldIdentity)) {
return false;
}
ActivePregen active = matchingActive(worldIdentity);
return shutdownAndSave(active, () -> PregeneratorJob.shutdownInstanceForWorld(worldIdentity));
}
public static Boolean pauseResume() {
@@ -125,4 +153,64 @@ public final class ModdedPregenJob {
status.append(ModdedCommandFeedback.footer());
return status;
}
private static ActivePregen matchingActive(String worldIdentity) {
ActivePregen active = ACTIVE.get();
return active != null && active.targets(worldIdentity) ? active : null;
}
private static boolean shutdownAndSave(ActivePregen active, BooleanSupplier shutdown) {
boolean deferred = active != null && active.method().deferFinalSaveToServerThread();
boolean stopped = false;
boolean result = false;
Throwable failure = null;
try {
result = shutdown.getAsBoolean();
stopped = result;
} catch (Throwable shutdownFailure) {
failure = shutdownFailure;
}
if (deferred) {
try {
if (stopped) {
active.method().completeDeferredFinalSave();
} else {
active.method().cancelDeferredFinalSave();
}
} catch (Throwable saveFailure) {
if (failure == null) {
failure = saveFailure;
} else if (saveFailure != failure) {
failure.addSuppressed(saveFailure);
}
}
}
if (failure == null && (stopped || PregeneratorJob.getInstance() == null)) {
ACTIVE.compareAndSet(active, null);
}
if (failure != null) {
throw propagate(failure);
}
return result;
}
private static RuntimeException propagate(Throwable failure) {
if (failure instanceof RuntimeException runtimeException) {
return runtimeException;
}
if (failure instanceof Error fatalError) {
throw fatalError;
}
return new IllegalStateException("Iris modded pregenerator shutdown failed", failure);
}
private record ActivePregen(Engine engine, ModdedPregenMethod method) {
private boolean targets(String worldIdentity) {
return engine != null
&& engine.getWorld() != null
&& Objects.equals(engine.getWorld().identity(), worldIdentity);
}
}
}
@@ -40,14 +40,18 @@ import java.util.concurrent.Semaphore;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.atomic.AtomicReference;
public final class ModdedPregenMethod implements PregeneratorMethod {
private static final Logger LOGGER = LoggerFactory.getLogger("Iris");
private static final TicketType PREGEN_TICKET = new TicketType(TicketType.NO_TIMEOUT, TicketType.FLAG_LOADING | TicketType.FLAG_KEEP_DIMENSION_ACTIVE);
private static final int ADAPTIVE_TIMEOUT_STEP = 3;
private static final long ADAPTIVE_RECOVERY_INTERVAL = 64L;
private static final long FINAL_SAVE_TIMEOUT_MILLIS = 10_000L;
private static final long FINAL_SAVE_POLL_MILLIS = 50L;
private static final boolean PARALLEL_CHUNK_SYSTEM = detectParallelChunkSystem();
private final ServerLevel level;
@@ -62,6 +66,10 @@ public final class ModdedPregenMethod implements PregeneratorMethod {
private final AtomicInteger adaptiveLimit;
private final AtomicInteger timeoutStreak = new AtomicInteger();
private final AtomicLong completed = new AtomicLong();
private final AtomicBoolean finalSaveOnServerThread = new AtomicBoolean(false);
private final AtomicBoolean finalSaveDeferred = new AtomicBoolean(false);
private final AtomicBoolean finalSaveCompleted = new AtomicBoolean(false);
private final AtomicReference<FinalSaveRequest> queuedFinalSave = new AtomicReference<>();
private final int timeoutSeconds;
private final PregenMantleBackpressure backpressure;
@@ -114,6 +122,9 @@ public final class ModdedPregenMethod implements PregeneratorMethod {
}
LOGGER.info("Iris modded pregen done: dim={} completed={} peakInFlight={} finalLimit={}",
level.dimension().identifier(), completed.get(), inFlightPeak.get(), adaptiveLimit.get());
if (deferFinalSaveIfRequested()) {
return;
}
saveLevel(true);
}
@@ -122,24 +133,156 @@ public final class ModdedPregenMethod implements PregeneratorMethod {
saveLevel(false);
}
private void saveLevel(boolean wait) {
CompletableFuture<Void> saved = new CompletableFuture<>();
level.getServer().execute(() -> {
try {
level.save(null, false, false);
} finally {
saved.complete(null);
boolean deferFinalSaveToServerThread() {
if (!level.getServer().isSameThread()) {
return false;
}
finalSaveOnServerThread.set(true);
return true;
}
void completeDeferredFinalSave() {
requireServerThreadForFinalSave();
cancelQueuedFinalSave();
if (!finalSaveCompleted.get()) {
saveLevelOnServerThread();
finalSaveCompleted.set(true);
}
finalSaveDeferred.set(false);
finalSaveOnServerThread.set(false);
}
void cancelDeferredFinalSave() {
requireServerThreadForFinalSave();
finalSaveOnServerThread.set(false);
if (finalSaveDeferred.get() || queuedFinalSave.get() != null) {
cancelQueuedFinalSave();
if (!finalSaveCompleted.get()) {
saveLevelOnServerThread();
finalSaveCompleted.set(true);
}
});
if (!wait) {
finalSaveDeferred.set(false);
}
}
boolean hasPendingFinalSave() {
return finalSaveOnServerThread.get()
|| finalSaveDeferred.get()
|| queuedFinalSave.get() != null;
}
private void saveLevel(boolean wait) {
if (wait) {
saveFinalLevel();
return;
}
MinecraftServer server = level.getServer();
if (server.isSameThread()) {
saveLevelOnServerThread();
return;
}
server.execute(this::saveLevelOnServerThread);
}
private void saveFinalLevel() {
MinecraftServer server = level.getServer();
if (server.isSameThread()) {
saveLevelOnServerThread();
finalSaveCompleted.set(true);
return;
}
if (deferFinalSaveIfRequested()) {
return;
}
FinalSaveRequest request = new FinalSaveRequest();
if (!queuedFinalSave.compareAndSet(null, request)) {
throw new IllegalStateException("Iris pregen final save is already queued for "
+ level.dimension().identifier());
}
try {
server.execute(() -> executeFinalSave(request));
} catch (RuntimeException | Error failure) {
queuedFinalSave.compareAndSet(request, null);
request.fail(failure);
throw failure;
}
awaitFinalSave(request);
}
private void executeFinalSave(FinalSaveRequest request) {
if (!request.claim()) {
return;
}
try {
saved.get(10, TimeUnit.SECONDS);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} catch (TimeoutException | ExecutionException e) {
LOGGER.warn("Iris pregen level save did not complete in time for {}", level.dimension().identifier());
saveLevelOnServerThread();
finalSaveCompleted.set(true);
request.complete();
} catch (RuntimeException | Error failure) {
request.fail(failure);
throw failure;
} finally {
queuedFinalSave.compareAndSet(request, null);
}
}
private void awaitFinalSave(FinalSaveRequest request) {
long deadline = System.nanoTime() + TimeUnit.MILLISECONDS.toNanos(FINAL_SAVE_TIMEOUT_MILLIS);
while (true) {
if (deferFinalSaveIfRequested()) {
return;
}
long remainingNanos = deadline - System.nanoTime();
if (remainingNanos <= 0L) {
LOGGER.warn("Iris pregen level save did not complete in time for {}", level.dimension().identifier());
return;
}
long waitMillis = Math.max(1L, Math.min(FINAL_SAVE_POLL_MILLIS,
TimeUnit.NANOSECONDS.toMillis(remainingNanos)));
try {
request.completion().get(waitMillis, TimeUnit.MILLISECONDS);
return;
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return;
} catch (TimeoutException e) {
continue;
} catch (ExecutionException e) {
Throwable cause = e.getCause() == null ? e : e.getCause();
LOGGER.error("Iris pregen level save failed for {}", level.dimension().identifier(), cause);
throw new IllegalStateException("Iris pregen level save failed for "
+ level.dimension().identifier(), cause);
}
}
}
private boolean deferFinalSaveIfRequested() {
if (!finalSaveOnServerThread.get()) {
return false;
}
finalSaveDeferred.set(true);
if (finalSaveOnServerThread.get()) {
return true;
}
finalSaveDeferred.set(false);
return false;
}
private void cancelQueuedFinalSave() {
FinalSaveRequest request = queuedFinalSave.getAndSet(null);
if (request != null) {
request.cancel();
}
}
private void saveLevelOnServerThread() {
level.save(null, false, false);
}
private void requireServerThreadForFinalSave() {
if (!level.getServer().isSameThread()) {
throw new IllegalStateException("Iris pregen final save must run on the Minecraft server thread for "
+ level.dimension().identifier());
}
}
@@ -361,4 +504,31 @@ public final class ModdedPregenMethod implements PregeneratorMethod {
public Mantle getMantle() {
return engine.getMantle().getMantle();
}
private static final class FinalSaveRequest {
private final CompletableFuture<Void> completion = new CompletableFuture<>();
private final AtomicBoolean active = new AtomicBoolean(true);
private boolean claim() {
return active.compareAndSet(true, false);
}
private void complete() {
completion.complete(null);
}
private void fail(Throwable failure) {
active.set(false);
completion.completeExceptionally(failure);
}
private void cancel() {
active.set(false);
completion.complete(null);
}
private CompletableFuture<Void> completion() {
return completion;
}
}
}
@@ -19,13 +19,13 @@
package art.arcane.iris.modded.service;
import art.arcane.iris.core.IrisSettings;
import art.arcane.iris.core.gui.PregeneratorJob;
import art.arcane.iris.core.pregenerator.MantleHeapPressure;
import art.arcane.iris.core.runtime.GoldenHashEngine;
import art.arcane.iris.core.tools.WorldMaintenance;
import art.arcane.iris.core.service.EngineMaintenance;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.framework.GenerationSessionException;
import art.arcane.iris.engine.framework.GenerationSessionLease;
import art.arcane.iris.modded.ModdedWorldEngines;
import art.arcane.iris.spi.IrisLogging;
import art.arcane.iris.util.project.context.IrisContext;
import net.minecraft.server.MinecraftServer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -33,71 +33,74 @@ import org.slf4j.LoggerFactory;
import java.util.Collection;
import java.util.Collections;
import java.util.IdentityHashMap;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.RejectedExecutionException;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
public final class ModdedEngineMaintenanceService implements ModdedTickableService {
private static final Logger LOGGER = LoggerFactory.getLogger("Iris");
private static final long TRIM_PERIOD_MILLIS = 2_000L;
private static final long MAINTENANCE_PERIOD_MILLIS = 2_000L;
private static final long SAVE_PERIOD_MILLIS = 60_000L;
private static final long SHUTDOWN_TIMEOUT_SECONDS = 30L;
private static final long INTERRUPT_DRAIN_TIMEOUT_SECONDS = 5L;
private final AtomicInteger tectonicLimit = new AtomicInteger(30);
private final Set<Engine> inFlight = Collections.synchronizedSet(Collections.newSetFromMap(new IdentityHashMap<>()));
private final Map<Engine, Long> lastSavedAt = Collections.synchronizedMap(new IdentityHashMap<>());
private volatile ExecutorService service;
private long lastMaintenanceAt;
private long lastSaveAt;
@Override
public void onEnable() {
if (service != null) {
ExecutorService current = service;
if (current != null && !current.isShutdown()) {
return;
}
IrisSettings.IrisSettingsPerformance settings = IrisSettings.get().getPerformance();
IrisSettings.IrisSettingsEngineSVC engineSettings = settings.getEngineSVC();
ThreadFactory factory = (engineSettings.isUseVirtualThreads()
IrisSettings.IrisSettingsEngineSVC settings = IrisSettings.get().getPerformance().getEngineSVC();
ThreadFactory factory = (settings.isUseVirtualThreads()
? Thread.ofVirtual()
: Thread.ofPlatform().priority(engineSettings.getPriority()))
: Thread.ofPlatform().priority(settings.getPriority()))
.name("Iris EngineSVC-", 0)
.factory();
service = Executors.newThreadPerTaskExecutor(factory);
tectonicLimit.set(settings.getTectonicPlateSize());
service = Executors.newFixedThreadPool(EngineMaintenance.workerParallelism(), factory);
lastMaintenanceAt = 0L;
lastSaveAt = System.currentTimeMillis();
inFlight.clear();
lastSavedAt.clear();
}
@Override
public void onDisable() {
ExecutorService active = service;
service = null;
if (active != null) {
active.shutdown();
boolean drained = shutdownAndDrain(active);
if (drained) {
inFlight.clear();
}
lastSavedAt.clear();
if (!drained) {
throw new IllegalStateException("Iris engine maintenance workers remained active during service shutdown");
}
inFlight.clear();
}
@Override
public void onServerTick(MinecraftServer server) {
ExecutorService active = service;
if (active == null) {
if (active == null || active.isShutdown()) {
return;
}
long now = System.currentTimeMillis();
if (now - lastMaintenanceAt < TRIM_PERIOD_MILLIS) {
if (now - lastMaintenanceAt < MAINTENANCE_PERIOD_MILLIS) {
return;
}
lastMaintenanceAt = now;
boolean flush = now - lastSaveAt >= SAVE_PERIOD_MILLIS;
if (flush) {
lastSaveAt = now;
}
Collection<Engine> engines = ModdedWorldEngines.activeEngines();
int share = tectonicLimit.get() / Math.max(engines.size(), 1);
reconcileSaveState(engines, now);
for (Engine engine : engines) {
if (!inFlight.add(engine)) {
continue;
@@ -105,101 +108,118 @@ public final class ModdedEngineMaintenanceService implements ModdedTickableServi
try {
active.execute(() -> {
try {
maintain(engine, share, flush);
maintain(engine, now);
} finally {
inFlight.remove(engine);
}
});
} catch (RejectedExecutionException rejected) {
} catch (RejectedExecutionException exception) {
inFlight.remove(engine);
if (active == service && !active.isShutdown()) {
LOGGER.error("Iris rejected engine maintenance for {}", engineName(engine), exception);
}
}
}
}
private void maintain(Engine engine, int share, boolean flush) {
if (engine == null || engine.isClosed() || engine.getMantle().getMantle().isClosed()) {
private void maintain(Engine engine, long scheduledAt) {
if (engine == null || engine.isClosing() || engine.isClosed()) {
return;
}
if (pregenTargets(engine)) {
return;
}
if (flush) {
try {
engine.save();
} catch (Throwable e) {
if (isMantleClosed(e)) {
return;
}
IrisLogging.reportError(e);
LOGGER.error("Iris engine save failed for {}", engine.getWorld().name(), e);
}
}
if (!shouldReduce(engine) || shouldSkipForMaintenance(engine) || GoldenHashEngine.isActive()) {
return;
}
try {
if (pregenTargets(engine) && MantleHeapPressure.overHighWater()) {
engine.getMantle().trim(0L, 0);
} else {
engine.getMantle().trim(TimeUnit.SECONDS.toMillis(IrisSettings.get().getPerformance().getMantleKeepAlive()), activeTectonicLimit(engine, share));
}
long unloadStart = System.currentTimeMillis();
boolean heapPressure = pregenTargets(engine) && MantleHeapPressure.overHighWater();
int unloadLimit = (heapPressure || IrisSettings.get().getPerformance().getEngineSVC().forceMulticoreWrite) ? 0 : activeTectonicLimit(engine, share);
int count = engine.getMantle().unloadTectonicPlate(unloadLimit);
if (heapPressure && MantleHeapPressure.overPanicWater()) {
MantleHeapPressure.requestPanicReclaim();
}
if (count > 0) {
LOGGER.debug("Iris unloaded {} tectonic plates in {}ms for {}", count, System.currentTimeMillis() - unloadStart, engine.getWorld().name());
}
} catch (Throwable e) {
if (isMantleClosed(e)) {
try (GenerationSessionLease lease = engine.acquireGenerationLease("modded_engine_maintenance");
IrisContext.Scope ignored = IrisContext.open(engine, lease.sessionId(), null)) {
if (!EngineMaintenance.isAvailable(engine)) {
return;
}
IrisLogging.reportError(e);
LOGGER.error("Iris engine maintenance failed for {}", engine.getWorld().name(), e);
boolean pregeneratorTargetsWorld = EngineMaintenance.pregeneratorTargets(engine);
if (pregeneratorTargetsWorld) {
return;
}
saveIfDue(engine, scheduledAt);
if (!EngineMaintenance.shouldRun(engine)) {
return;
}
EngineMaintenance.Outcome outcome = EngineMaintenance.run(engine);
if (outcome.unloadedTectonicPlates() > 0) {
LOGGER.debug("Iris unloaded {} tectonic plates in {}ms for {}",
outcome.unloadedTectonicPlates(), outcome.unloadDurationMillis(), engineName(engine));
}
} catch (GenerationSessionException exception) {
if (engine.isClosing() || exception.isExpectedTeardown()) {
return;
}
IrisLogging.reportError(exception);
LOGGER.error("Iris engine maintenance session failed for {}", engineName(engine), exception);
} catch (Throwable exception) {
if (EngineMaintenance.isMantleClosed(exception)) {
return;
}
IrisLogging.reportError(exception);
LOGGER.error("Iris engine maintenance failed for {}", engineName(engine), exception);
}
}
private boolean shouldReduce(Engine engine) {
if (!engine.isStudio() || IrisSettings.get().getPerformance().isTrimMantleInStudio()) {
private void saveIfDue(Engine engine, long scheduledAt) {
Long lastSave = lastSavedAt.get(engine);
if (lastSave == null || scheduledAt - lastSave < SAVE_PERIOD_MILLIS) {
return;
}
try {
engine.save();
lastSavedAt.put(engine, System.currentTimeMillis());
} catch (Throwable exception) {
if (EngineMaintenance.isMantleClosed(exception)) {
return;
}
IrisLogging.reportError(exception);
LOGGER.error("Iris engine save failed for {}", engineName(engine), exception);
}
}
private void reconcileSaveState(Collection<Engine> engines, long now) {
Set<Engine> activeEngines = Collections.newSetFromMap(new IdentityHashMap<>());
activeEngines.addAll(engines);
synchronized (lastSavedAt) {
lastSavedAt.keySet().removeIf(engine -> !activeEngines.contains(engine));
for (Engine engine : activeEngines) {
lastSavedAt.putIfAbsent(engine, now);
}
}
}
private boolean shutdownAndDrain(ExecutorService active) {
if (active == null) {
return true;
}
return pregenTargets(engine);
}
private boolean shouldSkipForMaintenance(Engine engine) {
if (engine.getWorld() == null || !WorldMaintenance.isWorldMaintenanceActive(engine.getWorld().identity())) {
return false;
}
return !pregenTargets(engine);
}
private boolean pregenTargets(Engine engine) {
if (engine.getWorld() == null) {
return false;
}
PregeneratorJob job = PregeneratorJob.getInstance();
return job != null && job.targetsWorldIdentity(engine.getWorld().identity());
}
private int activeTectonicLimit(Engine engine, int share) {
if (!pregenTargets(engine)) {
return share;
}
return Math.max(share, IrisSettings.get().getPregen().getEffectiveResidentTectonicPlates(engine.getHeight()));
}
private static boolean isMantleClosed(Throwable throwable) {
Throwable current = throwable;
while (current != null) {
String message = current.getMessage();
if (message != null && message.toLowerCase(Locale.ROOT).contains("mantle is closed")) {
active.shutdown();
try {
if (active.awaitTermination(SHUTDOWN_TIMEOUT_SECONDS, TimeUnit.SECONDS)) {
return true;
}
current = current.getCause();
active.shutdownNow();
if (active.awaitTermination(INTERRUPT_DRAIN_TIMEOUT_SECONDS, TimeUnit.SECONDS)) {
return true;
}
IllegalStateException failure = new IllegalStateException(
"Iris engine maintenance workers did not stop after shutdownNow");
IrisLogging.reportError(failure);
LOGGER.error("Iris engine maintenance did not terminate; active engine lifecycle leases will block unsafe shutdown", failure);
return false;
} catch (InterruptedException exception) {
active.shutdownNow();
Thread.currentThread().interrupt();
IrisLogging.reportError(exception);
LOGGER.error("Interrupted while draining Iris engine maintenance", exception);
return false;
}
return false;
}
private static String engineName(Engine engine) {
return engine == null || engine.getWorld() == null ? "<unbound>" : engine.getWorld().name();
}
}
@@ -18,6 +18,7 @@
package art.arcane.iris.modded.service;
import art.arcane.iris.core.loader.IrisData;
import art.arcane.iris.engine.framework.MeteredCache;
import art.arcane.iris.engine.framework.PreservationRegistry;
import org.slf4j.Logger;
@@ -55,6 +56,7 @@ public final class ModdedPreservationService implements ModdedService, Preservat
@Override
public void dereference() {
IrisData.dereference();
threads.removeIf((Thread thread) -> !thread.isAlive());
services.removeIf(ExecutorService::isShutdown);
caches.removeIf((WeakReference<MeteredCache> ref) -> {
@@ -30,6 +30,7 @@ import art.arcane.iris.modded.IrisModdedChunkGenerator;
import art.arcane.iris.modded.ModdedDimensionManager;
import art.arcane.iris.modded.ModdedForcedDatapack;
import art.arcane.iris.modded.ModdedWorkspaceGenerator;
import art.arcane.iris.modded.command.ModdedPregenJob;
import art.arcane.volmlib.util.collection.KList;
import art.arcane.volmlib.util.io.ReactiveFolder;
import art.arcane.volmlib.util.scheduling.ChronoLatch;
@@ -180,7 +181,10 @@ public final class ModdedStudioHotloadService implements ModdedTickableService,
@Override
public void shutdownPregenerator(Engine engine) {
PregeneratorJob.shutdownInstance();
IrisWorld world = engine.getWorld();
if (world != null) {
ModdedPregenJob.shutdownForWorld(world.identity());
}
}
@Override
@@ -0,0 +1,227 @@
package art.arcane.iris.modded.service;
import art.arcane.iris.modded.ModdedEngineBootstrap;
import art.arcane.iris.modded.ModdedScheduler;
import net.minecraft.core.BlockPos;
import net.minecraft.core.particles.BlockParticleOption;
import net.minecraft.core.particles.ParticleTypes;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.sounds.SoundEvents;
import net.minecraft.sounds.SoundSource;
import net.minecraft.world.entity.item.ItemEntity;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.level.block.state.BlockState;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.concurrent.atomic.AtomicBoolean;
final class ModdedTreeFellerPresentation {
private static final Logger LOGGER = LoggerFactory.getLogger("Iris");
private static final int MIN_BLOCKS_PER_PULSE = 4;
private static final int MAX_BLOCKS_PER_PULSE = 64;
private static final int TARGET_EROSION_PULSES = 60;
private static final int MAX_EFFECT_ORIGINS_PER_PULSE = 16;
private final ServerPlayer player;
private final ServerLevel sourceLevel;
private final List<ItemStack> pendingDrops = new ArrayList<>();
private final AtomicBoolean effectFailureReported = new AtomicBoolean();
private final AtomicBoolean deliveryFailureReported = new AtomicBoolean();
private boolean flushScheduled;
private double fallbackX;
private double fallbackY;
private double fallbackZ;
ModdedTreeFellerPresentation(ServerPlayer player, ServerLevel sourceLevel) {
this.player = player;
this.sourceLevel = sourceLevel;
this.fallbackX = player.getX();
this.fallbackY = player.getY() + 0.15D;
this.fallbackZ = player.getZ();
}
static int blocksPerPulse(int blockCount) {
int requested = Math.max(1, (blockCount + TARGET_EROSION_PULSES - 1) / TARGET_EROSION_PULSES);
return Math.max(MIN_BLOCKS_PER_PULSE, Math.min(requested, MAX_BLOCKS_PER_PULSE));
}
static int effectStride(int blocksPerPulse) {
return Math.max(
1,
(blocksPerPulse + MAX_EFFECT_ORIGINS_PER_PULSE - 1) / MAX_EFFECT_ORIGINS_PER_PULSE
);
}
static List<ItemStack> consolidateDrops(Collection<ItemStack> drops) {
List<ItemStack> consolidated = new ArrayList<>();
for (ItemStack drop : drops) {
mergeDrop(consolidated, drop);
}
return List.copyOf(consolidated);
}
void activate(BlockPos position, BlockState state) {
try {
double x = position.getX() + 0.5D;
double y = position.getY() + 0.5D;
double z = position.getZ() + 0.5D;
sourceLevel.sendParticles(ParticleTypes.ENCHANT, x, y, z, 24, 0.45D, 0.45D, 0.45D, 0.18D);
sourceLevel.sendParticles(ParticleTypes.END_ROD, x, y, z, 8, 0.25D, 0.25D, 0.25D, 0.035D);
sourceLevel.sendParticles(
new BlockParticleOption(ParticleTypes.BLOCK, state),
x,
y,
z,
8,
0.25D,
0.25D,
0.25D,
0.04D
);
sourceLevel.playSound(null, position, SoundEvents.ENCHANTMENT_TABLE_USE, SoundSource.PLAYERS, 0.55F, 1.35F);
sourceLevel.playSound(null, position, SoundEvents.AMETHYST_BLOCK_CHIME, SoundSource.PLAYERS, 0.4F, 0.8F);
} catch (Throwable error) {
reportEffectFailure(error);
}
}
void erode(BlockPos position, BlockState state, int processed, int effectStride, float pitch) {
if (processed % effectStride != 0) {
return;
}
try {
double x = position.getX() + 0.5D;
double y = position.getY() + 0.5D;
double z = position.getZ() + 0.5D;
sourceLevel.sendParticles(
new BlockParticleOption(ParticleTypes.BLOCK, state),
x,
y,
z,
5,
0.3D,
0.3D,
0.3D,
0.04D
);
sourceLevel.sendParticles(ParticleTypes.ENCHANT, x, y, z, 3, 0.28D, 0.28D, 0.28D, 0.12D);
sourceLevel.playSound(null, position, SoundEvents.AMETHYST_BLOCK_CHIME, SoundSource.PLAYERS, 0.22F, pitch);
} catch (Throwable error) {
reportEffectFailure(error);
}
}
synchronized boolean route(Iterable<ItemStack> drops) {
for (ItemStack drop : drops) {
if (drop != null && !drop.isEmpty()) {
pendingDrops.add(drop.copy());
}
}
scheduleFlush();
return true;
}
synchronized void flush() {
flushScheduled = false;
if (pendingDrops.isEmpty()) {
return;
}
List<ItemStack> drops = consolidateDrops(pendingDrops);
pendingDrops.clear();
boolean atPlayer = !player.isRemoved() && player.level() == sourceLevel;
double x = atPlayer ? player.getX() : fallbackX;
double y = atPlayer ? player.getY() + 0.15D : fallbackY;
double z = atPlayer ? player.getZ() : fallbackZ;
if (atPlayer) {
fallbackX = x;
fallbackY = y;
fallbackZ = z;
}
int delivered = 0;
for (ItemStack drop : drops) {
try {
ItemEntity item = new ItemEntity(sourceLevel, x, y, z, drop, 0D, 0.08D, 0D);
item.setDefaultPickUpDelay();
if (sourceLevel.addFreshEntity(item)) {
delivered++;
} else {
pendingDrops.add(drop.copy());
}
} catch (Throwable error) {
pendingDrops.add(drop.copy());
reportDeliveryFailure(error);
}
}
try {
int particles = Math.min(32, 6 + (delivered * 2));
sourceLevel.sendParticles(ParticleTypes.ENCHANT, x, y + 0.35D, z, particles, 0.3D, 0.25D, 0.3D, 0.1D);
sourceLevel.playSound(null, x, y, z, SoundEvents.AMETHYST_BLOCK_CHIME, SoundSource.PLAYERS, 0.28F, 1.75F);
} catch (Throwable error) {
reportEffectFailure(error);
}
if (!pendingDrops.isEmpty()) {
scheduleFlush();
}
}
synchronized void finish() {
flush();
}
private synchronized void scheduleFlush() {
if (pendingDrops.isEmpty() || flushScheduled) {
return;
}
ModdedScheduler scheduler = ModdedEngineBootstrap.schedulerOrNull();
if (scheduler == null) {
flush();
return;
}
flushScheduled = true;
scheduler.laterGlobal(this::flush, 1);
}
private static void mergeDrop(List<ItemStack> consolidated, ItemStack drop) {
if (drop == null || drop.isEmpty()) {
return;
}
ItemStack remaining = drop.copy();
for (ItemStack existing : consolidated) {
if (!ItemStack.isSameItemSameComponents(existing, remaining)) {
continue;
}
int capacity = existing.getMaxStackSize() - existing.getCount();
if (capacity <= 0) {
continue;
}
int moved = Math.min(capacity, remaining.getCount());
existing.grow(moved);
remaining.shrink(moved);
if (remaining.isEmpty()) {
return;
}
}
while (!remaining.isEmpty()) {
int amount = Math.min(remaining.getCount(), remaining.getMaxStackSize());
consolidated.add(remaining.copyWithCount(amount));
remaining.shrink(amount);
}
}
private void reportEffectFailure(Throwable error) {
if (effectFailureReported.compareAndSet(false, true)) {
LOGGER.error("Iris modded tree-feller presentation failed", error);
}
}
private void reportDeliveryFailure(Throwable error) {
if (deliveryFailureReported.compareAndSet(false, true)) {
LOGGER.error("Iris modded tree-feller drop delivery failed", error);
}
}
}
@@ -0,0 +1,554 @@
package art.arcane.iris.modded.service;
import art.arcane.iris.core.IrisSettings;
import art.arcane.iris.core.service.tree.TreeDefinitionIndex;
import art.arcane.iris.core.service.tree.TreeMarkerTraversal;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.framework.StructurePlacementMarker;
import art.arcane.iris.engine.framework.TreeBlockMaterial;
import art.arcane.iris.modded.ModdedBlockBreakHandler;
import art.arcane.iris.modded.ModdedBlockState;
import art.arcane.iris.modded.ModdedEngineBootstrap;
import art.arcane.iris.modded.ModdedScheduler;
import net.minecraft.core.BlockPos;
import net.minecraft.core.component.DataComponents;
import net.minecraft.server.MinecraftServer;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.tags.BlockTags;
import net.minecraft.tags.ItemTags;
import net.minecraft.world.entity.EquipmentSlot;
import net.minecraft.world.item.Item;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.level.block.Block;
import net.minecraft.world.level.block.entity.BlockEntity;
import net.minecraft.world.level.block.state.BlockState;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.WeakHashMap;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ThreadLocalRandom;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.function.BooleanSupplier;
public final class ModdedTreeFellerService implements ModdedTickableService {
private static final Logger LOGGER = LoggerFactory.getLogger("Iris");
private static final ThreadLocal<Integer> BREAK_PROBE_DEPTH = ThreadLocal.withInitial(() -> 0);
private final AtomicBoolean enabled = new AtomicBoolean();
private final Set<TreeClaim> activeClaims = ConcurrentHashMap.newKeySet();
private final Set<FellingRun> activeRuns = ConcurrentHashMap.newKeySet();
private final Map<Engine, TreeDefinitionIndex> definitions = Collections.synchronizedMap(new WeakHashMap<>());
public static boolean isBreakProbe() {
return BREAK_PROBE_DEPTH.get() > 0;
}
public static boolean runBreakProbe(BooleanSupplier probe) {
int depth = BREAK_PROBE_DEPTH.get();
BREAK_PROBE_DEPTH.set(depth + 1);
try {
return probe.getAsBoolean();
} finally {
if (depth == 0) {
BREAK_PROBE_DEPTH.remove();
} else {
BREAK_PROBE_DEPTH.set(depth);
}
}
}
@Override
public void onEnable() {
enabled.set(true);
}
@Override
public void onDisable() {
enabled.set(false);
for (FellingRun run : List.copyOf(activeRuns)) {
finish(run);
}
activeRuns.clear();
activeClaims.clear();
definitions.clear();
}
@Override
public void onServerTick(MinecraftServer server) {
for (FellingRun run : List.copyOf(activeRuns)) {
if (!isRunControlActive(run)) {
finish(run);
}
}
}
public PreparedOrigin prepare(
ServerLevel level,
ServerPlayer player,
BlockPos position,
BlockState state
) {
IrisSettings.IrisSettingsTreeFeller settings = IrisSettings.get().getTreeFeller();
if (!enabled.get()
|| settings == null
|| !settings.isEnabled()
|| !player.gameMode().isSurvival()
|| !player.isShiftKeyDown()
|| !state.is(BlockTags.LOGS)
|| !player.getInventory().getSelectedItem().is(ItemTags.AXES)
|| !ModdedEngineBootstrap.loader().hasTreeFellerPermission(player)) {
return null;
}
Engine engine = ModdedBlockBreakHandler.engineFor(level);
if (engine == null || engine.isClosed()) {
return null;
}
int minimumY = level.getMinY();
String marker = markerAt(engine, minimumY, position);
StructurePlacementMarker.Decoded decoded = StructurePlacementMarker.decode(marker);
if (decoded == null || decoded.structureAware()) {
return null;
}
TreeBlockMaterial expectedMaterial = materialAt(engine, minimumY, position);
if (expectedMaterial != null && !expectedMaterial.matches(ModdedBlockState.serialize(state))) {
return null;
}
if (expectedMaterial == null
&& !decoded.objectKey().startsWith("trees/")
&& !definitionIndex(engine).isTreeMarker(marker)) {
return null;
}
ItemStack tool = player.getInventory().getSelectedItem();
return new PreparedOrigin(
this,
level,
player,
position.immutable(),
state,
engine,
marker,
minimumY,
level.getMaxY(),
player.getInventory().getSelectedSlot(),
tool.copy(),
settings.getDurabilityPreservationChance()
);
}
public OriginDropRoute completeOrigin(PreparedOrigin prepared) {
if (prepared == null
|| prepared.owner() != this
|| !enabled.get()
|| prepared.engine().isClosed()) {
return null;
}
TreeClaim claim = new TreeClaim(prepared.level(), prepared.marker());
if (!activeClaims.add(claim)) {
return null;
}
ModdedTreeFellerPresentation presentation = new ModdedTreeFellerPresentation(
prepared.player(),
prepared.level()
);
FellingRun run = new FellingRun(claim, prepared, presentation);
if (!normalizeOriginTool(run)) {
activeClaims.remove(claim);
return null;
}
activeRuns.add(run);
presentation.activate(prepared.position(), prepared.state());
OriginDropRoute route = presentation::route;
if (run.toolBroken) {
ModdedBlockBreakHandler.completeManagedBreak(prepared.level(), prepared.position());
finish(run);
return route;
}
discover(run);
return route;
}
private TreeDefinitionIndex definitionIndex(Engine engine) {
synchronized (definitions) {
return definitions.computeIfAbsent(engine, TreeDefinitionIndex::build);
}
}
private boolean normalizeOriginTool(FellingRun run) {
PreparedOrigin prepared = run.prepared;
ItemStack before = prepared.toolBefore();
ItemStack current = prepared.player().getInventory().getItem(prepared.heldSlot());
boolean currentMatches = !current.isEmpty()
&& ItemStack.matchesIgnoringComponents(
before,
current,
(componentType) -> componentType == DataComponents.DAMAGE
);
if (!before.isDamageableItem()) {
if (!currentMatches || !ItemStack.isSameItemSameComponents(before, current)) {
return false;
}
run.expectedTool = current.copy();
return true;
}
boolean preserve = ThreadLocalRandom.current().nextInt(100) < prepared.preservationChance();
int desiredDamage = before.getDamageValue() + (preserve ? 0 : 1);
if (desiredDamage >= before.getMaxDamage()) {
if (!current.isEmpty() && !currentMatches) {
return false;
}
Item brokenItem = before.getItem();
prepared.player().getInventory().setItem(prepared.heldSlot(), ItemStack.EMPTY);
if (!current.isEmpty()) {
prepared.player().onEquippedItemBroken(brokenItem, EquipmentSlot.MAINHAND);
}
prepared.player().inventoryMenu.sendAllDataToRemote();
run.expectedTool = ItemStack.EMPTY;
run.toolBroken = true;
return true;
}
if (!current.isEmpty() && !currentMatches) {
return false;
}
ItemStack normalized = before.copy();
normalized.setDamageValue(desiredDamage);
prepared.player().getInventory().setItem(prepared.heldSlot(), normalized);
prepared.player().inventoryMenu.sendAllDataToRemote();
run.expectedTool = normalized.copy();
return true;
}
private void discover(FellingRun run) {
ModdedScheduler scheduler = ModdedEngineBootstrap.schedulerOrNull();
if (scheduler == null) {
ModdedBlockBreakHandler.completeManagedBreak(run.prepared.level(), run.prepared.position());
finish(run);
return;
}
scheduler.async(() -> {
TreeMarkerTraversal.Discovery discovery;
try {
PreparedOrigin prepared = run.prepared;
TreeMarkerTraversal.Position trigger = positionOf(prepared.position());
discovery = TreeMarkerTraversal.discover(
trigger,
prepared.marker(),
prepared.minimumY(),
prepared.maximumY(),
(x, y, z) -> markerAt(prepared.engine(), prepared.minimumY(), x, y, z)
);
} catch (Throwable error) {
LOGGER.error("Iris modded tree-feller discovery failed", error);
discovery = new TreeMarkerTraversal.Discovery(List.of(), false);
}
TreeMarkerTraversal.Discovery resolved = discovery;
scheduler.global(() -> beginErosion(run, resolved));
});
}
private void beginErosion(FellingRun run, TreeMarkerTraversal.Discovery discovery) {
ModdedBlockBreakHandler.completeManagedBreak(run.prepared.level(), run.prepared.position());
if (run.finished.get() || !discovery.complete()) {
finish(run);
return;
}
TreeMarkerTraversal.Position trigger = positionOf(run.prepared.position());
run.work = discovery.members().stream()
.filter((position) -> !position.equals(trigger))
.toList();
if (run.work.isEmpty()) {
finish(run);
return;
}
run.blocksPerPulse = ModdedTreeFellerPresentation.blocksPerPulse(run.work.size());
run.effectStride = ModdedTreeFellerPresentation.effectStride(run.blocksPerPulse);
scheduleNextPulse(run);
}
private void scheduleNextPulse(FellingRun run) {
ModdedScheduler scheduler = ModdedEngineBootstrap.schedulerOrNull();
if (scheduler == null) {
finish(run);
return;
}
scheduler.laterGlobal(() -> processPulse(run), 1);
}
private void processPulse(FellingRun run) {
if (run.finished.get() || !isRunControlActive(run)) {
finish(run);
return;
}
int processedThisPulse = 0;
while (processedThisPulse < run.blocksPerPulse && run.cursor < run.work.size()) {
if (!isRunControlActive(run)) {
finish(run);
return;
}
TreeMarkerTraversal.Position position = run.work.get(run.cursor++);
MemberResult result = processMember(run, position);
if (result == MemberResult.STOP) {
finish(run);
return;
}
processedThisPulse++;
}
run.presentation.flush();
if (run.cursor >= run.work.size()) {
finish(run);
return;
}
scheduleNextPulse(run);
}
private MemberResult processMember(FellingRun run, TreeMarkerTraversal.Position position) {
PreparedOrigin prepared = run.prepared;
BlockPos blockPosition = blockPosition(position);
ServerLevel level = prepared.level();
if (!level.isLoaded(blockPosition)) {
return MemberResult.STOP;
}
BlockState state = level.getBlockState(blockPosition);
if (state.isAir()) {
ModdedBlockBreakHandler.completeManagedBreak(level, blockPosition);
return MemberResult.CONTINUE;
}
if (!prepared.marker().equals(markerAt(prepared.engine(), prepared.minimumY(), blockPosition))) {
return MemberResult.STOP;
}
TreeBlockMaterial expectedMaterial = materialAt(prepared.engine(), prepared.minimumY(), blockPosition);
if (expectedMaterial != null && !expectedMaterial.matches(ModdedBlockState.serialize(state))) {
ModdedBlockBreakHandler.completeManagedBreak(level, blockPosition);
return state.is(BlockTags.LOGS) ? MemberResult.STOP : MemberResult.CONTINUE;
}
boolean log = state.is(BlockTags.LOGS);
ServerPlayer player = prepared.player();
if (!level.mayInteract(player, blockPosition)
|| player.blockActionRestricted(level, blockPosition, player.gameMode())
|| !player.getInventory().getSelectedItem().canDestroyBlock(
state,
level,
blockPosition,
player
)) {
return log ? MemberResult.STOP : MemberResult.CONTINUE;
}
if (!ModdedEngineBootstrap.loader().canTreeFellerBreak(
level,
player,
blockPosition,
state
)) {
return log ? MemberResult.STOP : MemberResult.CONTINUE;
}
if (!state.equals(level.getBlockState(blockPosition))
|| !prepared.marker().equals(markerAt(prepared.engine(), prepared.minimumY(), blockPosition))) {
return MemberResult.STOP;
}
ItemStack toolForDrops = prepared.player().getInventory().getSelectedItem().copy();
BlockEntity blockEntity = state.hasBlockEntity() ? level.getBlockEntity(blockPosition) : null;
List<ItemStack> vanillaDrops = Block.getDrops(
state,
level,
blockPosition,
blockEntity,
player,
toolForDrops
);
ModdedBlockBreakHandler.Result customDrops = ModdedBlockBreakHandler.evaluateManagedDrops(
level,
blockPosition,
state
);
ToolReservation reservation = log ? reserveToolDamage(run) : ToolReservation.free(toolForDrops);
if (reservation == null) {
return MemberResult.STOP;
}
if (!level.destroyBlock(blockPosition, false, player, 512)) {
refundToolDamage(run, reservation);
return MemberResult.STOP;
}
ModdedBlockBreakHandler.completeManagedBreak(level, blockPosition);
int processed = run.processed++;
float progress = run.work.size() <= 1 ? 1F : (float) processed / (float) (run.work.size() - 1);
float pitch = Math.min(1.95F, 0.65F + (progress * 1.25F));
run.presentation.erode(blockPosition, state, processed, run.effectStride, pitch);
run.presentation.route(customDrops.combinedDrops(vanillaDrops));
return reservation.broke() ? MemberResult.STOP : MemberResult.CONTINUE;
}
private ToolReservation reserveToolDamage(FellingRun run) {
PreparedOrigin prepared = run.prepared;
ItemStack current = prepared.player().getInventory().getSelectedItem();
if (!ItemStack.isSameItemSameComponents(current, run.expectedTool) || !current.is(ItemTags.AXES)) {
return null;
}
ItemStack before = current.copy();
if (!current.isDamageableItem()
|| ThreadLocalRandom.current().nextInt(100) < prepared.preservationChance()) {
return ToolReservation.free(before);
}
int nextDamage = current.getDamageValue() + 1;
if (nextDamage >= current.getMaxDamage()) {
Item brokenItem = current.getItem();
prepared.player().getInventory().setSelectedItem(ItemStack.EMPTY);
prepared.player().onEquippedItemBroken(brokenItem, EquipmentSlot.MAINHAND);
prepared.player().inventoryMenu.sendAllDataToRemote();
run.expectedTool = ItemStack.EMPTY;
return new ToolReservation(before, true, true);
}
current.setDamageValue(nextDamage);
prepared.player().getInventory().setSelectedItem(current);
prepared.player().inventoryMenu.sendAllDataToRemote();
run.expectedTool = current.copy();
return new ToolReservation(before, true, false);
}
private void refundToolDamage(FellingRun run, ToolReservation reservation) {
if (!reservation.charged()) {
return;
}
PreparedOrigin prepared = run.prepared;
ItemStack current = prepared.player().getInventory().getSelectedItem();
boolean expectedEmpty = run.expectedTool.isEmpty();
if ((expectedEmpty && current.isEmpty())
|| (!expectedEmpty && ItemStack.isSameItemSameComponents(current, run.expectedTool))) {
ItemStack restored = reservation.before().copy();
prepared.player().getInventory().setSelectedItem(restored);
prepared.player().inventoryMenu.sendAllDataToRemote();
run.expectedTool = restored.copy();
}
}
private boolean isRunControlActive(FellingRun run) {
PreparedOrigin prepared = run.prepared;
ServerPlayer player = prepared.player();
IrisSettings.IrisSettingsTreeFeller settings = IrisSettings.get().getTreeFeller();
if (!enabled.get()
|| settings == null
|| !settings.isEnabled()
|| run.finished.get()
|| player.isRemoved()
|| player.hasDisconnected()
|| player.level() != prepared.level()
|| !player.gameMode().isSurvival()
|| !player.isShiftKeyDown()
|| player.getInventory().getSelectedSlot() != prepared.heldSlot()
|| run.expectedTool.isEmpty()) {
return false;
}
ItemStack current = player.getInventory().getSelectedItem();
return current.is(ItemTags.AXES) && ItemStack.isSameItemSameComponents(current, run.expectedTool);
}
private void finish(FellingRun run) {
if (run.finished.compareAndSet(false, true)) {
activeRuns.remove(run);
activeClaims.remove(run.claim);
ModdedBlockBreakHandler.completeManagedBreak(run.prepared.level(), run.prepared.position());
run.presentation.finish();
}
}
private String markerAt(Engine engine, int minimumY, BlockPos position) {
return markerAt(engine, minimumY, position.getX(), position.getY(), position.getZ());
}
private String markerAt(Engine engine, int minimumY, int x, int y, int z) {
return engine.getMantle().getMantle().get(x, y - minimumY, z, String.class);
}
private TreeBlockMaterial materialAt(Engine engine, int minimumY, BlockPos position) {
return engine.getMantle().getMantle().get(
position.getX(),
position.getY() - minimumY,
position.getZ(),
TreeBlockMaterial.class
);
}
private TreeMarkerTraversal.Position positionOf(BlockPos position) {
return new TreeMarkerTraversal.Position(position.getX(), position.getY(), position.getZ());
}
private BlockPos blockPosition(TreeMarkerTraversal.Position position) {
return new BlockPos(position.x(), position.y(), position.z());
}
public record PreparedOrigin(
ModdedTreeFellerService owner,
ServerLevel level,
ServerPlayer player,
BlockPos position,
BlockState state,
Engine engine,
String marker,
int minimumY,
int maximumY,
int heldSlot,
ItemStack toolBefore,
int preservationChance
) {
public PreparedOrigin {
Objects.requireNonNull(owner, "owner");
Objects.requireNonNull(level, "level");
Objects.requireNonNull(player, "player");
Objects.requireNonNull(position, "position");
Objects.requireNonNull(state, "state");
Objects.requireNonNull(engine, "engine");
Objects.requireNonNull(marker, "marker");
Objects.requireNonNull(toolBefore, "toolBefore");
}
}
@FunctionalInterface
public interface OriginDropRoute {
boolean route(Iterable<ItemStack> drops);
}
private enum MemberResult {
CONTINUE,
STOP
}
private record TreeClaim(ServerLevel level, String marker) {
}
private record ToolReservation(ItemStack before, boolean charged, boolean broke) {
private static ToolReservation free(ItemStack before) {
return new ToolReservation(before, false, false);
}
}
private static final class FellingRun {
private final TreeClaim claim;
private final PreparedOrigin prepared;
private final ModdedTreeFellerPresentation presentation;
private final AtomicBoolean finished = new AtomicBoolean();
private ItemStack expectedTool = ItemStack.EMPTY;
private List<TreeMarkerTraversal.Position> work = List.of();
private int cursor;
private int processed;
private int blocksPerPulse = 1;
private int effectStride = 1;
private boolean toolBroken;
private FellingRun(
TreeClaim claim,
PreparedOrigin prepared,
ModdedTreeFellerPresentation presentation
) {
this.claim = claim;
this.prepared = prepared;
this.presentation = presentation;
}
}
}
@@ -0,0 +1,143 @@
package art.arcane.iris.modded;
import org.junit.Test;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
public class ModdedGenerationLeaseContractTest {
private static final String SOURCE_ROOT_PROPERTY = "iris.moddedCommonSources";
@Test
public void biomeRuntimeReadsAreGenerationLeased() throws IOException {
String source = source("art/arcane/iris/modded/IrisModdedBiomeSource.java");
assertTrue(source.contains("tryAcquireGenerationLease(engine, \"modded_structure_biome\")"));
assertTrue(source.contains("tryAcquireGenerationLease(engine, \"modded_visible_biome\")"));
assertTrue(source.contains("tryAcquireGenerationLease(engine, \"modded_biomes_within\")"));
assertTrue(source.contains("tryAcquireGenerationLease(engine, \"modded_structure_reachability\")"));
assertTrue(source.contains("tryAcquireGenerationLease(engine, \"modded_structure_state_biome\")"));
assertTrue(source.contains("tryAcquireGenerationLease(engine, \"modded_structure_biome_keys\")"));
assertTrue(source.contains("engine == null || engine.isClosed()"));
assertTrue(source.contains("catch (GenerationSessionException e)"));
assertTrue(source.contains("e.isExpectedTeardown()"));
assertTrue(source.contains("Iris structure biome key lookup was rejected during an engine transition"));
assertTrue(source.contains("source.clearCache()"));
}
@Test
public void terrainAndHeightQueriesShareGenerationSessions() throws IOException {
String source = source("art/arcane/iris/modded/IrisModdedChunkGenerator.java");
assertTrue(source.contains("generationEngine.acquireGenerationLease(\"modded_chunk_pipeline\")"));
assertTrue(source.contains("current.acquireGenerationLease(\"modded_base_height\")"));
assertTrue(source.contains("current.acquireGenerationLease(\"modded_base_column\")"));
assertTrue(source.contains("current.acquireGenerationLease(\"modded_configured_biome_keys\")"));
assertTrue(source.contains("generationEngine.isClosing() || e.isExpectedTeardown()"));
String terrain = method(source, "private ChunkAccess generateTerrain(");
int sessionFailure = terrain.indexOf("catch (GenerationSessionException e)");
int generalFailure = terrain.indexOf("catch (Throwable e)", sessionFailure);
String sessionHandler = terrain.substring(sessionFailure, generalFailure);
assertTrue(sessionHandler.contains("throw new IllegalStateException"));
assertFalse(sessionHandler.contains("return chunk;"));
String references = method(source, "public void createReferences(");
assertTrue(references.contains("requireGenerationLease(current, \"modded_create_references\")"));
assertTrue(references.contains("IrisContext.open(current, lease.sessionId(), null)"));
assertTrue(references.contains("super.createReferences(level, structureManager, chunk);"));
}
@Test
public void preservationDereferencesSharedPackData() throws IOException {
String source = source("art/arcane/iris/modded/service/ModdedPreservationService.java");
int dereference = source.indexOf("public void dereference()");
int irisData = source.indexOf("IrisData.dereference();", dereference);
int trackedResources = source.indexOf("threads.removeIf", dereference);
assertTrue(dereference >= 0);
assertTrue(irisData > dereference);
assertTrue(trackedResources > irisData);
}
@Test
public void hotloadShutdownOnlyTargetsItsOwnWorld() throws IOException {
String source = source("art/arcane/iris/modded/service/ModdedStudioHotloadService.java");
int shutdown = source.indexOf("public void shutdownPregenerator(Engine engine)");
int nextMethod = source.indexOf("private boolean throttled", shutdown);
String method = source.substring(shutdown, nextMethod);
assertTrue(method.contains("ModdedPregenJob.shutdownForWorld(world.identity());"));
assertFalse(method.contains("PregeneratorJob.shutdownInstance();"));
}
@Test
public void maintenanceOwnsOneGenerationSessionUntilWorkCompletes() throws IOException {
String source = source("art/arcane/iris/modded/service/ModdedEngineMaintenanceService.java");
String maintain = method(source, "private void maintain(Engine engine, long scheduledAt)");
assertTrue(maintain.contains("engine.acquireGenerationLease(\"modded_engine_maintenance\")"));
assertTrue(maintain.contains("IrisContext.open(engine, lease.sessionId(), null)"));
assertTrue(maintain.contains("exception.isExpectedTeardown()"));
assertTrue(source.contains("active.awaitTermination(INTERRUPT_DRAIN_TIMEOUT_SECONDS, TimeUnit.SECONDS)"));
}
@Test
public void blockingPregenShutdownDefersItsFinalSaveToTheServerThread() throws IOException {
String jobSource = source("art/arcane/iris/modded/command/ModdedPregenJob.java");
String shutdown = method(jobSource, "private static boolean shutdownAndSave(");
assertTrue(shutdown.contains("deferFinalSaveToServerThread()"));
assertTrue(shutdown.contains("completeDeferredFinalSave()"));
assertTrue(shutdown.contains("cancelDeferredFinalSave()"));
String methodSource = source("art/arcane/iris/modded/command/ModdedPregenMethod.java");
String close = method(methodSource, "public void close()");
assertTrue(close.contains("deferFinalSaveIfRequested()"));
String await = method(methodSource, "private void awaitFinalSave(");
assertTrue(await.contains("FINAL_SAVE_POLL_MILLIS"));
assertTrue(await.contains("deferFinalSaveIfRequested()"));
String complete = method(methodSource, "void completeDeferredFinalSave()");
int cancel = complete.indexOf("cancelQueuedFinalSave();");
int directSave = complete.indexOf("saveLevelOnServerThread();");
assertTrue(cancel >= 0);
assertTrue(directSave > cancel);
String queuedSave = method(methodSource, "private void executeFinalSave(");
assertTrue(queuedSave.contains("if (!request.claim())"));
String cancelRequest = method(methodSource, "private void cancelQueuedFinalSave()");
assertTrue(cancelRequest.contains("request.cancel();"));
String pending = method(methodSource, "boolean hasPendingFinalSave()");
assertTrue(pending.contains("queuedFinalSave.get() != null"));
}
private static String source(String relativePath) throws IOException {
String root = System.getProperty(SOURCE_ROOT_PROPERTY);
assertTrue("Missing system property " + SOURCE_ROOT_PROPERTY, root != null && !root.isBlank());
return Files.readString(Path.of(root, relativePath));
}
private static String method(String source, String signature) {
int start = source.indexOf(signature);
assertTrue("Missing source contract signature: " + signature, start >= 0);
int openBrace = source.indexOf('{', start);
assertTrue("Missing source contract method body: " + signature, openBrace >= 0);
int depth = 0;
for (int index = openBrace; index < source.length(); index++) {
char current = source.charAt(index);
if (current == '{') {
depth++;
} else if (current == '}') {
depth--;
if (depth == 0) {
return source.substring(start, index + 1);
}
}
}
throw new IllegalArgumentException("Unclosed source contract method: " + signature);
}
}
@@ -141,7 +141,35 @@ public class ModdedLifecycleFailureContractTest {
String source = source("ModdedEngineBootstrap.java");
String stop = method(source, "public static void stop(");
assertEquals(1, occurrences(stop, "ModdedStartup.reset();"));
assertEquals(1, occurrences(stop, "ModdedStartup::reset"));
}
@Test
public void engineBootstrapAttemptsEveryShutdownStageAndAggregatesFailures() throws IOException {
String source = source("ModdedEngineBootstrap.java");
String stop = method(source, "public static void stop(");
assertBefore(stop, "\"services\"", "\"world engines\"");
assertBefore(stop, "\"world engines\"", "\"dimension manager\"");
assertBefore(stop, "\"server state\"", "if (failure != null)");
assertTrue(stop.contains("throw propagateStopFailure(failure);"));
String runStage = method(source, "private static Throwable runStopStage(");
assertTrue(runStage.contains("catch (Throwable stageFailure)"));
assertTrue(runStage.contains("failure.addSuppressed(stageFailure);"));
}
@Test
public void worldShutdownRemovesOnlySuccessfullyClosedMappings() throws IOException {
String source = source("ModdedWorldEngines.java");
String shutdown = method(source, "public static void shutdown()");
assertTrue(shutdown.contains("new ArrayList<>(ENGINES.entrySet())"));
assertFalse(shutdown.contains("ENGINES.clear()"));
assertBefore(shutdown, "close(engine);", "ENGINES.remove(level, engine)");
assertTrue(shutdown.contains("ENGINES.containsKey(level)"));
assertTrue(shutdown.contains("failure.addSuppressed(e);"));
assertTrue(shutdown.contains("failed mappings were retained"));
}
@Test
@@ -160,7 +188,37 @@ public class ModdedLifecycleFailureContractTest {
String unbind = method(source, "synchronized void unbindEngine(ServerLevel level)");
assertFalse(unbind.contains("finally"));
assertBefore(unbind, "unloading = true;", "ModdedWorldEngines.evictOrThrow(level);");
assertBefore(unbind, "ModdedWorldEngines.evictOrThrow(level);", "clearEngineBinding();");
String binding = method(source, "private Engine bindEngine(ServerLevel level)");
assertTrue(binding.contains("requireBindingAllowed();"));
assertTrue(binding.contains("requireCompletedShutdown(cached);"));
String bindLevel = method(source, "synchronized void bindLevel(ServerLevel level)");
assertBefore(bindLevel, "requireCompletedShutdown(engine);", "unloading = false;");
String boundEngine = method(source, "public Engine engineIfBound()");
assertTrue(boundEngine.contains("unloading || current == null || current.isClosing() || current.isClosed()"));
}
@Test
public void loaderUnloadSurfacesCloseFailureAndDynamicRemovalUsesStrictEviction() throws IOException {
String bootstrapSource = source("ModdedEngineBootstrap.java");
String unload = method(bootstrapSource, "public static void levelUnloaded(ServerLevel level)");
String failure = catchBlock(unload);
assertTrue(failure.contains("LOGGER.error("));
assertTrue(failure.contains("throw "));
String managerSource = source("ModdedDimensionManager.java");
String remove = method(managerSource, "public static boolean remove(MinecraftServer server, String dimensionId, boolean wipeStorage)");
assertTrue(remove.contains("ModdedWorldEngines.evictOrThrow(level);"));
assertFalse(remove.contains("ModdedWorldEngines.evict(level);"));
assertTrue(remove.contains("generatorUnbound = true;"));
assertTrue(remove.contains("rollbackRemoval(server, serverAccess, key, level, generator, generatorUnbound, e);"));
String rollback = method(managerSource, "private static void rollbackRemoval(");
assertTrue(rollback.contains("serverAccess.hasLevel(server, key)"));
assertTrue(rollback.contains("generator.bindLevel(level);"));
assertTrue(rollback.contains("failure.addSuppressed(rollbackFailure);"));
assertTrue(rollback.contains("LOGGER.error("));
}
@Test
@@ -48,7 +48,28 @@ public class ModdedServiceManagerTest {
assertNull(manager.service(SecondService.class));
}
@Test
public void disableAttemptsEveryServiceInReverseOrderAndAggregatesFailures() {
ModdedServiceManager manager = new ModdedServiceManager();
RuntimeException firstFailure = new RuntimeException("first disable failed");
RuntimeException secondFailure = new RuntimeException("second disable failed");
FirstService first = manager.register(FirstService.class, new FirstService());
first.disableFailure = firstFailure;
SecondService second = manager.register(
SecondService.class, new SecondService(null, secondFailure));
manager.enableAll();
IllegalStateException thrown = assertThrows(IllegalStateException.class, manager::disableAll);
assertEquals(1, first.disableCount);
assertEquals(1, second.disableCount);
assertSame(secondFailure, thrown.getCause());
assertEquals(1, secondFailure.getSuppressed().length);
assertSame(firstFailure, secondFailure.getSuppressed()[0]);
}
private static final class FirstService implements ModdedService {
private RuntimeException disableFailure;
private int enableCount;
private int disableCount;
@@ -60,6 +81,9 @@ public class ModdedServiceManagerTest {
@Override
public void onDisable() {
disableCount++;
if (disableFailure != null) {
throw disableFailure;
}
}
}
@@ -0,0 +1,54 @@
package art.arcane.iris.modded.service;
import net.minecraft.SharedConstants;
import net.minecraft.core.component.DataComponentMap;
import net.minecraft.core.component.DataComponents;
import net.minecraft.server.Bootstrap;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.item.Items;
import org.junit.BeforeClass;
import org.junit.Test;
import java.util.List;
import static org.junit.Assert.assertEquals;
public class ModdedTreeFellerPresentationTest {
@BeforeClass
public static void bootstrapMinecraftRegistries() {
SharedConstants.tryDetectVersion();
Bootstrap.bootStrap();
DataComponentMap stackComponents = DataComponentMap.builder()
.set(DataComponents.MAX_STACK_SIZE, 64)
.build();
if (!Items.OAK_LOG.builtInRegistryHolder().areComponentsBound()) {
Items.OAK_LOG.builtInRegistryHolder().bindComponents(stackComponents);
}
if (!Items.BIRCH_LOG.builtInRegistryHolder().areComponentsBound()) {
Items.BIRCH_LOG.builtInRegistryHolder().bindComponents(stackComponents);
}
}
@Test
public void pulseSizingKeepsErosionReadableAndBounded() {
assertEquals(4, ModdedTreeFellerPresentation.blocksPerPulse(1));
assertEquals(4, ModdedTreeFellerPresentation.blocksPerPulse(240));
assertEquals(10, ModdedTreeFellerPresentation.blocksPerPulse(600));
assertEquals(64, ModdedTreeFellerPresentation.blocksPerPulse(100_000));
assertEquals(4, ModdedTreeFellerPresentation.effectStride(64));
}
@Test
public void compatibleDropsConsolidateWithoutExceedingStackLimits() {
List<ItemStack> drops = ModdedTreeFellerPresentation.consolidateDrops(List.of(
new ItemStack(Items.OAK_LOG, 48),
new ItemStack(Items.OAK_LOG, 48),
new ItemStack(Items.BIRCH_LOG, 3)
));
assertEquals(3, drops.size());
assertEquals(64, drops.get(0).getCount());
assertEquals(32, drops.get(1).getCount());
assertEquals(3, drops.get(2).getCount());
}
}
@@ -0,0 +1,37 @@
package art.arcane.iris.modded.service;
import org.junit.Test;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
public class ModdedTreeFellerServiceTest {
@Test
public void syntheticBreakProbeDepthIsNestedAndRestored() {
assertFalse(ModdedTreeFellerService.isBreakProbe());
boolean accepted = ModdedTreeFellerService.runBreakProbe(() -> {
assertTrue(ModdedTreeFellerService.isBreakProbe());
return ModdedTreeFellerService.runBreakProbe(() -> {
assertTrue(ModdedTreeFellerService.isBreakProbe());
return true;
});
});
assertTrue(accepted);
assertFalse(ModdedTreeFellerService.isBreakProbe());
}
@Test
public void syntheticBreakProbeDepthIsRestoredAfterFailure() {
try {
ModdedTreeFellerService.runBreakProbe(() -> {
throw new IllegalStateException("probe failure");
});
fail("Expected probe failure");
} catch (IllegalStateException expected) {
assertFalse(ModdedTreeFellerService.isBreakProbe());
}
}
}
@@ -27,6 +27,7 @@ import art.arcane.iris.modded.command.ModdedWandService;
import com.mojang.serialization.MapCodec;
import net.minecraft.core.registries.Registries;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.server.packs.PackType;
import net.minecraft.world.InteractionResult;
import net.minecraft.world.entity.item.ItemEntity;
@@ -34,15 +35,18 @@ import net.minecraft.world.entity.player.Player;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.level.chunk.ChunkGenerator;
import net.neoforged.api.distmarker.Dist;
import net.neoforged.bus.api.EventPriority;
import net.neoforged.bus.api.IEventBus;
import net.neoforged.fml.common.Mod;
import net.neoforged.fml.loading.FMLEnvironment;
import net.neoforged.fml.loading.FMLLoader;
import net.neoforged.neoforge.common.NeoForge;
import net.neoforged.neoforge.common.util.BlockSnapshot;
import net.neoforged.neoforge.event.AddPackFindersEvent;
import net.neoforged.neoforge.event.RegisterCommandsEvent;
import net.neoforged.neoforge.event.entity.player.PlayerInteractEvent;
import net.neoforged.neoforge.event.level.BlockDropsEvent;
import net.neoforged.neoforge.event.level.BlockEvent;
import net.neoforged.neoforge.event.level.LevelEvent;
import net.neoforged.neoforge.event.level.block.BreakBlockEvent;
import net.neoforged.neoforge.event.server.ServerAboutToStartEvent;
@@ -51,6 +55,9 @@ import net.neoforged.neoforge.event.server.ServerStartingEvent;
import net.neoforged.neoforge.event.server.ServerStoppingEvent;
import net.neoforged.neoforge.event.tick.ServerTickEvent;
import net.neoforged.neoforge.registries.DeferredRegister;
import net.neoforged.neoforge.server.permission.events.PermissionGatherEvent;
import java.util.List;
@Mod("irisworldgen")
public final class IrisNeoForgeBootstrap {
@@ -82,18 +89,55 @@ public final class IrisNeoForgeBootstrap {
ModdedEngineBootstrap.levelLoaded(level);
}
});
NeoForge.EVENT_BUS.addListener((RegisterCommandsEvent event) -> IrisModdedCommands.register(event.getDispatcher()));
NeoForge.EVENT_BUS.addListener((BreakBlockEvent event) -> {
if (event.getLevel() instanceof ServerLevel level && !event.isCanceled()) {
ModdedBlockBreakHandler.prepare(level, event.getPos(), event.getState());
NeoForge.EVENT_BUS.addListener((LevelEvent.Unload event) -> {
if (event.getLevel() instanceof ServerLevel level) {
ModdedEngineBootstrap.levelUnloaded(level);
}
});
NeoForge.EVENT_BUS.addListener((RegisterCommandsEvent event) -> IrisModdedCommands.register(event.getDispatcher()));
NeoForge.EVENT_BUS.addListener((PermissionGatherEvent.Nodes event) ->
event.addNodes(NeoForgeModdedLoader.TREE_FELLER_PERMISSION));
NeoForge.EVENT_BUS.addListener((BreakBlockEvent event) -> {
if (event.getLevel() instanceof ServerLevel level
&& event.getPlayer() instanceof ServerPlayer player
&& !event.isCanceled()) {
ModdedBlockBreakHandler.prepare(level, player, event.getPos(), event.getState());
}
});
NeoForge.EVENT_BUS.addListener(
EventPriority.LOWEST,
false,
(BlockEvent.EntityPlaceEvent event) -> {
if (!(event instanceof BlockEvent.EntityMultiPlaceEvent)
&& event.getLevel() instanceof ServerLevel level) {
ModdedBlockBreakHandler.clearPlacedProvenance(level, event.getPos());
}
}
);
NeoForge.EVENT_BUS.addListener(
EventPriority.LOWEST,
false,
(BlockEvent.EntityMultiPlaceEvent event) -> {
for (BlockSnapshot snapshot : event.getReplacedBlockSnapshots()) {
if (snapshot.getLevel() instanceof ServerLevel level) {
ModdedBlockBreakHandler.clearPlacedProvenance(level, snapshot.getPos());
}
}
}
);
NeoForge.EVENT_BUS.addListener((BlockDropsEvent event) -> {
if (!(event.getBreaker() instanceof Player)) {
return;
}
ServerLevel level = event.getLevel();
ModdedBlockBreakHandler.Result result = ModdedBlockBreakHandler.complete(level, event.getPos(), event.getState());
List<ItemStack> vanillaDrops = event.getDrops().stream()
.map(ItemEntity::getItem)
.toList();
if (result.routeCombinedDrops(vanillaDrops)) {
event.getDrops().clear();
return;
}
if (result.replaceVanillaDrops()) {
event.getDrops().clear();
}
@@ -19,18 +19,37 @@
package art.arcane.iris.neoforge;
import art.arcane.iris.modded.ModdedLoader;
import art.arcane.iris.modded.service.ModdedTreeFellerService;
import net.minecraft.commands.Commands;
import net.minecraft.core.BlockPos;
import net.minecraft.server.MinecraftServer;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.world.level.block.state.BlockState;
import net.neoforged.neoforge.common.NeoForge;
import net.neoforged.neoforge.event.level.block.BreakBlockEvent;
import net.neoforged.fml.ModList;
import net.neoforged.fml.loading.FMLEnvironment;
import net.neoforged.fml.loading.FMLLoader;
import net.neoforged.fml.loading.FMLPaths;
import net.neoforged.neoforge.server.ServerLifecycleHooks;
import net.neoforged.neoforge.server.permission.PermissionAPI;
import net.neoforged.neoforge.server.permission.nodes.PermissionNode;
import net.neoforged.neoforge.server.permission.nodes.PermissionTypes;
import net.neoforged.neoforgespi.language.IModFileInfo;
import java.io.File;
import java.nio.file.Path;
public final class NeoForgeModdedLoader implements ModdedLoader {
public static final PermissionNode<Boolean> TREE_FELLER_PERMISSION = new PermissionNode<>(
"iris",
"treefeller",
PermissionTypes.BOOLEAN,
(player, playerId, contexts) ->
player != null && Commands.LEVEL_GAMEMASTERS.check(player.permissions())
);
@Override
public String platformName() {
return "neoforge";
@@ -73,4 +92,23 @@ public final class NeoForgeModdedLoader implements ModdedLoader {
IModFileInfo info = ModList.get().getModFileById("irisworldgen");
return info == null ? null : info.getFile().getFilePath().toFile();
}
@Override
public boolean hasTreeFellerPermission(ServerPlayer player) {
return PermissionAPI.getPermission(player, TREE_FELLER_PERMISSION);
}
@Override
public boolean canTreeFellerBreak(
ServerLevel level,
ServerPlayer player,
BlockPos position,
BlockState state
) {
return ModdedTreeFellerService.runBreakProbe(() -> {
BreakBlockEvent event = new BreakBlockEvent(level, position, state, player);
NeoForge.EVENT_BUS.post(event);
return !event.isCanceled();
});
}
}
@@ -45,6 +45,7 @@ public class IrisSettings {
private IrisSettingsPerformance performance = new IrisSettingsPerformance();
private IrisSettingsPregen pregen = new IrisSettingsPregen();
private IrisSettingsSentry sentry = new IrisSettingsSentry();
private IrisSettingsTreeFeller treeFeller = new IrisSettingsTreeFeller();
public static int getThreadCount(int c) {
return Math.max(switch (c) {
@@ -229,16 +230,8 @@ public class IrisSettings {
public int noiseCacheSize = 1_024;
public int resourceLoaderCacheSize = 1_024;
public int objectLoaderCacheSize = 4_096;
public int tectonicPlateSize = -1;
public int mantleCleanupDelay = 200;
public boolean simdKernels = true;
public int getTectonicPlateSize() {
if (tectonicPlateSize > 0)
return tectonicPlateSize;
return (int) (getHardware.getProcessMemory() / 512L);
}
}
@Data
@@ -286,6 +279,16 @@ public class IrisSettings {
public boolean preventLeafDecay = true;
}
@Data
public static class IrisSettingsTreeFeller {
public boolean enabled = false;
public int durabilityPreservationChance = 0;
public int getDurabilityPreservationChance() {
return Math.max(0, Math.min(durabilityPreservationChance, 100));
}
}
@Data
public static class IrisSettingsStudio {
public boolean studio = true;
@@ -300,9 +303,22 @@ public class IrisSettings {
public boolean useVirtualThreads = true;
public boolean forceMulticoreWrite = false;
public int priority = Thread.NORM_PRIORITY;
public int parallelism = -1;
public int getPriority() {
return Math.max(Math.min(priority, Thread.MAX_PRIORITY), Thread.MIN_PRIORITY);
}
public int getParallelism() {
int processors = Math.max(1, Runtime.getRuntime().availableProcessors());
if (parallelism > 0) {
int maximumParallelism = processors > Integer.MAX_VALUE / 2
? Integer.MAX_VALUE
: processors * 2;
return Math.min(parallelism, maximumParallelism);
}
return Math.max(1, (int) Math.ceil(Math.sqrt(processors)));
}
}
}
@@ -45,6 +45,7 @@ import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Consumer;
public class PregeneratorJob implements PregenListener, PregenRenderSource {
private static final long WORLD_SHUTDOWN_TIMEOUT_MILLIS = 15_000L;
private static final Color COLOR_EXISTS = parseColor("#4d7d5b");
private static final Color COLOR_BLACK = parseColor("#4d7d5b");
private static final Color COLOR_MANTLE = parseColor("#3c2773");
@@ -130,18 +131,36 @@ public class PregeneratorJob implements PregenListener, PregenRenderSource {
return true;
}
public static boolean shutdownInstanceForWorld(String worldIdentity) {
PregeneratorJob inst = instance.get();
if (inst == null || !inst.targetsWorldIdentity(worldIdentity)) {
return false;
}
return shutdownAndWait(inst, WORLD_SHUTDOWN_TIMEOUT_MILLIS);
}
public static boolean shutdownAndWait(long timeoutMs) {
PregeneratorJob inst = instance.get();
if (inst == null) {
return false;
}
return shutdownAndWait(inst, timeoutMs);
}
private static boolean shutdownAndWait(PregeneratorJob inst, long timeoutMs) {
inst.pregenerator.close();
inst.worker.interrupt();
try {
inst.worker.join(timeoutMs);
inst.worker.join(Math.max(1L, timeoutMs));
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new IllegalStateException("Interrupted while stopping the Iris pregenerator.", e);
}
if (inst.worker.isAlive()) {
throw new IllegalStateException("Timed out while stopping the Iris pregenerator after "
+ Math.max(1L, timeoutMs) + "ms.");
}
instance.compareAndSet(inst, null);
return true;
@@ -184,7 +203,7 @@ public class PregeneratorJob implements PregenListener, PregenRenderSource {
return inst == null ? -1L : Math.max(0L, inst.lastChunksRemaining);
}
public record PregenProgress(double percent, long generated, long totalChunks, double chunksPerSecond, long chunksRemaining, long eta, long elapsed, String method, boolean paused, long failed, String worldName) {
public record PregenProgress(double percent, long generated, long totalChunks, double chunksPerSecond, long chunksRemaining, long eta, long elapsed, String method, boolean paused, long failed, String worldName, String worldIdentity) {
}
public static PregenProgress progressSnapshot() {
@@ -205,7 +224,8 @@ public class PregeneratorJob implements PregenListener, PregenRenderSource {
inst.lastMethod,
inst.paused(),
inst.pregenerator.getFailedChunks(),
inst.worldName());
inst.worldName(),
inst.worldIdentity());
}
public String worldName() {
@@ -216,6 +236,13 @@ public class PregeneratorJob implements PregenListener, PregenRenderSource {
return engine.getWorld().name();
}
public String worldIdentity() {
if (engine == null || engine.getWorld() == null) {
return null;
}
return engine.getWorld().identity();
}
public boolean targetsWorldIdentity(String worldIdentity) {
if (worldIdentity == null || engine == null || engine.getWorld() == null) {
return false;
@@ -65,7 +65,11 @@ import art.arcane.iris.util.common.parallel.MultiBurst;
import art.arcane.iris.util.common.reflect.KeyedType;
import art.arcane.volmlib.util.scheduling.ChronoLatch;
import art.arcane.iris.util.common.scheduling.J;
import art.arcane.iris.util.project.context.IrisContext;
import lombok.AccessLevel;
import lombok.Data;
import lombok.Getter;
import lombok.Setter;
import org.jetbrains.annotations.Nullable;
import java.io.File;
@@ -73,15 +77,20 @@ import java.io.FileReader;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
@Data
public class IrisData implements ExclusionStrategy, TypeAdapterFactory {
private static final KMap<File, IrisData> dataLoaders = new KMap<>();
private static final Map<File, IrisData> dataLoaders = new ConcurrentHashMap<>();
private final File dataFolder;
private final int id;
private final boolean datapackCompiler;
@@ -108,14 +117,15 @@ public class IrisData implements ExclusionStrategy, TypeAdapterFactory {
private Gson snippetLoader;
private GsonBuilder builder;
private KMap<Class<? extends IrisRegistrant>, ResourceLoader<? extends IrisRegistrant>> loaders = new KMap<>();
private Engine engine;
@Getter(AccessLevel.NONE)
@Setter(AccessLevel.NONE)
private final transient List<Engine> engines = new ArrayList<>();
private IrisData(File dataFolder) {
this(dataFolder, false);
}
private IrisData(File dataFolder, boolean datapackCompiler) {
this.engine = null;
this.dataFolder = dataFolder;
this.id = RNG.r.imax();
this.datapackCompiler = datapackCompiler;
@@ -130,6 +140,10 @@ public class IrisData implements ExclusionStrategy, TypeAdapterFactory {
return dataLoaders.computeIfAbsent(dataFolder, IrisData::new);
}
public static IrisData openRuntime(File dataFolder) {
return new IrisData(dataFolder);
}
public static IrisData openDatapackCompiler(File dataFolder) {
return new IrisData(dataFolder, true);
}
@@ -275,9 +289,72 @@ public class IrisData implements ExclusionStrategy, TypeAdapterFactory {
}
public void cleanupEngine() {
if (engine != null && engine.isClosed()) {
engine = null;
IrisLogging.debug("Dereferenced Data<Engine> " + getId() + " " + getDataFolder());
int removed;
synchronized (engines) {
int previousSize = engines.size();
removeClosedEngines();
removed = previousSize - engines.size();
}
if (removed > 0) {
IrisLogging.debug("Dereferenced " + removed + " Data<Engine> registration(s) " + getId() + " " + getDataFolder());
}
}
public Engine getEngine() {
IrisContext context = IrisContext.get();
if (context != null) {
Engine contextEngine = context.getEngine();
if (!contextEngine.isClosed() && contextEngine.getData() == this) {
return contextEngine;
}
}
synchronized (engines) {
removeClosedEngines();
return engines.size() == 1 ? engines.get(0) : null;
}
}
public List<Engine> getEngines() {
synchronized (engines) {
removeClosedEngines();
return List.copyOf(engines);
}
}
public void registerEngine(Engine engine) {
Objects.requireNonNull(engine, "engine");
synchronized (engines) {
for (Engine registeredEngine : engines) {
if (registeredEngine == engine) {
return;
}
}
engines.add(engine);
}
}
public void unregisterEngine(Engine engine) {
if (engine == null) {
return;
}
synchronized (engines) {
Iterator<Engine> iterator = engines.iterator();
while (iterator.hasNext()) {
if (iterator.next() == engine) {
iterator.remove();
return;
}
}
}
}
private void removeClosedEngines() {
Iterator<Engine> iterator = engines.iterator();
while (iterator.hasNext()) {
if (iterator.next().isClosed()) {
iterator.remove();
}
}
}
@@ -287,6 +364,9 @@ public class IrisData implements ExclusionStrategy, TypeAdapterFactory {
public void close() {
closed = true;
dump();
synchronized (engines) {
engines.clear();
}
if (dataLoaders.get(dataFolder) == this) {
dataLoaders.remove(dataFolder);
}
@@ -365,7 +445,7 @@ public class IrisData implements ExclusionStrategy, TypeAdapterFactory {
gson = builder.create();
if (engine != null) {
for (Engine engine : getEngines()) {
engine.hotload();
}
}
@@ -405,7 +485,7 @@ public class IrisData implements ExclusionStrategy, TypeAdapterFactory {
invalidateLoader(structureLoader);
invalidateLoader(jigsawPoolLoader);
invalidateLoader(jigsawPieceLoader);
if (engine != null) {
for (Engine engine : getEngines()) {
IrisStructureLocator.invalidate(engine);
}
}
@@ -65,6 +65,16 @@ public final class MantleHeapPressure {
return false;
}
public static double reclaimUrgency(double fraction) {
if (!Double.isFinite(fraction) || fraction <= LOW_WATER) {
return 0D;
}
if (fraction >= HIGH_WATER) {
return 1D;
}
return (fraction - LOW_WATER) / (HIGH_WATER - LOW_WATER);
}
public static boolean overPanicWater() {
return usedFraction() >= PANIC_WATER;
}
@@ -0,0 +1,94 @@
package art.arcane.iris.core.service;
import art.arcane.iris.core.IrisSettings;
import art.arcane.iris.core.gui.PregeneratorJob;
import art.arcane.iris.core.pregenerator.MantleHeapPressure;
import art.arcane.iris.core.runtime.GoldenHashEngine;
import art.arcane.iris.core.tools.WorldMaintenance;
import art.arcane.iris.engine.framework.Engine;
import java.util.Locale;
import java.util.concurrent.TimeUnit;
public final class EngineMaintenance {
private EngineMaintenance() {
}
public static boolean isAvailable(Engine engine) {
return engine != null
&& !engine.isClosing()
&& !engine.isClosed()
&& !engine.getMantle().getMantle().isClosed();
}
public static boolean pregeneratorTargets(Engine engine) {
if (engine == null || engine.getWorld() == null) {
return false;
}
PregeneratorJob pregeneratorJob = PregeneratorJob.getInstance();
return pregeneratorJob != null && pregeneratorJob.targetsWorldIdentity(engine.getWorld().identity());
}
public static boolean shouldRun(Engine engine) {
if (engine.isStudio()
&& !IrisSettings.get().getPerformance().isTrimMantleInStudio()) {
return false;
}
if (GoldenHashEngine.isActive()) {
return false;
}
return engine.getWorld() == null
|| !WorldMaintenance.isWorldMaintenanceActive(engine.getWorld().identity());
}
public static int workerParallelism() {
return IrisSettings.get().getPerformance().getEngineSVC().getParallelism();
}
public static Outcome run(Engine engine) {
IrisSettings.IrisSettingsPerformance settings = IrisSettings.get().getPerformance();
double heapUsage = MantleHeapPressure.usedFraction();
Plan plan = plan(settings.getMantleKeepAlive(), heapUsage, settings.getEngineSVC().isForceMulticoreWrite());
engine.getMantle().trim(plan.idleDurationMillis());
long unloadStart = System.nanoTime();
int unloadedTectonicPlates = engine.getMantle().unloadTectonicPlate(
plan.multicoreUnload() ? 0 : Integer.MAX_VALUE);
if (plan.heapPressure() && MantleHeapPressure.overPanicWater()) {
MantleHeapPressure.requestPanicReclaim();
}
return new Outcome(
unloadedTectonicPlates,
TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - unloadStart));
}
public static boolean isMantleClosed(Throwable throwable) {
Throwable current = throwable;
while (current != null) {
String message = current.getMessage();
if (message != null && message.toLowerCase(Locale.ROOT).contains("mantle is closed")) {
return true;
}
current = current.getCause();
}
return false;
}
static Plan plan(int mantleKeepAliveSeconds, double heapUsage, boolean forceMulticoreWrite) {
long baseIdleDurationMillis = TimeUnit.SECONDS.toMillis(Math.max(0, mantleKeepAliveSeconds));
double reclaimUrgency = MantleHeapPressure.reclaimUrgency(heapUsage);
long idleDurationMillis = Math.round(baseIdleDurationMillis * (1D - reclaimUrgency));
boolean heapPressure = reclaimUrgency >= 1D;
return new Plan(idleDurationMillis, heapPressure || forceMulticoreWrite, heapPressure);
}
public record Outcome(int unloadedTectonicPlates, long unloadDurationMillis) {
}
record Plan(long idleDurationMillis, boolean multicoreUnload, boolean heapPressure) {
}
}
@@ -23,6 +23,7 @@ import art.arcane.iris.spi.IrisServices;
import art.arcane.iris.core.loader.IrisData;
import art.arcane.iris.core.tools.IrisToolbelt;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.framework.TreeBlockMaterial;
import art.arcane.iris.engine.object.IObjectPlacer;
import art.arcane.iris.engine.object.IrisBiome;
import art.arcane.iris.engine.object.IrisDimension;
@@ -59,6 +60,7 @@ import org.bukkit.event.world.StructureGrowEvent;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.ThreadLocalRandom;
import java.util.function.Predicate;
public class TreeSVC implements IrisService {
@@ -138,6 +140,7 @@ public class TreeSVC implements IrisService {
saplingPlane.forEach(block -> block.setType(Material.AIR));
IrisObject object = worldAccess.getData().getObjectLoader().load(placement.getPlace().getRandom(RNG.r));
String treeMarker = object.getLoadKey() + "@" + ThreadLocalRandom.current().nextInt(Integer.MAX_VALUE);
List<BlockState> blockStateList = new KList<>();
KMap<Location, BlockData> dataCache = new KMap<>();
// TODO: REAL CLASSES!!!!
@@ -234,7 +237,7 @@ public class TreeSVC implements IrisService {
event.setCancelled(true);
J.s(() -> {
Runnable growTask = () -> {
StructureGrowEvent iGrow = new StructureGrowEvent(event.getLocation(), event.getSpecies(), event.isFromBonemeal(), event.getPlayer(), blockStateList);
block = true;
@@ -253,9 +256,20 @@ public class TreeSVC implements IrisService {
block.setBlockData(data.getBase(), false);
IrisServices.get(ExternalDataSVC.class).processUpdate(engine, block, data.getCustom());
} else block.setBlockData(d, false);
int mantleY = block.getY() - event.getWorld().getMinHeight();
engine.getMantle().getMantle().set(block.getX(), mantleY, block.getZ(), treeMarker);
engine.getMantle().getMantle().set(
block.getX(),
mantleY,
block.getZ(),
TreeBlockMaterial.of(block.getBlockData().getAsString())
);
}
}
});
};
if (!J.runAt(event.getLocation(), growTask) && !J.isFolia()) {
J.s(growTask);
}
}
/**
@@ -0,0 +1,6 @@
package art.arcane.iris.core.service.tree;
@FunctionalInterface
public interface BlockDropRouter {
boolean routeDrop(Object drop);
}
@@ -0,0 +1,97 @@
package art.arcane.iris.core.service.tree;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.framework.StructurePlacementMarker;
import art.arcane.iris.engine.object.IrisBiome;
import art.arcane.iris.engine.object.IrisObjectPlacement;
import art.arcane.iris.engine.object.IrisProceduralObjects;
import art.arcane.iris.engine.object.IrisProceduralTree;
import art.arcane.iris.engine.object.IrisRegion;
import java.util.HashSet;
import java.util.Set;
public final class TreeDefinitionIndex {
private static final String PROCEDURAL_TREE_PREFIX = "procedural/tree/";
private final Set<String> explicitObjectKeys;
private final Set<String> proceduralTreeKeys;
private TreeDefinitionIndex(Set<String> explicitObjectKeys, Set<String> proceduralTreeKeys) {
this.explicitObjectKeys = Set.copyOf(explicitObjectKeys);
this.proceduralTreeKeys = Set.copyOf(proceduralTreeKeys);
}
public static TreeDefinitionIndex build(Engine engine) {
Set<String> explicitObjectKeys = new HashSet<>();
Set<String> proceduralTreeKeys = new HashSet<>();
for (IrisRegion region : engine.getDimension().getAllRegions(engine)) {
if (region == null) {
continue;
}
collectPlacements(region.getObjects(), explicitObjectKeys);
collectProceduralTrees(region.getProceduralObjects(), proceduralTreeKeys);
}
for (IrisBiome biome : engine.getDimension().getReachableBiomes(engine)) {
if (biome == null) {
continue;
}
collectPlacements(biome.getObjects(), explicitObjectKeys);
collectProceduralTrees(biome.getProceduralObjects(), proceduralTreeKeys);
}
return new TreeDefinitionIndex(explicitObjectKeys, proceduralTreeKeys);
}
public boolean isTreeMarker(String marker) {
StructurePlacementMarker.Decoded decoded = StructurePlacementMarker.decode(marker);
if (decoded == null || decoded.structureAware()) {
return false;
}
String objectKey = decoded.objectKey();
if (objectKey.startsWith(PROCEDURAL_TREE_PREFIX)) {
return proceduralTreeKeys.contains(objectKey);
}
if (objectKey.startsWith("procedural/")) {
return false;
}
return objectKey.startsWith("trees/") || explicitObjectKeys.contains(objectKey);
}
private static void collectPlacements(Iterable<IrisObjectPlacement> placements, Set<String> objectKeys) {
if (placements == null) {
return;
}
for (IrisObjectPlacement placement : placements) {
if (placement == null
|| placement.getTrees() == null
|| placement.getTrees().isEmpty()
|| placement.getPlace() == null) {
continue;
}
for (String objectKey : placement.getPlace()) {
if (objectKey != null && !objectKey.isBlank()) {
objectKeys.add(objectKey);
}
}
}
}
private static void collectProceduralTrees(IrisProceduralObjects proceduralObjects, Set<String> objectKeys) {
if (proceduralObjects == null || proceduralObjects.getTrees() == null) {
return;
}
for (IrisProceduralTree tree : proceduralObjects.getTrees()) {
if (tree == null || tree.getName() == null || tree.getName().isBlank()) {
continue;
}
int variants = Math.max(1, tree.getVariants());
for (int index = 0; index < variants; index++) {
objectKeys.add(tree.getVariantLoadKey(index));
}
}
}
}
@@ -0,0 +1,84 @@
package art.arcane.iris.core.service.tree;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
public final class TreeMarkerTraversal {
public static final int MAX_MEMBERS = 131_072;
public static final int MAX_VISITED = 1_000_000;
public static final int MAX_AXIS_DISTANCE = 256;
private TreeMarkerTraversal() {
}
public static Discovery discover(Position trigger, String marker, int minimumY, int maximumY, MarkerLookup lookup) {
ArrayDeque<Position> pending = new ArrayDeque<>();
Set<Position> visited = new HashSet<>();
List<Position> members = new ArrayList<>();
pending.add(trigger);
visited.add(trigger);
while (!pending.isEmpty()) {
Position current = pending.removeFirst();
if (!marker.equals(lookup.markerAt(current.x(), current.y(), current.z()))) {
continue;
}
if (members.size() >= MAX_MEMBERS) {
return new Discovery(List.copyOf(members), false);
}
members.add(current);
for (int xOffset = -1; xOffset <= 1; xOffset++) {
for (int yOffset = -1; yOffset <= 1; yOffset++) {
for (int zOffset = -1; zOffset <= 1; zOffset++) {
if (xOffset == 0 && yOffset == 0 && zOffset == 0) {
continue;
}
Position next = current.offset(xOffset, yOffset, zOffset);
if (next.y() < minimumY || next.y() >= maximumY) {
continue;
}
if (!withinAxisBounds(trigger, next)) {
if (marker.equals(lookup.markerAt(next.x(), next.y(), next.z()))) {
return new Discovery(List.copyOf(members), false);
}
continue;
}
if (!visited.add(next)) {
continue;
}
if (visited.size() > MAX_VISITED) {
return new Discovery(List.copyOf(members), false);
}
pending.addLast(next);
}
}
}
}
return new Discovery(List.copyOf(members), true);
}
private static boolean withinAxisBounds(Position trigger, Position position) {
return Math.abs(position.x() - trigger.x()) <= MAX_AXIS_DISTANCE
&& Math.abs(position.y() - trigger.y()) <= MAX_AXIS_DISTANCE
&& Math.abs(position.z() - trigger.z()) <= MAX_AXIS_DISTANCE;
}
@FunctionalInterface
public interface MarkerLookup {
String markerAt(int x, int y, int z);
}
public record Position(int x, int y, int z) {
public Position offset(int xOffset, int yOffset, int zOffset) {
return new Position(x + xOffset, y + yOffset, z + zOffset);
}
}
public record Discovery(List<Position> members, boolean complete) {
}
}
@@ -19,30 +19,87 @@
package art.arcane.iris.engine;
import art.arcane.iris.spi.IrisLogging;
import art.arcane.volmlib.util.collection.KMap;
public class EnginePanic {
private static final KMap<String, String> stuff = new KMap<>();
private static KMap<String, String> last = new KMap<>();
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.concurrent.atomic.AtomicReference;
public final class EnginePanic {
private static final Diagnostics GLOBAL = scoped("global");
private EnginePanic() {
}
public static Diagnostics scoped(String scope) {
return new Diagnostics(scope);
}
public static void add(String key, String value) {
stuff.put(key, value);
GLOBAL.add(key, value);
}
public static void saveLast() {
last = stuff.copy();
GLOBAL.saveLast();
}
public static void lastPanic() {
for (String i : last.keySet()) {
IrisLogging.error("Last Panic " + i + ": " + stuff.get(i));
}
GLOBAL.lastPanic();
}
public static void panic() {
lastPanic();
for (String i : stuff.keySet()) {
IrisLogging.error("Engine Panic " + i + ": " + stuff.get(i));
GLOBAL.panic();
}
public static final class Diagnostics {
private final String scope;
private final ThreadLocal<LinkedHashMap<String, String>> current = ThreadLocal.withInitial(LinkedHashMap::new);
private final AtomicReference<Map<String, String>> last = new AtomicReference<>(Map.of());
private Diagnostics(String scope) {
this.scope = scope == null || scope.isBlank() ? "unknown" : scope;
}
public void add(String key, String value) {
current.get().put(String.valueOf(key), String.valueOf(value));
}
public void saveLast() {
last.set(currentSnapshot());
current.remove();
}
public void lastPanic() {
log("Last Panic", last.get());
}
public void panic() {
Map<String, String> currentSnapshot = currentSnapshot();
lastPanic();
log("Engine Panic", currentSnapshot);
current.remove();
}
Map<String, String> currentSnapshot() {
return immutableSnapshot(current.get());
}
Map<String, String> lastSnapshot() {
return last.get();
}
private void log(String prefix, Map<String, String> snapshot) {
for (Map.Entry<String, String> entry : snapshot.entrySet()) {
IrisLogging.error(prefix + " [" + scope + "] " + entry.getKey() + ": " + entry.getValue());
}
}
private static Map<String, String> immutableSnapshot(Map<String, String> values) {
if (values.isEmpty()) {
return Map.of();
}
return Collections.unmodifiableMap(new LinkedHashMap<>(values));
}
}
}
@@ -48,7 +48,7 @@ public final class GenerationCacheWarmer {
KList<IrisBiome> biomes = engine.getAllBiomes();
biomes.sort(Comparator.comparing(IrisBiome::getLoadKey));
for (IrisBiome biome : biomes) {
warmPlacements(biome.getObjects(), root, counter, data);
warmPlacements(biome.getObjects(), root, counter, data, engine);
warmDecorators(biome.getDecorators(), root, counter, data);
warmOres(biome.getOres(), root, counter, data);
warmProcedural(biome.getProceduralObjects(), root, counter, data);
@@ -57,7 +57,7 @@ public final class GenerationCacheWarmer {
KList<IrisRegion> regions = engine.getDimension().getAllRegions(engine);
regions.sort(Comparator.comparing(IrisRegion::getLoadKey));
for (IrisRegion region : regions) {
warmPlacements(region.getObjects(), root, counter, data);
warmPlacements(region.getObjects(), root, counter, data, engine);
warmOres(region.getOres(), root, counter, data);
warmProcedural(region.getProceduralObjects(), root, counter, data);
}
@@ -71,7 +71,8 @@ public final class GenerationCacheWarmer {
IrisLogging.debug("[IrisEngine timing] cache warm " + counter[0] + " configs=" + (M.ms() - start) + "ms");
}
private static void warmPlacements(KList<IrisObjectPlacement> placements, RNG root, int[] counter, IrisData data) {
private static void warmPlacements(KList<IrisObjectPlacement> placements, RNG root, int[] counter,
IrisData data, Engine engine) {
if (placements == null) {
return;
}
@@ -80,7 +81,7 @@ public final class GenerationCacheWarmer {
continue;
}
RNG rng = root.nextParallelRNG(counter[0]++);
placement.getSurfaceWarp(rng, data);
placement.getSurfaceWarp(rng, data, engine);
placement.getDensity(rng, 0, 0, data);
}
}
@@ -111,7 +111,7 @@ public class IrisComplex implements DataProvider {
public IrisComplex(Engine engine, boolean simple) {
int cacheSize = IrisSettings.get().getPerformance().getNoiseCacheSize();
IrisBiome emptyBiome = new IrisBiome();
IrisBiome emptyBiome = new IrisBiome().setInferredType(InferredType.CAVE);
UUID focusUUID = UUID.nameUUIDFromBytes("focus".getBytes());
this.rng = new RNG(engine.getSeedManager().getComplex());
this.data = engine.getData();
@@ -124,19 +124,23 @@ public class IrisComplex implements DataProvider {
Map<InferredType, ProceduralStream<IrisBiome>> inferredStreams = new HashMap<>();
if (focusBiome != null) {
focusBiome.setInferredType(InferredType.LAND);
focusBiome = focusBiome.withInferredType(InferredType.LAND);
focusRegion = findRegion(focusBiome, engine);
}
//@builder
if (focusRegion != null) {
prepareInferredBiomes(focusRegion);
focusRegion.getAllBiomes(this).forEach(this::registerGenerators);
} else {
engine.getDimension()
.getRegions()
.forEach(i -> data.getRegionLoader().load(i)
.getAllBiomes(this)
.forEach(this::registerGenerators));
engine.getDimension().getRegions().forEach(regionKey -> {
IrisRegion region = data.getRegionLoader().load(regionKey);
if (region == null) {
return;
}
prepareInferredBiomes(region);
region.getAllBiomes(this).forEach(this::registerGenerators);
});
}
generatorBounds = buildGeneratorBounds(engine);
KList<IrisShapedGeneratorStyle> overlayNoise = engine.getDimension().getOverlayNoise();
@@ -171,7 +175,7 @@ public class IrisComplex implements DataProvider {
-> engine.getDimension().getCaveBiomeStyle().create(rng.nextParallelRNG(InferredType.CAVE.ordinal()), getData()).stream()
.zoom(engine.getDimension().getBiomeZoom())
.zoom(r.getCaveBiomeZoom())
.selectRarity(data.getBiomeLoader().loadAll(r.getCaveBiomes()))
.selectRarity(loadInferredBiomes(r.getCaveBiomes(), InferredType.CAVE))
.onNull(emptyBiome)
).convertAware2D(ProceduralStream::get).cache2D("caveBiomeStream", engine, cacheSize).waste("Cave Biome Stream");
inferredStreams.put(InferredType.CAVE, caveBiomeStream);
@@ -181,7 +185,7 @@ public class IrisComplex implements DataProvider {
.zoom(engine.getDimension().getBiomeZoom())
.zoom(engine.getDimension().getLandZoom())
.zoom(r.getLandBiomeZoom())
.selectRarity(data.getBiomeLoader().loadAll(r.getLandBiomes(), (t) -> t.setInferredType(InferredType.LAND)))
.selectRarity(loadInferredBiomes(r.getLandBiomes(), InferredType.LAND))
).convertAware2D(ProceduralStream::get)
.cache2D("landBiomeStream", engine, cacheSize).waste("Land Biome Stream");
inferredStreams.put(InferredType.LAND, landBiomeStream);
@@ -191,7 +195,7 @@ public class IrisComplex implements DataProvider {
.zoom(engine.getDimension().getBiomeZoom())
.zoom(engine.getDimension().getSeaZoom())
.zoom(r.getSeaBiomeZoom())
.selectRarity(data.getBiomeLoader().loadAll(r.getSeaBiomes(), (t) -> t.setInferredType(InferredType.SEA)))
.selectRarity(loadInferredBiomes(r.getSeaBiomes(), InferredType.SEA))
).convertAware2D(ProceduralStream::get)
.cache2D("seaBiomeStream", engine, cacheSize).waste("Sea Biome Stream");
inferredStreams.put(InferredType.SEA, seaBiomeStream);
@@ -200,7 +204,7 @@ public class IrisComplex implements DataProvider {
-> engine.getDimension().getShoreBiomeStyle().create(rng.nextParallelRNG(InferredType.SHORE.ordinal()), getData()).stream()
.zoom(engine.getDimension().getBiomeZoom())
.zoom(r.getShoreBiomeZoom())
.selectRarity(data.getBiomeLoader().loadAll(r.getShoreBiomes(), (t) -> t.setInferredType(InferredType.SHORE)))
.selectRarity(loadInferredBiomes(r.getShoreBiomes(), InferredType.SHORE))
).convertAware2D(ProceduralStream::get).cache2D("shoreBiomeStream", engine, cacheSize).waste("Shore Biome Stream");
inferredStreams.put(InferredType.SHORE, shoreBiomeStream);
bridgeStream = focusBiome != null ? ProceduralStream.of((x, z) -> focusBiome.getInferredType(),
@@ -312,22 +316,49 @@ public class IrisComplex implements DataProvider {
}
private IrisBiome fixBiomeType(Double height, IrisBiome biome, IrisRegion region, Double x, Double z, double fluidHeight) {
IrisBiome resolved = resolveSurfaceBiome(
height,
biome,
region,
x,
z,
fluidHeight,
landBiomeStream,
seaBiomeStream,
shoreBiomeStream);
return resolved == biome ? biome : implode(resolved, x, z);
}
static IrisBiome resolveSurfaceBiome(
double height,
IrisBiome biome,
IrisRegion region,
double x,
double z,
double fluidHeight,
ProceduralStream<IrisBiome> landBiomes,
ProceduralStream<IrisBiome> seaBiomes,
ProceduralStream<IrisBiome> shoreBiomes
) {
if (biome == null || region == null) {
return biome;
}
double sh = region.getShoreHeight(x, z);
if (height >= fluidHeight - 1 && height <= fluidHeight + sh && !biome.isShore()) {
return shoreBiomeStream.get(x, z);
return shoreBiomes.get(x, z);
}
if (height > fluidHeight + sh && !biome.isLand()) {
return landBiomeStream.get(x, z);
return landBiomes.get(x, z);
}
if (height < fluidHeight && !biome.isAquatic()) {
return seaBiomeStream.get(x, z);
return seaBiomes.get(x, z);
}
if (height == fluidHeight && !biome.isShore()) {
return shoreBiomeStream.get(x, z);
return shoreBiomes.get(x, z);
}
return biome;
@@ -446,6 +477,21 @@ public class IrisComplex implements DataProvider {
return Math.max(Math.min(getInterpolatedHeight(engine, x, z, seed) + fluidHeight + overlayStream.get(x, z), engine.getHeight()), 0);
}
private void prepareInferredBiomes(IrisRegion region) {
loadInferredBiomes(region.getLandBiomes(), InferredType.LAND);
loadInferredBiomes(region.getCaveBiomes(), InferredType.CAVE);
loadInferredBiomes(region.getSeaBiomes(), InferredType.SEA);
loadInferredBiomes(region.getShoreBiomes(), InferredType.SHORE);
}
private KList<IrisBiome> loadInferredBiomes(KList<String> keys, InferredType type) {
KList<IrisBiome> inferred = new KList<>();
for (IrisBiome biome : data.getBiomeLoader().loadAll(keys)) {
inferred.add(biome.withInferredType(type));
}
return inferred;
}
private void registerGenerators(IrisBiome biome) {
generatorBiomes.add(biome);
biome.getGenerators().forEach(c -> registerGenerator(c.getCachedGenerator(this)));
@@ -532,8 +578,7 @@ public class IrisComplex implements DataProvider {
CNG childCell = b.getChildrenGenerator(rng, 123, b.getChildShrinkFactor());
ChildSelectionPlan childSelectionPlan = resolveChildSelectionPlan(b);
IrisBiome biome = childSelectionPlan.select(childCell, x, z);
biome.setInferredType(b.getInferredType());
IrisBiome biome = childSelectionPlan.select(childCell, x, z).withInferredType(b.getInferredType());
return implode(biome, x, z, max - 1);
}
@@ -640,7 +685,7 @@ public class IrisComplex implements DataProvider {
}
}
private static class ChildSelectionPlan {
static final class ChildSelectionPlan {
private final IrisBiome[] mappedBiomes;
private final int maxIndex;
@@ -649,7 +694,7 @@ public class IrisComplex implements DataProvider {
this.maxIndex = mappedBiomes.length - 1;
}
private static ChildSelectionPlan create(KList<IrisBiome> options) {
static ChildSelectionPlan create(KList<IrisBiome> options) {
if (options.isEmpty()) {
return new ChildSelectionPlan(new IrisBiome[0]);
}
@@ -690,7 +735,7 @@ public class IrisComplex implements DataProvider {
return new ChildSelectionPlan(mappedBiomes);
}
private IrisBiome select(CNG childCell, double x, double z) {
IrisBiome select(CNG childCell, double x, double z) {
if (mappedBiomes.length == 0) {
return null;
}
File diff suppressed because it is too large Load Diff
@@ -36,19 +36,23 @@ import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Semaphore;
import java.util.concurrent.ThreadLocalRandom;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicLong;
public class IrisEngineEffects extends EngineAssignedComponent implements EngineEffects {
private static final long EFFECT_BUDGET_NANOS = 1_500_000L;
private static final long EMPTY_PLAYER_REFRESH_NANOS = 1_000_000_000L;
private final ConcurrentHashMap<UUID, EnginePlayer> players;
private final Semaphore limit;
private final AtomicBoolean playerMapUpdateQueued;
private final AtomicLong nextEmptyRefresh;
public IrisEngineEffects(Engine engine) {
super(engine, "FX");
players = new ConcurrentHashMap<>();
limit = new Semaphore(1);
playerMapUpdateQueued = new AtomicBoolean(false);
nextEmptyRefresh = new AtomicLong(0L);
}
@Override
@@ -93,7 +97,15 @@ public class IrisEngineEffects extends EngineAssignedComponent implements Engine
return;
}
try {
if (players.isEmpty() || M.r(0.02)) {
if (players.isEmpty()) {
long now = System.nanoTime();
long next = nextEmptyRefresh.get();
if (now >= next && nextEmptyRefresh.compareAndSet(next, now + EMPTY_PLAYER_REFRESH_NANOS)) {
updatePlayerMap();
}
return;
}
if (M.r(0.02)) {
updatePlayerMap();
return;
}
@@ -22,7 +22,6 @@ import art.arcane.iris.spi.IrisPlatforms;
import art.arcane.iris.core.IrisSettings;
import art.arcane.iris.core.loader.IrisData;
import art.arcane.iris.core.tools.WorldMaintenance;
import art.arcane.iris.engine.EnginePanic;
import art.arcane.iris.core.nms.container.Pair;
import art.arcane.iris.engine.data.cache.AtomicCache;
import art.arcane.iris.engine.framework.Engine;
@@ -66,7 +65,9 @@ import java.io.IOException;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.function.Supplier;
@Data
@EqualsAndHashCode(exclude = "engine")
@@ -182,8 +183,8 @@ public class IrisEngineMantle implements EngineMantle {
IrisMatterSupport.ensureRegistered();
File dataFolder = new File(engine.getWorld().worldFolder(), "mantle");
int worldHeight = engine.getTarget().getHeight();
MantleDataAdapter<Matter> adapter = createRuntimeDataAdapter(engine.getData());
MantleHooks hooks = createRuntimeHooks();
MantleDataAdapter<Matter> adapter = createDataAdapter(engine::getData);
MantleHooks hooks = createHooks(EnginePanic.scoped("world " + engine.getWorld().name()));
art.arcane.volmlib.util.mantle.Mantle.RegionIO<TectonicPlate<Matter>> regionIO =
createRegionIO(dataFolder, worldHeight, adapter, hooks);
return new Mantle<>(
@@ -199,14 +200,14 @@ public class IrisEngineMantle implements EngineMantle {
}
public static MantleDataAdapter<Matter> createRuntimeDataAdapter(IrisData data) {
return createDataAdapter(data);
return createDataAdapter(() -> data);
}
public static MantleHooks createRuntimeHooks() {
return createHooks();
return createHooks(EnginePanic.scoped("runtime mantle"));
}
private static MantleDataAdapter<Matter> createDataAdapter(IrisData data) {
private static MantleDataAdapter<Matter> createDataAdapter(Supplier<IrisData> dataSupplier) {
return new MantleDataAdapter<>() {
@Override
public Matter createSection() {
@@ -215,6 +216,7 @@ public class IrisEngineMantle implements EngineMantle {
@Override
public Matter readSection(art.arcane.volmlib.util.io.CountingDataInputStream din) throws IOException {
IrisData data = Objects.requireNonNull(dataSupplier.get(), "Iris mantle data is unavailable.");
try (IrisMatterContext.Scope scope = IrisMatterContext.open(data)) {
return Matter.readDin(din);
}
@@ -285,11 +287,11 @@ public class IrisEngineMantle implements EngineMantle {
};
}
private static MantleHooks createHooks() {
private static MantleHooks createHooks(EnginePanic.Diagnostics panic) {
return new MantleHooks() {
@Override
public void onBeforeReadSection(int index) {
EnginePanic.add("read.section", "Section[" + index + "]");
panic.add("read.section", "Section[" + index + "]");
}
@Override
@@ -299,22 +301,22 @@ public class IrisEngineMantle implements EngineMantle {
art.arcane.volmlib.util.io.CountingDataInputStream din,
IOException error) {
IrisLogging.error("Failed to read chunk section, skipping it.");
EnginePanic.add("read.byte.range", start + " " + end);
EnginePanic.add("read.byte.current", din.count() + "");
panic.add("read.byte.range", start + " " + end);
panic.add("read.byte.current", din.count() + "");
IrisLogging.reportError(error);
error.printStackTrace();
EnginePanic.panic();
panic.panic();
TectonicPlate.addError();
}
@Override
public void onBeforeReadChunk(int index) {
EnginePanic.add("read-chunk", "Chunk[" + index + "]");
panic.add("read-chunk", "Chunk[" + index + "]");
}
@Override
public void onAfterReadChunk(int index) {
EnginePanic.saveLast();
panic.saveLast();
}
@Override
@@ -324,11 +326,11 @@ public class IrisEngineMantle implements EngineMantle {
art.arcane.volmlib.util.io.CountingDataInputStream din,
Throwable error) {
IrisLogging.error("Failed to read chunk, creating a new chunk instead.");
EnginePanic.add("read.byte.range", start + " " + end);
EnginePanic.add("read.byte.current", din.count() + "");
panic.add("read.byte.range", start + " " + end);
panic.add("read.byte.current", din.count() + "");
IrisLogging.reportError(error);
error.printStackTrace();
EnginePanic.panic();
panic.panic();
}
@Override
@@ -22,6 +22,7 @@ import art.arcane.iris.platform.bukkit.BukkitWorldBinding;
import art.arcane.iris.core.IrisSettings;
import art.arcane.iris.core.gui.PregeneratorJob;
import art.arcane.iris.core.loader.IrisData;
import art.arcane.iris.core.service.tree.BlockDropRouter;
import art.arcane.iris.core.tools.IrisToolbelt;
import art.arcane.iris.engine.data.cache.Cache;
import art.arcane.iris.engine.framework.Engine;
@@ -82,9 +83,12 @@ import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.BiConsumer;
import java.util.function.Consumer;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import java.util.stream.Stream;
@@ -95,7 +99,6 @@ public class IrisWorldManager extends EngineAssignedWorldManager {
private static final int MAX_FORCED_CHUNK_UPDATES = 128;
private final Looper looper;
private final int id;
private final KList<Runnable> updateQueue = new KList<>();
private final ChronoLatch cl;
private final ChronoLatch clw;
@@ -111,10 +114,15 @@ public class IrisWorldManager extends EngineAssignedWorldManager {
private final Set<Long> chunkUpdateQueue = ConcurrentHashMap.newKeySet();
private final AtomicBoolean chunkUpdateScanScheduled = new AtomicBoolean();
private final AtomicBoolean chunkDiscoveryScanScheduled = new AtomicBoolean();
private int entityCount = 0;
private int actuallySpawned = 0;
private final AtomicBoolean entityCountWarningReported = new AtomicBoolean();
private final AtomicBoolean entityCountErrorReported = new AtomicBoolean();
private boolean looperStopped;
private boolean cleanupServiceStopped;
private volatile int entityCount = 0;
private final AtomicInteger actuallySpawned = new AtomicInteger();
private int cooldown = 0;
private int forcedChunkUpdateCursor = 0;
private volatile boolean entityCountValid = false;
private volatile boolean playersPresent = false;
private KSet<Position2> injectBiomes = new KSet<>();
private volatile int loadedChunkCount = 0;
@@ -128,7 +136,6 @@ public class IrisWorldManager extends EngineAssignedWorldManager {
chunkUpdater = null;
chunkDiscovery = null;
cleanupService = null;
id = -1;
}
public IrisWorldManager(Engine engine) {
@@ -143,55 +150,82 @@ public class IrisWorldManager extends EngineAssignedWorldManager {
thread.setPriority(Thread.MIN_PRIORITY);
return thread;
});
id = engine.getCacheID();
looper = new Looper() {
@Override
protected long loop() {
if (getEngine().isClosed() || getEngine().getCacheID() != id) {
interrupt();
if (!isManagerStarted()) {
return -1L;
}
if (!getEngine().getWorld().hasPlatformWorld() && clw.flip()) {
J.runGlobal(() -> BukkitWorldBinding.tryBind(getEngine().getWorld()));
}
if (getEngine().getWorld().hasPlatformWorld()) {
if (chunkUpdater.flip()) {
updateChunks();
}
if (!playersPresent) {
return 5000;
}
if (chunkDiscovery.flip()) {
discoverChunks();
}
if (cln.flip()) {
engine.getEngineData().cleanup(getEngine());
}
if (!IrisSettings.get().getWorld().isMarkerEntitySpawningSystem() && !IrisSettings.get().getWorld().isAmbientEntitySpawningSystem()) {
return 3000;
}
onAsyncTick();
}
return IrisSettings.get().getWorld().getAsyncTickIntervalMS();
return callManagerTask(
"bukkit_world_manager_loop",
IrisWorldManager.this::runLoop,
250L);
}
};
looper.setPriority(Thread.MIN_PRIORITY);
looper.setName("Iris World Manager " + getTarget().getWorld().name());
}
public void startManager() {
private long runLoop() {
if (getEngine().isClosed()) {
looper.interrupt();
return -1L;
}
if (!getEngine().getWorld().hasPlatformWorld() && clw.flip()) {
J.runGlobal(() -> runManagerTask(
"bukkit_world_manager_bind",
() -> BukkitWorldBinding.tryBind(getEngine().getWorld())));
}
if (getEngine().getWorld().hasPlatformWorld()) {
if (chunkUpdater.flip()) {
updateChunks();
}
if (!playersPresent) {
return 5000L;
}
if (chunkDiscovery.flip()) {
discoverChunks();
}
if (cln.flip()) {
getEngine().getEngineData().cleanup(getEngine());
}
if (!IrisSettings.get().getWorld().isMarkerEntitySpawningSystem()
&& !IrisSettings.get().getWorld().isAmbientEntitySpawningSystem()) {
return 3000L;
}
onAsyncTick();
}
return IrisSettings.get().getWorld().getAsyncTickIntervalMS();
}
@Override
public void start() {
super.start();
if (!looper.isAlive()) {
looper.start();
}
}
private Runnable managedTask(String operation, Runnable task) {
return () -> runManagerTask(operation, task);
}
private Runnable managedTask(String operation, Runnable task, Runnable unavailable) {
return () -> {
if (!runManagerTask(operation, task)) {
unavailable.run();
}
};
}
private void discoverChunks() {
World world = BukkitWorldBinding.world(getEngine().getWorld());
if (world == null) {
@@ -206,7 +240,7 @@ public class IrisWorldManager extends EngineAssignedWorldManager {
return;
}
boolean scheduled = J.runGlobal(() -> {
boolean scheduled = J.runGlobal(managedTask("bukkit_world_manager_discover_chunks", () -> {
try {
if (getEngine().isClosed() || !world.equals(BukkitWorldBinding.world(getEngine().getWorld()))) {
return;
@@ -217,7 +251,7 @@ public class IrisWorldManager extends EngineAssignedWorldManager {
continue;
}
J.runEntity(player, () -> {
J.runEntity(player, managedTask("bukkit_world_manager_discover_player", () -> {
if (!player.isOnline() || !world.equals(player.getWorld())) {
return;
}
@@ -232,14 +266,14 @@ public class IrisWorldManager extends EngineAssignedWorldManager {
raiseDiscoveredChunkFlag(world, chunkX, chunkZ);
}
}
});
}));
}
} catch (Throwable e) {
IrisLogging.reportError(e);
} finally {
chunkDiscoveryScanScheduled.set(false);
}
});
}, () -> chunkDiscoveryScanScheduled.set(false)));
if (!scheduled) {
chunkDiscoveryScanScheduled.set(false);
}
@@ -260,7 +294,7 @@ public class IrisWorldManager extends EngineAssignedWorldManager {
return;
}
J.a(() -> {
J.a(managedTask("bukkit_world_manager_discovered_flag", () -> {
try {
Mantle<Matter> mantle = getMantle();
if (!mantle.hasFlag(chunkX, chunkZ, MantleFlag.DISCOVERED)) {
@@ -271,7 +305,7 @@ public class IrisWorldManager extends EngineAssignedWorldManager {
} finally {
discoveredFlagQueue.remove(key);
}
});
}, () -> discoveredFlagQueue.remove(key)));
}
private void updateChunks() {
@@ -288,7 +322,10 @@ public class IrisWorldManager extends EngineAssignedWorldManager {
return;
}
boolean scheduled = J.runGlobal(() -> updateChunksOnGlobal(world));
boolean scheduled = J.runGlobal(managedTask(
"bukkit_world_manager_update_chunks",
() -> updateChunksOnGlobal(world),
() -> chunkUpdateScanScheduled.set(false)));
if (!scheduled) {
chunkUpdateScanScheduled.set(false);
}
@@ -308,7 +345,9 @@ public class IrisWorldManager extends EngineAssignedWorldManager {
continue;
}
J.runEntity(player, () -> schedulePlayerChunkUpdates(world, player));
J.runEntity(player, managedTask(
"bukkit_world_manager_player_chunk_updates",
() -> schedulePlayerChunkUpdates(world, player)));
}
scheduleForcedChunkUpdates(world);
@@ -363,13 +402,13 @@ public class IrisWorldManager extends EngineAssignedWorldManager {
}
try {
boolean scheduled = J.runRegion(world, chunkX, chunkZ, () -> {
boolean scheduled = J.runRegion(world, chunkX, chunkZ, managedTask("bukkit_world_manager_chunk_update", () -> {
try {
updateChunkRegion(world, chunkX, chunkZ);
} finally {
chunkUpdateQueue.remove(key);
}
});
}, () -> chunkUpdateQueue.remove(key)));
if (!scheduled) {
chunkUpdateQueue.remove(key);
}
@@ -409,12 +448,12 @@ public class IrisWorldManager extends EngineAssignedWorldManager {
raiseInitialSpawnMarkerFlag(world, chunkX, chunkZ, () -> {
int delay = RNG.r.i(5, 200);
J.runRegion(world, chunkX, chunkZ, () -> {
J.runRegion(world, chunkX, chunkZ, managedTask("bukkit_world_manager_initial_spawn_followup", () -> {
if (!world.isChunkLoaded(chunkX, chunkZ)) {
return;
}
spawnIn(world.getChunkAt(chunkX, chunkZ), true);
}, delay);
}), delay);
Chunk markerChunk = world.getChunkAt(chunkX, chunkZ);
forEachMarkerSpawner(markerChunk, (block, spawners) -> {
@@ -442,7 +481,7 @@ public class IrisWorldManager extends EngineAssignedWorldManager {
return;
}
J.a(() -> {
J.a(managedTask("bukkit_world_manager_spawn_marker_flag", () -> {
boolean raised = false;
try {
Mantle<Matter> mantle = getMantle();
@@ -460,13 +499,13 @@ public class IrisWorldManager extends EngineAssignedWorldManager {
return;
}
J.runRegion(world, chunkX, chunkZ, () -> {
J.runRegion(world, chunkX, chunkZ, managedTask("bukkit_world_manager_spawn_marker_callback", () -> {
if (!world.isChunkLoaded(chunkX, chunkZ) || !Chunks.isSafe(world, chunkX, chunkZ)) {
return;
}
onFirstRaise.run();
});
});
}));
}, () -> markerFlagQueue.remove(key)));
}
private void warmupMantleChunkAsync(int chunkX, int chunkZ) {
@@ -475,7 +514,7 @@ public class IrisWorldManager extends EngineAssignedWorldManager {
return;
}
J.a(() -> {
J.a(managedTask("bukkit_world_manager_mantle_warmup", () -> {
try {
getMantle().getChunk(chunkX, chunkZ);
} catch (Throwable e) {
@@ -483,11 +522,11 @@ public class IrisWorldManager extends EngineAssignedWorldManager {
} finally {
mantleWarmupQueue.remove(key);
}
});
}, () -> mantleWarmupQueue.remove(key)));
}
private boolean onAsyncTick() {
if (getEngine().isClosed()) {
if (getEngine().isClosing() || getEngine().isClosed()) {
return false;
}
@@ -496,7 +535,7 @@ public class IrisWorldManager extends EngineAssignedWorldManager {
return false;
}
actuallySpawned = 0;
actuallySpawned.set(0);
if (!getEngine().getWorld().hasPlatformWorld()) {
IrisLogging.debug("Can't spawn. No real world");
@@ -509,8 +548,16 @@ public class IrisWorldManager extends EngineAssignedWorldManager {
World realWorld = BukkitWorldBinding.world(getEngine().getWorld());
if (realWorld == null) {
entityCount = 0;
entityCountValid = false;
} else if (J.isFolia()) {
entityCount = getFoliaLivingEntityCount(realWorld);
Integer count = getFoliaLivingEntityCount(realWorld);
if (count != null) {
entityCount = count;
entityCountValid = true;
resetEntityCountFailures();
} else {
entityCountValid = false;
}
} else {
CompletableFuture<Integer> future = new CompletableFuture<>();
boolean scheduled = J.runGlobal(() -> {
@@ -526,14 +573,32 @@ public class IrisWorldManager extends EngineAssignedWorldManager {
future.completeExceptionally(ex);
}
});
entityCount = scheduled ? future.get(2, TimeUnit.SECONDS) : 0;
if (scheduled) {
entityCount = future.get(2, TimeUnit.SECONDS);
entityCountValid = true;
resetEntityCountFailures();
} else {
reportEntityCountFailure("Unable to schedule the global entity count; pausing Iris entity spawning until a complete count is available.", null);
}
}
} catch (InterruptedException e) {
entityCountValid = false;
Thread.currentThread().interrupt();
return false;
} catch (TimeoutException e) {
reportEntityCountFailure("Timed out while counting entities; pausing Iris entity spawning until a complete count is available.", null);
} catch (ExecutionException e) {
Throwable cause = e.getCause() == null ? e : e.getCause();
reportEntityCountFailure("Failed to count entities; pausing Iris entity spawning until a complete count is available.", cause);
} catch (Throwable e) {
IrisLogging.reportError(e);
close();
reportEntityCountFailure("Failed to count entities; pausing Iris entity spawning until a complete count is available.", e);
}
}
if (!entityCountValid) {
return false;
}
double epx = getEntitySaturation();
if (epx > IrisSettings.get().getWorld().getTargetSpawnEntitiesPerChunk()) {
IrisLogging.debug("Can't spawn. The entity per chunk ratio is at " + Form.pc(epx, 2) + " > 100% (total entities " + entityCount + ")");
@@ -549,16 +614,22 @@ public class IrisWorldManager extends EngineAssignedWorldManager {
Position2[] cc = getLoadedChunkPositionsSnapshot(world);
while (spawnBuffer-- > 0) {
if (getEngine().isClosing() || getEngine().isClosed()) {
return actuallySpawned.get() > 0;
}
if (cc.length == 0) {
IrisLogging.debug("Can't spawn. No chunks!");
return false;
}
Position2 c = cc[RNG.r.nextInt(cc.length)];
spawnChunkSafely(world, c.getX(), c.getZ(), false);
if (!spawnChunkSafely(world, c.getX(), c.getZ(), false)) {
return actuallySpawned.get() > 0;
}
}
return actuallySpawned > 0;
return actuallySpawned.get() > 0;
}
private boolean isPregenActiveForThisWorld() {
@@ -604,13 +675,16 @@ public class IrisWorldManager extends EngineAssignedWorldManager {
try {
return future.get(2, TimeUnit.SECONDS);
} catch (Throwable e) {
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return new Position2[0];
} catch (ExecutionException | TimeoutException e) {
IrisLogging.reportError(e);
return new Position2[0];
}
}
private int getFoliaLivingEntityCount(World world) {
private Integer getFoliaLivingEntityCount(World world) {
CompletableFuture<List<Player>> playerFuture = new CompletableFuture<>();
boolean scheduled = J.runGlobal(() -> {
try {
@@ -620,18 +694,29 @@ public class IrisWorldManager extends EngineAssignedWorldManager {
}
});
if (!scheduled) {
return 0;
reportEntityCountFailure("Unable to schedule the Folia player snapshot; pausing Iris entity spawning until a complete count is available.", null);
return null;
}
List<Player> players;
try {
players = playerFuture.get(2, TimeUnit.SECONDS);
} catch (Throwable e) {
IrisLogging.reportError(e);
return 0;
} catch (InterruptedException e) {
entityCountValid = false;
Thread.currentThread().interrupt();
return null;
} catch (TimeoutException e) {
reportEntityCountFailure("Timed out while reading the Folia player snapshot; pausing Iris entity spawning until a complete count is available.", null);
return null;
} catch (ExecutionException e) {
Throwable cause = e.getCause() == null ? e : e.getCause();
reportEntityCountFailure("Failed to read the Folia player snapshot; pausing Iris entity spawning until a complete count is available.", cause);
return null;
}
Map<String, Entity> candidates = new ConcurrentHashMap<>();
AtomicBoolean incomplete = new AtomicBoolean();
AtomicReference<Throwable> failure = new AtomicReference<>();
CountDownLatch latch = new CountDownLatch(players.size());
for (Player player : players) {
@@ -651,21 +736,28 @@ public class IrisWorldManager extends EngineAssignedWorldManager {
candidates.put(nearby.getUniqueId().toString(), nearby);
}
}
} catch (Throwable e) {
incomplete.set(true);
failure.compareAndSet(null, e);
} finally {
latch.countDown();
}
})) {
incomplete.set(true);
latch.countDown();
}
}
try {
latch.await(2, TimeUnit.SECONDS);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
if (!awaitEntityTasks(latch, 2, TimeUnit.SECONDS) || incomplete.get()) {
if (!Thread.currentThread().isInterrupted()) {
reportEntityCountFailure("The Folia entity candidate scan was incomplete; pausing Iris entity spawning until a complete count is available.", failure.get());
}
return null;
}
AtomicInteger count = new AtomicInteger();
incomplete.set(false);
failure.set(null);
CountDownLatch entityLatch = new CountDownLatch(candidates.size());
for (Entity entity : candidates.values()) {
if (!J.runEntity(entity, () -> {
@@ -673,47 +765,117 @@ public class IrisWorldManager extends EngineAssignedWorldManager {
if (entity instanceof LivingEntity && world.equals(entity.getWorld()) && !entity.isDead()) {
count.incrementAndGet();
}
} catch (Throwable e) {
incomplete.set(true);
failure.compareAndSet(null, e);
} finally {
entityLatch.countDown();
}
})) {
incomplete.set(true);
entityLatch.countDown();
}
}
try {
entityLatch.await(2, TimeUnit.SECONDS);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
if (!awaitEntityTasks(entityLatch, 2, TimeUnit.SECONDS) || incomplete.get()) {
if (!Thread.currentThread().isInterrupted()) {
reportEntityCountFailure("The Folia entity validation scan was incomplete; pausing Iris entity spawning until a complete count is available.", failure.get());
}
return null;
}
return count.get();
}
private void spawnChunkSafely(World world, int chunkX, int chunkZ, boolean initial) {
static boolean awaitEntityTasks(CountDownLatch latch, long timeout, TimeUnit unit) {
try {
return latch.await(timeout, unit);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return false;
}
}
private boolean spawnChunkSafely(World world, int chunkX, int chunkZ, boolean initial) {
if (world == null) {
return;
return false;
}
CompletableFuture<Void> future = new CompletableFuture<>();
J.runRegion(world, chunkX, chunkZ, () -> {
try {
if (!world.isChunkLoaded(chunkX, chunkZ) || !Chunks.isSafe(world, chunkX, chunkZ)) {
return;
}
spawnIn(world.getChunkAt(chunkX, chunkZ), initial);
} finally {
future.complete(null);
AtomicBoolean failureReported = new AtomicBoolean();
future.whenComplete((ignored, failure) -> {
if (failure != null) {
reportSpawnFailure(chunkX, chunkZ, failure, failureReported);
}
});
boolean scheduled;
try {
scheduled = J.runRegion(world, chunkX, chunkZ, () -> {
try {
if (!world.isChunkLoaded(chunkX, chunkZ) || !Chunks.isSafe(world, chunkX, chunkZ)) {
future.complete(null);
return;
}
spawnIn(world.getChunkAt(chunkX, chunkZ), initial);
future.complete(null);
} catch (Throwable e) {
future.completeExceptionally(e);
}
});
} catch (Throwable e) {
IrisLogging.reportError("Failed to schedule an Iris entity spawn for chunk " + chunkX + "," + chunkZ + ".", e);
return false;
}
if (!scheduled) {
IrisLogging.debug("Skipped Iris entity spawning because the region task was not accepted for chunk " + chunkX + "," + chunkZ + ".");
return false;
}
try {
future.get(5, TimeUnit.SECONDS);
} catch (Throwable e) {
IrisLogging.reportError(e);
return true;
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return false;
} catch (TimeoutException e) {
IrisLogging.warn("Timed out waiting for Iris entity spawning in chunk %d,%d; deferring the remaining spawn buffer.", chunkX, chunkZ);
return false;
} catch (ExecutionException e) {
Throwable cause = e.getCause() == null ? e : e.getCause();
reportSpawnFailure(chunkX, chunkZ, cause, failureReported);
return false;
}
}
private void reportEntityCountFailure(String message, Throwable error) {
entityCountValid = false;
if (error != null) {
if (entityCountErrorReported.compareAndSet(false, true)) {
IrisLogging.reportError(message, error);
}
return;
}
if (entityCountWarningReported.compareAndSet(false, true)) {
IrisLogging.warn(message);
}
}
private void resetEntityCountFailures() {
entityCountWarningReported.set(false);
entityCountErrorReported.set(false);
}
private void reportSpawnFailure(int chunkX, int chunkZ, Throwable failure, AtomicBoolean failureReported) {
if (!failureReported.compareAndSet(false, true)) {
return;
}
Throwable cause = failure.getCause() == null ? failure : failure.getCause();
IrisLogging.reportError("Failed to spawn Iris entities in chunk " + chunkX + "," + chunkZ + ".", cause);
}
private void spawnIn(Chunk c, boolean initial) {
if (getEngine().isClosed()) {
return;
@@ -736,8 +898,10 @@ public class IrisWorldManager extends EngineAssignedWorldManager {
}
spawn(block, s, false);
J.runRegion(c.getWorld(), c.getX(), c.getZ(), () -> raiseInitialSpawnMarkerFlag(c.getWorld(), c.getX(), c.getZ(),
() -> spawn(block, s, true)));
J.runRegion(c.getWorld(), c.getX(), c.getZ(), managedTask(
"bukkit_world_manager_marker_spawn_followup",
() -> raiseInitialSpawnMarkerFlag(c.getWorld(), c.getX(), c.getZ(),
() -> spawn(block, s, true))));
});
}
@@ -775,17 +939,13 @@ public class IrisWorldManager extends EngineAssignedWorldManager {
if (v == null || v.getReferenceSpawner() == null)
return;
try {
spawn(c, v);
} catch (Throwable e) {
J.runRegion(c.getWorld(), c.getX(), c.getZ(), () -> spawn(c, v));
}
spawn(c, v);
}
private void spawn(Chunk c, IrisEntitySpawn i) {
IrisSpawner ref = i.getReferenceSpawner();
int s = i.spawn(getEngine(), c, RNG.r);
actuallySpawned += s;
actuallySpawned.addAndGet(s);
if (s > 0) {
ref.spawn(getEngine(), c.getX(), c.getZ());
}
@@ -797,7 +957,7 @@ public class IrisWorldManager extends EngineAssignedWorldManager {
return;
int s = i.spawn(getEngine(), pos, RNG.r);
actuallySpawned += s;
actuallySpawned.addAndGet(s);
if (s > 0) {
ref.spawn(getEngine(), PowerOfTwoCoordinates.blockToChunkFloor(pos.getX()), PowerOfTwoCoordinates.blockToChunkFloor(pos.getZ()));
}
@@ -857,10 +1017,10 @@ public class IrisWorldManager extends EngineAssignedWorldManager {
int cX = e.getX(), cZ = e.getZ();
Long key = Cache.key(cX, cZ);
cleanup.put(key, cleanupService.schedule(() -> {
cleanup.put(key, cleanupService.schedule(managedTask("bukkit_world_manager_chunk_cleanup", () -> {
cleanup.remove(key);
getEngine().cleanupMantleChunk(cX, cZ);
}, Math.max(IrisSettings.get().getPerformance().mantleCleanupDelay * 50L, 0), TimeUnit.MILLISECONDS));
}, () -> cleanup.remove(key)), Math.max(IrisSettings.get().getPerformance().mantleCleanupDelay * 50L, 0), TimeUnit.MILLISECONDS));
}
@Override
@@ -898,15 +1058,17 @@ public class IrisWorldManager extends EngineAssignedWorldManager {
@Override
public void teleportAsync(PlayerTeleportEvent e) {
e.setCancelled(true);
warmupAreaAsync(e.getPlayer(), e.getTo(), () -> J.runEntity(e.getPlayer(), () -> {
warmupAreaAsync(e.getPlayer(), e.getTo(), () -> J.runEntity(e.getPlayer(), managedTask(
"bukkit_world_manager_teleport",
() -> {
ignoreTP.set(true);
e.getPlayer().teleport(e.getTo(), e.getCause());
ignoreTP.set(false);
}));
})));
}
private void warmupAreaAsync(Player player, Location to, Runnable r) {
J.a(() -> {
J.a(managedTask("bukkit_world_manager_teleport_warmup", () -> {
int viewDistance = 2;
KList<Future<Chunk>> futures = new KList<>();
for (int i = -viewDistance; i <= viewDistance; i++) {
@@ -945,7 +1107,7 @@ public class IrisWorldManager extends EngineAssignedWorldManager {
return "Loading Chunks";
}
}.queue(futures).execute(new VolmitSender(player), true, r);
});
}));
}
public Map<IrisPosition, KSet<IrisSpawner>> getSpawnersFromMarkers(Chunk c) {
@@ -1024,14 +1186,14 @@ public class IrisWorldManager extends EngineAssignedWorldManager {
return;
}
J.a(() -> {
J.a(managedTask("bukkit_world_manager_marker_scan", () -> {
try {
Map<IrisPosition, MarkerSpawnData> markerData = collectMarkerSpawnData(chunkX, chunkZ);
if (markerData.isEmpty()) {
return;
}
J.runRegion(world, chunkX, chunkZ, () -> {
J.runRegion(world, chunkX, chunkZ, managedTask("bukkit_world_manager_marker_scan_region", () -> {
if (!world.isChunkLoaded(chunkX, chunkZ) || !Chunks.isSafe(world, chunkX, chunkZ)) {
return;
}
@@ -1050,13 +1212,13 @@ public class IrisWorldManager extends EngineAssignedWorldManager {
consumer.accept(new IrisPosition(relative.getX(), relative.getY() + minY, relative.getZ()), data.spawners);
});
});
}));
} catch (Throwable e) {
IrisLogging.reportError(e);
} finally {
markerScanQueue.remove(key);
}
});
}, () -> markerScanQueue.remove(key)));
}
private Map<IrisPosition, MarkerSpawnData> collectMarkerSpawnData(int chunkX, int chunkZ) {
@@ -1107,13 +1269,13 @@ public class IrisWorldManager extends EngineAssignedWorldManager {
}
private void removeMarkerAsync(IrisPosition marker) {
J.a(() -> {
J.a(managedTask("bukkit_world_manager_remove_marker", () -> {
try {
getMantle().remove(marker.getX(), marker.getY(), marker.getZ(), MatterMarker.class);
} catch (Throwable e) {
IrisLogging.reportError(e);
}
});
}));
}
private static final class MarkerSpawnData {
@@ -1127,21 +1289,6 @@ public class IrisWorldManager extends EngineAssignedWorldManager {
int blockX = e.getBlock().getX();
int mantleY = toMantleY(e.getBlock().getY(), getEngine().getWorld().minHeight());
int blockZ = e.getBlock().getZ();
J.a(() -> {
MatterMarker marker = getMantle().get(blockX, mantleY, blockZ, MatterMarker.class);
if (marker != null) {
if (marker.getTag().equals("cave_floor") || marker.getTag().equals("cave_ceiling")) {
return;
}
IrisMarker mark = getData().getMarkerLoader().load(marker.getTag());
if (mark == null || mark.isRemoveOnChange()) {
getMantle().remove(blockX, mantleY, blockZ, MatterMarker.class);
}
}
});
KList<ItemStack> d = new KList<>();
IrisBiome b = EngineBukkitOps.getBiome(getEngine(), e.getBlock().getLocation());
@@ -1159,15 +1306,29 @@ public class IrisWorldManager extends EngineAssignedWorldManager {
e.setDropItems(false);
}
if (d.isNotEmpty()) {
World w = e.getBlock().getWorld();
Location dropLocation = e.getBlock().getLocation().clone().add(.5, .5, .5);
Runnable dropTask = () -> d.forEach(item -> w.dropItemNaturally(dropLocation, item));
if (!J.runAt(dropLocation, dropTask)) {
if (!J.isFolia()) {
J.s(dropTask);
}
World w = e.getBlock().getWorld();
Location blockLocation = e.getBlock().getLocation();
Location dropLocation = blockLocation.clone().add(.5, .5, .5);
BlockDropRouter dropRouter = e instanceof BlockDropRouter router ? router : null;
Runnable finalizedBreak = managedTask("bukkit_world_manager_block_break_finalize", () -> {
if (e.isCancelled()) {
return;
}
J.a(managedTask("bukkit_world_manager_block_break_marker", () -> {
MatterMarker marker = getMantle().get(blockX, mantleY, blockZ, MatterMarker.class);
if (marker == null || marker.getTag().equals("cave_floor") || marker.getTag().equals("cave_ceiling")) {
return;
}
IrisMarker mark = getData().getMarkerLoader().load(marker.getTag());
if (mark == null || mark.isRemoveOnChange()) {
getMantle().remove(blockX, mantleY, blockZ, MatterMarker.class);
}
}));
routeDrops(d, dropRouter, item -> w.dropItemNaturally(dropLocation, item));
});
if (!J.runAt(blockLocation, finalizedBreak, 1) && !J.isFolia()) {
J.s(finalizedBreak, 1);
}
}
}
@@ -1176,6 +1337,22 @@ public class IrisWorldManager extends EngineAssignedWorldManager {
return worldY - minHeight;
}
static <T> void routeDrops(Iterable<T> drops, BlockDropRouter router, Consumer<T> fallback) {
for (T drop : drops) {
boolean routed = false;
if (router != null) {
try {
routed = router.routeDrop(drop);
} catch (Throwable error) {
IrisLogging.reportError("Failed to route a deferred Iris block drop.", error);
}
}
if (!routed) {
fallback.accept(drop);
}
}
}
static int toWorldY(int mantleY, int minHeight) {
return mantleY + minHeight;
}
@@ -1190,11 +1367,35 @@ public class IrisWorldManager extends EngineAssignedWorldManager {
}
@Override
public void close() {
super.close();
looper.interrupt();
if (cleanupService != null) {
cleanupService.shutdownNow();
public synchronized void close() {
Throwable failure = null;
try {
super.close();
} catch (Throwable e) {
failure = e;
}
if (!looperStopped) {
try {
if (looper != null) {
looper.interrupt();
}
looperStopped = true;
} catch (Throwable e) {
failure = appendCloseFailure(failure, e);
}
}
if (!cleanupServiceStopped) {
try {
if (cleanupService != null) {
cleanupService.shutdownNow();
}
cleanupServiceStopped = true;
} catch (Throwable e) {
failure = appendCloseFailure(failure, e);
}
}
if (failure != null) {
throw new IllegalStateException("Failed to completely stop the Bukkit Iris world manager.", failure);
}
}
@@ -1212,6 +1413,16 @@ public class IrisWorldManager extends EngineAssignedWorldManager {
return (double) entityCount / (loadedChunkCount + 1) * 1.28;
}
private static Throwable appendCloseFailure(Throwable failure, Throwable next) {
if (failure == null) {
return next;
}
if (failure != next) {
failure.addSuppressed(next);
}
return failure;
}
@Data
private static class ChunkCounter implements Predicate<IrisSpawner> {
private final Entity[] entities;
@@ -69,7 +69,7 @@ public class UpperDimensionContext implements DataProvider {
engine.getData(),
chunkHeight,
complex.getHeightStream(),
complex.getBaseBiomeStream(),
complex.getTrueBiomeStream(),
complex.getRegionStream(),
complex.getRockStream(),
true
@@ -89,6 +89,8 @@ public class UpperDimensionContext implements DataProvider {
Map<IrisInterpolator, Set<IrisGenerator>> generators = new HashMap<>();
Set<IrisBiome> allBiomes = Collections.newSetFromMap(new IdentityHashMap<>());
Map<IrisBiome, IrisComplex.ChildSelectionPlan> childSelectionPlans =
Collections.synchronizedMap(new IdentityHashMap<>());
upperDim.getRegions().forEach(regionKey -> {
IrisRegion region = upperData.getRegionLoader().load(regionKey);
if (region != null) {
@@ -135,8 +137,7 @@ public class UpperDimensionContext implements DataProvider {
.zoom(upperDim.getBiomeZoom())
.zoom(upperDim.getLandZoom())
.zoom(r.getLandBiomeZoom())
.selectRarity(upperData.getBiomeLoader().loadAll(r.getLandBiomes(),
t -> t.setInferredType(InferredType.LAND))))
.selectRarity(loadInferredBiomes(upperData, r.getLandBiomes(), InferredType.LAND)))
.convertAware2D(ProceduralStream::get);
ProceduralStream<IrisBiome> seaBiomeStream = regionStream
.convert(r -> upperDim.getSeaBiomeStyle()
@@ -144,16 +145,14 @@ public class UpperDimensionContext implements DataProvider {
.zoom(upperDim.getBiomeZoom())
.zoom(upperDim.getSeaZoom())
.zoom(r.getSeaBiomeZoom())
.selectRarity(upperData.getBiomeLoader().loadAll(r.getSeaBiomes(),
t -> t.setInferredType(InferredType.SEA))))
.selectRarity(loadInferredBiomes(upperData, r.getSeaBiomes(), InferredType.SEA)))
.convertAware2D(ProceduralStream::get);
ProceduralStream<IrisBiome> shoreBiomeStream = regionStream
.convert(r -> upperDim.getShoreBiomeStyle()
.create(rng.nextParallelRNG(InferredType.SHORE.ordinal()), upperData).stream()
.zoom(upperDim.getBiomeZoom())
.zoom(r.getShoreBiomeZoom())
.selectRarity(upperData.getBiomeLoader().loadAll(r.getShoreBiomes(),
t -> t.setInferredType(InferredType.SHORE))))
.selectRarity(loadInferredBiomes(upperData, r.getShoreBiomes(), InferredType.SHORE)))
.convertAware2D(ProceduralStream::get);
Map<InferredType, ProceduralStream<IrisBiome>> inferredStreams = new HashMap<>();
@@ -170,7 +169,9 @@ public class UpperDimensionContext implements DataProvider {
.convertAware2D((t, x, z) -> {
ProceduralStream<IrisBiome> stream = inferredStreams.get(t);
return stream != null ? stream.get(x, z) : inferredStreams.get(InferredType.LAND).get(x, z);
});
})
.convertAware2D((biome, x, z) -> implode(
biome, x, z, rng, dataProvider, childSelectionPlans, 3));
KList<IrisShapedGeneratorStyle> overlayNoise = upperDim.getOverlayNoise();
ProceduralStream<Double> overlayStream = overlayNoise.isEmpty()
@@ -235,6 +236,23 @@ public class UpperDimensionContext implements DataProvider {
return Math.max(Math.min(interpolatedHeight + fluidHeight + overlayStream.get(x, z), chunkHeight), 0);
}, Interpolated.DOUBLE);
ProceduralStream<IrisBiome> finalBiomeStream = heightStream.convertAware2D((height, x, z) -> {
IrisBiome baseBiome = baseBiomeStream.get(x, z);
IrisBiome resolved = IrisComplex.resolveSurfaceBiome(
height,
baseBiome,
regionStream.get(x, z),
x,
z,
fluidHeight,
landBiomeStream,
seaBiomeStream,
shoreBiomeStream);
return resolved == baseBiome
? baseBiome
: implode(resolved, x, z, rng, dataProvider, childSelectionPlans, 3);
});
ProceduralStream<PlatformBlockState> rockStream = upperDim.getRockPalette()
.getLayerGenerator(rng.nextParallelRNG(45), upperData).stream()
.select(upperDim.getRockPalette().getBlockData(upperData));
@@ -244,13 +262,67 @@ public class UpperDimensionContext implements DataProvider {
upperData,
chunkHeight,
heightStream,
baseBiomeStream,
finalBiomeStream,
regionStream,
rockStream,
false
);
}
private static KList<IrisBiome> loadInferredBiomes(IrisData data, KList<String> keys, InferredType type) {
KList<IrisBiome> inferred = new KList<>();
for (IrisBiome biome : data.getBiomeLoader().loadAll(keys)) {
inferred.add(biome.withInferredType(type));
}
return inferred;
}
private static IrisBiome implode(
IrisBiome biome,
double x,
double z,
RNG rng,
DataProvider dataProvider,
Map<IrisBiome, IrisComplex.ChildSelectionPlan> childSelectionPlans,
int remainingDepth
) {
if (biome == null || remainingDepth < 0 || biome.getChildren().isEmpty()) {
return biome;
}
IrisComplex.ChildSelectionPlan selectionPlan = childSelectionPlans.get(biome);
if (selectionPlan == null) {
synchronized (childSelectionPlans) {
selectionPlan = childSelectionPlans.get(biome);
if (selectionPlan == null) {
KList<IrisBiome> options = new KList<>();
for (IrisBiome child : biome.getRealChildren(dataProvider)) {
if (child != null) {
options.add(child);
}
}
options.add(biome);
selectionPlan = IrisComplex.ChildSelectionPlan.create(options);
childSelectionPlans.put(biome, selectionPlan);
}
}
}
IrisBiome selected = selectionPlan.select(
biome.getChildrenGenerator(rng, 123, biome.getChildShrinkFactor()), x, z);
if (selected == null) {
return biome;
}
return implode(
selected.withInferredType(biome.getInferredType()),
x,
z,
rng,
dataProvider,
childSelectionPlans,
remainingDepth - 1);
}
public int getUpperSurfaceY(int x, int z) {
double rawHeight = heightStream.get((double) x, (double) z);
return chunkHeight - 1 - (int) Math.round(rawHeight);
@@ -48,6 +48,7 @@ import art.arcane.iris.spi.PlatformBlockState;
import art.arcane.volmlib.util.collection.KList;
import art.arcane.volmlib.util.collection.KMap;
import art.arcane.iris.util.project.context.ChunkContext;
import art.arcane.iris.util.project.context.IrisContext;
import art.arcane.iris.util.common.data.DataProvider;
import art.arcane.iris.util.common.data.B;
import art.arcane.volmlib.util.documentation.BlockCoordinates;
@@ -127,6 +128,15 @@ public interface Engine extends DataProvider, Fallible, BlockUpdater, Renderer,
return GenerationSessionLease.noop();
}
IrisContext context = IrisContext.get();
if (context != null && context.getEngine() == this && context.getGenerationSessionId() != 0L) {
return generationSessions.continueSession(operation, context.getGenerationSessionId());
}
if (isClosing() || isClosed()) {
throw new GenerationSessionException("Generation session rejected new work for " + operation
+ " while the Iris engine is closing.", isClosed());
}
return generationSessions.acquire(operation);
}
@@ -33,82 +33,194 @@ import org.bukkit.event.block.BlockPlaceEvent;
import org.bukkit.event.world.ChunkLoadEvent;
import org.bukkit.event.world.ChunkUnloadEvent;
import org.bukkit.event.world.WorldSaveEvent;
import org.bukkit.event.world.WorldUnloadEvent;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.function.Supplier;
public abstract class EngineAssignedWorldManager extends EngineAssignedComponent implements EngineWorldManager, BukkitEngineWorldManager, Listener {
private final int taskId;
private final Object managerLifecycleLock;
private final AtomicBoolean started;
private boolean listenerRegistered;
private boolean closeRequested;
private int taskId;
protected AtomicBoolean ignoreTP = new AtomicBoolean(false);
public EngineAssignedWorldManager() {
super(null, null);
managerLifecycleLock = new Object();
started = new AtomicBoolean(false);
taskId = -1;
}
public EngineAssignedWorldManager(Engine engine) {
super(engine, "World");
BukkitPlatform.volmitPlugin().registerListener(this);
taskId = J.sr(this::onTick, 1);
managerLifecycleLock = new Object();
started = new AtomicBoolean(false);
taskId = -1;
}
@Override
public void start() {
synchronized (managerLifecycleLock) {
if (started.get()) {
return;
}
if (closeRequested) {
throw new IllegalStateException("Cannot restart a closed Iris world manager.");
}
started.set(true);
try {
listenerRegistered = true;
registerManagerListener();
taskId = scheduleManagerTick(() -> runManagerTask("bukkit_world_manager_tick", this::onTick));
} catch (Throwable e) {
started.set(false);
closeRequested = true;
Throwable cleanupFailure = closeManagerResources();
if (cleanupFailure != null) {
e.addSuppressed(cleanupFailure);
}
throw propagate(e);
}
}
}
@EventHandler
public void on(IrisEngineHotloadEvent e) {
for (Player i : BukkitWorldBinding.players(e.getEngine().getWorld())) {
i.playSound(i.getLocation(), Sound.BLOCK_AMETHYST_BLOCK_BREAK, 1f, 1.8f);
VolmitSender s = new VolmitSender(i);
s.sendTitle(C.IRIS + "Engine " + C.AQUA + "<font:minecraft:uniform>Hotloaded", 70, 60, 410);
}
runManagerTask("bukkit_world_manager_hotload_event", () -> {
for (Player i : BukkitWorldBinding.players(e.getEngine().getWorld())) {
i.playSound(i.getLocation(), Sound.BLOCK_AMETHYST_BLOCK_BREAK, 1f, 1.8f);
VolmitSender s = new VolmitSender(i);
s.sendTitle(C.IRIS + "Engine " + C.AQUA + "<font:minecraft:uniform>Hotloaded", 70, 60, 410);
}
});
}
@EventHandler
public void on(WorldSaveEvent e) {
if (e.getWorld().equals(BukkitWorldBinding.world(getTarget().getWorld()))) {
getEngine().save();
}
}
@EventHandler
public void on(WorldUnloadEvent e) {
if (e.getWorld().equals(BukkitWorldBinding.world(getTarget().getWorld()))) {
getEngine().close();
}
runManagerTask("bukkit_world_manager_save", () -> {
if (e.getWorld().equals(BukkitWorldBinding.world(getTarget().getWorld()))) {
getEngine().save();
}
});
}
@EventHandler
public void on(BlockBreakEvent e) {
if (e.getPlayer().getWorld().equals(BukkitWorldBinding.world(getTarget().getWorld()))) {
onBlockBreak(e);
}
runManagerTask("bukkit_world_manager_block_break", () -> {
if (e.getPlayer().getWorld().equals(BukkitWorldBinding.world(getTarget().getWorld()))) {
onBlockBreak(e);
}
});
}
@EventHandler
public void on(BlockPlaceEvent e) {
if (e.getPlayer().getWorld().equals(BukkitWorldBinding.world(getTarget().getWorld()))) {
onBlockPlace(e);
}
runManagerTask("bukkit_world_manager_block_place", () -> {
if (e.getPlayer().getWorld().equals(BukkitWorldBinding.world(getTarget().getWorld()))) {
onBlockPlace(e);
}
});
}
@EventHandler
public void on(ChunkLoadEvent e) {
if (e.getChunk().getWorld().equals(BukkitWorldBinding.world(getTarget().getWorld()))) {
onChunkLoad(e.getChunk(), e.isNewChunk());
}
runManagerTask("bukkit_world_manager_chunk_load", () -> {
if (e.getChunk().getWorld().equals(BukkitWorldBinding.world(getTarget().getWorld()))) {
onChunkLoad(e.getChunk(), e.isNewChunk());
}
});
}
@EventHandler
public void on(ChunkUnloadEvent e) {
if (e.getChunk().getWorld().equals(BukkitWorldBinding.world(getTarget().getWorld()))) {
onChunkUnload(e.getChunk());
}
runManagerTask("bukkit_world_manager_chunk_unload", () -> {
if (e.getChunk().getWorld().equals(BukkitWorldBinding.world(getTarget().getWorld()))) {
onChunkUnload(e.getChunk());
}
});
}
@Override
public void close() {
super.close();
BukkitPlatform.volmitPlugin().unregisterListener(this);
if (taskId != -1) {
J.csr(taskId);
Throwable failure;
synchronized (managerLifecycleLock) {
closeRequested = true;
started.set(false);
failure = closeManagerResources();
}
if (failure != null) {
throw new IllegalStateException("Failed to completely stop the Iris world manager.", failure);
}
}
protected void registerManagerListener() {
BukkitPlatform.volmitPlugin().registerListener(this);
}
protected int scheduleManagerTick(Runnable tick) {
return J.sr(tick, 1);
}
protected void unregisterManagerListener() {
BukkitPlatform.volmitPlugin().unregisterListener(this);
}
protected void cancelManagerTick(int scheduledTaskId) {
J.csr(scheduledTaskId);
}
private Throwable closeManagerResources() {
Throwable failure = null;
if (listenerRegistered) {
try {
unregisterManagerListener();
listenerRegistered = false;
} catch (Throwable e) {
failure = e;
}
}
if (taskId != -1) {
try {
cancelManagerTick(taskId);
taskId = -1;
} catch (Throwable e) {
if (failure == null) {
failure = e;
} else if (failure != e) {
failure.addSuppressed(e);
}
}
}
return failure;
}
private RuntimeException propagate(Throwable failure) {
if (failure instanceof RuntimeException runtimeException) {
return runtimeException;
}
if (failure instanceof Error error) {
throw error;
}
return new IllegalStateException(failure);
}
protected boolean runManagerTask(String operation, Runnable task) {
if (!started.get()) {
return false;
}
return EngineLifecycleTasks.run(getEngine(), operation, task);
}
protected <T> T callManagerTask(String operation, Supplier<T> task, T unavailable) {
if (!started.get()) {
return unavailable;
}
return EngineLifecycleTasks.call(getEngine(), operation, task, unavailable);
}
protected boolean isManagerStarted() {
return started.get();
}
}
@@ -0,0 +1,40 @@
package art.arcane.iris.engine.framework;
import art.arcane.iris.util.project.context.IrisContext;
import java.util.Objects;
import java.util.function.Supplier;
public final class EngineLifecycleTasks {
private EngineLifecycleTasks() {
}
public static boolean run(Engine engine, String operation, Runnable task) {
Objects.requireNonNull(engine);
Objects.requireNonNull(operation);
Objects.requireNonNull(task);
try (GenerationSessionLease lease = engine.acquireGenerationLease(operation);
IrisContext.Scope ignored = IrisContext.open(engine, lease.sessionId(), null)) {
task.run();
return true;
} catch (GenerationSessionException exception) {
if (engine.isClosing() || engine.isClosed() || exception.isExpectedTeardown()) {
return false;
}
throw new IllegalStateException("Iris lifecycle rejected " + operation + ".", exception);
}
}
public static <T> T call(Engine engine, String operation, Supplier<T> task, T unavailable) {
Objects.requireNonNull(task);
try (GenerationSessionLease lease = engine.acquireGenerationLease(operation);
IrisContext.Scope ignored = IrisContext.open(engine, lease.sessionId(), null)) {
return task.get();
} catch (GenerationSessionException exception) {
if (engine.isClosing() || engine.isClosed() || exception.isExpectedTeardown()) {
return unavailable;
}
throw new IllegalStateException("Iris lifecycle rejected " + operation + ".", exception);
}
}
}
@@ -22,6 +22,9 @@ import art.arcane.volmlib.util.atomics.AtomicRollingSequence;
import art.arcane.volmlib.util.collection.KMap;
import lombok.Data;
import java.util.LinkedHashMap;
import java.util.Map;
@Data
public class EngineMetrics {
private final AtomicRollingSequence total;
@@ -111,4 +114,24 @@ public class EngineMetrics {
return v;
}
public Map<String, Double> telemetryAverages() {
Map<String, Double> averages = new LinkedHashMap<>();
averages.put("total", total.getAverage());
averages.put("updates", updates.getAverage());
averages.put("terrain", terrain.getAverage());
averages.put("biome", biome.getAverage());
averages.put("post", post.getAverage());
averages.put("perfection", perfection.getAverage());
averages.put("decoration", decoration.getAverage());
averages.put("cave", cave.getAverage());
averages.put("deposit", deposit.getAverage());
averages.put("carve.resolve", carveResolve.getAverage());
averages.put("carve.apply", carveApply.getAverage());
averages.put("context.prefill", contextPrefill.getAverage());
averages.put("pregen.wait.permit", pregenWaitPermit.getAverage());
averages.put("pregen.wait.adaptive", pregenWaitAdaptive.getAverage());
averages.entrySet().removeIf(entry -> !Double.isFinite(entry.getValue()) || entry.getValue() < 0D);
return Map.copyOf(averages);
}
}
@@ -32,8 +32,8 @@ import lombok.ToString;
public class EngineTarget {
private final MultiBurst burster;
private final IrisData data;
private IrisDimension dimension;
private IrisWorld world;
private final IrisDimension dimension;
private final IrisWorld world;
public EngineTarget(IrisWorld world, IrisDimension dimension, IrisData data) {
this.world = world;
@@ -0,0 +1,222 @@
package art.arcane.iris.engine.framework;
import art.arcane.iris.engine.object.IrisEngineStatistics;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
public record EngineTelemetrySnapshot(
long sampledAtMs,
String worldIdentity,
String worldName,
String dimensionKey,
boolean active,
boolean studio,
boolean closing,
boolean failed,
long loadedChunks,
long loadedEntities,
double entitySaturation,
long generatedSession,
long generatedTotal,
double chunksPerSecond,
long blockUpdatesPerSecond,
int parallelism,
int activeGenerationLeases,
long hotloadsTotal,
long mantleResidentPlates,
long mantleQueuedPlates,
double mantleIdleMs,
Map<String, Double> generationTimingsMs
) {
public EngineTelemetrySnapshot {
if (worldIdentity == null || worldIdentity.isBlank()) {
throw new IllegalArgumentException("Engine telemetry requires a world identity");
}
worldIdentity = worldIdentity.trim();
worldName = worldName == null || worldName.isBlank() ? worldIdentity : worldName.trim();
dimensionKey = dimensionKey == null ? "" : dimensionKey.trim();
loadedChunks = Math.max(0L, loadedChunks);
loadedEntities = Math.max(0L, loadedEntities);
entitySaturation = finiteNonNegative(entitySaturation);
generatedSession = Math.max(0L, generatedSession);
generatedTotal = Math.max(0L, generatedTotal);
chunksPerSecond = finiteNonNegative(chunksPerSecond);
blockUpdatesPerSecond = Math.max(0L, blockUpdatesPerSecond);
parallelism = Math.max(0, parallelism);
activeGenerationLeases = Math.max(0, activeGenerationLeases);
hotloadsTotal = Math.max(0L, hotloadsTotal);
mantleResidentPlates = Math.max(0L, mantleResidentPlates);
mantleQueuedPlates = Math.max(0L, mantleQueuedPlates);
mantleIdleMs = finiteNonNegative(mantleIdleMs);
generationTimingsMs = sanitizeTimings(generationTimingsMs);
}
public static EngineTelemetrySnapshot capture(Engine engine, double chunksPerSecond, long sampledAtMs) {
if (engine == null) {
throw new IllegalArgumentException("Engine cannot be null");
}
EngineWorldManager worldManager = engine.getWorldManager();
GenerationSessionManager sessions = engine.getGenerationSessions();
IrisEngineStatistics statistics = engine.getEngineData().getStatistics();
boolean closing = engine.isClosing();
boolean closed = engine.isClosed();
boolean failed = engine.hasFailed();
return new EngineTelemetrySnapshot(
sampledAtMs,
engine.getWorld().identity(),
engine.getWorld().name(),
engine.getDimension().getLoadKey(),
!closing && !closed && !failed,
engine.isStudio(),
closing,
failed,
worldManager == null ? 0L : worldManager.getChunkCount(),
worldManager == null ? 0L : worldManager.getEntityCount(),
worldManager == null ? 0D : worldManager.getEntitySaturation(),
engine.getGenerated(),
statistics == null ? 0L : statistics.getChunksGenerated(),
chunksPerSecond,
engine.getBlockUpdatesPerSecond(),
engine.getParallelism(),
sessions == null ? 0 : sessions.activeLeases(),
statistics == null ? 0L : statistics.getTotalHotloads(),
engine.getMantle().getLoadedRegionCount(),
engine.getMantle().getUnloadRegionCount(),
engine.getMantle().getAdjustedIdleDuration(),
engine.getMetrics().telemetryAverages()
);
}
public static Aggregate aggregate(List<EngineTelemetrySnapshot> snapshots) {
List<EngineTelemetrySnapshot> safeSnapshots = snapshots == null ? List.of() : snapshots;
int active = 0;
int studio = 0;
int closing = 0;
int failed = 0;
long loadedChunks = 0L;
long loadedEntities = 0L;
double entitySaturation = 0D;
long generatedSession = 0L;
long generatedTotal = 0L;
double chunksPerSecond = 0D;
long blockUpdatesPerSecond = 0L;
int parallelism = 0;
int activeGenerationLeases = 0;
long hotloadsTotal = 0L;
long mantleResidentPlates = 0L;
long mantleQueuedPlates = 0L;
double mantleIdleTotal = 0D;
double mantleIdleMax = 0D;
double mantleIdleMin = Double.POSITIVE_INFINITY;
int mantleIdleCount = 0;
int worldCount = 0;
Map<String, Double> timingMaxima = new LinkedHashMap<>();
for (EngineTelemetrySnapshot snapshot : safeSnapshots) {
if (snapshot == null) {
continue;
}
worldCount++;
active += snapshot.active() ? 1 : 0;
studio += snapshot.studio() ? 1 : 0;
closing += snapshot.closing() ? 1 : 0;
failed += snapshot.failed() ? 1 : 0;
loadedChunks += snapshot.loadedChunks();
loadedEntities += snapshot.loadedEntities();
entitySaturation = Math.max(entitySaturation, snapshot.entitySaturation());
generatedSession += snapshot.generatedSession();
generatedTotal += snapshot.generatedTotal();
chunksPerSecond += snapshot.chunksPerSecond();
blockUpdatesPerSecond += snapshot.blockUpdatesPerSecond();
parallelism += snapshot.parallelism();
activeGenerationLeases += snapshot.activeGenerationLeases();
hotloadsTotal += snapshot.hotloadsTotal();
mantleResidentPlates += snapshot.mantleResidentPlates();
mantleQueuedPlates += snapshot.mantleQueuedPlates();
mantleIdleTotal += snapshot.mantleIdleMs();
mantleIdleMax = Math.max(mantleIdleMax, snapshot.mantleIdleMs());
mantleIdleMin = Math.min(mantleIdleMin, snapshot.mantleIdleMs());
mantleIdleCount++;
for (Map.Entry<String, Double> entry : snapshot.generationTimingsMs().entrySet()) {
timingMaxima.merge(entry.getKey(), entry.getValue(), Math::max);
}
}
return new Aggregate(
worldCount,
active,
studio,
closing,
failed,
loadedChunks,
loadedEntities,
entitySaturation,
generatedSession,
generatedTotal,
chunksPerSecond,
blockUpdatesPerSecond,
parallelism,
activeGenerationLeases,
hotloadsTotal,
mantleResidentPlates,
mantleQueuedPlates,
mantleIdleCount == 0 ? 0D : mantleIdleTotal / mantleIdleCount,
mantleIdleMax,
mantleIdleCount == 0 ? 0D : mantleIdleMin,
Map.copyOf(timingMaxima)
);
}
private static double finiteNonNegative(double value) {
return Double.isFinite(value) && value > 0D ? value : 0D;
}
private static Map<String, Double> sanitizeTimings(Map<String, Double> timings) {
if (timings == null || timings.isEmpty()) {
return Map.of();
}
Map<String, Double> sanitized = new LinkedHashMap<>(timings.size());
for (Map.Entry<String, Double> entry : timings.entrySet()) {
String key = entry.getKey();
Double value = entry.getValue();
if (key == null || key.isBlank() || value == null || !Double.isFinite(value) || value < 0D) {
continue;
}
sanitized.put(key, value);
}
return Map.copyOf(sanitized);
}
public record Aggregate(
int worldCount,
int active,
int studio,
int closing,
int failed,
long loadedChunks,
long loadedEntities,
double entitySaturationMax,
long generatedSession,
long generatedTotal,
double chunksPerSecond,
long blockUpdatesPerSecond,
int parallelism,
int activeGenerationLeases,
long hotloadsTotal,
long mantleResidentPlates,
long mantleQueuedPlates,
double mantleIdleAverageMs,
double mantleIdleMaxMs,
double mantleIdleMinMs,
Map<String, Double> generationTimingMaximaMs
) {
public Aggregate {
generationTimingMaximaMs = generationTimingMaximaMs == null
? Map.of()
: Map.copyOf(generationTimingMaximaMs);
}
}
}
@@ -20,6 +20,9 @@ package art.arcane.iris.engine.framework;
@SuppressWarnings("EmptyMethod")
public interface EngineWorldManager {
default void start() {
}
void close();
int getEntityCount();
@@ -38,6 +38,23 @@ public final class GenerationSessionManager {
}
}
public GenerationSessionLease continueSession(String operation, long sessionId) throws GenerationSessionException {
while (true) {
GenerationSessionState state = current.get();
if (state == null || state.sessionId() != sessionId) {
throw rejected(operation, state);
}
state.activeLeases().incrementAndGet();
if (state != current.get()) {
releaseLease(state);
continue;
}
return new GenerationSessionLease(this, state, state.sessionId());
}
}
public long currentSessionId() {
GenerationSessionState state = current.get();
return state == null ? 0L : state.sessionId();
@@ -25,6 +25,7 @@ import art.arcane.iris.engine.object.IrisObjectPlacement;
import art.arcane.iris.engine.object.IrisRegion;
import art.arcane.iris.util.common.parallel.BurstExecutor;
import art.arcane.iris.util.common.parallel.MultiBurst;
import art.arcane.iris.util.project.context.IrisContext;
import art.arcane.volmlib.util.collection.KList;
import art.arcane.volmlib.util.collection.KMap;
import art.arcane.volmlib.util.collection.KSet;
@@ -32,7 +33,7 @@ import art.arcane.volmlib.util.math.Position2;
import art.arcane.volmlib.util.scheduling.PrecisionStopwatch;
import java.util.ArrayDeque;
import java.util.concurrent.Future;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;
@@ -105,18 +106,15 @@ public final class HintedLocator<T> implements Locator<T> {
}
@Override
public Future<Position2> find(Engine engine, Position2 pos, long timeout, Consumer<Integer> checks) throws WrongEngineBroException {
public CompletableFuture<Position2> find(Engine engine, Position2 pos, long timeout, Consumer<Integer> checks) throws WrongEngineBroException {
if (engine.isClosed()) {
throw new WrongEngineBroException();
}
Locator.cancelSearch();
return MultiBurst.burst.completeValue(() -> {
AtomicBoolean stop = new AtomicBoolean(false);
LocatorCanceller.cancel = () -> stop.set(true);
try {
AtomicBoolean stop = new AtomicBoolean(false);
CompletableFuture<Position2> search = MultiBurst.burst.completeValueAsync(() -> {
try (GenerationSessionLease lease = engine.acquireGenerationLease("hinted_locator_search");
IrisContext.Scope ignored = IrisContext.open(engine, lease.sessionId(), null)) {
SearchPlan plan = planner.apply(engine);
if (!plan.isPossible()) {
@@ -124,10 +122,9 @@ public final class HintedLocator<T> implements Locator<T> {
}
return search(engine, plan, pos, timeout, checks, stop);
} finally {
LocatorCanceller.cancel = null;
}
});
return LocatorCanceller.requestScoped(search, stop);
}
private Position2 search(Engine engine, SearchPlan plan, Position2 pos, long timeout, Consumer<Integer> checks, AtomicBoolean stop) {
@@ -140,7 +137,7 @@ public final class HintedLocator<T> implements Locator<T> {
KList<Position2> batch = new KList<>();
for (int ring = 0; ring <= maxRing; ring++) {
if (stop.get() || stopwatch.getMilliseconds() >= timeout) {
if (stop.get() || engine.isClosing() || stopwatch.getMilliseconds() >= timeout) {
return null;
}
@@ -173,7 +170,7 @@ public final class HintedLocator<T> implements Locator<T> {
int index = i;
Position2 sample = batch.get(i);
executor.queue(() -> {
if (stop.get() || (fine && matched.get())) {
if (stop.get() || engine.isClosing() || (fine && matched.get())) {
return;
}
@@ -210,7 +207,7 @@ public final class HintedLocator<T> implements Locator<T> {
return batch.get(i);
}
if (stop.get() || stopwatch.getMilliseconds() >= timeout) {
if (stop.get() || engine.isClosing() || stopwatch.getMilliseconds() >= timeout) {
return null;
}
@@ -228,7 +225,7 @@ public final class HintedLocator<T> implements Locator<T> {
KList<Position2> cells = new KList<>();
for (int ring = 0; ring <= stride; ring++) {
if (stop.get() || stopwatch.getMilliseconds() >= timeout) {
if (stop.get() || engine.isClosing() || stopwatch.getMilliseconds() >= timeout) {
return null;
}
@@ -239,7 +236,7 @@ public final class HintedLocator<T> implements Locator<T> {
for (Position2 cell : cells) {
executor.queue(() -> {
if (stop.get() || found.get() != null) {
if (stop.get() || engine.isClosing() || found.get() != null) {
return;
}
@@ -34,7 +34,7 @@ import art.arcane.iris.util.common.parallel.MultiBurst;
import art.arcane.volmlib.util.scheduling.PrecisionStopwatch;
import java.util.Set;
import java.util.concurrent.Future;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;
@@ -42,13 +42,6 @@ import java.util.function.Consumer;
@FunctionalInterface
public interface Locator<T> {
static void cancelSearch() {
if (LocatorCanceller.cancel != null) {
LocatorCanceller.cancel.run();
LocatorCanceller.cancel = null;
}
}
static Locator<IrisRegion> region(String loadKey) {
Locator<IrisRegion> exact = (e, c) -> e.getRegion((c.getX() << 4) + 8, (c.getZ() << 4) + 8).getLoadKey().equals(loadKey);
return new HintedLocator<>(exact, (engine) -> HintedLocator.regionPlan(engine, loadKey));
@@ -107,55 +100,54 @@ public interface Locator<T> {
boolean matches(Engine engine, Position2 chunk);
default Future<Position2> find(Engine engine, Position2 pos, long timeout, Consumer<Integer> checks) throws WrongEngineBroException {
default CompletableFuture<Position2> find(Engine engine, Position2 pos, long timeout, Consumer<Integer> checks) throws WrongEngineBroException {
if (engine.isClosed()) {
throw new WrongEngineBroException();
}
cancelSearch();
AtomicBoolean stop = new AtomicBoolean(false);
CompletableFuture<Position2> search = MultiBurst.burst.completeValueAsync(() -> {
try (GenerationSessionLease lease = engine.acquireGenerationLease("locator_search");
IrisContext.Scope ignored = IrisContext.open(engine, lease.sessionId(), null)) {
int tc = IrisSettings.getThreadCount(IrisSettings.get().getConcurrency().getParallelism()) * 32;
MultiBurst burst = MultiBurst.burst;
AtomicBoolean found = new AtomicBoolean(false);
AtomicInteger searched = new AtomicInteger();
AtomicReference<Position2> foundPos = new AtomicReference<>();
PrecisionStopwatch px = PrecisionStopwatch.start();
AtomicReference<Position2> next = new AtomicReference<>(pos);
Spiraler s = new Spiraler(100000, 100000, (x, z) -> next.set(new Position2(x, z)));
s.setOffset(pos.getX(), pos.getZ());
s.next();
while (!found.get() && !stop.get() && !engine.isClosing() && px.getMilliseconds() < timeout) {
BurstExecutor e = burst.burst(tc);
return MultiBurst.burst.completeValue(() -> {
int tc = IrisSettings.getThreadCount(IrisSettings.get().getConcurrency().getParallelism()) * 32;
MultiBurst burst = MultiBurst.burst;
AtomicBoolean found = new AtomicBoolean(false);
AtomicInteger searched = new AtomicInteger();
AtomicBoolean stop = new AtomicBoolean(false);
AtomicReference<Position2> foundPos = new AtomicReference<>();
PrecisionStopwatch px = PrecisionStopwatch.start();
LocatorCanceller.cancel = () -> stop.set(true);
AtomicReference<Position2> next = new AtomicReference<>(pos);
Spiraler s = new Spiraler(100000, 100000, (x, z) -> next.set(new Position2(x, z)));
s.setOffset(pos.getX(), pos.getZ());
s.next();
while (!found.get() && !stop.get() && px.getMilliseconds() < timeout) {
BurstExecutor e = burst.burst(tc);
for (int i = 0; i < tc; i++) {
Position2 p = next.get();
s.next();
e.queue(() -> {
if (found.get() || stop.get() || engine.isClosing()) {
return;
}
if (matches(engine, p)) {
foundPos.compareAndSet(null, p);
found.set(true);
}
searched.incrementAndGet();
});
}
for (int i = 0; i < tc; i++) {
Position2 p = next.get();
s.next();
e.queue(() -> {
if (found.get()) {
return;
}
if (matches(engine, p)) {
foundPos.compareAndSet(null, p);
found.set(true);
}
searched.incrementAndGet();
});
e.complete();
checks.accept(searched.get());
}
e.complete();
checks.accept(searched.get());
if (found.get() && foundPos.get() != null) {
return foundPos.get();
}
return null;
}
LocatorCanceller.cancel = null;
if (found.get() && foundPos.get() != null) {
return foundPos.get();
}
return null;
});
return LocatorCanceller.requestScoped(search, stop);
}
}
@@ -18,6 +18,49 @@
package art.arcane.iris.engine.framework;
public class LocatorCanceller {
import java.util.Objects;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.atomic.AtomicBoolean;
public final class LocatorCanceller {
protected static Runnable cancel = null;
private LocatorCanceller() {
}
static <T> CompletableFuture<T> requestScoped(CompletableFuture<T> future, AtomicBoolean stop) {
return new RequestFuture<>(Objects.requireNonNull(future), Objects.requireNonNull(stop));
}
private static final class RequestFuture<T> extends CompletableFuture<T> {
private final CompletableFuture<T> delegate;
private final AtomicBoolean stop;
private RequestFuture(CompletableFuture<T> delegate, AtomicBoolean stop) {
this.delegate = delegate;
this.stop = stop;
delegate.whenComplete((value, exception) -> {
if (isDone()) {
return;
}
if (exception == null) {
complete(value);
} else {
completeExceptionally(exception);
}
});
}
@Override
public boolean cancel(boolean mayInterruptIfRunning) {
if (isDone()) {
return false;
}
stop.set(true);
boolean cancelled = super.cancel(mayInterruptIfRunning);
delegate.cancel(mayInterruptIfRunning);
return cancelled;
}
}
}

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