mirror of
https://github.com/VolmitSoftware/Iris.git
synced 2026-10-25 13:30:08 +00:00
dwa
This commit is contained in:
1 parent
4dea984d77
commit
45230c0689
234 files changed
+13818
-2979
No files matched your search
+139
-21
@@ -1,14 +1,17 @@
|
||||
package art.arcane.iris.core.nms.v26_2_R1;
|
||||
|
||||
import art.arcane.iris.platform.bukkit.BukkitWorldBinding;
|
||||
import com.mojang.serialization.MapCodec;
|
||||
import art.arcane.iris.spi.IrisLogging;
|
||||
import art.arcane.iris.engine.data.cache.AtomicCache;
|
||||
import art.arcane.iris.engine.framework.Engine;
|
||||
import art.arcane.iris.engine.object.IrisBiome;
|
||||
import art.arcane.iris.engine.object.IrisBiomeCustom;
|
||||
import art.arcane.iris.engine.object.IrisDimension;
|
||||
import art.arcane.volmlib.util.collection.KMap;
|
||||
import art.arcane.volmlib.util.math.RNG;
|
||||
import net.minecraft.core.Holder;
|
||||
import net.minecraft.core.QuartPos;
|
||||
import net.minecraft.core.Registry;
|
||||
import net.minecraft.core.RegistryAccess;
|
||||
import net.minecraft.core.registries.Registries;
|
||||
@@ -26,12 +29,14 @@ import java.lang.reflect.Field;
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
import java.util.IdentityHashMap;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
public class CustomBiomeSource extends BiomeSource {
|
||||
@@ -42,11 +47,13 @@ public class CustomBiomeSource extends BiomeSource {
|
||||
private final Registry<Biome> biomeCustomRegistry;
|
||||
private final Registry<Biome> biomeRegistry;
|
||||
private final AtomicCache<RegistryAccess> registryAccess = new AtomicCache<>();
|
||||
private final KMap<String, Holder<Biome>> customBiomes;
|
||||
private final Map<Biome, Holder<Biome>> vanillaSpawnBiomes;
|
||||
private final Holder<Biome> fallbackBiome;
|
||||
private final ConcurrentHashMap<Long, Holder<Biome>> noiseBiomeCache = new ConcurrentHashMap<>();
|
||||
private final ConcurrentHashMap<Long, Holder<Biome>> structureBiomeCache = new ConcurrentHashMap<>();
|
||||
private final ConcurrentHashMap<Long, Holder<Biome>> surfaceStructureBiomeCache = new ConcurrentHashMap<>();
|
||||
private volatile KMap<String, Holder<Biome>> customBiomes;
|
||||
private volatile Map<Biome, Holder<Biome>> vanillaSpawnBiomes;
|
||||
private volatile IrisDimension cacheDimension;
|
||||
|
||||
public CustomBiomeSource(long seed, Engine engine, World world) {
|
||||
this.engine = engine;
|
||||
@@ -56,20 +63,19 @@ public class CustomBiomeSource extends BiomeSource {
|
||||
this.fallbackBiome = resolveFallbackBiome(this.biomeRegistry, this.biomeCustomRegistry);
|
||||
this.customBiomes = fillCustomBiomes(this.biomeCustomRegistry, engine, this.fallbackBiome);
|
||||
this.vanillaSpawnBiomes = fillVanillaSpawnBiomes(this.biomeCustomRegistry, this.biomeRegistry, engine);
|
||||
this.cacheDimension = engine.getDimension();
|
||||
}
|
||||
|
||||
private static List<Holder<Biome>> getAllBiomes(Registry<Biome> customRegistry, Registry<Biome> registry, Engine engine, Holder<Biome> fallback) {
|
||||
LinkedHashSet<Holder<Biome>> biomes = new LinkedHashSet<>();
|
||||
if (fallback != null) {
|
||||
biomes.add(fallback);
|
||||
}
|
||||
boolean resolutionFailed = false;
|
||||
|
||||
for (IrisBiome i : engine.getAllBiomes()) {
|
||||
Holder<Biome> vanillaHolder = NMSBinding.biomeToBiomeBase(registry, i.getVanillaDerivative());
|
||||
if (vanillaHolder != null) {
|
||||
biomes.add(vanillaHolder);
|
||||
} else if (!i.isCustom() && fallback != null) {
|
||||
biomes.add(fallback);
|
||||
} else if (!i.isCustom()) {
|
||||
resolutionFailed = true;
|
||||
}
|
||||
|
||||
if (i.isCustom()) {
|
||||
@@ -77,13 +83,17 @@ public class CustomBiomeSource extends BiomeSource {
|
||||
Holder<Biome> customHolder = resolveCustomBiomeHolder(customRegistry, engine, j.getId());
|
||||
if (customHolder != null) {
|
||||
biomes.add(customHolder);
|
||||
} else if (fallback != null) {
|
||||
biomes.add(fallback);
|
||||
} else {
|
||||
resolutionFailed = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ((resolutionFailed || biomes.isEmpty()) && fallback != null) {
|
||||
biomes.add(fallback);
|
||||
}
|
||||
|
||||
return new ArrayList<>(biomes);
|
||||
}
|
||||
|
||||
@@ -135,12 +145,21 @@ public class CustomBiomeSource extends BiomeSource {
|
||||
|
||||
@Override
|
||||
protected Stream<Holder<Biome>> collectPossibleBiomes() {
|
||||
return getAllBiomes(
|
||||
((RegistryAccess) getFor(RegistryAccess.Frozen.class, ((CraftServer) Bukkit.getServer()).getHandle().getServer()))
|
||||
.lookup(Registries.BIOME).orElse(null),
|
||||
((CraftWorld) engine.getWorld().realWorld()).getHandle().registryAccess().lookup(Registries.BIOME).orElse(null),
|
||||
engine,
|
||||
fallbackBiome).stream();
|
||||
return possibleStructureBiomes().stream();
|
||||
}
|
||||
|
||||
Set<Holder<Biome>> possibleStructureBiomes() {
|
||||
ensureCachesCurrent();
|
||||
World world = BukkitWorldBinding.world(engine.getWorld());
|
||||
if (world == null) {
|
||||
throw new IllegalStateException("Iris biome source has no bound Bukkit world");
|
||||
}
|
||||
Registry<Biome> customRegistry = ((RegistryAccess) getFor(
|
||||
RegistryAccess.Frozen.class, ((CraftServer) Bukkit.getServer()).getHandle().getServer()))
|
||||
.lookup(Registries.BIOME).orElse(null);
|
||||
Registry<Biome> worldRegistry = ((CraftWorld) world).getHandle().registryAccess()
|
||||
.lookup(Registries.BIOME).orElse(null);
|
||||
return Set.copyOf(getAllBiomes(customRegistry, worldRegistry, engine, fallbackBiome));
|
||||
}
|
||||
|
||||
private KMap<String, Holder<Biome>> fillCustomBiomes(Registry<Biome> customRegistry, Engine engine, Holder<Biome> fallback) {
|
||||
@@ -194,6 +213,7 @@ public class CustomBiomeSource extends BiomeSource {
|
||||
}
|
||||
|
||||
Holder<Biome> getVanillaSpawnBiome(Holder<Biome> biome) {
|
||||
ensureCachesCurrent();
|
||||
if (biome == null) {
|
||||
return null;
|
||||
}
|
||||
@@ -211,6 +231,11 @@ public class CustomBiomeSource extends BiomeSource {
|
||||
|
||||
@Override
|
||||
public Holder<Biome> getNoiseBiome(int x, int y, int z, Climate.Sampler sampler) {
|
||||
ensureCachesCurrent();
|
||||
if (isGuaranteedSurfaceBiome(y)) {
|
||||
return getSurfaceStructureBiomeHolder(x, z);
|
||||
}
|
||||
|
||||
long cacheKey = packNoiseKey(x, y, z);
|
||||
Holder<Biome> cachedHolder = structureBiomeCache.get(cacheKey);
|
||||
if (cachedHolder != null) {
|
||||
@@ -230,7 +255,69 @@ public class CustomBiomeSource extends BiomeSource {
|
||||
return resolvedHolder;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<Holder<Biome>> getBiomesWithin(int x, int y, int z, int radius, Climate.Sampler sampler) {
|
||||
ensureCachesCurrent();
|
||||
int minQuartY = QuartPos.fromBlock(y - radius);
|
||||
boolean monumentQuery = radius == 29
|
||||
&& y == engine.getMinHeight() + engine.getDimension().getFluidHeight();
|
||||
if (!monumentQuery && !isGuaranteedSurfaceBiome(minQuartY)) {
|
||||
return super.getBiomesWithin(x, y, z, radius, sampler);
|
||||
}
|
||||
int minQuartX = QuartPos.fromBlock(x - radius);
|
||||
int maxQuartX = QuartPos.fromBlock(x + radius);
|
||||
int minQuartZ = QuartPos.fromBlock(z - radius);
|
||||
int maxQuartZ = QuartPos.fromBlock(z + radius);
|
||||
int columns = (maxQuartX - minQuartX + 1) * (maxQuartZ - minQuartZ + 1);
|
||||
Set<Holder<Biome>> biomes = new HashSet<>(columns);
|
||||
for (int quartZ = minQuartZ; quartZ <= maxQuartZ; quartZ++) {
|
||||
for (int quartX = minQuartX; quartX <= maxQuartX; quartX++) {
|
||||
biomes.add(getSurfaceStructureBiomeHolder(quartX, quartZ));
|
||||
}
|
||||
}
|
||||
return biomes;
|
||||
}
|
||||
|
||||
private Holder<Biome> getSurfaceStructureBiomeHolder(int x, int z) {
|
||||
long columnKey = packColumnKey(x, z);
|
||||
Holder<Biome> surfaceHolder = surfaceStructureBiomeCache.get(columnKey);
|
||||
if (surfaceHolder != null) {
|
||||
return surfaceHolder;
|
||||
}
|
||||
Holder<Biome> resolvedSurfaceHolder = resolveSurfaceStructureBiomeHolder(x, z);
|
||||
Holder<Biome> existingSurfaceHolder = surfaceStructureBiomeCache.putIfAbsent(columnKey, resolvedSurfaceHolder);
|
||||
if (existingSurfaceHolder != null) {
|
||||
return existingSurfaceHolder;
|
||||
}
|
||||
if (surfaceStructureBiomeCache.size() > NOISE_BIOME_CACHE_MAX) {
|
||||
surfaceStructureBiomeCache.clear();
|
||||
}
|
||||
return resolvedSurfaceHolder;
|
||||
}
|
||||
|
||||
private boolean isGuaranteedSurfaceBiome(int quartY) {
|
||||
if (engine == null || engine.isClosed() || engine.getComplex() == null) {
|
||||
return false;
|
||||
}
|
||||
int worldMinHeight = engine.getWorld().minHeight();
|
||||
int internalY = (quartY << 2) - worldMinHeight;
|
||||
int caveSwitchInternalY = Math.max(-8 - worldMinHeight, 40);
|
||||
return internalY > caveSwitchInternalY;
|
||||
}
|
||||
|
||||
private Holder<Biome> resolveSurfaceStructureBiomeHolder(int x, int z) {
|
||||
int blockX = x << 2;
|
||||
int blockZ = z << 2;
|
||||
IrisBiome irisBiome = engine.getComplex().getTrueBiomeStream().get(blockX, blockZ);
|
||||
if (irisBiome == null) {
|
||||
return getFallbackBiome();
|
||||
}
|
||||
Holder<Biome> holder = NMSBinding.biomeToBiomeBase(biomeRegistry, irisBiome.getVanillaDerivative());
|
||||
return holder == null ? getFallbackBiome() : holder;
|
||||
}
|
||||
|
||||
public Holder<Biome> getVisibleNoiseBiome(int x, int y, int z, Climate.Sampler sampler) {
|
||||
ensureCachesCurrent();
|
||||
long cacheKey = packNoiseKey(x, y, z);
|
||||
Holder<Biome> cachedHolder = noiseBiomeCache.get(cacheKey);
|
||||
if (cachedHolder != null) {
|
||||
@@ -250,6 +337,28 @@ public class CustomBiomeSource extends BiomeSource {
|
||||
return resolvedHolder;
|
||||
}
|
||||
|
||||
private void ensureCachesCurrent() {
|
||||
IrisDimension dimension = engine.getDimension();
|
||||
if (cacheDimension == dimension) {
|
||||
return;
|
||||
}
|
||||
synchronized (this) {
|
||||
if (cacheDimension == dimension) {
|
||||
return;
|
||||
}
|
||||
KMap<String, Holder<Biome>> refreshedCustomBiomes = fillCustomBiomes(
|
||||
biomeCustomRegistry, engine, fallbackBiome);
|
||||
Map<Biome, Holder<Biome>> refreshedSpawnBiomes = fillVanillaSpawnBiomes(
|
||||
biomeCustomRegistry, biomeRegistry, engine);
|
||||
noiseBiomeCache.clear();
|
||||
structureBiomeCache.clear();
|
||||
surfaceStructureBiomeCache.clear();
|
||||
customBiomes = refreshedCustomBiomes;
|
||||
vanillaSpawnBiomes = refreshedSpawnBiomes;
|
||||
cacheDimension = dimension;
|
||||
}
|
||||
}
|
||||
|
||||
private Holder<Biome> resolveStructureBiomeHolder(int x, int y, int z) {
|
||||
BiomeResolution resolution = resolveBiomeResolution(x, y, z);
|
||||
if (resolution == null) {
|
||||
@@ -315,14 +424,19 @@ public class CustomBiomeSource extends BiomeSource {
|
||||
int blockY = y << 2;
|
||||
int worldMinHeight = engine.getWorld().minHeight();
|
||||
int internalY = blockY - worldMinHeight;
|
||||
int surfaceInternalY = engine.getComplex().getHeightStream().get(blockX, blockZ).intValue();
|
||||
int caveSwitchInternalY = Math.max(-8 - worldMinHeight, 40);
|
||||
boolean deepUnderground = internalY <= caveSwitchInternalY;
|
||||
boolean belowSurface = internalY <= surfaceInternalY - 8;
|
||||
boolean underground = deepUnderground && belowSurface;
|
||||
IrisBiome irisBiome = underground
|
||||
? engine.getCaveBiome(blockX, internalY, blockZ)
|
||||
: engine.getComplex().getTrueBiomeStream().get(blockX, blockZ);
|
||||
boolean underground = false;
|
||||
IrisBiome irisBiome;
|
||||
if (deepUnderground) {
|
||||
int surfaceInternalY = engine.getComplex().getHeightStream().get(blockX, blockZ).intValue();
|
||||
underground = internalY <= surfaceInternalY - 8;
|
||||
irisBiome = underground
|
||||
? engine.getCaveBiome(blockX, internalY, blockZ)
|
||||
: engine.getComplex().getTrueBiomeStream().get(blockX, blockZ);
|
||||
} else {
|
||||
irisBiome = engine.getComplex().getTrueBiomeStream().get(blockX, blockZ);
|
||||
}
|
||||
if (irisBiome == null && underground) {
|
||||
irisBiome = engine.getComplex().getTrueBiomeStream().get(blockX, blockZ);
|
||||
}
|
||||
@@ -357,6 +471,10 @@ public class CustomBiomeSource extends BiomeSource {
|
||||
| ((long) y & 4095L);
|
||||
}
|
||||
|
||||
private static long packColumnKey(int x, int z) {
|
||||
return ((long) x << 32) ^ ((long) z & 4294967295L);
|
||||
}
|
||||
|
||||
private static Holder<Biome> resolveCustomBiomeHolder(Registry<Biome> customRegistry, Engine engine, String customBiomeId) {
|
||||
if (customRegistry == null || engine == null || customBiomeId == null || customBiomeId.isBlank()) {
|
||||
return null;
|
||||
|
||||
+227
-86
@@ -1,5 +1,20 @@
|
||||
package art.arcane.iris.core.nms.v26_2_R1;
|
||||
|
||||
import art.arcane.iris.engine.framework.Engine;
|
||||
import art.arcane.iris.engine.framework.IrisStructureLocator;
|
||||
import art.arcane.iris.engine.object.IrisDimension;
|
||||
import art.arcane.iris.engine.object.IrisImportedStructureControl;
|
||||
import art.arcane.iris.engine.object.IrisNativeStructureDecision;
|
||||
import art.arcane.iris.engine.object.IrisVanillaStructureStiltSettings;
|
||||
import art.arcane.iris.nativegen.NativeStructurePostProcessor;
|
||||
import art.arcane.iris.spi.IrisLogging;
|
||||
import art.arcane.iris.spi.PlatformBlockState;
|
||||
import art.arcane.iris.util.common.data.IrisCustomData;
|
||||
import art.arcane.iris.util.common.reflect.WrappedField;
|
||||
import art.arcane.iris.util.common.reflect.WrappedReturningMethod;
|
||||
import art.arcane.volmlib.util.math.RNG;
|
||||
import com.mojang.datafixers.util.Pair;
|
||||
import com.mojang.serialization.MapCodec;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.core.Holder;
|
||||
import net.minecraft.core.HolderLookup;
|
||||
@@ -7,6 +22,13 @@ import net.minecraft.core.HolderSet;
|
||||
import net.minecraft.core.Registry;
|
||||
import net.minecraft.core.RegistryAccess;
|
||||
import net.minecraft.core.SectionPos;
|
||||
import net.minecraft.core.registries.Registries;
|
||||
import net.minecraft.resources.Identifier;
|
||||
import net.minecraft.resources.ResourceKey;
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
import net.minecraft.server.level.WorldGenRegion;
|
||||
import net.minecraft.util.random.Weighted;
|
||||
import net.minecraft.util.random.WeightedList;
|
||||
import net.minecraft.world.level.ChunkPos;
|
||||
import net.minecraft.world.level.Level;
|
||||
import net.minecraft.world.level.LevelHeightAccessor;
|
||||
@@ -23,26 +45,6 @@ import net.minecraft.world.level.levelgen.RandomState;
|
||||
import net.minecraft.world.level.levelgen.RandomSupport;
|
||||
import net.minecraft.world.level.levelgen.WorldgenRandom;
|
||||
import net.minecraft.world.level.levelgen.XoroshiroRandomSource;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import com.mojang.datafixers.util.Pair;
|
||||
import com.mojang.serialization.MapCodec;
|
||||
import art.arcane.iris.spi.IrisLogging;
|
||||
import art.arcane.iris.engine.framework.Engine;
|
||||
import art.arcane.iris.engine.object.IrisImportedStructureControl;
|
||||
import art.arcane.iris.util.common.reflect.WrappedField;
|
||||
import art.arcane.iris.util.common.reflect.WrappedReturningMethod;
|
||||
import net.minecraft.resources.Identifier;
|
||||
import net.minecraft.resources.ResourceKey;
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
import net.minecraft.server.level.WorldGenRegion;
|
||||
import net.minecraft.util.random.Weighted;
|
||||
import net.minecraft.util.random.WeightedList;
|
||||
import net.minecraft.world.entity.EntityType;
|
||||
import net.minecraft.world.entity.MobCategory;
|
||||
import net.minecraft.world.level.block.Blocks;
|
||||
@@ -52,35 +54,44 @@ import net.minecraft.world.level.chunk.ChunkGenerator;
|
||||
import net.minecraft.world.level.chunk.ChunkGeneratorStructureState;
|
||||
import net.minecraft.world.level.chunk.status.ChunkStatus;
|
||||
import net.minecraft.world.level.levelgen.blending.Blender;
|
||||
import net.minecraft.world.level.levelgen.structure.BoundingBox;
|
||||
import net.minecraft.world.level.levelgen.structure.Structure;
|
||||
import net.minecraft.world.level.levelgen.structure.StructureSet;
|
||||
import net.minecraft.world.level.levelgen.structure.StructureStart;
|
||||
import net.minecraft.world.level.levelgen.structure.BoundingBox;
|
||||
import net.minecraft.world.level.levelgen.structure.templatesystem.StructureTemplateManager;
|
||||
import net.minecraft.core.registries.Registries;
|
||||
import java.util.stream.Collectors;
|
||||
import art.arcane.iris.engine.framework.IrisStructureLocator;
|
||||
import org.bukkit.World;
|
||||
import org.bukkit.craftbukkit.CraftWorld;
|
||||
import org.bukkit.craftbukkit.block.data.CraftBlockData;
|
||||
import org.bukkit.craftbukkit.generator.CustomChunkGenerator;
|
||||
import org.bukkit.block.data.BlockData;
|
||||
import org.spigotmc.SpigotWorldConfig;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
import java.lang.reflect.Field;
|
||||
import java.lang.reflect.InvocationTargetException;
|
||||
import java.lang.reflect.Method;
|
||||
import java.lang.reflect.Proxy;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
public class IrisChunkGenerator extends CustomChunkGenerator {
|
||||
private static final String NATIVE_MONUMENT_KEY = "minecraft:monument";
|
||||
private static final WrappedField<ChunkGenerator, BiomeSource> BIOME_SOURCE;
|
||||
private static final WrappedReturningMethod<Heightmap, Object> SET_HEIGHT;
|
||||
private final ChunkGenerator delegate;
|
||||
private final Engine engine;
|
||||
private final CustomBiomeSource customBiomeSource;
|
||||
private final int runtimeMinY;
|
||||
private final int runtimeHeight;
|
||||
private final ConcurrentHashMap<SpawnTableKey, WeightedList<MobSpawnSettings.SpawnerData>> mergedSpawnTables = new ConcurrentHashMap<>();
|
||||
private volatile Set<String> reachableStructureKeysCache;
|
||||
private volatile ReachableStructureCache reachableStructureCache;
|
||||
private volatile StructureStepCache structureStepCache;
|
||||
|
||||
public IrisChunkGenerator(ChunkGenerator delegate, long seed, Engine engine, World world) {
|
||||
this(delegate, engine, world, new CustomBiomeSource(seed, engine, world));
|
||||
@@ -91,6 +102,9 @@ public class IrisChunkGenerator extends CustomChunkGenerator {
|
||||
this.delegate = delegate;
|
||||
this.engine = engine;
|
||||
this.customBiomeSource = customBiomeSource;
|
||||
ServerLevel level = ((CraftWorld) world).getHandle();
|
||||
this.runtimeMinY = level.getMinY();
|
||||
this.runtimeHeight = level.getHeight();
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -105,16 +119,20 @@ public class IrisChunkGenerator extends CustomChunkGenerator {
|
||||
if (id == null) {
|
||||
continue;
|
||||
}
|
||||
int[] at = IrisStructureLocator.locate(engine, id.toString(), pos.getX(), pos.getZ(), Math.max(1, radius));
|
||||
if (at == null) {
|
||||
IrisStructureLocator.LocateResult result = IrisStructureLocator.locate(
|
||||
engine, id.toString(), pos.getX(), pos.getZ(), Math.max(1, radius));
|
||||
if (result.status() == IrisStructureLocator.LocateStatus.SEARCH_LIMIT_REACHED) {
|
||||
continue;
|
||||
}
|
||||
long dx = (long) at[0] - pos.getX();
|
||||
long dz = (long) at[2] - pos.getZ();
|
||||
if (!result.found()) {
|
||||
continue;
|
||||
}
|
||||
long dx = (long) result.originX() - pos.getX();
|
||||
long dz = (long) result.originZ() - pos.getZ();
|
||||
long d = dx * dx + dz * dz;
|
||||
if (d < bestDist) {
|
||||
bestDist = d;
|
||||
best = new BlockPos(at[0], at[1], at[2]);
|
||||
best = new BlockPos(result.originX(), result.baseY(), result.originZ());
|
||||
bestHolder = holder;
|
||||
}
|
||||
}
|
||||
@@ -122,7 +140,8 @@ public class IrisChunkGenerator extends CustomChunkGenerator {
|
||||
return Pair.of(best, bestHolder);
|
||||
}
|
||||
} catch (Throwable e) {
|
||||
IrisLogging.reportError(e);
|
||||
IrisLogging.reportError("Iris-placed structure lookup failed near "
|
||||
+ pos.getX() + ", " + pos.getZ() + ".", e);
|
||||
}
|
||||
if (!importedControl().active()) {
|
||||
return null;
|
||||
@@ -134,28 +153,30 @@ public class IrisChunkGenerator extends CustomChunkGenerator {
|
||||
try {
|
||||
return delegate.findNearestMapStructure(level, reachable, pos, radius, findUnexplored);
|
||||
} catch (Throwable e) {
|
||||
IrisLogging.error("Vanilla structure locate failed near " + pos.getX() + ", " + pos.getZ() + ": " + e);
|
||||
IrisLogging.reportError(e);
|
||||
IrisLogging.reportError("Vanilla structure locate failed near "
|
||||
+ pos.getX() + ", " + pos.getZ() + ".", e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private HolderSet<Structure> filterReachableStructures(ServerLevel level, HolderSet<Structure> holders) {
|
||||
Set<String> reachable = reachableStructureKeysCache;
|
||||
if (reachable == null) {
|
||||
reachable = VanillaStructureBiomes.reachableStructureKeys(level, delegate.getBiomeSource());
|
||||
reachableStructureKeysCache = reachable;
|
||||
}
|
||||
if (reachable.isEmpty()) {
|
||||
return holders;
|
||||
}
|
||||
Set<String> reachable = reachableStructureKeys(level);
|
||||
IrisImportedStructureControl control = importedControl();
|
||||
Registry<Structure> registry = level.registryAccess().lookupOrThrow(Registries.STRUCTURE);
|
||||
List<Holder<Structure>> kept = new ArrayList<>();
|
||||
List<Holder<Structure>> kept = new ArrayList<>(holders.size());
|
||||
for (Holder<Structure> holder : holders) {
|
||||
Object id = registry.getKey(holder.value());
|
||||
if (id != null && reachable.contains(id.toString())) {
|
||||
kept.add(holder);
|
||||
if (id == null) {
|
||||
continue;
|
||||
}
|
||||
String key = id.toString();
|
||||
IrisNativeStructureDecision decision = control.resolve(
|
||||
key, NativeStructurePostProcessor.isUndergroundStep(holder.value().step()));
|
||||
if (NATIVE_MONUMENT_KEY.equals(key) || !decision.generate()
|
||||
|| IrisStructureLocator.suppressesVanilla(engine, key) || !reachable.contains(key)) {
|
||||
continue;
|
||||
}
|
||||
kept.add(holder);
|
||||
}
|
||||
if (kept.size() == holders.size()) {
|
||||
return holders;
|
||||
@@ -163,6 +184,30 @@ public class IrisChunkGenerator extends CustomChunkGenerator {
|
||||
return HolderSet.direct(kept);
|
||||
}
|
||||
|
||||
private Set<String> reachableStructureKeys(ServerLevel level) {
|
||||
IrisDimension dimension = engine.getDimension();
|
||||
ReachableStructureCache cached = reachableStructureCache;
|
||||
if (cached != null && cached.dimension() == dimension) {
|
||||
return cached.keys();
|
||||
}
|
||||
synchronized (this) {
|
||||
cached = reachableStructureCache;
|
||||
if (cached != null && cached.dimension() == dimension) {
|
||||
return cached.keys();
|
||||
}
|
||||
Set<String> reachable;
|
||||
try {
|
||||
reachable = Set.copyOf(VanillaStructureBiomes.reachableStructureKeys(level, customBiomeSource));
|
||||
} catch (Throwable error) {
|
||||
IrisLogging.reportError("Iris could not resolve native structure biome reachability; "
|
||||
+ "native locate is disabled until the next hotload.", error);
|
||||
reachable = Set.of();
|
||||
}
|
||||
reachableStructureCache = new ReachableStructureCache(dimension, reachable);
|
||||
return reachable;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected MapCodec<? extends ChunkGenerator> codec() {
|
||||
return MapCodec.unit(null);
|
||||
@@ -177,12 +222,12 @@ public class IrisChunkGenerator extends CustomChunkGenerator {
|
||||
|
||||
@Override
|
||||
public int getMinY() {
|
||||
return delegate.getMinY();
|
||||
return runtimeMinY;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getSeaLevel() {
|
||||
return delegate.getSeaLevel();
|
||||
return runtimeMinY + engine.getDimension().getFluidHeight();
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -190,7 +235,42 @@ public class IrisChunkGenerator extends CustomChunkGenerator {
|
||||
if (!importedControl().active()) {
|
||||
return;
|
||||
}
|
||||
Map<Structure, StructureStart> previousStarts = new HashMap<>(access.getAllStarts());
|
||||
super.createStructures(registryAccess, structureState, structureManager, access, templateManager, levelKey);
|
||||
adjustGeneratedStructures(registryAccess, access, previousStarts);
|
||||
}
|
||||
|
||||
private void adjustGeneratedStructures(RegistryAccess registryAccess, ChunkAccess access, Map<Structure, StructureStart> previousStarts) {
|
||||
Registry<Structure> registry = registryAccess.lookupOrThrow(Registries.STRUCTURE);
|
||||
IrisImportedStructureControl control = importedControl();
|
||||
for (Map.Entry<Structure, StructureStart> entry : access.getAllStarts().entrySet()) {
|
||||
Structure structure = entry.getKey();
|
||||
StructureStart start = entry.getValue();
|
||||
if (!start.isValid() || previousStarts.get(structure) == start) {
|
||||
continue;
|
||||
}
|
||||
Identifier id = registry.getKey(structure);
|
||||
String structureId = id == null ? null : id.toString();
|
||||
IrisNativeStructureDecision decision = control.resolve(
|
||||
structureId, NativeStructurePostProcessor.isUndergroundStep(structure.step()));
|
||||
if (!decision.generate() || IrisStructureLocator.suppressesVanilla(engine, structureId)) {
|
||||
access.setStartForStructure(structure, StructureStart.INVALID_START);
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
NativeStructurePostProcessor.applyVerticalShift(
|
||||
start,
|
||||
decision.yShift(),
|
||||
access.getMinY(),
|
||||
access.getMinY() + access.getHeight());
|
||||
} catch (RuntimeException error) {
|
||||
access.setStartForStructure(structure, StructureStart.INVALID_START);
|
||||
IrisLogging.reportError("Iris rejected native structure " + structureId + " in chunk "
|
||||
+ access.getPos().x() + "," + access.getPos().z()
|
||||
+ " because its vertical bounds are invalid.", error);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private IrisImportedStructureControl importedControl() {
|
||||
@@ -281,68 +361,119 @@ public class IrisChunkGenerator extends CustomChunkGenerator {
|
||||
SectionPos sectionPos = SectionPos.of(chunkPos, world.getMinSectionY());
|
||||
BlockPos origin = sectionPos.origin();
|
||||
Registry<Structure> registry = world.registryAccess().lookupOrThrow(Registries.STRUCTURE);
|
||||
Map<Integer, List<Structure>> byStep = registry.stream().collect(Collectors.groupingBy(s -> s.step().ordinal()));
|
||||
List<List<Structure>> byStep = structuresByStep(registry);
|
||||
WorldgenRandom random = new WorldgenRandom(new XoroshiroRandomSource(RandomSupport.generateUniqueSeed()));
|
||||
long decoSeed = random.setDecorationSeed(world.getSeed(), origin.getX(), origin.getZ());
|
||||
BoundingBox area = writableArea(chunk);
|
||||
int steps = GenerationStep.Decoration.values().length;
|
||||
IrisImportedStructureControl control = importedControl();
|
||||
List<NativePlacementGroup> placementGroups = new ArrayList<>();
|
||||
List<NativeStructurePostProcessor.VegetationTarget> vegetationTargets = new ArrayList<>();
|
||||
for (int step = 0; step < steps; step++) {
|
||||
int index = 0;
|
||||
for (Structure structure : byStep.getOrDefault(step, List.of())) {
|
||||
for (Structure structure : byStep.get(step)) {
|
||||
Object id = registry.getKey(structure);
|
||||
String structureId = id == null ? null : id.toString();
|
||||
if (control.shouldGenerate(structureId) && !IrisStructureLocator.suppressesVanilla(engine, structureId)) {
|
||||
random.setFeatureSeed(decoSeed, index, step);
|
||||
int[] offset = control.resolveOffset(structureId, isUndergroundStep(structure.step()));
|
||||
boolean shifted = offset[0] != 0 || offset[1] != 0 || offset[2] != 0;
|
||||
WorldGenLevel target = shifted ? shiftedLevel(world, offset[0], offset[1], offset[2]) : world;
|
||||
BoundingBox placeArea = shifted
|
||||
? new BoundingBox(area.minX() - offset[0], area.minY(), area.minZ() - offset[2], area.maxX() - offset[0], area.maxY(), area.maxZ() - offset[2])
|
||||
: area;
|
||||
IrisNativeStructureDecision decision = control.resolve(
|
||||
structureId, NativeStructurePostProcessor.isUndergroundStep(structure.step()));
|
||||
if (decision.generate() && !IrisStructureLocator.suppressesVanilla(engine, structureId)) {
|
||||
try {
|
||||
structureManager.startsForStructure(sectionPos, structure)
|
||||
.forEach(start -> start.placeInChunk(target, structureManager, this, random, placeArea, chunkPos));
|
||||
List<StructureStart> starts = structureManager.startsForStructure(sectionPos, structure);
|
||||
if (!starts.isEmpty()) {
|
||||
List<StructureStart> resolvedStarts = List.copyOf(starts);
|
||||
placementGroups.add(new NativePlacementGroup(
|
||||
structureId, decision, index, step, resolvedStarts));
|
||||
for (StructureStart start : resolvedStarts) {
|
||||
vegetationTargets.add(new NativeStructurePostProcessor.VegetationTarget(
|
||||
start, decision.clearVegetation()));
|
||||
}
|
||||
}
|
||||
} catch (Throwable e) {
|
||||
IrisLogging.reportError(e);
|
||||
IrisLogging.reportError("Iris failed to resolve native structure " + structureId
|
||||
+ " in chunk " + chunkPos.x() + "," + chunkPos.z() + ".", e);
|
||||
}
|
||||
}
|
||||
index++;
|
||||
}
|
||||
}
|
||||
try {
|
||||
NativeStructurePostProcessor.clearIntersectingVegetation(
|
||||
world, chunk, area, vegetationTargets);
|
||||
} catch (Throwable e) {
|
||||
IrisLogging.reportError("Iris failed to clear vegetation from native structures in chunk "
|
||||
+ chunkPos.x() + "," + chunkPos.z() + ".", e);
|
||||
}
|
||||
for (NativePlacementGroup group : placementGroups) {
|
||||
random.setFeatureSeed(decoSeed, group.featureIndex(), group.step());
|
||||
try {
|
||||
for (StructureStart start : group.starts()) {
|
||||
placeVanillaStructure(world, structureManager, random, area, chunkPos,
|
||||
group.structureId(), start, group.decision());
|
||||
}
|
||||
} catch (Throwable e) {
|
||||
IrisLogging.reportError("Iris failed to place native structure " + group.structureId()
|
||||
+ " in chunk " + chunkPos.x() + "," + chunkPos.z() + ".", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean isUndergroundStep(GenerationStep.Decoration step) {
|
||||
return step == GenerationStep.Decoration.UNDERGROUND_STRUCTURES
|
||||
|| step == GenerationStep.Decoration.STRONGHOLDS;
|
||||
private void placeVanillaStructure(WorldGenLevel world, StructureManager structureManager, WorldgenRandom random,
|
||||
BoundingBox area, ChunkPos chunkPos, String structureId, StructureStart start,
|
||||
IrisNativeStructureDecision decision) {
|
||||
NativeStructurePostProcessor.place(world, structureManager, this, random, area, chunkPos,
|
||||
structureId, start, decision, this::resolveStiltBlock);
|
||||
}
|
||||
|
||||
private WorldGenLevel shiftedLevel(WorldGenLevel world, int dx, int dy, int dz) {
|
||||
return (WorldGenLevel) Proxy.newProxyInstance(
|
||||
WorldGenLevel.class.getClassLoader(),
|
||||
new Class<?>[]{WorldGenLevel.class},
|
||||
(proxy, method, args) -> {
|
||||
if (args != null) {
|
||||
for (int i = 0; i < args.length; i++) {
|
||||
if (args[i] instanceof BlockPos bp) {
|
||||
args[i] = new BlockPos(bp.getX() + dx, bp.getY() + dy, bp.getZ() + dz);
|
||||
}
|
||||
}
|
||||
}
|
||||
try {
|
||||
return method.invoke(world, args);
|
||||
} catch (InvocationTargetException e) {
|
||||
throw e.getCause();
|
||||
}
|
||||
});
|
||||
private List<List<Structure>> structuresByStep(Registry<Structure> registry) {
|
||||
StructureStepCache cached = structureStepCache;
|
||||
if (cached != null && cached.registry() == registry) {
|
||||
return cached.structures();
|
||||
}
|
||||
synchronized (this) {
|
||||
cached = structureStepCache;
|
||||
if (cached != null && cached.registry() == registry) {
|
||||
return cached.structures();
|
||||
}
|
||||
int steps = GenerationStep.Decoration.values().length;
|
||||
List<List<Structure>> grouped = new ArrayList<>(steps);
|
||||
for (int step = 0; step < steps; step++) {
|
||||
grouped.add(new ArrayList<>());
|
||||
}
|
||||
for (Structure structure : registry) {
|
||||
grouped.get(structure.step().ordinal()).add(structure);
|
||||
}
|
||||
for (int step = 0; step < steps; step++) {
|
||||
grouped.set(step, List.copyOf(grouped.get(step)));
|
||||
}
|
||||
List<List<Structure>> resolved = List.copyOf(grouped);
|
||||
structureStepCache = new StructureStepCache(registry, resolved);
|
||||
return resolved;
|
||||
}
|
||||
}
|
||||
|
||||
private BlockState resolveStiltBlock(IrisVanillaStructureStiltSettings settings, RNG rng, int x, int y, int z) {
|
||||
if (settings.getPalette() == null) {
|
||||
return Blocks.COBBLESTONE.defaultBlockState();
|
||||
}
|
||||
PlatformBlockState platformState = settings.getPalette().get(rng, x, y, z, engine.getData());
|
||||
if (platformState == null || !(platformState.nativeHandle() instanceof BlockData blockData)) {
|
||||
return Blocks.COBBLESTONE.defaultBlockState();
|
||||
}
|
||||
if (blockData instanceof IrisCustomData customData) {
|
||||
blockData = customData.getBase();
|
||||
}
|
||||
if (blockData instanceof CraftBlockData craftBlockData) {
|
||||
return craftBlockData.getState();
|
||||
}
|
||||
return Blocks.COBBLESTONE.defaultBlockState();
|
||||
}
|
||||
|
||||
private BoundingBox writableArea(ChunkAccess chunk) {
|
||||
ChunkPos cp = chunk.getPos();
|
||||
int i = cp.getMinBlockX();
|
||||
int j = cp.getMinBlockZ();
|
||||
int minY = getMinY() + 1;
|
||||
int maxY = getMinY() + engine.getHeight() - 1;
|
||||
int minY = chunk.getMinY();
|
||||
int maxY = minY + chunk.getHeight() - 1;
|
||||
return new BoundingBox(i, minY, j, i + 15, maxY, j + 15);
|
||||
}
|
||||
|
||||
@@ -403,7 +534,7 @@ public class IrisChunkGenerator extends CustomChunkGenerator {
|
||||
|
||||
@Override
|
||||
public int getGenDepth() {
|
||||
return delegate.getGenDepth();
|
||||
return runtimeHeight;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -447,7 +578,7 @@ public class IrisChunkGenerator extends CustomChunkGenerator {
|
||||
|
||||
Method setHeight = null;
|
||||
for (Method method : Heightmap.class.getDeclaredMethods()) {
|
||||
var types = method.getParameterTypes();
|
||||
Class<?>[] types = method.getParameterTypes();
|
||||
if (types.length != 3 || !Arrays.equals(types, new Class<?>[]{int.class, int.class, int.class})
|
||||
|| !method.getReturnType().equals(void.class))
|
||||
continue;
|
||||
@@ -475,4 +606,14 @@ public class IrisChunkGenerator extends CustomChunkGenerator {
|
||||
|
||||
private record SpawnTableKey(Biome biome, MobCategory category) {
|
||||
}
|
||||
|
||||
private record ReachableStructureCache(IrisDimension dimension, Set<String> keys) {
|
||||
}
|
||||
|
||||
private record StructureStepCache(Registry<Structure> registry, List<List<Structure>> structures) {
|
||||
}
|
||||
|
||||
private record NativePlacementGroup(String structureId, IrisNativeStructureDecision decision,
|
||||
int featureIndex, int step, List<StructureStart> starts) {
|
||||
}
|
||||
}
|
||||
+69
-50
@@ -13,6 +13,7 @@ import art.arcane.iris.core.nms.datapack.DataVersion;
|
||||
import art.arcane.iris.engine.data.cache.AtomicCache;
|
||||
import art.arcane.iris.engine.data.chunk.TerrainChunk;
|
||||
import art.arcane.iris.engine.framework.Engine;
|
||||
import art.arcane.iris.engine.object.IrisDimensionRuntimeContract;
|
||||
import art.arcane.iris.engine.platform.PlatformChunkGenerator;
|
||||
import art.arcane.iris.util.project.agent.Agent;
|
||||
import art.arcane.volmlib.util.collection.KList;
|
||||
@@ -71,6 +72,7 @@ import net.minecraft.resources.Identifier;
|
||||
import net.minecraft.resources.ResourceKey;
|
||||
import net.minecraft.server.MinecraftServer;
|
||||
import net.minecraft.server.commands.data.BlockDataAccessor;
|
||||
import net.minecraft.server.level.ChunkMap;
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
import net.minecraft.server.level.ThreadedLevelLightEngine;
|
||||
import net.minecraft.tags.TagKey;
|
||||
@@ -93,6 +95,7 @@ import net.minecraft.world.level.chunk.ProtoChunk;
|
||||
import net.minecraft.world.level.chunk.status.ChunkStatus;
|
||||
import net.minecraft.world.level.chunk.status.WorldGenContext;
|
||||
import net.minecraft.world.level.dimension.LevelStem;
|
||||
import net.minecraft.world.level.dimension.DimensionType;
|
||||
import net.minecraft.world.level.levelgen.FlatLevelSource;
|
||||
import net.minecraft.world.level.levelgen.Heightmap;
|
||||
import net.minecraft.world.level.levelgen.WorldgenRandom;
|
||||
@@ -1019,30 +1022,39 @@ public class NMSBinding implements INMSBinding {
|
||||
}
|
||||
|
||||
public void inject(long seed, Engine engine, World world) throws NoSuchFieldException, IllegalAccessException {
|
||||
var chunkMap = ((CraftWorld)world).getHandle().getChunkSource().chunkMap;
|
||||
var worldGenContextField = getField(chunkMap.getClass(), WorldGenContext.class);
|
||||
ServerLevel level = ((CraftWorld) world).getHandle();
|
||||
validateDimensionContract(engine, world, level);
|
||||
|
||||
ChunkMap chunkMap = level.getChunkSource().chunkMap;
|
||||
Field worldGenContextField = getField(chunkMap.getClass(), WorldGenContext.class);
|
||||
worldGenContextField.setAccessible(true);
|
||||
var worldGenContext = (WorldGenContext) worldGenContextField.get(chunkMap);
|
||||
var dimensionType = chunkMap.level.dimensionTypeRegistration().unwrapKey().orElse(null);
|
||||
String expectedDimensionType = "iris:" + engine.getDimension().getDimensionTypeKey();
|
||||
if (dimensionType != null) {
|
||||
String actualDimensionType = dimensionType.identifier().toString();
|
||||
if (!dimensionType.identifier().getNamespace().equals("iris")) {
|
||||
IrisLogging.error("Loaded world %s with invalid dimension type! expected=%s actual=%s", world.getName(), expectedDimensionType, actualDimensionType);
|
||||
} else {
|
||||
IrisLogging.debug("Loaded world " + world.getName() + " with Iris dimension type " + actualDimensionType);
|
||||
}
|
||||
} else {
|
||||
IrisLogging.error("Loaded world %s with unknown dimension type! expected=%s", world.getName(), expectedDimensionType);
|
||||
}
|
||||
WorldGenContext worldGenContext = (WorldGenContext) worldGenContextField.get(chunkMap);
|
||||
|
||||
IrisChunkGenerator irisGenerator = new IrisChunkGenerator(worldGenContext.generator(), seed, engine, world);
|
||||
var newContext = new WorldGenContext(
|
||||
WorldGenContext newContext = new WorldGenContext(
|
||||
worldGenContext.level(), irisGenerator,
|
||||
worldGenContext.structureManager(), worldGenContext.lightEngine(), worldGenContext.mainThreadExecutor(), worldGenContext.unsavedListener());
|
||||
|
||||
worldGenContextField.set(chunkMap, newContext);
|
||||
retargetStructureCheck(((CraftWorld) world).getHandle(), irisGenerator);
|
||||
retargetStructureCheck(level, irisGenerator);
|
||||
}
|
||||
|
||||
private void validateDimensionContract(Engine engine, World world, ServerLevel level) {
|
||||
DimensionType actualType = level.dimensionType();
|
||||
String actualTypeKey = level.dimensionTypeRegistration().unwrapKey()
|
||||
.map(key -> key.identifier().toString())
|
||||
.orElse("<unregistered>");
|
||||
IrisDimensionRuntimeContract expected = IrisDimensionRuntimeContract.expected(engine.getDimension(), "iris");
|
||||
IrisDimensionRuntimeContract actual = new IrisDimensionRuntimeContract(
|
||||
actualTypeKey,
|
||||
actualType.minY(),
|
||||
actualType.height(),
|
||||
actualType.logicalHeight());
|
||||
String runtimeName = "Bukkit world '" + world.getName() + "'";
|
||||
expected.requireExact(runtimeName, actual);
|
||||
expected.requireHeight(runtimeName, level.getMinY(), level.getHeight());
|
||||
expected.requireHeight(runtimeName, world.getMinHeight(), world.getMaxHeight() - world.getMinHeight());
|
||||
IrisLogging.debug("Loaded world " + world.getName() + " with exact Iris dimension type " + actualTypeKey);
|
||||
}
|
||||
|
||||
private static void retargetStructureCheck(ServerLevel level, IrisChunkGenerator generator) throws NoSuchFieldException, IllegalAccessException {
|
||||
@@ -1208,33 +1220,38 @@ public class NMSBinding implements INMSBinding {
|
||||
|
||||
@Override
|
||||
public boolean injectBukkit() {
|
||||
if (injected.getAndSet(true))
|
||||
return true;
|
||||
try {
|
||||
IrisLogging.info("Injecting Bukkit");
|
||||
new AgentBuilder.Default()
|
||||
.disableClassFormatChanges()
|
||||
.with(AgentBuilder.RedefinitionStrategy.RETRANSFORMATION)
|
||||
.type(ElementMatchers.is(ServerLevel.class))
|
||||
.transform((builder, typeDescription, classLoader, module, protectionDomain) ->
|
||||
builder.visit(Advice.to(ServerLevelAdvice.class).on(ElementMatchers.isConstructor()
|
||||
.and(ElementMatchers.takesArgument(0, MinecraftServer.class))
|
||||
.and(ElementMatchers.takesArgument(5, LevelStem.class)))))
|
||||
.installOn(Agent.getInstrumentation());
|
||||
ByteBuddy buddy = new ByteBuddy();
|
||||
for (Class<?> clazz : List.of(ChunkAccess.class, ProtoChunk.class)) {
|
||||
buddy.redefine(clazz)
|
||||
.visit(Advice.to(ChunkAccessAdvice.class).on(ElementMatchers.isMethod().and(ElementMatchers.takesArguments(ShortList.class, int.class))))
|
||||
.make()
|
||||
.load(clazz.getClassLoader(), Agent.installed());
|
||||
synchronized (injected) {
|
||||
if (injected.get()) {
|
||||
return true;
|
||||
}
|
||||
try {
|
||||
IrisLogging.info("Injecting Bukkit");
|
||||
new AgentBuilder.Default()
|
||||
.disableClassFormatChanges()
|
||||
.with(AgentBuilder.RedefinitionStrategy.RETRANSFORMATION)
|
||||
.type(ElementMatchers.is(ServerLevel.class))
|
||||
.transform((builder, typeDescription, classLoader, module, protectionDomain) ->
|
||||
builder.visit(Advice.to(ServerLevelAdvice.class).on(ElementMatchers.isConstructor()
|
||||
.and(ElementMatchers.takesArgument(0, MinecraftServer.class))
|
||||
.and(ElementMatchers.takesArgument(5, LevelStem.class))
|
||||
.and(ElementMatchers.takesArgument(12, ChunkGenerator.class)))))
|
||||
.installOn(Agent.getInstrumentation());
|
||||
ByteBuddy buddy = new ByteBuddy();
|
||||
for (Class<?> clazz : List.of(ChunkAccess.class, ProtoChunk.class)) {
|
||||
buddy.redefine(clazz)
|
||||
.visit(Advice.to(ChunkAccessAdvice.class).on(ElementMatchers.isMethod().and(ElementMatchers.takesArguments(ShortList.class, int.class))))
|
||||
.make()
|
||||
.load(clazz.getClassLoader(), Agent.installed());
|
||||
}
|
||||
|
||||
return true;
|
||||
} catch (Throwable e) {
|
||||
IrisLogging.error(C.RED + "Failed to inject Bukkit");
|
||||
e.printStackTrace();
|
||||
injected.set(true);
|
||||
return true;
|
||||
} catch (Throwable e) {
|
||||
IrisLogging.error(C.RED + "Failed to inject Bukkit");
|
||||
e.printStackTrace();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -1304,7 +1321,8 @@ public class NMSBinding implements INMSBinding {
|
||||
static void enter(
|
||||
@Advice.Argument(0) MinecraftServer server,
|
||||
@Advice.Argument(4) ResourceKey<Level> dimensionKey,
|
||||
@Advice.Argument(value = 5, readOnly = false) LevelStem levelStem
|
||||
@Advice.Argument(value = 5, readOnly = false) LevelStem levelStem,
|
||||
@Advice.Argument(12) ChunkGenerator constructorGenerator
|
||||
) {
|
||||
if (dimensionKey == null)
|
||||
return;
|
||||
@@ -1315,18 +1333,19 @@ public class NMSBinding implements INMSBinding {
|
||||
return;
|
||||
}
|
||||
|
||||
Object generator = Class.forName("art.arcane.iris.core.lifecycle.WorldLifecycleStaging", true, Bukkit.getPluginManager().getPlugin("Iris")
|
||||
.getClass()
|
||||
.getClassLoader())
|
||||
.getDeclaredMethod("consumeStemGenerator", String.class)
|
||||
.invoke(null, levelId);
|
||||
ClassLoader pluginClassLoader = Bukkit.getPluginManager().getPlugin("Iris").getClass().getClassLoader();
|
||||
Class<?> generatorType = Class.forName("art.arcane.iris.engine.platform.PlatformChunkGenerator", true, pluginClassLoader);
|
||||
Object generator = generatorType.isInstance(constructorGenerator) ? constructorGenerator : null;
|
||||
if (generator == null) {
|
||||
generator = Class.forName("art.arcane.iris.core.lifecycle.WorldLifecycleStaging", true, pluginClassLoader)
|
||||
.getDeclaredMethod("consumeStemGenerator", String.class)
|
||||
.invoke(null, levelId);
|
||||
}
|
||||
if (!(generator instanceof ChunkGenerator gen) || !gen.getClass().getPackageName().startsWith("art.arcane.iris")) {
|
||||
return;
|
||||
}
|
||||
|
||||
Object bindings = Class.forName("art.arcane.iris.core.nms.INMS", true, Bukkit.getPluginManager().getPlugin("Iris")
|
||||
.getClass()
|
||||
.getClassLoader())
|
||||
Object bindings = Class.forName("art.arcane.iris.core.nms.INMS", true, pluginClassLoader)
|
||||
.getDeclaredMethod("get")
|
||||
.invoke(null);
|
||||
if (bindings == null) {
|
||||
|
||||
+4
-1
@@ -25,7 +25,10 @@ final class VanillaStructureBiomes {
|
||||
if (source == null) {
|
||||
return keys;
|
||||
}
|
||||
for (Holder<Biome> holder : source.possibleBiomes()) {
|
||||
Set<Holder<Biome>> possibleBiomes = source instanceof CustomBiomeSource customBiomeSource
|
||||
? customBiomeSource.possibleStructureBiomes()
|
||||
: source.possibleBiomes();
|
||||
for (Holder<Biome> holder : possibleBiomes) {
|
||||
Optional<ResourceKey<Biome>> key = holder.unwrapKey();
|
||||
if (key.isPresent()) {
|
||||
keys.add(key.get().identifier().toString());
|
||||
|
||||
+71
@@ -0,0 +1,71 @@
|
||||
package art.arcane.iris.core.nms.v26_2_R1;
|
||||
|
||||
import org.junit.Test;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
|
||||
public class IrisChunkGeneratorMonumentLocateContractTest {
|
||||
@Test
|
||||
public void irisPlacementRunsBeforeExactNativeMonumentIsRemovedFromDelegateLookup() throws IOException {
|
||||
String source = Files.readString(Path.of(System.getProperty("iris.nmsChunkGeneratorSource")));
|
||||
int findStart = source.indexOf("findNearestMapStructure(ServerLevel level");
|
||||
assertTrue(findStart >= 0);
|
||||
int filterStart = source.indexOf("private HolderSet<Structure> filterReachableStructures", findStart);
|
||||
assertTrue(filterStart > findStart);
|
||||
String findMethod = source.substring(findStart, filterStart);
|
||||
int irisLocate = findMethod.indexOf("IrisStructureLocator.locate(");
|
||||
int searchLimit = findMethod.indexOf("LocateStatus.SEARCH_LIMIT_REACHED", irisLocate);
|
||||
int limitSkip = findMethod.indexOf("continue;", searchLimit);
|
||||
int nativeFilter = findMethod.indexOf("filterReachableStructures(level, holders)");
|
||||
int delegateLocate = findMethod.indexOf("delegate.findNearestMapStructure(level, reachable");
|
||||
int reachabilityStart = source.indexOf("private Set<String> reachableStructureKeys", filterStart);
|
||||
assertTrue(reachabilityStart > filterStart);
|
||||
String filterMethod = source.substring(filterStart, reachabilityStart);
|
||||
int monumentReject = filterMethod.indexOf("if (NATIVE_MONUMENT_KEY.equals(key)");
|
||||
int rejectContinue = filterMethod.indexOf("continue;", monumentReject);
|
||||
|
||||
assertTrue(irisLocate >= 0);
|
||||
assertTrue(searchLimit > irisLocate);
|
||||
assertTrue(limitSkip > searchLimit);
|
||||
assertTrue(nativeFilter > limitSkip);
|
||||
assertTrue(delegateLocate > nativeFilter);
|
||||
assertTrue(monumentReject >= 0);
|
||||
assertTrue(rejectContinue > monumentReject);
|
||||
assertTrue(findMethod.contains("new BlockPos(result.originX(), result.baseY(), result.originZ())"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void stiltSupportUsesPlacedSolidOccupancyWithoutSnapshotDifferenceRequirement() throws IOException {
|
||||
String source = Files.readString(Path.of(System.getProperty("iris.nativeStructurePostProcessorSource")));
|
||||
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);
|
||||
|
||||
assertTrue(placement >= 0);
|
||||
assertTrue(stiltPlacement > placement);
|
||||
assertTrue(occupancyCheck > stiltPlacement);
|
||||
assertFalse(source.contains("state.equals("));
|
||||
assertFalse(source.contains("snapshot.states"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void verticalShiftMovesPiecesJigsawJunctionsAndCachedBoundsTogether() throws IOException {
|
||||
String source = Files.readString(Path.of(System.getProperty("iris.nativeStructurePostProcessorSource")));
|
||||
int method = source.indexOf("public static int applyVerticalShift");
|
||||
int clamp = source.indexOf("StructureVerticalBounds.clampOffset", method);
|
||||
int pieceMove = source.indexOf("piece.move(0, offsetY, 0)", clamp);
|
||||
int junctionMove = source.indexOf("junction.getSourceGroundY() + offsetY", pieceMove);
|
||||
int boundsMove = source.indexOf("bounds.move(0, offsetY, 0)", junctionMove);
|
||||
|
||||
assertTrue(method >= 0);
|
||||
assertTrue(clamp > method);
|
||||
assertTrue(pieceMove > clamp);
|
||||
assertTrue(junctionMove > pieceMove);
|
||||
assertTrue(boundsMove > junctionMove);
|
||||
}
|
||||
}
|
||||
+38
@@ -0,0 +1,38 @@
|
||||
package art.arcane.iris.nativegen;
|
||||
|
||||
import net.minecraft.world.level.levelgen.GenerationStep;
|
||||
import org.junit.Test;
|
||||
|
||||
import static org.junit.Assert.assertFalse;
|
||||
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));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void buriedStructuresPreserveUnrelatedSurfaceForest() {
|
||||
assertFalse(NativeStructurePostProcessor.shouldClearVegetationColumn(99, 100, false));
|
||||
assertFalse(NativeStructurePostProcessor.shouldClearVegetationColumn(20, 100, false));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void explicitVegetationOptionForcesUnusualPlacementCleanup() {
|
||||
assertTrue(NativeStructurePostProcessor.shouldClearVegetationColumn(20, 100, true));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void allUndergroundGenerationStepsShareOneClassification() {
|
||||
assertTrue(NativeStructurePostProcessor.isUndergroundStep(
|
||||
GenerationStep.Decoration.UNDERGROUND_STRUCTURES));
|
||||
assertTrue(NativeStructurePostProcessor.isUndergroundStep(
|
||||
GenerationStep.Decoration.UNDERGROUND_DECORATION));
|
||||
assertTrue(NativeStructurePostProcessor.isUndergroundStep(
|
||||
GenerationStep.Decoration.STRONGHOLDS));
|
||||
assertFalse(NativeStructurePostProcessor.isUndergroundStep(
|
||||
GenerationStep.Decoration.SURFACE_STRUCTURES));
|
||||
}
|
||||
}
|
||||
@@ -3,11 +3,13 @@ def mainClass = 'art.arcane.iris.Iris'
|
||||
def bootstrapperClass = 'art.arcane.iris.IrisBootstrap'
|
||||
|
||||
String volmLibCoordinate = providers.gradleProperty('volmLibCoordinate')
|
||||
.orElse('com.github.VolmitSoftware:VolmLib:cdf7e8bb880199734dacd386a6616bda799fcde1')
|
||||
.orElse('com.github.VolmitSoftware:VolmLib:4c38988b344792a79f925f57f8a675bc85fc1bed')
|
||||
.get()
|
||||
|
||||
dependencies {
|
||||
compileOnly(project(':core'))
|
||||
compileOnly(libs.lombok)
|
||||
annotationProcessor(libs.lombok)
|
||||
compileOnly(volmLibCoordinate) {
|
||||
transitive = false
|
||||
}
|
||||
@@ -21,7 +23,6 @@ dependencies {
|
||||
transitive = false
|
||||
}
|
||||
compileOnly(libs.placeholderApi)
|
||||
compileOnly(libs.multiverseCore)
|
||||
}
|
||||
|
||||
tasks.named('processResources').configure {
|
||||
@@ -41,3 +42,8 @@ tasks.named('processResources').configure {
|
||||
tasks.named('jar', Jar).configure {
|
||||
archiveBaseName.set('iris-bukkit-plugin')
|
||||
}
|
||||
|
||||
tasks.named('test').configure {
|
||||
systemProperty('iris.commandFindSource', file('src/main/java/art/arcane/iris/core/commands/CommandFind.java').absolutePath)
|
||||
systemProperty('iris.commandStructureSource', file('src/main/java/art/arcane/iris/core/commands/CommandStructure.java').absolutePath)
|
||||
}
|
||||
@@ -18,8 +18,12 @@
|
||||
|
||||
package art.arcane.iris;
|
||||
|
||||
import art.arcane.iris.engine.IrisEngineEffects;
|
||||
import art.arcane.iris.engine.IrisWorldManager;
|
||||
|
||||
import art.arcane.iris.engine.framework.EngineComponentCleanup;
|
||||
import art.arcane.iris.engine.framework.EngineEffectsProvider;
|
||||
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;
|
||||
@@ -29,6 +33,7 @@ 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;
|
||||
@@ -57,6 +62,7 @@ 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;
|
||||
@@ -116,6 +122,7 @@ 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;
|
||||
@@ -201,27 +208,35 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
|
||||
}
|
||||
}
|
||||
|
||||
public static KList<Object> initialize(String s, Class<? extends Annotation> slicedClass) {
|
||||
private static <T> KList<T> initialize(String s, Class<T> requiredType) {
|
||||
JarScanner js = new JarScanner(instance.getJarFile(), s);
|
||||
KList<Object> v = new KList<>();
|
||||
KList<T> v = new KList<>();
|
||||
J.attempt(js::scan);
|
||||
for (Class<?> i : js.getClasses()) {
|
||||
if (slicedClass == null || i.isAnnotationPresent(slicedClass)) {
|
||||
try {
|
||||
v.add(i.getDeclaredConstructor().newInstance());
|
||||
} catch (Throwable ex) {
|
||||
Iris.warn("Skipped class initialization for %s: %s%s",
|
||||
i.getName(),
|
||||
ex.getClass().getSimpleName(),
|
||||
ex.getMessage() == null ? "" : " - " + ex.getMessage());
|
||||
Iris.reportError(ex);
|
||||
}
|
||||
if (!isConcreteImplementation(i, requiredType)) {
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
v.add(requiredType.cast(i.getDeclaredConstructor().newInstance()));
|
||||
} catch (Throwable ex) {
|
||||
Iris.warn("Skipped class initialization for %s: %s%s",
|
||||
i.getName(),
|
||||
ex.getClass().getSimpleName(),
|
||||
ex.getMessage() == null ? "" : " - " + ex.getMessage());
|
||||
Iris.reportError(ex);
|
||||
}
|
||||
}
|
||||
|
||||
return v;
|
||||
}
|
||||
|
||||
static boolean isConcreteImplementation(Class<?> candidate, Class<?> requiredType) {
|
||||
int modifiers = candidate.getModifiers();
|
||||
return requiredType.isAssignableFrom(candidate)
|
||||
&& !candidate.isInterface()
|
||||
&& !Modifier.isAbstract(modifiers);
|
||||
}
|
||||
|
||||
public static KList<Class<?>> getClasses(String s, Class<? extends Annotation> slicedClass) {
|
||||
JarScanner js = new JarScanner(instance.getJarFile(), s);
|
||||
KList<Class<?>> v = new KList<>();
|
||||
@@ -243,10 +258,6 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
|
||||
return v;
|
||||
}
|
||||
|
||||
public static KList<Object> initialize(String s) {
|
||||
return initialize(s, null);
|
||||
}
|
||||
|
||||
public static void sq(Runnable r) {
|
||||
synchronized (syncJobs) {
|
||||
syncJobs.queue(r);
|
||||
@@ -599,9 +610,10 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
|
||||
services = new KMap<>();
|
||||
setupAudience();
|
||||
Bindings.setupSentry();
|
||||
initialize("art.arcane.iris.core.service").forEach((i) -> {
|
||||
services.put((Class<? extends IrisService>) i.getClass(), (IrisService) i);
|
||||
IrisServices.register(i.getClass(), i);
|
||||
initialize("art.arcane.iris.core.service", IrisService.class).forEach((i) -> {
|
||||
Class<? extends IrisService> serviceType = i.getClass().asSubclass(IrisService.class);
|
||||
services.put(serviceType, i);
|
||||
IrisServices.register(serviceType, i);
|
||||
});
|
||||
IrisServices.register(BlockEditAccess.class, services.get(EditSVC.class));
|
||||
IrisServices.register(PreservationRegistry.class, services.get(PreservationSVC.class));
|
||||
@@ -619,6 +631,9 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
|
||||
tickets = new ChunkTickets();
|
||||
linkMultiverseCore = new MultiverseCoreLink();
|
||||
IrisServices.register(MultiverseCoreLink.class, linkMultiverseCore);
|
||||
IrisServices.register(EngineComponentCleanup.class, (EngineComponentCleanup) BukkitPlatform::unregisterListener);
|
||||
IrisServices.register(EngineEffectsProvider.class, (EngineEffectsProvider) IrisEngineEffects::new);
|
||||
IrisServices.register(EnginePlatformHooks.class, new BukkitEnginePlatformHooks());
|
||||
IrisServices.register(EngineWorldManagerProvider.class, (EngineWorldManagerProvider) (Engine engine) -> {
|
||||
IrisWorldManager manager = new IrisWorldManager(engine);
|
||||
manager.startManager();
|
||||
@@ -711,7 +726,7 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
|
||||
Iris.info(C.LIGHT_PURPLE + "Preparing Spawn for " + s + "' using Iris:" + generator + "...");
|
||||
WorldCreator c = WorldCreator.ofKey(worldKey)
|
||||
.generator(gen)
|
||||
.environment(dim.getEnvironment());
|
||||
.environment(BukkitEnvironment.from(dim.getEnvironment()));
|
||||
Long stagedSeed = IrisWorlds.readBukkitWorldSeed(s);
|
||||
if (stagedSeed != null) {
|
||||
c.seed(stagedSeed);
|
||||
@@ -1067,7 +1082,7 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
|
||||
}
|
||||
|
||||
private void setupPapi() {
|
||||
if (Bukkit.getPluginManager().getPlugin("PlaceholderAPI") != null) {
|
||||
if (Bukkit.getPluginManager().isPluginEnabled("PlaceholderAPI")) {
|
||||
new IrisPapiExpansion().register();
|
||||
}
|
||||
}
|
||||
@@ -1215,10 +1230,9 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
|
||||
NamespacedKey worldKey = IrisWorldStorage.keyFromLegacyName(worldName);
|
||||
|
||||
IrisWorld w = IrisWorld.builder()
|
||||
.key(worldKey)
|
||||
.platformIdentity(worldKey.toString())
|
||||
.name(worldName)
|
||||
.seed(1337)
|
||||
.environment(dim.getEnvironment())
|
||||
.worldFolder(IrisWorldStorage.dimensionRoot(worldKey))
|
||||
.minHeight(dim.getMinHeight())
|
||||
.maxHeight(dim.getMaxHeight())
|
||||
|
||||
@@ -2,46 +2,19 @@ package art.arcane.iris;
|
||||
|
||||
import art.arcane.iris.core.pack.DefaultPackBootstrapProvisioner;
|
||||
import art.arcane.iris.core.pack.DefaultPackBootstrapProvisioner.ProvisionResult;
|
||||
import io.papermc.paper.datapack.Datapack;
|
||||
import io.papermc.paper.datapack.DatapackRegistrar;
|
||||
import io.papermc.paper.datapack.DiscoveredDatapack;
|
||||
import io.papermc.paper.plugin.bootstrap.BootstrapContext;
|
||||
import io.papermc.paper.plugin.bootstrap.PluginBootstrap;
|
||||
import io.papermc.paper.plugin.lifecycle.event.types.LifecycleEvents;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Path;
|
||||
|
||||
@SuppressWarnings("UnstableApiUsage")
|
||||
public final class IrisBootstrap implements PluginBootstrap {
|
||||
static final String PACK_ID = "generated";
|
||||
|
||||
@Override
|
||||
public void bootstrap(BootstrapContext context) {
|
||||
ProvisionResult provisioned = provision(context);
|
||||
Path datapackRoot = provisioned.datapackRoot();
|
||||
context.getLogger().info("Iris startup datapack is {} at {}", provisioned.status(), datapackRoot);
|
||||
context.getLifecycleManager().registerEventHandler(LifecycleEvents.DATAPACK_DISCOVERY, event -> {
|
||||
try {
|
||||
discoverPack(event.registrar(), datapackRoot);
|
||||
} catch (IOException e) {
|
||||
throw new IllegalStateException("Unable to discover the Iris startup datapack at " + datapackRoot, e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
static DiscoveredDatapack discoverPack(DatapackRegistrar registrar, Path datapackRoot) throws IOException {
|
||||
DiscoveredDatapack datapack = registrar.discoverPack(
|
||||
datapackRoot,
|
||||
PACK_ID,
|
||||
configurer -> configurer
|
||||
.autoEnableOnServerStart(true)
|
||||
.position(true, Datapack.Position.TOP)
|
||||
);
|
||||
if (datapack == null) {
|
||||
throw new IllegalStateException("Paper did not accept the Iris startup datapack at " + datapackRoot);
|
||||
}
|
||||
return datapack;
|
||||
}
|
||||
|
||||
private static ProvisionResult provision(BootstrapContext context) {
|
||||
|
||||
+35
@@ -0,0 +1,35 @@
|
||||
/*
|
||||
* 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.commands;
|
||||
|
||||
final class BukkitNativeStructureLocatePolicy {
|
||||
private static final String MONUMENT_KEY = "minecraft:monument";
|
||||
private static final String UNAVAILABLE_MESSAGE = "Native monument locating is unavailable on Paper, Purpur, and Folia because a cold search can stall the server thread. The monument lookup was skipped; monument generation is unaffected.";
|
||||
|
||||
private BukkitNativeStructureLocatePolicy() {
|
||||
}
|
||||
|
||||
static boolean isUnavailable(String structureKey) {
|
||||
return structureKey != null && MONUMENT_KEY.equalsIgnoreCase(structureKey.trim());
|
||||
}
|
||||
|
||||
static String unavailableMessage() {
|
||||
return UNAVAILABLE_MESSAGE;
|
||||
}
|
||||
}
|
||||
+95
-13
@@ -42,6 +42,7 @@ import io.papermc.paper.registry.RegistryKey;
|
||||
import org.bukkit.Location;
|
||||
import org.bukkit.NamespacedKey;
|
||||
import org.bukkit.Registry;
|
||||
import org.bukkit.World;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.generator.structure.Structure;
|
||||
import org.bukkit.util.StructureSearchResult;
|
||||
@@ -98,7 +99,7 @@ public class CommandFind implements DirectorExecutor {
|
||||
EngineBukkitOps.gotoPOI(e, type, player(), teleport);
|
||||
}
|
||||
|
||||
@Director(description = "Find a structure (a vanilla key like minecraft:village_plains or minecraft:stronghold, or an imported iris structure key)")
|
||||
@Director(description = "Find a structure (a vanilla key like minecraft:village_plains or minecraft:stronghold, or an imported iris structure key)", sync = true)
|
||||
public void structure(
|
||||
@Param(description = "The structure to look for (e.g. minecraft:village_plains, minecraft:stronghold, minecraft_ancient_city)", customHandler = StructureHandler.class)
|
||||
String structure
|
||||
@@ -117,7 +118,12 @@ public class CommandFind implements DirectorExecutor {
|
||||
}
|
||||
|
||||
if (IrisStructureLocator.isPlaced(e, structure)) {
|
||||
EngineBukkitOps.gotoStructure(e, structure, player(), true);
|
||||
locateIrisStructure(e, structure, commandSender);
|
||||
return;
|
||||
}
|
||||
|
||||
if (BukkitNativeStructureLocatePolicy.isUnavailable(structure)) {
|
||||
commandSender.sendMessage(C.RED + BukkitNativeStructureLocatePolicy.unavailableMessage());
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -127,6 +133,8 @@ public class CommandFind implements DirectorExecutor {
|
||||
return;
|
||||
}
|
||||
|
||||
World targetWorld = target.getWorld();
|
||||
Location origin = target.getLocation();
|
||||
commandSender.sendMessage(C.GRAY + "Locating " + structure + "...");
|
||||
J.s(() -> {
|
||||
try {
|
||||
@@ -140,32 +148,66 @@ public class CommandFind implements DirectorExecutor {
|
||||
}
|
||||
}
|
||||
if (match == null) {
|
||||
commandSender.sendMessage(C.RED + "Unknown structure: " + structure);
|
||||
sendStructureMessage(target, commandSender, C.RED + "Unknown structure: " + structure);
|
||||
return;
|
||||
}
|
||||
if (!StructureReachability.isReachable(e, structure)) {
|
||||
KList<String> miss = StructureReachability.missingBiomeKeys(e, structure);
|
||||
commandSender.sendMessage(C.YELLOW + structure + " cannot generate in this world (its required biomes are not produced by this pack"
|
||||
+ (miss.isEmpty() ? "" : ": needs " + String.join("/", miss)) + ").");
|
||||
sendStructureMessage(target, commandSender,
|
||||
C.YELLOW + structure + " cannot generate in this world (its required biomes are not produced by this pack"
|
||||
+ (miss.isEmpty() ? "" : ": needs " + String.join("/", miss)) + ").");
|
||||
return;
|
||||
}
|
||||
StructureSearchResult result = target.getWorld().locateNearestStructure(target.getLocation(), match, 100, true);
|
||||
StructureSearchResult result = targetWorld.locateNearestStructure(origin, match, 100, false);
|
||||
if (result == null || result.getLocation() == null) {
|
||||
commandSender.sendMessage(C.YELLOW + "No " + structure + " found within range of you.");
|
||||
sendStructureMessage(target, commandSender, C.YELLOW + "No " + structure + " found within range of you.");
|
||||
return;
|
||||
}
|
||||
Location at = result.getLocation();
|
||||
int y = target.getWorld().getHighestBlockYAt(at.getBlockX(), at.getBlockZ()) + 2;
|
||||
Location dest = new Location(target.getWorld(), at.getBlockX() + 0.5, y, at.getBlockZ() + 0.5);
|
||||
BukkitPlatform.teleportAsync(target, dest);
|
||||
commandSender.sendMessage(C.GREEN + "Teleported to " + structure + " @ " + at.getBlockX() + ", " + at.getBlockZ());
|
||||
prepareStructureTeleport(target, targetWorld, commandSender, structure, result.getLocation(), false);
|
||||
} catch (Throwable t) {
|
||||
commandSender.sendMessage(C.RED + "Could not locate " + structure + ": " + t.getClass().getSimpleName());
|
||||
sendStructureMessage(target, commandSender, C.RED + "Could not locate " + structure + ": " + t.getClass().getSimpleName());
|
||||
Iris.reportError("Could not locate structure '" + structure + "'.", t);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void locateIrisStructure(Engine engine, String structure, VolmitSender commandSender) {
|
||||
Player target = player();
|
||||
if (target == null) {
|
||||
commandSender.sendMessage(C.GOLD + "Run this in-game to teleport to a structure.");
|
||||
return;
|
||||
}
|
||||
World targetWorld = target.getWorld();
|
||||
Location origin = target.getLocation();
|
||||
int blockX = origin.getBlockX();
|
||||
int blockZ = origin.getBlockZ();
|
||||
commandSender.sendMessage(C.GRAY + "Locating " + structure + "...");
|
||||
J.a(() -> {
|
||||
try {
|
||||
IrisStructureLocator.LocateResult result =
|
||||
IrisStructureLocator.locate(engine, structure, blockX, blockZ, 1024);
|
||||
if (result.status() == IrisStructureLocator.LocateStatus.SEARCH_LIMIT_REACHED) {
|
||||
sendStructureMessage(target, commandSender,
|
||||
C.YELLOW + "Unable to locate " + structure
|
||||
+ ": the density search safety limit was reached before the full 1024-chunk radius was searched.");
|
||||
return;
|
||||
}
|
||||
if (!result.found()) {
|
||||
sendStructureMessage(target, commandSender,
|
||||
C.YELLOW + "No " + structure + " found within 1024 chunks of you.");
|
||||
return;
|
||||
}
|
||||
Location destination = new Location(
|
||||
targetWorld, result.originX(), result.baseY(), result.originZ());
|
||||
prepareStructureTeleport(target, targetWorld, commandSender, structure, destination, true);
|
||||
} catch (Throwable t) {
|
||||
sendStructureMessage(target, commandSender,
|
||||
C.RED + "Could not locate " + structure + ": " + t.getClass().getSimpleName());
|
||||
Iris.reportError("Could not locate Iris-placed structure '" + structure + "'.", t);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Director(description = "Find an object")
|
||||
public void object(
|
||||
@Param(description = "The object to look for", customHandler = ObjectHandler.class)
|
||||
@@ -199,4 +241,44 @@ public class CommandFind implements DirectorExecutor {
|
||||
|
||||
sender().sendMessage(C.RED + object + " is not configured in any region/biome object placements.");
|
||||
}
|
||||
|
||||
private void prepareStructureTeleport(Player target, World world, VolmitSender commandSender, String structure,
|
||||
Location at, boolean useLocatedY) {
|
||||
int chunkX = at.getBlockX() >> 4;
|
||||
int chunkZ = at.getBlockZ() >> 4;
|
||||
BukkitPlatform.chunkAtAsync(world, chunkX, chunkZ, true).whenComplete((chunk, error) -> {
|
||||
if (error != null) {
|
||||
sendStructureMessage(target, commandSender, C.RED + "Could not load the destination for " + structure + ".");
|
||||
Iris.reportError("Could not load structure destination '" + structure + "'.", error);
|
||||
return;
|
||||
}
|
||||
boolean scheduled = J.runRegion(world, chunkX, chunkZ,
|
||||
() -> teleportToStructure(target, world, commandSender, structure, at, useLocatedY));
|
||||
if (!scheduled) {
|
||||
sendStructureMessage(target, commandSender, C.RED + "Could not schedule the destination lookup for " + structure + ".");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void teleportToStructure(Player target, World world, VolmitSender commandSender, String structure,
|
||||
Location at, boolean useLocatedY) {
|
||||
try {
|
||||
int y = useLocatedY
|
||||
? Math.max(world.getMinHeight() + 1, Math.min(world.getMaxHeight() - 1, at.getBlockY() + 2))
|
||||
: world.getHighestBlockYAt(at.getBlockX(), at.getBlockZ()) + 2;
|
||||
Location destination = new Location(world, at.getBlockX() + 0.5, y, at.getBlockZ() + 0.5);
|
||||
J.runEntity(target, () -> {
|
||||
BukkitPlatform.teleportAsync(target, destination);
|
||||
commandSender.sendMessage(C.GREEN + "Teleported to " + structure + " @ "
|
||||
+ at.getBlockX() + ", " + y + ", " + at.getBlockZ());
|
||||
});
|
||||
} catch (Throwable t) {
|
||||
sendStructureMessage(target, commandSender, C.RED + "Could not prepare the destination for " + structure + ".");
|
||||
Iris.reportError("Could not prepare structure destination '" + structure + "'.", t);
|
||||
}
|
||||
}
|
||||
|
||||
private void sendStructureMessage(Player target, VolmitSender commandSender, String message) {
|
||||
J.runEntity(target, () -> commandSender.sendMessage(message));
|
||||
}
|
||||
}
|
||||
+124
-56
@@ -18,6 +18,7 @@
|
||||
|
||||
package art.arcane.iris.core.commands;
|
||||
|
||||
import art.arcane.iris.Iris;
|
||||
import art.arcane.iris.core.loader.IrisData;
|
||||
import art.arcane.iris.core.structure.BulkStructureImporter;
|
||||
import art.arcane.iris.core.structure.StructureCaptureImporter;
|
||||
@@ -41,7 +42,9 @@ import art.arcane.volmlib.util.director.annotations.Director;
|
||||
import art.arcane.volmlib.util.director.annotations.Param;
|
||||
import art.arcane.iris.core.tools.IrisToolbelt;
|
||||
import art.arcane.iris.engine.platform.PlatformChunkGenerator;
|
||||
import art.arcane.iris.util.common.plugin.VolmitSender;
|
||||
import art.arcane.volmlib.util.math.RNG;
|
||||
import art.arcane.iris.util.common.scheduling.J;
|
||||
import io.papermc.paper.registry.RegistryAccess;
|
||||
import io.papermc.paper.registry.RegistryKey;
|
||||
import org.bukkit.Bukkit;
|
||||
@@ -51,12 +54,12 @@ import org.bukkit.Registry;
|
||||
import org.bukkit.World;
|
||||
import org.bukkit.block.Block;
|
||||
import org.bukkit.block.data.BlockData;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.generator.structure.Structure;
|
||||
import org.bukkit.util.StructureSearchResult;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
@@ -111,7 +114,7 @@ public class CommandStructure implements DirectorExecutor {
|
||||
sender().sendMessage(C.GRAY + "Captured " + report.imported() + " structures. Place them from a 'structures' list and regenerate chunks. Delete a structures/*.json to re-capture it.");
|
||||
}
|
||||
|
||||
@Director(description = "Locate every vanilla/datapack/iris structure to verify which are locatable in this world. Heavy synchronous search per structure - keep the radius modest. 'Not found' can mean rarer than the radius, a different dimension (nether/end structures never appear in the overworld), or a structure that generates but cannot be located; generation itself happens during chunk decoration, independent of locate.", aliases = {"locateall"}, origin = DirectorOrigin.BOTH, sync = true)
|
||||
@Director(description = "Verify native structure eligibility and locate Iris-placed structures without running blocking native searches.", aliases = {"locateall"}, origin = DirectorOrigin.BOTH, sync = true)
|
||||
public void verify(
|
||||
@Param(description = "The dimension to verify", aliases = "dim")
|
||||
IrisDimension dimension,
|
||||
@@ -124,85 +127,150 @@ public class CommandStructure implements DirectorExecutor {
|
||||
return;
|
||||
}
|
||||
boolean senderIsPlayer = sender() != null && sender().isPlayer();
|
||||
Location center = (senderIsPlayer && player().getWorld() == world) ? player().getLocation() : world.getSpawnLocation();
|
||||
Location center = senderIsPlayer && player().getWorld() == world
|
||||
? player().getLocation()
|
||||
: world.getSpawnLocation();
|
||||
int searchRadius = Math.max(1, Math.min(radius, 1000));
|
||||
|
||||
sender().sendMessage(C.GREEN + "Verifying structures in " + C.WHITE + world.getName() + C.GREEN + " from " + center.getBlockX() + "," + center.getBlockZ() + " within " + searchRadius + " chunks...");
|
||||
|
||||
Engine engine = null;
|
||||
PlatformChunkGenerator access = IrisToolbelt.access(world);
|
||||
if (access != null) {
|
||||
engine = access.getEngine();
|
||||
Engine engine = access == null ? null : access.getEngine();
|
||||
if (engine == null) {
|
||||
sender().sendMessage(C.RED + "The selected Iris world has no active generator engine.");
|
||||
return;
|
||||
}
|
||||
Set<String> reachable = engine == null ? Collections.emptySet() : StructureReachability.reachableKeys(engine);
|
||||
|
||||
int found = 0;
|
||||
int missing = 0;
|
||||
int unreachable = 0;
|
||||
int irisPlaced = 0;
|
||||
KList<String> notFound = new KList<>();
|
||||
KList<String> cannotGenerate = new KList<>();
|
||||
KList<String> structureKeys = new KList<>();
|
||||
Registry<Structure> structureRegistry = RegistryAccess.registryAccess().getRegistry(RegistryKey.STRUCTURE);
|
||||
for (Structure structure : structureRegistry) {
|
||||
NamespacedKey key = structureRegistry.getKey(structure);
|
||||
String keyName = key == null ? structure.toString() : key.toString();
|
||||
boolean isIrisPlaced = engine != null && IrisStructureLocator.suppressesVanilla(engine, keyName);
|
||||
boolean isReachable = engine != null && reachable.contains(keyName.toLowerCase());
|
||||
if (engine != null && !isIrisPlaced && !isReachable) {
|
||||
unreachable++;
|
||||
KList<String> miss = StructureReachability.missingBiomeKeys(engine, keyName);
|
||||
cannotGenerate.add(keyName + (miss.isEmpty() ? "" : " (needs " + String.join("/", miss) + ")"));
|
||||
if (key != null) {
|
||||
structureKeys.add(key.toString());
|
||||
}
|
||||
}
|
||||
VolmitSender commandSender = sender();
|
||||
Player target = senderIsPlayer ? player() : null;
|
||||
commandSender.sendMessage(C.GREEN + "Verifying structures in " + C.WHITE + world.getName()
|
||||
+ C.GREEN + " from " + center.getBlockX() + "," + center.getBlockZ()
|
||||
+ " within " + searchRadius + " chunks...");
|
||||
int centerX = center.getBlockX();
|
||||
int centerZ = center.getBlockZ();
|
||||
J.a(() -> runVerification(engine, structureKeys, centerX, centerZ, searchRadius, commandSender, target));
|
||||
}
|
||||
|
||||
private void runVerification(Engine engine, KList<String> structureKeys, int centerX, int centerZ,
|
||||
int searchRadius, VolmitSender commandSender, Player target) {
|
||||
KList<String> messages = new KList<>();
|
||||
Set<String> reachable;
|
||||
try {
|
||||
reachable = StructureReachability.reachableKeys(engine);
|
||||
} catch (Throwable error) {
|
||||
messages.add(C.RED + "Structure verification could not resolve native biome reachability.");
|
||||
sendVerificationMessages(commandSender, target, messages);
|
||||
Iris.reportError("Could not resolve native structure biome reachability for verification.", error);
|
||||
return;
|
||||
}
|
||||
int located = 0;
|
||||
int nativeEligible = 0;
|
||||
int disabled = 0;
|
||||
int unreachable = 0;
|
||||
int unavailable = 0;
|
||||
int searchLimited = 0;
|
||||
int errors = 0;
|
||||
for (String keyName : structureKeys) {
|
||||
boolean irisPlaced = IrisStructureLocator.isPlaced(engine, keyName);
|
||||
if (irisPlaced) {
|
||||
try {
|
||||
IrisStructureLocator.LocateResult result =
|
||||
IrisStructureLocator.locate(engine, keyName, centerX, centerZ, searchRadius);
|
||||
if (result.status() == IrisStructureLocator.LocateStatus.SEARCH_LIMIT_REACHED) {
|
||||
searchLimited++;
|
||||
messages.add(C.YELLOW + "[iris-search-limit] " + C.WHITE + keyName + C.YELLOW
|
||||
+ ": unable to complete the density search before its safety limit was reached");
|
||||
continue;
|
||||
}
|
||||
if (!result.found()) {
|
||||
messages.add(C.YELLOW + "[iris-not-found] " + C.WHITE + keyName);
|
||||
continue;
|
||||
}
|
||||
located++;
|
||||
messages.add(C.AQUA + "[iris] " + C.WHITE + keyName + C.GREEN + " @ "
|
||||
+ result.originX() + "," + result.baseY() + "," + result.originZ());
|
||||
} catch (Throwable error) {
|
||||
errors++;
|
||||
messages.add(C.RED + "[error] " + C.WHITE + keyName + C.RED + ": "
|
||||
+ error.getClass().getSimpleName());
|
||||
Iris.reportError("Could not verify Iris-placed structure '" + keyName + "'.", error);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (isIrisPlaced) {
|
||||
irisPlaced++;
|
||||
if (!engine.getDimension().getImportedStructures().shouldGenerate(keyName)) {
|
||||
disabled++;
|
||||
messages.add(C.GRAY + "[disabled] " + C.WHITE + keyName);
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
StructureSearchResult result = world.locateNearestStructure(center, structure, searchRadius, true);
|
||||
if (result != null && result.getLocation() != null) {
|
||||
found++;
|
||||
Location l = result.getLocation();
|
||||
sender().sendMessage((isIrisPlaced ? C.AQUA + "[iris] " : C.GREEN + "[ok] ") + C.WHITE + keyName + C.GREEN + " @ " + l.getBlockX() + "," + l.getBlockZ());
|
||||
} else {
|
||||
missing++;
|
||||
notFound.add(keyName);
|
||||
}
|
||||
} catch (Throwable e) {
|
||||
missing++;
|
||||
notFound.add(keyName + " (error: " + e.getClass().getSimpleName() + ")");
|
||||
if (!reachable.contains(keyName.toLowerCase(Locale.ROOT))) {
|
||||
unreachable++;
|
||||
KList<String> missing = StructureReachability.missingBiomeKeys(engine, keyName);
|
||||
messages.add(C.YELLOW + "[unreachable] " + C.WHITE + keyName
|
||||
+ (missing.isEmpty() ? "" : C.YELLOW + " needs " + String.join("/", missing)));
|
||||
continue;
|
||||
}
|
||||
if (BukkitNativeStructureLocatePolicy.isUnavailable(keyName)) {
|
||||
unavailable++;
|
||||
messages.add(C.RED + "[unavailable] " + C.WHITE + keyName + C.RED + ": "
|
||||
+ BukkitNativeStructureLocatePolicy.unavailableMessage());
|
||||
continue;
|
||||
}
|
||||
nativeEligible++;
|
||||
messages.add(C.GREEN + "[native-eligible] " + C.WHITE + keyName);
|
||||
}
|
||||
messages.add(C.GREEN + "Structure verify: " + C.WHITE + located + C.GREEN + " Iris placements located, "
|
||||
+ C.WHITE + nativeEligible + C.GREEN + " native structures eligible, "
|
||||
+ C.WHITE + disabled + C.GREEN + " disabled by policy, "
|
||||
+ C.WHITE + unreachable + C.GREEN + " biome-unreachable, "
|
||||
+ C.WHITE + unavailable + C.GREEN + " unavailable on this platform, "
|
||||
+ C.WHITE + searchLimited + C.GREEN + " density searches safety-limited, "
|
||||
+ C.WHITE + errors + C.GREEN + " errors. Native eligibility is checked without running blocking live locates.");
|
||||
sendVerificationMessages(commandSender, target, messages);
|
||||
}
|
||||
|
||||
sender().sendMessage(C.GREEN + "Structure verify: " + C.WHITE + found + C.GREEN + " located (" + irisPlaced + " iris-placed), "
|
||||
+ C.WHITE + unreachable + C.GREEN + " cannot generate here, "
|
||||
+ C.WHITE + missing + C.GREEN + " reachable-but-not-found within " + searchRadius + " chunks.");
|
||||
if (!cannotGenerate.isEmpty()) {
|
||||
sender().sendMessage(C.RED + "Cannot generate (required biomes absent from this pack): " + C.GRAY + String.join(", ", cannotGenerate));
|
||||
}
|
||||
if (!notFound.isEmpty()) {
|
||||
sender().sendMessage(C.YELLOW + "Reachable but not found (rarer than radius): " + C.GRAY + String.join(", ", notFound));
|
||||
private void sendVerificationMessages(VolmitSender commandSender, Player target,
|
||||
KList<String> messages) {
|
||||
Runnable send = () -> {
|
||||
for (String message : messages) {
|
||||
commandSender.sendMessage(message);
|
||||
}
|
||||
};
|
||||
if (target != null) {
|
||||
J.runEntity(target, send);
|
||||
} else {
|
||||
J.s(send);
|
||||
}
|
||||
}
|
||||
|
||||
private World resolveIrisWorld(IrisDimension dimension) {
|
||||
if (sender() != null && sender().isPlayer() && IrisToolbelt.isIrisWorld(player().getWorld())) {
|
||||
return player().getWorld();
|
||||
PlatformChunkGenerator playerGenerator = IrisToolbelt.access(player().getWorld());
|
||||
if (matchesDimension(playerGenerator, dimension.getLoadKey())) {
|
||||
return player().getWorld();
|
||||
}
|
||||
}
|
||||
World fallback = null;
|
||||
for (World w : Bukkit.getWorlds()) {
|
||||
if (!IrisToolbelt.isIrisWorld(w)) {
|
||||
continue;
|
||||
}
|
||||
if (fallback == null) {
|
||||
fallback = w;
|
||||
}
|
||||
PlatformChunkGenerator gen = IrisToolbelt.access(w);
|
||||
if (gen != null && gen.getEngine() != null && gen.getEngine().getDimension() != null
|
||||
&& dimension.getLoadKey().equals(gen.getEngine().getDimension().getLoadKey())) {
|
||||
if (matchesDimension(gen, dimension.getLoadKey())) {
|
||||
return w;
|
||||
}
|
||||
}
|
||||
return fallback;
|
||||
return null;
|
||||
}
|
||||
|
||||
static boolean matchesDimension(PlatformChunkGenerator generator, String dimensionKey) {
|
||||
return dimensionKey != null
|
||||
&& generator != null
|
||||
&& generator.getEngine() != null
|
||||
&& generator.getEngine().getDimension() != null
|
||||
&& dimensionKey.equals(generator.getEngine().getDimension().getLoadKey());
|
||||
}
|
||||
|
||||
@Director(description = "Resolve an iris structure's jigsaw graph and report piece count & bounds", origin = DirectorOrigin.BOTH)
|
||||
|
||||
+7
-5
@@ -18,6 +18,7 @@
|
||||
|
||||
package art.arcane.iris.core.commands;
|
||||
|
||||
import art.arcane.iris.platform.bukkit.BukkitWorldBinding;
|
||||
import art.arcane.iris.Iris;
|
||||
import art.arcane.iris.platform.bukkit.BukkitPlatform;
|
||||
import art.arcane.iris.core.IrisSettings;
|
||||
@@ -43,6 +44,7 @@ import art.arcane.iris.engine.object.IrisNoiseGenerator;
|
||||
import art.arcane.iris.engine.object.IrisObject;
|
||||
import art.arcane.iris.engine.object.IrisObjectPlacement;
|
||||
import art.arcane.iris.engine.object.IrisRegion;
|
||||
import art.arcane.iris.engine.object.IrisWorld;
|
||||
import art.arcane.iris.engine.object.NoiseStyle;
|
||||
import art.arcane.iris.engine.platform.EngineBukkitOps;
|
||||
import art.arcane.iris.engine.platform.PlatformChunkGenerator;
|
||||
@@ -621,14 +623,14 @@ public class CommandStudio implements DirectorExecutor {
|
||||
}
|
||||
|
||||
sender().sendMessage(C.GREEN + "Sending you to the studio world!");
|
||||
var player = player();
|
||||
BukkitPlatform.teleportAsync(player(), Iris.service(StudioSVC.class)
|
||||
Player player = player();
|
||||
IrisWorld studioWorld = Iris.service(StudioSVC.class)
|
||||
.getActiveProject()
|
||||
.getActiveProvider()
|
||||
.getTarget()
|
||||
.getWorld()
|
||||
.spawnLocation()
|
||||
).thenRun(() -> player.setGameMode(GameMode.SPECTATOR));
|
||||
.getWorld();
|
||||
BukkitPlatform.teleportAsync(player, BukkitWorldBinding.spawnLocation(studioWorld))
|
||||
.thenRun(() -> player.setGameMode(GameMode.SPECTATOR));
|
||||
}
|
||||
|
||||
@Director(description = "Update your dimension projects VSCode workspace")
|
||||
|
||||
+5
-4
@@ -18,6 +18,7 @@
|
||||
|
||||
package art.arcane.iris.core.gui;
|
||||
|
||||
import art.arcane.iris.platform.bukkit.BukkitWorldBinding;
|
||||
import art.arcane.iris.engine.IrisComplex;
|
||||
import art.arcane.iris.engine.framework.Engine;
|
||||
import art.arcane.iris.engine.framework.render.RenderType;
|
||||
@@ -47,7 +48,7 @@ public final class BukkitVisionOverlay implements GuiOverlay {
|
||||
public List<GuiMarker> players() {
|
||||
IrisWorld world = engine.getWorld();
|
||||
List<GuiMarker> markers = new ArrayList<>();
|
||||
for (Player player : world.getPlayers()) {
|
||||
for (Player player : BukkitWorldBinding.players(world)) {
|
||||
markers.add(GuiMarker.player(player.getName(), player.getLocation().getX(), player.getLocation().getZ()));
|
||||
}
|
||||
return markers;
|
||||
@@ -58,7 +59,7 @@ public final class BukkitVisionOverlay implements GuiOverlay {
|
||||
J.s(() -> {
|
||||
IrisWorld world = engine.getWorld();
|
||||
List<GuiMarker> markers = new ArrayList<>();
|
||||
for (LivingEntity entity : world.getEntitiesByClass(LivingEntity.class)) {
|
||||
for (LivingEntity entity : BukkitWorldBinding.entities(world, LivingEntity.class)) {
|
||||
if (entity instanceof Player) {
|
||||
continue;
|
||||
}
|
||||
@@ -78,11 +79,11 @@ public final class BukkitVisionOverlay implements GuiOverlay {
|
||||
@Override
|
||||
public void teleport(double worldX, double worldZ) {
|
||||
IrisWorld world = engine.getWorld();
|
||||
if (!world.hasRealWorld()) {
|
||||
if (!world.hasPlatformWorld()) {
|
||||
return;
|
||||
}
|
||||
J.s(() -> {
|
||||
List<Player> players = world.getPlayers();
|
||||
List<Player> players = BukkitWorldBinding.players(world);
|
||||
if (players.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
+122
@@ -0,0 +1,122 @@
|
||||
/*
|
||||
* 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.runtime;
|
||||
|
||||
import art.arcane.iris.core.ServerConfigurator;
|
||||
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.tools.IrisToolbelt;
|
||||
import art.arcane.iris.core.tools.WorldMaintenance;
|
||||
import art.arcane.iris.engine.framework.Engine;
|
||||
import art.arcane.iris.engine.framework.EngineMode;
|
||||
import art.arcane.iris.engine.framework.EnginePlatformHooks;
|
||||
import art.arcane.iris.engine.object.IrisDimension;
|
||||
import art.arcane.iris.engine.object.IrisDimensionRuntimeContract;
|
||||
import art.arcane.iris.engine.object.IrisWorld;
|
||||
import art.arcane.iris.platform.bukkit.BukkitWorldBinding;
|
||||
import art.arcane.iris.spi.IrisPlatforms;
|
||||
import art.arcane.iris.util.common.scheduling.J;
|
||||
import org.bukkit.World;
|
||||
|
||||
public final class BukkitEnginePlatformHooks implements EnginePlatformHooks {
|
||||
@Override
|
||||
public void refreshWorkspace(Engine engine) {
|
||||
new IrisProject(engine.getData().getDataFolder()).updateWorkspace();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void refreshDatapackWorkspace(Engine engine) {
|
||||
DatapackIngestService.refreshWorkspace(engine.getData());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void reloadDatapacks(Engine engine) {
|
||||
synchronized (ServerConfigurator.class) {
|
||||
ServerConfigurator.installDataPacks(false);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void fireHotloadEvent(Engine engine) {
|
||||
IrisPlatforms.get().callEvent(new IrisEngineHotloadEvent(engine));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void validateDimensionHotload(Engine engine, IrisDimension replacement) {
|
||||
IrisDimensionRuntimeContract.requireHotloadCompatible(
|
||||
"Bukkit Studio world '" + engine.getWorld().name() + "'",
|
||||
engine.getDimension(),
|
||||
replacement,
|
||||
"iris");
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isPregeneratorActive(Engine engine) {
|
||||
IrisWorld world = engine.getWorld();
|
||||
if (world == null) {
|
||||
return false;
|
||||
}
|
||||
PregeneratorJob pregeneratorJob = PregeneratorJob.getInstance();
|
||||
return pregeneratorJob != null && pregeneratorJob.targetsWorldIdentity(world.identity());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void shutdownPregenerator(Engine engine) {
|
||||
PregeneratorJob.shutdownInstance();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean shouldDisableChunkContextCache(Engine engine) {
|
||||
IrisWorld world = engine.getWorld();
|
||||
if (!J.isFolia() || world == null || !world.hasPlatformWorld()) {
|
||||
return false;
|
||||
}
|
||||
boolean maintenanceActive = WorldMaintenance.isWorldMaintenanceActive(world.identity());
|
||||
return EngineMode.shouldDisableContextCacheForMaintenance(maintenanceActive, isPregeneratorActive(engine));
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean shouldSkipMantleCleanup(Engine engine) {
|
||||
IrisWorld world = engine.getWorld();
|
||||
return world != null
|
||||
&& WorldMaintenance.isWorldMaintenanceActive(world.identity())
|
||||
&& !isPregeneratorActive(engine);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean shouldSkipMantleMarkerRead(Engine engine, int chunkX, int chunkZ) {
|
||||
IrisWorld irisWorld = engine.getWorld();
|
||||
if (!J.isFolia() || irisWorld == null || !irisWorld.hasPlatformWorld()) {
|
||||
return false;
|
||||
}
|
||||
World world = BukkitWorldBinding.world(irisWorld);
|
||||
return world != null && J.isOwnedByCurrentRegion(world, chunkX, chunkZ);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean shouldBypassMantleStages(Engine engine) {
|
||||
if (!J.isFolia() || !engine.getWorld().hasPlatformWorld()) {
|
||||
return false;
|
||||
}
|
||||
World world = BukkitWorldBinding.world(engine.getWorld());
|
||||
return world != null && IrisToolbelt.isWorldMaintenanceBypassingMantleStages(world);
|
||||
}
|
||||
}
|
||||
+5
-3
@@ -1,5 +1,6 @@
|
||||
package art.arcane.iris.core.service;
|
||||
|
||||
import art.arcane.iris.platform.bukkit.BukkitWorldBinding;
|
||||
import com.google.common.util.concurrent.AtomicDouble;
|
||||
import art.arcane.iris.spi.IrisLogging;
|
||||
import art.arcane.iris.spi.IrisServices;
|
||||
@@ -9,6 +10,7 @@ import art.arcane.iris.core.runtime.GoldenHashScanner;
|
||||
import art.arcane.iris.core.tools.IrisToolbelt;
|
||||
import art.arcane.iris.engine.framework.Engine;
|
||||
import art.arcane.iris.engine.platform.PlatformChunkGenerator;
|
||||
import art.arcane.volmlib.util.bukkit.WorldIdentity;
|
||||
import art.arcane.volmlib.util.collection.KMap;
|
||||
import art.arcane.iris.util.common.format.C;
|
||||
import art.arcane.volmlib.util.format.Form;
|
||||
@@ -312,7 +314,7 @@ public class IrisEngineSVC implements IrisService {
|
||||
|| engine.getMantle().getMantle().isClosed()
|
||||
|| !shouldReduce(engine))
|
||||
return;
|
||||
World engineWorld = engine.getWorld().realWorld();
|
||||
World engineWorld = BukkitWorldBinding.world(engine.getWorld());
|
||||
if (shouldSkipForMaintenance(engineWorld)) {
|
||||
return;
|
||||
}
|
||||
@@ -346,7 +348,7 @@ public class IrisEngineSVC implements IrisService {
|
||||
|| engine.getMantle().getMantle().isClosed()
|
||||
|| !shouldReduce(engine))
|
||||
return;
|
||||
World engineWorld = engine.getWorld().realWorld();
|
||||
World engineWorld = BukkitWorldBinding.world(engine.getWorld());
|
||||
if (shouldSkipForMaintenance(engineWorld)) {
|
||||
return;
|
||||
}
|
||||
@@ -446,7 +448,7 @@ public class IrisEngineSVC implements IrisService {
|
||||
}
|
||||
|
||||
PregeneratorJob pregeneratorJob = PregeneratorJob.getInstance();
|
||||
boolean pregeneratorTargetsWorld = pregeneratorJob != null && pregeneratorJob.targetsWorld(world);
|
||||
boolean pregeneratorTargetsWorld = pregeneratorJob != null && pregeneratorJob.targetsWorldIdentity(WorldIdentity.serialize(world));
|
||||
return shouldSkipMantleReductionForMaintenance(maintenanceActive, pregeneratorTargetsWorld);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,3 +8,65 @@ load: STARTUP
|
||||
authors: [ cyberpwn, NextdoorPsycho, Vatuu ]
|
||||
website: volmit.com
|
||||
description: More than a Dimension!
|
||||
dependencies:
|
||||
server:
|
||||
PlaceholderAPI:
|
||||
load: BEFORE
|
||||
required: false
|
||||
join-classpath: true
|
||||
CraftEngine:
|
||||
load: BEFORE
|
||||
required: false
|
||||
join-classpath: true
|
||||
Nexo:
|
||||
load: BEFORE
|
||||
required: false
|
||||
join-classpath: true
|
||||
ItemsAdder:
|
||||
load: BEFORE
|
||||
required: false
|
||||
join-classpath: true
|
||||
SCore:
|
||||
load: BEFORE
|
||||
required: false
|
||||
join-classpath: true
|
||||
ExecutableItems:
|
||||
load: BEFORE
|
||||
required: false
|
||||
join-classpath: false
|
||||
MythicLib:
|
||||
load: BEFORE
|
||||
required: false
|
||||
join-classpath: true
|
||||
MMOItems:
|
||||
load: BEFORE
|
||||
required: false
|
||||
join-classpath: true
|
||||
eco:
|
||||
load: BEFORE
|
||||
required: false
|
||||
join-classpath: true
|
||||
EcoItems:
|
||||
load: BEFORE
|
||||
required: false
|
||||
join-classpath: true
|
||||
MythicMobs:
|
||||
load: BEFORE
|
||||
required: false
|
||||
join-classpath: true
|
||||
MythicCrucible:
|
||||
load: BEFORE
|
||||
required: false
|
||||
join-classpath: true
|
||||
KGenerators:
|
||||
load: BEFORE
|
||||
required: false
|
||||
join-classpath: true
|
||||
Multiverse-Core:
|
||||
load: AFTER
|
||||
required: false
|
||||
join-classpath: true
|
||||
WorldEdit:
|
||||
load: BEFORE
|
||||
required: false
|
||||
join-classpath: true
|
||||
@@ -3,6 +3,22 @@ version: ${version}
|
||||
main: ${main}
|
||||
folia-supported: true
|
||||
load: STARTUP
|
||||
softdepend:
|
||||
- PlaceholderAPI
|
||||
- CraftEngine
|
||||
- Nexo
|
||||
- ItemsAdder
|
||||
- SCore
|
||||
- ExecutableItems
|
||||
- MythicLib
|
||||
- MMOItems
|
||||
- eco
|
||||
- EcoItems
|
||||
- MythicMobs
|
||||
- MythicCrucible
|
||||
- KGenerators
|
||||
- WorldEdit
|
||||
loadbefore: [ Multiverse-Core ]
|
||||
authors: [ cyberpwn, NextdoorPsycho, Vatuu ]
|
||||
website: volmit.com
|
||||
description: More than a Dimension!
|
||||
|
||||
@@ -1,199 +0,0 @@
|
||||
package art.arcane.iris;
|
||||
|
||||
import io.papermc.paper.datapack.Datapack;
|
||||
import io.papermc.paper.datapack.DatapackRegistrar;
|
||||
import io.papermc.paper.datapack.DatapackSource;
|
||||
import io.papermc.paper.datapack.DiscoveredDatapack;
|
||||
import io.papermc.paper.plugin.configuration.PluginMeta;
|
||||
import net.kyori.adventure.text.Component;
|
||||
import org.bukkit.FeatureFlag;
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.junit.rules.TemporaryFolder;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.net.URI;
|
||||
import java.nio.file.Path;
|
||||
import java.util.Collections;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertSame;
|
||||
import static org.junit.Assert.assertThrows;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
public class IrisBootstrapTest {
|
||||
@Rule
|
||||
public TemporaryFolder temporaryFolder = new TemporaryFolder();
|
||||
|
||||
@Test
|
||||
public void discoverPackAutoEnablesAtFixedTop() throws Exception {
|
||||
File datapackDirectory = temporaryFolder.newFolder("datapack");
|
||||
RecordingRegistrar registrar = new RecordingRegistrar(DiscoveryBehavior.ACCEPT);
|
||||
|
||||
DiscoveredDatapack discovered = IrisBootstrap.discoverPack(registrar, datapackDirectory.toPath());
|
||||
|
||||
assertSame(registrar.discoveredDatapack, discovered);
|
||||
assertEquals(datapackDirectory.toPath(), registrar.discoveredPath);
|
||||
assertEquals(IrisBootstrap.PACK_ID, registrar.discoveredId);
|
||||
assertTrue(registrar.configurer.autoEnableOnServerStart);
|
||||
assertTrue(registrar.configurer.fixedPosition);
|
||||
assertEquals(Datapack.Position.TOP, registrar.configurer.position);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void rejectedDiscoveryReportsDatapackPath() throws Exception {
|
||||
File datapackDirectory = temporaryFolder.newFolder("datapack");
|
||||
RecordingRegistrar registrar = new RecordingRegistrar(DiscoveryBehavior.REJECT);
|
||||
|
||||
IllegalStateException failure = assertThrows(
|
||||
IllegalStateException.class,
|
||||
() -> IrisBootstrap.discoverPack(registrar, datapackDirectory.toPath())
|
||||
);
|
||||
|
||||
assertTrue(failure.getMessage().contains(datapackDirectory.toPath().toString()));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void discoveryIoFailurePropagates() throws Exception {
|
||||
File datapackDirectory = temporaryFolder.newFolder("datapack");
|
||||
RecordingRegistrar registrar = new RecordingRegistrar(DiscoveryBehavior.FAIL);
|
||||
|
||||
IOException failure = assertThrows(
|
||||
IOException.class,
|
||||
() -> IrisBootstrap.discoverPack(registrar, datapackDirectory.toPath())
|
||||
);
|
||||
|
||||
assertEquals("discovery failed", failure.getMessage());
|
||||
}
|
||||
|
||||
private enum DiscoveryBehavior {
|
||||
ACCEPT,
|
||||
REJECT,
|
||||
FAIL
|
||||
}
|
||||
|
||||
private static final class RecordingConfigurer implements DatapackRegistrar.Configurer {
|
||||
private boolean autoEnableOnServerStart;
|
||||
private boolean fixedPosition;
|
||||
private Datapack.Position position;
|
||||
|
||||
@Override
|
||||
public DatapackRegistrar.Configurer title(Component title) {
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public DatapackRegistrar.Configurer autoEnableOnServerStart(boolean autoEnableOnServerStart) {
|
||||
this.autoEnableOnServerStart = autoEnableOnServerStart;
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public DatapackRegistrar.Configurer position(boolean fixed, Datapack.Position position) {
|
||||
this.fixedPosition = fixed;
|
||||
this.position = position;
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
private static final class RecordingRegistrar implements DatapackRegistrar {
|
||||
private final DiscoveryBehavior behavior;
|
||||
private final RecordingConfigurer configurer;
|
||||
private final DiscoveredDatapack discoveredDatapack;
|
||||
private Path discoveredPath;
|
||||
private String discoveredId;
|
||||
|
||||
private RecordingRegistrar(DiscoveryBehavior behavior) {
|
||||
this.behavior = behavior;
|
||||
this.configurer = new RecordingConfigurer();
|
||||
this.discoveredDatapack = new StubDiscoveredDatapack();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasPackDiscovered(String name) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public DiscoveredDatapack getDiscoveredPack(String name) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean removeDiscoveredPack(String name) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, DiscoveredDatapack> getDiscoveredPacks() {
|
||||
return Collections.emptyMap();
|
||||
}
|
||||
|
||||
@Override
|
||||
public DiscoveredDatapack discoverPack(URI uri, String id, Consumer<Configurer> configurer) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public DiscoveredDatapack discoverPack(Path path, String id, Consumer<Configurer> configurer) throws IOException {
|
||||
this.discoveredPath = path;
|
||||
this.discoveredId = id;
|
||||
if (behavior == DiscoveryBehavior.FAIL) {
|
||||
throw new IOException("discovery failed");
|
||||
}
|
||||
configurer.accept(this.configurer);
|
||||
return behavior == DiscoveryBehavior.ACCEPT ? discoveredDatapack : null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public DiscoveredDatapack discoverPack(PluginMeta pluginMeta, URI uri, String id, Consumer<Configurer> configurer) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public DiscoveredDatapack discoverPack(PluginMeta pluginMeta, Path path, String id, Consumer<Configurer> configurer) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
}
|
||||
|
||||
private static final class StubDiscoveredDatapack implements DiscoveredDatapack {
|
||||
@Override
|
||||
public String getName() {
|
||||
return IrisBootstrap.PACK_ID;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Component getTitle() {
|
||||
return Component.text(IrisBootstrap.PACK_ID);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Component getDescription() {
|
||||
return Component.empty();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isRequired() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Datapack.Compatibility getCompatibility() {
|
||||
return Datapack.Compatibility.COMPATIBLE;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<FeatureFlag> getRequiredFeatures() {
|
||||
return Collections.emptySet();
|
||||
}
|
||||
|
||||
@Override
|
||||
public DatapackSource getSource() {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,8 @@
|
||||
package art.arcane.iris;
|
||||
|
||||
import art.arcane.iris.core.splash.IrisSplashPackScanner;
|
||||
import art.arcane.iris.core.service.CommandSVC;
|
||||
import art.arcane.iris.util.common.plugin.IrisService;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
@@ -13,9 +15,18 @@ import java.util.Comparator;
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
public class IrisDiagnosticsTest {
|
||||
@Test
|
||||
public void serviceInitializationSkipsPackageHelperClasses() throws Exception {
|
||||
Class<?> registrar = Class.forName("art.arcane.iris.core.service.PaperCommandRegistrar");
|
||||
|
||||
assertFalse(Iris.isConcreteImplementation(registrar, IrisService.class));
|
||||
assertTrue(Iris.isConcreteImplementation(CommandSVC.class, IrisService.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void reportErrorWithContextPrintsFullStacktrace() {
|
||||
ByteArrayOutputStream output = new ByteArrayOutputStream();
|
||||
|
||||
@@ -18,6 +18,30 @@ import static org.junit.Assert.assertNotNull;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
public class PaperPluginMetadataTest {
|
||||
private static final List<String> OPTIONAL_PLUGIN_IDS = List.of(
|
||||
"PlaceholderAPI",
|
||||
"CraftEngine",
|
||||
"Nexo",
|
||||
"ItemsAdder",
|
||||
"SCore",
|
||||
"ExecutableItems",
|
||||
"MythicLib",
|
||||
"MMOItems",
|
||||
"eco",
|
||||
"EcoItems",
|
||||
"MythicMobs",
|
||||
"MythicCrucible",
|
||||
"KGenerators",
|
||||
"Multiverse-Core",
|
||||
"WorldEdit"
|
||||
);
|
||||
private static final List<String> JOINED_PLUGIN_IDS = OPTIONAL_PLUGIN_IDS.stream()
|
||||
.filter(pluginId -> !"ExecutableItems".equals(pluginId) && !"Multiverse-Core".equals(pluginId))
|
||||
.toList();
|
||||
private static final List<String> BUKKIT_SOFT_DEPEND_IDS = OPTIONAL_PLUGIN_IDS.stream()
|
||||
.filter(pluginId -> !"Multiverse-Core".equals(pluginId))
|
||||
.toList();
|
||||
|
||||
@Test
|
||||
public void paperMetadataDeclaresBootstrapAndFoliaSupport() throws Exception {
|
||||
String metadata;
|
||||
@@ -30,6 +54,11 @@ public class PaperPluginMetadataTest {
|
||||
assertTrue(metadata.contains("folia-supported: true"));
|
||||
assertTrue(metadata.contains("load: STARTUP"));
|
||||
assertFalse(metadata.contains("commands:"));
|
||||
for (String pluginId : JOINED_PLUGIN_IDS) {
|
||||
assertTrue(metadata.contains(optionalDependencyBlock(pluginId, "BEFORE", true)));
|
||||
}
|
||||
assertTrue(metadata.contains(optionalDependencyBlock("ExecutableItems", "BEFORE", false)));
|
||||
assertTrue(metadata.contains(optionalDependencyBlock("Multiverse-Core", "AFTER", true)));
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -45,6 +74,8 @@ public class PaperPluginMetadataTest {
|
||||
Map<String, Map<String, Object>> commands = metadata.getCommands();
|
||||
assertTrue(commands.containsKey("iris"));
|
||||
assertEquals(List.of("ir", "irs"), commands.get("iris").get("aliases"));
|
||||
assertEquals(BUKKIT_SOFT_DEPEND_IDS, metadata.getSoftDepend());
|
||||
assertEquals(List.of("Multiverse-Core"), metadata.getLoadBeforePlugins());
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -56,4 +87,11 @@ public class PaperPluginMetadataTest {
|
||||
assertTrue(Files.isRegularFile(bukkitMetadata));
|
||||
assertFalse(Files.readString(bukkitMetadata, StandardCharsets.UTF_8).contains("${"));
|
||||
}
|
||||
|
||||
private static String optionalDependencyBlock(String pluginId, String loadOrder, boolean joinClasspath) {
|
||||
return " " + pluginId + ":\n"
|
||||
+ " load: " + loadOrder + "\n"
|
||||
+ " required: false\n"
|
||||
+ " join-classpath: " + joinClasspath + "\n";
|
||||
}
|
||||
}
|
||||
+32
@@ -0,0 +1,32 @@
|
||||
package art.arcane.iris.core.commands;
|
||||
|
||||
import org.junit.Test;
|
||||
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
public class BukkitNativeStructureLocatePolicyTest {
|
||||
@Test
|
||||
public void nativeMonumentLocateIsUnavailable() {
|
||||
assertTrue(BukkitNativeStructureLocatePolicy.isUnavailable("minecraft:monument"));
|
||||
assertTrue(BukkitNativeStructureLocatePolicy.isUnavailable(" MINECRAFT:MONUMENT "));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void unrelatedAndNonCanonicalKeysRemainAvailable() {
|
||||
assertFalse(BukkitNativeStructureLocatePolicy.isUnavailable(null));
|
||||
assertFalse(BukkitNativeStructureLocatePolicy.isUnavailable(""));
|
||||
assertFalse(BukkitNativeStructureLocatePolicy.isUnavailable("minecraft:stronghold"));
|
||||
assertFalse(BukkitNativeStructureLocatePolicy.isUnavailable("minecraft:ocean_monument"));
|
||||
assertFalse(BukkitNativeStructureLocatePolicy.isUnavailable("minecraft:ocean_monuments"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void rejectionExplainsSafetyAndGenerationBehavior() {
|
||||
String message = BukkitNativeStructureLocatePolicy.unavailableMessage().toLowerCase();
|
||||
|
||||
assertTrue(message.contains("unavailable"));
|
||||
assertTrue(message.contains("stall the server thread"));
|
||||
assertTrue(message.contains("generation is unaffected"));
|
||||
}
|
||||
}
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
package art.arcane.iris.core.commands;
|
||||
|
||||
import org.junit.Test;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
public class IrisStructureLocateCommandContractTest {
|
||||
@Test
|
||||
public void findReportsDensityLimitAndUsesExactLocatedOrigin() throws IOException {
|
||||
String source = Files.readString(Path.of(System.getProperty("iris.commandFindSource")));
|
||||
|
||||
assertTrue(source.contains("LocateStatus.SEARCH_LIMIT_REACHED"));
|
||||
assertTrue(source.contains("the density search safety limit was reached"));
|
||||
assertTrue(source.contains("result.originX(), result.baseY(), result.originZ()"));
|
||||
assertFalse(source.contains("at[0] + 8"));
|
||||
assertFalse(source.contains("at[2] + 8"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void structureVerifyReportsDensityLimitAndUsesExactLocatedOrigin() throws IOException {
|
||||
String source = Files.readString(Path.of(System.getProperty("iris.commandStructureSource")));
|
||||
|
||||
assertTrue(source.contains("[iris-search-limit]"));
|
||||
assertTrue(source.contains("density searches safety-limited"));
|
||||
assertTrue(source.contains("result.originX() + \",\" + result.baseY() + \",\" + result.originZ()"));
|
||||
assertFalse(source.contains("at[0] + 8"));
|
||||
assertFalse(source.contains("at[2] + 8"));
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user