mirror of
https://github.com/VolmitSoftware/Iris.git
synced 2026-08-27 04:37:47 +00:00
🧹
This commit is contained in:
+30
-7
@@ -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
|
||||
|
||||
+32
-24
@@ -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());
|
||||
|
||||
+60
-11
@@ -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,
|
||||
|
||||
-106
@@ -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);
|
||||
}
|
||||
}
|
||||
+21
@@ -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);
|
||||
|
||||
+15
-6
@@ -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
@@ -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;
|
||||
|
||||
+1
-1
@@ -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);
|
||||
|
||||
+12
-12
@@ -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);
|
||||
|
||||
+8
-8
@@ -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"));
|
||||
|
||||
+10
-10
@@ -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());
|
||||
|
||||
+89
-90
@@ -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];
|
||||
|
||||
+15
-15
@@ -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;
|
||||
}
|
||||
}
|
||||
+173
@@ -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());
|
||||
}
|
||||
}
|
||||
+228
@@ -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();
|
||||
}
|
||||
}
|
||||
+38
-20
@@ -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)
|
||||
|
||||
+1
-14
@@ -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)
|
||||
|
||||
+156
-104
@@ -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)
|
||||
|
||||
+51
-13
@@ -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");
|
||||
|
||||
+92
-55
@@ -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() + ".");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+13
-2
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
+97
-27
@@ -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));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
+2
-1
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
+22
@@ -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);
|
||||
}
|
||||
}
|
||||
+81
@@ -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
|
||||
) {
|
||||
}
|
||||
}
|
||||
+34
-814
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+590
@@ -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;
|
||||
}
|
||||
}
|
||||
+151
@@ -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 {
|
||||
|
||||
+5
-5
@@ -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;
|
||||
|
||||
|
||||
+1
-1
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user