This commit is contained in:
Brian Neumann-Fopiano committed 2026-07-21 12:00:17 -04:00
1 parent d76dadec30
commit f6f06c1ab9
141 files changed
+10713 -1721

No files matched your search

@@ -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);
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");
}
if (!irisBiome.isCustom()) {
continue;
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));
}
}
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();
});
}
}