This commit is contained in:
Brian Neumann-Fopiano
2026-07-30 12:31:39 -04:00
parent 0afbfcb7a2
commit 324cf9e095
374 changed files with 22705 additions and 25650 deletions
@@ -106,11 +106,26 @@ public class CustomBiomeSource extends BiomeSource {
return o;
}
return invokeFor(type, source);
o = invokeFor(type, source);
if (o != null) {
return o;
}
throw new IllegalStateException("Iris cannot resolve a " + type.getName()
+ " from " + source.getClass().getName() + " on this server version");
}
private static Object fieldFor(Class<?> returns, Object in) {
return fieldForClass(returns, in.getClass(), in);
for (Class<?> sourceType = in.getClass(); sourceType != null; sourceType = sourceType.getSuperclass()) {
Object o = fieldForClass(returns, sourceType, in);
if (o != null) {
return o;
}
}
return null;
}
private static Object invokeFor(Class<?> returns, Object in) {
@@ -120,8 +135,9 @@ public class CustomBiomeSource extends BiomeSource {
try {
IrisLogging.debug("[NMS] Found " + returns.getSimpleName() + " in " + in.getClass().getSimpleName() + "." + i.getName() + "()");
return i.invoke(in);
} catch (Throwable e) {
e.printStackTrace();
} catch (ReflectiveOperationException | RuntimeException e) {
throw new IllegalStateException("Iris failed to invoke " + in.getClass().getName() + "."
+ i.getName() + "() for " + returns.getName(), e);
}
}
}
@@ -137,8 +153,9 @@ public class CustomBiomeSource extends BiomeSource {
try {
IrisLogging.debug("[NMS] Found " + returnType.getSimpleName() + " in " + sourceType.getSimpleName() + "." + i.getName());
return (T) i.get(in);
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (IllegalAccessException | RuntimeException e) {
throw new IllegalStateException("Iris failed to read " + sourceType.getName() + "."
+ i.getName() + " for " + returnType.getName(), e);
}
}
}
@@ -241,7 +258,13 @@ public class CustomBiomeSource extends BiomeSource {
}
private RegistryAccess registry() {
return registryAccess.aquire(() -> (RegistryAccess) getFor(RegistryAccess.Frozen.class, ((CraftServer) Bukkit.getServer()).getHandle().getServer()));
RegistryAccess access = registryAccess.aquire(() -> (RegistryAccess) getFor(RegistryAccess.Frozen.class, ((CraftServer) Bukkit.getServer()).getHandle().getServer()));
if (access == null) {
throw new IllegalStateException("Iris cannot resolve the Minecraft registry access on this server version");
}
return access;
}
@Override
@@ -15,6 +15,11 @@ import art.arcane.iris.nativegen.NativeStructureStartInjector;
import art.arcane.iris.nativegen.NativeStructureReferenceEnvelope;
import art.arcane.iris.nativegen.NativeStructureLocateResults;
import art.arcane.iris.nativegen.NativeStructurePostProcessor;
import art.arcane.iris.nativegen.NativeStructureSurfaceFitter;
import art.arcane.iris.nativegen.NativeStructureTerrainIntegrator;
import art.arcane.iris.nativegen.NativeStructureVegetationClearer;
import art.arcane.iris.nativegen.NativeStructureVerticalPlacer;
import art.arcane.iris.nativegen.WorldgenTerrainHeightmaps;
import art.arcane.iris.spi.PlatformBlockState;
import art.arcane.iris.util.common.data.IrisCustomData;
import art.arcane.iris.util.common.reflect.WrappedField;
@@ -184,7 +189,7 @@ public class IrisChunkGenerator extends CustomChunkGenerator {
}
String key = id.toString();
IrisNativeStructureDecision decision = NativeStructureGenerationPolicy.resolve(engine,
key, NativeStructurePostProcessor.isUndergroundStep(holder.value().step()));
key, NativeStructureVegetationClearer.isUndergroundStep(holder.value().step()));
if (!decision.generate()) {
continue;
}
@@ -290,7 +295,7 @@ public class IrisChunkGenerator extends CustomChunkGenerator {
throw NativeStructureGenerationException.failure(
"resolution", null, chunkPos.x(), chunkPos.z());
}
boolean undergroundStep = NativeStructurePostProcessor.isUndergroundStep(structure.step());
boolean undergroundStep = NativeStructureVegetationClearer.isUndergroundStep(structure.step());
IrisNativeStructureDecision decision;
try {
decision = NativeStructureGenerationPolicy.resolve(engine,
@@ -304,7 +309,7 @@ public class IrisChunkGenerator extends CustomChunkGenerator {
continue;
}
try {
NativeStructurePostProcessor.applyVerticalPlacement(
NativeStructureVerticalPlacer.applyVerticalPlacement(
start,
structureId,
decision.yShift(),
@@ -317,7 +322,7 @@ public class IrisChunkGenerator extends CustomChunkGenerator {
(x, z) -> engine.getHeight(x, z, true) + engine.getMinHeight());
StructureStart wrapped = NativeStructureReferenceEnvelope.wrap(
start, structure, start.getReferences(), templateManager,
NativeStructurePostProcessor.resolveNativeTerrain(start, decision.terrain()));
NativeStructureTerrainIntegrator.resolveNativeTerrain(start, decision.terrain()));
access.setStartForStructure(structure, wrapped);
} catch (Throwable error) {
throw NativeStructureGenerationException.failure(
@@ -448,8 +453,8 @@ public class IrisChunkGenerator extends CustomChunkGenerator {
List<NativePlacementGroup> placementGroups = new ArrayList<>();
List<StructureStart> heightmapStarts = new ArrayList<>();
List<StructureStart> nativeStarts = new ArrayList<>();
List<NativeStructurePostProcessor.VegetationTarget> vegetationTargets = new ArrayList<>();
List<NativeStructurePostProcessor.TerrainTarget> terrainTargets = new ArrayList<>();
List<NativeStructureVegetationClearer.VegetationTarget> vegetationTargets = new ArrayList<>();
List<NativeStructureTerrainIntegrator.TerrainTarget> terrainTargets = new ArrayList<>();
for (int step = 0; step < steps; step++) {
int index = 0;
for (Structure structure : byStep.get(step)) {
@@ -461,7 +466,7 @@ public class IrisChunkGenerator extends CustomChunkGenerator {
}
try {
IrisNativeStructureDecision sourceDecision = NativeStructureGenerationPolicy.resolve(engine,
structureId, NativeStructurePostProcessor.isUndergroundStep(structure.step()));
structureId, NativeStructureVegetationClearer.isUndergroundStep(structure.step()));
List<StructureStart> starts = structureManager.startsForStructure(sectionPos, structure);
List<NativePlacement> resolvedPlacements = new ArrayList<>(starts.size());
for (StructureStart start : starts) {
@@ -474,17 +479,17 @@ public class IrisChunkGenerator extends CustomChunkGenerator {
}
resolvedPlacements.add(new NativePlacement(start, decision));
heightmapStarts.add(start);
terrainTargets.add(new NativeStructurePostProcessor.TerrainTarget(
terrainTargets.add(new NativeStructureTerrainIntegrator.TerrainTarget(
structureId, start,
NativeStructurePostProcessor.resolveNativeTerrain(
NativeStructureTerrainIntegrator.resolveNativeTerrain(
start, decision.terrain())));
if (plan == null || !plan.placement().isUnderground()) {
nativeStarts.add(start);
}
boolean clearEntireFootprint = NativeStructurePostProcessor
boolean clearEntireFootprint = NativeStructureVegetationClearer
.shouldClearEntireVegetationFootprint(
structure.step(), decision.clearVegetation());
vegetationTargets.add(new NativeStructurePostProcessor.VegetationTarget(
vegetationTargets.add(new NativeStructureVegetationClearer.VegetationTarget(
start, clearEntireFootprint));
}
if (!resolvedPlacements.isEmpty()) {
@@ -507,7 +512,7 @@ public class IrisChunkGenerator extends CustomChunkGenerator {
chunkPos.x(), chunkPos.z(), error);
}
try {
NativeStructurePostProcessor.prepareSurfaceStructures(
NativeStructureSurfaceFitter.prepareSurfaceStructures(
world, area, nativeStarts,
(x, z) -> engine.getHeight(x, z, true) + engine.getMinHeight());
} catch (Throwable error) {
@@ -516,7 +521,7 @@ public class IrisChunkGenerator extends CustomChunkGenerator {
chunkPos.x(), chunkPos.z(), error);
}
try {
NativeStructurePostProcessor.clearIntersectingVegetation(
NativeStructureVegetationClearer.clearIntersectingVegetation(
world, chunk, area, vegetationTargets);
} catch (Throwable error) {
throw NativeStructureGenerationException.failure(
@@ -741,27 +746,30 @@ public class IrisChunkGenerator extends CustomChunkGenerator {
}
static {
Field biomeSource = null;
List<Field> biomeSources = new ArrayList<>(1);
for (Field field : ChunkGenerator.class.getDeclaredFields()) {
if (!field.getType().equals(BiomeSource.class))
continue;
biomeSource = field;
break;
biomeSources.add(field);
}
if (biomeSource == null)
throw new RuntimeException("Could not find biomeSource field in ChunkGenerator!");
if (biomeSources.size() != 1)
throw new IllegalStateException("Expected exactly one BiomeSource field in ChunkGenerator, found "
+ biomeSources.size() + " " + biomeSources.stream().map(Field::getName).toList());
Field biomeSource = biomeSources.getFirst();
Method setHeight = null;
List<Method> setHeights = new ArrayList<>(1);
for (Method method : Heightmap.class.getDeclaredMethods()) {
Class<?>[] types = method.getParameterTypes();
if (types.length != 3 || !Arrays.equals(types, new Class<?>[]{int.class, int.class, int.class})
if (!method.getName().equals("setHeight")
|| !Arrays.equals(types, new Class<?>[]{int.class, int.class, int.class})
|| !method.getReturnType().equals(void.class))
continue;
setHeight = method;
break;
setHeights.add(method);
}
if (setHeight == null)
throw new RuntimeException("Could not find setHeight method in Heightmap!");
if (setHeights.size() != 1)
throw new IllegalStateException("Expected exactly one Heightmap.setHeight(int,int,int) method, found "
+ setHeights.size());
Method setHeight = setHeights.getFirst();
BIOME_SOURCE = new WrappedField<>(ChunkGenerator.class, biomeSource.getName());
SET_HEIGHT = new WrappedReturningMethod<>(Heightmap.class, setHeight.getName(), setHeight.getParameterTypes());
@@ -171,7 +171,14 @@ public class NMSBinding implements INMSBinding {
return o;
}
return invokeFor(type, source);
o = invokeFor(type, source);
if (o != null) {
return o;
}
throw new IllegalStateException("Iris cannot resolve a " + type.getName()
+ " from " + source.getClass().getName() + " on this server version");
}
private static Object invokeFor(Class<?> returns, Object in) {
@@ -181,8 +188,9 @@ public class NMSBinding implements INMSBinding {
try {
IrisLogging.debug("[NMS] Found " + returns.getSimpleName() + " in " + in.getClass().getSimpleName() + "." + i.getName() + "()");
return i.invoke(in);
} catch (Throwable e) {
e.printStackTrace();
} catch (ReflectiveOperationException | RuntimeException e) {
throw new IllegalStateException("Iris failed to invoke " + in.getClass().getName() + "."
+ i.getName() + "() for " + returns.getName(), e);
}
}
}
@@ -191,7 +199,15 @@ public class NMSBinding implements INMSBinding {
}
private static Object fieldFor(Class<?> returns, Object in) {
return fieldForClass(returns, in.getClass(), in);
for (Class<?> sourceType = in.getClass(); sourceType != null; sourceType = sourceType.getSuperclass()) {
Object o = fieldForClass(returns, sourceType, in);
if (o != null) {
return o;
}
}
return null;
}
@SuppressWarnings("unchecked")
@@ -202,8 +218,9 @@ public class NMSBinding implements INMSBinding {
try {
IrisLogging.debug("[NMS] Found " + returnType.getSimpleName() + " in " + sourceType.getSimpleName() + "." + i.getName());
return (T) i.get(in);
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (IllegalAccessException | RuntimeException e) {
throw new IllegalStateException("Iris failed to read " + sourceType.getName() + "."
+ i.getName() + " for " + returnType.getName(), e);
}
}
}
@@ -366,11 +383,18 @@ public class NMSBinding implements INMSBinding {
}
private RegistryAccess registry() {
return registryAccess.aquire(() -> (RegistryAccess) getFor(RegistryAccess.Frozen.class, ((CraftServer) Bukkit.getServer()).getHandle().getServer()));
RegistryAccess access = registryAccess.aquire(() -> (RegistryAccess) getFor(RegistryAccess.Frozen.class, ((CraftServer) Bukkit.getServer()).getHandle().getServer()));
if (access == null) {
throw new IllegalStateException("Iris cannot resolve the Minecraft registry access on this server version");
}
return access;
}
private Registry<net.minecraft.world.level.biome.Biome> getCustomBiomeRegistry() {
return registry().lookup(Registries.BIOME).orElse(null);
return registry().lookup(Registries.BIOME).orElseThrow(() -> new IllegalStateException(
"Iris cannot resolve the Minecraft biome registry on this server version"));
}
private Registry<Block> getBlockRegistry() {
@@ -435,7 +459,26 @@ public class NMSBinding implements INMSBinding {
@Override
public String getKeyForBiomeBase(Object biomeBase) {
return getCustomBiomeRegistry().getKey((net.minecraft.world.level.biome.Biome) biomeBase).getPath(); // something, not something:something
net.minecraft.world.level.biome.Biome biome;
if (biomeBase instanceof Holder<?> holder) {
Object value = holder.value();
if (!(value instanceof net.minecraft.world.level.biome.Biome held)) {
throw new IllegalArgumentException("Iris cannot read a biome key from holder value "
+ (value == null ? "null" : value.getClass().getName()));
}
biome = held;
} else if (biomeBase instanceof net.minecraft.world.level.biome.Biome direct) {
biome = direct;
} else {
throw new IllegalArgumentException("Iris cannot read a biome key from "
+ (biomeBase == null ? "null" : biomeBase.getClass().getName()));
}
Identifier key = getCustomBiomeRegistry().getKey(biome);
if (key == null) {
throw new IllegalStateException("Iris found no registry key for biome " + biome);
}
return key.getPath(); // something, not something:something
}
@Override
@@ -805,7 +848,7 @@ public class NMSBinding implements INMSBinding {
@Override
public MCAPaletteAccess createPalette() {
MCAIdMapper<BlockState> registry = registryCache.aquireNasty(() -> {
MCAIdMapper<BlockState> registry = registryCache.aquireNastyPrint(() -> {
Field cf = IdMapper.class.getDeclaredField("tToId");
Field df = IdMapper.class.getDeclaredField("idToT");
Field bf = IdMapper.class.getDeclaredField("nextId");
@@ -818,7 +861,13 @@ public class NMSBinding implements INMSBinding {
List<BlockState> d = (List<BlockState>) df.get(blockData);
return new MCAIdMapper<BlockState>(c, d, b);
});
MCAPalette<BlockState> global = globalCache.aquireNasty(() -> new MCAGlobalPalette<>(registry, ((CraftBlockData) AIR).getState()));
if (registry == null) {
throw new IllegalStateException("Iris cannot mirror the Minecraft block state id map on this server version");
}
MCAPalette<BlockState> global = globalCache.aquireNastyPrint(() -> new MCAGlobalPalette<>(registry, ((CraftBlockData) AIR).getState()));
if (global == null) {
throw new IllegalStateException("Iris cannot build the global block state palette on this server version");
}
java.util.Map<CompoundTag, BlockState> innerDecodeCache = new java.util.concurrent.ConcurrentHashMap<>(64);
java.util.Map<CompoundTag, BlockState> outerDecodeCache = new java.util.concurrent.ConcurrentHashMap<>(64);
MCAPalettedContainer<BlockState> container = new MCAPalettedContainer<>(global, registry,
@@ -1,106 +0,0 @@
package art.arcane.iris.core.nms.v26_2_R1;
import net.minecraft.core.SectionPos;
import net.minecraft.util.Mth;
import net.minecraft.util.SimpleBitStorage;
import net.minecraft.world.level.ChunkPos;
import net.minecraft.world.level.WorldGenLevel;
import net.minecraft.world.level.chunk.ChunkAccess;
import net.minecraft.world.level.levelgen.Heightmap;
import net.minecraft.world.level.levelgen.structure.BoundingBox;
import net.minecraft.world.level.levelgen.structure.StructureStart;
import java.util.HashSet;
import java.util.List;
import java.util.Objects;
import java.util.Set;
import java.util.function.IntBinaryOperator;
/**
* Writes WORLD_SURFACE_WG and OCEAN_FLOOR_WG from Iris engine heights.
*
* Iris writes its terrain straight into LevelChunkSection, so neither vanilla's noise fill nor
* ProtoChunk.setBlockState ever updates the two worldgen heightmaps. Without these writes
* ChunkAccess.getHeight lazily primes the map from finished blocks - Iris trees, leaves and snow
* included - and vanilla pieces that self-snap against WORLD_SURFACE_WG (igloo) or OCEAN_FLOOR_WG
* (ocean ruins, buried treasure, mineshaft interiors) anchor to the canopy instead of the terrain.
*
* Both resolvers take world block coordinates and return the absolute first free Y, matching
* Heightmap's internal "first available" semantics: WORLD_SURFACE_WG counts fluid, OCEAN_FLOOR_WG
* does not.
*/
final class WorldgenTerrainHeightmaps {
private static final int COLUMNS = 256;
private static final int PLACEMENT_CHUNK_MARGIN = 1;
private WorldgenTerrainHeightmaps() {
}
static void primeTerrain(ChunkAccess chunk, IntBinaryOperator surfaceFirstFreeY,
IntBinaryOperator floorFirstFreeY) {
Objects.requireNonNull(chunk, "Iris worldgen heightmap priming requires a chunk");
Objects.requireNonNull(surfaceFirstFreeY,
"Iris worldgen heightmap priming requires a surface height resolver");
Objects.requireNonNull(floorFirstFreeY,
"Iris worldgen heightmap priming requires an ocean floor height resolver");
write(chunk, Heightmap.Types.WORLD_SURFACE_WG, surfaceFirstFreeY);
write(chunk, Heightmap.Types.OCEAN_FLOOR_WG, floorFirstFreeY);
}
static void primeStructurePlacement(WorldGenLevel world, List<StructureStart> starts,
IntBinaryOperator surfaceFirstFreeY,
IntBinaryOperator floorFirstFreeY) {
Objects.requireNonNull(world,
"Iris worldgen heightmap priming requires a generation level");
if (starts == null || starts.isEmpty()) {
return;
}
Set<Long> primed = new HashSet<>();
for (StructureStart start : starts) {
if (start == null || !start.isValid()) {
continue;
}
BoundingBox bounds = start.getBoundingBox();
int minChunkX = SectionPos.blockToSectionCoord(bounds.minX()) - PLACEMENT_CHUNK_MARGIN;
int maxChunkX = SectionPos.blockToSectionCoord(bounds.maxX()) + PLACEMENT_CHUNK_MARGIN;
int minChunkZ = SectionPos.blockToSectionCoord(bounds.minZ()) - PLACEMENT_CHUNK_MARGIN;
int maxChunkZ = SectionPos.blockToSectionCoord(bounds.maxZ()) + PLACEMENT_CHUNK_MARGIN;
for (int chunkX = minChunkX; chunkX <= maxChunkX; chunkX++) {
for (int chunkZ = minChunkZ; chunkZ <= maxChunkZ; chunkZ++) {
if (!primed.add(ChunkPos.pack(chunkX, chunkZ))) {
continue;
}
if (!world.hasChunk(chunkX, chunkZ)) {
continue;
}
primeTerrain(world.getChunk(chunkX, chunkZ), surfaceFirstFreeY, floorFirstFreeY);
}
}
}
}
private static void write(ChunkAccess chunk, Heightmap.Types type, IntBinaryOperator firstFreeY) {
ChunkPos pos = chunk.getPos();
int minY = chunk.getMinY();
int height = chunk.getHeight();
int baseX = pos.getMinBlockX();
int baseZ = pos.getMinBlockZ();
SimpleBitStorage storage = new SimpleBitStorage(Mth.ceillog2(height + 1), COLUMNS);
for (int z = 0; z < 16; z++) {
for (int x = 0; x < 16; x++) {
int firstFree = firstFreeY.applyAsInt(baseX + x, baseZ + z) - minY;
storage.set(x + z * 16, Mth.clamp(firstFree, 0, height));
}
}
Heightmap heightmap = chunk.getOrCreateHeightmapUnprimed(type);
long[] packed = storage.getRaw();
int expected = heightmap.getRawData().length;
if (expected != packed.length) {
throw new IllegalStateException("Iris cannot prime " + type + " for chunk "
+ pos.x() + "," + pos.z() + ": packed " + packed.length
+ " words for a heightmap storing " + expected
+ "; Heightmap.setRawData would silently fall back to block scanning");
}
heightmap.setRawData(chunk, type, packed);
}
}
@@ -91,6 +91,27 @@ public class IrisChunkGeneratorFailureContractTest {
assertTrue(source.contains("engine.acquireGenerationLease(\"bukkit_nms_worldgen_heightmaps\")"));
}
@Test
public void worldgenHeightmapPrimingLivesInTheSharedNativegenSources() throws IOException {
Path nativegen = Path.of(System.getProperty("iris.nativeStructurePostProcessorSource")).getParent();
Path shared = nativegen.resolve("WorldgenTerrainHeightmaps.java");
assertTrue("Worldgen heightmap priming must be shared with the modded loaders through "
+ nativegen, Files.isRegularFile(shared));
String heightmaps = Files.readString(shared);
assertTrue(heightmaps.contains("package art.arcane.iris.nativegen;"));
assertTrue(heightmaps.contains("public static void primeTerrain("));
assertTrue(heightmaps.contains("public static void primeStructurePlacement("));
assertFalse(heightmaps.contains("org.bukkit"));
assertFalse(heightmaps.contains("craftbukkit"));
String source = Files.readString(Path.of(System.getProperty("iris.nmsChunkGeneratorSource")));
assertTrue(source.contains("import art.arcane.iris.nativegen.WorldgenTerrainHeightmaps;"));
}
private static int occurrences(String source, String needle) {
int count = 0;
int index = source.indexOf(needle);
@@ -79,23 +79,32 @@ public class IrisChunkGeneratorMonumentLocateContractTest {
@Test
public void stiltSupportUsesPlacedSolidOccupancyWithoutSnapshotDifferenceRequirement() throws IOException {
String source = Files.readString(Path.of(System.getProperty("iris.nativeStructurePostProcessorSource")));
Path processor = Path.of(System.getProperty("iris.nativeStructurePostProcessorSource"));
String source = Files.readString(processor);
String foundation = Files.readString(
processor.resolveSibling("NativeStructureFoundationBuilder.java"));
int placement = source.indexOf("start.placeInChunk(world, structureManager, generator");
int stiltPlacement = source.indexOf("placeStilts(world, area, structureId, start", placement);
int occupancyCheck = source.indexOf("if (state.isSolid())", stiltPlacement);
int terrainFloor = source.indexOf("Math.max(terrainY,", stiltPlacement);
int stiltDefinition = foundation.indexOf("static void placeStilts(");
int occupancyCheck = foundation.indexOf("if (state.isSolid())", stiltDefinition);
int terrainFloor = foundation.indexOf("Math.max(terrainY,", stiltDefinition);
assertTrue(placement >= 0);
assertTrue(stiltPlacement > placement);
assertTrue(occupancyCheck > stiltPlacement);
assertTrue(terrainFloor > stiltPlacement);
assertTrue(stiltDefinition >= 0);
assertTrue(occupancyCheck > stiltDefinition);
assertTrue(terrainFloor > stiltDefinition);
assertFalse(source.contains("state.equals("));
assertFalse(foundation.contains("state.equals("));
assertFalse(source.contains("snapshot.states"));
assertFalse(foundation.contains("snapshot.states"));
}
@Test
public void verticalPlacementMovesPiecesMonumentChildrenJigsawJunctionsAndCachedBoundsTogether() throws IOException {
String source = Files.readString(Path.of(System.getProperty("iris.nativeStructurePostProcessorSource")));
String source = Files.readString(
Path.of(System.getProperty("iris.nativeStructurePostProcessorSource"))
.resolveSibling("NativeStructureVerticalPlacer.java"));
int placementStart = source.indexOf("public static int applyVerticalPlacement");
int shiftStart = source.indexOf("public static int applyVerticalShift", placementStart);
int alignmentStart = source.indexOf("static int alignOceanMonumentToSeaLevel", shiftStart);
@@ -1,5 +1,6 @@
package art.arcane.iris.core.nms.v26_2_R1;
import art.arcane.iris.nativegen.WorldgenTerrainHeightmaps;
import com.mojang.serialization.Codec;
import net.minecraft.SharedConstants;
import net.minecraft.core.BlockPos;
@@ -95,7 +95,7 @@ public class NativeStructureFactoryTest {
new PiecesContainer(List.of(piece))
);
StructureStart relocated = NativeStructurePostProcessor.relocateToMinY(
StructureStart relocated = NativeStructureVerticalPlacer.relocateToMinY(
start, source, -20, LevelHeightAccessor.create(-64, 384));
assertNotSame(start, relocated);
@@ -61,7 +61,7 @@ public class NativeStructurePostProcessorEncaseTest {
put(blocks, bounds.minX(), bounds.minY(), bounds.minZ(), Blocks.DIRT.defaultBlockState());
put(blocks, bounds.maxX(), bounds.minY(), bounds.maxZ(), Blocks.WATER.defaultBlockState());
NativeStructurePostProcessor.integrateTerrain(
NativeStructureTerrainIntegrator.integrateTerrain(
world(blocks), area, "minecraft:stronghold", start,
new IrisStructureTerrain()
.setMode(IrisStructureTerrainMode.ENCASE)
@@ -86,7 +86,7 @@ public class NativeStructurePostProcessorEncaseTest {
BoundingBox bounds = start.getPieces().getFirst().getBoundingBox();
Map<BlockPos, BlockState> blocks = new HashMap<>();
NativeStructurePostProcessor.integrateTerrain(
NativeStructureTerrainIntegrator.integrateTerrain(
world(blocks), bounds, "minecraft:stronghold", start,
new IrisStructureTerrain().setMode(IrisStructureTerrainMode.ENCASE),
null);
@@ -105,7 +105,7 @@ public class NativeStructurePostProcessorEncaseTest {
Map<BlockPos, BlockState> blocks = new HashMap<>();
IrisMaterialPalette palette = new IrisMaterialPalette().qclear().qadd("minecraft:tuff");
NativeStructurePostProcessor.integrateTerrain(
NativeStructureTerrainIntegrator.integrateTerrain(
world(blocks), bounds, "minecraft:stronghold", start,
new IrisStructureTerrain()
.setMode(IrisStructureTerrainMode.ENCASE)
@@ -131,7 +131,7 @@ public class NativeStructurePostProcessorEncaseTest {
@Test
public void buryAndEncapsulateAdaptationsAutoDefaultToEncase() {
for (TerrainAdjustment adjustment : List.of(TerrainAdjustment.BURY, TerrainAdjustment.ENCAPSULATE)) {
IrisStructureTerrain resolved = NativeStructurePostProcessor.resolveNativeTerrain(
IrisStructureTerrain resolved = NativeStructureTerrainIntegrator.resolveNativeTerrain(
start(adjustment, 64), null);
assertEquals(IrisStructureTerrainMode.ENCASE, resolved.resolvedMode());
@@ -146,7 +146,7 @@ public class NativeStructurePostProcessorEncaseTest {
public void otherAdaptationsNeverAutoDefaultToEncase() {
for (TerrainAdjustment adjustment : List.of(
TerrainAdjustment.NONE, TerrainAdjustment.BEARD_THIN, TerrainAdjustment.BEARD_BOX)) {
assertNull(NativeStructurePostProcessor.resolveNativeTerrain(
assertNull(NativeStructureTerrainIntegrator.resolveNativeTerrain(
start(adjustment, 64), null));
}
}
@@ -156,7 +156,7 @@ public class NativeStructurePostProcessorEncaseTest {
IrisStructureTerrain configured = new IrisStructureTerrain()
.setMode(IrisStructureTerrainMode.SOURCE);
assertSame(configured, NativeStructurePostProcessor.resolveNativeTerrain(
assertSame(configured, NativeStructureTerrainIntegrator.resolveNativeTerrain(
start(TerrainAdjustment.BURY, 64), configured));
}
@@ -189,9 +189,9 @@ public class NativeStructurePostProcessorEncaseTest {
StructureStart first = start(TerrainAdjustment.BURY, 64);
StructureStart second = start(TerrainAdjustment.BURY, 64);
NativeStructurePostProcessor.applyVerticalShift(
NativeStructureVerticalPlacer.applyVerticalShift(
first, -64, -256, 320, true, false, band, (x, z) -> 40);
NativeStructurePostProcessor.applyVerticalShift(
NativeStructureVerticalPlacer.applyVerticalShift(
second, -64, -256, 320, true, false, band, (x, z) -> 40);
BoundingBox bounds = first.getBoundingBox();
@@ -199,7 +199,7 @@ public class NativeStructurePostProcessorEncaseTest {
assertTrue(bounds.minY() >= -120);
assertTrue(bounds.maxY() <= -20);
int repeated = NativeStructurePostProcessor.applyVerticalShift(
int repeated = NativeStructureVerticalPlacer.applyVerticalShift(
first, -64, -256, 320, true, false, band, (x, z) -> 40);
assertEquals(0, repeated);
}
@@ -209,7 +209,7 @@ public class NativeStructurePostProcessorEncaseTest {
IrisStructureYBand band = new IrisStructureYBand().setMin(-50).setMax(-45);
StructureStart start = start(TerrainAdjustment.BURY, 64);
NativeStructurePostProcessor.applyVerticalShift(
NativeStructureVerticalPlacer.applyVerticalShift(
start, 0, -256, 320, true, false, band, (x, z) -> 40);
BoundingBox bounds = start.getBoundingBox();
@@ -225,7 +225,7 @@ public class NativeStructurePostProcessorEncaseTest {
BoundingBox bounds = start.getBoundingBox();
int height = bounds.maxY() - bounds.minY();
NativeStructurePostProcessor.applyVerticalShift(
NativeStructureVerticalPlacer.applyVerticalShift(
start, -200, -256, 320, true, false, band, (x, z) -> 40);
assertEquals(-64 - height / 2, start.getBoundingBox().minY());
@@ -237,7 +237,7 @@ public class NativeStructurePostProcessorEncaseTest {
StructureStart start = start(TerrainAdjustment.BURY, 64);
int minY = start.getBoundingBox().minY();
int offset = NativeStructurePostProcessor.applyVerticalShift(
int offset = NativeStructureVerticalPlacer.applyVerticalShift(
start, 0, -256, 320, true, true, band, (x, z) -> 40);
assertEquals(0, offset);
@@ -33,7 +33,7 @@ public class NativeStructurePostProcessorMonumentTest {
public void vanillaSeaLevelKeepsTheVanillaMonumentHeight() {
StructureStart start = monumentStart(1337L);
int offset = NativeStructurePostProcessor.applyVerticalPlacement(
int offset = NativeStructureVerticalPlacer.applyVerticalPlacement(
start, "minecraft:monument", 0, 63, -64, 320, false, false, null, (x, z) -> 0);
assertEquals(0, offset);
@@ -45,11 +45,11 @@ public class NativeStructurePostProcessorMonumentTest {
public void shiftedSeaLevelMovesTheShellAndEveryRoomTogether() {
StructureStart start = monumentStart(1337L);
OceanMonumentPieces.MonumentBuilding building = monumentBuilding(start);
List<StructurePiece> children = NativeStructurePostProcessor.monumentChildPieces(building);
List<StructurePiece> children = NativeStructureVerticalPlacer.monumentChildPieces(building);
int[] childMinY = minimumYs(children);
BoundingBox cachedBounds = start.getBoundingBox();
int offset = NativeStructurePostProcessor.applyVerticalPlacement(
int offset = NativeStructureVerticalPlacer.applyVerticalPlacement(
start, "minecraft:monument", 0, 50, -256, 512, false, false, null, (x, z) -> 0);
assertEquals(-13, offset);
@@ -63,7 +63,7 @@ public class NativeStructurePostProcessorMonumentTest {
assertEquals(childMinY[i] - 13, children.get(i).getBoundingBox().minY());
}
int repeatedOffset = NativeStructurePostProcessor.applyVerticalPlacement(
int repeatedOffset = NativeStructureVerticalPlacer.applyVerticalPlacement(
start, "minecraft:monument", 0, 50, -256, 512, false, false, null, (x, z) -> 0);
assertEquals(0, repeatedOffset);
}
@@ -72,7 +72,7 @@ public class NativeStructurePostProcessorMonumentTest {
public void configuredOffsetIsRelativeToTheActualSeaLevel() {
StructureStart start = monumentStart(1337L);
int offset = NativeStructurePostProcessor.applyVerticalPlacement(
int offset = NativeStructureVerticalPlacer.applyVerticalPlacement(
start, "minecraft:monument", 3, 50, -256, 512, false, false, null, (x, z) -> 0);
assertEquals(-10, offset);
@@ -85,7 +85,7 @@ public class NativeStructurePostProcessorMonumentTest {
long seed = 1337L;
ChunkPos chunkPos = new ChunkPos(0, 0);
StructureStart initial = monumentStart(seed);
NativeStructurePostProcessor.applyVerticalPlacement(
NativeStructureVerticalPlacer.applyVerticalPlacement(
initial, "minecraft:monument", 0, 50, -256, 512, false, false, null, (x, z) -> 0);
PiecesContainer regenerated = OceanMonumentStructure.regeneratePiecesAfterLoad(
@@ -94,7 +94,7 @@ public class NativeStructurePostProcessorMonumentTest {
monumentStructure(), chunkPos, 0, regenerated);
assertEquals(39, reloaded.getBoundingBox().minY());
int offset = NativeStructurePostProcessor.applyVerticalPlacement(
int offset = NativeStructureVerticalPlacer.applyVerticalPlacement(
reloaded, "minecraft:monument", 0, 50, -256, 512, false, false, null, (x, z) -> 0);
assertEquals(-13, offset);
assertEquals(26, reloaded.getBoundingBox().minY());
@@ -105,7 +105,7 @@ public class NativeStructurePostProcessorMonumentTest {
public void impossibleSeaLevelAlignmentFailsInsteadOfClippingTheMonument() {
StructureStart start = monumentStart(1337L);
try {
NativeStructurePostProcessor.applyVerticalPlacement(
NativeStructureVerticalPlacer.applyVerticalPlacement(
start, "minecraft:monument", 0, -50, -64, 320, false, false, null, (x, z) -> 0);
} catch (IllegalStateException error) {
assertTrue(error.getMessage().contains("cannot align"));
@@ -45,7 +45,7 @@ public class NativeStructurePostProcessorScatteredFeatureTest {
BoundingBox cachedBounds = start.getBoundingBox();
BoundingBox footprint = piece.getBoundingBox();
int offset = NativeStructurePostProcessor.applyVerticalPlacement(
int offset = NativeStructureVerticalPlacer.applyVerticalPlacement(
start, "minecraft:desert_pyramid", 0, 63, -64, 320, false, false, null,
(x, z) -> x == footprint.maxX() && z == footprint.maxZ() ? 64 : 92);
@@ -63,7 +63,7 @@ public class NativeStructurePostProcessorScatteredFeatureTest {
StructureStart start = jungleStart(piece);
BoundingBox footprint = piece.getBoundingBox();
int offset = NativeStructurePostProcessor.applyVerticalPlacement(
int offset = NativeStructureVerticalPlacer.applyVerticalPlacement(
start, "minecraft:jungle_pyramid", 4, 63, -64, 320, false, false, null,
(x, z) -> x == footprint.minX() && z == footprint.minZ() ? 88 : 70);
@@ -78,9 +78,9 @@ public class NativeStructurePostProcessorScatteredFeatureTest {
TestDesertPyramidPiece piece = new TestDesertPyramidPiece(RandomSource.create(23L), 0, 0);
StructureStart start = desertStart(piece);
int initialOffset = NativeStructurePostProcessor.applyVerticalPlacement(
int initialOffset = NativeStructureVerticalPlacer.applyVerticalPlacement(
start, "minecraft:desert_pyramid", 0, 63, -64, 320, false, false, null, (x, z) -> 80);
int repeatedOffset = NativeStructurePostProcessor.applyVerticalPlacement(
int repeatedOffset = NativeStructureVerticalPlacer.applyVerticalPlacement(
start, "minecraft:desert_pyramid", 0, 63, -64, 320, false, false, null, (x, z) -> 80);
assertEquals(17, initialOffset);
@@ -93,7 +93,7 @@ public class NativeStructurePostProcessorScatteredFeatureTest {
TestDesertPyramidPiece piece = new TestDesertPyramidPiece(RandomSource.create(29L), 0, 0);
StructureStart start = desertStart(piece);
int offset = NativeStructurePostProcessor.applyVerticalPlacement(
int offset = NativeStructureVerticalPlacer.applyVerticalPlacement(
start, "minecraft:desert_pyramid", 0, 63, -64, 320, false, false, null, (x, z) -> 318);
assertEquals(241, offset);
@@ -110,12 +110,12 @@ public class NativeStructurePostProcessorScatteredFeatureTest {
StructureStart jungleStart = jungleStart(jungle);
StructureStart swampStart = swampStart(swamp);
assertFalse(NativeStructurePostProcessor.requiresSurfaceTerrain(desertStart));
assertFalse(NativeStructurePostProcessor.requiresSurfaceTerrain(jungleStart));
assertFalse(NativeStructurePostProcessor.requiresSurfaceTerrain(swampStart));
assertFalse(NativeStructureSurfaceFitter.requiresSurfaceTerrain(desertStart));
assertFalse(NativeStructureSurfaceFitter.requiresSurfaceTerrain(jungleStart));
assertFalse(NativeStructureSurfaceFitter.requiresSurfaceTerrain(swampStart));
AtomicInteger terrainQueries = new AtomicInteger();
int offset = NativeStructurePostProcessor.applyVerticalPlacement(
int offset = NativeStructureVerticalPlacer.applyVerticalPlacement(
swampStart, "minecraft:swamp_hut", 0, 63, -64, 320, false, false, null,
(x, z) -> terrainQueries.incrementAndGet());
assertEquals(0, offset);
@@ -125,7 +125,7 @@ public class NativeStructurePostProcessorScatteredFeatureTest {
@Test
public void scatteredHeightFieldMatchesTheRuntimeContract() {
Field field = NativeStructurePostProcessor.resolveScatteredHeightPositionField();
Field field = NativeStructureReflection.resolveScatteredHeightPositionField();
assertEquals(ScatteredFeaturePiece.class, field.getDeclaringClass());
assertEquals(int.class, field.getType());
@@ -61,13 +61,13 @@ public class NativeStructurePostProcessorSurfaceTerrainTest {
@Test
public void onlySurfaceBeardThinStructuresPrepareTerrain() {
assertTrue(NativeStructurePostProcessor.shouldPrepareSurfaceTerrain(
assertTrue(NativeStructureSurfaceFitter.shouldPrepareSurfaceTerrain(
TerrainAdjustment.BEARD_THIN, GenerationStep.Decoration.SURFACE_STRUCTURES));
assertFalse(NativeStructurePostProcessor.shouldPrepareSurfaceTerrain(
assertFalse(NativeStructureSurfaceFitter.shouldPrepareSurfaceTerrain(
TerrainAdjustment.BURY, GenerationStep.Decoration.SURFACE_STRUCTURES));
assertFalse(NativeStructurePostProcessor.shouldPrepareSurfaceTerrain(
assertFalse(NativeStructureSurfaceFitter.shouldPrepareSurfaceTerrain(
TerrainAdjustment.BEARD_BOX, GenerationStep.Decoration.SURFACE_STRUCTURES));
assertFalse(NativeStructurePostProcessor.shouldPrepareSurfaceTerrain(
assertFalse(NativeStructureSurfaceFitter.shouldPrepareSurfaceTerrain(
TerrainAdjustment.ENCAPSULATE, GenerationStep.Decoration.SURFACE_STRUCTURES));
}
@@ -78,86 +78,86 @@ public class NativeStructurePostProcessorSurfaceTerrainTest {
TerrainAdjustment.BURY,
TerrainAdjustment.BEARD_BOX,
TerrainAdjustment.ENCAPSULATE)) {
assertFalse(NativeStructurePostProcessor.shouldPrepareSurfaceTerrain(
assertFalse(NativeStructureSurfaceFitter.shouldPrepareSurfaceTerrain(
adjustment, GenerationStep.Decoration.UNDERGROUND_STRUCTURES));
}
}
@Test
public void surfaceAnchorIsFlushInsideAndUnchangedAtRadius() {
NativeStructurePostProcessor.SurfaceAnchor anchor = anchor(80, 2);
NativeStructureSurfaceFitter.SurfaceAnchor anchor = anchor(80, 2);
assertEquals(80, NativeStructurePostProcessor.resolveSurfaceTarget(
assertEquals(80, NativeStructureSurfaceFitter.resolveSurfaceTarget(
List.of(anchor), 2, 2, 64));
assertEquals(64, NativeStructurePostProcessor.resolveSurfaceTarget(
assertEquals(64, NativeStructureSurfaceFitter.resolveSurfaceTarget(
List.of(anchor), 16, 2, 64));
assertEquals(64, NativeStructurePostProcessor.resolveSurfaceTarget(
assertEquals(64, NativeStructureSurfaceFitter.resolveSurfaceTarget(
List.of(anchor), 17, 2, 64));
}
@Test
public void surfaceAnchorRaisesAndLowersThroughTheTaper() {
NativeStructurePostProcessor.SurfaceAnchor raised = anchor(80, 2);
NativeStructurePostProcessor.SurfaceAnchor lowered = anchor(64, 2);
NativeStructureSurfaceFitter.SurfaceAnchor raised = anchor(80, 2);
NativeStructureSurfaceFitter.SurfaceAnchor lowered = anchor(64, 2);
assertEquals(68, NativeStructurePostProcessor.resolveSurfaceTarget(
assertEquals(68, NativeStructureSurfaceFitter.resolveSurfaceTarget(
List.of(raised), 10, 2, 64));
assertEquals(76, NativeStructurePostProcessor.resolveSurfaceTarget(
assertEquals(76, NativeStructureSurfaceFitter.resolveSurfaceTarget(
List.of(lowered), 10, 2, 80));
}
@Test
public void containingRigidFloorsHaveDeterministicPriority() {
NativeStructurePostProcessor.SurfaceAnchor rigid = anchor(70, 2);
NativeStructurePostProcessor.SurfaceAnchor junction = anchor(90, 1);
NativeStructureSurfaceFitter.SurfaceAnchor rigid = anchor(70, 2);
NativeStructureSurfaceFitter.SurfaceAnchor junction = anchor(90, 1);
assertEquals(70, NativeStructurePostProcessor.resolveSurfaceTarget(
assertEquals(70, NativeStructureSurfaceFitter.resolveSurfaceTarget(
List.of(rigid, junction), 2, 2, 64));
assertEquals(70, NativeStructurePostProcessor.resolveSurfaceTarget(
assertEquals(70, NativeStructureSurfaceFitter.resolveSurfaceTarget(
List.of(junction, rigid), 2, 2, 64));
NativeStructurePostProcessor.SurfaceAnchor weakTie = anchor(48, 1);
NativeStructurePostProcessor.SurfaceAnchor strongTie = anchor(80, 2);
assertEquals(80, NativeStructurePostProcessor.resolveSurfaceTarget(
NativeStructureSurfaceFitter.SurfaceAnchor weakTie = anchor(48, 1);
NativeStructureSurfaceFitter.SurfaceAnchor strongTie = anchor(80, 2);
assertEquals(80, NativeStructureSurfaceFitter.resolveSurfaceTarget(
List.of(weakTie, strongTie), 2, 2, 64));
assertEquals(80, NativeStructurePostProcessor.resolveSurfaceTarget(
assertEquals(80, NativeStructureSurfaceFitter.resolveSurfaceTarget(
List.of(strongTie, weakTie), 2, 2, 64));
}
@Test
public void containingFootprintOverridesAnAdjacentPiecesFalloff() {
NativeStructurePostProcessor.SurfaceAnchor local =
new NativeStructurePostProcessor.SurfaceAnchor(0, 4, 0, 4, 65, 2);
NativeStructurePostProcessor.SurfaceAnchor adjacent =
new NativeStructurePostProcessor.SurfaceAnchor(5, 9, 0, 4, 80, 2);
NativeStructureSurfaceFitter.SurfaceAnchor local =
new NativeStructureSurfaceFitter.SurfaceAnchor(0, 4, 0, 4, 65, 2);
NativeStructureSurfaceFitter.SurfaceAnchor adjacent =
new NativeStructureSurfaceFitter.SurfaceAnchor(5, 9, 0, 4, 80, 2);
assertEquals(77, NativeStructurePostProcessor.resolveSurfaceTarget(
assertEquals(77, NativeStructureSurfaceFitter.resolveSurfaceTarget(
List.of(adjacent), 4, 2, 64));
assertEquals(65, NativeStructurePostProcessor.resolveSurfaceTarget(
assertEquals(65, NativeStructureSurfaceFitter.resolveSurfaceTarget(
List.of(local, adjacent), 4, 2, 64));
assertEquals(65, NativeStructurePostProcessor.resolveSurfaceTarget(
assertEquals(65, NativeStructureSurfaceFitter.resolveSurfaceTarget(
List.of(adjacent, local), 4, 2, 64));
}
@Test
public void opposingFalloffsBlendWithoutAnAbruptMidpointSeam() {
NativeStructurePostProcessor.SurfaceAnchor high =
new NativeStructurePostProcessor.SurfaceAnchor(0, 0, 0, 0, 80, 2);
NativeStructurePostProcessor.SurfaceAnchor low =
new NativeStructurePostProcessor.SurfaceAnchor(12, 12, 0, 0, 48, 2);
int previous = NativeStructurePostProcessor.resolveSurfaceTarget(
NativeStructureSurfaceFitter.SurfaceAnchor high =
new NativeStructureSurfaceFitter.SurfaceAnchor(0, 0, 0, 0, 80, 2);
NativeStructureSurfaceFitter.SurfaceAnchor low =
new NativeStructureSurfaceFitter.SurfaceAnchor(12, 12, 0, 0, 48, 2);
int previous = NativeStructureSurfaceFitter.resolveSurfaceTarget(
List.of(high, low), 0, 0, 64);
for (int x = 1; x <= 12; x++) {
int forward = NativeStructurePostProcessor.resolveSurfaceTarget(
int forward = NativeStructureSurfaceFitter.resolveSurfaceTarget(
List.of(high, low), x, 0, 64);
int reversed = NativeStructurePostProcessor.resolveSurfaceTarget(
int reversed = NativeStructureSurfaceFitter.resolveSurfaceTarget(
List.of(low, high), x, 0, 64);
assertEquals(forward, reversed);
assertTrue(Math.abs(forward - previous) <= 4);
previous = forward;
}
assertEquals(64, NativeStructurePostProcessor.resolveSurfaceTarget(
assertEquals(64, NativeStructureSurfaceFitter.resolveSurfaceTarget(
List.of(high, low), 6, 0, 64));
}
@@ -170,7 +170,7 @@ public class NativeStructurePostProcessorSurfaceTerrainTest {
put(lowered, 0, 64, 0, Blocks.GRASS_BLOCK.defaultBlockState());
put(lowered, 0, 65, 0, Blocks.DANDELION.defaultBlockState());
NativeStructurePostProcessor.applySurfaceColumn(
NativeStructureSurfaceFitter.applySurfaceColumn(
world(lowered), new BlockPos.MutableBlockPos(),
0, 0, 64, 62, -64, 319);
@@ -185,7 +185,7 @@ public class NativeStructurePostProcessorSurfaceTerrainTest {
put(raised, 0, 64, 0, Blocks.GRASS_BLOCK.defaultBlockState());
put(raised, 0, 65, 0, Blocks.DANDELION.defaultBlockState());
NativeStructurePostProcessor.applySurfaceColumn(
NativeStructureSurfaceFitter.applySurfaceColumn(
world(raised), new BlockPos.MutableBlockPos(),
0, 0, 64, 68, -64, 319);
@@ -205,7 +205,7 @@ public class NativeStructurePostProcessorSurfaceTerrainTest {
put(blocks, 0, 66, 0, log);
put(blocks, 0, 68, 0, leaves);
NativeStructurePostProcessor.applySurfaceColumn(
NativeStructureSurfaceFitter.applySurfaceColumn(
world(blocks), new BlockPos.MutableBlockPos(),
0, 0, 64, 68, -64, 319);
@@ -221,7 +221,7 @@ public class NativeStructurePostProcessorSurfaceTerrainTest {
put(blocks, 0, 64, 0, Blocks.GRAVEL.defaultBlockState());
put(blocks, 0, 65, 0, Blocks.WATER.defaultBlockState());
NativeStructurePostProcessor.applySurfaceColumn(
NativeStructureSurfaceFitter.applySurfaceColumn(
world(blocks), new BlockPos.MutableBlockPos(),
0, 0, 64, 62, -64, 319);
@@ -236,7 +236,7 @@ public class NativeStructurePostProcessorSurfaceTerrainTest {
StructureStart start = desertStart();
int minY = start.getBoundingBox().minY();
int offset = NativeStructurePostProcessor.applyVerticalShift(
int offset = NativeStructureVerticalPlacer.applyVerticalShift(
start, 0, -64, 320, true, true, null, (x, z) -> 40);
assertEquals(0, offset);
@@ -248,7 +248,7 @@ public class NativeStructurePostProcessorSurfaceTerrainTest {
StructureStart start = desertStart();
int minY = start.getBoundingBox().minY();
int offset = NativeStructurePostProcessor.applyVerticalShift(
int offset = NativeStructureVerticalPlacer.applyVerticalShift(
start, -8, -64, 320, true, true, null, (x, z) -> 40);
assertEquals(-8, offset);
@@ -262,7 +262,7 @@ public class NativeStructurePostProcessorSurfaceTerrainTest {
int maxY = start.getBoundingBox().maxY();
int expected = 40 - 1 - maxY;
int offset = NativeStructurePostProcessor.applyVerticalShift(
int offset = NativeStructureVerticalPlacer.applyVerticalShift(
start, 0, -64, 320, true, false, null, (x, z) -> 40);
assertEquals(expected, offset);
@@ -275,7 +275,7 @@ public class NativeStructurePostProcessorSurfaceTerrainTest {
int minY = start.getBoundingBox().minY();
int worldMinY = minY - 4;
int offset = NativeStructurePostProcessor.applyVerticalShift(
int offset = NativeStructureVerticalPlacer.applyVerticalShift(
start, 0, worldMinY, 320, true, false, null, (x, z) -> worldMinY);
assertEquals(-4, offset);
@@ -289,7 +289,7 @@ public class NativeStructurePostProcessorSurfaceTerrainTest {
Map<BlockPos, BlockState> blocks = new HashMap<>();
put(blocks, bounds.minX(), bounds.minY(), bounds.minZ(), Blocks.STONE.defaultBlockState());
NativeStructurePostProcessor.integrateTerrain(
NativeStructureTerrainIntegrator.integrateTerrain(
world(blocks), bounds, "minecraft:desert_pyramid", start,
new IrisStructureTerrain().setMode(IrisStructureTerrainMode.VACUUM), null);
@@ -310,7 +310,7 @@ public class NativeStructurePostProcessorSurfaceTerrainTest {
put(blocks, bounds.maxX(), bounds.maxY() + 1, bounds.maxZ(),
Blocks.STONE.defaultBlockState());
NativeStructurePostProcessor.integrateTerrain(
NativeStructureTerrainIntegrator.integrateTerrain(
world(blocks), area, "minecraft:desert_pyramid", start,
new IrisStructureTerrain()
.setMode(IrisStructureTerrainMode.FORCE_CARVE)
@@ -345,7 +345,7 @@ public class NativeStructurePostProcessorSurfaceTerrainTest {
put(blocks, firstBounds.minX(), y, z, Blocks.STONE.defaultBlockState());
put(blocks, gapX, y, z, Blocks.STONE.defaultBlockState());
NativeStructurePostProcessor.integrateTerrain(
NativeStructureTerrainIntegrator.integrateTerrain(
world(blocks), area, "minecraft:ancient_city", start,
new IrisStructureTerrain().setMode(IrisStructureTerrainMode.FORCE_CARVE), null);
@@ -372,7 +372,7 @@ public class NativeStructurePostProcessorSurfaceTerrainTest {
new BlockPos(1, 2, 0), Blocks.DEEPSLATE.defaultBlockState(), null)));
Map<Long, int[]> columns = new HashMap<>();
assertTrue(NativeStructurePostProcessor.emitTemplateColumns(
assertTrue(NativeStructureTerrainIntegrator.emitTemplateColumns(
List.of(template), new BlockPos(0, 0, 0), Rotation.NONE,
new BoundingBox(0, 0, 0, 1, 2, 0),
(x, z, minY, maxY) -> columns.put((long) x << 32 | z & 0xffffffffL,
@@ -392,7 +392,7 @@ public class NativeStructurePostProcessorSurfaceTerrainTest {
new BlockPos(0, 1, 0), Blocks.STRUCTURE_VOID.defaultBlockState(), null)));
Map<Long, int[]> columns = new HashMap<>();
assertTrue(NativeStructurePostProcessor.emitTemplateColumns(
assertTrue(NativeStructureTerrainIntegrator.emitTemplateColumns(
List.of(template), new BlockPos(0, 0, 0), Rotation.NONE,
new BoundingBox(0, 0, 0, 0, 1, 0),
(x, z, minY, maxY) -> columns.put((long) x << 32 | z & 0xffffffffL,
@@ -406,7 +406,7 @@ public class NativeStructurePostProcessorSurfaceTerrainTest {
StructureStart start = desertStart();
BoundingBox bounds = start.getPieces().getFirst().getBoundingBox();
StructureCarvingFootprint footprint = NativeStructurePostProcessor.carveFootprint(
StructureCarvingFootprint footprint = NativeStructureTerrainIntegrator.carveFootprint(
start, 4, NativeStructurePostProcessorSurfaceTerrainTest::forbiddenTemplateManager);
assertEquals(bounds.minX() - 4, footprint.minX());
@@ -423,13 +423,13 @@ public class NativeStructurePostProcessorSurfaceTerrainTest {
public void carveFootprintIsComputedOncePerStartAndPadding() {
StructureStart start = desertStart();
StructureCarvingFootprint first = NativeStructurePostProcessor.carveFootprint(
StructureCarvingFootprint first = NativeStructureTerrainIntegrator.carveFootprint(
start, 6, NativeStructurePostProcessorSurfaceTerrainTest::forbiddenTemplateManager);
StructureCarvingFootprint repeated = NativeStructurePostProcessor.carveFootprint(
StructureCarvingFootprint repeated = NativeStructureTerrainIntegrator.carveFootprint(
start, 6, NativeStructurePostProcessorSurfaceTerrainTest::forbiddenTemplateManager);
StructureCarvingFootprint widened = NativeStructurePostProcessor.carveFootprint(
StructureCarvingFootprint widened = NativeStructureTerrainIntegrator.carveFootprint(
start, 7, NativeStructurePostProcessorSurfaceTerrainTest::forbiddenTemplateManager);
StructureCarvingFootprint other = NativeStructurePostProcessor.carveFootprint(
StructureCarvingFootprint other = NativeStructureTerrainIntegrator.carveFootprint(
desertStart(), 6, NativeStructurePostProcessorSurfaceTerrainTest::forbiddenTemplateManager);
assertSame(first, repeated);
@@ -441,13 +441,13 @@ public class NativeStructurePostProcessorSurfaceTerrainTest {
public void organicCarveNeverCutsBelowTheColumnSupportingFloor() {
StructureStart start = desertStart();
BoundingBox bounds = start.getPieces().getFirst().getBoundingBox();
NativeStructurePostProcessor.OrganicCarve carve = organicCarve(start, 6);
NativeStructureTerrainIntegrator.OrganicCarve carve = organicCarve(start, 6);
BoundingBox area = new BoundingBox(
bounds.minX() - 6, bounds.minY() - 4, bounds.minZ() - 6,
bounds.maxX() + 6, bounds.maxY() + 12, bounds.maxZ() + 6);
Map<BlockPos, BlockState> blocks = fill(area);
NativeStructurePostProcessor.carveOrganicColumns(world(blocks), area, carve);
NativeStructureTerrainIntegrator.carveOrganicColumns(world(blocks), area, carve);
int centerX = bounds.minX() + bounds.getXSpan() / 2;
int centerZ = bounds.minZ() + bounds.getZSpan() / 2;
@@ -471,9 +471,9 @@ public class NativeStructurePostProcessorSurfaceTerrainTest {
Map<BlockPos, BlockState> uniformBlocks = fill(area);
Map<BlockPos, BlockState> lobedBlocks = fill(area);
NativeStructurePostProcessor.carveOrganicColumns(
NativeStructureTerrainIntegrator.carveOrganicColumns(
world(uniformBlocks), area, organicCarve(start, 10, 0D));
NativeStructurePostProcessor.carveOrganicColumns(
NativeStructureTerrainIntegrator.carveOrganicColumns(
world(lobedBlocks), area, organicCarve(start, 10, 0.85D));
int uniform = 0;
@@ -521,9 +521,9 @@ public class NativeStructurePostProcessorSurfaceTerrainTest {
Map<BlockPos, BlockState> narrowBlocks = fill(narrow);
// Each chunk context rebuilds its own noise channels from the shared start identity.
NativeStructurePostProcessor.carveOrganicColumns(
NativeStructureTerrainIntegrator.carveOrganicColumns(
world(wideBlocks), wide, organicCarve(start, 6));
NativeStructurePostProcessor.carveOrganicColumns(
NativeStructureTerrainIntegrator.carveOrganicColumns(
world(narrowBlocks), narrow, organicCarve(start, 6));
int carved = 0;
@@ -552,7 +552,7 @@ public class NativeStructurePostProcessorSurfaceTerrainTest {
int centerX = bounds.minX() + bounds.getXSpan() / 2;
int centerZ = bounds.minZ() + bounds.getZSpan() / 2;
NativeStructurePostProcessor.integrateTerrain(
NativeStructureTerrainIntegrator.integrateTerrain(
world(blocks), area, "minecraft:ancient_city", start,
new IrisStructureTerrain()
.setMode(IrisStructureTerrainMode.FORCE_CARVE)
@@ -572,21 +572,21 @@ public class NativeStructurePostProcessorSurfaceTerrainTest {
@Test
public void sparseStiltGridIsDeterministicAndPreflightsGround() {
assertTrue(NativeStructurePostProcessor.isStiltColumn(0, 0, 4));
assertTrue(NativeStructurePostProcessor.isStiltColumn(-4, 8, 4));
assertFalse(NativeStructurePostProcessor.isStiltColumn(1, 0, 4));
assertTrue(NativeStructurePostProcessor.isStiltColumn(1, 1, 1));
assertTrue(NativeStructureFoundationBuilder.isStiltColumn(0, 0, 4));
assertTrue(NativeStructureFoundationBuilder.isStiltColumn(-4, 8, 4));
assertFalse(NativeStructureFoundationBuilder.isStiltColumn(1, 0, 4));
assertTrue(NativeStructureFoundationBuilder.isStiltColumn(1, 1, 1));
Map<BlockPos, BlockState> blocks = new HashMap<>();
put(blocks, 0, 7, 0, Blocks.DEEPSLATE.defaultBlockState());
put(blocks, 0, 8, 0, Blocks.SCULK_VEIN.defaultBlockState());
BlockPos.MutableBlockPos position = new BlockPos.MutableBlockPos();
assertEquals(7, NativeStructurePostProcessor.findStiltAnchorY(
assertEquals(7, NativeStructureFoundationBuilder.findStiltAnchorY(
world(blocks), 0, 0, 10, 2, -64, -64, position));
assertEquals(Integer.MIN_VALUE, NativeStructurePostProcessor.findStiltAnchorY(
assertEquals(Integer.MIN_VALUE, NativeStructureFoundationBuilder.findStiltAnchorY(
world(blocks), 0, 0, 10, 1, -64, -64, position));
assertEquals(Integer.MIN_VALUE, NativeStructurePostProcessor.findStiltAnchorY(
assertEquals(Integer.MIN_VALUE, NativeStructureFoundationBuilder.findStiltAnchorY(
world(new HashMap<>()), 0, 0, 10, 64, -64, -64, position));
}
@@ -635,7 +635,7 @@ public class NativeStructurePostProcessorSurfaceTerrainTest {
@Test
public void singlePoolTemplateFieldMatchesTheRuntimeContract() {
Field field = NativeStructurePostProcessor.resolveSinglePoolTemplateField();
Field field = NativeStructureReflection.resolveSinglePoolTemplateField();
assertEquals(SinglePoolElement.class, field.getDeclaringClass());
assertEquals(Either.class, field.getType());
@@ -649,12 +649,12 @@ public class NativeStructurePostProcessorSurfaceTerrainTest {
public void runtimeTemplatesAndLegacyAirUseTheExactContract() {
StructureTemplate runtimeTemplate = new StructureTemplate();
assertEquals(runtimeTemplate, NativeStructurePostProcessor.resolveTemplateReference(
assertEquals(runtimeTemplate, NativeStructureReflection.resolveTemplateReference(
Either.right(runtimeTemplate), null));
assertFalse(NativeStructurePostProcessor.shouldClearLegacyAir(79, 80, false));
assertTrue(NativeStructurePostProcessor.shouldClearLegacyAir(80, 80, false));
assertTrue(NativeStructurePostProcessor.shouldClearLegacyAir(96, 80, false));
assertFalse(NativeStructurePostProcessor.shouldClearLegacyAir(96, 80, true));
assertFalse(NativeStructureTerrainIntegrator.shouldClearLegacyAir(79, 80, false));
assertTrue(NativeStructureTerrainIntegrator.shouldClearLegacyAir(80, 80, false));
assertTrue(NativeStructureTerrainIntegrator.shouldClearLegacyAir(96, 80, false));
assertFalse(NativeStructureTerrainIntegrator.shouldClearLegacyAir(96, 80, true));
}
@Test
@@ -687,7 +687,7 @@ public class NativeStructurePostProcessorSurfaceTerrainTest {
blocks.put(belowFloorPosition, Blocks.DIRT.defaultBlockState());
blocks.put(outsidePosition, Blocks.DIRT.defaultBlockState());
NativeStructurePostProcessor.clearTemplateAir(
NativeStructureTerrainIntegrator.clearTemplateAir(
world(blocks), template, origin, 80, settings);
assertEquals(Blocks.AIR.defaultBlockState(), blocks.get(clearPosition));
@@ -699,11 +699,11 @@ public class NativeStructurePostProcessorSurfaceTerrainTest {
public void unrelatedPieceBoundsAreRejectedBeforeTemplateScanning() {
BoundingBox area = new BoundingBox(0, -64, 0, 15, 319, 15);
assertTrue(NativeStructurePostProcessor.intersects(
assertTrue(NativeStructureTerrainIntegrator.intersects(
new BoundingBox(15, 60, 15, 30, 90, 30), area));
assertFalse(NativeStructurePostProcessor.intersects(
assertFalse(NativeStructureTerrainIntegrator.intersects(
new BoundingBox(16, 60, 16, 30, 90, 30), area));
assertFalse(NativeStructurePostProcessor.intersects(
assertFalse(NativeStructureTerrainIntegrator.intersects(
new BoundingBox(0, 320, 0, 15, 350, 15), area));
}
@@ -717,23 +717,22 @@ public class NativeStructurePostProcessorSurfaceTerrainTest {
BlockState log = Blocks.OAK_LOG.defaultBlockState();
blocks.put(origin, log);
NativeStructurePostProcessor.clearTemplateAir(world(blocks), template, origin, 80, settings);
NativeStructureTerrainIntegrator.clearTemplateAir(world(blocks), template, origin, 80, settings);
assertEquals(log, blocks.get(origin));
}
private static NativeStructurePostProcessor.SurfaceAnchor anchor(int meetY, int strength) {
return new NativeStructurePostProcessor.SurfaceAnchor(0, 4, 0, 4, meetY, strength);
private static NativeStructureSurfaceFitter.SurfaceAnchor anchor(int meetY, int strength) {
return new NativeStructureSurfaceFitter.SurfaceAnchor(0, 4, 0, 4, meetY, strength);
}
private static NativeStructurePostProcessor.OrganicCarve organicCarve(StructureStart start,
int horizontalPadding) {
private static NativeStructureTerrainIntegrator.OrganicCarve organicCarve(
StructureStart start, int horizontalPadding) {
return organicCarve(start, horizontalPadding, 0.85D);
}
private static NativeStructurePostProcessor.OrganicCarve organicCarve(StructureStart start,
int horizontalPadding,
double lobeStrength) {
private static NativeStructureTerrainIntegrator.OrganicCarve organicCarve(
StructureStart start, int horizontalPadding, double lobeStrength) {
IrisStructureTerrain terrain = new IrisStructureTerrain()
.setMode(IrisStructureTerrainMode.FORCE_CARVE)
.setShape(IrisStructureCarveShape.ERODED)
@@ -743,8 +742,8 @@ public class NativeStructurePostProcessorSurfaceTerrainTest {
.setErosionStrength(1D)
.setErosionFrequency(0.05D)
.setLobeStrength(lobeStrength);
return NativeStructurePostProcessor.organicCarve(
NativeStructurePostProcessor.carveFootprint(start, horizontalPadding,
return NativeStructureTerrainIntegrator.organicCarve(
NativeStructureTerrainIntegrator.carveFootprint(start, horizontalPadding,
NativeStructurePostProcessorSurfaceTerrainTest::forbiddenTemplateManager),
terrain, IrisStructureCarveShape.ERODED, TEST_SEED);
}
@@ -778,8 +777,8 @@ public class NativeStructurePostProcessorSurfaceTerrainTest {
SLAB_WIDTH - 1 + SLAB_PADDING, transectY, SLAB_DEPTH - 1);
Map<BlockPos, BlockState> blocks = fill(area);
NativeStructurePostProcessor.carveOrganicColumns(world(blocks), area,
NativeStructurePostProcessor.organicCarve(
NativeStructureTerrainIntegrator.carveOrganicColumns(world(blocks), area,
NativeStructureTerrainIntegrator.organicCarve(
footprint, terrain, IrisStructureCarveShape.ERODED, TEST_SEED));
int[] depths = new int[SLAB_DEPTH];
@@ -11,53 +11,53 @@ import static org.junit.Assert.assertTrue;
public class NativeStructurePostProcessorVegetationTest {
@Test
public void surfaceStructuresClearTreeColumnsAutomatically() {
assertTrue(NativeStructurePostProcessor.shouldClearVegetationColumn(100, 100, false));
assertTrue(NativeStructurePostProcessor.shouldClearVegetationColumn(116, 100, false));
assertTrue(NativeStructureVegetationClearer.shouldClearVegetationColumn(100, 100, false));
assertTrue(NativeStructureVegetationClearer.shouldClearVegetationColumn(116, 100, false));
}
@Test
public void buriedStructuresPreserveUnrelatedSurfaceForest() {
assertFalse(NativeStructurePostProcessor.shouldClearVegetationColumn(99, 100, false));
assertFalse(NativeStructurePostProcessor.shouldClearVegetationColumn(20, 100, false));
assertFalse(NativeStructureVegetationClearer.shouldClearVegetationColumn(99, 100, false));
assertFalse(NativeStructureVegetationClearer.shouldClearVegetationColumn(20, 100, false));
}
@Test
public void explicitVegetationOptionForcesUnusualPlacementCleanup() {
assertTrue(NativeStructurePostProcessor.shouldClearVegetationColumn(20, 100, true));
assertTrue(NativeStructureVegetationClearer.shouldClearVegetationColumn(20, 100, true));
}
@Test
public void surfaceStructuresPreserveVegetationUnlessConfigured() {
assertFalse(NativeStructurePostProcessor.shouldClearEntireVegetationFootprint(
assertFalse(NativeStructureVegetationClearer.shouldClearEntireVegetationFootprint(
GenerationStep.Decoration.SURFACE_STRUCTURES, false));
assertTrue(NativeStructurePostProcessor.shouldClearEntireVegetationFootprint(
assertTrue(NativeStructureVegetationClearer.shouldClearEntireVegetationFootprint(
GenerationStep.Decoration.SURFACE_STRUCTURES, true));
}
@Test
public void undergroundStructuresPreserveSurfaceVegetationUnlessConfigured() {
assertFalse(NativeStructurePostProcessor.shouldClearEntireVegetationFootprint(
assertFalse(NativeStructureVegetationClearer.shouldClearEntireVegetationFootprint(
GenerationStep.Decoration.UNDERGROUND_STRUCTURES, false));
assertTrue(NativeStructurePostProcessor.shouldClearEntireVegetationFootprint(
assertTrue(NativeStructureVegetationClearer.shouldClearEntireVegetationFootprint(
GenerationStep.Decoration.UNDERGROUND_STRUCTURES, true));
}
@Test
public void allUndergroundGenerationStepsShareOneClassification() {
assertTrue(NativeStructurePostProcessor.isUndergroundStep(
assertTrue(NativeStructureVegetationClearer.isUndergroundStep(
GenerationStep.Decoration.UNDERGROUND_STRUCTURES));
assertTrue(NativeStructurePostProcessor.isUndergroundStep(
assertTrue(NativeStructureVegetationClearer.isUndergroundStep(
GenerationStep.Decoration.UNDERGROUND_DECORATION));
assertTrue(NativeStructurePostProcessor.isUndergroundStep(
assertTrue(NativeStructureVegetationClearer.isUndergroundStep(
GenerationStep.Decoration.STRONGHOLDS));
assertFalse(NativeStructurePostProcessor.isUndergroundStep(
assertFalse(NativeStructureVegetationClearer.isUndergroundStep(
GenerationStep.Decoration.SURFACE_STRUCTURES));
}
@Test
public void undergroundStructuresUseTheLowestTerrainColumn() {
BoundingBox bounds = new BoundingBox(0, 60, 0, 1, 80, 0);
int offset = NativeStructurePostProcessor.resolveBuriedOffset(
int offset = NativeStructureVerticalPlacer.resolveBuriedOffset(
bounds, 0, -64, 320, (x, z) -> x == 1 ? 76 : 100);
assertEquals(-5, offset);
}
@@ -65,7 +65,7 @@ public class NativeStructurePostProcessorVegetationTest {
@Test
public void undergroundBurialClampsToTheWorldFloorInsteadOfFailing() {
BoundingBox bounds = new BoundingBox(0, 60, 0, 1, 80, 0);
assertEquals(-2, NativeStructurePostProcessor.resolveBuriedOffset(
assertEquals(-2, NativeStructureVerticalPlacer.resolveBuriedOffset(
bounds, 0, 58, 320, (x, z) -> x == 1 ? 76 : 100));
}
}
@@ -27,28 +27,23 @@ import art.arcane.iris.engine.framework.EnginePlatformHooks;
import art.arcane.iris.engine.framework.EngineWorldManagerProvider;
import art.arcane.iris.core.splash.IrisSplashComposer;
import art.arcane.iris.core.IrisSettings;
import art.arcane.iris.core.IrisWorldStorage;
import art.arcane.iris.core.IrisWorlds;
import art.arcane.iris.core.BukkitWorldReconciler;
import art.arcane.iris.core.IrisWorldGeneratorResolver;
import art.arcane.iris.core.PendingWorldDeleteQueue;
import art.arcane.iris.core.SettingsHotloadWatch;
import art.arcane.iris.core.ServerConfigurator;
import art.arcane.iris.core.datapack.DatapackIngestService;
import art.arcane.iris.core.lifecycle.PaperLibBootstrap;
import art.arcane.iris.core.lifecycle.WorldLifecycleService;
import art.arcane.iris.core.runtime.BukkitEnginePlatformHooks;
import art.arcane.iris.core.runtime.TransientWorldCleanupSupport;
import art.arcane.iris.core.runtime.WorldRuntimeControlService;
import art.arcane.iris.core.lifecycle.WorldLifecycleStaging;
import art.arcane.iris.api.terrain.IrisTerrainService;
import art.arcane.iris.core.link.IrisPapiInstaller;
import art.arcane.iris.core.link.IrisPapiListener;
import art.arcane.iris.core.link.IrisPapiState;
import art.arcane.iris.core.localization.IrisLanguage;
import art.arcane.iris.core.link.MultiverseCoreLink;
import art.arcane.iris.core.loader.IrisData;
import art.arcane.iris.core.nms.INMS;
import art.arcane.iris.core.pack.BrokenPackException;
import art.arcane.iris.core.pack.PackValidationRegistry;
import art.arcane.iris.core.pack.PackValidationResult;
import art.arcane.iris.core.pack.PackValidator;
import art.arcane.iris.core.gui.BukkitGuiHost;
import art.arcane.iris.core.gui.PregeneratorJob;
import art.arcane.iris.core.service.EditSVC;
@@ -61,19 +56,14 @@ 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;
import art.arcane.iris.engine.platform.BukkitChunkGenerator;
import art.arcane.iris.core.safeguard.IrisSafeguard;
import art.arcane.iris.engine.platform.PlatformChunkGenerator;
import art.arcane.iris.platform.bukkit.BukkitPlatform;
import art.arcane.iris.platform.bukkit.BukkitEnvironment;
import art.arcane.iris.spi.IrisLogging;
import art.arcane.iris.spi.IrisPlatforms;
import art.arcane.iris.spi.IrisServices;
import art.arcane.iris.spi.LogLevel;
import art.arcane.volmlib.integration.ReloadAware;
import art.arcane.volmlib.util.bukkit.WorldIdentity;
import art.arcane.volmlib.util.bukkit.papi.PlaceholderRegistration;
import art.arcane.volmlib.util.collection.KList;
import art.arcane.volmlib.util.collection.KMap;
@@ -96,17 +86,13 @@ import art.arcane.iris.util.common.plugin.VolmitPlugin;
import art.arcane.iris.util.common.plugin.VolmitSender;
import art.arcane.iris.util.common.plugin.chunk.ChunkTickets;
import art.arcane.iris.util.common.scheduling.J;
import art.arcane.iris.util.common.misc.ServerProperties;
import art.arcane.iris.util.simd.SimdSupport;
import art.arcane.volmlib.util.scheduling.Queue;
import art.arcane.volmlib.util.scheduling.ShurikenQueue;
import lombok.NonNull;
import org.bukkit.Bukkit;
import org.bukkit.GameMode;
import org.bukkit.Location;
import org.bukkit.NamespacedKey;
import org.bukkit.World;
import org.bukkit.WorldCreator;
import org.bukkit.block.data.BlockData;
import org.bukkit.command.Command;
import org.bukkit.command.CommandSender;
@@ -121,23 +107,14 @@ import org.bukkit.plugin.Plugin;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.io.BufferedInputStream;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileOutputStream;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.io.PrintWriter;
import java.lang.annotation.Annotation;
import java.lang.reflect.Modifier;
import java.net.URI;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Date;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.CompletableFuture;
@@ -145,7 +122,6 @@ import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.function.Predicate;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@@ -165,8 +141,6 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
public static ChunkTickets tickets;
private static VolmitSender sender;
private static Thread shutdownHook;
private static File settingsFile;
private static final String PENDING_WORLD_DELETE_FILE = "pending-world-deletes.txt";
private static final StackWalker DEBUG_STACK_WALKER = StackWalker.getInstance();
static {
try {
@@ -178,11 +152,18 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
}
}
private static final Object TEARDOWN_LOCK = new Object();
private final AtomicBoolean alreadyDrained = new AtomicBoolean(false);
private final AtomicBoolean servicesDisabled = new AtomicBoolean(false);
private final AtomicBoolean sharedRuntimeClosed = new AtomicBoolean(false);
private volatile PlaceholderRegistration papiRegistration;
private volatile IrisPapiListener papiListener;
private volatile IrisPapiState papiState;
private KMap<Class<? extends IrisService>, IrisService> services;
private final IrisWorldGeneratorResolver generatorResolver = new IrisWorldGeneratorResolver(this);
private final BukkitWorldReconciler worldReconciler = new BukkitWorldReconciler(this);
private final PendingWorldDeleteQueue pendingWorldDeletes = new PendingWorldDeleteQueue(this);
private volatile SettingsHotloadWatch settingsHotloadWatch;
public static VolmitSender getSender() {
if (sender == null) {
@@ -292,69 +273,6 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
}
}
public static File getCached(String name, String url) {
String h = IO.hash(name + "@" + url);
File f = Iris.instance.getDataFile("cache", h.substring(0, 2), h.substring(3, 5), h);
if (!f.exists()) {
try (BufferedInputStream in = new BufferedInputStream(URI.create(url).toURL().openStream()); FileOutputStream fileOutputStream = new FileOutputStream(f)) {
byte[] dataBuffer = new byte[1024];
int bytesRead;
while ((bytesRead = in.read(dataBuffer, 0, 1024)) != -1) {
fileOutputStream.write(dataBuffer, 0, bytesRead);
Iris.verbose("Aquiring " + name);
}
} catch (IOException e) {
Iris.reportError(e);
}
}
return f.exists() ? f : null;
}
public static String getNonCached(String name, String url) {
String h = IO.hash(name + "*" + url);
File f = Iris.instance.getDataFile("cache", h.substring(0, 2), h.substring(3, 5), h);
try (BufferedInputStream in = new BufferedInputStream(URI.create(url).toURL().openStream()); FileOutputStream fileOutputStream = new FileOutputStream(f)) {
byte[] dataBuffer = new byte[1024];
int bytesRead;
while ((bytesRead = in.read(dataBuffer, 0, 1024)) != -1) {
fileOutputStream.write(dataBuffer, 0, bytesRead);
}
} catch (IOException e) {
Iris.reportError(e);
}
try {
return IO.readAll(f);
} catch (IOException e) {
Iris.reportError(e);
}
return "";
}
public static File getNonCachedFile(String name, String url) {
String h = IO.hash(name + "*" + url);
File f = Iris.instance.getDataFile("cache", h.substring(0, 2), h.substring(3, 5), h);
Iris.verbose("Download " + name + " -> " + url);
try (BufferedInputStream in = new BufferedInputStream(URI.create(url).toURL().openStream()); FileOutputStream fileOutputStream = new FileOutputStream(f)) {
byte[] dataBuffer = new byte[1024];
int bytesRead;
while ((bytesRead = in.read(dataBuffer, 0, 1024)) != -1) {
fileOutputStream.write(dataBuffer, 0, bytesRead);
}
fileOutputStream.flush();
} catch (IOException e) {
e.printStackTrace();
Iris.reportError(e);
}
return f;
}
public static void warn(String format, Object... objs) {
msg(C.YELLOW + safeFormat(format, objs));
}
@@ -616,6 +534,10 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
private void enable() {
alreadyDrained.set(false);
servicesDisabled.set(false);
sharedRuntimeClosed.set(false);
MultiBurst.burst.reopen();
MultiBurst.ioBurst.reopen();
IrisLanguage.initialize();
PaperLibBootstrap.install();
SimdSupport.install();
@@ -635,7 +557,7 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
IrisServices.register(IrisCompat.class, compat);
ServerConfigurator.configure();
IrisToolbelt.applyPregenPerformanceProfile();
validateAllPacks();
generatorResolver.validateAllPacks();
IrisSafeguard.execute();
getSender().setTag(getTag());
splash();
@@ -649,37 +571,38 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
IrisServices.register(EnginePlatformHooks.class, new BukkitEnginePlatformHooks());
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");
IrisServices.register(art.arcane.iris.core.runtime.WorldDeletionQueue.class, (art.arcane.iris.core.runtime.WorldDeletionQueue) pendingWorldDeletes::queueWorldDeletionOnStartup);
SettingsHotloadWatch watch = new SettingsHotloadWatch(getDataFile("settings.json"));
settingsHotloadWatch = watch;
configHotloadEngine = new ConfigHotloadEngine(
Iris::isSettingsFile,
Iris::knownSettingsFiles,
Iris::readSettingsContent,
Iris::normalizeSettingsContent
watch::isSettingsFile,
watch::knownSettingsFiles,
watch::readSettingsContent,
watch::normalizeSettingsContent
);
configHotloadEngine.configure(3_000L, List.of(settingsFile), List.of());
configHotloadEngine.configure(3_000L, List.of(watch.settingsFile()), List.of());
services.values().forEach(IrisService::onEnable);
services.values().forEach(this::registerListener);
addShutdownHook();
processPendingStartupWorldDeletes();
pendingWorldDeletes.processPendingStartupWorldDeletes();
WorldLifecycleService.get();
WorldRuntimeControlService.get();
if (J.isFolia()) {
J.s(() -> checkForBukkitWorlds(s -> true), 1);
J.s(() -> worldReconciler.checkForBukkitWorlds(s -> true), 1);
}
J.s(() -> {
J.a(() -> IO.delete(getTemp()));
J.a(this::bstats);
J.ar(this::checkConfigHotload, 60);
J.ar(() -> settingsHotloadWatch.checkConfigHotload(configHotloadEngine), 60);
J.sr(this::tickQueue, 0);
J.s(this::setupPapi);
J.a(DatapackIngestService::autoIngestOnStartup, 60);
autoStartStudio();
if (!J.isFolia()) {
checkForBukkitWorlds(s -> true);
worldReconciler.checkForBukkitWorlds(s -> true);
}
IrisToolbelt.retainMantleDataForSlice(String.class.getCanonicalName());
IrisToolbelt.retainMantleDataForSlice(BlockData.class.getCanonicalName());
@@ -696,24 +619,7 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
return;
}
}
shutdownHook = new Thread(() -> {
if (alreadyDrained.compareAndSet(false, true)) {
try {
Bukkit.getWorlds()
.stream()
.map(World::getGenerator)
.filter(PlatformChunkGenerator.class::isInstance)
.map(PlatformChunkGenerator.class::cast)
.forEach(PlatformChunkGenerator::close);
} catch (Throwable e) {
Iris.reportError("Failed to close Iris world generators from the JVM shutdown hook.", e);
}
}
MultiBurst.burst.close();
MultiBurst.ioBurst.close();
IrisServices.clear();
}, "Iris-ShutdownHook");
shutdownHook = new Thread(() -> teardownRuntime("shutdown-hook", 30L), "Iris-ShutdownHook");
try {
Runtime.getRuntime().addShutdownHook(shutdownHook);
} catch (IllegalStateException ex) {
@@ -721,247 +627,8 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
}
}
public void checkForBukkitWorlds(Predicate<String> filter) {
try {
KList<String> deferredStartupWorlds = new KList<>();
IrisWorlds.readBukkitWorlds().forEach((s, generator) -> {
try {
NamespacedKey worldKey = IrisWorldStorage.keyFromName(s);
if (WorldIdentity.resolve(worldKey).isPresent() || !filter.test(s)) return;
Iris.info("Loading World: %s | Generator: %s", s, generator);
ChunkGenerator gen = getDefaultWorldGenerator(s, generator);
IrisDimension dim = loadDimension(s, generator);
assert dim != null && gen != null;
Iris.info(C.LIGHT_PURPLE + "Preparing Spawn for " + s + "' using Iris:" + generator + "...");
WorldCreator c = WorldCreator.ofKey(worldKey)
.generator(gen)
.environment(BukkitEnvironment.from(dim.getEnvironment()));
Long stagedSeed = IrisWorlds.readBukkitWorldSeed(s);
if (stagedSeed != null) {
c.seed(stagedSeed);
}
INMS.get().createWorld(c);
Iris.info(C.LIGHT_PURPLE + "Loaded " + s + "!");
} catch (Throwable e) {
if (containsCreateWorldUnsupportedOperation(e)) {
if (J.isFolia()) {
if (!deferredStartupWorlds.contains(s)) {
deferredStartupWorlds.add(s);
}
return;
}
Iris.error("Failed to load world " + s + "!");
Iris.error("This server denied Bukkit.createWorld for \"" + s + "\" at the current startup phase.");
Iris.error("Ensure Iris is loaded at STARTUP and restart after staging worlds in bukkit.yml.");
reportError("Failed to load staged startup world \"" + s + "\".", e);
return;
}
reportError("Failed to load startup world \"" + s + "\".", e);
}
});
if (!deferredStartupWorlds.isEmpty()) {
Iris.warn("Staged Iris worlds could not load on Folia: %s", String.join(", ", deferredStartupWorlds));
Iris.warn("Bukkit.createWorld is unsupported on this server and the Iris runtime world backend is unavailable (%s).", WorldLifecycleService.get().capabilities().paperLikeResolution());
}
} catch (Throwable e) {
reportError("Failed while loading startup Iris worlds.", e);
}
}
private static boolean containsCreateWorldUnsupportedOperation(Throwable throwable) {
Throwable cursor = throwable;
while (cursor != null) {
if (cursor instanceof UnsupportedOperationException || cursor instanceof IllegalStateException) {
for (StackTraceElement element : cursor.getStackTrace()) {
if ("org.bukkit.craftbukkit.CraftServer".equals(element.getClassName())
&& "createWorld".equals(element.getMethodName())) {
return true;
}
}
}
cursor = cursor.getCause();
}
return false;
}
public static synchronized int queueWorldDeletionOnStartup(Collection<String> worldNames) throws IOException {
if (instance == null || worldNames == null || worldNames.isEmpty()) {
return 0;
}
LinkedHashMap<String, String> queue = loadPendingWorldDeleteMap();
int before = queue.size();
for (String worldName : worldNames) {
String normalized = normalizeWorldName(worldName);
if (normalized == null) {
continue;
}
queue.putIfAbsent(normalized.toLowerCase(Locale.ROOT), normalized);
}
if (queue.size() != before) {
writePendingWorldDeleteMap(queue);
}
return queue.size() - before;
}
private void processPendingStartupWorldDeletes() {
try {
try {
int unregistered = art.arcane.iris.core.tools.IrisCreator.removeTransientStudioWorldsFromBukkitYml();
if (unregistered > 0) {
Iris.info("Unregistered " + unregistered + " transient studio world(s) from bukkit.yml on startup.");
}
} catch (Throwable e) {
Iris.reportError("Failed to unregister transient studio worlds from bukkit.yml on startup.", e);
}
LinkedHashMap<String, String> queue = loadPendingWorldDeleteMap();
for (String transientStudioWorld : TransientWorldCleanupSupport.collectTransientStudioWorldNames(IrisWorldStorage.levelRoot())) {
queue.putIfAbsent(transientStudioWorld.toLowerCase(Locale.ROOT), transientStudioWorld);
}
if (queue.isEmpty()) {
return;
}
LinkedHashMap<String, String> remaining = new LinkedHashMap<>();
for (String worldName : queue.values()) {
if (worldName.equalsIgnoreCase(ServerProperties.LEVEL_NAME)) {
Iris.warn("Skipping queued deletion for \"" + worldName + "\" because it is configured as level-name.");
continue;
}
NamespacedKey worldKey = IrisWorldStorage.keyFromName(worldName);
World loaded = WorldIdentity.resolve(worldKey).orElse(null);
if (loaded != null) {
if (TransientWorldCleanupSupport.isTransientStudioWorldName(worldName)) {
try {
PlatformChunkGenerator generator = IrisToolbelt.access(loaded);
if (generator != null) {
generator.close();
}
IrisToolbelt.evacuate(loaded);
Bukkit.unloadWorld(loaded, false);
Iris.info("Unloaded leftover studio world \"" + worldName + "\" for deletion.");
} catch (Throwable e) {
Iris.reportError("Failed to unload leftover studio world \"" + worldName + "\".", e);
}
if (WorldIdentity.resolve(worldKey).isPresent()) {
Iris.warn("Studio world \"" + worldName + "\" is still loaded after unload; will retry next startup.");
remaining.put(worldName.toLowerCase(Locale.ROOT), worldName);
continue;
}
} else {
Iris.warn("Skipping queued deletion for \"" + worldName + "\" because it is currently loaded.");
remaining.put(worldName.toLowerCase(Locale.ROOT), worldName);
continue;
}
}
boolean foundAny = false;
boolean deletedAll = true;
for (String familyWorldName : TransientWorldCleanupSupport.worldFamilyNames(worldName)) {
File worldFolder = IrisWorldStorage.dimensionRoot(familyWorldName);
if (!worldFolder.exists()) {
continue;
}
foundAny = true;
IO.delete(worldFolder);
if (worldFolder.exists()) {
deletedAll = false;
Iris.warn("Failed to delete queued world folder \"" + familyWorldName + "\". Retrying on next startup.");
} else {
Iris.info("Deleted queued world folder \"" + familyWorldName + "\".");
}
}
if (!foundAny) {
Iris.info("Queued world deletion skipped for \"" + worldName + "\" (folder missing).");
continue;
}
if (!deletedAll) {
remaining.put(worldName.toLowerCase(Locale.ROOT), worldName);
continue;
}
}
writePendingWorldDeleteMap(remaining);
} catch (Throwable e) {
Iris.error("Failed to process queued startup world deletions.");
reportError(e);
e.printStackTrace();
}
}
private static LinkedHashMap<String, String> loadPendingWorldDeleteMap() throws IOException {
LinkedHashMap<String, String> queue = new LinkedHashMap<>();
if (instance == null) {
return queue;
}
File queueFile = instance.getDataFile(PENDING_WORLD_DELETE_FILE);
if (!queueFile.exists()) {
return queue;
}
try (BufferedReader reader = new BufferedReader(new FileReader(queueFile))) {
String line;
while ((line = reader.readLine()) != null) {
String normalized = normalizeWorldName(line);
if (normalized == null) {
continue;
}
queue.putIfAbsent(normalized.toLowerCase(Locale.ROOT), normalized);
}
}
return queue;
}
private static void writePendingWorldDeleteMap(Map<String, String> queue) throws IOException {
if (instance == null) {
return;
}
File queueFile = instance.getDataFile(PENDING_WORLD_DELETE_FILE);
if (queue.isEmpty()) {
if (queueFile.exists()) {
IO.delete(queueFile);
}
return;
}
File parent = queueFile.getParentFile();
if (parent != null && !parent.exists() && !parent.mkdirs()) {
throw new IOException("Failed to create queue directory: " + parent.getAbsolutePath());
}
try (PrintWriter writer = new PrintWriter(new FileWriter(queueFile))) {
for (String worldName : queue.values()) {
writer.println(worldName);
}
}
}
@Nullable
private static String normalizeWorldName(String worldName) {
if (worldName == null) {
return null;
}
String trimmed = worldName.trim();
if (trimmed.isEmpty()) {
return null;
}
return trimmed;
public BukkitWorldReconciler worldReconciler() {
return worldReconciler;
}
private void autoStartStudio() {
@@ -1012,13 +679,7 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
public void onDisable() {
teardownPapi();
if (IrisSafeguard.isForceShutdown()) return;
if (alreadyDrained.compareAndSet(false, true)) {
drainWorldGenerators("onDisable", 30L);
}
if (services != null) {
services.values().forEach(IrisService::onDisable);
}
IrisServices.clear();
teardownRuntime("onDisable", 30L);
if (BukkitPlatform.hasHud()) {
BukkitPlatform.hudSlots().shutdown();
BukkitPlatform.hudLanes().shutdown();
@@ -1039,12 +700,54 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
@Override
public void onPreUnload(ReloadAware.PreUnloadReason reason) {
teardownPapi();
if (!alreadyDrained.compareAndSet(false, true)) {
if (alreadyDrained.get()) {
Iris.info("Pre-unload hook skipped; Iris already drained.");
return;
}
Iris.info("BileTools pre-unload hook fired (" + reason + "). Freezing all Iris worlds.");
drainWorldGenerators("pre-unload:" + reason, 45L);
drainOnce("pre-unload:" + reason, 45L);
}
/**
* Drains the world generators exactly once. Serialized against the JVM shutdown hook so a
* second caller cannot rip the pools or services out from under an in-flight drain.
*/
private void drainOnce(String reason, long timeoutSeconds) {
synchronized (TEARDOWN_LOCK) {
if (alreadyDrained.compareAndSet(false, true)) {
drainWorldGenerators(reason, timeoutSeconds);
}
}
}
/**
* Full teardown: generators, then services, then the shared pools and the service map.
* Both onDisable and the JVM shutdown hook route through here; whichever runs second is a no-op.
*/
private void teardownRuntime(String reason, long timeoutSeconds) {
synchronized (TEARDOWN_LOCK) {
if (alreadyDrained.compareAndSet(false, true)) {
drainWorldGenerators(reason, timeoutSeconds);
}
if (services != null && servicesDisabled.compareAndSet(false, true)) {
for (IrisService service : services.values()) {
try {
service.onDisable();
} catch (Throwable e) {
Iris.reportError("Failed to disable " + service.getClass().getSimpleName() + ".", e);
}
}
}
if (!sharedRuntimeClosed.compareAndSet(false, true)) {
return;
}
J.attempt(MultiBurst.burst::close);
J.attempt(MultiBurst.ioBurst::close);
IrisServices.clear();
}
}
private void drainWorldGenerators(String reason, long timeoutSeconds) {
@@ -1164,61 +867,6 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
return IrisSafeguard.mode().tag(subTag);
}
private void checkConfigHotload() {
if (configHotloadEngine == null) {
return;
}
for (File file : configHotloadEngine.pollTouchedFiles()) {
configHotloadEngine.processFileChange(file, ignored -> {
IrisSettings.invalidate();
IrisSettings.get();
IrisLanguage.reload();
return true;
}, ignored -> Iris.info("Hotloaded settings.json "));
}
IrisLanguage.update();
}
private static boolean isSettingsFile(File file) {
if (file == null || settingsFile == null) {
return false;
}
return settingsFile.getAbsoluteFile().equals(file.getAbsoluteFile());
}
private static List<File> knownSettingsFiles() {
if (settingsFile == null) {
return List.of();
}
return List.of(settingsFile);
}
private static String readSettingsContent(File file) {
if (file == null || !file.exists() || !file.isFile()) {
return null;
}
try {
return IO.readAll(file);
} catch (Throwable ex) {
Iris.warn("Failed to read settings file %s: %s%s",
file.getAbsolutePath(),
ex.getClass().getSimpleName(),
ex.getMessage() == null ? "" : " - " + ex.getMessage());
Iris.reportError(ex);
return null;
}
}
private static String normalizeSettingsContent(String text) {
if (text == null) {
return null;
}
return text.replace("\r\n", "\n").trim();
}
private void tickQueue() {
synchronized (Iris.syncJobs) {
if (!Iris.syncJobs.hasNext()) {
@@ -1256,117 +904,12 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
@Nullable
@Override
public BiomeProvider getDefaultBiomeProvider(@NotNull String worldName, @Nullable String id) {
org.bukkit.generator.BiomeProvider stagedBiomeProvider = WorldLifecycleStaging.consumeBiomeProvider(worldName);
if (stagedBiomeProvider != null) {
Iris.debug("Using staged runtime biome provider for " + worldName);
return stagedBiomeProvider;
}
Iris.debug("Biome Provider Called for " + worldName + " using ID: " + id);
return super.getDefaultBiomeProvider(worldName, id);
return generatorResolver.resolveDefaultBiomeProvider(worldName, id, () -> super.getDefaultBiomeProvider(worldName, id));
}
@Override
public ChunkGenerator getDefaultWorldGenerator(String worldName, String id) {
ChunkGenerator stagedGenerator = WorldLifecycleStaging.consumeGenerator(worldName);
if (stagedGenerator != null) {
Iris.debug("Using staged runtime generator for " + worldName);
return stagedGenerator;
}
Iris.debug("Default World Generator Called for " + worldName + " using ID: " + id);
if (id == null || id.isEmpty()) id = IrisSettings.get().getGenerator().getDefaultWorldType();
Iris.debug("Generator ID: " + id + " requested by bukkit/plugin");
PackValidationResult validation = PackValidationRegistry.get(id);
if (validation != null && !validation.isLoadable()) {
Iris.error("Refusing to create world '" + worldName + "' using broken pack '" + id + "':");
for (String reason : validation.getBlockingErrors()) {
Iris.error(" - " + reason);
}
throw new BrokenPackException(id, validation.getBlockingErrors());
}
IrisDimension dim = loadDimension(worldName, id);
if (dim == null) {
throw new RuntimeException("Can't find dimension " + id + "!");
}
Iris.debug("Assuming IrisDimension: " + dim.getName());
NamespacedKey worldKey = IrisWorldStorage.keyFromName(worldName);
IrisWorld w = IrisWorld.builder()
.platformIdentity(worldKey.toString())
.name(worldName)
.seed(1337)
.worldFolder(IrisWorldStorage.dimensionRoot(worldKey))
.minHeight(dim.getMinHeight())
.maxHeight(dim.getMaxHeight())
.build();
Iris.debug("Generator Config: " + w.toString());
File ff = new File(w.worldFolder(), "iris/pack");
File[] files = ff.listFiles();
if (files == null || files.length == 0)
IO.delete(ff);
if (!ff.exists()) {
ff.mkdirs();
dim = service(StudioSVC.class).installIntoWorld(getSender(), dim, w.worldFolder());
if (dim == null) {
throw new IllegalStateException("Failed to install dimension pack for " + id);
}
}
return new BukkitChunkGenerator(w, false, ff, dim.getLoadKey());
}
public static void validateAllPacks() {
File packsRoot = Iris.instance.getDataFolder("packs");
File[] packDirs = packsRoot.listFiles(File::isDirectory);
if (packDirs == null || packDirs.length == 0) {
return;
}
PackValidationRegistry.clear();
for (File packDir : packDirs) {
try {
PackValidationResult result = PackValidator.validate(packDir);
PackValidationRegistry.publish(result);
if (!result.isLoadable()) {
Iris.error("Pack '" + result.getPackName() + "' FAILED validation - world/studio creation will be refused. Reasons:");
for (String reason : result.getBlockingErrors()) {
Iris.error(" - " + reason);
}
} else if (!result.getWarnings().isEmpty()) {
Iris.info("Pack '" + result.getPackName() + "' validated ("
+ result.getWarnings().size() + " warning(s)).");
for (String warning : result.getWarnings()) {
Iris.warn(" [" + result.getPackName() + "] " + warning);
}
} else {
Iris.success("Pack '" + result.getPackName() + "' validated.");
}
} catch (Throwable e) {
Iris.reportError("Pack validation failed for '" + packDir.getName() + "'", e);
}
}
}
@Nullable
public static IrisDimension loadDimension(@NonNull String worldName, @NonNull String id) {
File pack = IrisWorldStorage.packRoot(IrisWorldStorage.keyFromName(worldName));
IrisDimension dimension = pack.isDirectory() ? IrisData.get(pack).getDimensionLoader().load(id) : null;
if (dimension == null) dimension = IrisData.loadAnyDimension(id, null);
if (dimension == null) {
Iris.warn("Unable to find dimension type " + id + " Looking for online packs...");
Iris.service(StudioSVC.class).downloadSearch(new VolmitSender(Bukkit.getConsoleSender()), id, false);
dimension = IrisData.loadAnyDimension(id, null);
if (dimension != null) {
Iris.info("Resolved missing dimension, proceeding.");
}
}
return dimension;
return generatorResolver.resolveDefaultWorldGenerator(worldName, id);
}
public void splash() {
@@ -0,0 +1,110 @@
/*
* Iris is a World Generator for Minecraft Bukkit Servers
* Copyright (c) 2026 Arcane Arts (Volmit Software)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package art.arcane.iris.core;
import art.arcane.iris.Iris;
import art.arcane.iris.core.lifecycle.WorldLifecycleService;
import art.arcane.iris.core.nms.INMS;
import art.arcane.iris.engine.object.IrisDimension;
import art.arcane.iris.platform.bukkit.BukkitEnvironment;
import art.arcane.iris.util.common.format.C;
import art.arcane.iris.util.common.scheduling.J;
import art.arcane.volmlib.util.bukkit.WorldIdentity;
import art.arcane.volmlib.util.collection.KList;
import org.bukkit.NamespacedKey;
import org.bukkit.WorldCreator;
import org.bukkit.generator.ChunkGenerator;
import java.util.function.Predicate;
/**
* Loads Iris worlds that are staged in bukkit.yml but not yet present on the server.
*/
public final class BukkitWorldReconciler {
private final Iris plugin;
public BukkitWorldReconciler(Iris plugin) {
this.plugin = plugin;
}
public void checkForBukkitWorlds(Predicate<String> filter) {
try {
KList<String> deferredStartupWorlds = new KList<>();
IrisWorlds.readBukkitWorlds().forEach((s, generator) -> {
try {
NamespacedKey worldKey = IrisWorldStorage.keyFromName(s);
if (WorldIdentity.resolve(worldKey).isPresent() || !filter.test(s)) return;
Iris.info("Loading World: %s | Generator: %s", s, generator);
ChunkGenerator gen = plugin.getDefaultWorldGenerator(s, generator);
IrisDimension dim = IrisWorldGeneratorResolver.loadDimension(s, generator);
assert dim != null && gen != null;
Iris.info(C.LIGHT_PURPLE + "Preparing Spawn for " + s + "' using Iris:" + generator + "...");
WorldCreator c = WorldCreator.ofKey(worldKey)
.generator(gen)
.environment(BukkitEnvironment.from(dim.getEnvironment()));
Long stagedSeed = IrisWorlds.readBukkitWorldSeed(s);
if (stagedSeed != null) {
c.seed(stagedSeed);
}
INMS.get().createWorld(c);
Iris.info(C.LIGHT_PURPLE + "Loaded " + s + "!");
} catch (Throwable e) {
if (containsCreateWorldUnsupportedOperation(e)) {
if (J.isFolia()) {
if (!deferredStartupWorlds.contains(s)) {
deferredStartupWorlds.add(s);
}
return;
}
Iris.error("Failed to load world " + s + "!");
Iris.error("This server denied Bukkit.createWorld for \"" + s + "\" at the current startup phase.");
Iris.error("Ensure Iris is loaded at STARTUP and restart after staging worlds in bukkit.yml.");
Iris.reportError("Failed to load staged startup world \"" + s + "\".", e);
return;
}
Iris.reportError("Failed to load startup world \"" + s + "\".", e);
}
});
if (!deferredStartupWorlds.isEmpty()) {
Iris.warn("Staged Iris worlds could not load on Folia: %s", String.join(", ", deferredStartupWorlds));
Iris.warn("Bukkit.createWorld is unsupported on this server and the Iris runtime world backend is unavailable (%s).", WorldLifecycleService.get().capabilities().paperLikeResolution());
}
} catch (Throwable e) {
Iris.reportError("Failed while loading startup Iris worlds.", e);
}
}
private static boolean containsCreateWorldUnsupportedOperation(Throwable throwable) {
Throwable cursor = throwable;
while (cursor != null) {
if (cursor instanceof UnsupportedOperationException || cursor instanceof IllegalStateException) {
for (StackTraceElement element : cursor.getStackTrace()) {
if ("org.bukkit.craftbukkit.CraftServer".equals(element.getClassName())
&& "createWorld".equals(element.getMethodName())) {
return true;
}
}
}
cursor = cursor.getCause();
}
return false;
}
}
@@ -0,0 +1,173 @@
/*
* Iris is a World Generator for Minecraft Bukkit Servers
* Copyright (c) 2026 Arcane Arts (Volmit Software)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package art.arcane.iris.core;
import art.arcane.iris.Iris;
import art.arcane.iris.core.lifecycle.WorldLifecycleStaging;
import art.arcane.iris.core.loader.IrisData;
import art.arcane.iris.core.pack.BrokenPackException;
import art.arcane.iris.core.pack.PackValidationRegistry;
import art.arcane.iris.core.pack.PackValidationResult;
import art.arcane.iris.core.pack.PackValidator;
import art.arcane.iris.core.service.StudioSVC;
import art.arcane.iris.engine.object.IrisDimension;
import art.arcane.iris.engine.object.IrisWorld;
import art.arcane.iris.engine.platform.BukkitChunkGenerator;
import art.arcane.iris.util.common.plugin.VolmitPlugin;
import art.arcane.iris.util.common.plugin.VolmitSender;
import art.arcane.volmlib.util.io.IO;
import lombok.NonNull;
import org.bukkit.Bukkit;
import org.bukkit.NamespacedKey;
import org.bukkit.generator.BiomeProvider;
import org.bukkit.generator.ChunkGenerator;
import org.jetbrains.annotations.Nullable;
import java.io.File;
import java.util.function.Supplier;
/**
* Pack validation, dimension lookup, and the world generator / biome provider resolution that the
* Bukkit plugin entry points delegate to.
*/
public final class IrisWorldGeneratorResolver {
private final VolmitPlugin plugin;
public IrisWorldGeneratorResolver(VolmitPlugin plugin) {
this.plugin = plugin;
}
public void validateAllPacks() {
File packsRoot = plugin.getDataFolder("packs");
File[] packDirs = packsRoot.listFiles(File::isDirectory);
if (packDirs == null || packDirs.length == 0) {
return;
}
PackValidationRegistry.clear();
for (File packDir : packDirs) {
try {
PackValidationResult result = PackValidator.validate(packDir);
PackValidationRegistry.publish(result);
if (!result.isLoadable()) {
Iris.error("Pack '" + result.getPackName() + "' FAILED validation - world/studio creation will be refused. Reasons:");
for (String reason : result.getBlockingErrors()) {
Iris.error(" - " + reason);
}
} else if (!result.getWarnings().isEmpty()) {
Iris.info("Pack '" + result.getPackName() + "' validated ("
+ result.getWarnings().size() + " warning(s)).");
for (String warning : result.getWarnings()) {
Iris.warn(" [" + result.getPackName() + "] " + warning);
}
} else {
Iris.success("Pack '" + result.getPackName() + "' validated.");
}
} catch (Throwable e) {
Iris.reportError("Pack validation failed for '" + packDir.getName() + "'", e);
}
}
}
@Nullable
public static IrisDimension loadDimension(@NonNull String worldName, @NonNull String id) {
File pack = IrisWorldStorage.packRoot(IrisWorldStorage.keyFromName(worldName));
IrisDimension dimension = pack.isDirectory() ? IrisData.get(pack).getDimensionLoader().load(id) : null;
if (dimension == null) dimension = IrisData.loadAnyDimension(id, null);
if (dimension == null) {
Iris.warn("Unable to find dimension type " + id + " Looking for online packs...");
Iris.service(StudioSVC.class).downloadSearch(new VolmitSender(Bukkit.getConsoleSender()), id, false);
dimension = IrisData.loadAnyDimension(id, null);
if (dimension != null) {
Iris.info("Resolved missing dimension, proceeding.");
}
}
return dimension;
}
/**
* Resolves the biome provider for a world, falling back to the supplied Bukkit default when
* Iris has nothing staged.
*/
@Nullable
public BiomeProvider resolveDefaultBiomeProvider(String worldName, @Nullable String id, Supplier<BiomeProvider> fallback) {
BiomeProvider stagedBiomeProvider = WorldLifecycleStaging.consumeBiomeProvider(worldName);
if (stagedBiomeProvider != null) {
Iris.debug("Using staged runtime biome provider for " + worldName);
return stagedBiomeProvider;
}
Iris.debug("Biome Provider Called for " + worldName + " using ID: " + id);
return fallback.get();
}
public ChunkGenerator resolveDefaultWorldGenerator(String worldName, String id) {
ChunkGenerator stagedGenerator = WorldLifecycleStaging.consumeGenerator(worldName);
if (stagedGenerator != null) {
Iris.debug("Using staged runtime generator for " + worldName);
return stagedGenerator;
}
Iris.debug("Default World Generator Called for " + worldName + " using ID: " + id);
if (id == null || id.isEmpty()) id = IrisSettings.get().getGenerator().getDefaultWorldType();
Iris.debug("Generator ID: " + id + " requested by bukkit/plugin");
PackValidationResult validation = PackValidationRegistry.get(id);
if (validation != null && !validation.isLoadable()) {
Iris.error("Refusing to create world '" + worldName + "' using broken pack '" + id + "':");
for (String reason : validation.getBlockingErrors()) {
Iris.error(" - " + reason);
}
throw new BrokenPackException(id, validation.getBlockingErrors());
}
IrisDimension dim = loadDimension(worldName, id);
if (dim == null) {
throw new RuntimeException("Can't find dimension " + id + "!");
}
Iris.debug("Assuming IrisDimension: " + dim.getName());
NamespacedKey worldKey = IrisWorldStorage.keyFromName(worldName);
IrisWorld w = IrisWorld.builder()
.platformIdentity(worldKey.toString())
.name(worldName)
.seed(1337)
.worldFolder(IrisWorldStorage.dimensionRoot(worldKey))
.minHeight(dim.getMinHeight())
.maxHeight(dim.getMaxHeight())
.build();
Iris.debug("Generator Config: " + w.toString());
File ff = new File(w.worldFolder(), "iris/pack");
File[] files = ff.listFiles();
if (files == null || files.length == 0)
IO.delete(ff);
if (!ff.exists()) {
ff.mkdirs();
dim = Iris.service(StudioSVC.class).installIntoWorld(Iris.getSender(), dim, w.worldFolder());
if (dim == null) {
throw new IllegalStateException("Failed to install dimension pack for " + id);
}
}
return new BukkitChunkGenerator(w, false, ff, dim.getLoadKey());
}
}
@@ -0,0 +1,228 @@
/*
* Iris is a World Generator for Minecraft Bukkit Servers
* Copyright (c) 2026 Arcane Arts (Volmit Software)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package art.arcane.iris.core;
import art.arcane.iris.Iris;
import art.arcane.iris.core.runtime.TransientWorldCleanupSupport;
import art.arcane.iris.core.tools.IrisCreator;
import art.arcane.iris.core.tools.IrisToolbelt;
import art.arcane.iris.engine.platform.PlatformChunkGenerator;
import art.arcane.iris.util.common.misc.ServerProperties;
import art.arcane.iris.util.common.plugin.VolmitPlugin;
import art.arcane.volmlib.util.bukkit.WorldIdentity;
import art.arcane.volmlib.util.io.IO;
import org.bukkit.Bukkit;
import org.bukkit.NamespacedKey;
import org.bukkit.World;
import org.jetbrains.annotations.Nullable;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.Collection;
import java.util.LinkedHashMap;
import java.util.Locale;
import java.util.Map;
/**
* Persistent queue of world folders that must be deleted on the next startup, plus the startup
* drain that actually removes them.
*/
public final class PendingWorldDeleteQueue {
private static final String PENDING_WORLD_DELETE_FILE = "pending-world-deletes.txt";
private final VolmitPlugin plugin;
public PendingWorldDeleteQueue(VolmitPlugin plugin) {
this.plugin = plugin;
}
public synchronized int queueWorldDeletionOnStartup(Collection<String> worldNames) throws IOException {
if (worldNames == null || worldNames.isEmpty()) {
return 0;
}
LinkedHashMap<String, String> queue = loadPendingWorldDeleteMap();
int before = queue.size();
for (String worldName : worldNames) {
String normalized = normalizeWorldName(worldName);
if (normalized == null) {
continue;
}
queue.putIfAbsent(normalized.toLowerCase(Locale.ROOT), normalized);
}
if (queue.size() != before) {
writePendingWorldDeleteMap(queue);
}
return queue.size() - before;
}
public void processPendingStartupWorldDeletes() {
try {
try {
int unregistered = IrisCreator.removeTransientStudioWorldsFromBukkitYml();
if (unregistered > 0) {
Iris.info("Unregistered " + unregistered + " transient studio world(s) from bukkit.yml on startup.");
}
} catch (Throwable e) {
Iris.reportError("Failed to unregister transient studio worlds from bukkit.yml on startup.", e);
}
LinkedHashMap<String, String> queue = loadPendingWorldDeleteMap();
for (String transientStudioWorld : TransientWorldCleanupSupport.collectTransientStudioWorldNames(IrisWorldStorage.levelRoot())) {
queue.putIfAbsent(transientStudioWorld.toLowerCase(Locale.ROOT), transientStudioWorld);
}
if (queue.isEmpty()) {
return;
}
LinkedHashMap<String, String> remaining = new LinkedHashMap<>();
for (String worldName : queue.values()) {
if (worldName.equalsIgnoreCase(ServerProperties.LEVEL_NAME)) {
Iris.warn("Skipping queued deletion for \"" + worldName + "\" because it is configured as level-name.");
continue;
}
NamespacedKey worldKey = IrisWorldStorage.keyFromName(worldName);
World loaded = WorldIdentity.resolve(worldKey).orElse(null);
if (loaded != null) {
if (TransientWorldCleanupSupport.isTransientStudioWorldName(worldName)) {
try {
PlatformChunkGenerator generator = IrisToolbelt.access(loaded);
if (generator != null) {
generator.close();
}
IrisToolbelt.evacuate(loaded);
Bukkit.unloadWorld(loaded, false);
Iris.info("Unloaded leftover studio world \"" + worldName + "\" for deletion.");
} catch (Throwable e) {
Iris.reportError("Failed to unload leftover studio world \"" + worldName + "\".", e);
}
if (WorldIdentity.resolve(worldKey).isPresent()) {
Iris.warn("Studio world \"" + worldName + "\" is still loaded after unload; will retry next startup.");
remaining.put(worldName.toLowerCase(Locale.ROOT), worldName);
continue;
}
} else {
Iris.warn("Skipping queued deletion for \"" + worldName + "\" because it is currently loaded.");
remaining.put(worldName.toLowerCase(Locale.ROOT), worldName);
continue;
}
}
boolean foundAny = false;
boolean deletedAll = true;
for (String familyWorldName : TransientWorldCleanupSupport.worldFamilyNames(worldName)) {
File worldFolder = IrisWorldStorage.dimensionRoot(familyWorldName);
if (!worldFolder.exists()) {
continue;
}
foundAny = true;
IO.delete(worldFolder);
if (worldFolder.exists()) {
deletedAll = false;
Iris.warn("Failed to delete queued world folder \"" + familyWorldName + "\". Retrying on next startup.");
} else {
Iris.info("Deleted queued world folder \"" + familyWorldName + "\".");
}
}
if (!foundAny) {
Iris.info("Queued world deletion skipped for \"" + worldName + "\" (folder missing).");
continue;
}
if (!deletedAll) {
remaining.put(worldName.toLowerCase(Locale.ROOT), worldName);
continue;
}
}
writePendingWorldDeleteMap(remaining);
} catch (Throwable e) {
Iris.error("Failed to process queued startup world deletions.");
Iris.reportError(e);
e.printStackTrace();
}
}
private LinkedHashMap<String, String> loadPendingWorldDeleteMap() throws IOException {
LinkedHashMap<String, String> queue = new LinkedHashMap<>();
File queueFile = plugin.getDataFile(PENDING_WORLD_DELETE_FILE);
if (!queueFile.exists()) {
return queue;
}
try (BufferedReader reader = new BufferedReader(new FileReader(queueFile))) {
String line;
while ((line = reader.readLine()) != null) {
String normalized = normalizeWorldName(line);
if (normalized == null) {
continue;
}
queue.putIfAbsent(normalized.toLowerCase(Locale.ROOT), normalized);
}
}
return queue;
}
private void writePendingWorldDeleteMap(Map<String, String> queue) throws IOException {
File queueFile = plugin.getDataFile(PENDING_WORLD_DELETE_FILE);
if (queue.isEmpty()) {
if (queueFile.exists()) {
IO.delete(queueFile);
}
return;
}
File parent = queueFile.getParentFile();
if (parent != null && !parent.exists() && !parent.mkdirs()) {
throw new IOException("Failed to create queue directory: " + parent.getAbsolutePath());
}
try (PrintWriter writer = new PrintWriter(new FileWriter(queueFile))) {
for (String worldName : queue.values()) {
writer.println(worldName);
}
}
}
@Nullable
private static String normalizeWorldName(String worldName) {
if (worldName == null) {
return null;
}
String trimmed = worldName.trim();
if (trimmed.isEmpty()) {
return null;
}
return trimmed;
}
}
@@ -0,0 +1,98 @@
/*
* Iris is a World Generator for Minecraft Bukkit Servers
* Copyright (c) 2026 Arcane Arts (Volmit Software)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package art.arcane.iris.core;
import art.arcane.iris.Iris;
import art.arcane.iris.core.localization.IrisLanguage;
import art.arcane.volmlib.util.hotload.ConfigHotloadEngine;
import art.arcane.volmlib.util.io.IO;
import java.io.File;
import java.util.List;
/**
* Identity and hotload handling for settings.json. Supplies the predicates the
* {@link ConfigHotloadEngine} is built from and drains the touched-file queue.
*/
public final class SettingsHotloadWatch {
private final File settingsFile;
public SettingsHotloadWatch(File settingsFile) {
this.settingsFile = settingsFile;
}
public File settingsFile() {
return settingsFile;
}
public void checkConfigHotload(ConfigHotloadEngine engine) {
if (engine == null) {
return;
}
for (File file : engine.pollTouchedFiles()) {
engine.processFileChange(file, ignored -> {
IrisSettings.invalidate();
IrisSettings.get();
IrisLanguage.reload();
return true;
}, ignored -> Iris.info("Hotloaded settings.json "));
}
IrisLanguage.update();
}
public boolean isSettingsFile(File file) {
if (file == null || settingsFile == null) {
return false;
}
return settingsFile.getAbsoluteFile().equals(file.getAbsoluteFile());
}
public List<File> knownSettingsFiles() {
if (settingsFile == null) {
return List.of();
}
return List.of(settingsFile);
}
public String readSettingsContent(File file) {
if (file == null || !file.exists() || !file.isFile()) {
return null;
}
try {
return IO.readAll(file);
} catch (Throwable ex) {
Iris.warn("Failed to read settings file %s: %s%s",
file.getAbsolutePath(),
ex.getClass().getSimpleName(),
ex.getMessage() == null ? "" : " - " + ex.getMessage());
Iris.reportError(ex);
return null;
}
}
public String normalizeSettingsContent(String text) {
if (text == null) {
return null;
}
return text.replace("\r\n", "\n").trim();
}
}
@@ -51,6 +51,7 @@ import art.arcane.iris.util.common.plugin.VolmitSender;
import art.arcane.iris.util.common.scheduling.J;
import org.bukkit.Bukkit;
import org.bukkit.World;
import org.bukkit.entity.Player;
import java.io.BufferedInputStream;
import java.io.File;
@@ -283,7 +284,7 @@ public class CommandDeveloper implements DirectorExecutor {
}
@Director(description = "Delete nearby chunk blocks for regen testing", descriptionKey = "iris.director.commanddeveloper.director.delete_nearby_chunk_blocks_regen_testing", name = "delete-chunk", aliases = {"dc"}, origin = DirectorOrigin.PLAYER, sync = true)
@Director(description = "Delete nearby chunk blocks for regen testing", descriptionKey = "iris.director.commanddeveloper.director.delete_nearby_chunk_blocks_regen_testing", name = "delete-chunk", aliases = {"dc"}, origin = DirectorOrigin.PLAYER)
public void deleteChunk(
@Param(description = "Radius in chunks around your current chunk", descriptionKey = "iris.director.commanddeveloper.param.radius_chunks_around_your_current_chunk", defaultValue = "0")
int radius
@@ -293,25 +294,34 @@ public class CommandDeveloper implements DirectorExecutor {
return;
}
World world = player().getWorld();
Player player = player();
VolmitSender commandSender = sender();
World world = player.getWorld();
if (!IrisToolbelt.isIrisWorld(world)) {
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_DEVELOPER_THIS_IS_NOT_IRIS_WORLD));
commandSender.sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_DEVELOPER_THIS_IS_NOT_IRIS_WORLD));
return;
}
PlatformChunkGenerator access = IrisToolbelt.access(world);
if (access == null || access.getEngine() == null) {
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_DEVELOPER_ENGINE_ACCESS_THIS_WORLD_IS_NULL));
commandSender.sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_DEVELOPER_ENGINE_ACCESS_THIS_WORLD_IS_NULL));
return;
}
int centerX = player().getLocation().getBlockX() >> 4;
int centerZ = player().getLocation().getBlockZ() >> 4;
Engine engine = access.getEngine();
int chunks = (radius * 2 + 1) * (radius * 2 + 1);
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_DEVELOPER_DELETE_STARTED_CHUNK_S_AROUND_CLEARING_BLOCKS_AIR, MessageArgument.untrusted("chunks", chunks), MessageArgument.untrusted("centerX", centerX), MessageArgument.untrusted("centerZ", centerZ)));
// The player position must be read on the thread owning the player; ChunkClearer hops per chunk itself.
if (!J.runEntity(player, () -> {
int centerX = player.getLocation().getBlockX() >> 4;
int centerZ = player.getLocation().getBlockZ() >> 4;
new ChunkClearer(world, access.getEngine(), sender(), centerX, centerZ, radius).start();
commandSender.sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_DEVELOPER_DELETE_STARTED_CHUNK_S_AROUND_CLEARING_BLOCKS_AIR, MessageArgument.untrusted("chunks", chunks), MessageArgument.untrusted("centerX", centerX), MessageArgument.untrusted("centerZ", centerZ)));
new ChunkClearer(world, engine, commandSender, centerX, centerZ, radius).start();
})) {
Iris.warn("Could not schedule delete-chunk on the thread owning " + player.getName() + ".");
}
}
@Director(description = "Test", descriptionKey = "iris.director.commanddeveloper.director.test_4", aliases = {"ip"})
@@ -332,7 +342,7 @@ public class CommandDeveloper implements DirectorExecutor {
// --- Regen ---
@Director(name = "regen", aliases = {"rg"}, description = "Delete and regenerate nearby chunks in place using Iris generation", descriptionKey = "iris.director.commanddeveloper.director.delete_regenerate_nearby_chunks_place_using_iris_generation", origin = DirectorOrigin.PLAYER, sync = true)
@Director(name = "regen", aliases = {"rg"}, description = "Delete and regenerate nearby chunks in place using Iris generation", descriptionKey = "iris.director.commanddeveloper.director.delete_regenerate_nearby_chunks_place_using_iris_generation", origin = DirectorOrigin.PLAYER)
public void regen(
@Param(name = "radius", description = "The radius of nearby chunks", descriptionKey = "iris.director.commanddeveloper.param.radius_nearby_chunks", defaultValue = "5")
int radius
@@ -342,29 +352,37 @@ public class CommandDeveloper implements DirectorExecutor {
return;
}
World world = player().getWorld();
Player player = player();
VolmitSender commandSender = sender();
World world = player.getWorld();
if (!IrisToolbelt.isIrisWorld(world)) {
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_DEVELOPER_YOU_MUST_BE_IRIS_WORLD_USE_REGEN));
commandSender.sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_DEVELOPER_YOU_MUST_BE_IRIS_WORLD_USE_REGEN));
return;
}
Engine engine = IrisToolbelt.access(world).getEngine();
if (engine == null) {
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_DEVELOPER_ENGINE_ACCESS_THIS_WORLD_IS_NULL_GENERATE_NEARBY_CHUNKS_FIRST));
commandSender.sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_DEVELOPER_ENGINE_ACCESS_THIS_WORLD_IS_NULL_GENERATE_NEARBY_CHUNKS_FIRST));
return;
}
int centerX = player().getLocation().getBlockX() >> 4;
int centerZ = player().getLocation().getBlockZ() >> 4;
int chunks = (radius * 2 + 1) * (radius * 2 + 1);
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_DEVELOPER_REGEN_STARTED_CHUNK_S_AROUND_DELETING_REGENERATING_PLACE, MessageArgument.untrusted("chunks", chunks), MessageArgument.untrusted("centerX", centerX), MessageArgument.untrusted("centerZ", centerZ)));
Iris.info("Regen run start: world=" + world.getName()
+ " center=" + centerX + "," + centerZ
+ " radius=" + radius
+ " chunks=" + chunks);
// The player position must be read on the thread owning the player; the regenerator hops per chunk itself.
if (!J.runEntity(player, () -> {
int centerX = player.getLocation().getBlockX() >> 4;
int centerZ = player.getLocation().getBlockZ() >> 4;
new InPlaceChunkRegenerator(world, engine, sender(), centerX, centerZ, radius).start();
commandSender.sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_DEVELOPER_REGEN_STARTED_CHUNK_S_AROUND_DELETING_REGENERATING_PLACE, MessageArgument.untrusted("chunks", chunks), MessageArgument.untrusted("centerX", centerX), MessageArgument.untrusted("centerZ", centerZ)));
Iris.info("Regen run start: world=" + world.getName()
+ " center=" + centerX + "," + centerZ
+ " radius=" + radius
+ " chunks=" + chunks);
new InPlaceChunkRegenerator(world, engine, commandSender, centerX, centerZ, radius).start();
})) {
Iris.warn("Could not schedule regen on the thread owning " + player.getName() + ".");
}
}
@Director(name = "goldenhash", aliases = {"gold"}, description = "Generate chunks into buffers (no world writes) and hash blocks+biomes; captures a golden file or verifies against an existing one. Resets mantle in the scanned area - use on disposable test worlds.", descriptionKey = "iris.director.commanddeveloper.director.generate_chunks_into_buffers_no_world_writes_hash_blocks_biomes_captures_golden", origin = DirectorOrigin.BOTH)
@@ -316,19 +316,6 @@ public class CommandIris implements DirectorExecutor {
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_IRIS_IRIS_V_BY_VOLMIT_SOFTWARE, MessageArgument.untrusted("value", Iris.instance.getDescription().getVersion())));
}
/*
/todo
@Director(description = "Benchmark a pack", descriptionKey = "iris.director.commandiris.director.benchmark_pack", origin = DirectorOrigin.CONSOLE)
public void packbenchmark(
@Param(description = "Dimension to benchmark", descriptionKey = "iris.director.commandiris.param.dimension_benchmark")
IrisDimension type
) throws InterruptedException {
BenchDimension = type.getLoadKey();
IrisPackBenchmarking.runBenchmark();
} */
@Director(description = "Print world height information", descriptionKey = "iris.director.commandiris.director.print_world_height_information", origin = DirectorOrigin.PLAYER)
public void height() {
if (sender().isPlayer()) {
@@ -588,7 +575,7 @@ public class CommandIris implements DirectorExecutor {
return;
}
Iris.instance.checkForBukkitWorlds(logicalWorldName::equals);
Iris.instance.worldReconciler().checkForBukkitWorlds(logicalWorldName::equals);
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_IRIS_LOADED_SUCCESSFULLY, MessageArgument.untrusted("logicalWorldName", logicalWorldName)));
}
@Director(description = "Evacuate an iris world", descriptionKey = "iris.director.commandiris.director.evacuate_iris_world", origin = DirectorOrigin.PLAYER, sync = true)
@@ -428,8 +428,13 @@ public class CommandObject implements DirectorExecutor {
@Director(description = "Get a powder that reveals objects", descriptionKey = "iris.director.commandobject.director.get_powder_that_reveals_objects", aliases = "d")
public void dust() {
player().getInventory().addItem(WandSVC.createDust());
sender().playSound(Sound.AMBIENT_SOUL_SAND_VALLEY_ADDITIONS, 1f, 1.5f);
VolmitSender commandSender = sender();
Player player = player();
onPlayerThread(player, () -> {
player.getInventory().addItem(WandSVC.createDust());
commandSender.playSound(Sound.AMBIENT_SOUL_SAND_VALLEY_ADDITIONS, 1f, 1.5f);
});
}
@Director(description = "Contract a selection based on your looking direction", descriptionKey = "iris.director.commandobject.director.contract_selection_based_on_your_looking_direction", aliases = "-")
@@ -437,28 +442,33 @@ public class CommandObject implements DirectorExecutor {
@Param(description = "The amount to inset by", descriptionKey = "iris.director.commandobject.param.amount_inset_by", defaultValue = "1")
int amount
) {
if (!WandSVC.isHoldingWand(player())) {
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_OBJECT_HOLD_YOUR_WAND));
return;
}
VolmitSender commandSender = sender();
Player player = player();
onPlayerThread(player, () -> {
if (!WandSVC.isHoldingWand(player)) {
commandSender.sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_OBJECT_HOLD_YOUR_WAND));
return;
}
Location[] b = WandSVC.getCuboid(player());
if (b == null || b[0] == null || b[1] == null) {
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_OBJECT_NO_AREA_SELECTED));
return;
}
Location a1 = b[0].clone();
Location a2 = b[1].clone();
Cuboid cursor = new Cuboid(a1, a2);
Direction d = Direction.closest(player().getLocation().getDirection()).reverse();
assert d != null;
cursor = cursor.expand(d.f(), -amount);
b[0] = cursor.getLowerNE();
b[1] = cursor.getUpperSW();
player().getInventory().setItemInMainHand(WandSVC.createWand(b[0], b[1]));
player().updateInventory();
sender().playSound(Sound.ENTITY_ITEM_FRAME_ROTATE_ITEM, 1f, 0.55f);
Location[] b = WandSVC.getCuboid(player);
if (b == null || b[0] == null || b[1] == null) {
commandSender.sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_OBJECT_NO_AREA_SELECTED));
return;
}
Location a1 = b[0].clone();
Location a2 = b[1].clone();
Cuboid cursor = new Cuboid(a1, a2);
Direction d = Direction.closest(player.getLocation().getDirection()).reverse();
assert d != null;
cursor = cursor.expand(d.f(), -amount);
b[0] = cursor.getLowerNE();
b[1] = cursor.getUpperSW();
player.getInventory().setItemInMainHand(WandSVC.createWand(b[0], b[1]));
player.updateInventory();
commandSender.playSound(Sound.ENTITY_ITEM_FRAME_ROTATE_ITEM, 1f, 0.55f);
});
}
@Director(description = "Set point 1 to look", descriptionKey = "iris.director.commandobject.director.set_point_1_look", aliases = "p1")
@@ -466,25 +476,30 @@ public class CommandObject implements DirectorExecutor {
@Param(description = "Whether to use your current position, or where you look", descriptionKey = "iris.director.commandobject.param.whether_use_your_current_position_where_you_look", defaultValue = "true")
boolean here
) {
if (!WandSVC.isHoldingWand(player())) {
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_OBJECT_READY_YOUR_WAND));
return;
}
VolmitSender commandSender = sender();
Player player = player();
if (WandSVC.isHoldingWand(player())) {
Location[] g = WandSVC.getCuboid(player());
if (g == null) {
onPlayerThread(player, () -> {
if (!WandSVC.isHoldingWand(player)) {
commandSender.sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_OBJECT_READY_YOUR_WAND));
return;
}
if (!here) {
// TODO: WARNING HEIGHT
g[1] = player().getTargetBlock(null, 256).getLocation().clone();
} else {
g[1] = player().getLocation().getBlock().getLocation().clone().add(0, -1, 0);
if (WandSVC.isHoldingWand(player)) {
Location[] g = WandSVC.getCuboid(player);
if (g == null) {
return;
}
if (!here) {
// TODO: WARNING HEIGHT
g[1] = player.getTargetBlock(null, 256).getLocation().clone();
} else {
g[1] = player.getLocation().getBlock().getLocation().clone().add(0, -1, 0);
}
player.getInventory().setItemInMainHand(WandSVC.createWand(g[0], g[1]));
}
player().getInventory().setItemInMainHand(WandSVC.createWand(g[0], g[1]));
}
});
}
@Director(description = "Set point 2 to look", descriptionKey = "iris.director.commandobject.director.set_point_2_look", aliases = "p2")
@@ -492,26 +507,31 @@ public class CommandObject implements DirectorExecutor {
@Param(description = "Whether to use your current position, or where you look", descriptionKey = "iris.director.commandobject.param.whether_use_your_current_position_where_you_look_2", defaultValue = "true")
boolean here
) {
if (!WandSVC.isHoldingWand(player())) {
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_OBJECT_READY_YOUR_WAND_2));
return;
}
VolmitSender commandSender = sender();
Player player = player();
if (WandSVC.isHoldingIrisWand(player())) {
Location[] g = WandSVC.getCuboid(player());
if (g == null) {
onPlayerThread(player, () -> {
if (!WandSVC.isHoldingWand(player)) {
commandSender.sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_OBJECT_READY_YOUR_WAND_2));
return;
}
if (!here) {
// TODO: WARNING HEIGHT
g[0] = player().getTargetBlock(null, 256).getLocation().clone();
} else {
g[0] = player().getLocation().getBlock().getLocation().clone().add(0, -1, 0);
if (WandSVC.isHoldingIrisWand(player)) {
Location[] g = WandSVC.getCuboid(player);
if (g == null) {
return;
}
if (!here) {
// TODO: WARNING HEIGHT
g[0] = player.getTargetBlock(null, 256).getLocation().clone();
} else {
g[0] = player.getLocation().getBlock().getLocation().clone().add(0, -1, 0);
}
player.getInventory().setItemInMainHand(WandSVC.createWand(g[0], g[1]));
}
player().getInventory().setItemInMainHand(WandSVC.createWand(g[0], g[1]));
}
});
}
@Director(description = "Paste an object", descriptionKey = "iris.director.commandobject.director.paste_object", sync = true)
@@ -540,8 +560,10 @@ public class CommandObject implements DirectorExecutor {
IrisObjectPlacement placement = new IrisObjectPlacement();
placement.setRotation(IrisObjectRotation.of(0, rotate, 0));
ItemStack wand = player().getInventory().getItemInMainHand();
Location block = player().getTargetBlock(skipBlocks, 256).getLocation().clone().add(0, 1, 0);
VolmitSender commandSender = sender();
Player player = player();
ItemStack wand = player.getInventory().getItemInMainHand();
Location block = player.getTargetBlock(skipBlocks, 256).getLocation().clone().add(0, 1, 0);
Map<Block, BlockData> futureChanges = new HashMap<>();
@@ -549,30 +571,50 @@ public class CommandObject implements DirectorExecutor {
o = o.scaled(scale, IrisObjectPlacementScaleInterpolator.TRICUBIC);
}
o.place(block.getBlockX(), block.getBlockY() + (int) o.getCenter().getY(), block.getBlockZ(), createPlacer(block.getWorld(), futureChanges), placement, new RNG(), null);
// Block writes must run on the thread owning the target chunk; the undo log stays global.
final IrisObject placed = o;
if (!J.runAt(block, () -> {
placed.place(block.getBlockX(), block.getBlockY() + (int) placed.getCenter().getY(), block.getBlockZ(), createPlacer(block.getWorld(), futureChanges), placement, new RNG(), null);
J.runGlobal(() -> Iris.service(ObjectSVC.class).addChanges(futureChanges));
Iris.service(ObjectSVC.class).addChanges(futureChanges);
if (edit) {
Vector center = new Vector(o.getCenter().getX(), o.getCenter().getY(), o.getCenter().getZ());
ItemStack newWand = WandSVC.createWand(block.clone().subtract(center).add(o.getW() - 1,
o.getH() + center.getY() - 1, o.getD() - 1), block.clone().subtract(center.clone().setY(0)));
if (WandSVC.isWand(wand)) {
wand = newWand;
player().getInventory().setItemInMainHand(wand);
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_OBJECT_UPDATED_WAND_OBJECTS_IOB, MessageArgument.untrusted("value", o.getLoadKey())));
} else {
int slot = WandSVC.findWand(player().getInventory());
if (slot == -1) {
player().getInventory().addItem(newWand);
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_OBJECT_GIVEN_NEW_WAND_OBJECTS_IOB, MessageArgument.untrusted("value", o.getLoadKey())));
} else {
player().getInventory().setItem(slot, newWand);
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_OBJECT_UPDATED_WAND_OBJECTS_IOB_2, MessageArgument.untrusted("value", o.getLoadKey())));
}
if (!edit) {
commandSender.sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_OBJECT_PLACED, MessageArgument.untrusted("object", object)));
return;
}
} else {
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_OBJECT_PLACED, MessageArgument.untrusted("object", object)));
onPlayerThread(player, () -> {
Vector center = new Vector(placed.getCenter().getX(), placed.getCenter().getY(), placed.getCenter().getZ());
ItemStack newWand = WandSVC.createWand(block.clone().subtract(center).add(placed.getW() - 1,
placed.getH() + center.getY() - 1, placed.getD() - 1), block.clone().subtract(center.clone().setY(0)));
if (WandSVC.isWand(wand)) {
player.getInventory().setItemInMainHand(newWand);
commandSender.sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_OBJECT_UPDATED_WAND_OBJECTS_IOB, MessageArgument.untrusted("value", placed.getLoadKey())));
} else {
int slot = WandSVC.findWand(player.getInventory());
if (slot == -1) {
player.getInventory().addItem(newWand);
commandSender.sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_OBJECT_GIVEN_NEW_WAND_OBJECTS_IOB, MessageArgument.untrusted("value", placed.getLoadKey())));
} else {
player.getInventory().setItem(slot, newWand);
commandSender.sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_OBJECT_UPDATED_WAND_OBJECTS_IOB_2, MessageArgument.untrusted("value", placed.getLoadKey())));
}
}
});
})) {
Iris.warn("Could not schedule the object paste at " + block.getBlockX() + ", " + block.getBlockY() + ", " + block.getBlockZ() + ".");
}
}
/**
* Runs the body on the thread owning the player, reporting when the hop cannot be scheduled.
*/
private void onPlayerThread(Player player, Runnable body) {
if (player == null) {
return;
}
if (!J.runEntity(player, body)) {
Iris.warn("Could not schedule /iris object on the thread owning " + player.getName() + ".");
}
}
@@ -616,30 +658,35 @@ public class CommandObject implements DirectorExecutor {
@Param(description = "The amount to shift by", descriptionKey = "iris.director.commandobject.param.amount_shift_by", defaultValue = "1")
int amount
) {
if (!WandSVC.isHoldingWand(player())) {
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_OBJECT_HOLD_YOUR_WAND_2));
return;
}
VolmitSender commandSender = sender();
Player player = player();
Location[] b = WandSVC.getCuboid(player());
if (b == null || b[0] == null || b[1] == null) {
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_OBJECT_NO_AREA_SELECTED_2));
return;
}
Location a1 = b[0].clone();
Location a2 = b[1].clone();
Direction d = Direction.closest(player().getLocation().getDirection()).reverse();
if (d == null) {
return; // HOW DID THIS HAPPEN
}
a1.add(d.toVector().multiply(amount));
a2.add(d.toVector().multiply(amount));
Cuboid cursor = new Cuboid(a1, a2);
b[0] = cursor.getLowerNE();
b[1] = cursor.getUpperSW();
player().getInventory().setItemInMainHand(WandSVC.createWand(b[0], b[1]));
player().updateInventory();
sender().playSound(Sound.ENTITY_ITEM_FRAME_ROTATE_ITEM, 1f, 0.55f);
onPlayerThread(player, () -> {
if (!WandSVC.isHoldingWand(player)) {
commandSender.sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_OBJECT_HOLD_YOUR_WAND_2));
return;
}
Location[] b = WandSVC.getCuboid(player);
if (b == null || b[0] == null || b[1] == null) {
commandSender.sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_OBJECT_NO_AREA_SELECTED_2));
return;
}
Location a1 = b[0].clone();
Location a2 = b[1].clone();
Direction d = Direction.closest(player.getLocation().getDirection()).reverse();
if (d == null) {
return; // HOW DID THIS HAPPEN
}
a1.add(d.toVector().multiply(amount));
a2.add(d.toVector().multiply(amount));
Cuboid cursor = new Cuboid(a1, a2);
b[0] = cursor.getLowerNE();
b[1] = cursor.getUpperSW();
player.getInventory().setItemInMainHand(WandSVC.createWand(b[0], b[1]));
player.updateInventory();
commandSender.playSound(Sound.ENTITY_ITEM_FRAME_ROTATE_ITEM, 1f, 0.55f);
});
}
@Director(description = "Undo a number of pastes", descriptionKey = "iris.director.commandobject.director.undo_number_pastes", aliases = "u")
@@ -655,20 +702,25 @@ public class CommandObject implements DirectorExecutor {
@Director(description = "Gets an object wand and grabs the current WorldEdit selection.", descriptionKey = "iris.director.commandobject.director.gets_object_wand_grabs_current_worldedit_selection", aliases = "we", origin = DirectorOrigin.PLAYER)
public void we() {
VolmitSender commandSender = sender();
Player player = player();
if (!Bukkit.getPluginManager().isPluginEnabled("WorldEdit")) {
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_OBJECT_YOU_CAN_T_GET_WORLDEDIT_SELECTION_WITHOUT_WORLDEDIT_YOU_KNOW));
commandSender.sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_OBJECT_YOU_CAN_T_GET_WORLDEDIT_SELECTION_WITHOUT_WORLDEDIT_YOU_KNOW));
return;
}
Cuboid locs = WorldEditLink.getSelection(sender().player());
Cuboid locs = WorldEditLink.getSelection(player);
if (locs == null) {
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_OBJECT_YOU_DON_T_HAVE_WORLDEDIT_SELECTION_THIS_WORLD));
commandSender.sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_OBJECT_YOU_DON_T_HAVE_WORLDEDIT_SELECTION_THIS_WORLD));
return;
}
sender().player().getInventory().addItem(WandSVC.createWand(locs.getLowerNE(), locs.getUpperSW()));
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_OBJECT_FRESH_WAND_WITH_YOUR_CURRENT_WORLDEDIT_SELECTION_ON_IT));
onPlayerThread(player, () -> {
player.getInventory().addItem(WandSVC.createWand(locs.getLowerNE(), locs.getUpperSW()));
commandSender.sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_OBJECT_FRESH_WAND_WITH_YOUR_CURRENT_WORLDEDIT_SELECTION_ON_IT));
});
}
@Director(description = "Get an object wand", descriptionKey = "iris.director.commandobject.director.get_object_wand", sync = true)
@@ -26,6 +26,7 @@ import art.arcane.iris.core.gui.NoiseExplorerGUI;
import art.arcane.iris.core.gui.VisionGUI;
import art.arcane.iris.core.loader.IrisData;
import art.arcane.iris.core.project.IrisProject;
import art.arcane.iris.core.project.IrisCodeWorkspace;
import art.arcane.iris.core.localization.RuntimeUiMessages;
import art.arcane.iris.core.service.BoardSVC;
import art.arcane.iris.core.service.StudioSVC;
@@ -104,6 +105,8 @@ import java.time.temporal.ChronoUnit;
import java.util.Date;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicLong;
import java.util.function.Supplier;
@@ -114,6 +117,7 @@ import art.arcane.volmlib.util.localization.MessageArgument;
import art.arcane.iris.core.localization.BukkitCommandMessagesExtended;
@Director(name = "studio", aliases = {"std", "s"}, description = "Studio Commands", descriptionKey = "iris.director.commandstudio.director.studio_commands")
public class CommandStudio implements DirectorExecutor {
private static final long CHUNK_SCAN_TIMEOUT_MS = 3_000;
private CommandEdit edit;
//private CommandDeepSearch deepSearch;
@@ -662,10 +666,18 @@ public class CommandStudio implements DirectorExecutor {
@Param(description = "The location to spawn the entity at", descriptionKey = "iris.director.commandstudio.param.location_spawn_entity_at", contextual = true)
Vector location
) {
VolmitSender commandSender = sender();
Engine spawnEngine = engine();
if (!IrisToolbelt.isIrisWorld(player().getWorld())) {
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_STUDIO_YOU_HAVE_BE_IRIS_WORLD_SPAWN_ENTITIES_PROPERLY_TRYING_SPAWN));
commandSender.sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_STUDIO_YOU_HAVE_BE_IRIS_WORLD_SPAWN_ENTITIES_PROPERLY_TRYING_SPAWN));
}
// Entity creation must run on the thread owning the destination chunk.
Location at = new Location(world(), location.getX(), location.getY(), location.getZ());
if (!J.runAt(at, () -> entity.spawn(spawnEngine, at))) {
Iris.warn("Could not schedule the entity spawn at " + at.getBlockX() + ", " + at.getBlockY() + ", " + at.getBlockZ() + ".");
}
entity.spawn(engine(), new Location(world(), location.getX(), location.getY(), location.getZ()));
}
@Director(description = "Teleport to the active studio world", descriptionKey = "iris.director.commandstudio.director.teleport_active_studio_world", aliases = "stp", origin = DirectorOrigin.PLAYER, sync = true)
@@ -697,7 +709,7 @@ public class CommandStudio implements DirectorExecutor {
IrisDimension dimension
) {
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_STUDIO_UPDATING_CODE_WORKSPACE, MessageArgument.untrusted("value", dimension.getName())));
if (new IrisProject(dimension.getLoader().getDataFolder()).updateWorkspace()) {
if (new IrisCodeWorkspace(new IrisProject(dimension.getLoader().getDataFolder())).updateWorkspace()) {
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_STUDIO_UPDATED_CODE_WORKSPACE, MessageArgument.untrusted("value", dimension.getName())));
} else {
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_STUDIO_INVALID_PROJECT_TRY_DELETING_CODE_WORKSPACE_FILE_TRY_AGAIN, MessageArgument.untrusted("value", dimension.getName())));
@@ -718,20 +730,46 @@ public class CommandStudio implements DirectorExecutor {
return;
}
KList<Chunk> chunks = new KList<>();
int bx = player().getLocation().getChunk().getX();
int bz = player().getLocation().getChunk().getZ();
Player reporter = player();
CountDownLatch gathered = new CountDownLatch(1);
try {
Location l = player().getTargetBlockExact(48, FluidCollisionMode.NEVER).getLocation();
// The raycast and the chunk loads need the thread owning the player; the report itself stays off it.
boolean scheduled = J.runEntity(reporter, () -> {
try {
int bx = reporter.getLocation().getChunk().getX();
int bz = reporter.getLocation().getChunk().getZ();
int cx = l.getChunk().getX();
int cz = l.getChunk().getZ();
new Spiraler(3, 3, (x, z) -> chunks.addIfMissing(world.getChunkAt(x + cx, z + cz))).drain();
} catch (Throwable e) {
Iris.reportError(e);
try {
Location l = reporter.getTargetBlockExact(48, FluidCollisionMode.NEVER).getLocation();
int cx = l.getChunk().getX();
int cz = l.getChunk().getZ();
new Spiraler(3, 3, (x, z) -> chunks.addIfMissing(world.getChunkAt(x + cx, z + cz))).drain();
} catch (Throwable e) {
Iris.reportError(e);
}
new Spiraler(3, 3, (x, z) -> chunks.addIfMissing(world.getChunkAt(x + bx, z + bz))).drain();
} finally {
gathered.countDown();
}
});
if (!scheduled) {
Iris.warn("Could not schedule the chunk report scan on the thread owning " + reporter.getName() + ".");
return;
}
try {
if (!gathered.await(CHUNK_SCAN_TIMEOUT_MS, TimeUnit.MILLISECONDS)) {
Iris.warn("Timed out waiting for the chunk report scan of " + reporter.getName() + ".");
return;
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return;
}
new Spiraler(3, 3, (x, z) -> chunks.addIfMissing(world.getChunkAt(x + bx, z + bz))).drain();
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_STUDIO_CAPTURING_IGENDATA_FROM_NEARBY_CHUNKS, MessageArgument.untrusted("value", chunks.size())));
try {
File ff = Iris.instance.getDataFile("reports/" + M.ms() + ".txt");
@@ -25,10 +25,13 @@ import art.arcane.iris.Iris;
import art.arcane.iris.core.edit.BlockSignal;
import art.arcane.iris.core.nms.INMS;
import art.arcane.iris.core.tools.IrisToolbelt;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.platform.EngineBukkitOps;
import art.arcane.iris.engine.object.IrisBiome;
import art.arcane.iris.engine.object.IrisRegion;
import art.arcane.iris.util.common.director.DirectorExecutor;
import art.arcane.iris.util.common.plugin.VolmitSender;
import art.arcane.iris.util.common.scheduling.J;
import art.arcane.volmlib.util.director.DirectorOrigin;
import art.arcane.volmlib.util.director.annotations.Director;
import art.arcane.volmlib.util.director.annotations.Param;
@@ -38,8 +41,10 @@ import org.bukkit.Chunk;
import org.bukkit.FluidCollisionMode;
import org.bukkit.Material;
import org.bukkit.NamespacedKey;
import org.bukkit.World;
import org.bukkit.block.Biome;
import org.bukkit.block.data.BlockData;
import org.bukkit.entity.Player;
import java.lang.reflect.Method;
import java.util.concurrent.atomic.AtomicInteger;
@@ -97,83 +102,115 @@ public class CommandWhat implements DirectorExecutor {
@Director(description = "What region am i in?", descriptionKey = "iris.director.commandwhat.director.what_region_am_i", origin = DirectorOrigin.PLAYER)
public void region() {
try {
Chunk chunk = world().getChunkAt(player().getLocation().getBlockX() >> 4, player().getLocation().getBlockZ() >> 4);
IrisRegion r = EngineBukkitOps.getRegion(engine(), chunk);
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_WHAT_IREGION, MessageArgument.untrusted("value", r.getLoadKey()), MessageArgument.untrusted("value2", r.getName())));
VolmitSender commandSender = sender();
Player player = player();
World world = world();
Engine engine = engine();
} catch (Throwable e) {
Iris.reportError(e);
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_WHAT_IRIS_WORLDS_ONLY));
}
// Chunk access must happen on the thread owning the player's chunk.
onPlayerThread(player, () -> {
try {
Chunk chunk = world.getChunkAt(player.getLocation().getBlockX() >> 4, player.getLocation().getBlockZ() >> 4);
IrisRegion r = EngineBukkitOps.getRegion(engine, chunk);
commandSender.sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_WHAT_IREGION, MessageArgument.untrusted("value", r.getLoadKey()), MessageArgument.untrusted("value2", r.getName())));
} catch (Throwable e) {
Iris.reportError(e);
commandSender.sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_WHAT_IRIS_WORLDS_ONLY));
}
});
}
@Director(description = "What block am i looking at?", descriptionKey = "iris.director.commandwhat.director.what_block_am_i_looking_at", origin = DirectorOrigin.PLAYER)
public void block() {
BlockData bd;
try {
bd = player().getTargetBlockExact(128, FluidCollisionMode.NEVER).getBlockData();
} catch (NullPointerException e) {
Iris.reportError(e);
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_WHAT_PLEASE_LOOK_AT_ANY_BLOCK_NOT_AT_SKY));
bd = null;
}
VolmitSender commandSender = sender();
Player player = player();
if (bd != null) {
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_WHAT_MATERIAL_3, MessageArgument.untrusted("value", bd.getMaterial().name())));
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_WHAT_FULL_2, MessageArgument.untrusted("value", bd.getAsString(true))));
if (BukkitBlockResolution.isStorage(bd)) {
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_WHAT_STORAGE_BLOCK_LOOT_CAPABLE));
// The raycast reads blocks, so it has to run on the thread owning the player.
onPlayerThread(player, () -> {
BlockData bd;
try {
bd = player.getTargetBlockExact(128, FluidCollisionMode.NEVER).getBlockData();
} catch (NullPointerException e) {
Iris.reportError(e);
commandSender.sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_WHAT_PLEASE_LOOK_AT_ANY_BLOCK_NOT_AT_SKY));
bd = null;
}
if (BukkitBlockResolution.isLit(bd)) {
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_WHAT_LIT_BLOCK_LIGHT_CAPABLE));
}
if (bd != null) {
commandSender.sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_WHAT_MATERIAL_3, MessageArgument.untrusted("value", bd.getMaterial().name())));
commandSender.sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_WHAT_FULL_2, MessageArgument.untrusted("value", bd.getAsString(true))));
if (BukkitBlockResolution.isFoliage(bd)) {
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_WHAT_FOLIAGE_BLOCK));
}
if (BukkitBlockResolution.isStorage(bd)) {
commandSender.sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_WHAT_STORAGE_BLOCK_LOOT_CAPABLE));
}
if (BukkitBlockResolution.isDecorant(bd)) {
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_WHAT_DECORANT_BLOCK));
}
if (BukkitBlockResolution.isLit(bd)) {
commandSender.sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_WHAT_LIT_BLOCK_LIGHT_CAPABLE));
}
if (BukkitBlockResolution.isFluid(bd)) {
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_WHAT_FLUID_BLOCK));
}
if (BukkitBlockResolution.isFoliage(bd)) {
commandSender.sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_WHAT_FOLIAGE_BLOCK));
}
if (BukkitBlockResolution.isFoliagePlantable(bd)) {
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_WHAT_PLANTABLE_FOLIAGE_BLOCK));
}
if (BukkitBlockResolution.isDecorant(bd)) {
commandSender.sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_WHAT_DECORANT_BLOCK));
}
if (BukkitBlockResolution.isSolid(bd)) {
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_WHAT_SOLID_BLOCK));
if (BukkitBlockResolution.isFluid(bd)) {
commandSender.sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_WHAT_FLUID_BLOCK));
}
if (BukkitBlockResolution.isFoliagePlantable(bd)) {
commandSender.sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_WHAT_PLANTABLE_FOLIAGE_BLOCK));
}
if (BukkitBlockResolution.isSolid(bd)) {
commandSender.sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_WHAT_SOLID_BLOCK));
}
}
}
});
}
@Director(description = "Show markers in chunk", descriptionKey = "iris.director.commandwhat.director.show_markers_chunk", origin = DirectorOrigin.PLAYER)
public void markers(@Param(description = "Marker name such as cave_floor or cave_ceiling", descriptionKey = "iris.director.commandwhat.param.marker_name_such_as_cave_floor_cave_ceiling") String marker) {
Chunk c = player().getLocation().getChunk();
VolmitSender commandSender = sender();
Player player = player();
if (IrisToolbelt.isIrisWorld(c.getWorld())) {
int m = 1;
AtomicInteger v = new AtomicInteger(0);
// Chunk lookup plus the block signals both need the thread owning the player's chunk.
onPlayerThread(player, () -> {
Chunk c = player.getLocation().getChunk();
for (int xxx = c.getX() - 4; xxx <= c.getX() + 4; xxx++) {
for (int zzz = c.getZ() - 4; zzz <= c.getZ() + 4; zzz++) {
IrisToolbelt.access(c.getWorld()).getEngine().getMantle().findMarkers(xxx, zzz, new MatterMarker(marker))
.convert((i) -> BukkitPlatform.toLocation(i, c.getWorld())).forEach((i) -> {
BlockSignal.of(i.getWorld(), i.getBlockX(), i.getBlockY(), i.getBlockZ(), 100);
v.incrementAndGet();
});
if (IrisToolbelt.isIrisWorld(c.getWorld())) {
AtomicInteger v = new AtomicInteger(0);
for (int xxx = c.getX() - 4; xxx <= c.getX() + 4; xxx++) {
for (int zzz = c.getZ() - 4; zzz <= c.getZ() + 4; zzz++) {
IrisToolbelt.access(c.getWorld()).getEngine().getMantle().findMarkers(xxx, zzz, new MatterMarker(marker))
.convert((i) -> BukkitPlatform.toLocation(i, c.getWorld())).forEach((i) -> {
BlockSignal.of(i.getWorld(), i.getBlockX(), i.getBlockY(), i.getBlockZ(), 100);
v.incrementAndGet();
});
}
}
}
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_WHAT_FOUND_NEARBY_MARKERS, MessageArgument.untrusted("value", v.get()), MessageArgument.untrusted("marker", marker)));
} else {
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_WHAT_IRIS_WORLDS_ONLY_2));
commandSender.sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_WHAT_FOUND_NEARBY_MARKERS, MessageArgument.untrusted("value", v.get()), MessageArgument.untrusted("marker", marker)));
} else {
commandSender.sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_WHAT_IRIS_WORLDS_ONLY_2));
}
});
}
/**
* Runs the body on the thread owning the player, reporting when the hop cannot be scheduled.
*/
private void onPlayerThread(Player player, Runnable body) {
if (player == null) {
return;
}
if (!J.runEntity(player, body)) {
Iris.warn("Could not schedule /iris what on the thread owning " + player.getName() + ".");
}
}
@@ -23,21 +23,24 @@ import org.bukkit.World;
import org.bukkit.block.Biome;
import org.bukkit.block.data.BlockData;
@SuppressWarnings("ClassCanBeRecord")
public class BukkitBlockEditor implements BlockEditor {
private final World world;
private volatile long last;
public BukkitBlockEditor(World world) {
this.world = world;
this.last = M.ms();
}
@Override
public void set(int x, int y, int z, BlockData d) {
touch();
world.getBlockAt(x, y, z).setBlockData(d, false);
}
@Override
public BlockData get(int x, int y, int z) {
touch();
return world.getBlockAt(x, y, z).getBlockData();
}
@@ -48,11 +51,12 @@ public class BukkitBlockEditor implements BlockEditor {
@Override
public long last() {
return M.ms();
return last;
}
@Override
public void setBiome(int x, int z, Biome b) {
touch();
int minHeight = world.getMinHeight();
int maxHeight = world.getMaxHeight();
for (int y = minHeight; y < maxHeight; y++) {
@@ -62,16 +66,23 @@ public class BukkitBlockEditor implements BlockEditor {
@Override
public void setBiome(int x, int y, int z, Biome b) {
touch();
world.setBiome(x, y, z, b);
}
@Override
public Biome getBiome(int x, int y, int z) {
touch();
return world.getBiome(x, y, z);
}
@Override
public Biome getBiome(int x, int z) {
touch();
return world.getBiome(x, world.getMinHeight(), z);
}
private void touch() {
last = M.ms();
}
}
@@ -18,6 +18,7 @@
package art.arcane.iris.core.gui;
import art.arcane.iris.platform.bukkit.BukkitPlatform;
import art.arcane.iris.platform.bukkit.BukkitWorldBinding;
import art.arcane.iris.engine.IrisComplex;
import art.arcane.iris.engine.framework.Engine;
@@ -26,72 +27,141 @@ import art.arcane.iris.engine.object.IrisWorld;
import art.arcane.iris.util.common.scheduling.J;
import art.arcane.volmlib.util.format.Form;
import org.bukkit.Location;
import org.bukkit.World;
import org.bukkit.entity.LivingEntity;
import org.bukkit.entity.Player;
import java.io.File;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Locale;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Consumer;
import static art.arcane.iris.util.common.data.registry.Attributes.MAX_HEALTH;
public final class BukkitVisionOverlay implements GuiOverlay {
private final Engine engine;
private final AtomicBoolean playerRefreshQueued = new AtomicBoolean();
private volatile List<GuiMarker> playerMarkers = List.of();
public BukkitVisionOverlay(Engine engine) {
this.engine = engine;
}
/**
* Called from the AWT event thread, so it may only hand back the last snapshot
* built by a server thread.
*/
@Override
public List<GuiMarker> players() {
IrisWorld world = engine.getWorld();
List<GuiMarker> markers = new ArrayList<>();
for (Player player : BukkitWorldBinding.players(world)) {
markers.add(GuiMarker.player(player.getName(), player.getLocation().getX(), player.getLocation().getZ()));
queuePlayerRefresh();
return playerMarkers;
}
private void queuePlayerRefresh() {
if (!playerRefreshQueued.compareAndSet(false, true)) {
return;
}
boolean scheduled = J.runGlobal(() -> {
try {
List<GuiMarker> markers = new ArrayList<>();
for (Player player : BukkitWorldBinding.players(engine.getWorld())) {
Location at = player.getLocation();
markers.add(GuiMarker.player(player.getName(), at.getX(), at.getZ()));
}
playerMarkers = List.copyOf(markers);
} finally {
playerRefreshQueued.set(false);
}
});
if (!scheduled) {
playerRefreshQueued.set(false);
}
return markers;
}
@Override
public void requestEntities(Consumer<List<GuiMarker>> sink) {
J.s(() -> {
IrisWorld world = engine.getWorld();
List<GuiMarker> markers = new ArrayList<>();
for (LivingEntity entity : BukkitWorldBinding.entities(world, LivingEntity.class)) {
if (entity instanceof Player) {
continue;
}
String label = Form.capitalizeWords(entity.getType().name().toLowerCase(Locale.ROOT).replaceAll("\\Q_\\E", " "));
double maxHealth = 0;
try {
maxHealth = entity.getAttribute(MAX_HEALTH).getValue();
} catch (Throwable ignored) {
}
markers.add(GuiMarker.entity(label, entity.getLocation().getX(), entity.getLocation().getY(), entity.getLocation().getZ(),
entity.getHealth(), maxHealth));
J.runGlobal(() -> {
IrisWorld target = engine.getWorld();
World world = BukkitWorldBinding.world(target);
if (world == null) {
sink.accept(List.of());
return;
}
List<LivingEntity> living = new ArrayList<>();
for (LivingEntity entity : BukkitWorldBinding.entities(target, LivingEntity.class)) {
if (!(entity instanceof Player)) {
living.add(entity);
}
}
if (living.isEmpty()) {
sink.accept(List.of());
return;
}
List<GuiMarker> collected = Collections.synchronizedList(new ArrayList<>(living.size()));
AtomicInteger pending = new AtomicInteger(living.size());
Runnable complete = () -> {
if (pending.decrementAndGet() == 0) {
sink.accept(List.copyOf(collected));
}
};
for (LivingEntity entity : living) {
Location at = entity.getLocation();
Runnable read = () -> {
try {
collected.add(marker(entity, at));
} catch (Throwable ignored) {
} finally {
complete.run();
}
};
if (!J.runRegion(world, at.getBlockX() >> 4, at.getBlockZ() >> 4, read)) {
complete.run();
}
}
sink.accept(markers);
});
}
private GuiMarker marker(LivingEntity entity, Location at) {
String label = Form.capitalizeWords(entity.getType().name().toLowerCase(Locale.ROOT).replaceAll("\\Q_\\E", " "));
double maxHealth = 0;
try {
maxHealth = entity.getAttribute(MAX_HEALTH).getValue();
} catch (Throwable ignored) {
}
return GuiMarker.entity(label, at.getX(), at.getY(), at.getZ(), entity.getHealth(), maxHealth);
}
@Override
public void teleport(double worldX, double worldZ) {
IrisWorld world = engine.getWorld();
if (!world.hasPlatformWorld()) {
IrisWorld target = engine.getWorld();
if (!target.hasPlatformWorld()) {
return;
}
J.s(() -> {
List<Player> players = BukkitWorldBinding.players(world);
J.runGlobal(() -> {
List<Player> players = BukkitWorldBinding.players(target);
if (players.isEmpty()) {
return;
}
Player player = players.get(0);
World world = player.getWorld();
int xx = (int) worldX;
int zz = (int) worldZ;
int yy = player.getWorld().getHighestBlockYAt(xx, zz) + 1;
player.teleport(new Location(player.getWorld(), xx, yy, zz));
J.runRegion(world, xx >> 4, zz >> 4, () -> {
int yy = world.getHighestBlockYAt(xx, zz) + 1;
Location destination = new Location(world, xx, yy, zz);
J.runEntity(player, () -> BukkitPlatform.teleportAsync(player, destination));
});
});
}
@@ -24,6 +24,7 @@ import art.arcane.iris.core.datapack.DatapackIngestService;
import art.arcane.iris.core.events.IrisEngineHotloadEvent;
import art.arcane.iris.core.gui.PregeneratorJob;
import art.arcane.iris.core.project.IrisProject;
import art.arcane.iris.core.project.IrisCodeWorkspace;
import art.arcane.iris.core.service.IrisApiEventSVC;
import art.arcane.iris.core.tools.IrisToolbelt;
import art.arcane.iris.core.tools.WorldMaintenance;
@@ -41,7 +42,7 @@ import org.bukkit.World;
public final class BukkitEnginePlatformHooks implements EnginePlatformHooks {
@Override
public void refreshWorkspace(Engine engine) {
new IrisProject(engine.getData().getDataFolder()).updateWorkspace();
new IrisCodeWorkspace(new IrisProject(engine.getData().getDataFolder())).updateWorkspace();
}
@Override
@@ -0,0 +1,45 @@
package art.arcane.iris.core.service;
import art.arcane.iris.api.tree.TreeFellerRunHooks;
import art.arcane.iris.core.service.TreeFellerModel.TreeCandidate;
import art.arcane.iris.core.service.TreeFellerModel.TreeClaim;
import art.arcane.iris.core.service.TreeFellerModel.TreeMember;
import org.bukkit.Location;
import org.bukkit.inventory.ItemStack;
import java.util.List;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
final class FellingRun {
final TreeClaim claim;
final TreeCandidate candidate;
final int preservationChance;
final TreeFellerRunHooks runHooks;
final int heldSlot;
final TreeFellerPresentation presentation;
final AtomicBoolean finished = new AtomicBoolean();
final AtomicInteger cursor = new AtomicInteger();
final AtomicInteger processed = new AtomicInteger();
volatile int blocksPerPulse = 1;
volatile int effectStride = 1;
volatile ItemStack expectedTool;
volatile List<TreeMember> work = List.of();
FellingRun(
TreeClaim claim,
TreeCandidate candidate,
int preservationChance,
TreeFellerRunHooks runHooks,
int heldSlot,
Location fallbackLocation
) {
this.claim = claim;
this.candidate = candidate;
this.preservationChance = preservationChance;
this.runHooks = runHooks;
this.heldSlot = heldSlot;
this.presentation = new TreeFellerPresentation(candidate.player(), candidate.world(), fallbackLocation);
this.expectedTool = candidate.tool().clone();
}
}
@@ -279,6 +279,11 @@ public final class IrisEngineSVC implements IrisService {
}
closing.completion().complete(null);
} else {
// A failed close must still stop conflicting with future registrations,
// otherwise the world never regains its maintenance task after a reload.
synchronized (registrationLock) {
closingGenerators.remove(closing);
}
closing.completion().completeExceptionally(failure);
}
}
@@ -0,0 +1,22 @@
package art.arcane.iris.core.service;
import art.arcane.iris.core.service.tree.BlockDropRouter;
import org.bukkit.block.Block;
import org.bukkit.entity.Player;
import org.bukkit.event.block.BlockBreakEvent;
final class RoutedBlockBreakEvent extends BlockBreakEvent implements BlockDropRouter {
private final TreeFellerSVC service;
private final FellingRun run;
RoutedBlockBreakEvent(Block block, Player player, FellingRun run, TreeFellerSVC service) {
super(block, player);
this.run = run;
this.service = service;
}
@Override
public boolean routeDrop(Object drop) {
return service.isServiceEnabled() && run.presentation.routeDrop(drop);
}
}
@@ -0,0 +1,81 @@
package art.arcane.iris.core.service;
import art.arcane.iris.api.tree.TreeFellerAccess;
import art.arcane.iris.api.tree.TreeFellerRunHooks;
import art.arcane.iris.core.service.tree.TreeMarkerTraversal;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.framework.TreeBlockMaterial;
import org.bukkit.World;
import org.bukkit.entity.Player;
import org.bukkit.inventory.ItemStack;
import java.util.UUID;
final class TreeFellerModel {
private TreeFellerModel() {
}
record PendingFell(
TreeCandidate candidate,
int preservationChance,
TreeFellerRunHooks runHooks
) {
PendingFell withAccess(TreeFellerAccess access) {
return new PendingFell(candidate.withAccess(access), preservationChance, runHooks);
}
}
record TreeCandidate(
TreeContext context,
World world,
Player player,
ItemStack tool,
TreeFellerAccess access,
TreeMarkerTraversal.Position trigger
) {
TreeCandidate withAccess(TreeFellerAccess access) {
return new TreeCandidate(context, world, player, tool, access, trigger);
}
}
record TreeContext(
Engine engine,
String marker,
TreeBlockMaterial expectedMaterial,
int minimumY,
int maximumY
) {
}
record TreeClaim(UUID worldId, String marker) {
}
record ProvenanceSnapshot(
Engine engine,
World world,
int minimumY,
TreeMarkerTraversal.Position position,
String marker,
TreeBlockMaterial material
) {
}
record ChunkPosition(int x, int z) {
}
record TreeMember(
TreeMarkerTraversal.Position position,
boolean log,
TreeBlockMaterial expectedMaterial,
int erosionOrder
) {
}
record DamageReservation(
ItemStack toolForDrops,
boolean charged,
boolean broke,
boolean logCostReserved
) {
}
}
@@ -5,27 +5,22 @@ import art.arcane.iris.api.tree.TreeFellerAccess;
import art.arcane.iris.api.tree.TreeFellerOptions;
import art.arcane.iris.api.tree.TreeFellerRunHooks;
import art.arcane.iris.core.IrisSettings;
import art.arcane.iris.core.service.tree.BlockDropRouter;
import art.arcane.iris.core.service.TreeFellerModel.PendingFell;
import art.arcane.iris.core.service.TreeFellerModel.ProvenanceSnapshot;
import art.arcane.iris.core.service.TreeFellerModel.TreeCandidate;
import art.arcane.iris.core.service.TreeFellerModel.TreeClaim;
import art.arcane.iris.core.service.tree.TreeDefinitionIndex;
import art.arcane.iris.core.service.tree.TreeMarkerTraversal;
import art.arcane.iris.core.tools.IrisToolbelt;
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.engine.platform.PlatformChunkGenerator;
import art.arcane.iris.platform.bukkit.BukkitPlatform;
import art.arcane.iris.spi.IrisLogging;
import art.arcane.iris.spi.IrisServices;
import art.arcane.iris.util.common.plugin.IrisService;
import art.arcane.iris.util.common.scheduling.J;
import org.bukkit.Bukkit;
import org.bukkit.GameMode;
import org.bukkit.Location;
import org.bukkit.Material;
import org.bukkit.Tag;
import org.bukkit.World;
import org.bukkit.block.Block;
import org.bukkit.block.data.BlockData;
import org.bukkit.entity.ExperienceOrb;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
@@ -36,40 +31,31 @@ import org.bukkit.event.player.PlayerItemHeldEvent;
import org.bukkit.event.player.PlayerSwapHandItemsEvent;
import org.bukkit.event.player.PlayerToggleSneakEvent;
import org.bukkit.inventory.ItemStack;
import org.bukkit.inventory.PlayerInventory;
import org.bukkit.inventory.meta.Damageable;
import org.bukkit.inventory.meta.ItemMeta;
import org.bukkit.plugin.ServicePriority;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.IdentityHashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.UUID;
import java.util.WeakHashMap;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ThreadLocalRandom;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
public class TreeFellerSVC implements IrisService, IrisTreeFellerService {
private static final String PERMISSION = "iris.treefeller";
private final AtomicBoolean serviceEnabled = new AtomicBoolean();
private final Map<BlockBreakEvent, PendingFell> pending = Collections.synchronizedMap(new IdentityHashMap<>());
private final Set<BlockBreakEvent> managedEvents = Collections.synchronizedSet(
final Set<BlockBreakEvent> managedEvents = Collections.synchronizedSet(
Collections.newSetFromMap(new IdentityHashMap<>())
);
private final Set<TreeClaim> activeClaims = ConcurrentHashMap.newKeySet();
private final Map<UUID, Set<FellingRun>> activeRuns = new ConcurrentHashMap<>();
final Set<TreeClaim> activeClaims = ConcurrentHashMap.newKeySet();
final Map<UUID, Set<FellingRun>> activeRuns = new ConcurrentHashMap<>();
private final Map<Engine, TreeDefinitionIndex> definitions = Collections.synchronizedMap(new WeakHashMap<>());
private final TreeProvenance provenance = new TreeProvenance(definitions);
private final TreeFellingRunner runner = new TreeFellingRunner(this, provenance);
@Override
public void onEnable() {
@@ -92,7 +78,7 @@ public class TreeFellerSVC implements IrisService, IrisTreeFellerService {
managedEvents.clear();
for (Set<FellingRun> runs : activeRuns.values()) {
for (FellingRun run : runs) {
finish(run);
runner.finish(run);
}
}
activeRuns.clear();
@@ -105,8 +91,12 @@ public class TreeFellerSVC implements IrisService, IrisTreeFellerService {
if (!serviceEnabled.get()
|| event == null
|| options == null
|| event.isCancelled()
|| isManagedBreak(event)) {
|| event.isCancelled()) {
return false;
}
// An event that already carries a pending fell may still be upgraded to
// INTEGRATION_OVERRIDE; only internal probes are refused outright.
if (!pending.containsKey(event) && isManagedBreak(event)) {
return false;
}
if (!canUse(event.getPlayer(), options.access())) {
@@ -115,7 +105,7 @@ public class TreeFellerSVC implements IrisService, IrisTreeFellerService {
TreeCandidate candidate;
try {
candidate = resolveCandidate(event.getBlock(), event.getPlayer());
candidate = provenance.resolveCandidate(event.getBlock(), event.getPlayer());
} catch (Throwable error) {
IrisLogging.reportError("Failed to resolve an Iris tree-feller request.", error);
return false;
@@ -153,7 +143,7 @@ public class TreeFellerSVC implements IrisService, IrisTreeFellerService {
return false;
}
try {
return resolveTreeContext(block) != null;
return provenance.resolveTreeContext(block) != null;
} catch (Throwable error) {
IrisLogging.reportError("Failed to inspect Iris tree provenance.", error);
return false;
@@ -186,7 +176,7 @@ public class TreeFellerSVC implements IrisService, IrisTreeFellerService {
TreeCandidate current;
try {
current = resolveCandidate(event.getBlock(), event.getPlayer());
current = provenance.resolveCandidate(event.getBlock(), event.getPlayer());
} catch (Throwable error) {
IrisLogging.reportError("Failed to finalize an Iris tree-feller request.", error);
deferSuccessfulBreakCleanup(event);
@@ -224,38 +214,38 @@ public class TreeFellerSVC implements IrisService, IrisTreeFellerService {
activeRuns.computeIfAbsent(event.getPlayer().getUniqueId(), ignored -> ConcurrentHashMap.newKeySet()).add(run);
notifyActivationAccepted(run.runHooks);
run.presentation.activate(event.getBlock());
discover(run);
runner.discover(run);
}
@EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
public void haltWhenSneakingStops(PlayerToggleSneakEvent event) {
if (!event.isSneaking()) {
finishRuns(event.getPlayer().getUniqueId());
runner.finishRuns(event.getPlayer().getUniqueId());
}
}
@EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
public void haltWhenHeldSlotChanges(PlayerItemHeldEvent event) {
if (event.getNewSlot() != event.getPreviousSlot()) {
finishRuns(event.getPlayer().getUniqueId());
runner.finishRuns(event.getPlayer().getUniqueId());
}
}
@EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
public void haltWhenHandsSwap(PlayerSwapHandItemsEvent event) {
finishRuns(event.getPlayer().getUniqueId());
runner.finishRuns(event.getPlayer().getUniqueId());
}
@EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
public void clearPlacedProvenance(BlockPlaceEvent event) {
ProvenanceSnapshot snapshot = captureProvenance(event.getBlockPlaced());
ProvenanceSnapshot snapshot = provenance.captureProvenance(event.getBlockPlaced());
if (snapshot == null) {
return;
}
Location location = event.getBlockPlaced().getLocation();
Runnable cleanup = () -> {
if (!event.isCancelled()) {
clearProvenanceIfMatching(snapshot);
provenance.clearProvenanceIfMatching(snapshot);
}
};
if (!J.runAt(location, cleanup, 1) && !J.isFolia()) {
@@ -263,6 +253,10 @@ public class TreeFellerSVC implements IrisService, IrisTreeFellerService {
}
}
boolean isServiceEnabled() {
return serviceEnabled.get();
}
private boolean canUse(Player player, TreeFellerAccess access) {
if (access == TreeFellerAccess.INTEGRATION_OVERRIDE) {
return true;
@@ -287,616 +281,19 @@ public class TreeFellerSVC implements IrisService, IrisTreeFellerService {
}
}
private TreeCandidate resolveCandidate(Block block, Player player) {
if (player.getGameMode() != GameMode.SURVIVAL || !player.isSneaking()) {
return null;
}
if (!Tag.LOGS.isTagged(block.getType())) {
return null;
}
ItemStack tool = player.getInventory().getItemInMainHand();
if (!isAxe(tool)) {
return null;
}
TreeContext context = resolveTreeContext(block);
if (context == null) {
return null;
}
return new TreeCandidate(
context,
block.getWorld(),
player,
tool.clone(),
TreeFellerAccess.STANDALONE,
new TreeMarkerTraversal.Position(block.getX(), block.getY(), block.getZ())
);
}
private TreeContext resolveTreeContext(Block block) {
if (block.getType().isAir()) {
return null;
}
PlatformChunkGenerator access = IrisToolbelt.access(block.getWorld());
if (access == null || access.getEngine() == null) {
return null;
}
Engine engine = access.getEngine();
World world = block.getWorld();
int minimumY = world.getMinHeight();
int maximumY = world.getMaxHeight();
int relativeY = block.getY() - minimumY;
String marker = markerAt(engine, minimumY, block.getX(), block.getY(), block.getZ());
StructurePlacementMarker.Decoded decoded = StructurePlacementMarker.decode(marker);
if (decoded == null || decoded.structureAware()) {
return null;
}
TreeBlockMaterial expected = engine.getMantle().getMantle().get(
block.getX(),
relativeY,
block.getZ(),
TreeBlockMaterial.class
);
if (expected != null && !matchesExpectedMaterial(block, expected)) {
return null;
}
if (expected == null
&& !decoded.objectKey().startsWith("trees/")
&& !definitionIndex(engine).isTreeMarker(marker)) {
return null;
}
return new TreeContext(engine, marker, expected, minimumY, maximumY);
}
private TreeDefinitionIndex definitionIndex(Engine engine) {
synchronized (definitions) {
return definitions.computeIfAbsent(engine, TreeDefinitionIndex::build);
}
}
private void discover(FellingRun run) {
J.a(() -> {
if (run.candidate.context().engine().isClosed()) {
finish(run);
return;
}
try {
TreeMarkerTraversal.Discovery discovery = TreeMarkerTraversal.discover(
run.candidate.trigger(),
run.candidate.context().marker(),
run.candidate.context().minimumY(),
run.candidate.context().maximumY(),
(x, y, z) -> markerAt(
run.candidate.context().engine(),
run.candidate.context().minimumY(),
x,
y,
z
)
);
List<TreeMarkerTraversal.Position> positions = positionsForFelling(discovery, run.candidate.trigger());
preflight(run, positions, discovery.complete());
} catch (Throwable error) {
IrisLogging.reportError("Failed to discover an Iris tree for felling.", error);
preflight(run, List.of(run.candidate.trigger()), false);
}
});
}
private void preflight(FellingRun run, List<TreeMarkerTraversal.Position> positions, boolean allowFallback) {
Map<ChunkPosition, List<TreeMarkerTraversal.Position>> grouped = groupByChunk(positions);
if (grouped.isEmpty()) {
finish(run);
return;
}
Map<TreeMarkerTraversal.Position, Integer> erosionOrder = new HashMap<>(positions.size());
for (int index = 0; index < positions.size(); index++) {
erosionOrder.put(positions.get(index), index);
}
List<TreeMember> members = Collections.synchronizedList(new ArrayList<>());
AtomicBoolean failed = new AtomicBoolean();
AtomicInteger remaining = new AtomicInteger(grouped.size());
AtomicBoolean completed = new AtomicBoolean();
for (Map.Entry<ChunkPosition, List<TreeMarkerTraversal.Position>> entry : grouped.entrySet()) {
ChunkPosition chunk = entry.getKey();
Runnable task = () -> {
try {
if (!run.candidate.world().isChunkLoaded(chunk.x(), chunk.z())) {
failed.set(true);
return;
}
for (TreeMarkerTraversal.Position position : entry.getValue()) {
TreeMember member = inspectMember(run, position, erosionOrder.getOrDefault(position, Integer.MAX_VALUE));
if (member != null) {
members.add(member);
}
}
} catch (Throwable error) {
failed.set(true);
IrisLogging.reportError(
"Failed to preflight an Iris tree-feller chunk at " + chunk.x() + "," + chunk.z() + ".",
error
);
} finally {
completePreflightGroup(run, members, failed, remaining, completed, allowFallback);
}
};
if (!J.runRegion(run.candidate.world(), chunk.x(), chunk.z(), task)) {
failed.set(true);
completePreflightGroup(run, members, failed, remaining, completed, allowFallback);
}
}
}
static List<TreeMarkerTraversal.Position> positionsForFelling(
TreeMarkerTraversal.Discovery discovery,
TreeMarkerTraversal.Position trigger
) {
return discovery.complete() ? discovery.members() : List.of(trigger);
}
private void completePreflightGroup(
FellingRun run,
List<TreeMember> members,
AtomicBoolean failed,
AtomicInteger remaining,
AtomicBoolean completed,
boolean allowFallback
) {
if (remaining.decrementAndGet() != 0 || !completed.compareAndSet(false, true)) {
return;
}
if (failed.get() && allowFallback) {
preflight(run, List.of(run.candidate.trigger()), false);
return;
}
if (failed.get()) {
finish(run);
return;
}
List<TreeMember> ordered = orderMembers(run.candidate.trigger(), members);
if (ordered.isEmpty() || !ordered.getFirst().position().equals(run.candidate.trigger())) {
finish(run);
return;
}
run.work = ordered;
run.blocksPerPulse = TreeFellerPresentation.blocksPerPulse(ordered.size());
run.effectStride = TreeFellerPresentation.effectStride(run.blocksPerPulse);
processNext(run);
}
private TreeMember inspectMember(FellingRun run, TreeMarkerTraversal.Position position, int erosionOrder) {
World world = run.candidate.world();
Block block = world.getBlockAt(position.x(), position.y(), position.z());
TreeContext context = run.candidate.context();
if (!context.marker().equals(markerAt(context.engine(), context.minimumY(), position))) {
return null;
}
TreeBlockMaterial expected = materialAt(context.engine(), context.minimumY(), position);
if (expected != null && !matchesExpectedMaterial(block, expected)) {
clearProvenance(context.engine(), context.minimumY(), position);
return null;
}
if (block.getType().isAir()) {
clearProvenance(context.engine(), context.minimumY(), position);
return null;
}
return new TreeMember(position, Tag.LOGS.isTagged(block.getType()), expected, erosionOrder);
}
private List<TreeMember> orderMembers(
TreeMarkerTraversal.Position trigger,
Collection<TreeMember> discovered
) {
Comparator<TreeMember> erosionOrder = Comparator
.comparingInt(TreeMember::erosionOrder)
.thenComparingInt(member -> member.position().y())
.thenComparingInt(member -> member.position().x())
.thenComparingInt(member -> member.position().z());
List<TreeMember> ordered = discovered.stream().sorted(erosionOrder).toList();
if (ordered.isEmpty() || !ordered.getFirst().position().equals(trigger)) {
return List.of();
}
return ordered;
}
private void processNext(FellingRun run) {
if (run.finished.get()) {
return;
}
int index = run.cursor.getAndIncrement();
if (index >= run.work.size()) {
finish(run);
return;
}
TreeMember member = run.work.get(index);
ChunkPosition chunk = new ChunkPosition(member.position().x() >> 4, member.position().z() >> 4);
Runnable task = () -> runTask(
run,
"Failed to prepare an Iris tree-feller block.",
() -> prepareBreak(run, member)
);
if (!J.runRegion(run.candidate.world(), chunk.x(), chunk.z(), task)) {
if (member.log()) {
finish(run);
} else {
continueRun(run);
}
}
}
private void prepareBreak(FellingRun run, TreeMember member) {
Block block = liveMemberBlock(run, member);
if (block == null) {
if (member.log()) {
finish(run);
} else {
continueRun(run);
}
return;
}
if (!member.log()) {
runMutationTask(
run,
member,
new DamageReservation(run.expectedTool.clone(), false, false, false),
new AtomicBoolean()
);
return;
}
Runnable task = () -> runTask(
run,
"Failed to reserve Iris tree-feller tool durability.",
() -> reserveDamage(run, member)
);
if (!J.runEntity(run.candidate.player(), task)) {
finish(run);
}
}
private void reserveDamage(FellingRun run, TreeMember member) {
Player player = run.candidate.player();
if (!isRunControlActive(run, player)) {
finish(run);
return;
}
PlayerInventory inventory = player.getInventory();
ItemStack current = inventory.getItem(run.heldSlot);
if (current == null
|| inventory.getHeldItemSlot() != run.heldSlot
|| !current.isSimilar(run.expectedTool)
|| !isAxe(current)) {
finish(run);
return;
}
if (!reserveLogCost(run)) {
finish(run);
return;
}
ItemStack before = current.clone();
ItemMeta meta = current.getItemMeta();
if (meta.isUnbreakable() || ThreadLocalRandom.current().nextInt(100) < run.preservationChance) {
scheduleMutation(run, member, new DamageReservation(before, false, false, true));
return;
}
if (!(meta instanceof Damageable damageable) || current.getType().getMaxDurability() <= 0) {
refundAndFinish(run, new DamageReservation(before, false, false, true));
return;
}
int nextDamage = damageable.getDamage() + 1;
boolean broke = nextDamage >= current.getType().getMaxDurability();
if (broke) {
inventory.setItem(run.heldSlot, new ItemStack(Material.AIR));
run.expectedTool = new ItemStack(Material.AIR);
} else {
damageable.setDamage(nextDamage);
current.setItemMeta(meta);
inventory.setItem(run.heldSlot, current);
run.expectedTool = current.clone();
}
scheduleMutation(run, member, new DamageReservation(before, true, broke, true));
}
private void scheduleMutation(FellingRun run, TreeMember member, DamageReservation reservation) {
TreeMarkerTraversal.Position position = member.position();
AtomicBoolean mutationSucceeded = new AtomicBoolean();
Runnable task = () -> runMutationTask(run, member, reservation, mutationSucceeded);
boolean scheduled;
try {
scheduled = J.runRegion(
run.candidate.world(),
position.x() >> 4,
position.z() >> 4,
task
);
} catch (Throwable error) {
IrisLogging.reportError("Failed to schedule an Iris tree-feller block removal.", error);
refundAndFinish(run, reservation);
return;
}
if (!scheduled) {
refundAndFinish(run, reservation);
}
}
private void runMutationTask(
FellingRun run,
TreeMember member,
DamageReservation reservation,
AtomicBoolean mutationSucceeded
) {
if (run.finished.get()) {
refundAndFinish(run, reservation);
return;
}
try {
probeAndMutate(run, member, reservation, mutationSucceeded);
} catch (Throwable error) {
IrisLogging.reportError("Failed to remove an Iris tree-feller block.", error);
if (mutationSucceeded.get()) {
finish(run);
} else {
refundAndFinish(run, reservation);
}
}
}
private void probeAndMutate(
FellingRun run,
TreeMember member,
DamageReservation reservation,
AtomicBoolean mutationSucceeded
) {
Block block = liveMemberBlock(run, member);
if (block == null) {
refundAndFinish(run, reservation);
return;
}
BlockBreakEvent probe = new RoutedBlockBreakEvent(block, run.candidate.player(), run);
managedEvents.add(probe);
try {
Bukkit.getPluginManager().callEvent(probe);
} catch (Throwable error) {
probe.setCancelled(true);
IrisLogging.reportError("Failed to dispatch an Iris tree-feller block probe.", error);
} finally {
managedEvents.remove(probe);
}
try {
if (probe.isCancelled()) {
if (reservation.charged() || reservation.logCostReserved()) {
refundAndFinish(run, reservation);
} else if (member.log()) {
finish(run);
} else {
continueRun(run);
}
return;
}
block = liveMemberBlock(run, member);
if (run.finished.get() || block == null) {
probe.setCancelled(true);
refundAndFinish(run, reservation);
return;
}
Location source = block.getLocation().clone().add(0.5D, 0.5D, 0.5D);
BlockData visualData = block.getBlockData().clone();
List<ItemStack> vanillaDrops = probe.isDropItems()
? block.getDrops(reservation.toolForDrops()).stream()
.map(ItemStack::clone)
.toList()
: List.of();
block.setType(Material.AIR, false);
if (!block.getType().isAir()) {
probe.setCancelled(true);
refundAndFinish(run, reservation);
return;
}
mutationSucceeded.set(true);
run.presentation.erode(
source,
visualData,
member.erosionOrder(),
run.processed.get(),
run.blocksPerPulse,
run.effectStride,
run.work.size()
);
clearProvenance(
run.candidate.context().engine(),
run.candidate.context().minimumY(),
member.position()
);
routeDrops(run, vanillaDrops, source);
if (!run.presentation.routeExperience(probe.getExpToDrop())) {
dropExperience(source, probe.getExpToDrop());
}
if (reservation.logCostReserved()) {
completeLogCost(run, reservation);
return;
}
completeSuccessfulMutation(run, reservation);
} catch (RuntimeException | Error error) {
if (!mutationSucceeded.get()) {
probe.setCancelled(true);
}
throw error;
}
}
private Block liveMemberBlock(FellingRun run, TreeMember member) {
World world = run.candidate.world();
TreeMarkerTraversal.Position position = member.position();
if (!world.isChunkLoaded(position.x() >> 4, position.z() >> 4)) {
return null;
}
Block block = world.getBlockAt(position.x(), position.y(), position.z());
TreeContext context = run.candidate.context();
if (!context.marker().equals(markerAt(context.engine(), context.minimumY(), position))) {
return null;
}
if (block.getType().isAir() || member.log() != Tag.LOGS.isTagged(block.getType())) {
clearProvenance(context.engine(), context.minimumY(), position);
return null;
}
TreeBlockMaterial expected = materialAt(context.engine(), context.minimumY(), position);
if (member.expectedMaterial() != null && !member.expectedMaterial().equals(expected)) {
clearProvenance(context.engine(), context.minimumY(), position);
return null;
}
if (expected != null && !matchesExpectedMaterial(block, expected)) {
clearProvenance(context.engine(), context.minimumY(), position);
return null;
}
return block;
}
private void refundAndFinish(FellingRun run, DamageReservation reservation) {
if (!reservation.charged() && !reservation.logCostReserved()) {
finish(run);
return;
}
if (!J.runEntity(run.candidate.player(), () -> {
if (reservation.charged()) {
PlayerInventory inventory = run.candidate.player().getInventory();
ItemStack current = inventory.getItem(run.heldSlot);
boolean expectedAir = run.expectedTool.getType() == Material.AIR;
boolean currentMatches = expectedAir
? current == null || current.getType() == Material.AIR
: current != null && current.isSimilar(run.expectedTool);
if (currentMatches) {
inventory.setItem(run.heldSlot, reservation.toolForDrops().clone());
run.expectedTool = reservation.toolForDrops().clone();
}
}
if (reservation.logCostReserved()) {
refundLogCost(run);
}
finish(run);
})) {
finish(run);
}
}
private boolean isRunControlActive(FellingRun run, Player player) {
return player.isOnline()
&& player.getGameMode() == GameMode.SURVIVAL
&& player.isSneaking()
&& player.getWorld().equals(run.candidate.world());
}
private boolean reserveLogCost(FellingRun run) {
try {
return run.runHooks.reserveLogCost();
} catch (Throwable error) {
IrisLogging.reportError("An Iris tree-feller integration log-cost reservation failed.", error);
return false;
}
}
private void completeLogCost(FellingRun run, DamageReservation reservation) {
if (!J.runEntity(run.candidate.player(), () -> {
try {
run.runHooks.commitLogCost();
} catch (Throwable error) {
IrisLogging.reportError("An Iris tree-feller integration log-cost commit failed.", error);
finish(run);
return;
}
completeSuccessfulMutation(run, reservation);
})) {
finish(run);
}
}
private void refundLogCost(FellingRun run) {
try {
run.runHooks.refundLogCost();
} catch (Throwable error) {
IrisLogging.reportError("An Iris tree-feller integration log-cost refund failed.", error);
}
}
private void completeSuccessfulMutation(FellingRun run, DamageReservation reservation) {
if (reservation.broke()) {
finish(run);
return;
}
continueRun(run);
}
private void continueRun(FellingRun run) {
int processed = run.processed.incrementAndGet();
if (processed % run.blocksPerPulse == 0) {
J.s(() -> runTask(run, "Failed to continue an Iris tree-feller run.", () -> processNext(run)), 1);
} else {
processNext(run);
}
}
private void runTask(FellingRun run, String context, Runnable task) {
if (run.finished.get() || !serviceEnabled.get()) {
finish(run);
return;
}
try {
task.run();
} catch (Throwable error) {
IrisLogging.reportError(context, error);
finish(run);
}
}
private void finish(FellingRun run) {
if (run.finished.compareAndSet(false, true)) {
activeClaims.remove(run.claim);
Set<FellingRun> runs = activeRuns.get(run.candidate.player().getUniqueId());
if (runs != null) {
runs.remove(run);
if (runs.isEmpty()) {
activeRuns.remove(run.candidate.player().getUniqueId(), runs);
}
}
run.presentation.finish();
}
}
private void finishRuns(UUID playerId) {
Set<FellingRun> runs = activeRuns.get(playerId);
if (runs == null) {
return;
}
for (FellingRun run : List.copyOf(runs)) {
finish(run);
}
}
private void releaseManagedLater(BlockBreakEvent event) {
J.s(() -> managedEvents.remove(event), 1);
}
private void deferSuccessfulBreakCleanup(BlockBreakEvent event) {
ProvenanceSnapshot snapshot = captureProvenance(event.getBlock());
ProvenanceSnapshot snapshot = provenance.captureProvenance(event.getBlock());
if (snapshot == null) {
return;
}
Location location = event.getBlock().getLocation();
Runnable cleanup = () -> {
if (!event.isCancelled()) {
clearProvenanceIfMatching(snapshot);
provenance.clearProvenanceIfMatching(snapshot);
}
};
if (!J.runAt(location, cleanup, 1) && !J.isFolia()) {
@@ -904,77 +301,11 @@ public class TreeFellerSVC implements IrisService, IrisTreeFellerService {
}
}
private Map<ChunkPosition, List<TreeMarkerTraversal.Position>> groupByChunk(
List<TreeMarkerTraversal.Position> positions
) {
Map<ChunkPosition, List<TreeMarkerTraversal.Position>> grouped = new LinkedHashMap<>();
for (TreeMarkerTraversal.Position position : positions) {
ChunkPosition chunk = new ChunkPosition(position.x() >> 4, position.z() >> 4);
grouped.computeIfAbsent(chunk, ignored -> new ArrayList<>()).add(position);
}
return grouped;
}
private String markerAt(Engine engine, int minimumY, TreeMarkerTraversal.Position position) {
return markerAt(engine, minimumY, position.x(), position.y(), position.z());
}
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, TreeMarkerTraversal.Position position) {
return engine.getMantle().getMantle().get(
position.x(),
position.y() - minimumY,
position.z(),
TreeBlockMaterial.class
);
}
private ProvenanceSnapshot captureProvenance(Block block) {
PlatformChunkGenerator access = IrisToolbelt.access(block.getWorld());
if (access == null || access.getEngine() == null) {
return null;
}
Engine engine = access.getEngine();
TreeMarkerTraversal.Position position = new TreeMarkerTraversal.Position(
block.getX(),
block.getY(),
block.getZ()
);
int minimumY = block.getWorld().getMinHeight();
String marker = markerAt(engine, minimumY, position);
TreeBlockMaterial material = materialAt(engine, minimumY, position);
if (marker == null && material == null) {
return null;
}
return new ProvenanceSnapshot(engine, block.getWorld(), minimumY, position, marker, material);
}
private void clearProvenanceIfMatching(ProvenanceSnapshot snapshot) {
String marker = markerAt(snapshot.engine(), snapshot.minimumY(), snapshot.position());
TreeBlockMaterial material = materialAt(snapshot.engine(), snapshot.minimumY(), snapshot.position());
if (Objects.equals(snapshot.marker(), marker) && Objects.equals(snapshot.material(), material)) {
clearProvenance(snapshot.engine(), snapshot.minimumY(), snapshot.position());
}
}
private void clearProvenance(Engine engine, int minimumY, TreeMarkerTraversal.Position position) {
int relativeY = position.y() - minimumY;
engine.getMantle().getMantle().remove(position.x(), relativeY, position.z(), String.class);
engine.getMantle().getMantle().remove(position.x(), relativeY, position.z(), TreeBlockMaterial.class);
}
private boolean matchesExpectedMaterial(Block block, TreeBlockMaterial expected) {
return expected.matches(block.getBlockData().getAsString());
}
private boolean isAxe(ItemStack item) {
static boolean isAxe(ItemStack item) {
return item != null && item.getType() != Material.AIR && item.getType().name().endsWith("_AXE");
}
private void routeDrops(FellingRun run, List<ItemStack> drops, Location source) {
void routeDrops(FellingRun run, List<ItemStack> drops, Location source) {
World world = source.getWorld();
if (world == null) {
return;
@@ -986,7 +317,7 @@ public class TreeFellerSVC implements IrisService, IrisTreeFellerService {
}
}
private void dropExperience(Location location, int experience) {
void dropExperience(Location location, int experience) {
if (experience <= 0 || location.getWorld() == null) {
return;
}
@@ -996,115 +327,4 @@ public class TreeFellerSVC implements IrisService, IrisTreeFellerService {
);
orb.setExperience(experience);
}
private record PendingFell(
TreeCandidate candidate,
int preservationChance,
TreeFellerRunHooks runHooks
) {
private PendingFell withAccess(TreeFellerAccess access) {
return new PendingFell(candidate.withAccess(access), preservationChance, runHooks);
}
}
private record TreeCandidate(
TreeContext context,
World world,
Player player,
ItemStack tool,
TreeFellerAccess access,
TreeMarkerTraversal.Position trigger
) {
private TreeCandidate withAccess(TreeFellerAccess access) {
return new TreeCandidate(context, world, player, tool, access, trigger);
}
}
private record TreeContext(
Engine engine,
String marker,
TreeBlockMaterial expectedMaterial,
int minimumY,
int maximumY
) {
}
private record TreeClaim(UUID worldId, String marker) {
}
private record ProvenanceSnapshot(
Engine engine,
World world,
int minimumY,
TreeMarkerTraversal.Position position,
String marker,
TreeBlockMaterial material
) {
}
private record ChunkPosition(int x, int z) {
}
private record TreeMember(
TreeMarkerTraversal.Position position,
boolean log,
TreeBlockMaterial expectedMaterial,
int erosionOrder
) {
}
private record DamageReservation(
ItemStack toolForDrops,
boolean charged,
boolean broke,
boolean logCostReserved
) {
}
private final class RoutedBlockBreakEvent extends BlockBreakEvent implements BlockDropRouter {
private final FellingRun run;
private RoutedBlockBreakEvent(Block block, Player player, FellingRun run) {
super(block, player);
this.run = run;
}
@Override
public boolean routeDrop(Object drop) {
return serviceEnabled.get() && run.presentation.routeDrop(drop);
}
}
private static final class FellingRun {
private final TreeClaim claim;
private final TreeCandidate candidate;
private final int preservationChance;
private final TreeFellerRunHooks runHooks;
private final int heldSlot;
private final TreeFellerPresentation presentation;
private final AtomicBoolean finished = new AtomicBoolean();
private final AtomicInteger cursor = new AtomicInteger();
private final AtomicInteger processed = new AtomicInteger();
private volatile int blocksPerPulse = 1;
private volatile int effectStride = 1;
private volatile ItemStack expectedTool;
private volatile List<TreeMember> work = List.of();
private FellingRun(
TreeClaim claim,
TreeCandidate candidate,
int preservationChance,
TreeFellerRunHooks runHooks,
int heldSlot,
Location fallbackLocation
) {
this.claim = claim;
this.candidate = candidate;
this.preservationChance = preservationChance;
this.runHooks = runHooks;
this.heldSlot = heldSlot;
this.presentation = new TreeFellerPresentation(candidate.player(), candidate.world(), fallbackLocation);
this.expectedTool = candidate.tool().clone();
}
}
}
@@ -0,0 +1,590 @@
package art.arcane.iris.core.service;
import art.arcane.iris.core.service.TreeFellerModel.ChunkPosition;
import art.arcane.iris.core.service.TreeFellerModel.DamageReservation;
import art.arcane.iris.core.service.TreeFellerModel.TreeContext;
import art.arcane.iris.core.service.TreeFellerModel.TreeMember;
import art.arcane.iris.core.service.tree.TreeMarkerTraversal;
import art.arcane.iris.engine.framework.TreeBlockMaterial;
import art.arcane.iris.spi.IrisLogging;
import art.arcane.iris.util.common.scheduling.J;
import org.bukkit.Bukkit;
import org.bukkit.GameMode;
import org.bukkit.Location;
import org.bukkit.Material;
import org.bukkit.Tag;
import org.bukkit.World;
import org.bukkit.block.Block;
import org.bukkit.block.data.BlockData;
import org.bukkit.entity.Player;
import org.bukkit.event.block.BlockBreakEvent;
import org.bukkit.inventory.ItemStack;
import org.bukkit.inventory.PlayerInventory;
import org.bukkit.inventory.meta.Damageable;
import org.bukkit.inventory.meta.ItemMeta;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.ThreadLocalRandom;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
final class TreeFellingRunner {
private final TreeFellerSVC service;
private final TreeProvenance provenance;
TreeFellingRunner(TreeFellerSVC service, TreeProvenance provenance) {
this.service = service;
this.provenance = provenance;
}
void discover(FellingRun run) {
J.a(() -> {
if (run.candidate.context().engine().isClosed()) {
finish(run);
return;
}
try {
TreeMarkerTraversal.Discovery discovery = TreeMarkerTraversal.discover(
run.candidate.trigger(),
run.candidate.context().marker(),
run.candidate.context().minimumY(),
run.candidate.context().maximumY(),
(x, y, z) -> provenance.markerAt(
run.candidate.context().engine(),
run.candidate.context().minimumY(),
x,
y,
z
)
);
List<TreeMarkerTraversal.Position> positions = positionsForFelling(discovery, run.candidate.trigger());
preflight(run, positions, discovery.complete());
} catch (Throwable error) {
IrisLogging.reportError("Failed to discover an Iris tree for felling.", error);
preflight(run, List.of(run.candidate.trigger()), false);
}
});
}
private void preflight(FellingRun run, List<TreeMarkerTraversal.Position> positions, boolean allowFallback) {
Map<ChunkPosition, List<TreeMarkerTraversal.Position>> grouped = groupByChunk(positions);
if (grouped.isEmpty()) {
finish(run);
return;
}
Map<TreeMarkerTraversal.Position, Integer> erosionOrder = new HashMap<>(positions.size());
for (int index = 0; index < positions.size(); index++) {
erosionOrder.put(positions.get(index), index);
}
List<TreeMember> members = Collections.synchronizedList(new ArrayList<>());
AtomicBoolean failed = new AtomicBoolean();
AtomicInteger remaining = new AtomicInteger(grouped.size());
AtomicBoolean completed = new AtomicBoolean();
for (Map.Entry<ChunkPosition, List<TreeMarkerTraversal.Position>> entry : grouped.entrySet()) {
ChunkPosition chunk = entry.getKey();
Runnable task = () -> {
try {
if (!run.candidate.world().isChunkLoaded(chunk.x(), chunk.z())) {
failed.set(true);
return;
}
for (TreeMarkerTraversal.Position position : entry.getValue()) {
TreeMember member = inspectMember(run, position, erosionOrder.getOrDefault(position, Integer.MAX_VALUE));
if (member != null) {
members.add(member);
}
}
} catch (Throwable error) {
failed.set(true);
IrisLogging.reportError(
"Failed to preflight an Iris tree-feller chunk at " + chunk.x() + "," + chunk.z() + ".",
error
);
} finally {
completePreflightGroup(run, members, failed, remaining, completed, allowFallback);
}
};
if (!J.runRegion(run.candidate.world(), chunk.x(), chunk.z(), task)) {
failed.set(true);
completePreflightGroup(run, members, failed, remaining, completed, allowFallback);
}
}
}
static List<TreeMarkerTraversal.Position> positionsForFelling(
TreeMarkerTraversal.Discovery discovery,
TreeMarkerTraversal.Position trigger
) {
return discovery.complete() ? discovery.members() : List.of(trigger);
}
private void completePreflightGroup(
FellingRun run,
List<TreeMember> members,
AtomicBoolean failed,
AtomicInteger remaining,
AtomicBoolean completed,
boolean allowFallback
) {
if (remaining.decrementAndGet() != 0 || !completed.compareAndSet(false, true)) {
return;
}
if (failed.get() && allowFallback) {
preflight(run, List.of(run.candidate.trigger()), false);
return;
}
if (failed.get()) {
finish(run);
return;
}
List<TreeMember> ordered = orderMembers(run.candidate.trigger(), members);
if (ordered.isEmpty() || !ordered.getFirst().position().equals(run.candidate.trigger())) {
finish(run);
return;
}
run.work = ordered;
run.blocksPerPulse = TreeFellerPresentation.blocksPerPulse(ordered.size());
run.effectStride = TreeFellerPresentation.effectStride(run.blocksPerPulse);
processNext(run);
}
private TreeMember inspectMember(FellingRun run, TreeMarkerTraversal.Position position, int erosionOrder) {
World world = run.candidate.world();
Block block = world.getBlockAt(position.x(), position.y(), position.z());
TreeContext context = run.candidate.context();
if (!context.marker().equals(provenance.markerAt(context.engine(), context.minimumY(), position))) {
return null;
}
TreeBlockMaterial expected = provenance.materialAt(context.engine(), context.minimumY(), position);
if (expected != null && !provenance.matchesExpectedMaterial(block, expected)) {
provenance.clearProvenance(context.engine(), context.minimumY(), position);
return null;
}
if (block.getType().isAir()) {
provenance.clearProvenance(context.engine(), context.minimumY(), position);
return null;
}
return new TreeMember(position, Tag.LOGS.isTagged(block.getType()), expected, erosionOrder);
}
private List<TreeMember> orderMembers(
TreeMarkerTraversal.Position trigger,
Collection<TreeMember> discovered
) {
Comparator<TreeMember> erosionOrder = Comparator
.comparingInt(TreeMember::erosionOrder)
.thenComparingInt(member -> member.position().y())
.thenComparingInt(member -> member.position().x())
.thenComparingInt(member -> member.position().z());
List<TreeMember> ordered = discovered.stream().sorted(erosionOrder).toList();
if (ordered.isEmpty() || !ordered.getFirst().position().equals(trigger)) {
return List.of();
}
return ordered;
}
private void processNext(FellingRun run) {
if (run.finished.get()) {
return;
}
int index = run.cursor.getAndIncrement();
if (index >= run.work.size()) {
finish(run);
return;
}
TreeMember member = run.work.get(index);
ChunkPosition chunk = new ChunkPosition(member.position().x() >> 4, member.position().z() >> 4);
Runnable task = () -> runTask(
run,
"Failed to prepare an Iris tree-feller block.",
() -> prepareBreak(run, member)
);
if (!J.runRegion(run.candidate.world(), chunk.x(), chunk.z(), task)) {
if (member.log()) {
finish(run);
} else {
continueRun(run);
}
}
}
private void prepareBreak(FellingRun run, TreeMember member) {
Block block = liveMemberBlock(run, member);
if (block == null) {
if (member.log()) {
finish(run);
} else {
continueRun(run);
}
return;
}
if (!member.log()) {
runMutationTask(
run,
member,
new DamageReservation(run.expectedTool.clone(), false, false, false),
new AtomicBoolean()
);
return;
}
Runnable task = () -> runTask(
run,
"Failed to reserve Iris tree-feller tool durability.",
() -> reserveDamage(run, member)
);
if (!J.runEntity(run.candidate.player(), task)) {
finish(run);
}
}
private void reserveDamage(FellingRun run, TreeMember member) {
Player player = run.candidate.player();
if (!isRunControlActive(run, player)) {
finish(run);
return;
}
PlayerInventory inventory = player.getInventory();
ItemStack current = inventory.getItem(run.heldSlot);
if (current == null
|| inventory.getHeldItemSlot() != run.heldSlot
|| !current.isSimilar(run.expectedTool)
|| !TreeFellerSVC.isAxe(current)) {
finish(run);
return;
}
if (!reserveLogCost(run)) {
finish(run);
return;
}
ItemStack before = current.clone();
ItemMeta meta = current.getItemMeta();
if (meta.isUnbreakable() || ThreadLocalRandom.current().nextInt(100) < run.preservationChance) {
scheduleMutation(run, member, new DamageReservation(before, false, false, true));
return;
}
if (!(meta instanceof Damageable damageable) || current.getType().getMaxDurability() <= 0) {
refundAndFinish(run, new DamageReservation(before, false, false, true));
return;
}
int nextDamage = damageable.getDamage() + 1;
boolean broke = nextDamage >= current.getType().getMaxDurability();
if (broke) {
inventory.setItem(run.heldSlot, new ItemStack(Material.AIR));
run.expectedTool = new ItemStack(Material.AIR);
} else {
damageable.setDamage(nextDamage);
current.setItemMeta(meta);
inventory.setItem(run.heldSlot, current);
run.expectedTool = current.clone();
}
scheduleMutation(run, member, new DamageReservation(before, true, broke, true));
}
private void scheduleMutation(FellingRun run, TreeMember member, DamageReservation reservation) {
TreeMarkerTraversal.Position position = member.position();
AtomicBoolean mutationSucceeded = new AtomicBoolean();
Runnable task = () -> runMutationTask(run, member, reservation, mutationSucceeded);
boolean scheduled;
try {
scheduled = J.runRegion(
run.candidate.world(),
position.x() >> 4,
position.z() >> 4,
task
);
} catch (Throwable error) {
IrisLogging.reportError("Failed to schedule an Iris tree-feller block removal.", error);
refundAndFinish(run, reservation);
return;
}
if (!scheduled) {
refundAndFinish(run, reservation);
}
}
private void runMutationTask(
FellingRun run,
TreeMember member,
DamageReservation reservation,
AtomicBoolean mutationSucceeded
) {
if (run.finished.get()) {
refundAndFinish(run, reservation);
return;
}
try {
probeAndMutate(run, member, reservation, mutationSucceeded);
} catch (Throwable error) {
IrisLogging.reportError("Failed to remove an Iris tree-feller block.", error);
if (mutationSucceeded.get()) {
finish(run);
} else {
refundAndFinish(run, reservation);
}
}
}
private void probeAndMutate(
FellingRun run,
TreeMember member,
DamageReservation reservation,
AtomicBoolean mutationSucceeded
) {
Block block = liveMemberBlock(run, member);
if (block == null) {
refundAndFinish(run, reservation);
return;
}
BlockBreakEvent probe = new RoutedBlockBreakEvent(block, run.candidate.player(), run, service);
service.managedEvents.add(probe);
try {
Bukkit.getPluginManager().callEvent(probe);
} catch (Throwable error) {
probe.setCancelled(true);
IrisLogging.reportError("Failed to dispatch an Iris tree-feller block probe.", error);
} finally {
service.managedEvents.remove(probe);
}
try {
if (probe.isCancelled()) {
if (reservation.charged() || reservation.logCostReserved()) {
refundAndFinish(run, reservation);
} else if (member.log()) {
finish(run);
} else {
continueRun(run);
}
return;
}
block = liveMemberBlock(run, member);
if (run.finished.get() || block == null) {
probe.setCancelled(true);
refundAndFinish(run, reservation);
return;
}
Location source = block.getLocation().clone().add(0.5D, 0.5D, 0.5D);
BlockData visualData = block.getBlockData().clone();
List<ItemStack> vanillaDrops = probe.isDropItems()
? block.getDrops(reservation.toolForDrops()).stream()
.map(ItemStack::clone)
.toList()
: List.of();
block.setType(Material.AIR, false);
if (!block.getType().isAir()) {
probe.setCancelled(true);
refundAndFinish(run, reservation);
return;
}
mutationSucceeded.set(true);
run.presentation.erode(
source,
visualData,
member.erosionOrder(),
run.processed.get(),
run.blocksPerPulse,
run.effectStride,
run.work.size()
);
provenance.clearProvenance(
run.candidate.context().engine(),
run.candidate.context().minimumY(),
member.position()
);
service.routeDrops(run, vanillaDrops, source);
if (!run.presentation.routeExperience(probe.getExpToDrop())) {
service.dropExperience(source, probe.getExpToDrop());
}
if (reservation.logCostReserved()) {
completeLogCost(run, reservation);
return;
}
completeSuccessfulMutation(run, reservation);
} catch (RuntimeException | Error error) {
if (!mutationSucceeded.get()) {
probe.setCancelled(true);
}
throw error;
}
}
private Block liveMemberBlock(FellingRun run, TreeMember member) {
World world = run.candidate.world();
TreeMarkerTraversal.Position position = member.position();
if (!world.isChunkLoaded(position.x() >> 4, position.z() >> 4)) {
return null;
}
Block block = world.getBlockAt(position.x(), position.y(), position.z());
TreeContext context = run.candidate.context();
if (!context.marker().equals(provenance.markerAt(context.engine(), context.minimumY(), position))) {
return null;
}
if (block.getType().isAir() || member.log() != Tag.LOGS.isTagged(block.getType())) {
provenance.clearProvenance(context.engine(), context.minimumY(), position);
return null;
}
TreeBlockMaterial expected = provenance.materialAt(context.engine(), context.minimumY(), position);
if (member.expectedMaterial() != null && !member.expectedMaterial().equals(expected)) {
provenance.clearProvenance(context.engine(), context.minimumY(), position);
return null;
}
if (expected != null && !provenance.matchesExpectedMaterial(block, expected)) {
provenance.clearProvenance(context.engine(), context.minimumY(), position);
return null;
}
return block;
}
private void refundAndFinish(FellingRun run, DamageReservation reservation) {
if (!reservation.charged() && !reservation.logCostReserved()) {
finish(run);
return;
}
if (!J.runEntity(run.candidate.player(), () -> {
if (reservation.charged()) {
PlayerInventory inventory = run.candidate.player().getInventory();
ItemStack current = inventory.getItem(run.heldSlot);
boolean expectedAir = run.expectedTool.getType() == Material.AIR;
boolean currentMatches = expectedAir
? current == null || current.getType() == Material.AIR
: current != null && current.isSimilar(run.expectedTool);
if (currentMatches) {
inventory.setItem(run.heldSlot, reservation.toolForDrops().clone());
run.expectedTool = reservation.toolForDrops().clone();
}
}
if (reservation.logCostReserved()) {
refundLogCost(run);
}
finish(run);
})) {
finish(run);
}
}
private boolean isRunControlActive(FellingRun run, Player player) {
return player.isOnline()
&& player.getGameMode() == GameMode.SURVIVAL
&& player.isSneaking()
&& player.getWorld().equals(run.candidate.world());
}
private boolean reserveLogCost(FellingRun run) {
try {
return run.runHooks.reserveLogCost();
} catch (Throwable error) {
IrisLogging.reportError("An Iris tree-feller integration log-cost reservation failed.", error);
return false;
}
}
private void completeLogCost(FellingRun run, DamageReservation reservation) {
if (!J.runEntity(run.candidate.player(), () -> {
try {
run.runHooks.commitLogCost();
} catch (Throwable error) {
IrisLogging.reportError("An Iris tree-feller integration log-cost commit failed.", error);
finish(run);
return;
}
completeSuccessfulMutation(run, reservation);
})) {
finish(run);
}
}
private void refundLogCost(FellingRun run) {
try {
run.runHooks.refundLogCost();
} catch (Throwable error) {
IrisLogging.reportError("An Iris tree-feller integration log-cost refund failed.", error);
}
}
private void completeSuccessfulMutation(FellingRun run, DamageReservation reservation) {
if (reservation.broke()) {
finish(run);
return;
}
continueRun(run);
}
private void continueRun(FellingRun run) {
int processed = run.processed.incrementAndGet();
if (processed % run.blocksPerPulse == 0) {
J.s(() -> runTask(run, "Failed to continue an Iris tree-feller run.", () -> processNext(run)), 1);
} else {
processNext(run);
}
}
private void runTask(FellingRun run, String context, Runnable task) {
if (run.finished.get() || !service.isServiceEnabled()) {
finish(run);
return;
}
try {
task.run();
} catch (Throwable error) {
IrisLogging.reportError(context, error);
finish(run);
}
}
void finish(FellingRun run) {
if (run.finished.compareAndSet(false, true)) {
service.activeClaims.remove(run.claim);
Set<FellingRun> runs = service.activeRuns.get(run.candidate.player().getUniqueId());
if (runs != null) {
runs.remove(run);
if (runs.isEmpty()) {
service.activeRuns.remove(run.candidate.player().getUniqueId(), runs);
}
}
run.presentation.finish();
}
}
void finishRuns(UUID playerId) {
Set<FellingRun> runs = service.activeRuns.get(playerId);
if (runs == null) {
return;
}
for (FellingRun run : List.copyOf(runs)) {
finish(run);
}
}
private Map<ChunkPosition, List<TreeMarkerTraversal.Position>> groupByChunk(
List<TreeMarkerTraversal.Position> positions
) {
Map<ChunkPosition, List<TreeMarkerTraversal.Position>> grouped = new LinkedHashMap<>();
for (TreeMarkerTraversal.Position position : positions) {
ChunkPosition chunk = new ChunkPosition(position.x() >> 4, position.z() >> 4);
grouped.computeIfAbsent(chunk, ignored -> new ArrayList<>()).add(position);
}
return grouped;
}
}
@@ -0,0 +1,151 @@
package art.arcane.iris.core.service;
import art.arcane.iris.api.tree.TreeFellerAccess;
import art.arcane.iris.core.service.TreeFellerModel.ProvenanceSnapshot;
import art.arcane.iris.core.service.TreeFellerModel.TreeCandidate;
import art.arcane.iris.core.service.TreeFellerModel.TreeContext;
import art.arcane.iris.core.service.tree.TreeDefinitionIndex;
import art.arcane.iris.core.service.tree.TreeMarkerTraversal;
import art.arcane.iris.core.tools.IrisToolbelt;
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.engine.platform.PlatformChunkGenerator;
import org.bukkit.GameMode;
import org.bukkit.Tag;
import org.bukkit.World;
import org.bukkit.block.Block;
import org.bukkit.entity.Player;
import org.bukkit.inventory.ItemStack;
import java.util.Map;
import java.util.Objects;
final class TreeProvenance {
private final Map<Engine, TreeDefinitionIndex> definitions;
TreeProvenance(Map<Engine, TreeDefinitionIndex> definitions) {
this.definitions = definitions;
}
TreeCandidate resolveCandidate(Block block, Player player) {
if (player.getGameMode() != GameMode.SURVIVAL || !player.isSneaking()) {
return null;
}
if (!Tag.LOGS.isTagged(block.getType())) {
return null;
}
ItemStack tool = player.getInventory().getItemInMainHand();
if (!TreeFellerSVC.isAxe(tool)) {
return null;
}
TreeContext context = resolveTreeContext(block);
if (context == null) {
return null;
}
return new TreeCandidate(
context,
block.getWorld(),
player,
tool.clone(),
TreeFellerAccess.STANDALONE,
new TreeMarkerTraversal.Position(block.getX(), block.getY(), block.getZ())
);
}
TreeContext resolveTreeContext(Block block) {
if (block.getType().isAir()) {
return null;
}
PlatformChunkGenerator access = IrisToolbelt.access(block.getWorld());
if (access == null || access.getEngine() == null) {
return null;
}
Engine engine = access.getEngine();
World world = block.getWorld();
int minimumY = world.getMinHeight();
int maximumY = world.getMaxHeight();
int relativeY = block.getY() - minimumY;
String marker = markerAt(engine, minimumY, block.getX(), block.getY(), block.getZ());
StructurePlacementMarker.Decoded decoded = StructurePlacementMarker.decode(marker);
if (decoded == null || decoded.structureAware()) {
return null;
}
TreeBlockMaterial expected = engine.getMantle().getMantle().get(
block.getX(),
relativeY,
block.getZ(),
TreeBlockMaterial.class
);
if (expected != null && !matchesExpectedMaterial(block, expected)) {
return null;
}
if (expected == null
&& !decoded.objectKey().startsWith("trees/")
&& !definitionIndex(engine).isTreeMarker(marker)) {
return null;
}
return new TreeContext(engine, marker, expected, minimumY, maximumY);
}
TreeDefinitionIndex definitionIndex(Engine engine) {
synchronized (definitions) {
return definitions.computeIfAbsent(engine, TreeDefinitionIndex::build);
}
}
String markerAt(Engine engine, int minimumY, TreeMarkerTraversal.Position position) {
return markerAt(engine, minimumY, position.x(), position.y(), position.z());
}
String markerAt(Engine engine, int minimumY, int x, int y, int z) {
return engine.getMantle().getMantle().get(x, y - minimumY, z, String.class);
}
TreeBlockMaterial materialAt(Engine engine, int minimumY, TreeMarkerTraversal.Position position) {
return engine.getMantle().getMantle().get(
position.x(),
position.y() - minimumY,
position.z(),
TreeBlockMaterial.class
);
}
ProvenanceSnapshot captureProvenance(Block block) {
PlatformChunkGenerator access = IrisToolbelt.access(block.getWorld());
if (access == null || access.getEngine() == null) {
return null;
}
Engine engine = access.getEngine();
TreeMarkerTraversal.Position position = new TreeMarkerTraversal.Position(
block.getX(),
block.getY(),
block.getZ()
);
int minimumY = block.getWorld().getMinHeight();
String marker = markerAt(engine, minimumY, position);
TreeBlockMaterial material = materialAt(engine, minimumY, position);
if (marker == null && material == null) {
return null;
}
return new ProvenanceSnapshot(engine, block.getWorld(), minimumY, position, marker, material);
}
void clearProvenanceIfMatching(ProvenanceSnapshot snapshot) {
String marker = markerAt(snapshot.engine(), snapshot.minimumY(), snapshot.position());
TreeBlockMaterial material = materialAt(snapshot.engine(), snapshot.minimumY(), snapshot.position());
if (Objects.equals(snapshot.marker(), marker) && Objects.equals(snapshot.material(), material)) {
clearProvenance(snapshot.engine(), snapshot.minimumY(), snapshot.position());
}
}
void clearProvenance(Engine engine, int minimumY, TreeMarkerTraversal.Position position) {
int relativeY = position.y() - minimumY;
engine.getMantle().getMantle().remove(position.x(), relativeY, position.z(), String.class);
engine.getMantle().getMantle().remove(position.x(), relativeY, position.z(), TreeBlockMaterial.class);
}
boolean matchesExpectedMaterial(Block block, TreeBlockMaterial expected) {
return expected.matches(block.getBlockData().getAsString());
}
}
@@ -361,11 +361,7 @@ public class WandSVC implements IrisService {
wand = createWand();
dust = createDust();
J.ar(() -> {
for (Player i : Bukkit.getOnlinePlayers()) {
tick(i);
}
}, 0);
J.ar(this::tickAll, 0);
}
@Override
@@ -373,6 +369,22 @@ public class WandSVC implements IrisService {
}
/**
* Async driver tick. The online player list is only read from the thread that owns it,
* and every wand draw is dispatched to the thread owning that player.
*/
private void tickAll() {
try {
J.runGlobal(() -> {
for (Player p : Bukkit.getOnlinePlayers()) {
J.runEntity(p, () -> tick(p));
}
});
} catch (Throwable e) {
Iris.reportError(e);
}
}
public void tick(Player p) {
try {
try {
@@ -22,9 +22,9 @@ public final class IrisColumnWalk {
int chunkMinBlockX = Math.max(query.minBlockX(), chunkX << 4);
int chunkMaxBlockX = Math.min(query.maxBlockX(), (chunkX << 4) + 15);
for (int blockZ = align(query.minBlockZ(), chunkMinBlockZ, stride); blockZ <= chunkMaxBlockZ; blockZ += stride) {
for (int blockX = align(query.minBlockX(), chunkMinBlockX, stride); blockX <= chunkMaxBlockX; blockX += stride) {
if (!visitor.visit(blockX, blockZ)) {
for (long blockZ = align(query.minBlockZ(), chunkMinBlockZ, stride); blockZ <= chunkMaxBlockZ; blockZ += stride) {
for (long blockX = align(query.minBlockX(), chunkMinBlockX, stride); blockX <= chunkMaxBlockX; blockX += stride) {
if (!visitor.visit((int) blockX, (int) blockZ)) {
return visited;
}
visited++;
@@ -36,10 +36,10 @@ public final class IrisColumnWalk {
return visited;
}
private static int align(int origin, int lowerBound, int stride) {
private static long align(int origin, int lowerBound, int stride) {
long offset = (long) lowerBound - (long) origin;
long steps = (offset + stride - 1L) / stride;
return (int) (origin + steps * stride);
return origin + steps * stride;
}
@FunctionalInterface
@@ -41,6 +41,10 @@ public class WandSelection {
public void draw() {
Location playerLoc = p.getLocation();
if (c.getWorld() == null || !c.getWorld().equals(playerLoc.getWorld())) {
return;
}
double maxDistanceSquared = 256 * 256;
int particleCount = 0;
@@ -37,7 +37,7 @@ public class TreeFellerEventOrderTest {
false
);
assertEquals(List.of(trigger), TreeFellerSVC.positionsForFelling(incomplete, trigger));
assertEquals(List.of(trigger), TreeFellingRunner.positionsForFelling(incomplete, trigger));
}
@Test