This commit is contained in:
Brian Neumann-Fopiano committed 2026-07-13 21:33:20 -04:00
1 parent 4dea984d77
commit 45230c0689
234 files changed
+13818 -2979

No files matched your search

+2 -2
View File
@@ -18,8 +18,8 @@ Consider supporting development by buying Iris on Spigot.
| Folia | plugin jar | 26.2 | Region-safe scheduling throughout | | Folia | plugin jar | 26.2 | Region-safe scheduling throughout |
| Spigot / CraftBukkit | plugin jar | 26.2 | Full feature set | | Spigot / CraftBukkit | plugin jar | 26.2 | Full feature set |
| Fabric | mod jar | 26.2 | Server worldgen + client HUD; requires Fabric Loader 0.19.3+ | | Fabric | mod jar | 26.2 | Server worldgen + client HUD; requires Fabric Loader 0.19.3+ |
| Forge | mod jar | 26.2 | Server worldgen + client HUD; requires Forge 65.0.0+ | | Forge | mod jar | 26.2 | Server worldgen + client HUD; requires Forge 65.0.4+ |
| NeoForge | mod jar | 26.2 | Server worldgen + client HUD; requires NeoForge 26.2+ | | NeoForge | mod jar | 26.2 | Server worldgen + client HUD; requires NeoForge 26.2.0.12-beta+ |
Java 25 is required on every platform. Java 25 is required on every platform.
@@ -1,14 +1,17 @@
package art.arcane.iris.core.nms.v26_2_R1; package art.arcane.iris.core.nms.v26_2_R1;
import art.arcane.iris.platform.bukkit.BukkitWorldBinding;
import com.mojang.serialization.MapCodec; import com.mojang.serialization.MapCodec;
import art.arcane.iris.spi.IrisLogging; import art.arcane.iris.spi.IrisLogging;
import art.arcane.iris.engine.data.cache.AtomicCache; import art.arcane.iris.engine.data.cache.AtomicCache;
import art.arcane.iris.engine.framework.Engine; import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.object.IrisBiome; import art.arcane.iris.engine.object.IrisBiome;
import art.arcane.iris.engine.object.IrisBiomeCustom; 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.collection.KMap;
import art.arcane.volmlib.util.math.RNG; import art.arcane.volmlib.util.math.RNG;
import net.minecraft.core.Holder; import net.minecraft.core.Holder;
import net.minecraft.core.QuartPos;
import net.minecraft.core.Registry; import net.minecraft.core.Registry;
import net.minecraft.core.RegistryAccess; import net.minecraft.core.RegistryAccess;
import net.minecraft.core.registries.Registries; import net.minecraft.core.registries.Registries;
@@ -26,12 +29,14 @@ import java.lang.reflect.Field;
import java.lang.reflect.Method; import java.lang.reflect.Method;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections; import java.util.Collections;
import java.util.HashSet;
import java.util.IdentityHashMap; import java.util.IdentityHashMap;
import java.util.Map; import java.util.Map;
import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentHashMap;
import java.util.LinkedHashSet; import java.util.LinkedHashSet;
import java.util.List; import java.util.List;
import java.util.Optional; import java.util.Optional;
import java.util.Set;
import java.util.stream.Stream; import java.util.stream.Stream;
public class CustomBiomeSource extends BiomeSource { public class CustomBiomeSource extends BiomeSource {
@@ -42,11 +47,13 @@ public class CustomBiomeSource extends BiomeSource {
private final Registry<Biome> biomeCustomRegistry; private final Registry<Biome> biomeCustomRegistry;
private final Registry<Biome> biomeRegistry; private final Registry<Biome> biomeRegistry;
private final AtomicCache<RegistryAccess> registryAccess = new AtomicCache<>(); 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 Holder<Biome> fallbackBiome;
private final ConcurrentHashMap<Long, Holder<Biome>> noiseBiomeCache = new ConcurrentHashMap<>(); private final ConcurrentHashMap<Long, Holder<Biome>> noiseBiomeCache = new ConcurrentHashMap<>();
private final ConcurrentHashMap<Long, Holder<Biome>> structureBiomeCache = 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) { public CustomBiomeSource(long seed, Engine engine, World world) {
this.engine = engine; this.engine = engine;
@@ -56,20 +63,19 @@ public class CustomBiomeSource extends BiomeSource {
this.fallbackBiome = resolveFallbackBiome(this.biomeRegistry, this.biomeCustomRegistry); this.fallbackBiome = resolveFallbackBiome(this.biomeRegistry, this.biomeCustomRegistry);
this.customBiomes = fillCustomBiomes(this.biomeCustomRegistry, engine, this.fallbackBiome); this.customBiomes = fillCustomBiomes(this.biomeCustomRegistry, engine, this.fallbackBiome);
this.vanillaSpawnBiomes = fillVanillaSpawnBiomes(this.biomeCustomRegistry, this.biomeRegistry, engine); 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) { private static List<Holder<Biome>> getAllBiomes(Registry<Biome> customRegistry, Registry<Biome> registry, Engine engine, Holder<Biome> fallback) {
LinkedHashSet<Holder<Biome>> biomes = new LinkedHashSet<>(); LinkedHashSet<Holder<Biome>> biomes = new LinkedHashSet<>();
if (fallback != null) { boolean resolutionFailed = false;
biomes.add(fallback);
}
for (IrisBiome i : engine.getAllBiomes()) { for (IrisBiome i : engine.getAllBiomes()) {
Holder<Biome> vanillaHolder = NMSBinding.biomeToBiomeBase(registry, i.getVanillaDerivative()); Holder<Biome> vanillaHolder = NMSBinding.biomeToBiomeBase(registry, i.getVanillaDerivative());
if (vanillaHolder != null) { if (vanillaHolder != null) {
biomes.add(vanillaHolder); biomes.add(vanillaHolder);
} else if (!i.isCustom() && fallback != null) { } else if (!i.isCustom()) {
biomes.add(fallback); resolutionFailed = true;
} }
if (i.isCustom()) { if (i.isCustom()) {
@@ -77,13 +83,17 @@ public class CustomBiomeSource extends BiomeSource {
Holder<Biome> customHolder = resolveCustomBiomeHolder(customRegistry, engine, j.getId()); Holder<Biome> customHolder = resolveCustomBiomeHolder(customRegistry, engine, j.getId());
if (customHolder != null) { if (customHolder != null) {
biomes.add(customHolder); biomes.add(customHolder);
} else if (fallback != null) { } else {
biomes.add(fallback); resolutionFailed = true;
} }
} }
} }
} }
if ((resolutionFailed || biomes.isEmpty()) && fallback != null) {
biomes.add(fallback);
}
return new ArrayList<>(biomes); return new ArrayList<>(biomes);
} }
@@ -135,12 +145,21 @@ public class CustomBiomeSource extends BiomeSource {
@Override @Override
protected Stream<Holder<Biome>> collectPossibleBiomes() { protected Stream<Holder<Biome>> collectPossibleBiomes() {
return getAllBiomes( return possibleStructureBiomes().stream();
((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), Set<Holder<Biome>> possibleStructureBiomes() {
engine, ensureCachesCurrent();
fallbackBiome).stream(); 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) { 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) { Holder<Biome> getVanillaSpawnBiome(Holder<Biome> biome) {
ensureCachesCurrent();
if (biome == null) { if (biome == null) {
return null; return null;
} }
@@ -211,6 +231,11 @@ public class CustomBiomeSource extends BiomeSource {
@Override @Override
public Holder<Biome> getNoiseBiome(int x, int y, int z, Climate.Sampler sampler) { 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); long cacheKey = packNoiseKey(x, y, z);
Holder<Biome> cachedHolder = structureBiomeCache.get(cacheKey); Holder<Biome> cachedHolder = structureBiomeCache.get(cacheKey);
if (cachedHolder != null) { if (cachedHolder != null) {
@@ -230,7 +255,69 @@ public class CustomBiomeSource extends BiomeSource {
return resolvedHolder; 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) { public Holder<Biome> getVisibleNoiseBiome(int x, int y, int z, Climate.Sampler sampler) {
ensureCachesCurrent();
long cacheKey = packNoiseKey(x, y, z); long cacheKey = packNoiseKey(x, y, z);
Holder<Biome> cachedHolder = noiseBiomeCache.get(cacheKey); Holder<Biome> cachedHolder = noiseBiomeCache.get(cacheKey);
if (cachedHolder != null) { if (cachedHolder != null) {
@@ -250,6 +337,28 @@ public class CustomBiomeSource extends BiomeSource {
return resolvedHolder; 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) { private Holder<Biome> resolveStructureBiomeHolder(int x, int y, int z) {
BiomeResolution resolution = resolveBiomeResolution(x, y, z); BiomeResolution resolution = resolveBiomeResolution(x, y, z);
if (resolution == null) { if (resolution == null) {
@@ -315,14 +424,19 @@ public class CustomBiomeSource extends BiomeSource {
int blockY = y << 2; int blockY = y << 2;
int worldMinHeight = engine.getWorld().minHeight(); int worldMinHeight = engine.getWorld().minHeight();
int internalY = blockY - worldMinHeight; int internalY = blockY - worldMinHeight;
int surfaceInternalY = engine.getComplex().getHeightStream().get(blockX, blockZ).intValue();
int caveSwitchInternalY = Math.max(-8 - worldMinHeight, 40); int caveSwitchInternalY = Math.max(-8 - worldMinHeight, 40);
boolean deepUnderground = internalY <= caveSwitchInternalY; boolean deepUnderground = internalY <= caveSwitchInternalY;
boolean belowSurface = internalY <= surfaceInternalY - 8; boolean underground = false;
boolean underground = deepUnderground && belowSurface; IrisBiome irisBiome;
IrisBiome irisBiome = underground if (deepUnderground) {
? engine.getCaveBiome(blockX, internalY, blockZ) int surfaceInternalY = engine.getComplex().getHeightStream().get(blockX, blockZ).intValue();
: engine.getComplex().getTrueBiomeStream().get(blockX, blockZ); 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) { if (irisBiome == null && underground) {
irisBiome = engine.getComplex().getTrueBiomeStream().get(blockX, blockZ); irisBiome = engine.getComplex().getTrueBiomeStream().get(blockX, blockZ);
} }
@@ -357,6 +471,10 @@ public class CustomBiomeSource extends BiomeSource {
| ((long) y & 4095L); | ((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) { private static Holder<Biome> resolveCustomBiomeHolder(Registry<Biome> customRegistry, Engine engine, String customBiomeId) {
if (customRegistry == null || engine == null || customBiomeId == null || customBiomeId.isBlank()) { if (customRegistry == null || engine == null || customBiomeId == null || customBiomeId.isBlank()) {
return null; return null;
@@ -1,5 +1,20 @@
package art.arcane.iris.core.nms.v26_2_R1; 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.BlockPos;
import net.minecraft.core.Holder; import net.minecraft.core.Holder;
import net.minecraft.core.HolderLookup; import net.minecraft.core.HolderLookup;
@@ -7,6 +22,13 @@ import net.minecraft.core.HolderSet;
import net.minecraft.core.Registry; import net.minecraft.core.Registry;
import net.minecraft.core.RegistryAccess; import net.minecraft.core.RegistryAccess;
import net.minecraft.core.SectionPos; 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.ChunkPos;
import net.minecraft.world.level.Level; import net.minecraft.world.level.Level;
import net.minecraft.world.level.LevelHeightAccessor; 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.RandomSupport;
import net.minecraft.world.level.levelgen.WorldgenRandom; import net.minecraft.world.level.levelgen.WorldgenRandom;
import net.minecraft.world.level.levelgen.XoroshiroRandomSource; 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.EntityType;
import net.minecraft.world.entity.MobCategory; import net.minecraft.world.entity.MobCategory;
import net.minecraft.world.level.block.Blocks; 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.ChunkGeneratorStructureState;
import net.minecraft.world.level.chunk.status.ChunkStatus; import net.minecraft.world.level.chunk.status.ChunkStatus;
import net.minecraft.world.level.levelgen.blending.Blender; 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.Structure;
import net.minecraft.world.level.levelgen.structure.StructureSet; import net.minecraft.world.level.levelgen.structure.StructureSet;
import net.minecraft.world.level.levelgen.structure.StructureStart; 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.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.World;
import org.bukkit.craftbukkit.CraftWorld; import org.bukkit.craftbukkit.CraftWorld;
import org.bukkit.craftbukkit.block.data.CraftBlockData;
import org.bukkit.craftbukkit.generator.CustomChunkGenerator; import org.bukkit.craftbukkit.generator.CustomChunkGenerator;
import org.bukkit.block.data.BlockData;
import org.spigotmc.SpigotWorldConfig; import org.spigotmc.SpigotWorldConfig;
import javax.annotation.Nullable; import javax.annotation.Nullable;
import java.lang.reflect.Field; import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method; 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.CompletableFuture;
import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentHashMap;
public class IrisChunkGenerator extends CustomChunkGenerator { 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 WrappedField<ChunkGenerator, BiomeSource> BIOME_SOURCE;
private static final WrappedReturningMethod<Heightmap, Object> SET_HEIGHT; private static final WrappedReturningMethod<Heightmap, Object> SET_HEIGHT;
private final ChunkGenerator delegate; private final ChunkGenerator delegate;
private final Engine engine; private final Engine engine;
private final CustomBiomeSource customBiomeSource; private final CustomBiomeSource customBiomeSource;
private final int runtimeMinY;
private final int runtimeHeight;
private final ConcurrentHashMap<SpawnTableKey, WeightedList<MobSpawnSettings.SpawnerData>> mergedSpawnTables = new ConcurrentHashMap<>(); 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) { public IrisChunkGenerator(ChunkGenerator delegate, long seed, Engine engine, World world) {
this(delegate, engine, world, new CustomBiomeSource(seed, engine, world)); this(delegate, engine, world, new CustomBiomeSource(seed, engine, world));
@@ -91,6 +102,9 @@ public class IrisChunkGenerator extends CustomChunkGenerator {
this.delegate = delegate; this.delegate = delegate;
this.engine = engine; this.engine = engine;
this.customBiomeSource = customBiomeSource; this.customBiomeSource = customBiomeSource;
ServerLevel level = ((CraftWorld) world).getHandle();
this.runtimeMinY = level.getMinY();
this.runtimeHeight = level.getHeight();
} }
@Override @Override
@@ -105,16 +119,20 @@ public class IrisChunkGenerator extends CustomChunkGenerator {
if (id == null) { if (id == null) {
continue; continue;
} }
int[] at = IrisStructureLocator.locate(engine, id.toString(), pos.getX(), pos.getZ(), Math.max(1, radius)); IrisStructureLocator.LocateResult result = IrisStructureLocator.locate(
if (at == null) { engine, id.toString(), pos.getX(), pos.getZ(), Math.max(1, radius));
if (result.status() == IrisStructureLocator.LocateStatus.SEARCH_LIMIT_REACHED) {
continue; continue;
} }
long dx = (long) at[0] - pos.getX(); if (!result.found()) {
long dz = (long) at[2] - pos.getZ(); continue;
}
long dx = (long) result.originX() - pos.getX();
long dz = (long) result.originZ() - pos.getZ();
long d = dx * dx + dz * dz; long d = dx * dx + dz * dz;
if (d < bestDist) { if (d < bestDist) {
bestDist = d; bestDist = d;
best = new BlockPos(at[0], at[1], at[2]); best = new BlockPos(result.originX(), result.baseY(), result.originZ());
bestHolder = holder; bestHolder = holder;
} }
} }
@@ -122,7 +140,8 @@ public class IrisChunkGenerator extends CustomChunkGenerator {
return Pair.of(best, bestHolder); return Pair.of(best, bestHolder);
} }
} catch (Throwable e) { } catch (Throwable e) {
IrisLogging.reportError(e); IrisLogging.reportError("Iris-placed structure lookup failed near "
+ pos.getX() + ", " + pos.getZ() + ".", e);
} }
if (!importedControl().active()) { if (!importedControl().active()) {
return null; return null;
@@ -134,28 +153,30 @@ public class IrisChunkGenerator extends CustomChunkGenerator {
try { try {
return delegate.findNearestMapStructure(level, reachable, pos, radius, findUnexplored); return delegate.findNearestMapStructure(level, reachable, pos, radius, findUnexplored);
} catch (Throwable e) { } catch (Throwable e) {
IrisLogging.error("Vanilla structure locate failed near " + pos.getX() + ", " + pos.getZ() + ": " + e); IrisLogging.reportError("Vanilla structure locate failed near "
IrisLogging.reportError(e); + pos.getX() + ", " + pos.getZ() + ".", e);
return null; return null;
} }
} }
private HolderSet<Structure> filterReachableStructures(ServerLevel level, HolderSet<Structure> holders) { private HolderSet<Structure> filterReachableStructures(ServerLevel level, HolderSet<Structure> holders) {
Set<String> reachable = reachableStructureKeysCache; Set<String> reachable = reachableStructureKeys(level);
if (reachable == null) { IrisImportedStructureControl control = importedControl();
reachable = VanillaStructureBiomes.reachableStructureKeys(level, delegate.getBiomeSource());
reachableStructureKeysCache = reachable;
}
if (reachable.isEmpty()) {
return holders;
}
Registry<Structure> registry = level.registryAccess().lookupOrThrow(Registries.STRUCTURE); 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) { for (Holder<Structure> holder : holders) {
Object id = registry.getKey(holder.value()); Object id = registry.getKey(holder.value());
if (id != null && reachable.contains(id.toString())) { if (id == null) {
kept.add(holder); 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()) { if (kept.size() == holders.size()) {
return holders; return holders;
@@ -163,6 +184,30 @@ public class IrisChunkGenerator extends CustomChunkGenerator {
return HolderSet.direct(kept); 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 @Override
protected MapCodec<? extends ChunkGenerator> codec() { protected MapCodec<? extends ChunkGenerator> codec() {
return MapCodec.unit(null); return MapCodec.unit(null);
@@ -177,12 +222,12 @@ public class IrisChunkGenerator extends CustomChunkGenerator {
@Override @Override
public int getMinY() { public int getMinY() {
return delegate.getMinY(); return runtimeMinY;
} }
@Override @Override
public int getSeaLevel() { public int getSeaLevel() {
return delegate.getSeaLevel(); return runtimeMinY + engine.getDimension().getFluidHeight();
} }
@Override @Override
@@ -190,7 +235,42 @@ public class IrisChunkGenerator extends CustomChunkGenerator {
if (!importedControl().active()) { if (!importedControl().active()) {
return; return;
} }
Map<Structure, StructureStart> previousStarts = new HashMap<>(access.getAllStarts());
super.createStructures(registryAccess, structureState, structureManager, access, templateManager, levelKey); 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() { private IrisImportedStructureControl importedControl() {
@@ -281,68 +361,119 @@ public class IrisChunkGenerator extends CustomChunkGenerator {
SectionPos sectionPos = SectionPos.of(chunkPos, world.getMinSectionY()); SectionPos sectionPos = SectionPos.of(chunkPos, world.getMinSectionY());
BlockPos origin = sectionPos.origin(); BlockPos origin = sectionPos.origin();
Registry<Structure> registry = world.registryAccess().lookupOrThrow(Registries.STRUCTURE); 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())); WorldgenRandom random = new WorldgenRandom(new XoroshiroRandomSource(RandomSupport.generateUniqueSeed()));
long decoSeed = random.setDecorationSeed(world.getSeed(), origin.getX(), origin.getZ()); long decoSeed = random.setDecorationSeed(world.getSeed(), origin.getX(), origin.getZ());
BoundingBox area = writableArea(chunk); BoundingBox area = writableArea(chunk);
int steps = GenerationStep.Decoration.values().length; int steps = GenerationStep.Decoration.values().length;
IrisImportedStructureControl control = importedControl(); IrisImportedStructureControl control = importedControl();
List<NativePlacementGroup> placementGroups = new ArrayList<>();
List<NativeStructurePostProcessor.VegetationTarget> vegetationTargets = new ArrayList<>();
for (int step = 0; step < steps; step++) { for (int step = 0; step < steps; step++) {
int index = 0; int index = 0;
for (Structure structure : byStep.getOrDefault(step, List.of())) { for (Structure structure : byStep.get(step)) {
Object id = registry.getKey(structure); Object id = registry.getKey(structure);
String structureId = id == null ? null : id.toString(); String structureId = id == null ? null : id.toString();
if (control.shouldGenerate(structureId) && !IrisStructureLocator.suppressesVanilla(engine, structureId)) { IrisNativeStructureDecision decision = control.resolve(
random.setFeatureSeed(decoSeed, index, step); structureId, NativeStructurePostProcessor.isUndergroundStep(structure.step()));
int[] offset = control.resolveOffset(structureId, isUndergroundStep(structure.step())); if (decision.generate() && !IrisStructureLocator.suppressesVanilla(engine, structureId)) {
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;
try { try {
structureManager.startsForStructure(sectionPos, structure) List<StructureStart> starts = structureManager.startsForStructure(sectionPos, structure);
.forEach(start -> start.placeInChunk(target, structureManager, this, random, placeArea, chunkPos)); 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) { } catch (Throwable e) {
IrisLogging.reportError(e); IrisLogging.reportError("Iris failed to resolve native structure " + structureId
+ " in chunk " + chunkPos.x() + "," + chunkPos.z() + ".", e);
} }
} }
index++; 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) { private void placeVanillaStructure(WorldGenLevel world, StructureManager structureManager, WorldgenRandom random,
return step == GenerationStep.Decoration.UNDERGROUND_STRUCTURES BoundingBox area, ChunkPos chunkPos, String structureId, StructureStart start,
|| step == GenerationStep.Decoration.STRONGHOLDS; 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) { private List<List<Structure>> structuresByStep(Registry<Structure> registry) {
return (WorldGenLevel) Proxy.newProxyInstance( StructureStepCache cached = structureStepCache;
WorldGenLevel.class.getClassLoader(), if (cached != null && cached.registry() == registry) {
new Class<?>[]{WorldGenLevel.class}, return cached.structures();
(proxy, method, args) -> { }
if (args != null) { synchronized (this) {
for (int i = 0; i < args.length; i++) { cached = structureStepCache;
if (args[i] instanceof BlockPos bp) { if (cached != null && cached.registry() == registry) {
args[i] = new BlockPos(bp.getX() + dx, bp.getY() + dy, bp.getZ() + dz); return cached.structures();
} }
} int steps = GenerationStep.Decoration.values().length;
} List<List<Structure>> grouped = new ArrayList<>(steps);
try { for (int step = 0; step < steps; step++) {
return method.invoke(world, args); grouped.add(new ArrayList<>());
} catch (InvocationTargetException e) { }
throw e.getCause(); 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) { private BoundingBox writableArea(ChunkAccess chunk) {
ChunkPos cp = chunk.getPos(); ChunkPos cp = chunk.getPos();
int i = cp.getMinBlockX(); int i = cp.getMinBlockX();
int j = cp.getMinBlockZ(); int j = cp.getMinBlockZ();
int minY = getMinY() + 1; int minY = chunk.getMinY();
int maxY = getMinY() + engine.getHeight() - 1; int maxY = minY + chunk.getHeight() - 1;
return new BoundingBox(i, minY, j, i + 15, maxY, j + 15); return new BoundingBox(i, minY, j, i + 15, maxY, j + 15);
} }
@@ -403,7 +534,7 @@ public class IrisChunkGenerator extends CustomChunkGenerator {
@Override @Override
public int getGenDepth() { public int getGenDepth() {
return delegate.getGenDepth(); return runtimeHeight;
} }
@Override @Override
@@ -447,7 +578,7 @@ public class IrisChunkGenerator extends CustomChunkGenerator {
Method setHeight = null; Method setHeight = null;
for (Method method : Heightmap.class.getDeclaredMethods()) { 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}) if (types.length != 3 || !Arrays.equals(types, new Class<?>[]{int.class, int.class, int.class})
|| !method.getReturnType().equals(void.class)) || !method.getReturnType().equals(void.class))
continue; continue;
@@ -475,4 +606,14 @@ public class IrisChunkGenerator extends CustomChunkGenerator {
private record SpawnTableKey(Biome biome, MobCategory category) { 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) {
}
} }
@@ -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.cache.AtomicCache;
import art.arcane.iris.engine.data.chunk.TerrainChunk; import art.arcane.iris.engine.data.chunk.TerrainChunk;
import art.arcane.iris.engine.framework.Engine; 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.engine.platform.PlatformChunkGenerator;
import art.arcane.iris.util.project.agent.Agent; import art.arcane.iris.util.project.agent.Agent;
import art.arcane.volmlib.util.collection.KList; import art.arcane.volmlib.util.collection.KList;
@@ -71,6 +72,7 @@ import net.minecraft.resources.Identifier;
import net.minecraft.resources.ResourceKey; import net.minecraft.resources.ResourceKey;
import net.minecraft.server.MinecraftServer; import net.minecraft.server.MinecraftServer;
import net.minecraft.server.commands.data.BlockDataAccessor; import net.minecraft.server.commands.data.BlockDataAccessor;
import net.minecraft.server.level.ChunkMap;
import net.minecraft.server.level.ServerLevel; import net.minecraft.server.level.ServerLevel;
import net.minecraft.server.level.ThreadedLevelLightEngine; import net.minecraft.server.level.ThreadedLevelLightEngine;
import net.minecraft.tags.TagKey; 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.ChunkStatus;
import net.minecraft.world.level.chunk.status.WorldGenContext; import net.minecraft.world.level.chunk.status.WorldGenContext;
import net.minecraft.world.level.dimension.LevelStem; 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.FlatLevelSource;
import net.minecraft.world.level.levelgen.Heightmap; import net.minecraft.world.level.levelgen.Heightmap;
import net.minecraft.world.level.levelgen.WorldgenRandom; 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 { public void inject(long seed, Engine engine, World world) throws NoSuchFieldException, IllegalAccessException {
var chunkMap = ((CraftWorld)world).getHandle().getChunkSource().chunkMap; ServerLevel level = ((CraftWorld) world).getHandle();
var worldGenContextField = getField(chunkMap.getClass(), WorldGenContext.class); validateDimensionContract(engine, world, level);
ChunkMap chunkMap = level.getChunkSource().chunkMap;
Field worldGenContextField = getField(chunkMap.getClass(), WorldGenContext.class);
worldGenContextField.setAccessible(true); worldGenContextField.setAccessible(true);
var worldGenContext = (WorldGenContext) worldGenContextField.get(chunkMap); WorldGenContext 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);
}
IrisChunkGenerator irisGenerator = new IrisChunkGenerator(worldGenContext.generator(), seed, engine, world); IrisChunkGenerator irisGenerator = new IrisChunkGenerator(worldGenContext.generator(), seed, engine, world);
var newContext = new WorldGenContext( WorldGenContext newContext = new WorldGenContext(
worldGenContext.level(), irisGenerator, worldGenContext.level(), irisGenerator,
worldGenContext.structureManager(), worldGenContext.lightEngine(), worldGenContext.mainThreadExecutor(), worldGenContext.unsavedListener()); worldGenContext.structureManager(), worldGenContext.lightEngine(), worldGenContext.mainThreadExecutor(), worldGenContext.unsavedListener());
worldGenContextField.set(chunkMap, newContext); 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 { private static void retargetStructureCheck(ServerLevel level, IrisChunkGenerator generator) throws NoSuchFieldException, IllegalAccessException {
@@ -1208,33 +1220,38 @@ public class NMSBinding implements INMSBinding {
@Override @Override
public boolean injectBukkit() { public boolean injectBukkit() {
if (injected.getAndSet(true)) synchronized (injected) {
return true; if (injected.get()) {
try { return true;
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());
} }
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; injected.set(true);
} catch (Throwable e) { return true;
IrisLogging.error(C.RED + "Failed to inject Bukkit"); } catch (Throwable e) {
e.printStackTrace(); IrisLogging.error(C.RED + "Failed to inject Bukkit");
e.printStackTrace();
return false;
}
} }
return false;
} }
@Override @Override
@@ -1304,7 +1321,8 @@ public class NMSBinding implements INMSBinding {
static void enter( static void enter(
@Advice.Argument(0) MinecraftServer server, @Advice.Argument(0) MinecraftServer server,
@Advice.Argument(4) ResourceKey<Level> dimensionKey, @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) if (dimensionKey == null)
return; return;
@@ -1315,18 +1333,19 @@ public class NMSBinding implements INMSBinding {
return; return;
} }
Object generator = Class.forName("art.arcane.iris.core.lifecycle.WorldLifecycleStaging", true, Bukkit.getPluginManager().getPlugin("Iris") ClassLoader pluginClassLoader = Bukkit.getPluginManager().getPlugin("Iris").getClass().getClassLoader();
.getClass() Class<?> generatorType = Class.forName("art.arcane.iris.engine.platform.PlatformChunkGenerator", true, pluginClassLoader);
.getClassLoader()) Object generator = generatorType.isInstance(constructorGenerator) ? constructorGenerator : null;
.getDeclaredMethod("consumeStemGenerator", String.class) if (generator == null) {
.invoke(null, levelId); 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")) { if (!(generator instanceof ChunkGenerator gen) || !gen.getClass().getPackageName().startsWith("art.arcane.iris")) {
return; return;
} }
Object bindings = Class.forName("art.arcane.iris.core.nms.INMS", true, Bukkit.getPluginManager().getPlugin("Iris") Object bindings = Class.forName("art.arcane.iris.core.nms.INMS", true, pluginClassLoader)
.getClass()
.getClassLoader())
.getDeclaredMethod("get") .getDeclaredMethod("get")
.invoke(null); .invoke(null);
if (bindings == null) { if (bindings == null) {
@@ -25,7 +25,10 @@ final class VanillaStructureBiomes {
if (source == null) { if (source == null) {
return keys; 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(); Optional<ResourceKey<Biome>> key = holder.unwrapKey();
if (key.isPresent()) { if (key.isPresent()) {
keys.add(key.get().identifier().toString()); keys.add(key.get().identifier().toString());
@@ -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);
}
}
@@ -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));
}
}
+8 -2
View File
@@ -3,11 +3,13 @@ def mainClass = 'art.arcane.iris.Iris'
def bootstrapperClass = 'art.arcane.iris.IrisBootstrap' def bootstrapperClass = 'art.arcane.iris.IrisBootstrap'
String volmLibCoordinate = providers.gradleProperty('volmLibCoordinate') String volmLibCoordinate = providers.gradleProperty('volmLibCoordinate')
.orElse('com.github.VolmitSoftware:VolmLib:cdf7e8bb880199734dacd386a6616bda799fcde1') .orElse('com.github.VolmitSoftware:VolmLib:4c38988b344792a79f925f57f8a675bc85fc1bed')
.get() .get()
dependencies { dependencies {
compileOnly(project(':core')) compileOnly(project(':core'))
compileOnly(libs.lombok)
annotationProcessor(libs.lombok)
compileOnly(volmLibCoordinate) { compileOnly(volmLibCoordinate) {
transitive = false transitive = false
} }
@@ -21,7 +23,6 @@ dependencies {
transitive = false transitive = false
} }
compileOnly(libs.placeholderApi) compileOnly(libs.placeholderApi)
compileOnly(libs.multiverseCore)
} }
tasks.named('processResources').configure { tasks.named('processResources').configure {
@@ -41,3 +42,8 @@ tasks.named('processResources').configure {
tasks.named('jar', Jar).configure { tasks.named('jar', Jar).configure {
archiveBaseName.set('iris-bukkit-plugin') 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; package art.arcane.iris;
import art.arcane.iris.engine.IrisEngineEffects;
import art.arcane.iris.engine.IrisWorldManager; 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.engine.framework.EngineWorldManagerProvider;
import art.arcane.iris.core.splash.IrisSplashComposer; import art.arcane.iris.core.splash.IrisSplashComposer;
import art.arcane.iris.core.IrisSettings; 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.datapack.DatapackIngestService;
import art.arcane.iris.core.lifecycle.PaperLibBootstrap; import art.arcane.iris.core.lifecycle.PaperLibBootstrap;
import art.arcane.iris.core.lifecycle.WorldLifecycleService; 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.TransientWorldCleanupSupport;
import art.arcane.iris.core.runtime.WorldRuntimeControlService; import art.arcane.iris.core.runtime.WorldRuntimeControlService;
import art.arcane.iris.core.lifecycle.WorldLifecycleStaging; 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.core.safeguard.IrisSafeguard;
import art.arcane.iris.engine.platform.PlatformChunkGenerator; import art.arcane.iris.engine.platform.PlatformChunkGenerator;
import art.arcane.iris.platform.bukkit.BukkitPlatform; 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.IrisLogging;
import art.arcane.iris.spi.IrisPlatforms; import art.arcane.iris.spi.IrisPlatforms;
import art.arcane.iris.spi.IrisServices; import art.arcane.iris.spi.IrisServices;
@@ -116,6 +122,7 @@ import java.io.FileWriter;
import java.io.IOException; import java.io.IOException;
import java.io.PrintWriter; import java.io.PrintWriter;
import java.lang.annotation.Annotation; import java.lang.annotation.Annotation;
import java.lang.reflect.Modifier;
import java.net.URI; import java.net.URI;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collection; 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); JarScanner js = new JarScanner(instance.getJarFile(), s);
KList<Object> v = new KList<>(); KList<T> v = new KList<>();
J.attempt(js::scan); J.attempt(js::scan);
for (Class<?> i : js.getClasses()) { for (Class<?> i : js.getClasses()) {
if (slicedClass == null || i.isAnnotationPresent(slicedClass)) { if (!isConcreteImplementation(i, requiredType)) {
try { continue;
v.add(i.getDeclaredConstructor().newInstance()); }
} catch (Throwable ex) { try {
Iris.warn("Skipped class initialization for %s: %s%s", v.add(requiredType.cast(i.getDeclaredConstructor().newInstance()));
i.getName(), } catch (Throwable ex) {
ex.getClass().getSimpleName(), Iris.warn("Skipped class initialization for %s: %s%s",
ex.getMessage() == null ? "" : " - " + ex.getMessage()); i.getName(),
Iris.reportError(ex); ex.getClass().getSimpleName(),
} ex.getMessage() == null ? "" : " - " + ex.getMessage());
Iris.reportError(ex);
} }
} }
return v; 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) { public static KList<Class<?>> getClasses(String s, Class<? extends Annotation> slicedClass) {
JarScanner js = new JarScanner(instance.getJarFile(), s); JarScanner js = new JarScanner(instance.getJarFile(), s);
KList<Class<?>> v = new KList<>(); KList<Class<?>> v = new KList<>();
@@ -243,10 +258,6 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
return v; return v;
} }
public static KList<Object> initialize(String s) {
return initialize(s, null);
}
public static void sq(Runnable r) { public static void sq(Runnable r) {
synchronized (syncJobs) { synchronized (syncJobs) {
syncJobs.queue(r); syncJobs.queue(r);
@@ -599,9 +610,10 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
services = new KMap<>(); services = new KMap<>();
setupAudience(); setupAudience();
Bindings.setupSentry(); Bindings.setupSentry();
initialize("art.arcane.iris.core.service").forEach((i) -> { initialize("art.arcane.iris.core.service", IrisService.class).forEach((i) -> {
services.put((Class<? extends IrisService>) i.getClass(), (IrisService) i); Class<? extends IrisService> serviceType = i.getClass().asSubclass(IrisService.class);
IrisServices.register(i.getClass(), i); services.put(serviceType, i);
IrisServices.register(serviceType, i);
}); });
IrisServices.register(BlockEditAccess.class, services.get(EditSVC.class)); IrisServices.register(BlockEditAccess.class, services.get(EditSVC.class));
IrisServices.register(PreservationRegistry.class, services.get(PreservationSVC.class)); IrisServices.register(PreservationRegistry.class, services.get(PreservationSVC.class));
@@ -619,6 +631,9 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
tickets = new ChunkTickets(); tickets = new ChunkTickets();
linkMultiverseCore = new MultiverseCoreLink(); linkMultiverseCore = new MultiverseCoreLink();
IrisServices.register(MultiverseCoreLink.class, linkMultiverseCore); 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) -> { IrisServices.register(EngineWorldManagerProvider.class, (EngineWorldManagerProvider) (Engine engine) -> {
IrisWorldManager manager = new IrisWorldManager(engine); IrisWorldManager manager = new IrisWorldManager(engine);
manager.startManager(); 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 + "..."); Iris.info(C.LIGHT_PURPLE + "Preparing Spawn for " + s + "' using Iris:" + generator + "...");
WorldCreator c = WorldCreator.ofKey(worldKey) WorldCreator c = WorldCreator.ofKey(worldKey)
.generator(gen) .generator(gen)
.environment(dim.getEnvironment()); .environment(BukkitEnvironment.from(dim.getEnvironment()));
Long stagedSeed = IrisWorlds.readBukkitWorldSeed(s); Long stagedSeed = IrisWorlds.readBukkitWorldSeed(s);
if (stagedSeed != null) { if (stagedSeed != null) {
c.seed(stagedSeed); c.seed(stagedSeed);
@@ -1067,7 +1082,7 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
} }
private void setupPapi() { private void setupPapi() {
if (Bukkit.getPluginManager().getPlugin("PlaceholderAPI") != null) { if (Bukkit.getPluginManager().isPluginEnabled("PlaceholderAPI")) {
new IrisPapiExpansion().register(); new IrisPapiExpansion().register();
} }
} }
@@ -1215,10 +1230,9 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
NamespacedKey worldKey = IrisWorldStorage.keyFromLegacyName(worldName); NamespacedKey worldKey = IrisWorldStorage.keyFromLegacyName(worldName);
IrisWorld w = IrisWorld.builder() IrisWorld w = IrisWorld.builder()
.key(worldKey) .platformIdentity(worldKey.toString())
.name(worldName) .name(worldName)
.seed(1337) .seed(1337)
.environment(dim.getEnvironment())
.worldFolder(IrisWorldStorage.dimensionRoot(worldKey)) .worldFolder(IrisWorldStorage.dimensionRoot(worldKey))
.minHeight(dim.getMinHeight()) .minHeight(dim.getMinHeight())
.maxHeight(dim.getMaxHeight()) .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;
import art.arcane.iris.core.pack.DefaultPackBootstrapProvisioner.ProvisionResult; 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.BootstrapContext;
import io.papermc.paper.plugin.bootstrap.PluginBootstrap; import io.papermc.paper.plugin.bootstrap.PluginBootstrap;
import io.papermc.paper.plugin.lifecycle.event.types.LifecycleEvents;
import java.io.IOException; import java.io.IOException;
import java.nio.file.Path; import java.nio.file.Path;
@SuppressWarnings("UnstableApiUsage") @SuppressWarnings("UnstableApiUsage")
public final class IrisBootstrap implements PluginBootstrap { public final class IrisBootstrap implements PluginBootstrap {
static final String PACK_ID = "generated";
@Override @Override
public void bootstrap(BootstrapContext context) { public void bootstrap(BootstrapContext context) {
ProvisionResult provisioned = provision(context); ProvisionResult provisioned = provision(context);
Path datapackRoot = provisioned.datapackRoot(); Path datapackRoot = provisioned.datapackRoot();
context.getLogger().info("Iris startup datapack is {} at {}", provisioned.status(), 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) { private static ProvisionResult provision(BootstrapContext context) {
@@ -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;
}
}
@@ -42,6 +42,7 @@ import io.papermc.paper.registry.RegistryKey;
import org.bukkit.Location; import org.bukkit.Location;
import org.bukkit.NamespacedKey; import org.bukkit.NamespacedKey;
import org.bukkit.Registry; import org.bukkit.Registry;
import org.bukkit.World;
import org.bukkit.entity.Player; import org.bukkit.entity.Player;
import org.bukkit.generator.structure.Structure; import org.bukkit.generator.structure.Structure;
import org.bukkit.util.StructureSearchResult; import org.bukkit.util.StructureSearchResult;
@@ -98,7 +99,7 @@ public class CommandFind implements DirectorExecutor {
EngineBukkitOps.gotoPOI(e, type, player(), teleport); 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( public void structure(
@Param(description = "The structure to look for (e.g. minecraft:village_plains, minecraft:stronghold, minecraft_ancient_city)", customHandler = StructureHandler.class) @Param(description = "The structure to look for (e.g. minecraft:village_plains, minecraft:stronghold, minecraft_ancient_city)", customHandler = StructureHandler.class)
String structure String structure
@@ -117,7 +118,12 @@ public class CommandFind implements DirectorExecutor {
} }
if (IrisStructureLocator.isPlaced(e, structure)) { 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; return;
} }
@@ -127,6 +133,8 @@ public class CommandFind implements DirectorExecutor {
return; return;
} }
World targetWorld = target.getWorld();
Location origin = target.getLocation();
commandSender.sendMessage(C.GRAY + "Locating " + structure + "..."); commandSender.sendMessage(C.GRAY + "Locating " + structure + "...");
J.s(() -> { J.s(() -> {
try { try {
@@ -140,32 +148,66 @@ public class CommandFind implements DirectorExecutor {
} }
} }
if (match == null) { if (match == null) {
commandSender.sendMessage(C.RED + "Unknown structure: " + structure); sendStructureMessage(target, commandSender, C.RED + "Unknown structure: " + structure);
return; return;
} }
if (!StructureReachability.isReachable(e, structure)) { if (!StructureReachability.isReachable(e, structure)) {
KList<String> miss = StructureReachability.missingBiomeKeys(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" sendStructureMessage(target, commandSender,
+ (miss.isEmpty() ? "" : ": needs " + String.join("/", miss)) + ")."); C.YELLOW + structure + " cannot generate in this world (its required biomes are not produced by this pack"
+ (miss.isEmpty() ? "" : ": needs " + String.join("/", miss)) + ").");
return; 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) { 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; return;
} }
Location at = result.getLocation(); prepareStructureTeleport(target, targetWorld, commandSender, structure, result.getLocation(), false);
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());
} catch (Throwable t) { } 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); 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") @Director(description = "Find an object")
public void object( public void object(
@Param(description = "The object to look for", customHandler = ObjectHandler.class) @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."); 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));
}
} }
@@ -18,6 +18,7 @@
package art.arcane.iris.core.commands; package art.arcane.iris.core.commands;
import art.arcane.iris.Iris;
import art.arcane.iris.core.loader.IrisData; import art.arcane.iris.core.loader.IrisData;
import art.arcane.iris.core.structure.BulkStructureImporter; import art.arcane.iris.core.structure.BulkStructureImporter;
import art.arcane.iris.core.structure.StructureCaptureImporter; 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.volmlib.util.director.annotations.Param;
import art.arcane.iris.core.tools.IrisToolbelt; import art.arcane.iris.core.tools.IrisToolbelt;
import art.arcane.iris.engine.platform.PlatformChunkGenerator; 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.volmlib.util.math.RNG;
import art.arcane.iris.util.common.scheduling.J;
import io.papermc.paper.registry.RegistryAccess; import io.papermc.paper.registry.RegistryAccess;
import io.papermc.paper.registry.RegistryKey; import io.papermc.paper.registry.RegistryKey;
import org.bukkit.Bukkit; import org.bukkit.Bukkit;
@@ -51,12 +54,12 @@ import org.bukkit.Registry;
import org.bukkit.World; import org.bukkit.World;
import org.bukkit.block.Block; import org.bukkit.block.Block;
import org.bukkit.block.data.BlockData; import org.bukkit.block.data.BlockData;
import org.bukkit.entity.Player;
import org.bukkit.generator.structure.Structure; import org.bukkit.generator.structure.Structure;
import org.bukkit.util.StructureSearchResult;
import java.io.File; import java.io.File;
import java.util.Collections;
import java.util.HashMap; import java.util.HashMap;
import java.util.Locale;
import java.util.Map; import java.util.Map;
import java.util.Set; 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."); 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( public void verify(
@Param(description = "The dimension to verify", aliases = "dim") @Param(description = "The dimension to verify", aliases = "dim")
IrisDimension dimension, IrisDimension dimension,
@@ -124,85 +127,150 @@ public class CommandStructure implements DirectorExecutor {
return; return;
} }
boolean senderIsPlayer = sender() != null && sender().isPlayer(); 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)); 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); PlatformChunkGenerator access = IrisToolbelt.access(world);
if (access != null) { Engine engine = access == null ? null : access.getEngine();
engine = 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); KList<String> structureKeys = new KList<>();
int found = 0;
int missing = 0;
int unreachable = 0;
int irisPlaced = 0;
KList<String> notFound = new KList<>();
KList<String> cannotGenerate = new KList<>();
Registry<Structure> structureRegistry = RegistryAccess.registryAccess().getRegistry(RegistryKey.STRUCTURE); Registry<Structure> structureRegistry = RegistryAccess.registryAccess().getRegistry(RegistryKey.STRUCTURE);
for (Structure structure : structureRegistry) { for (Structure structure : structureRegistry) {
NamespacedKey key = structureRegistry.getKey(structure); NamespacedKey key = structureRegistry.getKey(structure);
String keyName = key == null ? structure.toString() : key.toString(); if (key != null) {
boolean isIrisPlaced = engine != null && IrisStructureLocator.suppressesVanilla(engine, keyName); structureKeys.add(key.toString());
boolean isReachable = engine != null && reachable.contains(keyName.toLowerCase()); }
if (engine != null && !isIrisPlaced && !isReachable) { }
unreachable++; VolmitSender commandSender = sender();
KList<String> miss = StructureReachability.missingBiomeKeys(engine, keyName); Player target = senderIsPlayer ? player() : null;
cannotGenerate.add(keyName + (miss.isEmpty() ? "" : " (needs " + String.join("/", miss) + ")")); 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; continue;
} }
if (isIrisPlaced) { if (!engine.getDimension().getImportedStructures().shouldGenerate(keyName)) {
irisPlaced++; disabled++;
messages.add(C.GRAY + "[disabled] " + C.WHITE + keyName);
continue;
} }
try { if (!reachable.contains(keyName.toLowerCase(Locale.ROOT))) {
StructureSearchResult result = world.locateNearestStructure(center, structure, searchRadius, true); unreachable++;
if (result != null && result.getLocation() != null) { KList<String> missing = StructureReachability.missingBiomeKeys(engine, keyName);
found++; messages.add(C.YELLOW + "[unreachable] " + C.WHITE + keyName
Location l = result.getLocation(); + (missing.isEmpty() ? "" : C.YELLOW + " needs " + String.join("/", missing)));
sender().sendMessage((isIrisPlaced ? C.AQUA + "[iris] " : C.GREEN + "[ok] ") + C.WHITE + keyName + C.GREEN + " @ " + l.getBlockX() + "," + l.getBlockZ()); continue;
} else {
missing++;
notFound.add(keyName);
}
} catch (Throwable e) {
missing++;
notFound.add(keyName + " (error: " + e.getClass().getSimpleName() + ")");
} }
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), " private void sendVerificationMessages(VolmitSender commandSender, Player target,
+ C.WHITE + unreachable + C.GREEN + " cannot generate here, " KList<String> messages) {
+ C.WHITE + missing + C.GREEN + " reachable-but-not-found within " + searchRadius + " chunks."); Runnable send = () -> {
if (!cannotGenerate.isEmpty()) { for (String message : messages) {
sender().sendMessage(C.RED + "Cannot generate (required biomes absent from this pack): " + C.GRAY + String.join(", ", cannotGenerate)); commandSender.sendMessage(message);
} }
if (!notFound.isEmpty()) { };
sender().sendMessage(C.YELLOW + "Reachable but not found (rarer than radius): " + C.GRAY + String.join(", ", notFound)); if (target != null) {
J.runEntity(target, send);
} else {
J.s(send);
} }
} }
private World resolveIrisWorld(IrisDimension dimension) { private World resolveIrisWorld(IrisDimension dimension) {
if (sender() != null && sender().isPlayer() && IrisToolbelt.isIrisWorld(player().getWorld())) { 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()) { for (World w : Bukkit.getWorlds()) {
if (!IrisToolbelt.isIrisWorld(w)) { if (!IrisToolbelt.isIrisWorld(w)) {
continue; continue;
} }
if (fallback == null) {
fallback = w;
}
PlatformChunkGenerator gen = IrisToolbelt.access(w); PlatformChunkGenerator gen = IrisToolbelt.access(w);
if (gen != null && gen.getEngine() != null && gen.getEngine().getDimension() != null if (matchesDimension(gen, dimension.getLoadKey())) {
&& dimension.getLoadKey().equals(gen.getEngine().getDimension().getLoadKey())) {
return w; 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) @Director(description = "Resolve an iris structure's jigsaw graph and report piece count & bounds", origin = DirectorOrigin.BOTH)
@@ -18,6 +18,7 @@
package art.arcane.iris.core.commands; package art.arcane.iris.core.commands;
import art.arcane.iris.platform.bukkit.BukkitWorldBinding;
import art.arcane.iris.Iris; import art.arcane.iris.Iris;
import art.arcane.iris.platform.bukkit.BukkitPlatform; import art.arcane.iris.platform.bukkit.BukkitPlatform;
import art.arcane.iris.core.IrisSettings; 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.IrisObject;
import art.arcane.iris.engine.object.IrisObjectPlacement; import art.arcane.iris.engine.object.IrisObjectPlacement;
import art.arcane.iris.engine.object.IrisRegion; 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.object.NoiseStyle;
import art.arcane.iris.engine.platform.EngineBukkitOps; import art.arcane.iris.engine.platform.EngineBukkitOps;
import art.arcane.iris.engine.platform.PlatformChunkGenerator; 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!"); sender().sendMessage(C.GREEN + "Sending you to the studio world!");
var player = player(); Player player = player();
BukkitPlatform.teleportAsync(player(), Iris.service(StudioSVC.class) IrisWorld studioWorld = Iris.service(StudioSVC.class)
.getActiveProject() .getActiveProject()
.getActiveProvider() .getActiveProvider()
.getTarget() .getTarget()
.getWorld() .getWorld();
.spawnLocation() BukkitPlatform.teleportAsync(player, BukkitWorldBinding.spawnLocation(studioWorld))
).thenRun(() -> player.setGameMode(GameMode.SPECTATOR)); .thenRun(() -> player.setGameMode(GameMode.SPECTATOR));
} }
@Director(description = "Update your dimension projects VSCode workspace") @Director(description = "Update your dimension projects VSCode workspace")
@@ -18,6 +18,7 @@
package art.arcane.iris.core.gui; package art.arcane.iris.core.gui;
import art.arcane.iris.platform.bukkit.BukkitWorldBinding;
import art.arcane.iris.engine.IrisComplex; import art.arcane.iris.engine.IrisComplex;
import art.arcane.iris.engine.framework.Engine; import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.framework.render.RenderType; import art.arcane.iris.engine.framework.render.RenderType;
@@ -47,7 +48,7 @@ public final class BukkitVisionOverlay implements GuiOverlay {
public List<GuiMarker> players() { public List<GuiMarker> players() {
IrisWorld world = engine.getWorld(); IrisWorld world = engine.getWorld();
List<GuiMarker> markers = new ArrayList<>(); 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())); markers.add(GuiMarker.player(player.getName(), player.getLocation().getX(), player.getLocation().getZ()));
} }
return markers; return markers;
@@ -58,7 +59,7 @@ public final class BukkitVisionOverlay implements GuiOverlay {
J.s(() -> { J.s(() -> {
IrisWorld world = engine.getWorld(); IrisWorld world = engine.getWorld();
List<GuiMarker> markers = new ArrayList<>(); List<GuiMarker> markers = new ArrayList<>();
for (LivingEntity entity : world.getEntitiesByClass(LivingEntity.class)) { for (LivingEntity entity : BukkitWorldBinding.entities(world, LivingEntity.class)) {
if (entity instanceof Player) { if (entity instanceof Player) {
continue; continue;
} }
@@ -78,11 +79,11 @@ public final class BukkitVisionOverlay implements GuiOverlay {
@Override @Override
public void teleport(double worldX, double worldZ) { public void teleport(double worldX, double worldZ) {
IrisWorld world = engine.getWorld(); IrisWorld world = engine.getWorld();
if (!world.hasRealWorld()) { if (!world.hasPlatformWorld()) {
return; return;
} }
J.s(() -> { J.s(() -> {
List<Player> players = world.getPlayers(); List<Player> players = BukkitWorldBinding.players(world);
if (players.isEmpty()) { if (players.isEmpty()) {
return; return;
} }
@@ -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);
}
}
@@ -1,5 +1,6 @@
package art.arcane.iris.core.service; package art.arcane.iris.core.service;
import art.arcane.iris.platform.bukkit.BukkitWorldBinding;
import com.google.common.util.concurrent.AtomicDouble; import com.google.common.util.concurrent.AtomicDouble;
import art.arcane.iris.spi.IrisLogging; import art.arcane.iris.spi.IrisLogging;
import art.arcane.iris.spi.IrisServices; 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.core.tools.IrisToolbelt;
import art.arcane.iris.engine.framework.Engine; import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.platform.PlatformChunkGenerator; import art.arcane.iris.engine.platform.PlatformChunkGenerator;
import art.arcane.volmlib.util.bukkit.WorldIdentity;
import art.arcane.volmlib.util.collection.KMap; import art.arcane.volmlib.util.collection.KMap;
import art.arcane.iris.util.common.format.C; import art.arcane.iris.util.common.format.C;
import art.arcane.volmlib.util.format.Form; import art.arcane.volmlib.util.format.Form;
@@ -312,7 +314,7 @@ public class IrisEngineSVC implements IrisService {
|| engine.getMantle().getMantle().isClosed() || engine.getMantle().getMantle().isClosed()
|| !shouldReduce(engine)) || !shouldReduce(engine))
return; return;
World engineWorld = engine.getWorld().realWorld(); World engineWorld = BukkitWorldBinding.world(engine.getWorld());
if (shouldSkipForMaintenance(engineWorld)) { if (shouldSkipForMaintenance(engineWorld)) {
return; return;
} }
@@ -346,7 +348,7 @@ public class IrisEngineSVC implements IrisService {
|| engine.getMantle().getMantle().isClosed() || engine.getMantle().getMantle().isClosed()
|| !shouldReduce(engine)) || !shouldReduce(engine))
return; return;
World engineWorld = engine.getWorld().realWorld(); World engineWorld = BukkitWorldBinding.world(engine.getWorld());
if (shouldSkipForMaintenance(engineWorld)) { if (shouldSkipForMaintenance(engineWorld)) {
return; return;
} }
@@ -446,7 +448,7 @@ public class IrisEngineSVC implements IrisService {
} }
PregeneratorJob pregeneratorJob = PregeneratorJob.getInstance(); PregeneratorJob pregeneratorJob = PregeneratorJob.getInstance();
boolean pregeneratorTargetsWorld = pregeneratorJob != null && pregeneratorJob.targetsWorld(world); boolean pregeneratorTargetsWorld = pregeneratorJob != null && pregeneratorJob.targetsWorldIdentity(WorldIdentity.serialize(world));
return shouldSkipMantleReductionForMaintenance(maintenanceActive, pregeneratorTargetsWorld); return shouldSkipMantleReductionForMaintenance(maintenanceActive, pregeneratorTargetsWorld);
} }
} }
@@ -8,3 +8,65 @@ load: STARTUP
authors: [ cyberpwn, NextdoorPsycho, Vatuu ] authors: [ cyberpwn, NextdoorPsycho, Vatuu ]
website: volmit.com website: volmit.com
description: More than a Dimension! 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} main: ${main}
folia-supported: true folia-supported: true
load: STARTUP load: STARTUP
softdepend:
- PlaceholderAPI
- CraftEngine
- Nexo
- ItemsAdder
- SCore
- ExecutableItems
- MythicLib
- MMOItems
- eco
- EcoItems
- MythicMobs
- MythicCrucible
- KGenerators
- WorldEdit
loadbefore: [ Multiverse-Core ]
authors: [ cyberpwn, NextdoorPsycho, Vatuu ] authors: [ cyberpwn, NextdoorPsycho, Vatuu ]
website: volmit.com website: volmit.com
description: More than a Dimension! 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; package art.arcane.iris;
import art.arcane.iris.core.splash.IrisSplashPackScanner; 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 org.junit.Test;
import java.io.ByteArrayOutputStream; import java.io.ByteArrayOutputStream;
@@ -13,9 +15,18 @@ import java.util.Comparator;
import java.util.List; import java.util.List;
import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue; import static org.junit.Assert.assertTrue;
public class IrisDiagnosticsTest { 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 @Test
public void reportErrorWithContextPrintsFullStacktrace() { public void reportErrorWithContextPrintsFullStacktrace() {
ByteArrayOutputStream output = new ByteArrayOutputStream(); ByteArrayOutputStream output = new ByteArrayOutputStream();
@@ -18,6 +18,30 @@ import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue; import static org.junit.Assert.assertTrue;
public class PaperPluginMetadataTest { 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 @Test
public void paperMetadataDeclaresBootstrapAndFoliaSupport() throws Exception { public void paperMetadataDeclaresBootstrapAndFoliaSupport() throws Exception {
String metadata; String metadata;
@@ -30,6 +54,11 @@ public class PaperPluginMetadataTest {
assertTrue(metadata.contains("folia-supported: true")); assertTrue(metadata.contains("folia-supported: true"));
assertTrue(metadata.contains("load: STARTUP")); assertTrue(metadata.contains("load: STARTUP"));
assertFalse(metadata.contains("commands:")); 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 @Test
@@ -45,6 +74,8 @@ public class PaperPluginMetadataTest {
Map<String, Map<String, Object>> commands = metadata.getCommands(); Map<String, Map<String, Object>> commands = metadata.getCommands();
assertTrue(commands.containsKey("iris")); assertTrue(commands.containsKey("iris"));
assertEquals(List.of("ir", "irs"), commands.get("iris").get("aliases")); 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 @Test
@@ -56,4 +87,11 @@ public class PaperPluginMetadataTest {
assertTrue(Files.isRegularFile(bukkitMetadata)); assertTrue(Files.isRegularFile(bukkitMetadata));
assertFalse(Files.readString(bukkitMetadata, StandardCharsets.UTF_8).contains("${")); 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";
}
} }
@@ -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"));
}
}
@@ -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"));
}
}
+24 -13
View File
@@ -22,7 +22,7 @@ import org.gradle.jvm.toolchain.JavaLanguageVersion
plugins { plugins {
id 'java' id 'java'
id 'net.fabricmc.fabric-loom' version '1.17.11' id 'net.fabricmc.fabric-loom' version '1.17.14'
alias(libs.plugins.shadow) alias(libs.plugins.shadow)
} }
@@ -31,7 +31,7 @@ file('../../gradle.properties').withInputStream { InputStream stream -> rootProp
String irisVersion = providers.gradleProperty('irisVersion').getOrElse(rootProperties.getProperty('irisVersion', '4.0.0-26.2')) String irisVersion = providers.gradleProperty('irisVersion').getOrElse(rootProperties.getProperty('irisVersion', '4.0.0-26.2'))
String minecraftVersion = providers.gradleProperty('minecraftVersion').getOrElse(rootProperties.getProperty('minecraftVersion', '26.2')) String minecraftVersion = providers.gradleProperty('minecraftVersion').getOrElse(rootProperties.getProperty('minecraftVersion', '26.2'))
String fabricLoaderVersion = providers.gradleProperty('fabricLoaderVersion').getOrElse(rootProperties.getProperty('fabricLoaderVersion', '0.19.3')) String fabricLoaderVersion = providers.gradleProperty('fabricLoaderVersion').getOrElse(rootProperties.getProperty('fabricLoaderVersion', '0.19.3'))
String volmLibCoordinate = providers.gradleProperty('volmLibCoordinate').getOrElse(rootProperties.getProperty('volmLibCoordinate', 'com.github.VolmitSoftware:VolmLib:cdf7e8bb880199734dacd386a6616bda799fcde1')) String volmLibCoordinate = providers.gradleProperty('volmLibCoordinate').getOrElse(rootProperties.getProperty('volmLibCoordinate', 'com.github.VolmitSoftware:VolmLib:4c38988b344792a79f925f57f8a675bc85fc1bed'))
Closure<String> irisArtifactName = { String platform, String targetVersion -> Closure<String> irisArtifactName = { String platform, String targetVersion ->
return "Iris v${project.version} [${platform}] ${targetVersion}.jar" return "Iris v${project.version} [${platform}] ${targetVersion}.jar"
} }
@@ -49,6 +49,7 @@ sourceSets {
main { main {
java { java {
srcDir '../modded-common/src/main/java' srcDir '../modded-common/src/main/java'
srcDir '../minecraft-common/src/main/java'
srcDir '../client-common/src/main/java' srcDir '../client-common/src/main/java'
} }
resources { resources {
@@ -99,6 +100,8 @@ configurations.named('bundle').configure {
configurations.compileClasspath.extendsFrom(configurations.devBundle) configurations.compileClasspath.extendsFrom(configurations.devBundle)
configurations.runtimeClasspath.extendsFrom(configurations.devBundle) configurations.runtimeClasspath.extendsFrom(configurations.devBundle)
configurations.testCompileClasspath.extendsFrom(configurations.devBundle)
configurations.testRuntimeClasspath.extendsFrom(configurations.devBundle)
configurations.create('jij') { configurations.create('jij') {
transitive = true transitive = true
@@ -127,24 +130,28 @@ dependencies {
compileOnly(libs.spigot) { compileOnly(libs.spigot) {
transitive = false transitive = false
} }
add('bundle', volmLibCoordinate) {
transitive = false
}
add('devBundle', volmLibCoordinate) {
transitive = false
}
add('bundle', libs.zip) {
transitive = false
}
add('devBundle', libs.zip) {
transitive = false
}
List<Object> shared = [ List<Object> shared = [
"art.arcane:core:${irisVersion}".toString(), "art.arcane:core:${irisVersion}".toString(),
"art.arcane:spi:${irisVersion}".toString(), "art.arcane:spi:${irisVersion}".toString(),
volmLibCoordinate,
libs.paralithic, libs.paralithic,
libs.lru, libs.lru,
libs.kotlin.stdlib,
libs.kotlin.coroutines,
libs.commons.lang,
libs.commons.math3,
libs.caffeine, libs.caffeine,
libs.lz4, libs.dom4j,
libs.zip, libs.jaxen,
libs.sentry, libs.sentry
libs.oshi,
libs.byteBuddy.core,
libs.byteBuddy.agent
] ]
shared.each { Object notation -> shared.each { Object notation ->
add('bundle', notation) add('bundle', notation)
@@ -157,6 +164,10 @@ tasks.named('compileJava', JavaCompile).configure {
options.release.set(25) options.release.set(25)
} }
tasks.named('test').configure {
systemProperty('iris.moddedCommonSources', file('../modded-common/src/main/java').absolutePath)
}
loom { loom {
accessWidenerPath = file('src/main/resources/irisworldgen.accesswidener') accessWidenerPath = file('src/main/resources/irisworldgen.accesswidener')
runs { runs {
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+142
View File
@@ -0,0 +1,142 @@
[21:25:27] [Test worker/INFO]: Iris registered custom content provider 'iris_deferred_test'
[21:25:27] [Test worker/ERROR]: [worldcheck] server stop request failed
java.lang.IllegalStateException: stop request failed
at art.arcane.iris.modded.ModdedWorldCheckTest.lambda$stopRequestFailureForcesNonzeroResult$1(ModdedWorldCheckTest.java:218)
at art.arcane.iris.modded.ModdedWorldCheck.runAndRequestStop(ModdedWorldCheck.java:171)
at art.arcane.iris.modded.ModdedWorldCheckTest.stopRequestFailureForcesNonzeroResult(ModdedWorldCheckTest.java:215)
at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:104)
at java.base/java.lang.reflect.Method.invoke(Method.java:565)
at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:59)
at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12)
at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:56)
at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17)
at org.junit.runners.ParentRunner$3.evaluate(ParentRunner.java:306)
at org.junit.runners.BlockJUnit4ClassRunner$1.evaluate(BlockJUnit4ClassRunner.java:100)
at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:366)
at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:103)
at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:63)
at org.junit.runners.ParentRunner$4.run(ParentRunner.java:331)
at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:79)
at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:329)
at org.junit.runners.ParentRunner.access$100(ParentRunner.java:66)
at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:293)
at org.junit.runners.ParentRunner$3.evaluate(ParentRunner.java:306)
at org.junit.runners.ParentRunner.run(ParentRunner.java:413)
at org.junit.runner.JUnitCore.run(JUnitCore.java:137)
at org.junit.runner.JUnitCore.run(JUnitCore.java:115)
at org.gradle.api.internal.tasks.testing.junit.JUnitTestExecutor.runRequest(JUnitTestExecutor.java:175)
at org.gradle.api.internal.tasks.testing.junit.JUnitTestExecutor.accept(JUnitTestExecutor.java:84)
at org.gradle.api.internal.tasks.testing.junit.JUnitTestExecutor.accept(JUnitTestExecutor.java:47)
at org.gradle.api.internal.tasks.testing.junit.AbstractJUnitTestDefinitionProcessor.processTestDefinition(AbstractJUnitTestDefinitionProcessor.java:65)
at org.gradle.api.internal.tasks.testing.SuiteTestDefinitionProcessor.processTestDefinition(SuiteTestDefinitionProcessor.java:53)
at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:104)
at java.base/java.lang.reflect.Method.invoke(Method.java:565)
at org.gradle.internal.dispatch.MethodInvocation.invokeOn(MethodInvocation.java:77)
at org.gradle.internal.dispatch.ReflectionDispatch.dispatch(ReflectionDispatch.java:28)
at org.gradle.internal.dispatch.ReflectionDispatch.dispatch(ReflectionDispatch.java:19)
at org.gradle.internal.dispatch.ContextClassLoaderDispatch.dispatch(ContextClassLoaderDispatch.java:33)
at org.gradle.internal.dispatch.ProxyDispatchAdapter$DispatchingInvocationHandler.invoke(ProxyDispatchAdapter.java:88)
at jdk.proxy1/jdk.proxy1.$Proxy4.processTestDefinition(Unknown Source)
at org.gradle.api.internal.tasks.testing.worker.TestWorker$2.run(TestWorker.java:178)
at org.gradle.api.internal.tasks.testing.worker.TestWorker.executeAndMaintainThreadName(TestWorker.java:126)
at org.gradle.api.internal.tasks.testing.worker.TestWorker.execute(TestWorker.java:103)
at org.gradle.api.internal.tasks.testing.worker.TestWorker.execute(TestWorker.java:63)
at org.gradle.process.internal.worker.child.ActionExecutionWorker.execute(ActionExecutionWorker.java:56)
at org.gradle.process.internal.worker.child.SystemApplicationClassLoaderWorker.call(SystemApplicationClassLoaderWorker.java:122)
at org.gradle.process.internal.worker.child.SystemApplicationClassLoaderWorker.call(SystemApplicationClassLoaderWorker.java:72)
at worker.org.gradle.process.internal.worker.GradleWorkerMain.run(GradleWorkerMain.java:69)
at worker.org.gradle.process.internal.worker.GradleWorkerMain.main(GradleWorkerMain.java:74)
[21:25:27] [Test worker/ERROR]: [worldcheck] waiting for server shutdown failed
java.lang.IllegalStateException: shutdown wait failed
at art.arcane.iris.modded.ModdedWorldCheckTest.lambda$shutdownWaitFailureForcesNonzeroExit$0(ModdedWorldCheckTest.java:244)
at art.arcane.iris.modded.ModdedWorldCheck.awaitStopAndExit(ModdedWorldCheck.java:186)
at art.arcane.iris.modded.ModdedWorldCheckTest.shutdownWaitFailureForcesNonzeroExit(ModdedWorldCheckTest.java:242)
at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:104)
at java.base/java.lang.reflect.Method.invoke(Method.java:565)
at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:59)
at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12)
at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:56)
at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17)
at org.junit.runners.ParentRunner$3.evaluate(ParentRunner.java:306)
at org.junit.runners.BlockJUnit4ClassRunner$1.evaluate(BlockJUnit4ClassRunner.java:100)
at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:366)
at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:103)
at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:63)
at org.junit.runners.ParentRunner$4.run(ParentRunner.java:331)
at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:79)
at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:329)
at org.junit.runners.ParentRunner.access$100(ParentRunner.java:66)
at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:293)
at org.junit.runners.ParentRunner$3.evaluate(ParentRunner.java:306)
at org.junit.runners.ParentRunner.run(ParentRunner.java:413)
at org.junit.runner.JUnitCore.run(JUnitCore.java:137)
at org.junit.runner.JUnitCore.run(JUnitCore.java:115)
at org.gradle.api.internal.tasks.testing.junit.JUnitTestExecutor.runRequest(JUnitTestExecutor.java:175)
at org.gradle.api.internal.tasks.testing.junit.JUnitTestExecutor.accept(JUnitTestExecutor.java:84)
at org.gradle.api.internal.tasks.testing.junit.JUnitTestExecutor.accept(JUnitTestExecutor.java:47)
at org.gradle.api.internal.tasks.testing.junit.AbstractJUnitTestDefinitionProcessor.processTestDefinition(AbstractJUnitTestDefinitionProcessor.java:65)
at org.gradle.api.internal.tasks.testing.SuiteTestDefinitionProcessor.processTestDefinition(SuiteTestDefinitionProcessor.java:53)
at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:104)
at java.base/java.lang.reflect.Method.invoke(Method.java:565)
at org.gradle.internal.dispatch.MethodInvocation.invokeOn(MethodInvocation.java:77)
at org.gradle.internal.dispatch.ReflectionDispatch.dispatch(ReflectionDispatch.java:28)
at org.gradle.internal.dispatch.ReflectionDispatch.dispatch(ReflectionDispatch.java:19)
at org.gradle.internal.dispatch.ContextClassLoaderDispatch.dispatch(ContextClassLoaderDispatch.java:33)
at org.gradle.internal.dispatch.ProxyDispatchAdapter$DispatchingInvocationHandler.invoke(ProxyDispatchAdapter.java:88)
at jdk.proxy1/jdk.proxy1.$Proxy4.processTestDefinition(Unknown Source)
at org.gradle.api.internal.tasks.testing.worker.TestWorker$2.run(TestWorker.java:178)
at org.gradle.api.internal.tasks.testing.worker.TestWorker.executeAndMaintainThreadName(TestWorker.java:126)
at org.gradle.api.internal.tasks.testing.worker.TestWorker.execute(TestWorker.java:103)
at org.gradle.api.internal.tasks.testing.worker.TestWorker.execute(TestWorker.java:63)
at org.gradle.process.internal.worker.child.ActionExecutionWorker.execute(ActionExecutionWorker.java:56)
at org.gradle.process.internal.worker.child.SystemApplicationClassLoaderWorker.call(SystemApplicationClassLoaderWorker.java:122)
at org.gradle.process.internal.worker.child.SystemApplicationClassLoaderWorker.call(SystemApplicationClassLoaderWorker.java:72)
at worker.org.gradle.process.internal.worker.GradleWorkerMain.run(GradleWorkerMain.java:69)
at worker.org.gradle.process.internal.worker.GradleWorkerMain.main(GradleWorkerMain.java:74)
[21:25:27] [Test worker/ERROR]: [worldcheck] check failed
java.lang.IllegalStateException: check failed
at art.arcane.iris.modded.ModdedWorldCheckTest.lambda$thrownCheckStillRequestsStop$0(ModdedWorldCheckTest.java:204)
at art.arcane.iris.modded.ModdedWorldCheck.runAndRequestStop(ModdedWorldCheck.java:166)
at art.arcane.iris.modded.ModdedWorldCheckTest.thrownCheckStillRequestsStop(ModdedWorldCheckTest.java:202)
at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:104)
at java.base/java.lang.reflect.Method.invoke(Method.java:565)
at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:59)
at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12)
at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:56)
at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17)
at org.junit.runners.ParentRunner$3.evaluate(ParentRunner.java:306)
at org.junit.runners.BlockJUnit4ClassRunner$1.evaluate(BlockJUnit4ClassRunner.java:100)
at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:366)
at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:103)
at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:63)
at org.junit.runners.ParentRunner$4.run(ParentRunner.java:331)
at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:79)
at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:329)
at org.junit.runners.ParentRunner.access$100(ParentRunner.java:66)
at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:293)
at org.junit.runners.ParentRunner$3.evaluate(ParentRunner.java:306)
at org.junit.runners.ParentRunner.run(ParentRunner.java:413)
at org.junit.runner.JUnitCore.run(JUnitCore.java:137)
at org.junit.runner.JUnitCore.run(JUnitCore.java:115)
at org.gradle.api.internal.tasks.testing.junit.JUnitTestExecutor.runRequest(JUnitTestExecutor.java:175)
at org.gradle.api.internal.tasks.testing.junit.JUnitTestExecutor.accept(JUnitTestExecutor.java:84)
at org.gradle.api.internal.tasks.testing.junit.JUnitTestExecutor.accept(JUnitTestExecutor.java:47)
at org.gradle.api.internal.tasks.testing.junit.AbstractJUnitTestDefinitionProcessor.processTestDefinition(AbstractJUnitTestDefinitionProcessor.java:65)
at org.gradle.api.internal.tasks.testing.SuiteTestDefinitionProcessor.processTestDefinition(SuiteTestDefinitionProcessor.java:53)
at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:104)
at java.base/java.lang.reflect.Method.invoke(Method.java:565)
at org.gradle.internal.dispatch.MethodInvocation.invokeOn(MethodInvocation.java:77)
at org.gradle.internal.dispatch.ReflectionDispatch.dispatch(ReflectionDispatch.java:28)
at org.gradle.internal.dispatch.ReflectionDispatch.dispatch(ReflectionDispatch.java:19)
at org.gradle.internal.dispatch.ContextClassLoaderDispatch.dispatch(ContextClassLoaderDispatch.java:33)
at org.gradle.internal.dispatch.ProxyDispatchAdapter$DispatchingInvocationHandler.invoke(ProxyDispatchAdapter.java:88)
at jdk.proxy1/jdk.proxy1.$Proxy4.processTestDefinition(Unknown Source)
at org.gradle.api.internal.tasks.testing.worker.TestWorker$2.run(TestWorker.java:178)
at org.gradle.api.internal.tasks.testing.worker.TestWorker.executeAndMaintainThreadName(TestWorker.java:126)
at org.gradle.api.internal.tasks.testing.worker.TestWorker.execute(TestWorker.java:103)
at org.gradle.api.internal.tasks.testing.worker.TestWorker.execute(TestWorker.java:63)
at org.gradle.process.internal.worker.child.ActionExecutionWorker.execute(ActionExecutionWorker.java:56)
at org.gradle.process.internal.worker.child.SystemApplicationClassLoaderWorker.call(SystemApplicationClassLoaderWorker.java:122)
at org.gradle.process.internal.worker.child.SystemApplicationClassLoaderWorker.call(SystemApplicationClassLoaderWorker.java:72)
at worker.org.gradle.process.internal.worker.GradleWorkerMain.run(GradleWorkerMain.java:69)
at worker.org.gradle.process.internal.worker.GradleWorkerMain.main(GradleWorkerMain.java:74)
@@ -19,15 +19,18 @@
package art.arcane.iris.fabric; package art.arcane.iris.fabric;
import art.arcane.iris.modded.IrisModdedChunkGenerator; import art.arcane.iris.modded.IrisModdedChunkGenerator;
import art.arcane.iris.modded.ModdedBlockBreakHandler;
import art.arcane.iris.modded.ModdedEngineBootstrap; import art.arcane.iris.modded.ModdedEngineBootstrap;
import art.arcane.iris.modded.command.IrisModdedCommands; import art.arcane.iris.modded.command.IrisModdedCommands;
import art.arcane.iris.modded.command.ModdedWandService; import art.arcane.iris.modded.command.ModdedWandService;
import com.mojang.brigadier.CommandDispatcher; import com.mojang.brigadier.CommandDispatcher;
import net.fabricmc.api.ModInitializer; import net.fabricmc.api.ModInitializer;
import net.fabricmc.fabric.api.command.v2.CommandRegistrationCallback; import net.fabricmc.fabric.api.command.v2.CommandRegistrationCallback;
import net.fabricmc.fabric.api.event.lifecycle.v1.ServerLevelEvents;
import net.fabricmc.fabric.api.event.lifecycle.v1.ServerLifecycleEvents; import net.fabricmc.fabric.api.event.lifecycle.v1.ServerLifecycleEvents;
import net.fabricmc.fabric.api.event.lifecycle.v1.ServerTickEvents; import net.fabricmc.fabric.api.event.lifecycle.v1.ServerTickEvents;
import net.fabricmc.fabric.api.event.player.AttackBlockCallback; import net.fabricmc.fabric.api.event.player.AttackBlockCallback;
import net.fabricmc.fabric.api.event.player.PlayerBlockBreakEvents;
import net.fabricmc.fabric.api.event.player.UseBlockCallback; import net.fabricmc.fabric.api.event.player.UseBlockCallback;
import net.minecraft.commands.CommandBuildContext; import net.minecraft.commands.CommandBuildContext;
import net.minecraft.commands.CommandSourceStack; import net.minecraft.commands.CommandSourceStack;
@@ -38,10 +41,13 @@ import net.minecraft.core.Registry;
import net.minecraft.core.registries.BuiltInRegistries; import net.minecraft.core.registries.BuiltInRegistries;
import net.minecraft.resources.Identifier; import net.minecraft.resources.Identifier;
import net.minecraft.server.MinecraftServer; import net.minecraft.server.MinecraftServer;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.world.InteractionHand; import net.minecraft.world.InteractionHand;
import net.minecraft.world.InteractionResult; import net.minecraft.world.InteractionResult;
import net.minecraft.world.entity.player.Player; import net.minecraft.world.entity.player.Player;
import net.minecraft.world.level.Level; import net.minecraft.world.level.Level;
import net.minecraft.world.level.block.entity.BlockEntity;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.phys.BlockHitResult; import net.minecraft.world.phys.BlockHitResult;
public final class IrisFabricBootstrap implements ModInitializer { public final class IrisFabricBootstrap implements ModInitializer {
@@ -51,8 +57,16 @@ public final class IrisFabricBootstrap implements ModInitializer {
() -> Registry.register(BuiltInRegistries.CHUNK_GENERATOR, Identifier.fromNamespaceAndPath("irisworldgen", "iris"), IrisModdedChunkGenerator.CODEC)); () -> Registry.register(BuiltInRegistries.CHUNK_GENERATOR, Identifier.fromNamespaceAndPath("irisworldgen", "iris"), IrisModdedChunkGenerator.CODEC));
FabricProtocolNetworking.install(); FabricProtocolNetworking.install();
ServerLifecycleEvents.SERVER_STARTING.register((MinecraftServer server) -> ModdedEngineBootstrap.start(server)); ServerLifecycleEvents.SERVER_STARTING.register((MinecraftServer server) -> ModdedEngineBootstrap.start(server));
ServerLifecycleEvents.SERVER_STARTED.register((MinecraftServer server) -> ModdedEngineBootstrap.serverStarted(server));
ServerLifecycleEvents.SERVER_STOPPING.register((MinecraftServer server) -> ModdedEngineBootstrap.stop()); ServerLifecycleEvents.SERVER_STOPPING.register((MinecraftServer server) -> ModdedEngineBootstrap.stop());
ServerLevelEvents.LOAD.register((MinecraftServer server, ServerLevel level) -> ModdedEngineBootstrap.levelLoaded(level));
CommandRegistrationCallback.EVENT.register((CommandDispatcher<CommandSourceStack> dispatcher, CommandBuildContext buildContext, Commands.CommandSelection selection) -> IrisModdedCommands.register(dispatcher)); CommandRegistrationCallback.EVENT.register((CommandDispatcher<CommandSourceStack> dispatcher, CommandBuildContext buildContext, Commands.CommandSelection selection) -> IrisModdedCommands.register(dispatcher));
PlayerBlockBreakEvents.BEFORE.register((Level level, Player player, BlockPos pos, BlockState state, BlockEntity blockEntity) -> {
if (level instanceof ServerLevel serverLevel) {
ModdedBlockBreakHandler.prepare(serverLevel, pos, state);
}
return true;
});
AttackBlockCallback.EVENT.register((Player player, Level level, InteractionHand hand, BlockPos pos, Direction direction) -> AttackBlockCallback.EVENT.register((Player player, Level level, InteractionHand hand, BlockPos pos, Direction direction) ->
ModdedWandService.attackBlock(player, level, hand, pos) ? InteractionResult.SUCCESS : InteractionResult.PASS); ModdedWandService.attackBlock(player, level, hand, pos) ? InteractionResult.SUCCESS : InteractionResult.PASS);
UseBlockCallback.EVENT.register((Player player, Level level, InteractionHand hand, BlockHitResult hit) -> UseBlockCallback.EVENT.register((Player player, Level level, InteractionHand hand, BlockHitResult hit) ->
@@ -0,0 +1,57 @@
/*
* Iris is a World Generator for Minecraft 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.fabric.mixin;
import art.arcane.iris.modded.ModdedBlockBreakHandler;
import net.minecraft.core.BlockPos;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.world.entity.Entity;
import net.minecraft.world.entity.player.Player;
import net.minecraft.world.item.ItemInstance;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.level.block.Block;
import net.minecraft.world.level.block.entity.BlockEntity;
import net.minecraft.world.level.block.state.BlockState;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
import java.util.ArrayList;
import java.util.List;
@Mixin(Block.class)
public class BlockMixin {
@Inject(method = "getDrops(Lnet/minecraft/world/level/block/state/BlockState;Lnet/minecraft/server/level/ServerLevel;Lnet/minecraft/core/BlockPos;Lnet/minecraft/world/level/block/entity/BlockEntity;Lnet/minecraft/world/entity/Entity;Lnet/minecraft/world/item/ItemInstance;)Ljava/util/List;", at = @At("RETURN"), cancellable = true)
private static void iris$applyBlockDrops(BlockState state, ServerLevel level, BlockPos position, BlockEntity blockEntity,
Entity breaker, ItemInstance tool, CallbackInfoReturnable<List<ItemStack>> info) {
if (!(breaker instanceof Player)) {
return;
}
ModdedBlockBreakHandler.Result result = ModdedBlockBreakHandler.completePrepared(level, position);
if (result == null) {
return;
}
List<ItemStack> drops = result.replaceVanillaDrops()
? new ArrayList<>()
: new ArrayList<>(info.getReturnValue());
drops.addAll(result.drops());
info.setReturnValue(drops);
}
}
@@ -11,7 +11,7 @@
"license": "GPL-3.0", "license": "GPL-3.0",
"environment": "*", "environment": "*",
"accessWidener": "irisworldgen.accesswidener", "accessWidener": "irisworldgen.accesswidener",
"mixins": ["irisworldgen.mixins.json"], "mixins": ["irisworldgen.mixins.json", "irisworldgen.entity.mixins.json"],
"entrypoints": { "entrypoints": {
"main": ["art.arcane.iris.fabric.IrisFabricBootstrap"], "main": ["art.arcane.iris.fabric.IrisFabricBootstrap"],
"client": ["art.arcane.iris.fabric.IrisFabricClient"] "client": ["art.arcane.iris.fabric.IrisFabricClient"]
@@ -4,7 +4,7 @@
"package": "art.arcane.iris.fabric.mixin", "package": "art.arcane.iris.fabric.mixin",
"compatibilityLevel": "JAVA_25", "compatibilityLevel": "JAVA_25",
"mixins": [ "mixins": [
"LivingEntityMixin", "BlockMixin",
"ServerPacksSourceMixin" "ServerPacksSourceMixin"
], ],
"injectors": { "injectors": {
+42 -35
View File
@@ -22,7 +22,7 @@ import org.gradle.jvm.toolchain.JavaLanguageVersion
plugins { plugins {
id 'java' id 'java'
id 'net.minecraftforge.gradle' version '7.0.27' id 'net.minecraftforge.gradle' version '7.0.31'
alias(libs.plugins.shadow) alias(libs.plugins.shadow)
} }
@@ -30,8 +30,8 @@ Properties rootProperties = new Properties()
file('../../gradle.properties').withInputStream { InputStream stream -> rootProperties.load(stream) } file('../../gradle.properties').withInputStream { InputStream stream -> rootProperties.load(stream) }
String irisVersion = providers.gradleProperty('irisVersion').getOrElse(rootProperties.getProperty('irisVersion', '4.0.0-26.2')) String irisVersion = providers.gradleProperty('irisVersion').getOrElse(rootProperties.getProperty('irisVersion', '4.0.0-26.2'))
String minecraftVersion = providers.gradleProperty('minecraftVersion').getOrElse(rootProperties.getProperty('minecraftVersion', '26.2')) String minecraftVersion = providers.gradleProperty('minecraftVersion').getOrElse(rootProperties.getProperty('minecraftVersion', '26.2'))
String forgeVersion = providers.gradleProperty('forgeVersion').getOrElse(rootProperties.getProperty('forgeVersion', '26.2-65.0.0')) String forgeVersion = providers.gradleProperty('forgeVersion').getOrElse(rootProperties.getProperty('forgeVersion', '26.2-65.0.4'))
String volmLibCoordinate = providers.gradleProperty('volmLibCoordinate').getOrElse(rootProperties.getProperty('volmLibCoordinate', 'com.github.VolmitSoftware:VolmLib:cdf7e8bb880199734dacd386a6616bda799fcde1')) String volmLibCoordinate = providers.gradleProperty('volmLibCoordinate').getOrElse(rootProperties.getProperty('volmLibCoordinate', 'com.github.VolmitSoftware:VolmLib:4c38988b344792a79f925f57f8a675bc85fc1bed'))
Closure<String> loaderDisplayVersion = { String loaderVersion -> Closure<String> loaderDisplayVersion = { String loaderVersion ->
String coordinatePrefix = "${minecraftVersion}-" String coordinatePrefix = "${minecraftVersion}-"
if (loaderVersion.startsWith(coordinatePrefix)) { if (loaderVersion.startsWith(coordinatePrefix)) {
@@ -57,12 +57,18 @@ sourceSets {
main { main {
java { java {
srcDir '../modded-common/src/main/java' srcDir '../modded-common/src/main/java'
srcDir '../minecraft-common/src/main/java'
srcDir '../client-common/src/main/java' srcDir '../client-common/src/main/java'
} }
resources { resources {
srcDir '../modded-common/src/main/resources' srcDir '../modded-common/src/main/resources'
} }
} }
test {
java {
srcDir '../modded-common/src/test/java'
}
}
} }
repositories { repositories {
@@ -88,11 +94,6 @@ configurations {
canBeConsumed = false canBeConsumed = false
canBeResolved = true canBeResolved = true
} }
commonsLangRaw {
canBeConsumed = false
canBeResolved = true
transitive = false
}
} }
configurations.named('bundle').configure { configurations.named('bundle').configure {
@@ -116,8 +117,10 @@ configurations.named('runBundle').configure {
configurations.compileClasspath.extendsFrom(configurations.devBundle) configurations.compileClasspath.extendsFrom(configurations.devBundle)
configurations.runtimeClasspath.extendsFrom(configurations.runBundle) configurations.runtimeClasspath.extendsFrom(configurations.runBundle)
configurations.testCompileClasspath.extendsFrom(configurations.compileClasspath, configurations.devBundle)
configurations.testRuntimeClasspath.extendsFrom(configurations.runtimeClasspath, configurations.runBundle)
['compileClasspath', 'runtimeClasspath'].each { String name -> ['compileClasspath', 'runtimeClasspath', 'testCompileClasspath', 'testRuntimeClasspath'].each { String name ->
configurations.named(name).configure { configurations.named(name).configure {
resolutionStrategy.capabilitiesResolution.withCapability('org.lz4:lz4-java') { resolutionStrategy.capabilitiesResolution.withCapability('org.lz4:lz4-java') {
selectHighestVersion() selectHighestVersion()
@@ -127,48 +130,45 @@ configurations.runtimeClasspath.extendsFrom(configurations.runBundle)
dependencies { dependencies {
implementation minecraft.dependency("net.minecraftforge:forge:${forgeVersion}") implementation minecraft.dependency("net.minecraftforge:forge:${forgeVersion}")
testImplementation('junit:junit:4.13.2')
compileOnly('org.slf4j:slf4j-api:2.0.17') compileOnly('org.slf4j:slf4j-api:2.0.17')
compileOnly(libs.spigot) { compileOnly(libs.spigot) {
transitive = false transitive = false
} }
add('bundle', volmLibCoordinate) {
transitive = false
}
add('devBundle', volmLibCoordinate) {
transitive = false
}
add('runBundle', volmLibCoordinate) {
transitive = false
}
add('bundle', libs.zip) {
transitive = false
}
add('devBundle', libs.zip) {
transitive = false
}
add('runBundle', libs.zip) {
transitive = false
}
List<Object> shared = [ List<Object> shared = [
"art.arcane:core:${irisVersion}".toString(), "art.arcane:core:${irisVersion}".toString(),
"art.arcane:spi:${irisVersion}".toString(), "art.arcane:spi:${irisVersion}".toString(),
volmLibCoordinate,
libs.paralithic, libs.paralithic,
libs.lru, libs.lru,
libs.kotlin.stdlib,
libs.kotlin.coroutines,
libs.commons.math3,
libs.caffeine, libs.caffeine,
libs.lz4, libs.dom4j,
libs.zip, libs.jaxen,
libs.sentry, libs.sentry
libs.oshi,
libs.byteBuddy.core,
libs.byteBuddy.agent
] ]
shared.each { Object notation -> shared.each { Object notation ->
add('bundle', notation) add('bundle', notation)
add('devBundle', notation) add('devBundle', notation)
add('runBundle', notation) add('runBundle', notation)
} }
add('bundle', libs.commons.lang)
add('devBundle', libs.commons.lang)
add('commonsLangRaw', libs.commons.lang)
}
TaskProvider<Jar> sanitizedCommonsLang = tasks.register('sanitizedCommonsLang', Jar) {
archiveBaseName.set('commons-lang-sanitized')
destinationDirectory.set(layout.buildDirectory.dir('sanitized'))
from({ zipTree(configurations.named('commonsLangRaw').get().singleFile) })
exclude('org/apache/commons/lang/enum/**')
duplicatesStrategy = DuplicatesStrategy.EXCLUDE
}
dependencies {
add('runBundle', files(sanitizedCommonsLang))
} }
minecraft { minecraft {
@@ -193,7 +193,7 @@ minecraft {
systemProperty('iris.worldcheck', worldCheck) systemProperty('iris.worldcheck', worldCheck)
} }
jvmArgs('-Xmx8G') jvmArgs('-Xmx8G')
args('--nogui') args('--mixin.config', 'irisworldgen.entity.mixins.json', '--nogui')
} }
} }
} }
@@ -203,6 +203,10 @@ tasks.named('compileJava', JavaCompile).configure {
options.release.set(25) options.release.set(25)
} }
tasks.named('test').configure {
systemProperty('iris.moddedCommonSources', file('../modded-common/src/main/java').absolutePath)
}
processResources { processResources {
inputs.property('version', project.version) inputs.property('version', project.version)
inputs.property('minecraftVersion', minecraftVersion) inputs.property('minecraftVersion', minecraftVersion)
@@ -215,6 +219,9 @@ tasks.named('shadowJar', ShadowJar).configure {
doFirst { doFirst {
delete(layout.buildDirectory.file("libs/Iris-${project.version}+mc${minecraftVersion}-forge.jar")) delete(layout.buildDirectory.file("libs/Iris-${project.version}+mc${minecraftVersion}-forge.jar"))
} }
manifest {
attributes('MixinConfigs': 'irisworldgen.entity.mixins.json')
}
archiveFileName.set(irisArtifactName('Forge', "${minecraftVersion}+${loaderDisplayVersion(forgeVersion)}")) archiveFileName.set(irisArtifactName('Forge', "${minecraftVersion}+${loaderDisplayVersion(forgeVersion)}"))
configurations = [project.configurations.named('bundle').get()] configurations = [project.configurations.named('bundle').get()]
from(sourceSets.main.output) from(sourceSets.main.output)
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+145
View File
@@ -0,0 +1,145 @@
[13Jul2026 21:25:26.811] [Test worker/DEBUG] [io.netty.util.internal.logging.InternalLoggerFactory/]: Using SLF4J as the default logging framework
[13Jul2026 21:25:26.813] [Test worker/DEBUG] [io.netty.util.ResourceLeakDetector/]: -Dio.netty.leakDetection.level: simple
[13Jul2026 21:25:26.813] [Test worker/DEBUG] [io.netty.util.ResourceLeakDetector/]: -Dio.netty.leakDetection.targetRecords: 4
[13Jul2026 21:25:28.681] [Test worker/INFO] [Iris/]: Iris registered custom content provider 'iris_deferred_test'
[13Jul2026 21:25:28.758] [Test worker/ERROR] [Iris/]: [worldcheck] server stop request failed
java.lang.IllegalStateException: stop request failed
at art.arcane.iris.modded.ModdedWorldCheckTest.lambda$stopRequestFailureForcesNonzeroResult$1(ModdedWorldCheckTest.java:218) ~[test/:?]
at art.arcane.iris.modded.ModdedWorldCheck.runAndRequestStop(ModdedWorldCheck.java:171) ~[main/:?]
at art.arcane.iris.modded.ModdedWorldCheckTest.stopRequestFailureForcesNonzeroResult(ModdedWorldCheckTest.java:215) ~[test/:?]
at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:104) ~[?:?]
at java.base/java.lang.reflect.Method.invoke(Method.java:565) ~[?:?]
at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:59) ~[junit-4.13.2.jar:4.13.2]
at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12) ~[junit-4.13.2.jar:4.13.2]
at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:56) ~[junit-4.13.2.jar:4.13.2]
at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17) ~[junit-4.13.2.jar:4.13.2]
at org.junit.runners.ParentRunner$3.evaluate(ParentRunner.java:306) ~[junit-4.13.2.jar:4.13.2]
at org.junit.runners.BlockJUnit4ClassRunner$1.evaluate(BlockJUnit4ClassRunner.java:100) ~[junit-4.13.2.jar:4.13.2]
at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:366) ~[junit-4.13.2.jar:4.13.2]
at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:103) ~[junit-4.13.2.jar:4.13.2]
at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:63) ~[junit-4.13.2.jar:4.13.2]
at org.junit.runners.ParentRunner$4.run(ParentRunner.java:331) ~[junit-4.13.2.jar:4.13.2]
at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:79) ~[junit-4.13.2.jar:4.13.2]
at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:329) ~[junit-4.13.2.jar:4.13.2]
at org.junit.runners.ParentRunner.access$100(ParentRunner.java:66) ~[junit-4.13.2.jar:4.13.2]
at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:293) ~[junit-4.13.2.jar:4.13.2]
at org.junit.runners.ParentRunner$3.evaluate(ParentRunner.java:306) ~[junit-4.13.2.jar:4.13.2]
at org.junit.runners.ParentRunner.run(ParentRunner.java:413) ~[junit-4.13.2.jar:4.13.2]
at org.junit.runner.JUnitCore.run(JUnitCore.java:137) ~[junit-4.13.2.jar:4.13.2]
at org.junit.runner.JUnitCore.run(JUnitCore.java:115) ~[junit-4.13.2.jar:4.13.2]
at org.gradle.api.internal.tasks.testing.junit.JUnitTestExecutor.runRequest(JUnitTestExecutor.java:175) ~[?:?]
at org.gradle.api.internal.tasks.testing.junit.JUnitTestExecutor.accept(JUnitTestExecutor.java:84) ~[?:?]
at org.gradle.api.internal.tasks.testing.junit.JUnitTestExecutor.accept(JUnitTestExecutor.java:47) ~[?:?]
at org.gradle.api.internal.tasks.testing.junit.AbstractJUnitTestDefinitionProcessor.processTestDefinition(AbstractJUnitTestDefinitionProcessor.java:65) ~[?:?]
at org.gradle.api.internal.tasks.testing.SuiteTestDefinitionProcessor.processTestDefinition(SuiteTestDefinitionProcessor.java:53) ~[?:?]
at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:104) ~[?:?]
at java.base/java.lang.reflect.Method.invoke(Method.java:565) ~[?:?]
at org.gradle.internal.dispatch.MethodInvocation.invokeOn(MethodInvocation.java:77) ~[?:?]
at org.gradle.internal.dispatch.ReflectionDispatch.dispatch(ReflectionDispatch.java:28) ~[?:?]
at org.gradle.internal.dispatch.ReflectionDispatch.dispatch(ReflectionDispatch.java:19) ~[?:?]
at org.gradle.internal.dispatch.ContextClassLoaderDispatch.dispatch(ContextClassLoaderDispatch.java:33) ~[?:?]
at org.gradle.internal.dispatch.ProxyDispatchAdapter$DispatchingInvocationHandler.invoke(ProxyDispatchAdapter.java:88) ~[?:?]
at jdk.proxy1/jdk.proxy1.$Proxy4.processTestDefinition(Unknown Source) ~[?:?]
at org.gradle.api.internal.tasks.testing.worker.TestWorker$2.run(TestWorker.java:178) ~[?:?]
at org.gradle.api.internal.tasks.testing.worker.TestWorker.executeAndMaintainThreadName(TestWorker.java:126) ~[?:?]
at org.gradle.api.internal.tasks.testing.worker.TestWorker.execute(TestWorker.java:103) ~[?:?]
at org.gradle.api.internal.tasks.testing.worker.TestWorker.execute(TestWorker.java:63) ~[?:?]
at org.gradle.process.internal.worker.child.ActionExecutionWorker.execute(ActionExecutionWorker.java:56) ~[?:?]
at org.gradle.process.internal.worker.child.SystemApplicationClassLoaderWorker.call(SystemApplicationClassLoaderWorker.java:122) ~[?:?]
at org.gradle.process.internal.worker.child.SystemApplicationClassLoaderWorker.call(SystemApplicationClassLoaderWorker.java:72) ~[?:?]
at worker.org.gradle.process.internal.worker.GradleWorkerMain.run(GradleWorkerMain.java:69) [gradle-worker.jar:?]
at worker.org.gradle.process.internal.worker.GradleWorkerMain.main(GradleWorkerMain.java:74) [gradle-worker.jar:?]
[13Jul2026 21:25:28.765] [Test worker/ERROR] [Iris/]: [worldcheck] waiting for server shutdown failed
java.lang.IllegalStateException: shutdown wait failed
at art.arcane.iris.modded.ModdedWorldCheckTest.lambda$shutdownWaitFailureForcesNonzeroExit$0(ModdedWorldCheckTest.java:244) ~[test/:?]
at art.arcane.iris.modded.ModdedWorldCheck.awaitStopAndExit(ModdedWorldCheck.java:186) ~[main/:?]
at art.arcane.iris.modded.ModdedWorldCheckTest.shutdownWaitFailureForcesNonzeroExit(ModdedWorldCheckTest.java:242) ~[test/:?]
at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:104) ~[?:?]
at java.base/java.lang.reflect.Method.invoke(Method.java:565) ~[?:?]
at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:59) ~[junit-4.13.2.jar:4.13.2]
at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12) ~[junit-4.13.2.jar:4.13.2]
at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:56) ~[junit-4.13.2.jar:4.13.2]
at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17) ~[junit-4.13.2.jar:4.13.2]
at org.junit.runners.ParentRunner$3.evaluate(ParentRunner.java:306) ~[junit-4.13.2.jar:4.13.2]
at org.junit.runners.BlockJUnit4ClassRunner$1.evaluate(BlockJUnit4ClassRunner.java:100) ~[junit-4.13.2.jar:4.13.2]
at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:366) ~[junit-4.13.2.jar:4.13.2]
at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:103) ~[junit-4.13.2.jar:4.13.2]
at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:63) ~[junit-4.13.2.jar:4.13.2]
at org.junit.runners.ParentRunner$4.run(ParentRunner.java:331) ~[junit-4.13.2.jar:4.13.2]
at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:79) ~[junit-4.13.2.jar:4.13.2]
at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:329) ~[junit-4.13.2.jar:4.13.2]
at org.junit.runners.ParentRunner.access$100(ParentRunner.java:66) ~[junit-4.13.2.jar:4.13.2]
at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:293) ~[junit-4.13.2.jar:4.13.2]
at org.junit.runners.ParentRunner$3.evaluate(ParentRunner.java:306) ~[junit-4.13.2.jar:4.13.2]
at org.junit.runners.ParentRunner.run(ParentRunner.java:413) ~[junit-4.13.2.jar:4.13.2]
at org.junit.runner.JUnitCore.run(JUnitCore.java:137) ~[junit-4.13.2.jar:4.13.2]
at org.junit.runner.JUnitCore.run(JUnitCore.java:115) ~[junit-4.13.2.jar:4.13.2]
at org.gradle.api.internal.tasks.testing.junit.JUnitTestExecutor.runRequest(JUnitTestExecutor.java:175) ~[?:?]
at org.gradle.api.internal.tasks.testing.junit.JUnitTestExecutor.accept(JUnitTestExecutor.java:84) ~[?:?]
at org.gradle.api.internal.tasks.testing.junit.JUnitTestExecutor.accept(JUnitTestExecutor.java:47) ~[?:?]
at org.gradle.api.internal.tasks.testing.junit.AbstractJUnitTestDefinitionProcessor.processTestDefinition(AbstractJUnitTestDefinitionProcessor.java:65) ~[?:?]
at org.gradle.api.internal.tasks.testing.SuiteTestDefinitionProcessor.processTestDefinition(SuiteTestDefinitionProcessor.java:53) ~[?:?]
at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:104) ~[?:?]
at java.base/java.lang.reflect.Method.invoke(Method.java:565) ~[?:?]
at org.gradle.internal.dispatch.MethodInvocation.invokeOn(MethodInvocation.java:77) ~[?:?]
at org.gradle.internal.dispatch.ReflectionDispatch.dispatch(ReflectionDispatch.java:28) ~[?:?]
at org.gradle.internal.dispatch.ReflectionDispatch.dispatch(ReflectionDispatch.java:19) ~[?:?]
at org.gradle.internal.dispatch.ContextClassLoaderDispatch.dispatch(ContextClassLoaderDispatch.java:33) ~[?:?]
at org.gradle.internal.dispatch.ProxyDispatchAdapter$DispatchingInvocationHandler.invoke(ProxyDispatchAdapter.java:88) ~[?:?]
at jdk.proxy1/jdk.proxy1.$Proxy4.processTestDefinition(Unknown Source) ~[?:?]
at org.gradle.api.internal.tasks.testing.worker.TestWorker$2.run(TestWorker.java:178) ~[?:?]
at org.gradle.api.internal.tasks.testing.worker.TestWorker.executeAndMaintainThreadName(TestWorker.java:126) ~[?:?]
at org.gradle.api.internal.tasks.testing.worker.TestWorker.execute(TestWorker.java:103) ~[?:?]
at org.gradle.api.internal.tasks.testing.worker.TestWorker.execute(TestWorker.java:63) ~[?:?]
at org.gradle.process.internal.worker.child.ActionExecutionWorker.execute(ActionExecutionWorker.java:56) ~[?:?]
at org.gradle.process.internal.worker.child.SystemApplicationClassLoaderWorker.call(SystemApplicationClassLoaderWorker.java:122) ~[?:?]
at org.gradle.process.internal.worker.child.SystemApplicationClassLoaderWorker.call(SystemApplicationClassLoaderWorker.java:72) ~[?:?]
at worker.org.gradle.process.internal.worker.GradleWorkerMain.run(GradleWorkerMain.java:69) [gradle-worker.jar:?]
at worker.org.gradle.process.internal.worker.GradleWorkerMain.main(GradleWorkerMain.java:74) [gradle-worker.jar:?]
[13Jul2026 21:25:28.769] [Test worker/ERROR] [Iris/]: [worldcheck] check failed
java.lang.IllegalStateException: check failed
at art.arcane.iris.modded.ModdedWorldCheckTest.lambda$thrownCheckStillRequestsStop$0(ModdedWorldCheckTest.java:204) ~[test/:?]
at art.arcane.iris.modded.ModdedWorldCheck.runAndRequestStop(ModdedWorldCheck.java:166) ~[main/:?]
at art.arcane.iris.modded.ModdedWorldCheckTest.thrownCheckStillRequestsStop(ModdedWorldCheckTest.java:202) ~[test/:?]
at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:104) ~[?:?]
at java.base/java.lang.reflect.Method.invoke(Method.java:565) ~[?:?]
at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:59) ~[junit-4.13.2.jar:4.13.2]
at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12) ~[junit-4.13.2.jar:4.13.2]
at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:56) ~[junit-4.13.2.jar:4.13.2]
at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17) ~[junit-4.13.2.jar:4.13.2]
at org.junit.runners.ParentRunner$3.evaluate(ParentRunner.java:306) ~[junit-4.13.2.jar:4.13.2]
at org.junit.runners.BlockJUnit4ClassRunner$1.evaluate(BlockJUnit4ClassRunner.java:100) ~[junit-4.13.2.jar:4.13.2]
at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:366) ~[junit-4.13.2.jar:4.13.2]
at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:103) ~[junit-4.13.2.jar:4.13.2]
at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:63) ~[junit-4.13.2.jar:4.13.2]
at org.junit.runners.ParentRunner$4.run(ParentRunner.java:331) ~[junit-4.13.2.jar:4.13.2]
at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:79) ~[junit-4.13.2.jar:4.13.2]
at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:329) ~[junit-4.13.2.jar:4.13.2]
at org.junit.runners.ParentRunner.access$100(ParentRunner.java:66) ~[junit-4.13.2.jar:4.13.2]
at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:293) ~[junit-4.13.2.jar:4.13.2]
at org.junit.runners.ParentRunner$3.evaluate(ParentRunner.java:306) ~[junit-4.13.2.jar:4.13.2]
at org.junit.runners.ParentRunner.run(ParentRunner.java:413) ~[junit-4.13.2.jar:4.13.2]
at org.junit.runner.JUnitCore.run(JUnitCore.java:137) ~[junit-4.13.2.jar:4.13.2]
at org.junit.runner.JUnitCore.run(JUnitCore.java:115) ~[junit-4.13.2.jar:4.13.2]
at org.gradle.api.internal.tasks.testing.junit.JUnitTestExecutor.runRequest(JUnitTestExecutor.java:175) ~[?:?]
at org.gradle.api.internal.tasks.testing.junit.JUnitTestExecutor.accept(JUnitTestExecutor.java:84) ~[?:?]
at org.gradle.api.internal.tasks.testing.junit.JUnitTestExecutor.accept(JUnitTestExecutor.java:47) ~[?:?]
at org.gradle.api.internal.tasks.testing.junit.AbstractJUnitTestDefinitionProcessor.processTestDefinition(AbstractJUnitTestDefinitionProcessor.java:65) ~[?:?]
at org.gradle.api.internal.tasks.testing.SuiteTestDefinitionProcessor.processTestDefinition(SuiteTestDefinitionProcessor.java:53) ~[?:?]
at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:104) ~[?:?]
at java.base/java.lang.reflect.Method.invoke(Method.java:565) ~[?:?]
at org.gradle.internal.dispatch.MethodInvocation.invokeOn(MethodInvocation.java:77) ~[?:?]
at org.gradle.internal.dispatch.ReflectionDispatch.dispatch(ReflectionDispatch.java:28) ~[?:?]
at org.gradle.internal.dispatch.ReflectionDispatch.dispatch(ReflectionDispatch.java:19) ~[?:?]
at org.gradle.internal.dispatch.ContextClassLoaderDispatch.dispatch(ContextClassLoaderDispatch.java:33) ~[?:?]
at org.gradle.internal.dispatch.ProxyDispatchAdapter$DispatchingInvocationHandler.invoke(ProxyDispatchAdapter.java:88) ~[?:?]
at jdk.proxy1/jdk.proxy1.$Proxy4.processTestDefinition(Unknown Source) ~[?:?]
at org.gradle.api.internal.tasks.testing.worker.TestWorker$2.run(TestWorker.java:178) ~[?:?]
at org.gradle.api.internal.tasks.testing.worker.TestWorker.executeAndMaintainThreadName(TestWorker.java:126) ~[?:?]
at org.gradle.api.internal.tasks.testing.worker.TestWorker.execute(TestWorker.java:103) ~[?:?]
at org.gradle.api.internal.tasks.testing.worker.TestWorker.execute(TestWorker.java:63) ~[?:?]
at org.gradle.process.internal.worker.child.ActionExecutionWorker.execute(ActionExecutionWorker.java:56) ~[?:?]
at org.gradle.process.internal.worker.child.SystemApplicationClassLoaderWorker.call(SystemApplicationClassLoaderWorker.java:122) ~[?:?]
at org.gradle.process.internal.worker.child.SystemApplicationClassLoaderWorker.call(SystemApplicationClassLoaderWorker.java:72) ~[?:?]
at worker.org.gradle.process.internal.worker.GradleWorkerMain.run(GradleWorkerMain.java:69) [gradle-worker.jar:?]
at worker.org.gradle.process.internal.worker.GradleWorkerMain.main(GradleWorkerMain.java:74) [gradle-worker.jar:?]
+142
View File
@@ -0,0 +1,142 @@
[13Jul2026 21:25:28.681] [Test worker/INFO] [Iris/]: Iris registered custom content provider 'iris_deferred_test'
[13Jul2026 21:25:28.758] [Test worker/ERROR] [Iris/]: [worldcheck] server stop request failed
java.lang.IllegalStateException: stop request failed
at art.arcane.iris.modded.ModdedWorldCheckTest.lambda$stopRequestFailureForcesNonzeroResult$1(ModdedWorldCheckTest.java:218) ~[test/:?]
at art.arcane.iris.modded.ModdedWorldCheck.runAndRequestStop(ModdedWorldCheck.java:171) ~[main/:?]
at art.arcane.iris.modded.ModdedWorldCheckTest.stopRequestFailureForcesNonzeroResult(ModdedWorldCheckTest.java:215) ~[test/:?]
at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:104) ~[?:?]
at java.base/java.lang.reflect.Method.invoke(Method.java:565) ~[?:?]
at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:59) ~[junit-4.13.2.jar:4.13.2]
at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12) ~[junit-4.13.2.jar:4.13.2]
at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:56) ~[junit-4.13.2.jar:4.13.2]
at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17) ~[junit-4.13.2.jar:4.13.2]
at org.junit.runners.ParentRunner$3.evaluate(ParentRunner.java:306) ~[junit-4.13.2.jar:4.13.2]
at org.junit.runners.BlockJUnit4ClassRunner$1.evaluate(BlockJUnit4ClassRunner.java:100) ~[junit-4.13.2.jar:4.13.2]
at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:366) ~[junit-4.13.2.jar:4.13.2]
at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:103) ~[junit-4.13.2.jar:4.13.2]
at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:63) ~[junit-4.13.2.jar:4.13.2]
at org.junit.runners.ParentRunner$4.run(ParentRunner.java:331) ~[junit-4.13.2.jar:4.13.2]
at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:79) ~[junit-4.13.2.jar:4.13.2]
at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:329) ~[junit-4.13.2.jar:4.13.2]
at org.junit.runners.ParentRunner.access$100(ParentRunner.java:66) ~[junit-4.13.2.jar:4.13.2]
at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:293) ~[junit-4.13.2.jar:4.13.2]
at org.junit.runners.ParentRunner$3.evaluate(ParentRunner.java:306) ~[junit-4.13.2.jar:4.13.2]
at org.junit.runners.ParentRunner.run(ParentRunner.java:413) ~[junit-4.13.2.jar:4.13.2]
at org.junit.runner.JUnitCore.run(JUnitCore.java:137) ~[junit-4.13.2.jar:4.13.2]
at org.junit.runner.JUnitCore.run(JUnitCore.java:115) ~[junit-4.13.2.jar:4.13.2]
at org.gradle.api.internal.tasks.testing.junit.JUnitTestExecutor.runRequest(JUnitTestExecutor.java:175) ~[?:?]
at org.gradle.api.internal.tasks.testing.junit.JUnitTestExecutor.accept(JUnitTestExecutor.java:84) ~[?:?]
at org.gradle.api.internal.tasks.testing.junit.JUnitTestExecutor.accept(JUnitTestExecutor.java:47) ~[?:?]
at org.gradle.api.internal.tasks.testing.junit.AbstractJUnitTestDefinitionProcessor.processTestDefinition(AbstractJUnitTestDefinitionProcessor.java:65) ~[?:?]
at org.gradle.api.internal.tasks.testing.SuiteTestDefinitionProcessor.processTestDefinition(SuiteTestDefinitionProcessor.java:53) ~[?:?]
at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:104) ~[?:?]
at java.base/java.lang.reflect.Method.invoke(Method.java:565) ~[?:?]
at org.gradle.internal.dispatch.MethodInvocation.invokeOn(MethodInvocation.java:77) ~[?:?]
at org.gradle.internal.dispatch.ReflectionDispatch.dispatch(ReflectionDispatch.java:28) ~[?:?]
at org.gradle.internal.dispatch.ReflectionDispatch.dispatch(ReflectionDispatch.java:19) ~[?:?]
at org.gradle.internal.dispatch.ContextClassLoaderDispatch.dispatch(ContextClassLoaderDispatch.java:33) ~[?:?]
at org.gradle.internal.dispatch.ProxyDispatchAdapter$DispatchingInvocationHandler.invoke(ProxyDispatchAdapter.java:88) ~[?:?]
at jdk.proxy1/jdk.proxy1.$Proxy4.processTestDefinition(Unknown Source) ~[?:?]
at org.gradle.api.internal.tasks.testing.worker.TestWorker$2.run(TestWorker.java:178) ~[?:?]
at org.gradle.api.internal.tasks.testing.worker.TestWorker.executeAndMaintainThreadName(TestWorker.java:126) ~[?:?]
at org.gradle.api.internal.tasks.testing.worker.TestWorker.execute(TestWorker.java:103) ~[?:?]
at org.gradle.api.internal.tasks.testing.worker.TestWorker.execute(TestWorker.java:63) ~[?:?]
at org.gradle.process.internal.worker.child.ActionExecutionWorker.execute(ActionExecutionWorker.java:56) ~[?:?]
at org.gradle.process.internal.worker.child.SystemApplicationClassLoaderWorker.call(SystemApplicationClassLoaderWorker.java:122) ~[?:?]
at org.gradle.process.internal.worker.child.SystemApplicationClassLoaderWorker.call(SystemApplicationClassLoaderWorker.java:72) ~[?:?]
at worker.org.gradle.process.internal.worker.GradleWorkerMain.run(GradleWorkerMain.java:69) [gradle-worker.jar:?]
at worker.org.gradle.process.internal.worker.GradleWorkerMain.main(GradleWorkerMain.java:74) [gradle-worker.jar:?]
[13Jul2026 21:25:28.765] [Test worker/ERROR] [Iris/]: [worldcheck] waiting for server shutdown failed
java.lang.IllegalStateException: shutdown wait failed
at art.arcane.iris.modded.ModdedWorldCheckTest.lambda$shutdownWaitFailureForcesNonzeroExit$0(ModdedWorldCheckTest.java:244) ~[test/:?]
at art.arcane.iris.modded.ModdedWorldCheck.awaitStopAndExit(ModdedWorldCheck.java:186) ~[main/:?]
at art.arcane.iris.modded.ModdedWorldCheckTest.shutdownWaitFailureForcesNonzeroExit(ModdedWorldCheckTest.java:242) ~[test/:?]
at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:104) ~[?:?]
at java.base/java.lang.reflect.Method.invoke(Method.java:565) ~[?:?]
at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:59) ~[junit-4.13.2.jar:4.13.2]
at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12) ~[junit-4.13.2.jar:4.13.2]
at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:56) ~[junit-4.13.2.jar:4.13.2]
at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17) ~[junit-4.13.2.jar:4.13.2]
at org.junit.runners.ParentRunner$3.evaluate(ParentRunner.java:306) ~[junit-4.13.2.jar:4.13.2]
at org.junit.runners.BlockJUnit4ClassRunner$1.evaluate(BlockJUnit4ClassRunner.java:100) ~[junit-4.13.2.jar:4.13.2]
at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:366) ~[junit-4.13.2.jar:4.13.2]
at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:103) ~[junit-4.13.2.jar:4.13.2]
at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:63) ~[junit-4.13.2.jar:4.13.2]
at org.junit.runners.ParentRunner$4.run(ParentRunner.java:331) ~[junit-4.13.2.jar:4.13.2]
at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:79) ~[junit-4.13.2.jar:4.13.2]
at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:329) ~[junit-4.13.2.jar:4.13.2]
at org.junit.runners.ParentRunner.access$100(ParentRunner.java:66) ~[junit-4.13.2.jar:4.13.2]
at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:293) ~[junit-4.13.2.jar:4.13.2]
at org.junit.runners.ParentRunner$3.evaluate(ParentRunner.java:306) ~[junit-4.13.2.jar:4.13.2]
at org.junit.runners.ParentRunner.run(ParentRunner.java:413) ~[junit-4.13.2.jar:4.13.2]
at org.junit.runner.JUnitCore.run(JUnitCore.java:137) ~[junit-4.13.2.jar:4.13.2]
at org.junit.runner.JUnitCore.run(JUnitCore.java:115) ~[junit-4.13.2.jar:4.13.2]
at org.gradle.api.internal.tasks.testing.junit.JUnitTestExecutor.runRequest(JUnitTestExecutor.java:175) ~[?:?]
at org.gradle.api.internal.tasks.testing.junit.JUnitTestExecutor.accept(JUnitTestExecutor.java:84) ~[?:?]
at org.gradle.api.internal.tasks.testing.junit.JUnitTestExecutor.accept(JUnitTestExecutor.java:47) ~[?:?]
at org.gradle.api.internal.tasks.testing.junit.AbstractJUnitTestDefinitionProcessor.processTestDefinition(AbstractJUnitTestDefinitionProcessor.java:65) ~[?:?]
at org.gradle.api.internal.tasks.testing.SuiteTestDefinitionProcessor.processTestDefinition(SuiteTestDefinitionProcessor.java:53) ~[?:?]
at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:104) ~[?:?]
at java.base/java.lang.reflect.Method.invoke(Method.java:565) ~[?:?]
at org.gradle.internal.dispatch.MethodInvocation.invokeOn(MethodInvocation.java:77) ~[?:?]
at org.gradle.internal.dispatch.ReflectionDispatch.dispatch(ReflectionDispatch.java:28) ~[?:?]
at org.gradle.internal.dispatch.ReflectionDispatch.dispatch(ReflectionDispatch.java:19) ~[?:?]
at org.gradle.internal.dispatch.ContextClassLoaderDispatch.dispatch(ContextClassLoaderDispatch.java:33) ~[?:?]
at org.gradle.internal.dispatch.ProxyDispatchAdapter$DispatchingInvocationHandler.invoke(ProxyDispatchAdapter.java:88) ~[?:?]
at jdk.proxy1/jdk.proxy1.$Proxy4.processTestDefinition(Unknown Source) ~[?:?]
at org.gradle.api.internal.tasks.testing.worker.TestWorker$2.run(TestWorker.java:178) ~[?:?]
at org.gradle.api.internal.tasks.testing.worker.TestWorker.executeAndMaintainThreadName(TestWorker.java:126) ~[?:?]
at org.gradle.api.internal.tasks.testing.worker.TestWorker.execute(TestWorker.java:103) ~[?:?]
at org.gradle.api.internal.tasks.testing.worker.TestWorker.execute(TestWorker.java:63) ~[?:?]
at org.gradle.process.internal.worker.child.ActionExecutionWorker.execute(ActionExecutionWorker.java:56) ~[?:?]
at org.gradle.process.internal.worker.child.SystemApplicationClassLoaderWorker.call(SystemApplicationClassLoaderWorker.java:122) ~[?:?]
at org.gradle.process.internal.worker.child.SystemApplicationClassLoaderWorker.call(SystemApplicationClassLoaderWorker.java:72) ~[?:?]
at worker.org.gradle.process.internal.worker.GradleWorkerMain.run(GradleWorkerMain.java:69) [gradle-worker.jar:?]
at worker.org.gradle.process.internal.worker.GradleWorkerMain.main(GradleWorkerMain.java:74) [gradle-worker.jar:?]
[13Jul2026 21:25:28.769] [Test worker/ERROR] [Iris/]: [worldcheck] check failed
java.lang.IllegalStateException: check failed
at art.arcane.iris.modded.ModdedWorldCheckTest.lambda$thrownCheckStillRequestsStop$0(ModdedWorldCheckTest.java:204) ~[test/:?]
at art.arcane.iris.modded.ModdedWorldCheck.runAndRequestStop(ModdedWorldCheck.java:166) ~[main/:?]
at art.arcane.iris.modded.ModdedWorldCheckTest.thrownCheckStillRequestsStop(ModdedWorldCheckTest.java:202) ~[test/:?]
at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:104) ~[?:?]
at java.base/java.lang.reflect.Method.invoke(Method.java:565) ~[?:?]
at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:59) ~[junit-4.13.2.jar:4.13.2]
at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12) ~[junit-4.13.2.jar:4.13.2]
at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:56) ~[junit-4.13.2.jar:4.13.2]
at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17) ~[junit-4.13.2.jar:4.13.2]
at org.junit.runners.ParentRunner$3.evaluate(ParentRunner.java:306) ~[junit-4.13.2.jar:4.13.2]
at org.junit.runners.BlockJUnit4ClassRunner$1.evaluate(BlockJUnit4ClassRunner.java:100) ~[junit-4.13.2.jar:4.13.2]
at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:366) ~[junit-4.13.2.jar:4.13.2]
at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:103) ~[junit-4.13.2.jar:4.13.2]
at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:63) ~[junit-4.13.2.jar:4.13.2]
at org.junit.runners.ParentRunner$4.run(ParentRunner.java:331) ~[junit-4.13.2.jar:4.13.2]
at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:79) ~[junit-4.13.2.jar:4.13.2]
at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:329) ~[junit-4.13.2.jar:4.13.2]
at org.junit.runners.ParentRunner.access$100(ParentRunner.java:66) ~[junit-4.13.2.jar:4.13.2]
at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:293) ~[junit-4.13.2.jar:4.13.2]
at org.junit.runners.ParentRunner$3.evaluate(ParentRunner.java:306) ~[junit-4.13.2.jar:4.13.2]
at org.junit.runners.ParentRunner.run(ParentRunner.java:413) ~[junit-4.13.2.jar:4.13.2]
at org.junit.runner.JUnitCore.run(JUnitCore.java:137) ~[junit-4.13.2.jar:4.13.2]
at org.junit.runner.JUnitCore.run(JUnitCore.java:115) ~[junit-4.13.2.jar:4.13.2]
at org.gradle.api.internal.tasks.testing.junit.JUnitTestExecutor.runRequest(JUnitTestExecutor.java:175) ~[?:?]
at org.gradle.api.internal.tasks.testing.junit.JUnitTestExecutor.accept(JUnitTestExecutor.java:84) ~[?:?]
at org.gradle.api.internal.tasks.testing.junit.JUnitTestExecutor.accept(JUnitTestExecutor.java:47) ~[?:?]
at org.gradle.api.internal.tasks.testing.junit.AbstractJUnitTestDefinitionProcessor.processTestDefinition(AbstractJUnitTestDefinitionProcessor.java:65) ~[?:?]
at org.gradle.api.internal.tasks.testing.SuiteTestDefinitionProcessor.processTestDefinition(SuiteTestDefinitionProcessor.java:53) ~[?:?]
at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:104) ~[?:?]
at java.base/java.lang.reflect.Method.invoke(Method.java:565) ~[?:?]
at org.gradle.internal.dispatch.MethodInvocation.invokeOn(MethodInvocation.java:77) ~[?:?]
at org.gradle.internal.dispatch.ReflectionDispatch.dispatch(ReflectionDispatch.java:28) ~[?:?]
at org.gradle.internal.dispatch.ReflectionDispatch.dispatch(ReflectionDispatch.java:19) ~[?:?]
at org.gradle.internal.dispatch.ContextClassLoaderDispatch.dispatch(ContextClassLoaderDispatch.java:33) ~[?:?]
at org.gradle.internal.dispatch.ProxyDispatchAdapter$DispatchingInvocationHandler.invoke(ProxyDispatchAdapter.java:88) ~[?:?]
at jdk.proxy1/jdk.proxy1.$Proxy4.processTestDefinition(Unknown Source) ~[?:?]
at org.gradle.api.internal.tasks.testing.worker.TestWorker$2.run(TestWorker.java:178) ~[?:?]
at org.gradle.api.internal.tasks.testing.worker.TestWorker.executeAndMaintainThreadName(TestWorker.java:126) ~[?:?]
at org.gradle.api.internal.tasks.testing.worker.TestWorker.execute(TestWorker.java:103) ~[?:?]
at org.gradle.api.internal.tasks.testing.worker.TestWorker.execute(TestWorker.java:63) ~[?:?]
at org.gradle.process.internal.worker.child.ActionExecutionWorker.execute(ActionExecutionWorker.java:56) ~[?:?]
at org.gradle.process.internal.worker.child.SystemApplicationClassLoaderWorker.call(SystemApplicationClassLoaderWorker.java:122) ~[?:?]
at org.gradle.process.internal.worker.child.SystemApplicationClassLoaderWorker.call(SystemApplicationClassLoaderWorker.java:72) ~[?:?]
at worker.org.gradle.process.internal.worker.GradleWorkerMain.run(GradleWorkerMain.java:69) [gradle-worker.jar:?]
at worker.org.gradle.process.internal.worker.GradleWorkerMain.main(GradleWorkerMain.java:74) [gradle-worker.jar:?]
@@ -0,0 +1,74 @@
/*
* Iris is a World Generator for Minecraft 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.forge;
import art.arcane.iris.modded.ModdedBlockBreakHandler;
import com.mojang.serialization.MapCodec;
import com.mojang.serialization.codecs.RecordCodecBuilder;
import it.unimi.dsi.fastutil.objects.ObjectArrayList;
import net.minecraft.core.BlockPos;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.world.entity.Entity;
import net.minecraft.world.entity.player.Player;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.storage.loot.LootContext;
import net.minecraft.world.level.storage.loot.LootTable;
import net.minecraft.world.level.storage.loot.parameters.LootContextParams;
import net.minecraft.world.level.storage.loot.predicates.LootItemCondition;
import net.minecraft.world.phys.Vec3;
import net.minecraftforge.common.loot.IGlobalLootModifier;
import net.minecraftforge.common.loot.LootModifier;
import org.jetbrains.annotations.NotNull;
public final class IrisForgeBlockLootModifier extends LootModifier {
public static final MapCodec<IrisForgeBlockLootModifier> CODEC = RecordCodecBuilder.mapCodec(instance ->
codecStart(instance).apply(instance, IrisForgeBlockLootModifier::new));
public IrisForgeBlockLootModifier(LootItemCondition[] conditions) {
super(conditions);
}
@Override
public MapCodec<? extends IGlobalLootModifier> codec() {
return CODEC;
}
@NotNull
@Override
protected ObjectArrayList<ItemStack> doApply(LootTable table, ObjectArrayList<ItemStack> generatedLoot, LootContext context) {
Entity breaker = context.getOptionalParameter(LootContextParams.THIS_ENTITY);
BlockState state = context.getOptionalParameter(LootContextParams.BLOCK_STATE);
Vec3 origin = context.getOptionalParameter(LootContextParams.ORIGIN);
if (!(breaker instanceof Player) || state == null || origin == null) {
return generatedLoot;
}
ServerLevel level = context.getLevel();
BlockPos position = BlockPos.containing(origin);
ModdedBlockBreakHandler.Result result = ModdedBlockBreakHandler.completePrepared(level, position);
if (result == null) {
return generatedLoot;
}
if (result.replaceVanillaDrops()) {
generatedLoot.clear();
}
generatedLoot.addAll(result.drops());
return generatedLoot;
}
}
@@ -19,7 +19,7 @@
package art.arcane.iris.forge; package art.arcane.iris.forge;
import art.arcane.iris.modded.IrisModdedChunkGenerator; import art.arcane.iris.modded.IrisModdedChunkGenerator;
import art.arcane.iris.modded.ModdedDeathLoot; import art.arcane.iris.modded.ModdedBlockBreakHandler;
import art.arcane.iris.modded.ModdedEngineBootstrap; import art.arcane.iris.modded.ModdedEngineBootstrap;
import art.arcane.iris.modded.ModdedForcedDatapack; import art.arcane.iris.modded.ModdedForcedDatapack;
import art.arcane.iris.modded.ModdedProtocolHandler; import art.arcane.iris.modded.ModdedProtocolHandler;
@@ -27,23 +27,29 @@ import art.arcane.iris.modded.command.IrisModdedCommands;
import art.arcane.iris.modded.command.ModdedWandService; import art.arcane.iris.modded.command.ModdedWandService;
import com.mojang.serialization.MapCodec; import com.mojang.serialization.MapCodec;
import net.minecraft.core.registries.Registries; import net.minecraft.core.registries.Registries;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.server.level.ServerPlayer; import net.minecraft.server.level.ServerPlayer;
import net.minecraft.server.packs.PackType; import net.minecraft.server.packs.PackType;
import net.minecraft.world.level.chunk.ChunkGenerator; import net.minecraft.world.level.chunk.ChunkGenerator;
import net.minecraftforge.event.AddPackFindersEvent; import net.minecraftforge.event.AddPackFindersEvent;
import net.minecraftforge.event.RegisterCommandsEvent; import net.minecraftforge.event.RegisterCommandsEvent;
import net.minecraftforge.event.TickEvent; import net.minecraftforge.event.TickEvent;
import net.minecraftforge.event.entity.living.LivingDropsEvent;
import net.minecraftforge.event.entity.player.PlayerEvent; import net.minecraftforge.event.entity.player.PlayerEvent;
import net.minecraftforge.event.entity.player.PlayerInteractEvent; import net.minecraftforge.event.entity.player.PlayerInteractEvent;
import net.minecraftforge.event.level.BlockEvent;
import net.minecraftforge.event.level.LevelEvent;
import net.minecraftforge.event.server.ServerAboutToStartEvent;
import net.minecraftforge.event.server.ServerStartedEvent;
import net.minecraftforge.event.server.ServerStartingEvent; import net.minecraftforge.event.server.ServerStartingEvent;
import net.minecraftforge.event.server.ServerStoppingEvent; import net.minecraftforge.event.server.ServerStoppingEvent;
import net.minecraftforge.api.distmarker.Dist; import net.minecraftforge.api.distmarker.Dist;
import net.minecraftforge.common.loot.IGlobalLootModifier;
import net.minecraftforge.fml.common.Mod; import net.minecraftforge.fml.common.Mod;
import net.minecraftforge.fml.javafmlmod.FMLJavaModLoadingContext; import net.minecraftforge.fml.javafmlmod.FMLJavaModLoadingContext;
import net.minecraftforge.fml.loading.FMLEnvironment; import net.minecraftforge.fml.loading.FMLEnvironment;
import net.minecraftforge.fml.loading.FMLLoader; import net.minecraftforge.fml.loading.FMLLoader;
import net.minecraftforge.registries.DeferredRegister; import net.minecraftforge.registries.DeferredRegister;
import net.minecraftforge.registries.ForgeRegistries;
import java.util.function.Predicate; import java.util.function.Predicate;
@@ -54,6 +60,9 @@ public final class IrisForgeBootstrap {
DeferredRegister<MapCodec<? extends ChunkGenerator>> chunkGenerators = DeferredRegister.create(Registries.CHUNK_GENERATOR, "irisworldgen"); DeferredRegister<MapCodec<? extends ChunkGenerator>> chunkGenerators = DeferredRegister.create(Registries.CHUNK_GENERATOR, "irisworldgen");
chunkGenerators.register("iris", () -> IrisModdedChunkGenerator.CODEC); chunkGenerators.register("iris", () -> IrisModdedChunkGenerator.CODEC);
chunkGenerators.register(context.getModBusGroup()); chunkGenerators.register(context.getModBusGroup());
DeferredRegister<MapCodec<? extends IGlobalLootModifier>> lootModifiers = DeferredRegister.create(ForgeRegistries.GLOBAL_LOOT_MODIFIER_SERIALIZERS, "irisworldgen");
lootModifiers.register("block_drops", () -> IrisForgeBlockLootModifier.CODEC);
lootModifiers.register(context.getModBusGroup());
}); });
ForgeProtocolNetworking.register(); ForgeProtocolNetworking.register();
@@ -62,8 +71,15 @@ public final class IrisForgeBootstrap {
IrisForgeClient.init(); IrisForgeClient.init();
} }
ServerAboutToStartEvent.BUS.addListener((ServerAboutToStartEvent event) -> ModdedEngineBootstrap.serverAboutToStart(event.getServer()));
ServerStartingEvent.BUS.addListener((ServerStartingEvent event) -> ModdedEngineBootstrap.start(event.getServer())); ServerStartingEvent.BUS.addListener((ServerStartingEvent event) -> ModdedEngineBootstrap.start(event.getServer()));
ServerStartedEvent.BUS.addListener((ServerStartedEvent event) -> ModdedEngineBootstrap.serverStarted(event.getServer()));
ServerStoppingEvent.BUS.addListener((ServerStoppingEvent event) -> ModdedEngineBootstrap.stop()); ServerStoppingEvent.BUS.addListener((ServerStoppingEvent event) -> ModdedEngineBootstrap.stop());
LevelEvent.Load.BUS.addListener((LevelEvent.Load event) -> {
if (event.getLevel() instanceof ServerLevel level) {
ModdedEngineBootstrap.levelLoaded(level);
}
});
PlayerEvent.PlayerLoggedInEvent.BUS.addListener((PlayerEvent.PlayerLoggedInEvent event) -> { PlayerEvent.PlayerLoggedInEvent.BUS.addListener((PlayerEvent.PlayerLoggedInEvent event) -> {
if (event.getEntity() instanceof ServerPlayer player) { if (event.getEntity() instanceof ServerPlayer player) {
ModdedProtocolHandler.onPlayerJoin(player); ModdedProtocolHandler.onPlayerJoin(player);
@@ -80,7 +96,11 @@ public final class IrisForgeBootstrap {
} }
}); });
RegisterCommandsEvent.BUS.addListener((RegisterCommandsEvent event) -> IrisModdedCommands.register(event.getDispatcher())); RegisterCommandsEvent.BUS.addListener((RegisterCommandsEvent event) -> IrisModdedCommands.register(event.getDispatcher()));
LivingDropsEvent.BUS.addListener((LivingDropsEvent event) -> ModdedDeathLoot.handle(event.getEntity())); BlockEvent.BreakEvent.BUS.addListener((BlockEvent.BreakEvent event) -> {
if (event.getLevel() instanceof ServerLevel level && !event.getResult().isDenied()) {
ModdedBlockBreakHandler.prepare(level, event.getPos(), event.getState());
}
});
PlayerInteractEvent.LeftClickBlock.BUS.addListener((Predicate<PlayerInteractEvent.LeftClickBlock>) (PlayerInteractEvent.LeftClickBlock event) -> PlayerInteractEvent.LeftClickBlock.BUS.addListener((Predicate<PlayerInteractEvent.LeftClickBlock>) (PlayerInteractEvent.LeftClickBlock event) ->
ModdedWandService.attackBlock(event.getEntity(), event.getLevel(), event.getHand(), event.getPos())); ModdedWandService.attackBlock(event.getEntity(), event.getLevel(), event.getHand(), event.getPos()));
PlayerInteractEvent.RightClickBlock.BUS.addListener((Predicate<PlayerInteractEvent.RightClickBlock>) (PlayerInteractEvent.RightClickBlock event) -> PlayerInteractEvent.RightClickBlock.BUS.addListener((Predicate<PlayerInteractEvent.RightClickBlock>) (PlayerInteractEvent.RightClickBlock event) ->
@@ -0,0 +1,307 @@
package art.arcane.iris.nativegen;
import art.arcane.iris.engine.framework.StructureVerticalBounds;
import art.arcane.iris.engine.object.IrisNativeStructureDecision;
import art.arcane.iris.engine.object.IrisVanillaStructureStiltSettings;
import art.arcane.volmlib.util.math.RNG;
import net.minecraft.core.BlockPos;
import net.minecraft.tags.BlockTags;
import net.minecraft.world.level.ChunkPos;
import net.minecraft.world.level.StructureManager;
import net.minecraft.world.level.WorldGenLevel;
import net.minecraft.world.level.block.Blocks;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.chunk.ChunkAccess;
import net.minecraft.world.level.chunk.ChunkGenerator;
import net.minecraft.world.level.chunk.LevelChunkSection;
import net.minecraft.world.level.levelgen.GenerationStep;
import net.minecraft.world.level.levelgen.WorldgenRandom;
import net.minecraft.world.level.levelgen.structure.BoundingBox;
import net.minecraft.world.level.levelgen.structure.PoolElementStructurePiece;
import net.minecraft.world.level.levelgen.structure.StructurePiece;
import net.minecraft.world.level.levelgen.structure.StructureStart;
import net.minecraft.world.level.levelgen.structure.pools.JigsawJunction;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.BitSet;
import java.util.List;
public final class NativeStructurePostProcessor {
private static final int FOUNDATION_VERTICAL_TOLERANCE = 1;
private NativeStructurePostProcessor() {
}
public static void place(WorldGenLevel world, StructureManager structureManager, ChunkGenerator generator,
WorldgenRandom random, BoundingBox area, ChunkPos chunkPos, String structureId,
StructureStart start, IrisNativeStructureDecision decision,
StiltBlockResolver stiltBlockResolver) {
IrisVanillaStructureStiltSettings stilt = decision.stilt();
start.placeInChunk(world, structureManager, generator, random, area, chunkPos);
if (stilt != null) {
placeStilts(world, area, structureId, start, stilt, stiltBlockResolver);
}
}
public static int applyVerticalShift(StructureStart start, int requestedOffset, int worldMinY,
int worldMaxYExclusive) {
BoundingBox bounds = start.getBoundingBox();
int offsetY = StructureVerticalBounds.clampOffset(
bounds.minY(), bounds.maxY(), requestedOffset, worldMinY, worldMaxYExclusive);
if (offsetY == 0) {
return 0;
}
for (StructurePiece piece : start.getPieces()) {
piece.move(0, offsetY, 0);
if (piece instanceof PoolElementStructurePiece poolPiece) {
List<JigsawJunction> junctions = poolPiece.getJunctions();
for (int i = 0; i < junctions.size(); i++) {
JigsawJunction junction = junctions.get(i);
junctions.set(i, new JigsawJunction(
junction.getSourceX(),
junction.getSourceGroundY() + offsetY,
junction.getSourceZ(),
junction.getDeltaY(),
junction.getDestProjection()));
}
}
}
bounds.move(0, offsetY, 0);
return offsetY;
}
public static boolean isUndergroundStep(GenerationStep.Decoration step) {
return step == GenerationStep.Decoration.UNDERGROUND_STRUCTURES
|| step == GenerationStep.Decoration.UNDERGROUND_DECORATION
|| step == GenerationStep.Decoration.STRONGHOLDS;
}
public static void clearIntersectingVegetation(WorldGenLevel world, ChunkAccess chunk, BoundingBox area,
List<VegetationTarget> targets) {
if (targets == null || targets.isEmpty()) {
return;
}
VegetationSnapshot snapshot = captureVegetation(chunk, area);
if (snapshot.treeBlockCount() == 0) {
return;
}
boolean[] clearColumns = new boolean[area.getXSpan() * area.getZSpan()];
for (VegetationTarget target : targets) {
if (target != null && target.start() != null && target.start().isValid()) {
markVegetationColumns(area, snapshot, target, clearColumns);
}
}
clearVegetationColumns(world, area, snapshot, clearColumns);
}
private static VegetationSnapshot captureVegetation(ChunkAccess chunk, BoundingBox area) {
int width = area.getXSpan();
int depth = area.getZSpan();
BitSet[] columns = new BitSet[width * depth];
int[] lowestY = new int[columns.length];
Arrays.fill(lowestY, Integer.MAX_VALUE);
int treeBlockCount = 0;
LevelChunkSection[] sections = chunk.getSections();
int chunkMinX = chunk.getPos().getMinBlockX();
int chunkMinZ = chunk.getPos().getMinBlockZ();
for (int sectionIndex = 0; sectionIndex < sections.length; sectionIndex++) {
LevelChunkSection section = sections[sectionIndex];
int sectionMinY = chunk.getSectionYFromSectionIndex(sectionIndex) << 4;
int minY = Math.max(area.minY(), sectionMinY);
int maxY = Math.min(area.maxY(), sectionMinY + 15);
if (minY > maxY || section.hasOnlyAir() || !section.maybeHas(NativeStructurePostProcessor::isTreeBlock)) {
continue;
}
for (int y = minY; y <= maxY; y++) {
int localY = y - sectionMinY;
for (int z = area.minZ(); z <= area.maxZ(); z++) {
int localZ = z - chunkMinZ;
for (int x = area.minX(); x <= area.maxX(); x++) {
int localX = x - chunkMinX;
if (!isTreeBlock(section.getBlockState(localX, localY, localZ))) {
continue;
}
int column = (z - area.minZ()) * width + x - area.minX();
BitSet treeBlocks = columns[column];
if (treeBlocks == null) {
treeBlocks = new BitSet(area.getYSpan());
columns[column] = treeBlocks;
}
treeBlocks.set(y - area.minY());
lowestY[column] = Math.min(lowestY[column], y);
treeBlockCount++;
}
}
}
}
return new VegetationSnapshot(columns, lowestY, treeBlockCount);
}
private static void markVegetationColumns(BoundingBox area, VegetationSnapshot snapshot,
VegetationTarget target, boolean[] clearColumns) {
int width = area.getXSpan();
int[] pieceTops = new int[clearColumns.length];
Arrays.fill(pieceTops, Integer.MIN_VALUE);
for (StructurePiece piece : target.start().getPieces()) {
BoundingBox bounds = piece.getBoundingBox();
int minX = Math.max(area.minX(), bounds.minX());
int maxX = Math.min(area.maxX(), bounds.maxX());
int minZ = Math.max(area.minZ(), bounds.minZ());
int maxZ = Math.min(area.maxZ(), bounds.maxZ());
if (minX > maxX || minZ > maxZ) {
continue;
}
for (int z = minZ; z <= maxZ; z++) {
for (int x = minX; x <= maxX; x++) {
int column = (z - area.minZ()) * width + x - area.minX();
pieceTops[column] = Math.max(pieceTops[column], bounds.maxY());
}
}
}
for (int column = 0; column < pieceTops.length; column++) {
if (snapshot.columns()[column] == null || pieceTops[column] == Integer.MIN_VALUE) {
continue;
}
if (shouldClearVegetationColumn(pieceTops[column], snapshot.lowestY()[column], target.force())) {
clearColumns[column] = true;
}
}
}
static boolean shouldClearVegetationColumn(int pieceTopY, int lowestTreeY, boolean force) {
return force || pieceTopY >= lowestTreeY;
}
private static void clearVegetationColumns(WorldGenLevel world, BoundingBox area,
VegetationSnapshot snapshot, boolean[] clearColumns) {
int width = area.getXSpan();
BlockPos.MutableBlockPos position = new BlockPos.MutableBlockPos();
BlockState air = Blocks.AIR.defaultBlockState();
for (int z = area.minZ(); z <= area.maxZ(); z++) {
for (int x = area.minX(); x <= area.maxX(); x++) {
int column = (z - area.minZ()) * width + x - area.minX();
BitSet treeBlocks = snapshot.columns()[column];
if (!clearColumns[column] || treeBlocks == null) {
continue;
}
for (int bit = treeBlocks.nextSetBit(0); bit >= 0; bit = treeBlocks.nextSetBit(bit + 1)) {
int y = area.minY() + bit;
position.set(x, y, z);
BlockState state = world.getBlockState(position);
if (isTreeBlock(state)) {
world.setBlock(position, air, 2);
}
}
}
}
}
private static boolean isTreeBlock(BlockState state) {
return state.is(BlockTags.LOGS) || state.is(BlockTags.LEAVES);
}
private static List<FoundationColumn> foundationEnvelope(BoundingBox area, StructureStart start) {
BoundingBox structure = start.getBoundingBox();
int minX = Math.max(area.minX(), structure.minX());
int minZ = Math.max(area.minZ(), structure.minZ());
int maxX = Math.min(area.maxX(), structure.maxX());
int maxZ = Math.min(area.maxZ(), structure.maxZ());
if (minX > maxX || minZ > maxZ) {
return List.of();
}
List<StructurePiece> pieces = start.getPieces();
List<FoundationColumn> columns = new ArrayList<>((maxX - minX + 1) * (maxZ - minZ + 1));
BitSet envelope = new BitSet(area.getYSpan());
for (int z = minZ; z <= maxZ; z++) {
for (int x = minX; x <= maxX; x++) {
envelope.clear();
markFoundationEnvelope(envelope, pieces, area, x, z);
int cellCount = envelope.cardinality();
if (cellCount == 0) {
continue;
}
int[] ys = new int[cellCount];
int cell = 0;
for (int bit = envelope.nextSetBit(0); bit >= 0; bit = envelope.nextSetBit(bit + 1)) {
ys[cell++] = area.minY() + bit;
}
columns.add(new FoundationColumn(x, z, ys));
}
}
return List.copyOf(columns);
}
private static void markFoundationEnvelope(BitSet envelope, List<StructurePiece> pieces, BoundingBox area,
int x, int z) {
for (StructurePiece piece : pieces) {
BoundingBox bounds = piece.getBoundingBox();
if (x < bounds.minX() || x > bounds.maxX() || z < bounds.minZ() || z > bounds.maxZ()) {
continue;
}
int groundY = bounds.minY();
if (piece instanceof PoolElementStructurePiece poolPiece) {
groundY += poolPiece.getGroundLevelDelta();
if (groundY < bounds.minY()) {
continue;
}
}
int minY = Math.max(area.minY(), bounds.minY());
int maxY = Math.min(area.maxY(), Math.min(bounds.maxY(), groundY + FOUNDATION_VERTICAL_TOLERANCE));
if (minY <= maxY) {
envelope.set(minY - area.minY(), maxY - area.minY() + 1);
}
}
}
private static void placeStilts(WorldGenLevel world, BoundingBox area, String structureId,
StructureStart start, IrisVanillaStructureStiltSettings settings,
StiltBlockResolver stiltBlockResolver) {
BlockPos.MutableBlockPos position = new BlockPos.MutableBlockPos();
int structureHash = structureId == null ? 0 : structureId.hashCode();
RNG rng = new RNG(world.getSeed() ^ structureHash);
for (FoundationColumn column : foundationEnvelope(area, start)) {
int foundationY = findFoundationY(world, column, position);
if (foundationY == Integer.MIN_VALUE) {
continue;
}
for (int depth = 0, y = foundationY - 1;
depth < settings.getMaxDepth() && y >= area.minY(); depth++, y--) {
position.set(column.x(), y, column.z());
BlockState existingState = world.getBlockState(position);
boolean vegetation = existingState.is(BlockTags.LOGS) || existingState.is(BlockTags.LEAVES);
if (existingState.isSolid() && !vegetation) {
break;
}
BlockState stilt = stiltBlockResolver.resolve(settings, rng, column.x(), y, column.z());
world.setBlock(position, stilt == null ? Blocks.COBBLESTONE.defaultBlockState() : stilt, 2);
}
}
}
private static int findFoundationY(WorldGenLevel world, FoundationColumn column,
BlockPos.MutableBlockPos position) {
for (int cell = 0; cell < column.ys().length; cell++) {
int y = column.ys()[cell];
BlockState state = world.getBlockState(position.set(column.x(), y, column.z()));
if (state.isSolid()) {
return y;
}
}
return Integer.MIN_VALUE;
}
@FunctionalInterface
public interface StiltBlockResolver {
BlockState resolve(IrisVanillaStructureStiltSettings settings, RNG rng, int x, int y, int z);
}
private record FoundationColumn(int x, int z, int[] ys) {
}
public record VegetationTarget(StructureStart start, boolean force) {
}
private record VegetationSnapshot(BitSet[] columns, int[] lowestY, int treeBlockCount) {
}
}
@@ -0,0 +1,534 @@
/*
* Iris is a World Generator for Minecraft 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.modded;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.object.IrisBiome;
import art.arcane.iris.engine.object.IrisBiomeCustom;
import art.arcane.volmlib.util.math.RNG;
import com.mojang.serialization.MapCodec;
import net.minecraft.core.Holder;
import net.minecraft.core.HolderLookup;
import net.minecraft.core.QuartPos;
import net.minecraft.core.Registry;
import net.minecraft.core.registries.Registries;
import net.minecraft.resources.Identifier;
import net.minecraft.resources.ResourceKey;
import net.minecraft.server.MinecraftServer;
import net.minecraft.world.level.biome.Biome;
import net.minecraft.world.level.biome.BiomeSource;
import net.minecraft.world.level.biome.Climate;
import net.minecraft.world.level.levelgen.structure.Structure;
import net.minecraft.world.level.levelgen.structure.StructureSet;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.Locale;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.stream.Stream;
final class IrisModdedBiomeSource extends BiomeSource {
private static final int BIOME_CACHE_MAX = 262144;
private final BiomeSource serializedSource;
private final ConcurrentHashMap<Long, Holder<Biome>> visibleBiomeCache = new ConcurrentHashMap<>();
private final ConcurrentHashMap<Long, Holder<Biome>> structureBiomeCache = new ConcurrentHashMap<>();
private final ConcurrentHashMap<Long, Holder<Biome>> surfaceStructureBiomeCache = new ConcurrentHashMap<>();
private volatile IrisModdedChunkGenerator generator;
private volatile Set<String> possibleStructureBiomeKeys;
IrisModdedBiomeSource(BiomeSource serializedSource) {
this.serializedSource = serializedSource;
}
void bind(IrisModdedChunkGenerator generator) {
this.generator = generator;
}
void clearCaches() {
visibleBiomeCache.clear();
structureBiomeCache.clear();
surfaceStructureBiomeCache.clear();
possibleStructureBiomeKeys = null;
}
BiomeSource forStructureState(HolderLookup<StructureSet> structureSets) {
LinkedHashSet<Holder<Biome>> possible = new LinkedHashSet<>();
Registry<Biome> registry = biomeRegistry();
if (registry == null) {
throw new IllegalStateException("Iris cannot create structure state without the biome registry");
}
Set<String> generatedBiomeKeys = exactStructureBiomeKeys();
Set<String> missingBiomeKeys = new LinkedHashSet<>(generatedBiomeKeys);
missingBiomeKeys.removeAll(registeredBiomeKeys(registry));
if (!missingBiomeKeys.isEmpty()) {
throw new IllegalStateException("Iris structure biomes are not registered: " + missingBiomeKeys);
}
structureSets.listElements().forEach((Holder.Reference<StructureSet> structureSet) -> {
for (StructureSet.StructureSelectionEntry entry : structureSet.value().structures()) {
for (Holder<Biome> biome : entry.structure().value().biomes()) {
String key = holderKey(biome);
if (isGeneratedBiomeKey(key, generatedBiomeKeys)) {
possible.add(biome);
}
}
}
});
return new StructureStateBiomeSource(this, Set.copyOf(possible));
}
@Override
protected MapCodec<? extends BiomeSource> codec() {
throw new UnsupportedOperationException("IrisModdedBiomeSource is serialized through IrisModdedChunkGenerator");
}
@Override
protected Stream<Holder<Biome>> collectPossibleBiomes() {
Registry<Biome> registry = biomeRegistry();
if (registry == null) {
return serializedSource.possibleBiomes().stream();
}
Set<String> generatedBiomeKeys = possibleStructureBiomeKeys();
LinkedHashSet<Holder<Biome>> possible = new LinkedHashSet<>();
Holder<Biome> fallback = fallbackHolder(registry);
registry.listElements().forEach((Holder.Reference<Biome> reference) -> {
String key = holderKey(reference);
if (isGeneratedBiomeKey(key, generatedBiomeKeys)) {
possible.add(reference);
}
});
if (possible.isEmpty() && fallback != null) {
possible.add(fallback);
}
return possible.stream();
}
@Override
public Holder<Biome> getNoiseBiome(int quartX, int quartY, int quartZ, Climate.Sampler sampler) {
Engine engine = engineOrNull();
if (!isReady(engine)) {
return serializedSource.getNoiseBiome(quartX, quartY, quartZ, sampler);
}
return getNoiseBiome(engine, quartX, quartY, quartZ, sampler);
}
private Holder<Biome> getNoiseBiome(Engine engine, int quartX, int quartY, int quartZ,
Climate.Sampler sampler) {
if (isGuaranteedSurfaceBiome(quartY, engine.getMinHeight())) {
return getSurfaceStructureBiome(engine, quartX, quartZ, sampler);
}
long key = packNoiseKey(quartX, quartY, quartZ);
Holder<Biome> cached = structureBiomeCache.get(key);
if (cached != null) {
return cached;
}
Holder<Biome> resolved = resolveStructureBiome(engine, quartX, quartY, quartZ, sampler);
Holder<Biome> existing = structureBiomeCache.putIfAbsent(key, resolved);
if (structureBiomeCache.size() > BIOME_CACHE_MAX) {
structureBiomeCache.clear();
}
return existing == null ? resolved : existing;
}
Holder<Biome> getVisibleNoiseBiome(int quartX, int quartY, int quartZ, Climate.Sampler sampler) {
Engine engine = engineOrNull();
if (!isReady(engine)) {
return serializedSource.getNoiseBiome(quartX, quartY, quartZ, sampler);
}
long key = packNoiseKey(quartX, quartY, quartZ);
Holder<Biome> cached = visibleBiomeCache.get(key);
if (cached != null) {
return cached;
}
Holder<Biome> resolved = resolveVisibleBiome(engine, quartX, quartY, quartZ, sampler);
Holder<Biome> existing = visibleBiomeCache.putIfAbsent(key, resolved);
if (visibleBiomeCache.size() > BIOME_CACHE_MAX) {
visibleBiomeCache.clear();
}
return existing == null ? resolved : existing;
}
boolean isStructureReachable(Holder<Structure> structure) {
Set<String> possible = possibleStructureBiomeKeys();
for (Holder<Biome> biome : structure.value().biomes()) {
String key = holderKey(biome);
if (isGeneratedBiomeKey(key, possible)) {
return true;
}
}
return false;
}
@Override
public Set<Holder<Biome>> getBiomesWithin(int x, int y, int z, int radius, Climate.Sampler sampler) {
int minQuartY = QuartPos.fromBlock(y - radius);
Engine engine = engineOrNull();
if (!isReady(engine)) {
return super.getBiomesWithin(x, y, z, radius, sampler);
}
boolean monumentQuery = isMonumentSurfaceBiomeQuery(
y, radius, engine.getMinHeight(), engine.getDimension().getFluidHeight());
if (!monumentQuery && !isGuaranteedSurfaceBiome(minQuartY, engine.getMinHeight())) {
return super.getBiomesWithin(x, y, z, radius, sampler);
}
int minQuartX = QuartPos.fromBlock(x - radius);
int maxQuartX = QuartPos.fromBlock(x + radius);
int minQuartZ = QuartPos.fromBlock(z - radius);
int maxQuartZ = QuartPos.fromBlock(z + radius);
int columns = (maxQuartX - minQuartX + 1) * (maxQuartZ - minQuartZ + 1);
Set<Holder<Biome>> biomes = new HashSet<>(columns);
for (int quartZ = minQuartZ; quartZ <= maxQuartZ; quartZ++) {
for (int quartX = minQuartX; quartX <= maxQuartX; quartX++) {
biomes.add(getSurfaceStructureBiome(engine, quartX, quartZ, sampler));
}
}
return biomes;
}
private Holder<Biome> getSurfaceStructureBiome(Engine engine, int quartX, int quartZ,
Climate.Sampler sampler) {
long key = packColumnKey(quartX, quartZ);
Holder<Biome> cached = surfaceStructureBiomeCache.get(key);
if (cached != null) {
return cached;
}
Holder<Biome> resolved = resolveSurfaceStructureBiome(engine, quartX, quartZ, sampler);
Holder<Biome> existing = surfaceStructureBiomeCache.putIfAbsent(key, resolved);
if (surfaceStructureBiomeCache.size() > BIOME_CACHE_MAX) {
surfaceStructureBiomeCache.clear();
}
return existing == null ? resolved : existing;
}
private Holder<Biome> resolveSurfaceStructureBiome(Engine engine, int quartX, int quartZ,
Climate.Sampler sampler) {
Registry<Biome> registry = biomeRegistry();
if (!isReady(engine) || registry == null) {
return serializedSource.getNoiseBiome(quartX, 0, quartZ, sampler);
}
IrisBiome irisBiome = engine.getComplex().getTrueBiomeStream().get(quartX << 2, quartZ << 2);
Holder<Biome> resolved = irisBiome == null ? null : resolveHolder(registry, irisBiome.getVanillaDerivativeKey());
return resolved == null ? serializedSource.getNoiseBiome(quartX, 0, quartZ, sampler) : resolved;
}
private Holder<Biome> resolveStructureBiome(Engine engine, int quartX, int quartY, int quartZ,
Climate.Sampler sampler) {
Registry<Biome> registry = biomeRegistry();
BiomeResolution resolution = resolveBiomeResolution(engine, quartX, quartY, quartZ);
if (resolution == null || registry == null) {
return serializedSource.getNoiseBiome(quartX, quartY, quartZ, sampler);
}
Holder<Biome> resolved = resolveHolder(registry, resolution.irisBiome().getVanillaDerivativeKey());
if (resolved == null && resolution.irisBiome().isCustom()) {
IrisBiomeCustom customBiome = resolution.irisBiome().getCustomBiome(
resolution.rng(), engine, resolution.blockX(), resolution.blockY(), resolution.blockZ());
if (customBiome != null) {
resolved = resolveHolder(registry, engine.getDimension().getLoadKey().toLowerCase(Locale.ROOT)
+ ":" + customBiome.getId().toLowerCase(Locale.ROOT));
}
}
return resolved == null ? fallbackBiome(registry, quartX, quartY, quartZ, sampler) : resolved;
}
private Holder<Biome> resolveVisibleBiome(Engine engine, int quartX, int quartY, int quartZ,
Climate.Sampler sampler) {
Registry<Biome> registry = biomeRegistry();
BiomeResolution resolution = resolveBiomeResolution(engine, quartX, quartY, quartZ);
if (resolution == null || registry == null) {
return serializedSource.getNoiseBiome(quartX, quartY, quartZ, sampler);
}
String biomeKey;
if (resolution.irisBiome().isCustom()) {
IrisBiomeCustom customBiome = resolution.irisBiome().getCustomBiome(
resolution.rng(), engine, resolution.blockX(), resolution.blockY(), resolution.blockZ());
if (customBiome == null) {
return fallbackBiome(registry, quartX, quartY, quartZ, sampler);
}
biomeKey = engine.getDimension().getLoadKey().toLowerCase(Locale.ROOT)
+ ":" + customBiome.getId().toLowerCase(Locale.ROOT);
} else if (resolution.underground()) {
biomeKey = resolution.irisBiome().getGroundBiomeKey(
resolution.rng(), engine, resolution.blockX(), resolution.blockY(), resolution.blockZ());
} else {
biomeKey = resolution.irisBiome().getSkyBiomeKey(
resolution.rng(), engine, resolution.blockX(), resolution.blockY(), resolution.blockZ());
}
Holder<Biome> resolved = resolveHolder(registry, biomeKey);
return resolved == null
? fallbackBiome(registry, quartX, quartY, quartZ, sampler)
: resolved;
}
private BiomeResolution resolveBiomeResolution(Engine engine, int quartX, int quartY, int quartZ) {
if (!isReady(engine)) {
return null;
}
int blockX = quartX << 2;
int blockY = quartY << 2;
int blockZ = quartZ << 2;
int internalY = blockY - engine.getMinHeight();
int caveSwitchY = Math.max(-8 - engine.getMinHeight(), 40);
boolean underground = false;
IrisBiome irisBiome;
if (internalY <= caveSwitchY) {
int surfaceY = engine.getComplex().getHeightStream().get(blockX, blockZ).intValue();
underground = isUnderground(internalY, surfaceY);
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);
}
if (irisBiome == null) {
return null;
}
IrisModdedChunkGenerator current = generator;
long worldSeed = current == null ? engine.getWorld().getRawWorldSeed() : current.visibleBiomeSeed();
long seed = biomeResolutionSeed(worldSeed, blockX, blockY, blockZ);
return new BiomeResolution(irisBiome, underground, blockX, blockY, blockZ, new RNG(seed));
}
private Holder<Biome> resolveRequiredStructureBiome(int quartX, int quartY, int quartZ) {
IrisModdedChunkGenerator current = generator;
if (current == null) {
throw new IllegalStateException("Iris structure biome source is not bound to its generator");
}
Engine engine = current.awaitStructureEngine();
Registry<Biome> registry = biomeRegistry();
if (registry == null) {
throw new IllegalStateException("Iris structure biome lookup has no biome registry");
}
IrisBiome irisBiome;
if (isGuaranteedSurfaceBiome(quartY, engine.getMinHeight())) {
irisBiome = engine.getComplex().getTrueBiomeStream().get(quartX << 2, quartZ << 2);
} else {
BiomeResolution resolution = resolveBiomeResolution(engine, quartX, quartY, quartZ);
irisBiome = resolution == null ? null : resolution.irisBiome();
}
if (irisBiome == null) {
throw new IllegalStateException("Iris returned no structure biome at quart "
+ quartX + "," + quartY + "," + quartZ);
}
String derivativeKey = irisBiome.getVanillaDerivativeKey();
Holder<Biome> resolved = resolveHolder(registry, derivativeKey);
if (resolved == null) {
throw new IllegalStateException("Iris structure biome derivative '" + derivativeKey
+ "' is not registered at quart " + quartX + "," + quartY + "," + quartZ);
}
return resolved;
}
static long biomeResolutionSeed(long worldSeed, int blockX, int blockY, int blockZ) {
return worldSeed
^ ((long) blockX * 341873128712L)
^ ((long) blockY * 132897987541L)
^ ((long) blockZ * 42317861L);
}
static boolean isGeneratedBiomeKey(String key, Set<String> generatedBiomeKeys) {
return key != null && generatedBiomeKeys.contains(key.toLowerCase(Locale.ROOT));
}
static boolean isGuaranteedSurfaceBiome(int quartY, int minHeight) {
int internalY = (quartY << 2) - minHeight;
int caveSwitchY = Math.max(-8 - minHeight, 40);
return internalY > caveSwitchY;
}
static boolean isUnderground(int internalY, int surfaceY) {
return internalY <= surfaceY - 8;
}
static boolean isMonumentSurfaceBiomeQuery(int blockY, int radius, int minHeight, int fluidHeight) {
return radius == 29 && blockY == minHeight + fluidHeight;
}
private static boolean isReady(Engine engine) {
return engine != null && !engine.isClosed() && engine.getComplex() != null;
}
private Engine engineOrNull() {
IrisModdedChunkGenerator current = generator;
return current == null ? null : current.structureEngineOrNull();
}
private Set<String> possibleStructureBiomeKeys() {
Set<String> cached = possibleStructureBiomeKeys;
if (cached != null) {
return cached;
}
LinkedHashSet<String> possible = new LinkedHashSet<>(exactStructureBiomeKeys());
Registry<Biome> registry = biomeRegistry();
if (registry != null) {
Set<String> registered = registeredBiomeKeys(registry);
Holder<Biome> fallback = fallbackHolder(registry);
String fallbackKey = fallback == null ? null : holderKey(fallback);
if (fallbackKey != null && requiresPossibleBiomeFallback(possible, registered)) {
possible.add(fallbackKey);
}
}
Set<String> resolved = Set.copyOf(possible);
possibleStructureBiomeKeys = resolved;
return resolved;
}
private Set<String> exactStructureBiomeKeys() {
LinkedHashSet<String> possible = new LinkedHashSet<>();
Engine engine = engineOrNull();
if (isReady(engine)) {
String namespace = engine.getDimension().getLoadKey().toLowerCase(Locale.ROOT);
for (IrisBiome irisBiome : engine.getAllBiomes()) {
String derivative = normalizeKey(irisBiome.getVanillaDerivativeKey());
if (derivative != null) {
possible.add(derivative);
}
if (!irisBiome.isCustom()) {
continue;
}
for (IrisBiomeCustom customBiome : irisBiome.getCustomDerivitives()) {
possible.add(namespace + ":" + customBiome.getId().toLowerCase(Locale.ROOT));
}
}
}
IrisModdedChunkGenerator current = generator;
if (current != null) {
possible.addAll(current.configuredStructureBiomeKeys());
}
return Set.copyOf(possible);
}
static boolean requiresPossibleBiomeFallback(Set<String> configured, Set<String> registered) {
return configured.isEmpty() || !registered.containsAll(configured);
}
private static Set<String> registeredBiomeKeys(Registry<Biome> registry) {
Set<String> registered = new HashSet<>();
registry.listElements().forEach((Holder.Reference<Biome> reference) -> {
String key = holderKey(reference);
if (key != null) {
registered.add(key);
}
});
return registered;
}
private Registry<Biome> biomeRegistry() {
MinecraftServer server = ModdedEngineBootstrap.currentServer();
return server == null ? null : server.registryAccess().lookupOrThrow(Registries.BIOME);
}
private static Holder<Biome> resolveHolder(Registry<Biome> registry, String key) {
if (key == null || key.isBlank()) {
return null;
}
Identifier identifier = Identifier.tryParse(key);
if (identifier == null) {
return null;
}
return registry.get(identifier).<Holder<Biome>>map((Holder.Reference<Biome> reference) -> reference).orElse(null);
}
private static String holderKey(Holder<Biome> holder) {
return holder.unwrapKey()
.map((ResourceKey<Biome> key) -> key.identifier().toString().toLowerCase(Locale.ROOT))
.orElse(null);
}
private static String normalizeKey(String key) {
Identifier identifier = key == null ? null : Identifier.tryParse(key);
return identifier == null ? null : identifier.toString().toLowerCase(Locale.ROOT);
}
private static Holder<Biome> fallbackHolder(Registry<Biome> registry) {
if (registry == null) {
return null;
}
Holder<Biome> plains = resolveHolder(registry, "minecraft:plains");
if (plains != null) {
return plains;
}
return registry.listElements().findFirst().<Holder<Biome>>map(
(Holder.Reference<Biome> reference) -> reference).orElse(null);
}
private Holder<Biome> fallbackBiome(Registry<Biome> registry, int quartX, int quartY, int quartZ,
Climate.Sampler sampler) {
Holder<Biome> plains = resolveHolder(registry, "minecraft:plains");
return plains == null ? serializedSource.getNoiseBiome(quartX, quartY, quartZ, sampler) : plains;
}
private static long packNoiseKey(int x, int y, int z) {
return (((long) x & 67108863L) << 38)
| (((long) z & 67108863L) << 12)
| ((long) y & 4095L);
}
private static long packColumnKey(int x, int z) {
return ((long) x << 32) ^ ((long) z & 4294967295L);
}
private record BiomeResolution(IrisBiome irisBiome, boolean underground, int blockX, int blockY,
int blockZ, RNG rng) {
}
private static final class StructureStateBiomeSource extends BiomeSource {
private final IrisModdedBiomeSource delegate;
private final Set<Holder<Biome>> possibleBiomes;
private final ConcurrentHashMap<Long, Holder<Biome>> resolvedBiomes = new ConcurrentHashMap<>();
private StructureStateBiomeSource(IrisModdedBiomeSource delegate, Set<Holder<Biome>> possibleBiomes) {
this.delegate = delegate;
this.possibleBiomes = possibleBiomes;
}
@Override
protected MapCodec<? extends BiomeSource> codec() {
throw new UnsupportedOperationException("Structure state biome sources are not serializable");
}
@Override
protected Stream<Holder<Biome>> collectPossibleBiomes() {
return possibleBiomes.stream();
}
@Override
public Holder<Biome> getNoiseBiome(int x, int y, int z, Climate.Sampler sampler) {
long key = packNoiseKey(x, y, z);
Holder<Biome> cached = resolvedBiomes.get(key);
if (cached != null) {
return cached;
}
Holder<Biome> resolved = delegate.resolveRequiredStructureBiome(x, y, z);
Holder<Biome> existing = resolvedBiomes.putIfAbsent(key, resolved);
if (resolvedBiomes.size() > BIOME_CACHE_MAX) {
resolvedBiomes.clear();
}
return existing == null ? resolved : existing;
}
@Override
public Set<Holder<Biome>> getBiomesWithin(int x, int y, int z, int radius, Climate.Sampler sampler) {
return super.getBiomesWithin(x, y, z, radius, sampler);
}
}
}
@@ -18,22 +18,33 @@
package art.arcane.iris.modded; package art.arcane.iris.modded;
import art.arcane.iris.core.loader.IrisData;
import art.arcane.iris.engine.framework.Engine; import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.framework.GenerationSessionException; import art.arcane.iris.engine.framework.GenerationSessionException;
import art.arcane.iris.engine.framework.IrisStructureLocator;
import art.arcane.iris.engine.object.IrisBiome; import art.arcane.iris.engine.object.IrisBiome;
import art.arcane.iris.engine.object.IrisBiomeCustom; import art.arcane.iris.engine.object.IrisBiomeCustom;
import art.arcane.iris.engine.object.IrisImportedStructureControl;
import art.arcane.iris.engine.object.IrisDimension;
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.IrisPlatforms; import art.arcane.iris.spi.IrisPlatforms;
import art.arcane.iris.spi.PlatformBiome; import art.arcane.iris.spi.PlatformBiome;
import art.arcane.iris.spi.PlatformBlockState; import art.arcane.iris.spi.PlatformBlockState;
import art.arcane.iris.util.project.hunk.Hunk; import art.arcane.iris.util.project.hunk.Hunk;
import art.arcane.volmlib.util.math.RNG;
import com.mojang.datafixers.util.Pair;
import com.mojang.serialization.Codec; import com.mojang.serialization.Codec;
import com.mojang.serialization.MapCodec; import com.mojang.serialization.MapCodec;
import com.mojang.serialization.codecs.RecordCodecBuilder; import com.mojang.serialization.codecs.RecordCodecBuilder;
import net.minecraft.core.BlockPos; import net.minecraft.core.BlockPos;
import net.minecraft.core.Holder; import net.minecraft.core.Holder;
import net.minecraft.core.QuartPos; import net.minecraft.core.HolderLookup;
import net.minecraft.core.HolderSet;
import net.minecraft.core.Registry; import net.minecraft.core.Registry;
import net.minecraft.core.RegistryAccess; import net.minecraft.core.RegistryAccess;
import net.minecraft.core.SectionPos;
import net.minecraft.core.registries.Registries; import net.minecraft.core.registries.Registries;
import net.minecraft.resources.Identifier; import net.minecraft.resources.Identifier;
import net.minecraft.resources.ResourceKey; import net.minecraft.resources.ResourceKey;
@@ -52,40 +63,59 @@ import net.minecraft.world.level.biome.Biome;
import net.minecraft.world.level.biome.BiomeManager; import net.minecraft.world.level.biome.BiomeManager;
import net.minecraft.world.level.biome.BiomeResolver; import net.minecraft.world.level.biome.BiomeResolver;
import net.minecraft.world.level.biome.BiomeSource; import net.minecraft.world.level.biome.BiomeSource;
import net.minecraft.world.level.biome.Climate;
import net.minecraft.world.level.biome.MobSpawnSettings; import net.minecraft.world.level.biome.MobSpawnSettings;
import net.minecraft.world.level.block.Blocks; import net.minecraft.world.level.block.Blocks;
import net.minecraft.world.level.block.EntityBlock;
import net.minecraft.world.level.block.entity.BlockEntity;
import net.minecraft.world.level.block.state.BlockState; import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.chunk.ChunkAccess; import net.minecraft.world.level.chunk.ChunkAccess;
import net.minecraft.world.level.chunk.ChunkGenerator; import net.minecraft.world.level.chunk.ChunkGenerator;
import net.minecraft.world.level.chunk.ChunkGeneratorStructureState; import net.minecraft.world.level.chunk.ChunkGeneratorStructureState;
import net.minecraft.world.level.chunk.LevelChunkSection; import net.minecraft.world.level.chunk.LevelChunkSection;
import net.minecraft.world.level.levelgen.GenerationStep;
import net.minecraft.world.level.levelgen.Heightmap; import net.minecraft.world.level.levelgen.Heightmap;
import net.minecraft.world.level.levelgen.RandomState; 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 net.minecraft.world.level.levelgen.blending.Blender; 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.templatesystem.StructureTemplateManager; import net.minecraft.world.level.levelgen.structure.templatesystem.StructureTemplateManager;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import java.io.File;
import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.EnumSet; import java.util.EnumSet;
import java.util.HashMap;
import java.util.LinkedHashSet;
import java.util.List; import java.util.List;
import java.util.Locale; import java.util.Locale;
import java.util.Map;
import java.util.Optional; import java.util.Optional;
import java.util.Set; import java.util.Set;
import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService; import java.util.concurrent.ExecutorService;
import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.IntBinaryOperator;
public final class IrisModdedChunkGenerator extends ChunkGenerator { public final class IrisModdedChunkGenerator extends ChunkGenerator {
private static final int WORLD_CHECK_SHIFT_RECORD_LIMIT = 4096;
private static final boolean WORLD_CHECK_ENABLED = Boolean.getBoolean("iris.worldcheck");
private static final Logger LOGGER = LoggerFactory.getLogger("Iris"); private static final Logger LOGGER = LoggerFactory.getLogger("Iris");
public static final MapCodec<IrisModdedChunkGenerator> CODEC = RecordCodecBuilder.mapCodec((RecordCodecBuilder.Instance<IrisModdedChunkGenerator> instance) -> instance.group( public static final MapCodec<IrisModdedChunkGenerator> CODEC = RecordCodecBuilder.mapCodec((RecordCodecBuilder.Instance<IrisModdedChunkGenerator> instance) -> instance.group(
BiomeSource.CODEC.fieldOf("biome_source").forGetter((IrisModdedChunkGenerator generator) -> generator.biomeSource), BiomeSource.CODEC.fieldOf("biome_source").forGetter((IrisModdedChunkGenerator generator) -> generator.serializedBiomeSource),
Codec.STRING.fieldOf("dimension").forGetter((IrisModdedChunkGenerator generator) -> generator.dimensionKey) Codec.STRING.fieldOf("dimension").forGetter((IrisModdedChunkGenerator generator) -> generator.dimensionKey)
).apply(instance, IrisModdedChunkGenerator::new)); ).apply(instance, IrisModdedChunkGenerator::new));
@@ -141,10 +171,12 @@ public final class IrisModdedChunkGenerator extends ChunkGenerator {
private final String dimensionKey; private final String dimensionKey;
private final String defaultPack; private final String defaultPack;
private final String defaultDimensionKey; private final String defaultDimensionKey;
private final ConcurrentHashMap<String, Holder<Biome>> biomeHolders = new ConcurrentHashMap<>(); private final BiomeSource serializedBiomeSource;
private final IrisModdedBiomeSource structureBiomeSource;
private final EngineBinding<Engine> engineBinding = new EngineBinding<>(60L, TimeUnit.SECONDS);
private final ConcurrentHashMap<Biome, Holder<Biome>> vanillaSpawnBiomes = new ConcurrentHashMap<>(); private final ConcurrentHashMap<Biome, Holder<Biome>> vanillaSpawnBiomes = new ConcurrentHashMap<>();
private final ConcurrentHashMap<SpawnTableKey, WeightedList<MobSpawnSettings.SpawnerData>> mergedSpawnTables = new ConcurrentHashMap<>(); private final ConcurrentHashMap<SpawnTableKey, WeightedList<MobSpawnSettings.SpawnerData>> mergedSpawnTables = new ConcurrentHashMap<>();
private final Set<String> missingBiomeWarnings = ConcurrentHashMap.newKeySet(); private final ConcurrentHashMap<NativeStructureStartKey, Integer> worldCheckStructureShifts = new ConcurrentHashMap<>();
private final AtomicBoolean announced = new AtomicBoolean(false); private final AtomicBoolean announced = new AtomicBoolean(false);
private volatile boolean vanillaSpawnBiomesInitialized; private volatile boolean vanillaSpawnBiomesInitialized;
private volatile Engine engine; private volatile Engine engine;
@@ -152,10 +184,20 @@ public final class IrisModdedChunkGenerator extends ChunkGenerator {
private volatile String activeDimensionKey; private volatile String activeDimensionKey;
private volatile long seedOverride = Long.MIN_VALUE; private volatile long seedOverride = Long.MIN_VALUE;
private volatile long lastChunkGenAt = 0L; private volatile long lastChunkGenAt = 0L;
private volatile Set<String> configuredStructureBiomeKeys;
private volatile ConfiguredPack configuredPack;
private volatile StructureStepCache structureStepCache;
public IrisModdedChunkGenerator(BiomeSource biomeSource, String dimensionKey) { public IrisModdedChunkGenerator(BiomeSource biomeSource, String dimensionKey) {
super(biomeSource); this(biomeSource, dimensionKey, new IrisModdedBiomeSource(biomeSource));
}
private IrisModdedChunkGenerator(BiomeSource serializedBiomeSource, String dimensionKey, IrisModdedBiomeSource structureBiomeSource) {
super(structureBiomeSource);
this.dimensionKey = dimensionKey; this.dimensionKey = dimensionKey;
this.serializedBiomeSource = serializedBiomeSource;
this.structureBiomeSource = structureBiomeSource;
this.structureBiomeSource.bind(this);
int colon = dimensionKey.indexOf(':'); int colon = dimensionKey.indexOf(':');
this.defaultPack = colon >= 0 ? dimensionKey.substring(0, colon) : dimensionKey; this.defaultPack = colon >= 0 ? dimensionKey.substring(0, colon) : dimensionKey;
this.defaultDimensionKey = colon >= 0 ? dimensionKey.substring(colon + 1) : dimensionKey; this.defaultDimensionKey = colon >= 0 ? dimensionKey.substring(colon + 1) : dimensionKey;
@@ -172,9 +214,12 @@ public final class IrisModdedChunkGenerator extends ChunkGenerator {
this.activeDimensionKey = packDimensionKey; this.activeDimensionKey = packDimensionKey;
this.seedOverride = seed; this.seedOverride = seed;
this.engine = null; this.engine = null;
this.configuredStructureBiomeKeys = null;
this.configuredPack = null;
this.engineBinding.reset();
this.announced.set(false); this.announced.set(false);
this.biomeHolders.clear(); this.structureBiomeSource.clearCaches();
this.missingBiomeWarnings.clear(); this.worldCheckStructureShifts.clear();
resetVanillaSpawnBiomes(); resetVanillaSpawnBiomes();
} }
@@ -184,9 +229,12 @@ public final class IrisModdedChunkGenerator extends ChunkGenerator {
ModdedWorldEngines.evict(level); ModdedWorldEngines.evict(level);
} }
this.engine = null; this.engine = null;
this.configuredStructureBiomeKeys = null;
this.configuredPack = null;
this.engineBinding.reset();
this.announced.set(false); this.announced.set(false);
this.biomeHolders.clear(); this.structureBiomeSource.clearCaches();
this.missingBiomeWarnings.clear(); this.worldCheckStructureShifts.clear();
resetVanillaSpawnBiomes(); resetVanillaSpawnBiomes();
} }
@@ -207,6 +255,107 @@ public final class IrisModdedChunkGenerator extends ChunkGenerator {
return CODEC; return CODEC;
} }
@Override
public ChunkGeneratorStructureState createState(HolderLookup<StructureSet> structureSets, RandomState randomState, long seed) {
return ChunkGeneratorStructureState.createForNormal(
randomState, seed, structureBiomeSource.forStructureState(structureSets), structureSets);
}
@Override
public Pair<BlockPos, Holder<Structure>> findNearestMapStructure(ServerLevel level, HolderSet<Structure> holders,
BlockPos pos, int radius,
boolean findUnexplored) {
Engine current = engineOrNull();
if (current == null) {
return super.findNearestMapStructure(level, holders, pos, radius, findUnexplored);
}
try {
Pair<BlockPos, Holder<Structure>> irisPlaced = findNearestIrisStructure(
level, holders, pos, Math.max(1, radius), current);
if (irisPlaced != null) {
return irisPlaced;
}
} catch (Throwable e) {
LOGGER.error("Iris-placed structure locate failed near {},{}", pos.getX(), pos.getZ(), e);
}
IrisImportedStructureControl control = current.getDimension().getImportedStructures();
if (control == null || !control.active()) {
return null;
}
HolderSet<Structure> reachable = filterReachableNativeStructures(level, holders, current, control);
if (reachable.size() == 0) {
return null;
}
try {
return super.findNearestMapStructure(level, reachable, pos, radius, findUnexplored);
} catch (Throwable e) {
LOGGER.error("Native structure locate failed near {},{}", pos.getX(), pos.getZ(), e);
return null;
}
}
public boolean isNativeStructureReachable(Holder<Structure> structure) {
return structure != null && structureBiomeSource.isStructureReachable(structure);
}
private Pair<BlockPos, Holder<Structure>> findNearestIrisStructure(ServerLevel level,
HolderSet<Structure> holders,
BlockPos pos, int radius,
Engine current) {
Registry<Structure> registry = level.registryAccess().lookupOrThrow(Registries.STRUCTURE);
BlockPos best = null;
Holder<Structure> bestHolder = null;
long bestDistance = Long.MAX_VALUE;
for (Holder<Structure> holder : holders) {
Identifier id = registry.getKey(holder.value());
if (id == null) {
continue;
}
IrisStructureLocator.LocateResult result = IrisStructureLocator.locate(
current, id.toString(), pos.getX(), pos.getZ(), radius);
if (result.status() == IrisStructureLocator.LocateStatus.SEARCH_LIMIT_REACHED) {
continue;
}
if (!result.found()) {
continue;
}
long dx = (long) result.originX() - pos.getX();
long dz = (long) result.originZ() - pos.getZ();
long distance = dx * dx + dz * dz;
if (distance < bestDistance) {
bestDistance = distance;
best = new BlockPos(result.originX(), result.baseY(), result.originZ());
bestHolder = holder;
}
}
return best == null ? null : Pair.of(best, bestHolder);
}
private HolderSet<Structure> filterReachableNativeStructures(ServerLevel level, HolderSet<Structure> holders,
Engine current,
IrisImportedStructureControl control) {
try {
Registry<Structure> registry = level.registryAccess().lookupOrThrow(Registries.STRUCTURE);
List<Holder<Structure>> kept = new ArrayList<>(holders.size());
for (Holder<Structure> holder : holders) {
Identifier id = registry.getKey(holder.value());
String key = id == null ? null : id.toString();
IrisNativeStructureDecision decision = control.resolve(
key, NativeStructurePostProcessor.isUndergroundStep(holder.value().step()));
if (key == null || !decision.generate()
|| IrisStructureLocator.suppressesVanilla(current, key)
|| !structureBiomeSource.isStructureReachable(holder)) {
continue;
}
kept.add(holder);
}
return kept.size() == holders.size() ? holders : HolderSet.direct(kept);
} catch (Throwable error) {
LOGGER.error("Iris could not resolve native structure biome reachability; native locate failed closed", error);
return HolderSet.direct(List.of());
}
}
private ServerLevel boundLevel() { private ServerLevel boundLevel() {
MinecraftServer server = ModdedEngineBootstrap.currentServer(); MinecraftServer server = ModdedEngineBootstrap.currentServer();
if (server == null) { if (server == null) {
@@ -225,17 +374,53 @@ public final class IrisModdedChunkGenerator extends ChunkGenerator {
if (cached != null) { if (cached != null) {
return cached; return cached;
} }
ServerLevel level = boundLevel();
if (level == null) {
throw new IllegalStateException("Iris generator '" + dimensionKey + "' has no bound ServerLevel yet");
}
return bindEngine(level);
}
void bindLevel(ServerLevel level) {
if (level.getChunkSource().getGenerator() != this) {
throw new IllegalArgumentException("ServerLevel does not use Iris generator '" + dimensionKey + "'");
}
bindEngine(level);
}
private Engine bindEngine(ServerLevel level) {
Engine cached = engine;
if (cached != null && !cached.isClosed() && cached.getComplex() != null) {
engineBinding.complete(cached);
return cached;
}
synchronized (this) { synchronized (this) {
if (engine != null) { Engine existing = engine;
return engine; if (existing != null && !existing.isClosed() && existing.getComplex() != null) {
engineBinding.complete(existing);
return existing;
} }
ServerLevel level = boundLevel(); try {
if (level == null) { Engine created = ModdedWorldEngines.get(level, activePack, activeDimensionKey, seedOverride);
throw new IllegalStateException("Iris generator '" + dimensionKey + "' has no bound ServerLevel yet"); if (created.isClosed() || created.getComplex() == null) {
throw new IllegalStateException("Iris generator '" + dimensionKey
+ "' created an engine without a ready biome complex");
}
engine = created;
configuredStructureBiomeKeys = null;
structureBiomeSource.clearCaches();
engineBinding.complete(created);
return created;
} catch (Throwable error) {
engineBinding.fail(error);
if (error instanceof RuntimeException runtimeException) {
throw runtimeException;
}
if (error instanceof Error fatalError) {
throw fatalError;
}
throw new IllegalStateException("Iris generator '" + dimensionKey + "' failed to bind", error);
} }
Engine created = ModdedWorldEngines.get(level, activePack, activeDimensionKey, seedOverride);
engine = created;
return created;
} }
} }
@@ -251,6 +436,141 @@ public final class IrisModdedChunkGenerator extends ChunkGenerator {
} }
} }
Engine structureEngineOrNull() {
return engineOrNull();
}
Engine awaitStructureEngine() {
Engine current = engine;
if (current != null && !current.isClosed() && current.getComplex() != null) {
return current;
}
Engine bound = engineBinding.await(dimensionKey);
if (bound.isClosed() || bound.getComplex() == null) {
throw new IllegalStateException("Iris generator '" + dimensionKey
+ "' completed bootstrap without a ready biome complex");
}
return bound;
}
private Engine requireDataQueryEngine(String operation) {
Engine current = engine;
if (current != null && !current.isClosed() && current.getComplex() != null) {
return current;
}
ServerLevel level = boundLevel();
if (level != null) {
return bindEngine(level);
}
MinecraftServer server = ModdedEngineBootstrap.currentServer();
if (server == null) {
throw new IllegalStateException("Iris generator '" + dimensionKey + "' cannot answer "
+ operation + " without an active server");
}
if (server.isSameThread()) {
throw new IllegalStateException("Iris generator '" + dimensionKey + "' cannot answer "
+ operation + " on the server thread before its ServerLevel is bound");
}
return awaitStructureEngine();
}
long visibleBiomeSeed() {
long configuredSeed = seedOverride;
if (configuredSeed != Long.MIN_VALUE) {
return configuredSeed;
}
ServerLevel level = boundLevel();
if (level != null) {
return level.getSeed();
}
Engine current = engine;
return current == null ? 0L : current.getWorld().getRawWorldSeed();
}
Set<String> configuredStructureBiomeKeys() {
Set<String> cached = configuredStructureBiomeKeys;
if (cached != null) {
return cached;
}
synchronized (this) {
if (configuredStructureBiomeKeys != null) {
return configuredStructureBiomeKeys;
}
Engine current = engine;
Iterable<IrisBiome> biomes;
String namespace;
if (current != null && !current.isClosed()) {
biomes = current.getAllBiomes();
namespace = current.getDimension().getLoadKey();
} else {
ConfiguredPack configured = configuredPack();
Set<String> resolved = collectConfiguredBiomeKeys(configured.dimension(), configured.data());
configuredStructureBiomeKeys = resolved;
return resolved;
}
Set<String> resolved = collectConfiguredBiomeKeys(biomes, namespace);
configuredStructureBiomeKeys = resolved;
return resolved;
}
}
private ConfiguredPack configuredPack() {
ConfiguredPack cached = configuredPack;
if (cached != null) {
return cached;
}
synchronized (this) {
if (configuredPack != null) {
return configuredPack;
}
File packDirectory = ModdedWorldEngines.resolvePack(activePack, activeDimensionKey);
IrisData data = IrisData.get(packDirectory);
IrisDimension dimension = data.getDimensionLoader().load(activeDimensionKey);
if (dimension == null) {
throw new IllegalStateException("Iris dimension '" + activeDimensionKey
+ "' missing from pack " + packDirectory.getAbsolutePath());
}
ConfiguredPack resolved = new ConfiguredPack(data, dimension, dimensionMetadata(dimension));
configuredPack = resolved;
return resolved;
}
}
static DimensionMetadata dimensionMetadata(IrisDimension dimension) {
int minY = dimension.getMinHeight();
int maxY = dimension.getMaxHeight();
if (maxY <= minY) {
throw new IllegalStateException("Iris dimension '" + dimension.getLoadKey()
+ "' has invalid height range " + minY + ".." + maxY);
}
return new DimensionMetadata(minY, maxY, minY + dimension.getFluidHeight());
}
static Set<String> collectConfiguredBiomeKeys(IrisDimension dimension, IrisData data) {
return collectConfiguredBiomeKeys(dimension.getReachableBiomes(() -> data), dimension.getLoadKey());
}
static Set<String> collectConfiguredBiomeKeys(Iterable<IrisBiome> biomes, String dimensionLoadKey) {
LinkedHashSet<String> keys = new LinkedHashSet<>();
String namespace = dimensionLoadKey.toLowerCase(Locale.ROOT);
for (IrisBiome irisBiome : biomes) {
if (irisBiome == null) {
continue;
}
Identifier derivative = Identifier.tryParse(irisBiome.getVanillaDerivativeKey());
if (derivative != null) {
keys.add(derivative.toString().toLowerCase(Locale.ROOT));
}
if (!irisBiome.isCustom()) {
continue;
}
for (IrisBiomeCustom customBiome : irisBiome.getCustomDerivitives()) {
keys.add(namespace + ":" + customBiome.getId().toLowerCase(Locale.ROOT));
}
}
return Set.copyOf(keys);
}
public String dimensionKey() { public String dimensionKey() {
return dimensionKey; return dimensionKey;
} }
@@ -268,8 +588,9 @@ public final class IrisModdedChunkGenerator extends ChunkGenerator {
} }
public void onHotload() { public void onHotload() {
biomeHolders.clear(); configuredStructureBiomeKeys = null;
missingBiomeWarnings.clear(); structureBiomeSource.clearCaches();
worldCheckStructureShifts.clear();
resetVanillaSpawnBiomes(); resetVanillaSpawnBiomes();
} }
@@ -312,7 +633,7 @@ public final class IrisModdedChunkGenerator extends ChunkGenerator {
} }
String namespace = current.getDimension().getLoadKey().toLowerCase(Locale.ROOT); String namespace = current.getDimension().getLoadKey().toLowerCase(Locale.ROOT);
for (IrisBiome irisBiome : current.getDimension().getAllBiomes(current)) { for (IrisBiome irisBiome : current.getDimension().getReachableBiomes(current)) {
if (irisBiome == null || !irisBiome.isCustom()) { if (irisBiome == null || !irisBiome.isCustom()) {
continue; continue;
} }
@@ -348,6 +669,13 @@ public final class IrisModdedChunkGenerator extends ChunkGenerator {
vanillaSpawnBiomesInitialized = false; vanillaSpawnBiomesInitialized = false;
} }
@Override
public CompletableFuture<ChunkAccess> createBiomes(RandomState randomState, Blender blender,
StructureManager structureManager, ChunkAccess chunk) {
chunk.fillBiomesFromNoise(structureBiomeSource::getVisibleNoiseBiome, randomState.sampler());
return CompletableFuture.completedFuture(chunk);
}
@Override @Override
public CompletableFuture<ChunkAccess> fillFromNoise(Blender blender, RandomState randomState, StructureManager structureManager, ChunkAccess chunk) { public CompletableFuture<ChunkAccess> fillFromNoise(Blender blender, RandomState randomState, StructureManager structureManager, ChunkAccess chunk) {
Engine generationEngine = engine(); Engine generationEngine = engine();
@@ -363,20 +691,18 @@ public final class IrisModdedChunkGenerator extends ChunkGenerator {
int dimMaxY = generationEngine.getMaxHeight(); int dimMaxY = generationEngine.getMaxHeight();
int height = dimMaxY - dimMinY; int height = dimMaxY - dimMinY;
PlatformBlockState air = IrisPlatforms.get().registries().air(); PlatformBlockState air = IrisPlatforms.get().registries().air();
Registry<Biome> biomeRegistry = structureManager.registryAccess().lookupOrThrow(Registries.BIOME);
if (PARALLEL_CHUNK_SYSTEM) { if (PARALLEL_CHUNK_SYSTEM) {
return CompletableFuture.completedFuture( return CompletableFuture.completedFuture(
generateTerrain(chunk, generationEngine, pos, dimMinY, height, air, biomeRegistry, randomState)); generateTerrain(chunk, generationEngine, pos, dimMinY, height, air));
} }
return CompletableFuture.supplyAsync( return CompletableFuture.supplyAsync(
() -> generateTerrain(chunk, generationEngine, pos, dimMinY, height, air, biomeRegistry, randomState), () -> generateTerrain(chunk, generationEngine, pos, dimMinY, height, air),
genPool); genPool);
} }
private ChunkAccess generateTerrain(ChunkAccess chunk, Engine generationEngine, ChunkPos pos, private ChunkAccess generateTerrain(ChunkAccess chunk, Engine generationEngine, ChunkPos pos,
int dimMinY, int height, PlatformBlockState air, int dimMinY, int height, PlatformBlockState air) {
Registry<Biome> biomeRegistry, RandomState randomState) {
ModdedBlockBuffer blocks = new ModdedBlockBuffer(height, air); ModdedBlockBuffer blocks = new ModdedBlockBuffer(height, air);
Hunk<PlatformBiome> biomes = Hunk.newArrayHunk(16, height, 16); Hunk<PlatformBiome> biomes = Hunk.newArrayHunk(16, height, 16);
try { try {
@@ -394,50 +720,32 @@ public final class IrisModdedChunkGenerator extends ChunkGenerator {
} }
writeBlocks(chunk, blocks, dimMinY, height); writeBlocks(chunk, blocks, dimMinY, height);
Heightmap.primeHeightmaps(chunk, EnumSet.of(Heightmap.Types.WORLD_SURFACE_WG, Heightmap.Types.OCEAN_FLOOR_WG)); writeTerrainHeightmaps(chunk, generationEngine, pos, height);
chunk.fillBiomesFromNoise(new HunkBiomeResolver(this, biomes, biomeRegistry, pos, dimMinY, height), randomState.sampler()); Heightmap.primeHeightmaps(chunk, EnumSet.of(
Heightmap.Types.MOTION_BLOCKING,
Heightmap.Types.MOTION_BLOCKING_NO_LEAVES));
ModdedWorldManager.enqueueGenerated(generationEngine, pos.x(), pos.z()); ModdedWorldManager.enqueueGenerated(generationEngine, pos.x(), pos.z());
return chunk; return chunk;
} }
private Holder<Biome> fallbackBiome(Registry<Biome> registry) { private void writeTerrainHeightmaps(ChunkAccess chunk, Engine generationEngine, ChunkPos pos, int height) {
Holder<Biome> existing = biomeHolders.get("minecraft:plains"); int baseX = pos.getMinBlockX();
if (existing != null) { int baseZ = pos.getMinBlockZ();
return existing; writeTerrainHeightmap(chunk, Heightmap.Types.WORLD_SURFACE_WG, height,
} (x, z) -> generationEngine.getHeight(baseX + x, baseZ + z, false) + 1);
Holder<Biome> resolved = registry.get(Identifier.fromNamespaceAndPath("minecraft", "plains")) writeTerrainHeightmap(chunk, Heightmap.Types.OCEAN_FLOOR_WG, height,
.<Holder<Biome>>map((Holder.Reference<Biome> reference) -> reference) (x, z) -> generationEngine.getHeight(baseX + x, baseZ + z, true) + 1);
.orElseThrow(() -> new IllegalStateException("minecraft:plains missing from biome registry"));
Holder<Biome> raced = biomeHolders.putIfAbsent("minecraft:plains", resolved);
return raced != null ? raced : resolved;
} }
private Holder<Biome> holderFor(Registry<Biome> registry, PlatformBiome biome) { private void writeTerrainHeightmap(ChunkAccess chunk, Heightmap.Types type, int height,
if (biome == null) { IntBinaryOperator heightResolver) {
return fallbackBiome(registry); Heightmap heightmap = chunk.getOrCreateHeightmapUnprimed(type);
} heightmap.setRawData(chunk, type, ModdedHeightmaps.terrainRawData(height, heightResolver));
String key = biome.key();
Holder<Biome> existing = biomeHolders.get(key);
if (existing != null) {
return existing;
}
Identifier identifier = Identifier.tryParse(key);
Optional<Holder.Reference<Biome>> reference = identifier == null ? Optional.empty() : registry.get(identifier);
Holder<Biome> resolved = reference.<Holder<Biome>>map((Holder.Reference<Biome> value) -> value).orElseGet(() -> {
if (missingBiomeWarnings.size() < 256 && missingBiomeWarnings.add(key)) {
LOGGER.warn("Iris biome '{}' (pack {}) is not in the biome registry; writing minecraft:plains. Restart so the forced datapack registers the pack biomes.", key, activePack);
}
return fallbackBiome(registry);
});
Holder<Biome> raced = biomeHolders.putIfAbsent(key, resolved);
return raced != null ? raced : resolved;
} }
public BiomeResolver regenBiomeResolver(Registry<Biome> registry, Hunk<PlatformBiome> biomes, ChunkPos pos) { public BiomeResolver regenBiomeResolver() {
Engine current = engine(); engine();
int dimMinY = current.getMinHeight(); return structureBiomeSource::getVisibleNoiseBiome;
int height = current.getMaxHeight() - dimMinY;
return new HunkBiomeResolver(this, biomes, registry, pos, dimMinY, height);
} }
private void writeBlocks(ChunkAccess chunk, ModdedBlockBuffer blocks, int dimMinY, int height) { private void writeBlocks(ChunkAccess chunk, ModdedBlockBuffer blocks, int dimMinY, int height) {
@@ -445,6 +753,8 @@ public final class IrisModdedChunkGenerator extends ChunkGenerator {
int chunkMaxY = chunkMinY + chunk.getHeight(); int chunkMaxY = chunkMinY + chunk.getHeight();
int from = Math.max(dimMinY, chunkMinY); int from = Math.max(dimMinY, chunkMinY);
int to = Math.min(dimMinY + height, chunkMaxY); int to = Math.min(dimMinY + height, chunkMaxY);
int baseX = chunk.getPos().getMinBlockX();
int baseZ = chunk.getPos().getMinBlockZ();
for (int y = from; y < to; ) { for (int y = from; y < to; ) {
int sectionIndex = chunk.getSectionIndex(y); int sectionIndex = chunk.getSectionIndex(y);
@@ -462,7 +772,11 @@ public final class IrisModdedChunkGenerator extends ChunkGenerator {
continue; continue;
} }
PlatformBlockState state = blocks.getRaw(x, bufferY, z); PlatformBlockState state = blocks.getRaw(x, bufferY, z);
section.setBlockState(x, localY, z, (BlockState) state.nativeHandle(), false); BlockState blockState = (BlockState) state.nativeHandle();
section.setBlockState(x, localY, z, blockState, false);
if (blockState.hasBlockEntity()) {
createDefaultBlockEntity(chunk, new BlockPos(baseX + x, blockY, baseZ + z), blockState);
}
} }
} }
} }
@@ -473,30 +787,13 @@ public final class IrisModdedChunkGenerator extends ChunkGenerator {
} }
} }
private static final class HunkBiomeResolver implements BiomeResolver { static void createDefaultBlockEntity(ChunkAccess chunk, BlockPos position, BlockState state) {
private final IrisModdedChunkGenerator generator; if (!(state.getBlock() instanceof EntityBlock entityBlock)) {
private final Hunk<PlatformBiome> biomes; return;
private final Registry<Biome> registry;
private final ChunkPos pos;
private final int dimMinY;
private final int height;
private HunkBiomeResolver(IrisModdedChunkGenerator generator, Hunk<PlatformBiome> biomes, Registry<Biome> registry, ChunkPos pos, int dimMinY, int height) {
this.generator = generator;
this.biomes = biomes;
this.registry = registry;
this.pos = pos;
this.dimMinY = dimMinY;
this.height = height;
} }
BlockEntity blockEntity = entityBlock.newBlockEntity(position, state);
@Override if (blockEntity != null) {
public Holder<Biome> getNoiseBiome(int quartX, int quartY, int quartZ, Climate.Sampler sampler) { chunk.setBlockEntity(blockEntity);
int localX = QuartPos.toBlock(quartX) - pos.getMinBlockX();
int localZ = QuartPos.toBlock(quartZ) - pos.getMinBlockZ();
int bufferY = Math.max(0, Math.min(height - 1, QuartPos.toBlock(quartY) - dimMinY));
PlatformBiome biome = biomes.get(localX, bufferY, localZ);
return generator.holderFor(registry, biome);
} }
} }
@@ -510,14 +807,200 @@ public final class IrisModdedChunkGenerator extends ChunkGenerator {
@Override @Override
public void applyBiomeDecoration(WorldGenLevel level, ChunkAccess chunk, StructureManager structureManager) { public void applyBiomeDecoration(WorldGenLevel level, ChunkAccess chunk, StructureManager structureManager) {
if (importedControl().active()) {
placeVanillaStructures(level, chunk, structureManager);
}
} }
@Override @Override
public void createStructures(RegistryAccess registryAccess, ChunkGeneratorStructureState structureState, StructureManager structureManager, ChunkAccess chunk, StructureTemplateManager templateManager, ResourceKey<Level> levelKey) { public void createStructures(RegistryAccess registryAccess, ChunkGeneratorStructureState structureState, StructureManager structureManager, ChunkAccess chunk, StructureTemplateManager templateManager, ResourceKey<Level> levelKey) {
if (!importedControl().active()) {
return;
}
Map<Structure, StructureStart> previousStarts = new HashMap<>(chunk.getAllStarts());
super.createStructures(registryAccess, structureState, structureManager, chunk, templateManager, levelKey);
adjustGeneratedStructures(registryAccess, chunk, previousStarts);
} }
@Override @Override
public void createReferences(WorldGenLevel level, StructureManager structureManager, ChunkAccess chunk) { public void createReferences(WorldGenLevel level, StructureManager structureManager, ChunkAccess chunk) {
super.createReferences(level, structureManager, chunk);
}
private void adjustGeneratedStructures(RegistryAccess registryAccess, ChunkAccess chunk,
Map<Structure, StructureStart> previousStarts) {
Registry<Structure> registry = registryAccess.lookupOrThrow(Registries.STRUCTURE);
IrisImportedStructureControl control = importedControl();
for (Map.Entry<Structure, StructureStart> entry : chunk.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)) {
chunk.setStartForStructure(structure, StructureStart.INVALID_START);
continue;
}
int offsetY;
try {
offsetY = NativeStructurePostProcessor.applyVerticalShift(
start,
decision.yShift(),
chunk.getMinY(),
chunk.getMinY() + chunk.getHeight());
} catch (RuntimeException error) {
chunk.setStartForStructure(structure, StructureStart.INVALID_START);
LOGGER.error("Iris rejected native structure {} in chunk {},{} because its vertical bounds are invalid",
structureId, chunk.getPos().x(), chunk.getPos().z(), error);
continue;
}
recordWorldCheckStructureShift(structureId, start.getChunkPos(), offsetY);
}
}
private void placeVanillaStructures(WorldGenLevel world, ChunkAccess chunk, StructureManager structureManager) {
if (!structureManager.shouldGenerateStructures()) {
return;
}
ChunkPos chunkPos = chunk.getPos();
SectionPos sectionPos = SectionPos.of(chunkPos, world.getMinSectionY());
BlockPos origin = sectionPos.origin();
Registry<Structure> registry = world.registryAccess().lookupOrThrow(Registries.STRUCTURE);
List<List<Structure>> byStep = structuresByStep(registry);
WorldgenRandom random = new WorldgenRandom(new XoroshiroRandomSource(RandomSupport.generateUniqueSeed()));
long decorationSeed = 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.get(step)) {
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)) {
try {
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) {
LOGGER.error("Iris failed to resolve native structure {} in chunk {},{}",
structureId, chunkPos.x(), chunkPos.z(), e);
}
}
index++;
}
}
try {
NativeStructurePostProcessor.clearIntersectingVegetation(
world, chunk, area, vegetationTargets);
} catch (Throwable e) {
LOGGER.error("Iris failed to clear vegetation from native structures in chunk {},{}",
chunkPos.x(), chunkPos.z(), e);
}
for (NativePlacementGroup group : placementGroups) {
random.setFeatureSeed(decorationSeed, group.featureIndex(), group.step());
try {
for (StructureStart start : group.starts()) {
placeVanillaStructure(world, structureManager, random, area, chunkPos,
group.structureId(), start, group.decision());
}
} catch (Throwable e) {
LOGGER.error("Iris failed to place native structure {} in chunk {},{}",
group.structureId(), chunkPos.x(), chunkPos.z(), e);
}
}
}
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 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 void recordWorldCheckStructureShift(String structureId, ChunkPos startChunk, int offsetY) {
if (!WORLD_CHECK_ENABLED || structureId == null) {
return;
}
if (worldCheckStructureShifts.size() >= WORLD_CHECK_SHIFT_RECORD_LIMIT) {
worldCheckStructureShifts.clear();
}
worldCheckStructureShifts.put(new NativeStructureStartKey(structureId, startChunk.pack()), offsetY);
}
Integer worldCheckStructureShift(String structureId, ChunkPos startChunk) {
if (structureId == null || startChunk == null) {
return null;
}
return worldCheckStructureShifts.get(new NativeStructureStartKey(structureId, startChunk.pack()));
}
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 BlockState blockState)) {
return Blocks.COBBLESTONE.defaultBlockState();
}
return blockState;
}
private IrisImportedStructureControl importedControl() {
return engine().getDimension().getImportedStructures();
}
private BoundingBox writableArea(ChunkAccess chunk) {
ChunkPos chunkPos = chunk.getPos();
int minX = chunkPos.getMinBlockX();
int minZ = chunkPos.getMinBlockZ();
int minY = chunk.getMinY();
int maxY = minY + chunk.getHeight() - 1;
return new BoundingBox(minX, minY, minZ, minX + 15, maxY, minZ + 15);
} }
@Override @Override
@@ -526,48 +1009,58 @@ public final class IrisModdedChunkGenerator extends ChunkGenerator {
@Override @Override
public int getGenDepth() { public int getGenDepth() {
Engine current = engineOrNull(); Engine current = engine;
return current == null ? 384 : current.getMaxHeight() - current.getMinHeight(); return current == null || current.isClosed()
? configuredPack().metadata().depth()
: current.getMaxHeight() - current.getMinHeight();
} }
@Override @Override
public int getSeaLevel() { public int getSeaLevel() {
Engine current = engineOrNull(); Engine current = engine;
return current == null ? 63 : current.getMinHeight() + current.getDimension().getFluidHeight(); return current == null || current.isClosed()
? configuredPack().metadata().seaLevel()
: current.getMinHeight() + current.getDimension().getFluidHeight();
} }
@Override @Override
public int getMinY() { public int getMinY() {
Engine current = engineOrNull(); Engine current = engine;
return current == null ? -64 : current.getMinHeight(); return current == null || current.isClosed()
? configuredPack().metadata().minY()
: current.getMinHeight();
}
@Override
public int getSpawnHeight(LevelHeightAccessor heightAccessor) {
return clampSpawnHeight(heightAccessor.getMinY(), heightAccessor.getHeight());
}
static int clampSpawnHeight(int minY, int height) {
int minimum = minY + 1;
int maximum = minY + height - 2;
return Math.max(minimum, Math.min(maximum, 96));
} }
@Override @Override
public int getBaseHeight(int x, int z, Heightmap.Types type, LevelHeightAccessor heightAccessor, RandomState randomState) { public int getBaseHeight(int x, int z, Heightmap.Types type, LevelHeightAccessor heightAccessor, RandomState randomState) {
Engine current = engineOrNull(); Engine current = requireDataQueryEngine("base height");
if (current == null) { boolean ignoreFluid = !type.isOpaque().test(Blocks.WATER.defaultBlockState());
return heightAccessor.getMinY() + Math.min(heightAccessor.getHeight(), 64); return heightAccessor.getMinY() + current.getHeight(x, z, ignoreFluid) + 1;
}
boolean ignoreFluid = type == Heightmap.Types.OCEAN_FLOOR || type == Heightmap.Types.OCEAN_FLOOR_WG;
return current.getMinHeight() + current.getHeight(x, z, ignoreFluid) + 1;
} }
@Override @Override
public NoiseColumn getBaseColumn(int x, int z, LevelHeightAccessor heightAccessor, RandomState randomState) { public NoiseColumn getBaseColumn(int x, int z, LevelHeightAccessor heightAccessor, RandomState randomState) {
int minY = heightAccessor.getMinY(); int minY = heightAccessor.getMinY();
BlockState[] states = new BlockState[heightAccessor.getHeight()]; BlockState[] states = new BlockState[heightAccessor.getHeight()];
Engine current = engineOrNull(); Engine current = requireDataQueryEngine("base column");
BlockState airState = Blocks.AIR.defaultBlockState(); BlockState airState = Blocks.AIR.defaultBlockState();
if (current == null) { int surface = current.getHeight(x, z, true);
Arrays.fill(states, airState); int fluid = current.getHeight(x, z, false);
return new NoiseColumn(minY, states);
}
int surface = current.getMinHeight() + current.getHeight(x, z, true);
int fluid = current.getMinHeight() + current.getDimension().getFluidHeight();
BlockState stone = Blocks.STONE.defaultBlockState(); BlockState stone = Blocks.STONE.defaultBlockState();
BlockState water = Blocks.WATER.defaultBlockState(); BlockState water = Blocks.WATER.defaultBlockState();
int solidEnd = Math.max(0, Math.min(states.length, surface - minY + 1)); int solidEnd = Math.max(0, Math.min(states.length, surface + 1));
int fluidEnd = Math.max(solidEnd, Math.max(0, Math.min(states.length, fluid - minY + 1))); int fluidEnd = Math.max(solidEnd, Math.max(0, Math.min(states.length, fluid + 1)));
Arrays.fill(states, 0, solidEnd, stone); Arrays.fill(states, 0, solidEnd, stone);
Arrays.fill(states, solidEnd, fluidEnd, water); Arrays.fill(states, solidEnd, fluidEnd, water);
Arrays.fill(states, fluidEnd, states.length, airState); Arrays.fill(states, fluidEnd, states.length, airState);
@@ -581,4 +1074,62 @@ public final class IrisModdedChunkGenerator extends ChunkGenerator {
private record SpawnTableKey(Biome biome, MobCategory category) { private record SpawnTableKey(Biome biome, MobCategory category) {
} }
private record StructureStepCache(Registry<Structure> registry, List<List<Structure>> structures) {
}
private record NativeStructureStartKey(String structureId, long chunkPosition) {
}
private record NativePlacementGroup(String structureId, IrisNativeStructureDecision decision,
int featureIndex, int step, List<StructureStart> starts) {
}
record DimensionMetadata(int minY, int maxY, int seaLevel) {
int depth() {
return maxY - minY;
}
}
private record ConfiguredPack(IrisData data, IrisDimension dimension, DimensionMetadata metadata) {
}
static final class EngineBinding<T> {
private final long timeout;
private final TimeUnit timeoutUnit;
private volatile CompletableFuture<T> future = new CompletableFuture<>();
EngineBinding(long timeout, TimeUnit timeoutUnit) {
this.timeout = timeout;
this.timeoutUnit = timeoutUnit;
}
T await(String dimensionKey) {
try {
return future.get(timeout, timeoutUnit);
} catch (InterruptedException error) {
Thread.currentThread().interrupt();
throw new IllegalStateException("Interrupted while waiting for Iris generator '"
+ dimensionKey + "' to bind", error);
} catch (ExecutionException error) {
throw new IllegalStateException("Iris generator '" + dimensionKey + "' failed to bind",
error.getCause());
} catch (TimeoutException error) {
throw new IllegalStateException("Timed out waiting for Iris generator '"
+ dimensionKey + "' to bind", error);
}
}
void complete(T value) {
future.complete(value);
}
void fail(Throwable error) {
future.completeExceptionally(error);
}
void reset() {
future = new CompletableFuture<>();
}
}
} }
@@ -0,0 +1,257 @@
/*
* Iris is a World Generator for Minecraft 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.modded;
import art.arcane.iris.core.loader.IrisData;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.object.IrisBiome;
import art.arcane.iris.engine.object.IrisBlockData;
import art.arcane.iris.engine.object.IrisBlockDrops;
import art.arcane.iris.engine.object.IrisLoot;
import art.arcane.iris.engine.object.IrisMarker;
import art.arcane.iris.engine.object.IrisRegion;
import art.arcane.iris.spi.PlatformBlockState;
import art.arcane.volmlib.util.collection.KList;
import art.arcane.volmlib.util.math.RNG;
import art.arcane.volmlib.util.matter.MatterMarker;
import net.minecraft.core.BlockPos;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.util.Mth;
import net.minecraft.util.RandomSource;
import net.minecraft.world.entity.EntityTypes;
import net.minecraft.world.entity.item.ItemEntity;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.chunk.ChunkGenerator;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.concurrent.ConcurrentHashMap;
public final class ModdedBlockBreakHandler {
private static final Logger LOGGER = LoggerFactory.getLogger("Iris");
private static final ConcurrentHashMap<BreakKey, PendingBreak> PENDING = new ConcurrentHashMap<>();
private ModdedBlockBreakHandler() {
}
public static void prepare(ServerLevel level, BlockPos position, BlockState brokenState) {
if (engineFor(level) == null) {
return;
}
BreakKey key = new BreakKey(level, position.asLong());
PendingBreak pending = new PendingBreak(brokenState);
PENDING.put(key, pending);
ModdedScheduler scheduler = ModdedEngineBootstrap.schedulerOrNull();
if (scheduler != null) {
scheduler.laterGlobal(() -> finishPending(key, pending, position), 1);
}
}
public static void clear() {
PENDING.clear();
}
public static Result complete(ServerLevel level, BlockPos position, BlockState fallbackState) {
BreakKey key = new BreakKey(level, position.asLong());
PendingBreak pending = PENDING.remove(key);
BlockState brokenState = pending == null ? fallbackState : pending.brokenState();
return evaluateSafely(level, position, brokenState);
}
public static Result completePrepared(ServerLevel level, BlockPos position) {
BreakKey key = new BreakKey(level, position.asLong());
PendingBreak pending = PENDING.get(key);
if (pending == null || level.getBlockState(position).equals(pending.brokenState())) {
return null;
}
return PENDING.remove(key, pending) ? evaluateSafely(level, position, pending.brokenState()) : null;
}
public static void spawn(ServerLevel level, BlockPos position, KList<ItemStack> drops) {
for (ItemStack stack : drops) {
ItemEntity item = createDrop(level, position, stack);
if (item != null) {
level.addFreshEntity(item);
}
}
}
public static ItemEntity createDrop(ServerLevel level, BlockPos position, ItemStack stack) {
if (stack == null || stack.isEmpty()) {
return null;
}
RandomSource random = level.getRandom();
double x = position.getX() + 0.5D + Mth.nextDouble(random, -0.25D, 0.25D);
double y = position.getY() + 0.5D + Mth.nextDouble(random, -0.25D, 0.25D) - EntityTypes.ITEM.getHeight() / 2D;
double z = position.getZ() + 0.5D + Mth.nextDouble(random, -0.25D, 0.25D);
ItemEntity item = new ItemEntity(level, x, y, z, stack);
item.setDefaultPickUpDelay();
return item;
}
private static void finishPending(BreakKey key, PendingBreak pending, BlockPos position) {
if (!PENDING.remove(key, pending)) {
return;
}
ServerLevel level = key.level();
if (level.getBlockState(position).equals(pending.brokenState())) {
return;
}
Result result = evaluateSafely(level, position, pending.brokenState());
spawn(level, position, result.drops());
}
private static Result evaluateSafely(ServerLevel level, BlockPos position, BlockState brokenState) {
try {
return evaluate(level, position, brokenState);
} catch (Throwable error) {
LOGGER.error("Iris block-break processing failed at {},{},{} in {}", position.getX(), position.getY(), position.getZ(),
level.dimension().identifier(), error);
return Result.empty();
}
}
private static Result evaluate(ServerLevel level, BlockPos position, BlockState brokenState) {
Engine engine = engineFor(level);
if (engine == null || engine.isClosed()) {
return Result.empty();
}
removeMarker(engine, position);
KList<IrisBlockDrops> providers = providers(engine, position, brokenState);
if (providers.isEmpty()) {
return Result.empty();
}
KList<ItemStack> drops = new KList<>();
boolean replaceVanillaDrops = false;
for (IrisBlockDrops provider : providers) {
replaceVanillaDrops |= provider.isReplaceVanillaDrops();
for (IrisLoot loot : provider.getDrops()) {
if (RNG.r.i(1, loot.getRarity()) != loot.getRarity()) {
continue;
}
ItemStack stack = ModdedItemTranslator.stack(loot, RNG.r, level);
if (stack != null && !stack.isEmpty()) {
drops.add(stack);
}
}
}
return new Result(drops, replaceVanillaDrops);
}
private static void removeMarker(Engine engine, BlockPos position) {
int mantleY = position.getY() - engine.getMinHeight();
MatterMarker marker = engine.getMantle().getMantle().get(position.getX(), mantleY, position.getZ(), MatterMarker.class);
if (marker == null) {
return;
}
String tag = marker.getTag();
if ("cave_floor".equals(tag) || "cave_ceiling".equals(tag)) {
return;
}
IrisMarker configured = engine.getData().getMarkerLoader().load(tag);
if (configured == null || configured.isRemoveOnChange()) {
engine.getMantle().getMantle().remove(position.getX(), mantleY, position.getZ(), MatterMarker.class);
}
}
private static KList<IrisBlockDrops> providers(Engine engine, BlockPos position, BlockState brokenState) {
KList<IrisBlockDrops> providers = new KList<>();
IrisData data = engine.getData();
int relativeY = position.getY() - engine.getMinHeight();
IrisBiome biome = engine.getBiome(position.getX(), relativeY, position.getZ());
if (biome != null) {
addMatching(providers, biome.getBlockDrops(), brokenState, data);
}
if (skipsParents(providers)) {
return providers;
}
IrisRegion region = engine.getRegion(position.getX(), position.getZ());
if (region != null) {
addMatching(providers, region.getBlockDrops(), brokenState, data);
}
addMatching(providers, engine.getDimension().getBlockDrops(), brokenState, data);
return providers;
}
private static void addMatching(KList<IrisBlockDrops> matches, KList<IrisBlockDrops> candidates, BlockState brokenState, IrisData data) {
for (IrisBlockDrops candidate : candidates) {
if (matches(candidate, brokenState, data)) {
matches.add(candidate);
}
}
}
private static boolean skipsParents(KList<IrisBlockDrops> providers) {
for (IrisBlockDrops provider : providers) {
if (provider.isSkipParents()) {
return true;
}
}
return false;
}
private static boolean matches(IrisBlockDrops provider, BlockState brokenState, IrisData data) {
for (IrisBlockData configuredBlock : provider.getBlocks()) {
PlatformBlockState resolved = configuredBlock.getBlockData(data);
if (resolved == null || !(resolved.nativeHandle() instanceof BlockState configuredState)) {
continue;
}
if (matchesState(configuredState, brokenState, provider.isExactBlocks())) {
return true;
}
}
return false;
}
static boolean matchesState(BlockState configuredState, BlockState brokenState, boolean exact) {
return exact ? configuredState.equals(brokenState) : configuredState.getBlock() == brokenState.getBlock();
}
private static Engine engineFor(ServerLevel level) {
ChunkGenerator generator = level.getChunkSource().getGenerator();
if (!(generator instanceof IrisModdedChunkGenerator irisGenerator)) {
return null;
}
Engine engine = irisGenerator.engineIfBound();
if (engine != null) {
return engine;
}
try {
return irisGenerator.commandEngine();
} catch (Throwable error) {
LOGGER.error("Iris could not resolve the engine for a block break in {}", level.dimension().identifier(), error);
return null;
}
}
public record Result(KList<ItemStack> drops, boolean replaceVanillaDrops) {
private static Result empty() {
return new Result(new KList<>(), false);
}
}
private record BreakKey(ServerLevel level, long position) {
}
private record PendingBreak(BlockState brokenState) {
}
}
@@ -20,6 +20,7 @@ package art.arcane.iris.modded;
import art.arcane.iris.core.IrisSettings; import art.arcane.iris.core.IrisSettings;
import art.arcane.iris.modded.api.ModdedCustomContentRegistry; import art.arcane.iris.modded.api.ModdedCustomContentRegistry;
import art.arcane.iris.modded.api.ModdedBlockData;
import art.arcane.iris.spi.IrisLogging; import art.arcane.iris.spi.IrisLogging;
import art.arcane.volmlib.util.data.UnresolvedKeyLog; import art.arcane.volmlib.util.data.UnresolvedKeyLog;
import com.mojang.brigadier.StringReader; import com.mojang.brigadier.StringReader;
@@ -89,7 +90,7 @@ public final class ModdedBlockResolution {
private ModdedBlockResolution() { private ModdedBlockResolution() {
} }
record Parsed(BlockState state, Map<Property<?>, Comparable<?>> properties) { record Parsed(BlockState state, Map<Property<?>, Comparable<?>> properties, String deferredPlacementKey) {
} }
private static Set<Block> blockSet(String... ids) { private static Set<Block> blockSet(String... ids) {
@@ -171,12 +172,12 @@ public final class ModdedBlockResolution {
public static ModdedBlockState get(String bdxf) { public static ModdedBlockState get(String bdxf) {
Parsed parsed = resolveGet(bdxf); Parsed parsed = resolveGet(bdxf);
return ModdedBlockState.of(parsed.state(), parsed.properties()); return stateFrom(parsed);
} }
public static ModdedBlockState getNoCompat(String bdxf) { public static ModdedBlockState getNoCompat(String bdxf) {
Parsed parsed = resolveNoCompat(bdxf); Parsed parsed = resolveNoCompat(bdxf);
return ModdedBlockState.of(parsed.state(), parsed.properties()); return stateFrom(parsed);
} }
public static ModdedBlockState getOrNull(String bdxf) { public static ModdedBlockState getOrNull(String bdxf) {
@@ -185,7 +186,13 @@ public final class ModdedBlockResolution {
public static ModdedBlockState getOrNull(String bdxf, boolean warn) { public static ModdedBlockState getOrNull(String bdxf, boolean warn) {
Parsed parsed = resolveOrNull(bdxf, warn); Parsed parsed = resolveOrNull(bdxf, warn);
return parsed == null ? null : ModdedBlockState.of(parsed.state(), parsed.properties()); return parsed == null ? null : stateFrom(parsed);
}
private static ModdedBlockState stateFrom(Parsed parsed) {
return parsed.deferredPlacementKey() == null
? ModdedBlockState.of(parsed.state(), parsed.properties())
: ModdedBlockState.deferred(parsed.state(), parsed.properties(), parsed.deferredPlacementKey());
} }
static Parsed resolveGet(String bdxf) { static Parsed resolveGet(String bdxf) {
@@ -194,7 +201,7 @@ public final class ModdedBlockResolution {
return parsed; return parsed;
} }
IrisLogging.error("Can't find block data for " + bdxf); IrisLogging.error("Can't find block data for " + bdxf);
return new Parsed(AIR, null); return new Parsed(AIR, null, null);
} }
static Parsed resolveNoCompat(String bdxf) { static Parsed resolveNoCompat(String bdxf) {
@@ -202,7 +209,7 @@ public final class ModdedBlockResolution {
if (parsed != null) { if (parsed != null) {
return parsed; return parsed;
} }
return new Parsed(AIR, null); return new Parsed(AIR, null, null);
} }
static Parsed resolveOrNull(String bdxf, boolean warn) { static Parsed resolveOrNull(String bdxf, boolean warn) {
@@ -214,20 +221,20 @@ public final class ModdedBlockResolution {
} }
if (bd.equals("minecraft:grass_path")) { if (bd.equals("minecraft:grass_path")) {
return new Parsed(Blocks.DIRT_PATH.defaultBlockState(), null); return new Parsed(Blocks.DIRT_PATH.defaultBlockState(), null, null);
} }
Parsed bdx = parseBlockData(bd, warn); Parsed bdx = parseBlockData(bd, warn);
if (bdx == null) { if (bdx == null) {
BlockState provided = ModdedCustomContentRegistry.resolveBlock(bd); ModdedBlockData provided = ModdedCustomContentRegistry.resolveBlock(bd);
if (provided != null) { if (provided != null) {
return new Parsed(provided, null); return new Parsed(provided.state(), null, provided.deferredPlacement() ? bd : null);
} }
if (warn) { if (warn) {
warnUnresolved(bd, "Unknown Block Data '" + bd + "'"); warnUnresolved(bd, "Unknown Block Data '" + bd + "'");
} }
return new Parsed(AIR, null); return new Parsed(AIR, null, null);
} }
return bdx; return bdx;
@@ -243,7 +250,7 @@ public final class ModdedBlockResolution {
public static ModdedBlockState strictParse(String key) { public static ModdedBlockState strictParse(String key) {
Parsed parsed = parseStrict(key); Parsed parsed = parseStrict(key);
return ModdedBlockState.of(parsed.state(), parsed.properties()); return stateFrom(parsed);
} }
private static Parsed parseStrict(String key) { private static Parsed parseStrict(String key) {
@@ -257,7 +264,7 @@ public final class ModdedBlockResolution {
if (reader.canRead()) { if (reader.canRead()) {
throw new IllegalArgumentException("Could not parse remainder: " + reader.getRemaining()); throw new IllegalArgumentException("Could not parse remainder: " + reader.getRemaining());
} }
return new Parsed(result.blockState(), result.properties()); return new Parsed(result.blockState(), result.properties(), null);
} }
private static Parsed createBlockData(String s, boolean warn) { private static Parsed createBlockData(String s, boolean warn) {
@@ -283,7 +290,7 @@ public final class ModdedBlockResolution {
if (identifier == null || !BuiltInRegistries.BLOCK.containsKey(identifier)) { if (identifier == null || !BuiltInRegistries.BLOCK.containsKey(identifier)) {
return null; return null;
} }
return new Parsed(BuiltInRegistries.BLOCK.getValue(identifier).defaultBlockState(), null); return new Parsed(BuiltInRegistries.BLOCK.getValue(identifier).defaultBlockState(), null, null);
} }
private static Parsed parseBlockData(String ix, boolean warn) { private static Parsed parseBlockData(String ix, boolean warn) {
@@ -307,7 +314,7 @@ public final class ModdedBlockResolution {
if (bx.state().getBlock() instanceof LeavesBlock) { if (bx.state().getBlock() instanceof LeavesBlock) {
BlockState mutated = bx.state().setValue(LeavesBlock.PERSISTENT, shouldPreventLeafDecay()); BlockState mutated = bx.state().setValue(LeavesBlock.PERSISTENT, shouldPreventLeafDecay());
bx = new Parsed(mutated, bx.properties()); bx = new Parsed(mutated, bx.properties(), bx.deferredPlacementKey());
} }
return bx; return bx;
@@ -20,11 +20,13 @@ package art.arcane.iris.modded;
import art.arcane.iris.spi.PlatformBlockState; import art.arcane.iris.spi.PlatformBlockState;
import net.minecraft.core.registries.BuiltInRegistries; import net.minecraft.core.registries.BuiltInRegistries;
import net.minecraft.tags.BlockTags;
import net.minecraft.world.level.block.state.BlockState; import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.block.state.properties.Property; import net.minecraft.world.level.block.state.properties.Property;
import java.util.LinkedHashMap; import java.util.LinkedHashMap;
import java.util.Map; import java.util.Map;
import java.util.Objects;
import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentHashMap;
import java.util.stream.Collectors; import java.util.stream.Collectors;
@@ -35,24 +37,31 @@ public final class ModdedBlockState implements PlatformBlockState {
private final Map<Property<?>, Comparable<?>> parsedProperties; private final Map<Property<?>, Comparable<?>> parsedProperties;
private final String key; private final String key;
private final String namespace; private final String namespace;
private final String deferredPlacementKey;
private volatile Boolean air; private volatile Boolean air;
private volatile Boolean solid; private volatile Boolean solid;
private volatile Boolean occluding; private volatile Boolean occluding;
private volatile Boolean fluid; private volatile Boolean fluid;
private volatile Boolean water; private volatile Boolean water;
private volatile Boolean foliage; private volatile Boolean foliage;
private volatile Boolean treeBlock;
private volatile Boolean decorant; private volatile Boolean decorant;
private ModdedBlockState(BlockState state, Map<Property<?>, Comparable<?>> parsedProperties, String key) { private ModdedBlockState(BlockState state, Map<Property<?>, Comparable<?>> parsedProperties, String key, String deferredPlacementKey) {
this.state = state; this.state = state;
this.parsedProperties = parsedProperties; this.parsedProperties = parsedProperties;
this.key = key; this.key = key;
this.namespace = parseNamespace(key); this.namespace = parseNamespace(key);
this.deferredPlacementKey = deferredPlacementKey;
} }
public static ModdedBlockState of(BlockState state, Map<Property<?>, Comparable<?>> parsedProperties) { public static ModdedBlockState of(BlockState state, Map<Property<?>, Comparable<?>> parsedProperties) {
String key = serialize(state); String key = serialize(state);
return CACHE.computeIfAbsent(key, (String k) -> new ModdedBlockState(state, parsedProperties, k)); return CACHE.computeIfAbsent(key, (String k) -> new ModdedBlockState(state, parsedProperties, k, null));
}
public static ModdedBlockState deferred(BlockState state, Map<Property<?>, Comparable<?>> parsedProperties, String placementKey) {
return new ModdedBlockState(state, parsedProperties, serialize(state), Objects.requireNonNull(placementKey));
} }
public static String serialize(BlockState state) { public static String serialize(BlockState state) {
@@ -83,12 +92,12 @@ public final class ModdedBlockState implements PlatformBlockState {
if (!(other instanceof ModdedBlockState blockState)) { if (!(other instanceof ModdedBlockState blockState)) {
return false; return false;
} }
return state.equals(blockState.state); return state.equals(blockState.state) && Objects.equals(deferredPlacementKey, blockState.deferredPlacementKey);
} }
@Override @Override
public int hashCode() { public int hashCode() {
return key.hashCode(); return Objects.hash(state, deferredPlacementKey);
} }
private static String parseNamespace(String key) { private static String parseNamespace(String key) {
@@ -171,7 +180,17 @@ public final class ModdedBlockState implements PlatformBlockState {
@Override @Override
public boolean isCustom() { public boolean isCustom() {
return false; return deferredPlacementKey != null;
}
@Override
public String deferredPlacementKey() {
return deferredPlacementKey;
}
@Override
public PlatformBlockState placementBaseState() {
return deferredPlacementKey == null ? this : of(state, parsedProperties);
} }
@Override @Override
@@ -219,6 +238,16 @@ public final class ModdedBlockState implements PlatformBlockState {
return cached; return cached;
} }
@Override
public boolean isTreeBlock() {
Boolean cached = treeBlock;
if (cached == null) {
cached = state.is(BlockTags.LOGS) || state.is(BlockTags.LEAVES);
treeBlock = cached;
}
return cached;
}
@Override @Override
public boolean isFoliagePlantable() { public boolean isFoliagePlantable() {
return ModdedBlockResolution.isFoliagePlantable(state); return ModdedBlockResolution.isFoliagePlantable(state);
@@ -298,7 +327,17 @@ public final class ModdedBlockState implements PlatformBlockState {
@Override @Override
public PlatformBlockState withProperty(String name, String value) { public PlatformBlockState withProperty(String name, String value) {
String merged = mergeProperty(key, name, value); String merged = mergeProperty(key, name, value);
return ModdedBlockResolution.strictParse(merged); ModdedBlockState resolved = ModdedBlockResolution.strictParse(merged);
return withHandle(resolved.handle(), resolved.parsedProperties());
}
ModdedBlockState withHandle(BlockState updated, Map<Property<?>, Comparable<?>> properties) {
if (updated == state && properties == parsedProperties) {
return this;
}
return deferredPlacementKey == null
? of(updated, properties)
: deferred(updated, properties, deferredPlacementKey);
} }
@Override @Override
@@ -24,9 +24,12 @@ import art.arcane.iris.engine.object.IrisLootTable;
import art.arcane.iris.spi.IrisLogging; import art.arcane.iris.spi.IrisLogging;
import art.arcane.volmlib.util.collection.KList; import art.arcane.volmlib.util.collection.KList;
import art.arcane.volmlib.util.math.RNG; import art.arcane.volmlib.util.math.RNG;
import net.minecraft.core.registries.BuiltInRegistries;
import net.minecraft.server.level.ServerLevel; import net.minecraft.server.level.ServerLevel;
import net.minecraft.world.entity.Entity;
import net.minecraft.world.entity.LivingEntity; import net.minecraft.world.entity.LivingEntity;
import net.minecraft.world.entity.item.ItemEntity; import net.minecraft.world.entity.Mob;
import net.minecraft.world.entity.vehicle.ContainerEntity;
import net.minecraft.world.item.ItemStack; import net.minecraft.world.item.ItemStack;
import net.minecraft.world.level.chunk.ChunkGenerator; import net.minecraft.world.level.chunk.ChunkGenerator;
@@ -37,81 +40,125 @@ public final class ModdedDeathLoot {
private static final String TAG_PREFIX = "iris_loot|"; private static final String TAG_PREFIX = "iris_loot|";
private static final int LOOT_RNG_SALT = 345911; private static final int LOOT_RNG_SALT = 345911;
private static final Set<String> WARNED_TABLES = ConcurrentHashMap.newKeySet(); private static final Set<String> WARNED_TABLES = ConcurrentHashMap.newKeySet();
private static final Set<String> WARNED_ENTITY_TYPES = ConcurrentHashMap.newKeySet();
private ModdedDeathLoot() { private ModdedDeathLoot() {
} }
public static void tag(LivingEntity entity, KList<String> tableKeys, int spawnX, int spawnY, int spawnZ, RNG rng) { public static void bind(Engine engine, Entity entity, KList<String> tableKeys, int spawnX, int spawnY, int spawnZ, RNG rng) {
if (entity == null || tableKeys == null || tableKeys.isEmpty()) { if (engine == null || entity == null || tableKeys == null || tableKeys.isEmpty()) {
return; return;
} }
entity.addTag(TAG_PREFIX + rng.getSeed() + "|" + spawnX + "|" + spawnY + "|" + spawnZ + "|" + String.join(",", tableKeys)); LootBinding binding = new LootBinding(rng.getSeed(), spawnX, spawnY, spawnZ, new KList<>(tableKeys));
if (entity instanceof Mob) {
entity.addTag(encode(binding));
return;
}
if (entity instanceof ContainerEntity container && entity.level() instanceof ServerLevel level) {
fillContainer(engine, level, container, binding);
return;
}
String type = String.valueOf(BuiltInRegistries.ENTITY_TYPE.getKey(entity.getType()));
if (WARNED_ENTITY_TYPES.add(type)) {
IrisLogging.warn("Iris entity loot: entity type '" + type + "' does not expose a vanilla lootable path");
}
} }
public static void handle(LivingEntity entity) { public static boolean replaceBaseLoot(LivingEntity entity) {
if (entity == null || !(entity.level() instanceof ServerLevel level)) { if (entity == null || !(entity.level() instanceof ServerLevel level)) {
return; return false;
} }
String encoded = findTag(entity); String encoded = findTag(entity);
if (encoded == null) { if (encoded == null) {
return; return false;
} }
String[] parts = encoded.substring(TAG_PREFIX.length()).split("\\|", 5); LootBinding binding = decode(encoded);
if (parts.length < 5) { if (binding == null) {
IrisLogging.debug("Iris death loot: malformed loot tag '" + encoded + "'"); return true;
return;
}
long seed;
int spawnX;
int spawnY;
int spawnZ;
try {
seed = Long.parseLong(parts[0]);
spawnX = Integer.parseInt(parts[1]);
spawnY = Integer.parseInt(parts[2]);
spawnZ = Integer.parseInt(parts[3]);
} catch (NumberFormatException error) {
IrisLogging.debug("Iris death loot: malformed loot tag '" + encoded + "'");
return;
} }
Engine engine = engineFor(level); Engine engine = engineFor(level);
if (engine == null) { if (engine == null) {
return; return true;
} }
RNG lootRng = new RNG(seed).nextParallelRNG(LOOT_RNG_SALT); KList<ItemStack> stacks = resolve(engine, level, binding);
KList<ItemStack> drops = new KList<>(); emit(level, entity, stacks);
for (String key : parts[4].split(",")) { return true;
String trimmed = key.trim(); }
if (trimmed.isEmpty()) {
continue; static boolean hasBindingTag(Set<String> tags) {
for (String tag : tags) {
if (tag.startsWith(TAG_PREFIX)) {
return true;
} }
IrisLootTable table = engine.getData().getLootLoader().load(trimmed); }
return false;
}
private static void fillContainer(Engine engine, ServerLevel level, ContainerEntity container, LootBinding binding) {
container.setContainerLootTable(null);
container.clearItemStacks();
KList<ItemStack> items = resolve(engine, level, binding);
ModdedLootApplier.fillContainer(container, items, new RNG(binding.seed()));
}
private static KList<ItemStack> resolve(Engine engine, ServerLevel level, LootBinding binding) {
KList<ItemStack> drops = new KList<>();
for (String key : binding.tableKeys()) {
IrisLootTable table = engine.getData().getLootLoader().load(key);
if (table == null) { if (table == null) {
if (WARNED_TABLES.add(trimmed)) { if (WARNED_TABLES.add(key)) {
IrisLogging.warn("Iris death loot: unknown loot table '" + trimmed + "'"); IrisLogging.warn("Iris death loot: unknown loot table '" + key + "'");
} }
continue; continue;
} }
drops.addAll(ModdedItemTranslator.loot(table, lootRng, InventorySlotType.STORAGE, level, spawnX, spawnY, spawnZ)); RNG lootRng = new RNG(binding.seed()).nextParallelRNG(LOOT_RNG_SALT);
drops.addAll(ModdedItemTranslator.loot(table, lootRng, InventorySlotType.STORAGE, level,
binding.spawnX(), binding.spawnY(), binding.spawnZ()));
} }
emit(level, entity, drops); return drops;
} }
private static void emit(ServerLevel level, LivingEntity entity, KList<ItemStack> drops) { private static void emit(ServerLevel level, LivingEntity entity, KList<ItemStack> drops) {
double x = entity.getX();
double y = entity.getY() + 0.5D;
double z = entity.getZ();
for (ItemStack stack : drops) { for (ItemStack stack : drops) {
if (stack == null || stack.isEmpty()) { if (stack == null || stack.isEmpty()) {
continue; continue;
} }
ItemEntity itemEntity = new ItemEntity(level, x, y, z, stack); entity.spawnAtLocation(level, stack);
itemEntity.setDefaultPickUpDelay();
level.addFreshEntity(itemEntity);
} }
} }
private static String findTag(LivingEntity entity) { private static String encode(LootBinding binding) {
return TAG_PREFIX + binding.seed() + "|" + binding.spawnX() + "|" + binding.spawnY() + "|"
+ binding.spawnZ() + "|" + String.join(",", binding.tableKeys());
}
private static LootBinding decode(String encoded) {
String[] parts = encoded.substring(TAG_PREFIX.length()).split("\\|", 5);
if (parts.length < 5) {
IrisLogging.debug("Iris death loot: malformed loot tag '" + encoded + "'");
return null;
}
try {
long seed = Long.parseLong(parts[0]);
int spawnX = Integer.parseInt(parts[1]);
int spawnY = Integer.parseInt(parts[2]);
int spawnZ = Integer.parseInt(parts[3]);
KList<String> tableKeys = new KList<>();
for (String key : parts[4].split(",")) {
String trimmed = key.trim();
if (!trimmed.isEmpty()) {
tableKeys.add(trimmed);
}
}
return new LootBinding(seed, spawnX, spawnY, spawnZ, tableKeys);
} catch (NumberFormatException error) {
IrisLogging.debug("Iris death loot: malformed loot tag '" + encoded + "'");
return null;
}
}
private static String findTag(Entity entity) {
for (String tag : entity.entityTags()) { for (String tag : entity.entityTags()) {
if (tag.startsWith(TAG_PREFIX)) { if (tag.startsWith(TAG_PREFIX)) {
return tag; return tag;
@@ -127,4 +174,7 @@ public final class ModdedDeathLoot {
} }
return null; return null;
} }
private record LootBinding(long seed, int spawnX, int spawnY, int spawnZ, KList<String> tableKeys) {
}
} }
@@ -91,7 +91,7 @@ public final class ModdedDecoratorHooks implements DecoratorPlatformHooks.FaceFi
} }
} }
return ModdedBlockState.of(cloned, fabric.parsedProperties()); return fabric.withHandle(cloned, fabric.parsedProperties());
} }
@Override @Override
@@ -39,7 +39,6 @@ import net.minecraft.world.level.biome.Biome;
import net.minecraft.world.level.biome.BiomeManager; import net.minecraft.world.level.biome.BiomeManager;
import net.minecraft.world.level.biome.Biomes; import net.minecraft.world.level.biome.Biomes;
import net.minecraft.world.level.biome.FixedBiomeSource; import net.minecraft.world.level.biome.FixedBiomeSource;
import net.minecraft.world.level.dimension.BuiltinDimensionTypes;
import net.minecraft.world.level.dimension.DimensionType; import net.minecraft.world.level.dimension.DimensionType;
import net.minecraft.world.level.dimension.LevelStem; import net.minecraft.world.level.dimension.LevelStem;
import net.minecraft.world.level.levelgen.Heightmap; import net.minecraft.world.level.levelgen.Heightmap;
@@ -53,7 +52,6 @@ import org.slf4j.LoggerFactory;
import java.io.File; import java.io.File;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.Optional;
import java.util.Set; import java.util.Set;
import java.util.UUID; import java.util.UUID;
import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletableFuture;
@@ -64,7 +62,6 @@ public final class ModdedDimensionManager {
private static final Logger LOGGER = LoggerFactory.getLogger("Iris"); private static final Logger LOGGER = LoggerFactory.getLogger("Iris");
private static final Object LOCK = new Object(); private static final Object LOCK = new Object();
private static final ConcurrentHashMap<String, Handle> HANDLES = new ConcurrentHashMap<>(); private static final ConcurrentHashMap<String, Handle> HANDLES = new ConcurrentHashMap<>();
private static final Set<String> TYPE_FALLBACK_WARNINGS = ConcurrentHashMap.newKeySet();
private static final TicketType TELEPORT_WARM_TICKET = new TicketType(TicketType.NO_TIMEOUT, TicketType.FLAG_LOADING); private static final TicketType TELEPORT_WARM_TICKET = new TicketType(TicketType.NO_TIMEOUT, TicketType.FLAG_LOADING);
private static volatile ModdedServerAccess access; private static volatile ModdedServerAccess access;
@@ -234,21 +231,15 @@ public final class ModdedDimensionManager {
private static Holder<DimensionType> resolveDimensionType(RegistryAccess registryAccess, String pack, String packDimensionKey) { private static Holder<DimensionType> resolveDimensionType(RegistryAccess registryAccess, String pack, String packDimensionKey) {
Registry<DimensionType> registry = registryAccess.lookupOrThrow(Registries.DIMENSION_TYPE); Registry<DimensionType> registry = registryAccess.lookupOrThrow(Registries.DIMENSION_TYPE);
IrisDimension dimension = loadPackDimension(pack, packDimensionKey); IrisDimension dimension = loadPackDimension(pack, packDimensionKey);
if (dimension != null) { if (dimension == null) {
String typeRef = ModdedForcedDatapack.dimensionTypeRef(dimension); throw new IllegalStateException("Iris cannot resolve dimension type for missing pack dimension '"
ResourceKey<DimensionType> typeKey = ResourceKey.create(Registries.DIMENSION_TYPE, Identifier.parse(typeRef)); + packDimensionKey + "' in pack '" + pack + "'");
Optional<Holder.Reference<DimensionType>> packType = registry.get(typeKey);
if (packType.isPresent()) {
return packType.get();
}
if (TYPE_FALLBACK_WARNINGS.add(typeRef)) {
LOGGER.warn("Iris dimension type {} (pack {} dim {}) is not registered yet; injecting with fallback heights. Restart the server so the forced datapack installs it.", typeRef, pack, packDimensionKey);
}
} }
ResourceKey<DimensionType> studioPool = ResourceKey.create(Registries.DIMENSION_TYPE, Identifier.parse("irisworldgen:studio_pool")); String typeRef = ModdedForcedDatapack.dimensionTypeRef(dimension);
return registry.get(studioPool) ResourceKey<DimensionType> typeKey = ResourceKey.create(Registries.DIMENSION_TYPE, Identifier.parse(typeRef));
.map(reference -> (Holder<DimensionType>) reference) Holder.Reference<DimensionType> packType = ModdedForcedDatapack.requireRegisteredDimensionType(
.orElseGet(() -> registry.getOrThrow(BuiltinDimensionTypes.OVERWORLD)); typeRef, registry.get(typeKey), pack, packDimensionKey);
return packType;
} }
private static IrisDimension loadPackDimension(String pack, String packDimensionKey) { private static IrisDimension loadPackDimension(String pack, String packDimensionKey) {
@@ -295,6 +286,7 @@ public final class ModdedDimensionManager {
false); false);
serverAccess.putLevel(server, key, level); serverAccess.putLevel(server, key, level);
generator.bindLevel(level);
server.getPlayerList().addWorldborderListener(level); server.getPlayerList().addWorldborderListener(level);
return new Handle(dimensionId, pack, packDimensionKey, seed, level, generator); return new Handle(dimensionId, pack, packDimensionKey, seed, level, generator);
} }
@@ -21,6 +21,8 @@ package art.arcane.iris.modded;
import art.arcane.iris.core.gui.GuiHost; import art.arcane.iris.core.gui.GuiHost;
import art.arcane.iris.engine.decorator.DecoratorPlatformHooks; import art.arcane.iris.engine.decorator.DecoratorPlatformHooks;
import art.arcane.iris.engine.framework.Engine; import art.arcane.iris.engine.framework.Engine;
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.engine.framework.EngineWorldManagerProvider;
import art.arcane.iris.engine.framework.PreservationRegistry; import art.arcane.iris.engine.framework.PreservationRegistry;
import art.arcane.iris.engine.object.BlockDataMergeSupport; import art.arcane.iris.engine.object.BlockDataMergeSupport;
@@ -42,7 +44,12 @@ import art.arcane.iris.modded.service.ModdedSettingsHotloadService;
import art.arcane.iris.modded.service.ModdedStudioHotloadService; import art.arcane.iris.modded.service.ModdedStudioHotloadService;
import art.arcane.iris.spi.IrisPlatforms; import art.arcane.iris.spi.IrisPlatforms;
import art.arcane.iris.spi.IrisServices; import art.arcane.iris.spi.IrisServices;
import net.minecraft.core.BlockPos;
import net.minecraft.server.MinecraftServer; import net.minecraft.server.MinecraftServer;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.world.level.chunk.LevelChunk;
import net.minecraft.world.level.levelgen.Heightmap;
import net.minecraft.world.level.storage.LevelData;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
@@ -58,6 +65,8 @@ public final class ModdedEngineBootstrap {
private static volatile ModdedLoader loader; private static volatile ModdedLoader loader;
private static volatile ModdedPlatform platform; private static volatile ModdedPlatform platform;
private static volatile MinecraftServer currentServer; private static volatile MinecraftServer currentServer;
private static volatile MinecraftServer spawnCaptureServer;
private static volatile boolean initialSpawnWasDefault;
private ModdedEngineBootstrap() { private ModdedEngineBootstrap() {
} }
@@ -81,8 +90,10 @@ public final class ModdedEngineBootstrap {
} }
public static void start(MinecraftServer server) { public static void start(MinecraftServer server) {
captureInitialSpawn(server);
currentServer = server; currentServer = server;
bind(); bind();
bindWorldGenerators(server);
ModdedStartup.reset(); ModdedStartup.reset();
ModdedScheduler scheduler = schedulerOrNull(); ModdedScheduler scheduler = schedulerOrNull();
if (scheduler != null) { if (scheduler != null) {
@@ -94,11 +105,38 @@ public final class ModdedEngineBootstrap {
ModdedSentry.start(loader()); ModdedSentry.start(loader());
} }
private static void bindWorldGenerators(MinecraftServer server) {
for (ServerLevel level : server.getAllLevels()) {
if (level.getChunkSource().getGenerator() instanceof IrisModdedChunkGenerator generator) {
generator.bindLevel(level);
}
}
}
public static void serverAboutToStart(MinecraftServer server) {
captureInitialSpawn(server);
}
public static void serverStarted(MinecraftServer server) {
bindWorldGenerators(server);
reconcileSpawn(server);
ModdedWorldCheck.serverStarted(server);
}
public static void levelLoaded(ServerLevel level) {
if (level.getChunkSource().getGenerator() instanceof IrisModdedChunkGenerator generator) {
generator.bindLevel(level);
}
}
public static void stop() { public static void stop() {
MinecraftServer stoppingServer = currentServer;
ModdedWorldCheck.serverStopped(stoppingServer);
ModdedProtocolHandler.stop(); ModdedProtocolHandler.stop();
ModdedPregenJob.shutdown(); ModdedPregenJob.shutdown();
ModdedObjectUndo.clearAll(); ModdedObjectUndo.clearAll();
ModdedWandService.clearAll(); ModdedWandService.clearAll();
ModdedBlockBreakHandler.clear();
ModdedStudioCommands.clear(); ModdedStudioCommands.clear();
ModdedWorldEngines.shutdown(); ModdedWorldEngines.shutdown();
ModdedPrimaryWorldRouter.clear(); ModdedPrimaryWorldRouter.clear();
@@ -112,6 +150,49 @@ public final class ModdedEngineBootstrap {
ModdedSentry.flush(); ModdedSentry.flush();
ModdedStartup.reset(); ModdedStartup.reset();
currentServer = null; currentServer = null;
spawnCaptureServer = null;
initialSpawnWasDefault = false;
}
private static void captureInitialSpawn(MinecraftServer server) {
if (spawnCaptureServer == server) {
return;
}
LevelData.RespawnData respawnData = server.getRespawnData();
initialSpawnWasDefault = respawnData == null || LevelData.RespawnData.DEFAULT.equals(respawnData);
spawnCaptureServer = server;
}
private static void reconcileSpawn(MinecraftServer server) {
LevelData.RespawnData current = server.getRespawnData();
if (current == null) {
return;
}
ServerLevel level = server.getLevel(current.dimension());
if (level == null || !(level.getChunkSource().getGenerator() instanceof IrisModdedChunkGenerator)) {
return;
}
String dimensionId = level.dimension().identifier().toString();
boolean studio = dimensionId.startsWith("irisworldgen:studio_");
if (!shouldReconcileSpawn(initialSpawnWasDefault, studio, current.pos().getX(), current.pos().getZ())) {
return;
}
LevelChunk originChunk = level.getChunk(0, 0);
int surfaceY = originChunk.getHeight(Heightmap.Types.MOTION_BLOCKING, 0, 0) + 1;
BlockPos position = reconciledSpawnPosition(surfaceY, level.getMinY(), level.getHeight());
server.setRespawnData(LevelData.RespawnData.of(
level.dimension(), position, current.yaw(), current.pitch()));
LOGGER.info("Iris spawn reconciled for {} at {},{},{}", dimensionId,
position.getX(), position.getY(), position.getZ());
}
static boolean shouldReconcileSpawn(boolean initialDefault, boolean studio, int currentX, int currentZ) {
return initialDefault || studio || currentX == 0 && currentZ == 0;
}
static BlockPos reconciledSpawnPosition(int surfaceY, int minY, int height) {
int y = Math.max(minY + 1, Math.min(minY + height - 2, surfaceY));
return new BlockPos(0, y, 0);
} }
public static void bootCommon(ModdedLoader moddedLoader, String loaderDescription, Runnable chunkGeneratorRegistration) { public static void bootCommon(ModdedLoader moddedLoader, String loaderDescription, Runnable chunkGeneratorRegistration) {
@@ -191,6 +272,7 @@ public final class ModdedEngineBootstrap {
IrisObjectRotation.bindFallbackRotator(new ModdedStateRotator()); IrisObjectRotation.bindFallbackRotator(new ModdedStateRotator());
BlockDataMergeSupport.bindFallbackMerger(new ModdedStateMerger()); BlockDataMergeSupport.bindFallbackMerger(new ModdedStateMerger());
TileData.bindFallbackReader(new ModdedTileReader(boundLoader::currentServer)); TileData.bindFallbackReader(new ModdedTileReader(boundLoader::currentServer));
TileData.bindFallbackFactory(ModdedTileData::fromProperties);
ModdedGuiHost.install(); ModdedGuiHost.install();
ModdedDecoratorHooks decoratorHooks = new ModdedDecoratorHooks(); ModdedDecoratorHooks decoratorHooks = new ModdedDecoratorHooks();
DecoratorPlatformHooks.bind(decoratorHooks, decoratorHooks); DecoratorPlatformHooks.bind(decoratorHooks, decoratorHooks);
@@ -198,10 +280,12 @@ public final class ModdedEngineBootstrap {
SERVICE_MANAGER.register(ModdedLogFilterService.class, new ModdedLogFilterService()); SERVICE_MANAGER.register(ModdedLogFilterService.class, new ModdedLogFilterService());
SERVICE_MANAGER.register(ModdedEngineMaintenanceService.class, new ModdedEngineMaintenanceService()); SERVICE_MANAGER.register(ModdedEngineMaintenanceService.class, new ModdedEngineMaintenanceService());
SERVICE_MANAGER.register(ModdedSettingsHotloadService.class, new ModdedSettingsHotloadService()); SERVICE_MANAGER.register(ModdedSettingsHotloadService.class, new ModdedSettingsHotloadService());
SERVICE_MANAGER.register(ModdedStudioHotloadService.class, new ModdedStudioHotloadService()); ModdedStudioHotloadService studioHotloadService = SERVICE_MANAGER.register(ModdedStudioHotloadService.class, new ModdedStudioHotloadService());
SERVICE_MANAGER.register(ModdedChunkUpdateService.class, new ModdedChunkUpdateService()); SERVICE_MANAGER.register(ModdedChunkUpdateService.class, new ModdedChunkUpdateService());
SERVICE_MANAGER.register(ModdedEntitySpawnService.class, new ModdedEntitySpawnService()); SERVICE_MANAGER.register(ModdedEntitySpawnService.class, new ModdedEntitySpawnService());
IrisServices.register(PreservationRegistry.class, preservation); IrisServices.register(PreservationRegistry.class, preservation);
IrisServices.register(EngineEffectsProvider.class, (EngineEffectsProvider) ModdedEngineEffects::new);
IrisServices.register(EnginePlatformHooks.class, studioHotloadService);
IrisServices.register(EngineWorldManagerProvider.class, (EngineWorldManagerProvider) (Engine engine) -> new ModdedWorldManager(engine)); IrisServices.register(EngineWorldManagerProvider.class, (EngineWorldManagerProvider) (Engine engine) -> new ModdedWorldManager(engine));
ModdedCustomContentRegistry.discover(); ModdedCustomContentRegistry.discover();
platform = created; platform = created;
@@ -0,0 +1,499 @@
/*
* Iris is a World Generator for Minecraft 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.modded;
import art.arcane.iris.core.IrisSettings;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.framework.EngineAssignedComponent;
import art.arcane.iris.engine.framework.EngineEffects;
import art.arcane.iris.engine.object.IrisBiome;
import art.arcane.iris.engine.object.IrisCommand;
import art.arcane.iris.engine.object.IrisCommandRegistry;
import art.arcane.iris.engine.object.IrisEffect;
import art.arcane.iris.engine.object.IrisRegion;
import art.arcane.iris.spi.IrisLogging;
import art.arcane.volmlib.util.math.RNG;
import net.minecraft.core.Holder;
import net.minecraft.core.particles.ParticleType;
import net.minecraft.core.particles.SimpleParticleType;
import net.minecraft.core.registries.BuiltInRegistries;
import net.minecraft.network.protocol.game.ClientboundSoundPacket;
import net.minecraft.resources.Identifier;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.sounds.SoundEvent;
import net.minecraft.sounds.SoundSource;
import net.minecraft.world.effect.MobEffect;
import net.minecraft.world.effect.MobEffectInstance;
import net.minecraft.world.effect.MobEffects;
import net.minecraft.world.phys.Vec3;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ThreadLocalRandom;
import java.util.concurrent.atomic.AtomicBoolean;
public final class ModdedEngineEffects extends EngineAssignedComponent implements EngineEffects {
private static final long EFFECT_BUDGET_NANOS = 1_500_000L;
private static final long SAMPLE_INTERVAL_MILLIS = 55L;
private static final double SAMPLE_DISTANCE_SQUARED = 81.0D;
private static final double PLAYER_REFRESH_CHANCE = 0.02D;
private static final Set<String> UNKNOWN_POTIONS = ConcurrentHashMap.newKeySet();
private static final Set<String> UNKNOWN_SOUNDS = ConcurrentHashMap.newKeySet();
private static final Set<String> UNKNOWN_PARTICLES = ConcurrentHashMap.newKeySet();
private static final Set<String> UNSUPPORTED_PARTICLES = ConcurrentHashMap.newKeySet();
private final Map<UUID, PlayerState> players;
private final AtomicBoolean updateRequested;
private final AtomicBoolean tickRequested;
private final AtomicBoolean passQueued;
public ModdedEngineEffects(Engine engine) {
super(engine, "FX");
players = new HashMap<>();
updateRequested = new AtomicBoolean(false);
tickRequested = new AtomicBoolean(false);
passQueued = new AtomicBoolean(false);
}
@Override
public void updatePlayerMap() {
updateRequested.set(true);
queueMainPass();
}
@Override
public void tickRandomPlayer() {
tickRequested.set(true);
queueMainPass();
}
private void queueMainPass() {
ModdedScheduler scheduler = ModdedEngineBootstrap.schedulerOrNull();
if (scheduler == null || !passQueued.compareAndSet(false, true)) {
return;
}
scheduler.global(this::runQueuedPass);
}
private void runQueuedPass() {
try {
boolean shouldUpdate = updateRequested.getAndSet(false);
boolean shouldTick = tickRequested.getAndSet(false);
if (!shouldUpdate && !shouldTick) {
return;
}
ServerLevel level = resolveLevel();
if (level == null) {
players.clear();
return;
}
if (shouldUpdate) {
syncPlayers(level);
}
if (shouldTick) {
tickPlayers(level);
}
} catch (Throwable error) {
IrisLogging.reportError(error);
} finally {
passQueued.set(false);
if (updateRequested.get() || tickRequested.get()) {
queueMainPass();
}
}
}
private ServerLevel resolveLevel() {
Engine engine = getEngine();
if (engine.isClosed() || !engine.getWorld().hasPlatformWorld()) {
return null;
}
Object nativeWorld = engine.getWorld().platformWorld().nativeHandle();
return nativeWorld instanceof ServerLevel ? (ServerLevel) nativeWorld : null;
}
private void syncPlayers(ServerLevel level) {
List<ServerPlayer> activePlayers = level.players();
Set<UUID> activeIds = new HashSet<>(Math.max(16, activePlayers.size() * 2));
for (ServerPlayer player : activePlayers) {
UUID playerId = player.getUUID();
activeIds.add(playerId);
PlayerState state = players.get(playerId);
if (state == null) {
players.put(playerId, new PlayerState(player));
} else {
state.player(player);
}
}
Iterator<Map.Entry<UUID, PlayerState>> iterator = players.entrySet().iterator();
while (iterator.hasNext()) {
Map.Entry<UUID, PlayerState> entry = iterator.next();
if (!activeIds.contains(entry.getKey())) {
iterator.remove();
}
}
}
private void tickPlayers(ServerLevel level) {
if (players.isEmpty()) {
syncPlayers(level);
} else if (RNG.r.d() < PLAYER_REFRESH_CHANCE) {
syncPlayers(level);
return;
}
if (players.isEmpty()) {
return;
}
List<PlayerState> snapshot = new ArrayList<>(players.values());
long started = System.nanoTime();
int remaining = snapshot.size();
while (remaining-- > 0 && System.nanoTime() - started < EFFECT_BUDGET_NANOS) {
PlayerState state = snapshot.get(RNG.r.nextInt(snapshot.size()));
try {
tickPlayer(level, state);
} catch (Throwable error) {
IrisLogging.reportError(error);
}
}
}
private void tickPlayer(ServerLevel level, PlayerState state) {
ServerPlayer player = state.player();
if (!player.isAlive() || player.isRemoved() || player.level() != level) {
return;
}
samplePlayer(state);
if (!IrisSettings.get().getWorld().isEffectSystem()) {
return;
}
IrisRegion region = state.region();
if (region != null) {
applyEffects(level, player, region.getEffects());
}
IrisBiome biome = state.biome();
if (biome != null) {
applyEffects(level, player, biome.getEffects());
}
}
private void samplePlayer(PlayerState state) {
ServerPlayer player = state.player();
double deltaX = player.getX() - state.lastX();
double deltaY = player.getY() - state.lastY();
double deltaZ = player.getZ() - state.lastZ();
double distanceSquared = deltaX * deltaX + deltaY * deltaY + deltaZ * deltaZ;
long now = System.currentTimeMillis();
if (!needsSample(state.sampled(), now - state.lastSample(), distanceSquared)) {
return;
}
state.sampled(true);
state.lastPosition(player.getX(), player.getY(), player.getZ());
state.lastSample(now);
int blockX = floor(player.getX());
int blockY = floor(player.getY()) - getEngine().getWorld().minHeight();
int blockZ = floor(player.getZ());
state.biome(getEngine().getBiome(blockX, blockY, blockZ));
state.region(getEngine().getRegion(blockX, blockZ));
}
private void applyEffects(ServerLevel level, ServerPlayer player, Iterable<IrisEffect> effects) {
for (IrisEffect effect : effects) {
try {
applyEffect(level, player, effect);
} catch (Throwable error) {
IrisLogging.reportError(error);
}
}
}
private void applyEffect(ServerLevel level, ServerPlayer player, IrisEffect effect) {
if (!effect.shouldApplyNow()) {
return;
}
applySound(player, effect);
applyParticles(level, player, effect);
applyCommands(level, player, effect.getCommandRegistry());
applyPotion(player, effect);
}
private void applySound(ServerPlayer player, IrisEffect effect) {
String key = normalizeRegistryKey(effect.getSoundKey());
if (key == null) {
return;
}
Identifier identifier = Identifier.tryParse(key);
Optional<Holder.Reference<SoundEvent>> sound = identifier == null
? Optional.empty()
: BuiltInRegistries.SOUND_EVENT.get(identifier);
if (sound.isEmpty()) {
warnUnknown(UNKNOWN_SOUNDS, "sound", key);
return;
}
int distance = effect.getSoundDistance();
double x = player.getX() + RNG.r.i(-distance, distance);
double y = player.getY() + RNG.r.i(-distance, distance);
double z = player.getZ() + RNG.r.i(-distance, distance);
float volume = (float) effect.getVolume();
float pitch = (float) RNG.r.d(effect.getMinPitch(), effect.getMaxPitch());
player.connection.send(new ClientboundSoundPacket(
sound.get(), SoundSource.MASTER, x, y, z, volume, pitch, ThreadLocalRandom.current().nextLong()));
}
private void applyParticles(ServerLevel level, ServerPlayer player, IrisEffect effect) {
String key = normalizeRegistryKey(effect.getParticleEffectKey());
if (key == null) {
return;
}
Identifier identifier = Identifier.tryParse(key);
ParticleType<?> particleType = identifier == null
? null
: BuiltInRegistries.PARTICLE_TYPE.getValue(identifier);
if (particleType == null) {
warnUnknown(UNKNOWN_PARTICLES, "particle", key);
return;
}
if (!(particleType instanceof SimpleParticleType simpleParticle)) {
if (UNSUPPORTED_PARTICLES.add(key)) {
IrisLogging.warn("Particle type \"" + key + "\" requires particle data and cannot be used by an Iris effect without data.");
}
return;
}
Vec3 direction = player.getLookAngle();
double forward = RNG.r.i(effect.getParticleDistance()) + effect.getParticleAway();
double sideways = RNG.r.d(-effect.getParticleDistanceWidth(), effect.getParticleDistanceWidth());
double surfaceX = player.getX() + direction.x * forward + direction.z * sideways;
double surfaceZ = player.getZ() + direction.z * forward - direction.x * sideways;
int surfaceY = getEngine().getHeight(floor(surfaceX), floor(surfaceZ));
double x = surfaceX + RNG.r.d();
double y = surfaceY + 1.0D + level.getMinY() + RNG.r.i(effect.getParticleOffset());
double z = surfaceZ + RNG.r.d();
double altX = randomized(effect.getParticleAltX(), effect.isRandomAltX());
double altY = randomized(effect.getParticleAltY(), effect.isRandomAltY());
double altZ = randomized(effect.getParticleAltZ(), effect.isRandomAltZ());
level.sendParticles(player, simpleParticle, false, false,
x, y, z, effect.getParticleCount(), altX, altY, altZ, effect.getExtra());
}
private void applyCommands(ServerLevel level, ServerPlayer player, IrisCommandRegistry registry) {
if (registry == null || registry.getRawCommands() == null || registry.getRawCommands().isEmpty()) {
return;
}
ModdedPlatformWorld world = new ModdedPlatformWorld(level);
double x = commandCoordinate(player.getX(), registry.getCommandOffsetX(), registry.isCommandRandomAltX());
double y = commandCoordinate(player.getY(), registry.getCommandOffsetY(), registry.isCommandRandomAltY());
double z = commandCoordinate(player.getZ(), registry.getCommandOffsetZ(), registry.isCommandRandomAltZ());
for (IrisCommand command : registry.getRawCommands()) {
command.run(world, floor(x), floor(y), floor(z));
if (registry.isCommandAllRandomLocations()) {
x = commandCoordinate(player.getX(), registry.getCommandOffsetX(), registry.isCommandRandomAltX());
y = commandCoordinate(player.getY(), registry.getCommandOffsetY(), registry.isCommandRandomAltY());
z = commandCoordinate(player.getZ(), registry.getCommandOffsetZ(), registry.isCommandRandomAltZ());
}
}
}
private void applyPotion(ServerPlayer player, IrisEffect effect) {
int strength = effect.getPotionStrength();
if (strength < 0) {
return;
}
Holder<MobEffect> type = resolvePotion(effect.getPotionEffect());
MobEffectInstance current = player.getEffect(type);
if (current != null && !shouldReplacePotionEffect(current.getAmplifier(), strength)) {
return;
}
if (current != null) {
player.removeEffect(type);
}
int minimum = Math.min(effect.getPotionTicksMin(), effect.getPotionTicksMax());
int maximum = Math.max(effect.getPotionTicksMin(), effect.getPotionTicksMax());
int duration = RNG.r.i(minimum, maximum);
player.addEffect(new MobEffectInstance(type, duration, strength, true, false, false));
}
private Holder<MobEffect> resolvePotion(String rawKey) {
String key = normalizePotionEffectKey(rawKey);
Identifier identifier = Identifier.tryParse(key);
if (identifier != null) {
Optional<Holder.Reference<MobEffect>> effect = BuiltInRegistries.MOB_EFFECT.get(identifier);
if (effect.isPresent()) {
return effect.get();
}
}
if (UNKNOWN_POTIONS.add(key)) {
IrisLogging.warn("Unknown Potion Effect Type: \"" + rawKey + "\". Using LUCK instead.");
}
return MobEffects.LUCK;
}
static String normalizeRegistryKey(String rawKey) {
if (rawKey == null || rawKey.isBlank()) {
return null;
}
String normalized = rawKey.trim().toLowerCase(Locale.ROOT).replace(' ', '_');
return normalized.contains(":") ? normalized : "minecraft:" + normalized;
}
static String normalizePotionEffectKey(String rawKey) {
String normalized = normalizeRegistryKey(rawKey == null || rawKey.isBlank() ? "luck" : rawKey);
int separator = normalized.indexOf(':');
String namespace = normalized.substring(0, separator);
String path = normalized.substring(separator + 1);
String aliased = switch (path) {
case "slow" -> "slowness";
case "fast_digging" -> "haste";
case "slow_digging" -> "mining_fatigue";
case "increase_damage" -> "strength";
case "heal" -> "instant_health";
case "harm" -> "instant_damage";
case "jump" -> "jump_boost";
case "confusion" -> "nausea";
case "damage_resistance" -> "resistance";
default -> path;
};
return namespace + ":" + aliased;
}
static boolean needsSample(boolean sampled, long elapsedMillis, double distanceSquared) {
return !sampled || elapsedMillis > SAMPLE_INTERVAL_MILLIS && distanceSquared > SAMPLE_DISTANCE_SQUARED;
}
static boolean shouldReplacePotionEffect(int existingAmplifier, int configuredAmplifier) {
return existingAmplifier <= configuredAmplifier;
}
private static int floor(double value) {
return (int) Math.floor(value);
}
private static double randomized(double value, boolean random) {
return random ? RNG.r.d(-value, value) : value;
}
private static double commandCoordinate(double base, double offset, boolean random) {
return base + randomized(offset, random);
}
private static void warnUnknown(Set<String> warned, String type, String key) {
if (warned.add(key)) {
IrisLogging.warn("Unknown " + type + " type: \"" + key + "\".");
}
}
private static final class PlayerState {
private ServerPlayer player;
private IrisBiome biome;
private IrisRegion region;
private double lastX;
private double lastY;
private double lastZ;
private long lastSample;
private boolean sampled;
private PlayerState(ServerPlayer player) {
this.player = player;
lastX = player.getX();
lastY = player.getY();
lastZ = player.getZ();
lastSample = -1L;
sampled = false;
}
private ServerPlayer player() {
return player;
}
private void player(ServerPlayer player) {
this.player = player;
}
private IrisBiome biome() {
return biome;
}
private void biome(IrisBiome biome) {
this.biome = biome;
}
private IrisRegion region() {
return region;
}
private void region(IrisRegion region) {
this.region = region;
}
private double lastX() {
return lastX;
}
private double lastY() {
return lastY;
}
private double lastZ() {
return lastZ;
}
private void lastPosition(double x, double y, double z) {
lastX = x;
lastY = y;
lastZ = z;
}
private long lastSample() {
return lastSample;
}
private void lastSample(long lastSample) {
this.lastSample = lastSample;
}
private boolean sampled() {
return sampled;
}
private void sampled(boolean sampled) {
this.sampled = sampled;
}
}
}
@@ -0,0 +1,39 @@
package art.arcane.iris.modded;
import art.arcane.iris.spi.IrisLogging;
import net.minecraft.world.entity.Mob;
import java.util.Set;
public final class ModdedEntityAwareness {
private static final String UNAWARE_TAG = "iris_unaware";
private ModdedEntityAwareness() {
}
public static void configure(Mob mob, boolean aware) {
if (aware) {
mob.removeTag(UNAWARE_TAG);
return;
}
if (!mob.entityTags().contains(UNAWARE_TAG) && !mob.addTag(UNAWARE_TAG)) {
IrisLogging.warn("Iris could not mark generated mob '" + mob.getStringUUID() + "' as unaware");
}
}
public static boolean isAware(Mob mob) {
return isAware(mob.entityTags());
}
static boolean isAware(Set<String> tags) {
return !tags.contains(UNAWARE_TAG);
}
static void configureTags(Set<String> tags, boolean aware) {
if (aware) {
tags.remove(UNAWARE_TAG);
} else {
tags.add(UNAWARE_TAG);
}
}
}
@@ -0,0 +1,82 @@
package art.arcane.iris.modded;
import art.arcane.iris.engine.object.IrisCommand;
import art.arcane.iris.spi.IrisLogging;
import art.arcane.volmlib.util.collection.KList;
import net.minecraft.server.MinecraftServer;
import net.minecraft.server.level.ServerLevel;
final class ModdedEntityCommandRunner {
private ModdedEntityCommandRunner() {
}
static void run(KList<IrisCommand> commands, ServerLevel level, int blockX, int blockY, int blockZ) {
if (commands.isEmpty()) {
return;
}
MinecraftServer server = level.getServer();
ModdedScheduler scheduler = ModdedEngineBootstrap.schedulerOrNull();
if (server == null || scheduler == null) {
IrisLogging.error("Iris could not schedule entity commands because the modded server scheduler is unavailable.");
return;
}
ModdedPlatformWorld world = new ModdedPlatformWorld(level);
for (IrisCommand command : commands) {
if (command == null || !command.isValid(world)) {
continue;
}
schedule(command, server, scheduler, blockX, blockY, blockZ);
}
}
private static void schedule(IrisCommand command, MinecraftServer server, ModdedScheduler scheduler, int blockX, int blockY, int blockZ) {
int delay = clampDelay(command.getDelay(), 0);
int repeatDelay = clampDelay(command.getRepeatDelay(), 1);
for (String raw : command.getCommands()) {
String prepared = prepareCommand(raw, blockX, blockY, blockZ);
if (prepared == null) {
continue;
}
if (command.isRepeat()) {
scheduler.laterGlobal(() -> new RepeatingCommand(server, scheduler, prepared, repeatDelay).run(), delay);
} else {
scheduler.laterGlobal(() -> ModdedServerCommands.dispatch(server, prepared), delay);
}
}
}
static String prepareCommand(String raw, int blockX, int blockY, int blockZ) {
if (raw == null || raw.isBlank()) {
return null;
}
return (raw.startsWith("/") ? raw.substring(1) : raw)
.replace("{x}", String.valueOf(blockX))
.replace("{y}", String.valueOf(blockY))
.replace("{z}", String.valueOf(blockZ));
}
static int clampDelay(long delay, int minimum) {
return (int) Math.max(minimum, Math.min(Integer.MAX_VALUE, delay));
}
private static final class RepeatingCommand implements Runnable {
private final MinecraftServer server;
private final ModdedScheduler scheduler;
private final String command;
private final int interval;
private RepeatingCommand(MinecraftServer server, ModdedScheduler scheduler, String command, int interval) {
this.server = server;
this.scheduler = scheduler;
this.command = command;
this.interval = interval;
}
@Override
public void run() {
ModdedServerCommands.dispatch(server, command);
scheduler.laterGlobal(this, interval);
}
}
}
@@ -0,0 +1,43 @@
package art.arcane.iris.modded;
import art.arcane.iris.spi.IrisLogging;
import net.minecraft.world.entity.Entity;
import net.minecraft.world.entity.Mob;
import java.util.Set;
public final class ModdedEntityPersistence {
private static final String NON_PERSISTENT_TAG = "iris_non_persistent";
private ModdedEntityPersistence() {
}
public static void configure(Entity entity, boolean persistent) {
if (persistent) {
entity.removeTag(NON_PERSISTENT_TAG);
if (entity instanceof Mob mob) {
mob.setPersistenceRequired();
}
return;
}
if (!entity.entityTags().contains(NON_PERSISTENT_TAG) && !entity.addTag(NON_PERSISTENT_TAG)) {
IrisLogging.warn("Iris could not mark generated entity '" + entity.getStringUUID() + "' as non-persistent");
}
}
public static boolean shouldSave(Entity entity, boolean vanillaResult) {
return shouldSave(entity.entityTags(), vanillaResult);
}
static boolean shouldSave(Set<String> tags, boolean vanillaResult) {
return vanillaResult && !tags.contains(NON_PERSISTENT_TAG);
}
static void configureTags(Set<String> tags, boolean persistent) {
if (persistent) {
tags.remove(NON_PERSISTENT_TAG);
} else {
tags.add(NON_PERSISTENT_TAG);
}
}
}
@@ -21,8 +21,8 @@ package art.arcane.iris.modded;
import art.arcane.iris.core.IrisSettings; import art.arcane.iris.core.IrisSettings;
import art.arcane.iris.engine.framework.Engine; import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.object.IrisAttributeModifier; import art.arcane.iris.engine.object.IrisAttributeModifier;
import art.arcane.iris.engine.object.IrisCommand;
import art.arcane.iris.engine.object.IrisEntity; import art.arcane.iris.engine.object.IrisEntity;
import art.arcane.iris.engine.object.IrisEffect;
import art.arcane.iris.engine.object.IrisLoot; import art.arcane.iris.engine.object.IrisLoot;
import art.arcane.iris.modded.api.ModdedCustomContentRegistry; import art.arcane.iris.modded.api.ModdedCustomContentRegistry;
import art.arcane.iris.spi.IrisLogging; import art.arcane.iris.spi.IrisLogging;
@@ -31,13 +31,20 @@ import art.arcane.volmlib.util.math.RNG;
import net.minecraft.core.BlockPos; import net.minecraft.core.BlockPos;
import net.minecraft.core.Holder; import net.minecraft.core.Holder;
import net.minecraft.core.Registry; import net.minecraft.core.Registry;
import net.minecraft.core.particles.ItemParticleOption;
import net.minecraft.core.particles.ParticleOptions;
import net.minecraft.core.particles.ParticleType;
import net.minecraft.core.particles.ParticleTypes;
import net.minecraft.core.particles.SimpleParticleType;
import net.minecraft.core.registries.BuiltInRegistries; import net.minecraft.core.registries.BuiltInRegistries;
import net.minecraft.core.registries.Registries; import net.minecraft.core.registries.Registries;
import net.minecraft.network.chat.Component; import net.minecraft.network.chat.Component;
import net.minecraft.resources.Identifier; import net.minecraft.resources.Identifier;
import net.minecraft.server.MinecraftServer;
import net.minecraft.server.level.ServerLevel; import net.minecraft.server.level.ServerLevel;
import net.minecraft.world.entity.AgeableMob; import net.minecraft.server.level.ServerPlayer;
import net.minecraft.sounds.SoundEvent;
import net.minecraft.sounds.SoundEvents;
import net.minecraft.sounds.SoundSource;
import net.minecraft.world.entity.Entity; import net.minecraft.world.entity.Entity;
import net.minecraft.world.entity.EntitySpawnReason; import net.minecraft.world.entity.EntitySpawnReason;
import net.minecraft.world.entity.EntityType; import net.minecraft.world.entity.EntityType;
@@ -48,21 +55,28 @@ import net.minecraft.world.entity.ai.attributes.Attribute;
import net.minecraft.world.entity.ai.attributes.AttributeInstance; import net.minecraft.world.entity.ai.attributes.AttributeInstance;
import net.minecraft.world.entity.ai.attributes.AttributeModifier; import net.minecraft.world.entity.ai.attributes.AttributeModifier;
import net.minecraft.world.entity.animal.panda.Panda; import net.minecraft.world.entity.animal.panda.Panda;
import net.minecraft.world.entity.monster.zombie.Zombie;
import net.minecraft.world.entity.npc.villager.Villager; import net.minecraft.world.entity.npc.villager.Villager;
import net.minecraft.world.item.ItemStack; import net.minecraft.world.item.ItemStack;
import net.minecraft.world.level.block.Blocks;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.phys.AABB;
import java.util.Locale; import java.util.Locale;
import java.util.Set; import java.util.Set;
import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Predicate;
public final class ModdedEntitySpawner { public final class ModdedEntitySpawner {
private static final int PASSENGER_RNG_BASE = 234858; private static final int PASSENGER_RNG_BASE = 234858;
private static final int LEASH_RNG_SEED = 234548; private static final int LEASH_RNG_SEED = 234548;
private static final int PLAYER_EFFECT_RADIUS = 32;
private static final int RISE_MAX_MOVES = 101;
private static final String COLOR_CODES = "0123456789AaBbCcDdEeFfKkLlMmNnOoRrXx"; private static final String COLOR_CODES = "0123456789AaBbCcDdEeFfKkLlMmNnOoRrXx";
private static final Set<String> WARNED_TYPES = ConcurrentHashMap.newKeySet(); private static final Set<String> WARNED_TYPES = ConcurrentHashMap.newKeySet();
private static final Set<String> WARNED_ATTRIBUTES = ConcurrentHashMap.newKeySet(); private static final Set<String> WARNED_ATTRIBUTES = ConcurrentHashMap.newKeySet();
private static boolean warnedSpawnEffect = false; private static final Set<String> WARNED_EFFECT_SOUNDS = ConcurrentHashMap.newKeySet();
private static final Set<String> WARNED_EFFECT_PARTICLES = ConcurrentHashMap.newKeySet();
private static final Set<String> WARNED_PERSISTENCE_TYPES = ConcurrentHashMap.newKeySet();
private ModdedEntitySpawner() { private ModdedEntitySpawner() {
} }
@@ -71,12 +85,18 @@ public final class ModdedEntitySpawner {
if (engine == null || irisEntity == null || level == null) { if (engine == null || irisEntity == null || level == null) {
return null; return null;
} }
if (!chunksSafe(level, blockX >> 4, blockZ >> 4)) {
return null;
}
double x = blockX + 0.5; double x = blockX + 0.5;
double y = blockY + 0.5; double y = blockY + 0.5;
double z = blockZ + 0.5; double z = blockZ + 0.5;
boolean riseEffectActive = irisEntity.isSpawnEffectRiseOutOfGround() && hasPlayersNearby(level, x, y, z);
int spawnBlockY = riseEffectActive ? blockY - 5 : blockY;
double spawnY = spawnBlockY + 0.5;
Entity created = create(irisEntity, level, x, y, z); Entity created = create(irisEntity, level, x, spawnY, z);
if (created == null) { if (created == null) {
return null; return null;
} }
@@ -85,7 +105,7 @@ public final class ModdedEntitySpawner {
return created; return created;
} }
applyConfig(engine, irisEntity, created, level, blockX, blockY, blockZ, rng); applyConfig(engine, irisEntity, created, level, blockX, spawnBlockY, blockZ, rng, riseEffectActive);
return created; return created;
} }
@@ -99,12 +119,16 @@ public final class ModdedEntitySpawner {
return null; return null;
} }
return type.spawn(level, BlockPos.containing(x, y, z), reasonFor(irisEntity.getReason())); Entity entity = type.spawn(level, BlockPos.containing(x, y, z), reasonFor(irisEntity.getReason()));
if (entity != null) {
entity.snapTo(x, y, z, 0F, 0F);
}
return entity;
} }
private static void applyConfig(Engine engine, IrisEntity irisEntity, Entity entity, ServerLevel level, int blockX, int blockY, int blockZ, RNG rng) { private static void applyConfig(Engine engine, IrisEntity irisEntity, Entity entity, ServerLevel level, int blockX, int blockY, int blockZ, RNG rng, boolean riseEffectActive) {
String customName = irisEntity.getCustomName(); String customName = irisEntity.getCustomName();
if (customName != null && !customName.isBlank()) { if (customName != null) {
entity.setCustomName(Component.literal(colorize(customName))); entity.setCustomName(Component.literal(colorize(customName)));
} }
entity.setCustomNameVisible(irisEntity.isCustomNameVisible()); entity.setCustomNameVisible(irisEntity.isCustomNameVisible());
@@ -114,9 +138,7 @@ public final class ModdedEntitySpawner {
entity.setSilent(irisEntity.isSilent()); entity.setSilent(irisEntity.isSilent());
boolean persistent = irisEntity.isKeepEntity() || forcePersist(); boolean persistent = irisEntity.isKeepEntity() || forcePersist();
if (persistent && entity instanceof Mob persistentMob) { applyPersistence(entity, persistent);
persistentMob.setPersistenceRequired();
}
applyPassengers(engine, irisEntity, entity, level, blockX, blockY, blockZ, rng); applyPassengers(engine, irisEntity, entity, level, blockX, blockY, blockZ, rng);
@@ -124,12 +146,13 @@ public final class ModdedEntitySpawner {
applyAttributes(irisEntity, living, level, rng); applyAttributes(irisEntity, living, level, rng);
} }
if (entity instanceof LivingEntity lootHolder && !irisEntity.getLoot().getTables().isEmpty()) { if (!irisEntity.getLoot().getTables().isEmpty()) {
ModdedDeathLoot.tag(lootHolder, irisEntity.getLoot().getTables(), blockX, blockY, blockZ, rng); ModdedDeathLoot.bind(engine, entity, irisEntity.getLoot().getTables(), blockX, blockY, blockZ, rng);
} }
if (entity instanceof Mob mob) { if (entity instanceof Mob mob) {
mob.setNoAi(!irisEntity.isAi()); mob.setNoAi(shouldDisableAi(irisEntity.isAi()));
ModdedEntityAwareness.configure(mob, irisEntity.isAware());
mob.setCanPickUpLoot(irisEntity.isPickupItems()); mob.setCanPickUpLoot(irisEntity.isPickupItems());
if (!irisEntity.isRemovable()) { if (!irisEntity.isRemovable()) {
mob.setPersistenceRequired(); mob.setPersistenceRequired();
@@ -156,14 +179,14 @@ public final class ModdedEntitySpawner {
} }
if (entity instanceof Villager villager) { if (entity instanceof Villager villager) {
villager.setPersistenceRequired(); ModdedEntityPersistence.configure(villager, true);
} }
if (irisEntity.getSpawnEffect() != null || irisEntity.isSpawnEffectRiseOutOfGround()) { applySpawnEffect(irisEntity.getSpawnEffect(), entity, level);
noteSpawnEffectGap(); ModdedEntityCommandRunner.run(irisEntity.getRawCommands(), level, blockX, blockY, blockZ);
if (riseEffectActive && entity instanceof LivingEntity living) {
startRiseEffect(engine, level, entity, living);
} }
applyRawCommands(irisEntity, level, blockX, blockY, blockZ);
} }
private static void applyPassengers(Engine engine, IrisEntity irisEntity, Entity entity, ServerLevel level, int blockX, int blockY, int blockZ, RNG rng) { private static void applyPassengers(Engine engine, IrisEntity irisEntity, Entity entity, ServerLevel level, int blockX, int blockY, int blockZ, RNG rng) {
@@ -228,46 +251,153 @@ public final class ModdedEntitySpawner {
} }
private static void applyBaby(Entity entity) { private static void applyBaby(Entity entity) {
if (entity instanceof AgeableMob ageable) { if (entity instanceof Mob mob) {
ageable.setBaby(true); mob.setBaby(true);
return;
}
if (entity instanceof Zombie zombie) {
zombie.setBaby(true);
} }
} }
private static void applyRawCommands(IrisEntity irisEntity, ServerLevel level, int blockX, int blockY, int blockZ) { static boolean isAreaClearForSpawn(ServerLevel level, IrisEntity irisEntity, int blockX, int blockY, int blockZ) {
KList<IrisCommand> rawCommands = irisEntity.getRawCommands(); if (irisEntity.isSpecialType()) {
if (rawCommands.isEmpty()) { return true;
return;
} }
EntityType<?> type = resolveType(irisEntity.getType());
MinecraftServer server = level.getServer(); if (type == null) {
if (server == null) { return true;
return;
} }
return isAreaClearForSpawn(blockX, blockY, blockZ, type.getWidth(), type.getHeight(),
position -> level.getBlockState(position).is(Blocks.AIR));
}
for (IrisCommand command : rawCommands) { static boolean isAreaClearForSpawn(int blockX, int blockY, int blockZ, float width, float height, Predicate<BlockPos> isAir) {
for (String raw : command.getCommands()) { int radius = (int) (width / 2F);
if (raw == null || raw.isBlank()) { int endY = blockY + (int) height;
continue; for (int x = blockX - radius; x <= blockX + radius; x++) {
for (int y = blockY; y <= endY; y++) {
for (int z = blockZ - radius; z <= blockZ + radius; z++) {
if (!isAir.test(new BlockPos(x, y, z))) {
return false;
}
} }
String prepared = (raw.startsWith("/") ? raw.substring(1) : raw)
.replace("{x}", String.valueOf(blockX))
.replace("{y}", String.valueOf(blockY))
.replace("{z}", String.valueOf(blockZ));
ModdedServerCommands.dispatch(server, prepared);
} }
} }
return true;
} }
private static EntityType<?> resolveType(String key) { static boolean chunksSafe(ServerLevel level, int chunkX, int chunkZ) {
return allNeighborChunksLoaded(chunkX, chunkZ,
(x, z) -> level.getChunkSource().getChunkNow(x, z) != null);
}
static boolean allNeighborChunksLoaded(int chunkX, int chunkZ, ChunkLoadedLookup lookup) {
for (int x = chunkX - 1; x <= chunkX + 1; x++) {
for (int z = chunkZ - 1; z <= chunkZ + 1; z++) {
if (!lookup.loaded(x, z)) {
return false;
}
}
}
return true;
}
static boolean shouldDisableAi(boolean ai) {
return !ai;
}
private static boolean hasPlayersNearby(ServerLevel level, double x, double y, double z) {
AABB bounds = new AABB(
x - PLAYER_EFFECT_RADIUS,
y - PLAYER_EFFECT_RADIUS,
z - PLAYER_EFFECT_RADIUS,
x + PLAYER_EFFECT_RADIUS,
y + PLAYER_EFFECT_RADIUS,
z + PLAYER_EFFECT_RADIUS);
for (ServerPlayer player : level.players()) {
if (player.getBoundingBox().intersects(bounds)) {
return true;
}
}
return false;
}
private static void applySpawnEffect(IrisEffect effect, Entity entity, ServerLevel level) {
if (effect == null || !effect.shouldApplyNow()) {
return;
}
SoundEvent sound = resolveSound(effect.getSoundKey());
if (sound != null) {
double soundX = entity.getX() + RNG.r.i(-effect.getSoundDistance(), effect.getSoundDistance());
double soundY = entity.getY() + RNG.r.i(-effect.getSoundDistance(), effect.getSoundDistance());
double soundZ = entity.getZ() + RNG.r.i(-effect.getSoundDistance(), effect.getSoundDistance());
level.playSound(null, soundX, soundY, soundZ, sound, SoundSource.MASTER,
(float) effect.getVolume(), (float) RNG.r.d(effect.getMinPitch(), effect.getMaxPitch()));
}
ParticleOptions particle = resolveParticle(effect.getParticleEffectKey());
if (particle == null) {
return;
}
double addition = RNG.r.d();
double subtraction = RNG.r.d();
double particleX = entity.getX() + addition - subtraction + RNG.r.d();
double particleY = entity.getY() + 0.25 + addition - subtraction + level.getMinY() + RNG.r.i(effect.getParticleOffset());
double particleZ = entity.getZ() + addition - subtraction + RNG.r.d();
double altX = effect.isRandomAltX() ? RNG.r.d(-effect.getParticleAltX(), effect.getParticleAltX()) : effect.getParticleAltX();
double altY = effect.isRandomAltY() ? RNG.r.d(-effect.getParticleAltY(), effect.getParticleAltY()) : effect.getParticleAltY();
double altZ = effect.isRandomAltZ() ? RNG.r.d(-effect.getParticleAltZ(), effect.getParticleAltZ()) : effect.getParticleAltZ();
level.sendParticles(particle, particleX, particleY, particleZ, effect.getParticleCount(),
altX, altY, altZ, effect.getExtra());
}
private static SoundEvent resolveSound(String key) {
Identifier id = identifierFor(key);
if (id == null || !BuiltInRegistries.SOUND_EVENT.containsKey(id)) {
if (key != null && WARNED_EFFECT_SOUNDS.add(key)) {
IrisLogging.warn("Iris entity effect: unknown sound '" + key + "'");
}
return null;
}
return BuiltInRegistries.SOUND_EVENT.getValue(id);
}
private static ParticleOptions resolveParticle(String key) {
Identifier id = identifierFor(key);
if (id == null || !BuiltInRegistries.PARTICLE_TYPE.containsKey(id)) {
if (key != null && WARNED_EFFECT_PARTICLES.add(key)) {
IrisLogging.warn("Iris entity effect: unknown particle '" + key + "'");
}
return null;
}
ParticleType<?> type = BuiltInRegistries.PARTICLE_TYPE.getValue(id);
if (type instanceof SimpleParticleType simple) {
return simple;
}
if (WARNED_EFFECT_PARTICLES.add(key)) {
IrisLogging.warn("Iris entity effect: particle '" + key + "' requires data that the effect does not define");
}
return null;
}
private static Identifier identifierFor(String key) {
if (key == null || key.isBlank()) { if (key == null || key.isBlank()) {
return null; return null;
} }
String normalized = key.trim().toLowerCase(Locale.ROOT); String normalized = key.trim().toLowerCase(Locale.ROOT);
Identifier id = Identifier.tryParse(normalized.indexOf(':') >= 0 ? normalized : "minecraft:" + normalized); return Identifier.tryParse(normalized.indexOf(':') >= 0 ? normalized : "minecraft:" + normalized);
}
private static void startRiseEffect(Engine engine, ServerLevel level, Entity entity, LivingEntity living) {
RiseTask task = new RiseTask(engine, level, entity, living);
task.start();
}
static EntityType<?> resolveType(String key) {
if (key == null || key.isBlank()) {
return null;
}
String normalized = key.trim().toLowerCase(Locale.ROOT);
Identifier id = identifierFor(normalized);
if (id == null || !BuiltInRegistries.ENTITY_TYPE.containsKey(id)) { if (id == null || !BuiltInRegistries.ENTITY_TYPE.containsKey(id)) {
if (WARNED_TYPES.add(normalized)) { if (WARNED_TYPES.add(normalized)) {
IrisLogging.warn("Iris entity: unknown entity type '" + key + "'; skipping spawn"); IrisLogging.warn("Iris entity: unknown entity type '" + key + "'; skipping spawn");
@@ -277,23 +407,35 @@ public final class ModdedEntitySpawner {
return BuiltInRegistries.ENTITY_TYPE.getValue(id); return BuiltInRegistries.ENTITY_TYPE.getValue(id);
} }
private static EntitySpawnReason reasonFor(String reason) { static EntitySpawnReason reasonFor(String reason) {
if (reason == null || reason.isBlank()) { if (reason == null || reason.isBlank()) {
return EntitySpawnReason.NATURAL; return EntitySpawnReason.NATURAL;
} }
String value = reason.trim().toUpperCase(Locale.ROOT); String value = reason.trim().toUpperCase(Locale.ROOT);
String mapped = switch (value) { return switch (value) {
case "CHUNK_GEN" -> "CHUNK_GENERATION"; case "NATURAL", "DEFAULT" -> EntitySpawnReason.NATURAL;
case "SPAWNER_EGG" -> "SPAWN_EGG"; case "JOCKEY", "MOUNT" -> EntitySpawnReason.JOCKEY;
case "DISPENSE_EGG" -> "DISPENSER"; case "CHUNK_GEN" -> EntitySpawnReason.CHUNK_GENERATION;
case "BUILD_SNOWMAN", "BUILD_IRONGOLEM", "BUILD_WITHER", "CUSTOM", "DEFAULT" -> "MOB_SUMMONED"; case "SPAWNER" -> EntitySpawnReason.SPAWNER;
default -> value; case "TRIAL_SPAWNER" -> EntitySpawnReason.TRIAL_SPAWNER;
case "EGG", "BUILD_SNOWMAN", "BUILD_IRONGOLEM", "BUILD_COPPERGOLEM", "BUILD_WITHER",
"SLIME_SPLIT", "SILVERFISH_BLOCK", "TRAP", "ENDER_PEARL", "EXPLOSION",
"ENCHANTMENT", "OMINOUS_ITEM_SPAWNER", "POTION_EFFECT", "REANIMATE" -> EntitySpawnReason.TRIGGERED;
case "SPAWNER_EGG" -> EntitySpawnReason.SPAWN_ITEM_USE;
case "LIGHTNING", "VILLAGE_INVASION", "RAID", "CUSTOM" -> EntitySpawnReason.EVENT;
case "VILLAGE_DEFENSE", "SPELL" -> EntitySpawnReason.MOB_SUMMONED;
case "BREEDING", "OCELOT_BABY", "DUPLICATION", "REHYDRATION" -> EntitySpawnReason.BREEDING;
case "REINFORCEMENTS" -> EntitySpawnReason.REINFORCEMENT;
case "NETHER_PORTAL" -> EntitySpawnReason.STRUCTURE;
case "DISPENSE_EGG" -> EntitySpawnReason.DISPENSER;
case "INFECTION", "CURED", "DROWNED", "SHEARED", "PIGLIN_ZOMBIFIED", "FROZEN",
"METAMORPHOSIS" -> EntitySpawnReason.CONVERSION;
case "SHOULDER_ENTITY", "BEEHIVE" -> EntitySpawnReason.LOAD;
case "PATROL" -> EntitySpawnReason.PATROL;
case "COMMAND" -> EntitySpawnReason.COMMAND;
case "BUCKET" -> EntitySpawnReason.BUCKET;
default -> EntitySpawnReason.NATURAL;
}; };
try {
return EntitySpawnReason.valueOf(mapped);
} catch (IllegalArgumentException ignored) {
return EntitySpawnReason.NATURAL;
}
} }
private static Panda.Gene gene(String value) { private static Panda.Gene gene(String value) {
@@ -311,10 +453,13 @@ public final class ModdedEntitySpawner {
return IrisSettings.get().getWorld().isForcePersistEntities(); return IrisSettings.get().getWorld().isForcePersistEntities();
} }
private static void noteSpawnEffectGap() { private static void applyPersistence(Entity entity, boolean persistent) {
if (!warnedSpawnEffect) { ModdedEntityPersistence.configure(entity, persistent);
warnedSpawnEffect = true; if (persistent && (!entity.getType().canSerialize() || !entity.shouldBeSaved())) {
IrisLogging.debug("Iris entity spawnEffect / spawnEffectRiseOutOfGround are Bukkit-only visual paths and are skipped on modded."); String type = String.valueOf(BuiltInRegistries.ENTITY_TYPE.getKey(entity.getType()));
if (WARNED_PERSISTENCE_TYPES.add(type)) {
IrisLogging.warn("Iris entity: vanilla cannot persist non-serializable entity type '" + type + "'");
}
} }
} }
@@ -334,4 +479,106 @@ public final class ModdedEntitySpawner {
} }
return new String(chars); return new String(chars);
} }
@FunctionalInterface
interface ChunkLoadedLookup {
boolean loaded(int chunkX, int chunkZ);
}
private static final class RiseTask implements Runnable {
private final Engine engine;
private final ServerLevel level;
private final Entity entity;
private final LivingEntity living;
private final double x;
private final double z;
private final boolean originalInvulnerable;
private final boolean originalNoPhysics;
private final int originalInvulnerableTime;
private final boolean originalNoAi;
private double y;
private int moves;
private boolean restored;
private RiseTask(Engine engine, ServerLevel level, Entity entity, LivingEntity living) {
this.engine = engine;
this.level = level;
this.entity = entity;
this.living = living;
this.x = entity.getX();
this.y = entity.getY();
this.z = entity.getZ();
this.originalInvulnerable = entity.isInvulnerable();
this.originalNoPhysics = entity.noPhysics;
this.originalInvulnerableTime = entity.invulnerableTime;
this.originalNoAi = entity instanceof Mob mob && mob.isNoAi();
}
private void start() {
entity.setInvulnerable(true);
entity.noPhysics = true;
entity.invulnerableTime = 100_000;
if (entity instanceof Mob mob) {
mob.setNoAi(true);
}
run();
}
@Override
public void run() {
if (moves >= RISE_MAX_MOVES || !entity.isAlive() || engine.isClosed() || entity.level() != level) {
restore();
return;
}
BlockPos current = entity.blockPosition();
if (level.getChunkSource().getChunkNow(current.getX() >> 4, current.getZ() >> 4) == null) {
restore();
return;
}
BlockPos eye = BlockPos.containing(entity.getX(), living.getEyeY(), entity.getZ());
if (!ModdedBlockResolution.isSolid(level.getBlockState(current))
&& !ModdedBlockResolution.isSolid(level.getBlockState(eye))) {
restore();
return;
}
moves++;
y += 0.1;
entity.setPos(x, y, z);
emitEffects();
ModdedScheduler scheduler = ModdedEngineBootstrap.schedulerOrNull();
if (scheduler == null) {
IrisLogging.error("Iris could not continue an entity rise effect because the modded scheduler is unavailable.");
restore();
return;
}
scheduler.laterGlobal(this, 1);
}
private void emitEffects() {
BlockPos source = BlockPos.containing(entity.getX(), living.getEyeY() - 2, entity.getZ());
BlockState state = level.getBlockState(source);
ItemParticleOption item = new ItemParticleOption(ParticleTypes.ITEM, state.getBlock().asItem());
level.sendParticles(item, entity.getX(), living.getEyeY(), entity.getZ(), 6,
0.2, 0.4, 0.2, 0.06);
if (RNG.r.nextDouble() < 0.2) {
level.playSound(null, entity.getX(), entity.getY(), entity.getZ(), SoundEvents.CHORUS_FLOWER_GROW,
SoundSource.MASTER, 0.8F, 0.1F);
}
}
private void restore() {
if (restored) {
return;
}
restored = true;
entity.invulnerableTime = originalInvulnerableTime;
entity.noPhysics = originalNoPhysics;
entity.setInvulnerable(originalInvulnerable);
if (entity instanceof Mob mob) {
mob.setNoAi(originalNoAi);
}
}
}
} }
@@ -38,13 +38,14 @@ import org.slf4j.LoggerFactory;
import java.io.File; import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.nio.file.Files; import java.nio.file.Files;
import java.nio.file.Path; import java.nio.file.Path;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Comparator; import java.util.Comparator;
import java.util.LinkedHashMap;
import java.util.List; import java.util.List;
import java.util.Map;
import java.util.Optional; import java.util.Optional;
import java.util.function.Consumer; import java.util.function.Consumer;
import java.util.stream.Stream; import java.util.stream.Stream;
@@ -53,10 +54,6 @@ public final class ModdedForcedDatapack {
private static final Logger LOGGER = LoggerFactory.getLogger("Iris"); private static final Logger LOGGER = LoggerFactory.getLogger("Iris");
private static final String PACK_ID = "iris_worldgen"; private static final String PACK_ID = "iris_worldgen";
private static final String PACK_FOLDER = "iris"; private static final String PACK_FOLDER = "iris";
private static final String STUDIO_POOL_TYPE_KEY = "studio_pool";
private static final String STUDIO_POOL_TYPE_RESOURCE = "/data/irisworldgen/dimension_type/overworld.json";
private static final int STUDIO_POOL_MIN_Y = -256;
private static final int STUDIO_POOL_MAX_Y = 512;
private static final Object LOCK = new Object(); private static final Object LOCK = new Object();
private ModdedForcedDatapack() { private ModdedForcedDatapack() {
@@ -80,7 +77,7 @@ public final class ModdedForcedDatapack {
} }
private static Pack buildPack() { private static Pack buildPack() {
Path directory = generate(); Path directory = regenerate();
if (directory == null) { if (directory == null) {
return null; return null;
} }
@@ -98,7 +95,7 @@ public final class ModdedForcedDatapack {
return pack; return pack;
} }
private static Path generate() { public static Path regenerate() {
synchronized (LOCK) { synchronized (LOCK) {
try { try {
return write(); return write();
@@ -122,7 +119,7 @@ public final class ModdedForcedDatapack {
File packFolder = packDirectory.toFile(); File packFolder = packDirectory.toFile();
KList<File> folders = new KList<>(); KList<File> folders = new KList<>();
folders.add(packFolder); folders.add(packFolder);
KSet<String> seenBiomes = new KSet<>(); Map<String, KSet<String>> seenBiomes = new LinkedHashMap<>();
IDataFixer fixer = DataVersion.getLatest().get(); IDataFixer fixer = DataVersion.getLatest().get();
int packCount = 0; int packCount = 0;
@@ -137,18 +134,20 @@ public final class ModdedForcedDatapack {
} }
writePackMeta(packDirectory); writePackMeta(packDirectory);
writeStudioPoolType(packDirectory); if ("forge".equalsIgnoreCase(ModdedEngineBootstrap.loader().platformName())) {
writeForgeBlockLootModifier(packDirectory);
}
if (!presetIds.isEmpty()) { if (!presetIds.isEmpty()) {
writeWorldPresetTag(packDirectory, presetIds); writeWorldPresetTag(packDirectory, presetIds);
} }
LOGGER.info("Iris forced startup datapack regenerated: {} pack(s), {} world preset(s), {} custom biome(s) at {}", packCount, presetIds.size(), seenBiomes.size(), packDirectory); LOGGER.info("Iris forced startup datapack regenerated: {} pack(s), {} world preset(s), {} custom biome(s) at {}", packCount, presetIds.size(), countBiomes(seenBiomes), packDirectory);
if (packCount == 0) { if (packCount == 0) {
LOGGER.warn("Iris installed NO worldgen packs into the forced datapack - custom biomes and their colors will NOT generate. Install a pack (e.g. /iris download overworld) and restart the server before creating an Iris world."); LOGGER.warn("Iris installed NO worldgen packs into the forced datapack - custom biomes and their colors will NOT generate. Install a pack (e.g. /iris download overworld) and restart the server before creating an Iris world.");
} }
return packDirectory; return packDirectory;
} }
private static boolean installPack(File packFolder, IDataFixer fixer, KList<File> folders, KSet<String> seenBiomes, KList<String> presetIds) { private static boolean installPack(File packFolder, IDataFixer fixer, KList<File> folders, Map<String, KSet<String>> seenBiomes, KList<String> presetIds) {
String packName = packFolder.getName(); String packName = packFolder.getName();
File[] dimensionFiles = new File(packFolder, "dimensions").listFiles((File file) -> file.isFile() && file.getName().endsWith(".json")); File[] dimensionFiles = new File(packFolder, "dimensions").listFiles((File file) -> file.isFile() && file.getName().endsWith(".json"));
if (dimensionFiles == null || dimensionFiles.length == 0) { if (dimensionFiles == null || dimensionFiles.length == 0) {
@@ -163,7 +162,7 @@ public final class ModdedForcedDatapack {
if (dimension == null) { if (dimension == null) {
continue; continue;
} }
dimension.installBiomes(fixer, () -> data, folders, seenBiomes); dimension.installBiomes(fixer, () -> data, folders, biomesForNamespace(seenBiomes, dimension.getLoadKey()));
writeDimensionType(folders, fixer, dimension); writeDimensionType(folders, fixer, dimension);
String presetKey = dimensionKey.equals(packName) ? packName : packName + "_" + dimensionKey; String presetKey = dimensionKey.equals(packName) ? packName : packName + "_" + dimensionKey;
writeWorldPreset(folders, dimension, packName, dimensionKey, presetKey); writeWorldPreset(folders, dimension, packName, dimensionKey, presetKey);
@@ -176,10 +175,19 @@ public final class ModdedForcedDatapack {
return installed; return installed;
} }
static KSet<String> biomesForNamespace(Map<String, KSet<String>> biomes, String namespace) {
return biomes.computeIfAbsent(namespace, ignored -> new KSet<>());
}
public static String dimensionTypeRef(IrisDimension dimension) { public static String dimensionTypeRef(IrisDimension dimension) {
return fitsStudioPool(dimension) return "irisworldgen:" + dimension.getDimensionTypeKey();
? "irisworldgen:" + STUDIO_POOL_TYPE_KEY }
: "irisworldgen:" + dimension.getDimensionTypeKey();
static <T> T requireRegisteredDimensionType(String typeRef, Optional<T> registeredType,
String pack, String packDimensionKey) {
return registeredType.orElseThrow(() -> new IllegalStateException(
"Iris dimension type '" + typeRef + "' for pack '" + pack + "' dimension '"
+ packDimensionKey + "' is not loaded. Restart the server so the forced Iris datapack registers it before creating the world."));
} }
private static void writeWorldPreset(KList<File> folders, IrisDimension dimension, String packName, String dimensionKey, String presetKey) throws IOException { private static void writeWorldPreset(KList<File> folders, IrisDimension dimension, String packName, String dimensionKey, String presetKey) throws IOException {
@@ -250,10 +258,7 @@ public final class ModdedForcedDatapack {
Files.writeString(output, json, StandardCharsets.UTF_8); Files.writeString(output, json, StandardCharsets.UTF_8);
} }
private static void writeDimensionType(KList<File> folders, IDataFixer fixer, IrisDimension dimension) throws IOException { static void writeDimensionType(KList<File> folders, IDataFixer fixer, IrisDimension dimension) throws IOException {
if (fitsStudioPool(dimension)) {
return;
}
IrisDimensionType type = dimension.getDimensionType(); IrisDimensionType type = dimension.getDimensionType();
String json = type.toJson(fixer); String json = type.toJson(fixer);
String typeKey = dimension.getDimensionTypeKey(); String typeKey = dimension.getDimensionTypeKey();
@@ -264,23 +269,20 @@ public final class ModdedForcedDatapack {
} }
} }
private static boolean fitsStudioPool(IrisDimension dimension) { static void writeForgeBlockLootModifier(Path packDirectory) throws IOException {
return dimension.getMinHeight() >= STUDIO_POOL_MIN_Y && dimension.getMaxHeight() <= STUDIO_POOL_MAX_Y; Path list = packDirectory.resolve("data").resolve("forge").resolve("loot_modifiers").resolve("global_loot_modifiers.json");
} Files.createDirectories(list.getParent());
Files.writeString(list, "{\n"
+ " \"replace\": false,\n"
+ " \"entries\": [\"irisworldgen:block_drops\"]\n"
+ "}\n", StandardCharsets.UTF_8);
private static void writeStudioPoolType(Path packDirectory) throws IOException { Path modifier = packDirectory.resolve("data").resolve("irisworldgen").resolve("loot_modifiers").resolve("block_drops.json");
Path output = packDirectory.resolve("data").resolve("irisworldgen").resolve("dimension_type").resolve(STUDIO_POOL_TYPE_KEY + ".json"); Files.createDirectories(modifier.getParent());
Files.createDirectories(output.getParent()); Files.writeString(modifier, "{\n"
Files.writeString(output, readStudioPoolType(), StandardCharsets.UTF_8); + " \"type\": \"irisworldgen:block_drops\",\n"
} + " \"conditions\": []\n"
+ "}\n", StandardCharsets.UTF_8);
private static String readStudioPoolType() throws IOException {
try (InputStream stream = ModdedForcedDatapack.class.getResourceAsStream(STUDIO_POOL_TYPE_RESOURCE)) {
if (stream == null) {
throw new IOException("Bundled studio pool dimension type resource is missing: " + STUDIO_POOL_TYPE_RESOURCE);
}
return new String(stream.readAllBytes(), StandardCharsets.UTF_8);
}
} }
private static void writePackMeta(Path packDirectory) throws IOException { private static void writePackMeta(Path packDirectory) throws IOException {
@@ -296,6 +298,14 @@ public final class ModdedForcedDatapack {
Files.writeString(packDirectory.resolve("pack.mcmeta"), json, StandardCharsets.UTF_8); Files.writeString(packDirectory.resolve("pack.mcmeta"), json, StandardCharsets.UTF_8);
} }
private static int countBiomes(Map<String, KSet<String>> biomes) {
int count = 0;
for (KSet<String> values : biomes.values()) {
count += values.size();
}
return count;
}
private static void clean(Path packDirectory) throws IOException { private static void clean(Path packDirectory) throws IOException {
if (!Files.exists(packDirectory)) { if (!Files.exists(packDirectory)) {
return; return;
@@ -0,0 +1,22 @@
package art.arcane.iris.modded;
import net.minecraft.util.Mth;
import net.minecraft.util.SimpleBitStorage;
import java.util.function.IntBinaryOperator;
final class ModdedHeightmaps {
private ModdedHeightmaps() {
}
static long[] terrainRawData(int height, IntBinaryOperator heightResolver) {
SimpleBitStorage storage = new SimpleBitStorage(Mth.ceillog2(height + 1), 256);
for (int z = 0; z < 16; z++) {
for (int x = 0; x < 16; x++) {
int value = Mth.clamp(heightResolver.applyAsInt(x, z), 0, height);
storage.set(x + z * 16, value);
}
}
return storage.getRaw();
}
}
@@ -23,6 +23,8 @@ import art.arcane.iris.engine.framework.PlacedObject;
import art.arcane.iris.engine.object.IObjectLoot; import art.arcane.iris.engine.object.IObjectLoot;
import art.arcane.iris.engine.object.InventorySlotType; import art.arcane.iris.engine.object.InventorySlotType;
import art.arcane.iris.engine.object.IrisBiome; import art.arcane.iris.engine.object.IrisBiome;
import art.arcane.iris.engine.object.IrisLootMode;
import art.arcane.iris.engine.object.IrisLootReference;
import art.arcane.iris.engine.object.IrisLootTable; import art.arcane.iris.engine.object.IrisLootTable;
import art.arcane.iris.engine.object.IrisObjectLoot; import art.arcane.iris.engine.object.IrisObjectLoot;
import art.arcane.iris.engine.object.IrisObjectPlacement; import art.arcane.iris.engine.object.IrisObjectPlacement;
@@ -43,41 +45,24 @@ import net.minecraft.world.Container;
import net.minecraft.world.item.ItemStack; import net.minecraft.world.item.ItemStack;
import net.minecraft.world.level.block.ChestBlock; import net.minecraft.world.level.block.ChestBlock;
import net.minecraft.world.level.block.entity.BlockEntity; import net.minecraft.world.level.block.entity.BlockEntity;
import net.minecraft.world.level.block.entity.RandomizableContainerBlockEntity;
import net.minecraft.world.level.block.state.BlockState; import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.storage.loot.LootParams;
import net.minecraft.world.level.storage.loot.LootTable;
import net.minecraft.world.level.storage.loot.parameters.LootContextParamSets;
import net.minecraft.world.level.storage.loot.parameters.LootContextParams;
import net.minecraft.world.phys.Vec3;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.function.Predicate;
public final class ModdedLootApplier { public final class ModdedLootApplier {
private ModdedLootApplier() { private ModdedLootApplier() {
} }
private record Candidate(IrisLootTable table, Identifier vanillaId, int weight) {
}
private record Resolution(KList<IrisLootTable> tables, Identifier vanillaTable) {
boolean isEmpty() {
return tables.isEmpty() && vanillaTable == null;
}
}
public static void apply(Engine engine, ServerLevel level, BlockPos pos, BlockState state, MantleChunk<Matter> mc, RNG rng, int localX, int localZ) { public static void apply(Engine engine, ServerLevel level, BlockPos pos, BlockState state, MantleChunk<Matter> mc, RNG rng, int localX, int localZ) {
Resolution resolution = resolveTables(engine, rng, pos, state, mc); List<LootSource> sources = resolveSources(engine, level, rng, pos, state, mc);
if (resolution.isEmpty()) { if (sources.isEmpty()) {
return;
}
if (resolution.vanillaTable() != null) {
BlockEntity blockEntity = level.getBlockEntity(pos);
if (blockEntity instanceof RandomizableContainerBlockEntity randomizable) {
randomizable.setLootTable(ResourceKey.create(Registries.LOOT_TABLE, resolution.vanillaTable()), rng.nextLong());
} else {
IrisLogging.debug("Iris loot: no randomizable container at " + pos + " for vanilla table " + resolution.vanillaTable());
}
}
if (resolution.tables().isEmpty()) {
return; return;
} }
@@ -88,40 +73,43 @@ public final class ModdedLootApplier {
} }
KList<ItemStack> items = new KList<>(); KList<ItemStack> items = new KList<>();
for (IrisLootTable table : resolution.tables()) { LootParams nativeParams = null;
if (table == null) { for (LootSource source : sources) {
if (source instanceof IrisLootSource irisSource) {
IrisLootTable table = irisSource.table();
if (table == null) {
continue;
}
items.addAll(ModdedItemTranslator.loot(table, rng, InventorySlotType.STORAGE, level, localX, pos.getY(), localZ));
continue; continue;
} }
items.addAll(ModdedItemTranslator.loot(table, rng, InventorySlotType.STORAGE, level, localX, pos.getY(), localZ)); if (source instanceof NativeLootSource nativeSource) {
if (nativeParams == null) {
nativeParams = new LootParams.Builder(level)
.withParameter(LootContextParams.ORIGIN, Vec3.atCenterOf(pos))
.create(LootContextParamSets.CHEST);
}
items.addAll(nativeSource.table().getRandomItems(nativeParams, rng.nextLong()));
}
} }
for (ItemStack item : items) { fillContainer(container, items, rng);
addItem(container, item);
}
scramble(container, rng);
container.setChanged();
} }
private static Resolution resolveTables(Engine engine, RNG rng, BlockPos pos, BlockState state, MantleChunk<Matter> mc) { private static List<LootSource> resolveSources(Engine engine, ServerLevel level, RNG rng, BlockPos pos, BlockState state, MantleChunk<Matter> mc) {
int rx = pos.getX(); int rx = pos.getX();
int rz = pos.getZ(); int rz = pos.getZ();
int ry = pos.getY() - engine.getWorld().minHeight(); int ry = pos.getY() - engine.getWorld().minHeight();
double he = engine.getComplex().getHeightStream().get(rx, rz); double he = engine.getComplex().getHeightStream().get(rx, rz);
KList<IrisLootTable> tables = new KList<>(); List<LootSource> sources = new ArrayList<>();
Identifier vanillaTable = null;
PlacedObject po = engine.getObjectPlacement(rx, ry, rz, mc); PlacedObject po = engine.getObjectPlacement(rx, ry, rz, mc);
if (po != null && po.getPlacement() != null && ModdedBlockResolution.isStorageChest(state)) { if (po != null && po.getPlacement() != null && ModdedBlockResolution.isStorageChest(state)) {
Candidate picked = pickPlacementTable(engine, po.getPlacement(), state, rng); Candidate picked = pickPlacementTable(engine, level, po.getPlacement(), state, rng);
if (picked != null) { if (picked != null) {
if (picked.table() != null) { sources.add(picked.source());
tables.add(picked.table());
} else {
vanillaTable = picked.vanillaId();
}
if (po.getPlacement().isOverrideGlobalLoot()) { if (po.getPlacement().isOverrideGlobalLoot()) {
return new Resolution(tables, vanillaTable); return sources;
} }
} }
} }
@@ -131,28 +119,16 @@ public final class ModdedLootApplier {
IrisBiome biomeUnder = ry < he ? engine.getCaveBiome(rx, ry, rz) : biomeSurface; IrisBiome biomeUnder = ry < he ? engine.getCaveBiome(rx, ry, rz) : biomeSurface;
double multiplier = 1D * engine.getDimension().getLoot().getMultiplier() * region.getLoot().getMultiplier() * biomeSurface.getLoot().getMultiplier() * biomeUnder.getLoot().getMultiplier(); double multiplier = 1D * engine.getDimension().getLoot().getMultiplier() * region.getLoot().getMultiplier() * biomeSurface.getLoot().getMultiplier() * biomeUnder.getLoot().getMultiplier();
boolean fallback = tables.isEmpty() && vanillaTable == null; boolean fallback = sources.isEmpty();
engine.injectTables(tables, engine.getDimension().getLoot(), fallback); injectReferenceSources(sources, engine.getDimension().getLoot(), engine, fallback);
engine.injectTables(tables, region.getLoot(), fallback); injectReferenceSources(sources, region.getLoot(), engine, fallback);
engine.injectTables(tables, biomeSurface.getLoot(), fallback); injectReferenceSources(sources, biomeSurface.getLoot(), engine, fallback);
engine.injectTables(tables, biomeUnder.getLoot(), fallback); injectReferenceSources(sources, biomeUnder.getLoot(), engine, fallback);
scaleSources(sources, multiplier, rng);
if (tables.isNotEmpty()) { return sources;
int target = (int) Math.round(tables.size() * multiplier);
while (tables.size() < target && tables.isNotEmpty()) {
tables.add(tables.get(rng.i(tables.size() - 1)));
}
while (tables.size() > target && tables.isNotEmpty()) {
tables.remove(rng.i(tables.size() - 1));
}
}
return new Resolution(tables, vanillaTable);
} }
private static Candidate pickPlacementTable(Engine engine, IrisObjectPlacement placement, BlockState state, RNG rng) { private static Candidate pickPlacementTable(Engine engine, ServerLevel level, IrisObjectPlacement placement, BlockState state, RNG rng) {
List<Candidate> exact = new ArrayList<>(); List<Candidate> exact = new ArrayList<>();
List<Candidate> basic = new ArrayList<>(); List<Candidate> basic = new ArrayList<>();
List<Candidate> global = new ArrayList<>(); List<Candidate> global = new ArrayList<>();
@@ -166,25 +142,93 @@ public final class ModdedLootApplier {
IrisLogging.warn("Couldn't find loot table " + loot.getName()); IrisLogging.warn("Couldn't find loot table " + loot.getName());
continue; continue;
} }
bucket(engine, loot, new Candidate(table, null, loot.getWeight()), state, exact, basic, global); bucket(engine, loot, new Candidate(new IrisLootSource(table), loot.getWeight()), state, exact, basic, global);
} }
for (IrisObjectVanillaLoot loot : placement.getVanillaLoot()) { for (IrisObjectVanillaLoot loot : placement.getVanillaLoot()) {
if (loot == null) { if (loot == null) {
continue; continue;
} }
Identifier id = loot.getName() == null ? null : Identifier.tryParse(loot.getName()); ResourceKey<LootTable> key = resolveNativeKey(loot.getName(), candidate -> hasNativeTable(level, candidate));
if (id == null) { if (key == null) {
IrisLogging.warn("Couldn't parse vanilla loot table " + loot.getName()); IrisLogging.warn("Couldn't find vanilla loot table " + loot.getName());
continue; continue;
} }
bucket(engine, loot, new Candidate(null, id, loot.getWeight()), state, exact, basic, global); LootTable table = level.getServer().reloadableRegistries().lookup()
.lookupOrThrow(Registries.LOOT_TABLE)
.getOrThrow(key)
.value();
bucket(engine, loot, new Candidate(new NativeLootSource(table), loot.getWeight()), state, exact, basic, global);
} }
List<Candidate> pool = !exact.isEmpty() ? exact : !basic.isEmpty() ? basic : global; List<Candidate> pool = !exact.isEmpty() ? exact : !basic.isEmpty() ? basic : global;
return pickWeighted(pool, rng); return pickWeighted(pool, rng);
} }
static <T> void injectSources(List<T> sources, List<? extends T> additions, IrisLootMode mode, boolean fallback) {
if (mode == IrisLootMode.FALLBACK && !fallback) {
return;
}
if (mode == IrisLootMode.CLEAR || mode == IrisLootMode.REPLACE) {
sources.clear();
}
sources.addAll(additions);
}
static <T> void scaleSources(List<T> sources, double multiplier, RNG rng) {
if (sources.isEmpty()) {
return;
}
int target = (int) Math.round(sources.size() * multiplier);
while (sources.size() < target && !sources.isEmpty()) {
sources.add(sources.get(rng.i(sources.size() - 1)));
}
while (sources.size() > target && !sources.isEmpty()) {
sources.remove(rng.i(sources.size() - 1));
}
}
static ResourceKey<LootTable> resolveNativeKey(String name, Predicate<ResourceKey<LootTable>> registryContains) {
Identifier id = name == null ? null : Identifier.tryParse(name);
if (id == null) {
return null;
}
ResourceKey<LootTable> key = ResourceKey.create(Registries.LOOT_TABLE, id);
return registryContains.test(key) ? key : null;
}
static void fillContainer(Container container, List<ItemStack> items, RNG rng) {
for (ItemStack item : items) {
addItem(container, item);
}
scramble(container, rng);
container.setChanged();
}
private static void injectReferenceSources(List<LootSource> sources, IrisLootReference reference, Engine engine, boolean fallback) {
IrisLootMode mode = reference.getMode();
if (mode == IrisLootMode.FALLBACK && !fallback) {
return;
}
injectSources(sources, irisSources(reference, engine), mode, fallback);
}
private static List<LootSource> irisSources(IrisLootReference reference, Engine engine) {
KList<IrisLootTable> tables = reference.getLootTables(engine.getComplex());
List<LootSource> sources = new ArrayList<>(tables.size());
for (IrisLootTable table : tables) {
sources.add(new IrisLootSource(table));
}
return sources;
}
private static boolean hasNativeTable(ServerLevel level, ResourceKey<LootTable> key) {
return level.getServer().reloadableRegistries().lookup()
.lookup(Registries.LOOT_TABLE)
.flatMap(registry -> registry.get(key))
.isPresent();
}
private static void bucket(Engine engine, IObjectLoot loot, Candidate candidate, BlockState state, List<Candidate> exact, List<Candidate> basic, List<Candidate> global) { private static void bucket(Engine engine, IObjectLoot loot, Candidate candidate, BlockState state, List<Candidate> exact, List<Candidate> basic, List<Candidate> global) {
if (loot.getFilter().isEmpty()) { if (loot.getFilter().isEmpty()) {
global.add(candidate); global.add(candidate);
@@ -288,4 +332,16 @@ public final class ModdedLootApplier {
container.setItem(i, items[i]); container.setItem(i, items[i]);
} }
} }
private interface LootSource {
}
private record IrisLootSource(IrisLootTable table) implements LootSource {
}
private record NativeLootSource(LootTable table) implements LootSource {
}
private record Candidate(LootSource source, int weight) {
}
} }
@@ -0,0 +1,108 @@
/*
* Iris is a World Generator for Minecraft 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.modded;
import art.arcane.iris.spi.PlatformBiome;
import art.arcane.iris.spi.PlatformBlockState;
import art.arcane.iris.spi.PlatformWorld;
import net.minecraft.core.BlockPos;
import net.minecraft.core.Holder;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.world.level.biome.Biome;
import net.minecraft.world.level.block.state.BlockState;
public final class ModdedPlatformWorld implements PlatformWorld {
private final ServerLevel level;
public ModdedPlatformWorld(ServerLevel level) {
this.level = level;
}
@Override
public String name() {
return level.dimension().identifier().toString();
}
@Override
public long seed() {
return level.getSeed();
}
@Override
public int minHeight() {
return level.getMinY();
}
@Override
public int maxHeight() {
return exclusiveMaxHeight(level.getMinY(), level.getHeight());
}
@Override
public PlatformBlockState getBlock(int x, int y, int z) {
return ModdedBlockState.of(level.getBlockState(new BlockPos(x, y, z)), null);
}
@Override
public void setBlock(int x, int y, int z, PlatformBlockState block, int flags) {
level.setBlock(new BlockPos(x, y, z), (BlockState) block.nativeHandle(), flags);
}
@Override
public PlatformBiome getBiome(int x, int y, int z) {
Holder<Biome> biome = level.getBiome(new BlockPos(x, y, z));
String key = biome.unwrapKey()
.map(resourceKey -> resourceKey.identifier().toString())
.orElse("minecraft:plains");
return ModdedBiome.of(biome.value(), key);
}
@Override
public boolean isChunkLoaded(int chunkX, int chunkZ) {
return level.getChunkSource().getChunkNow(chunkX, chunkZ) != null;
}
@Override
public long getTime() {
return level.getDefaultClockTime();
}
@Override
public boolean isStorming() {
return level.isRaining();
}
@Override
public boolean isThundering() {
return level.isThundering();
}
@Override
public Object nativeHandle() {
return level;
}
ServerLevel level() {
return level;
}
static int exclusiveMaxHeight(int minHeight, int height) {
return minHeight + height;
}
}
@@ -30,23 +30,24 @@ public final class ModdedStateMerger implements BlockDataMergeSupport.StateMerge
public PlatformBlockState merge(PlatformBlockState base, PlatformBlockState update) { public PlatformBlockState merge(PlatformBlockState base, PlatformBlockState update) {
ModdedBlockState fabricBase = (ModdedBlockState) base; ModdedBlockState fabricBase = (ModdedBlockState) base;
ModdedBlockState fabricUpdate = (ModdedBlockState) update; ModdedBlockState fabricUpdate = (ModdedBlockState) update;
ModdedBlockState metadataSource = fabricUpdate.isCustom() ? fabricUpdate : fabricBase;
try { try {
return ModdedBlockState.of(mergeStates(fabricBase.handle(), fabricUpdate.handle(), fabricUpdate.parsedProperties()), null); return metadataSource.withHandle(mergeStates(fabricBase.handle(), fabricUpdate.handle(), fabricUpdate.parsedProperties()), null);
} catch (IllegalArgumentException e) { } catch (IllegalArgumentException e) {
ModdedBlockResolution.Parsed normalizedBase = ModdedBlockResolution.resolveGet(ModdedBlockState.serialize(fabricBase.handle())); ModdedBlockResolution.Parsed normalizedBase = ModdedBlockResolution.resolveGet(ModdedBlockState.serialize(fabricBase.handle()));
ModdedBlockResolution.Parsed normalizedUpdate = ModdedBlockResolution.resolveGet(ModdedBlockState.serialize(fabricUpdate.handle())); ModdedBlockResolution.Parsed normalizedUpdate = ModdedBlockResolution.resolveGet(ModdedBlockState.serialize(fabricUpdate.handle()));
if (normalizedBase != null && normalizedUpdate != null) { if (normalizedBase != null && normalizedUpdate != null) {
try { try {
return ModdedBlockState.of(mergeStates(normalizedBase.state(), normalizedUpdate.state(), normalizedUpdate.properties()), null); return metadataSource.withHandle(mergeStates(normalizedBase.state(), normalizedUpdate.state(), normalizedUpdate.properties()), null);
} catch (IllegalArgumentException ignored) { } catch (IllegalArgumentException ignored) {
return ModdedBlockState.of(normalizedUpdate.state(), normalizedUpdate.properties()); return metadataSource.withHandle(normalizedUpdate.state(), normalizedUpdate.properties());
} }
} }
if (normalizedUpdate != null) { if (normalizedUpdate != null) {
return ModdedBlockState.of(normalizedUpdate.state(), normalizedUpdate.properties()); return metadataSource.withHandle(normalizedUpdate.state(), normalizedUpdate.properties());
} }
return update; return update;
@@ -168,7 +168,7 @@ public final class ModdedStateRotator implements IrisObjectRotation.StateRotator
return null; return null;
} }
return d == fabric.handle() ? state : ModdedBlockState.of(d, fabric.parsedProperties()); return d == fabric.handle() ? state : fabric.withHandle(d, fabric.parsedProperties());
} }
private static Property<?> findFacing(BlockState state) { private static Property<?> findFacing(BlockState state) {
@@ -18,18 +18,41 @@
package art.arcane.iris.modded; package art.arcane.iris.modded;
import art.arcane.iris.spi.IrisLogging;
import art.arcane.iris.spi.PlatformStructureHooks; import art.arcane.iris.spi.PlatformStructureHooks;
import art.arcane.iris.spi.PlatformWorld; import art.arcane.iris.spi.PlatformWorld;
import net.minecraft.core.BlockPos;
import net.minecraft.core.Holder; import net.minecraft.core.Holder;
import net.minecraft.core.Registry; import net.minecraft.core.Registry;
import net.minecraft.core.registries.Registries; import net.minecraft.core.registries.Registries;
import net.minecraft.resources.Identifier; import net.minecraft.resources.Identifier;
import net.minecraft.resources.ResourceKey;
import net.minecraft.server.MinecraftServer; import net.minecraft.server.MinecraftServer;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.world.level.ChunkPos;
import net.minecraft.world.level.StructureManager;
import net.minecraft.world.level.biome.Biome; import net.minecraft.world.level.biome.Biome;
import net.minecraft.world.level.biome.BiomeSource;
import net.minecraft.world.level.chunk.ChunkGenerator;
import net.minecraft.world.level.levelgen.RandomState;
import net.minecraft.world.level.levelgen.WorldgenRandom;
import net.minecraft.world.level.levelgen.XoroshiroRandomSource;
import net.minecraft.world.level.levelgen.feature.AbstractHugeMushroomFeature;
import net.minecraft.world.level.levelgen.feature.ConfiguredFeature;
import net.minecraft.world.level.levelgen.feature.FallenTreeFeature;
import net.minecraft.world.level.levelgen.feature.Feature;
import net.minecraft.world.level.levelgen.feature.TreeFeature;
import net.minecraft.world.level.levelgen.structure.BoundingBox;
import net.minecraft.world.level.levelgen.structure.Structure; import net.minecraft.world.level.levelgen.structure.Structure;
import net.minecraft.world.level.levelgen.structure.StructureStart;
import net.minecraft.world.level.levelgen.structure.templatesystem.StructureTemplateManager;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.LinkedHashSet;
import java.util.List; import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.function.Supplier; import java.util.function.Supplier;
public final class ModdedStructureHooks implements PlatformStructureHooks { public final class ModdedStructureHooks implements PlatformStructureHooks {
@@ -52,64 +75,254 @@ public final class ModdedStructureHooks implements PlatformStructureHooks {
@Override @Override
public List<String> structureBiomeKeys(String structureKey) { public List<String> structureBiomeKeys(String structureKey) {
List<String> keys = new ArrayList<>(); List<String> keys = new ArrayList<>();
MinecraftServer instance = server.get(); try {
if (instance == null) { MinecraftServer instance = server.get();
return keys; if (instance == null) {
} return keys;
Identifier identifier = Identifier.tryParse(structureKey); }
if (identifier == null) { Identifier identifier = Identifier.tryParse(structureKey);
return keys; if (identifier == null) {
} return keys;
Registry<Structure> registry = instance.registryAccess().lookupOrThrow(Registries.STRUCTURE); }
Structure structure = registry.getValue(identifier); Registry<Structure> registry = instance.registryAccess().lookupOrThrow(Registries.STRUCTURE);
if (structure == null) { Structure structure = registry.getValue(identifier);
return keys; if (structure == null) {
} return keys;
for (Holder<Biome> holder : structure.biomes()) { }
holder.unwrapKey().ifPresent((net.minecraft.resources.ResourceKey<Biome> key) -> keys.add(key.identifier().toString())); for (Holder<Biome> holder : structure.biomes()) {
Optional<ResourceKey<Biome>> key = holder.unwrapKey();
if (key.isPresent()) {
keys.add(key.get().identifier().toString());
}
}
} catch (Throwable error) {
IrisLogging.reportError(error);
} }
return keys; return keys;
} }
@Override @Override
public List<String> objectFeatureKeys() { public List<String> objectFeatureKeys() {
return registryKeys(Registries.CONFIGURED_FEATURE); List<String> keys = new ArrayList<>();
try {
MinecraftServer instance = server.get();
if (instance == null) {
return keys;
}
Registry<ConfiguredFeature<?, ?>> registry = instance.registryAccess().lookupOrThrow(Registries.CONFIGURED_FEATURE);
for (Identifier identifier : registry.keySet()) {
ConfiguredFeature<?, ?> configuredFeature = registry.getValue(identifier);
if (configuredFeature == null) {
continue;
}
String group = classifyFeature(configuredFeature.feature());
if (group != null) {
keys.add(group + "|" + identifier);
}
}
} catch (Throwable error) {
IrisLogging.reportError(error);
}
return keys;
} }
@Override @Override
public List<String> reachableStructureKeys(PlatformWorld world) { public List<String> reachableStructureKeys(PlatformWorld world) {
return List.of(); List<String> keys = new ArrayList<>();
try {
ServerLevel level = level(world);
if (level == null) {
return keys;
}
BiomeSource source = level.getChunkSource().getGenerator().getBiomeSource();
Set<String> possibleBiomes = possibleBiomeKeys(source);
if (possibleBiomes.isEmpty()) {
return keys;
}
Registry<Structure> registry = level.registryAccess().lookupOrThrow(Registries.STRUCTURE);
for (Map.Entry<ResourceKey<Structure>, Structure> entry : registry.entrySet()) {
if (hasPossibleBiome(entry.getValue(), possibleBiomes)) {
keys.add(entry.getKey().identifier().toString());
}
}
} catch (Throwable error) {
IrisLogging.reportError(error);
}
return keys;
} }
@Override @Override
public List<String> possibleBiomeKeys(PlatformWorld world) { public List<String> possibleBiomeKeys(PlatformWorld world) {
return List.of(); List<String> keys = new ArrayList<>();
try {
ServerLevel level = level(world);
if (level == null) {
return keys;
}
keys.addAll(possibleBiomeKeys(level.getChunkSource().getGenerator().getBiomeSource()));
} catch (Throwable error) {
IrisLogging.reportError(error);
}
return keys;
} }
@Override @Override
public boolean placeFeature(PlatformWorld world, int x, int y, int z, String featureKey, long seed) { public boolean placeFeature(PlatformWorld world, int x, int y, int z, String featureKey, long seed) {
return false; try {
ServerLevel level = level(world);
Identifier identifier = Identifier.tryParse(featureKey);
if (level == null || identifier == null) {
return false;
}
ChunkGenerator generator = level.getChunkSource().getGenerator();
Registry<ConfiguredFeature<?, ?>> registry = level.registryAccess().lookupOrThrow(Registries.CONFIGURED_FEATURE);
ConfiguredFeature<?, ?> configuredFeature = registry.getValue(identifier);
if (configuredFeature == null) {
return false;
}
WorldgenRandom random = new WorldgenRandom(new XoroshiroRandomSource(seed));
return configuredFeature.place(level, generator, random, new BlockPos(x, y, z));
} catch (Throwable error) {
IrisLogging.reportError(error);
return false;
}
} }
@Override @Override
public int[] placeStructure(PlatformWorld world, int chunkX, int chunkZ, String structureKey, long seed, int maxSpan) { public int[] placeStructure(PlatformWorld world, int chunkX, int chunkZ, String structureKey, long seed, int maxSpan) {
return null; try {
ServerLevel level = level(world);
Identifier identifier = Identifier.tryParse(structureKey);
if (level == null || identifier == null) {
return null;
}
ChunkGenerator generator = level.getChunkSource().getGenerator();
Registry<Structure> registry = level.registryAccess().lookupOrThrow(Registries.STRUCTURE);
Structure structure = registry.getValue(identifier);
if (structure == null) {
return null;
}
Holder<Structure> holder = registry.wrapAsHolder(structure);
RandomState randomState = level.getChunkSource().randomState();
StructureTemplateManager templateManager = level.getStructureManager();
StructureManager structureManager = level.structureManager();
BiomeSource biomeSource = generator.getBiomeSource();
ChunkPos chunkPos = new ChunkPos(chunkX, chunkZ);
StructureStart start = structure.generate(
holder,
level.dimension(),
level.registryAccess(),
generator,
biomeSource,
randomState,
templateManager,
seed,
chunkPos,
0,
level,
biome -> true);
if (start == null || !start.isValid()) {
return null;
}
BoundingBox box = start.getBoundingBox();
if (!isWithinSpan(box, maxSpan)) {
return null;
}
placeChunks(level, structureManager, generator, start, box, seed);
return bounds(box);
} catch (Throwable error) {
IrisLogging.reportError(error);
return null;
}
} }
@Override @Override
public boolean supportsStructurePlacement() { public boolean supportsStructurePlacement() {
return true;
}
static String classifyFeature(Feature<?> feature) {
if (feature instanceof TreeFeature) {
return "trees";
}
if (feature instanceof FallenTreeFeature) {
return "fallen_trees";
}
if (feature instanceof AbstractHugeMushroomFeature) {
return "mushrooms";
}
return null;
}
static boolean isWithinSpan(BoundingBox box, int maxSpan) {
return maxSpan <= 0
|| box.getXSpan() <= maxSpan && box.getYSpan() <= maxSpan && box.getZSpan() <= maxSpan;
}
static int[] bounds(BoundingBox box) {
return new int[]{box.minX(), box.minY(), box.minZ(), box.maxX(), box.maxY(), box.maxZ()};
}
private static Set<String> possibleBiomeKeys(BiomeSource source) {
Set<String> keys = new LinkedHashSet<>();
if (source == null) {
return keys;
}
for (Holder<Biome> holder : source.possibleBiomes()) {
Optional<ResourceKey<Biome>> key = holder.unwrapKey();
if (key.isPresent()) {
keys.add(key.get().identifier().toString());
}
}
return keys;
}
private static boolean hasPossibleBiome(Structure structure, Set<String> possibleBiomeKeys) {
for (Holder<Biome> holder : structure.biomes()) {
Optional<ResourceKey<Biome>> key = holder.unwrapKey();
if (key.isPresent() && possibleBiomeKeys.contains(key.get().identifier().toString())) {
return true;
}
}
return false; return false;
} }
private <T> List<String> registryKeys(net.minecraft.resources.ResourceKey<net.minecraft.core.Registry<T>> registryKey) { private static ServerLevel level(PlatformWorld world) {
List<String> keys = new ArrayList<>(); return world instanceof ModdedPlatformWorld moddedWorld ? moddedWorld.level() : null;
MinecraftServer instance = server.get(); }
if (instance == null) {
return keys; private static void placeChunks(ServerLevel level, StructureManager structureManager, ChunkGenerator generator,
StructureStart start, BoundingBox box, long seed) {
WorldgenRandom random = new WorldgenRandom(new XoroshiroRandomSource(seed));
int minChunkX = box.minX() >> 4;
int maxChunkX = box.maxX() >> 4;
int minChunkZ = box.minZ() >> 4;
int maxChunkZ = box.maxZ() >> 4;
for (int chunkX = minChunkX; chunkX <= maxChunkX; chunkX++) {
for (int chunkZ = minChunkZ; chunkZ <= maxChunkZ; chunkZ++) {
level.getChunk(chunkX, chunkZ);
ChunkPos chunkPos = new ChunkPos(chunkX, chunkZ);
BoundingBox chunkBox = new BoundingBox(
chunkPos.getMinBlockX(), box.minY(), chunkPos.getMinBlockZ(),
chunkPos.getMaxBlockX(), box.maxY(), chunkPos.getMaxBlockZ());
start.placeInChunk(level, structureManager, generator, random, chunkBox, chunkPos);
}
} }
Registry<T> registry = instance.registryAccess().lookupOrThrow(registryKey); }
for (Identifier identifier : registry.keySet()) {
keys.add(identifier.toString()); private <T> List<String> registryKeys(ResourceKey<Registry<T>> registryKey) {
List<String> keys = new ArrayList<>();
try {
MinecraftServer instance = server.get();
if (instance == null) {
return keys;
}
Registry<T> registry = instance.registryAccess().lookupOrThrow(registryKey);
for (Identifier identifier : registry.keySet()) {
keys.add(identifier.toString());
}
} catch (Throwable error) {
IrisLogging.reportError(error);
} }
return keys; return keys;
} }
@@ -19,26 +19,58 @@
package art.arcane.iris.modded; package art.arcane.iris.modded;
import art.arcane.iris.engine.object.TileData; import art.arcane.iris.engine.object.TileData;
import art.arcane.iris.spi.PlatformBlockState;
import art.arcane.volmlib.util.collection.KMap; import art.arcane.volmlib.util.collection.KMap;
import com.google.gson.Gson; import com.google.gson.Gson;
import com.google.gson.GsonBuilder; import com.google.gson.GsonBuilder;
import com.google.gson.Strictness; import com.google.gson.Strictness;
import net.minecraft.nbt.ByteTag;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.nbt.DoubleTag;
import net.minecraft.nbt.FloatTag;
import net.minecraft.nbt.IntTag;
import net.minecraft.nbt.ListTag;
import net.minecraft.nbt.LongTag;
import net.minecraft.nbt.NbtUtils;
import net.minecraft.nbt.ShortTag;
import net.minecraft.nbt.StringTag;
import net.minecraft.nbt.Tag;
import net.minecraft.core.registries.BuiltInRegistries;
import net.minecraft.resources.Identifier;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.util.ProblemReporter;
import net.minecraft.world.level.block.Block;
import net.minecraft.world.level.block.entity.BlockEntity;
import net.minecraft.world.level.block.entity.BannerBlockEntity;
import net.minecraft.world.level.block.entity.RandomizableContainerBlockEntity;
import net.minecraft.world.level.block.entity.SignBlockEntity;
import net.minecraft.world.level.block.entity.SpawnerBlockEntity;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.block.state.properties.Property;
import net.minecraft.world.level.storage.TagValueInput;
import java.io.ByteArrayOutputStream; import java.io.ByteArrayOutputStream;
import java.io.DataOutputStream; import java.io.DataOutputStream;
import java.io.IOException; import java.io.IOException;
import java.util.List;
import java.util.Map;
public final class ModdedTileData extends TileData { public final class ModdedTileData extends TileData {
public static final String NBT_PROPERTY = "nbt"; public static final String NBT_PROPERTY = "nbt";
static final String LEGACY_BANNER_COLOR_PROPERTY = "iris:legacy_banner_color";
private static final Gson GSON = new GsonBuilder().disableHtmlEscaping().setStrictness(Strictness.LENIENT).create(); private static final Gson GSON = new GsonBuilder().disableHtmlEscaping().setStrictness(Strictness.LENIENT).create();
private final byte[] raw; private final byte[] raw;
private final KMap<String, Object> tileProperties; private final KMap<String, Object> tileProperties;
private final String expectedBlockKey;
private final int legacyType;
ModdedTileData(byte[] raw, KMap<String, Object> tileProperties) { ModdedTileData(byte[] raw, KMap<String, Object> tileProperties, String expectedBlockKey, int legacyType) {
super(); super();
this.raw = raw; this.raw = raw;
this.tileProperties = tileProperties == null ? new KMap<>() : tileProperties; this.tileProperties = tileProperties == null ? new KMap<>() : tileProperties;
this.expectedBlockKey = expectedBlockKey;
this.legacyType = legacyType;
} }
public static ModdedTileData capture(String blockKey, String snbt) throws IOException { public static ModdedTileData capture(String blockKey, String snbt) throws IOException {
@@ -49,7 +81,26 @@ public final class ModdedTileData extends TileData {
out.writeUTF(blockKey); out.writeUTF(blockKey);
out.writeUTF(GSON.toJson(properties)); out.writeUTF(GSON.toJson(properties));
} }
return new ModdedTileData(bytes.toByteArray(), properties); return new ModdedTileData(bytes.toByteArray(), properties, normalizeBlockKey(blockKey), -1);
}
public static ModdedTileData fromProperties(PlatformBlockState state, KMap<String, Object> properties) {
String blockKey = state.placementBaseState().key();
int bracket = blockKey.indexOf('[');
if (bracket >= 0) {
blockKey = blockKey.substring(0, bracket);
}
KMap<String, Object> copied = properties == null ? new KMap<>() : properties.copy();
try {
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
try (DataOutputStream out = new DataOutputStream(bytes)) {
out.writeUTF(blockKey);
out.writeUTF(GSON.toJson(copied));
}
return new ModdedTileData(bytes.toByteArray(), copied, normalizeBlockKey(blockKey), -1);
} catch (IOException e) {
throw new IllegalStateException("Failed to encode modded tile data for " + blockKey, e);
}
} }
public String snbt() { public String snbt() {
@@ -57,6 +108,136 @@ public final class ModdedTileData extends TileData {
return value == null ? null : value.toString(); return value == null ? null : value.toString();
} }
public boolean apply(BlockEntity blockEntity, ServerLevel level) throws Exception {
CompoundTag payload = payload();
if (payload == null) {
return false;
}
CompoundTag merged = blockEntity.saveWithoutMetadata(level.registryAccess());
merged.merge(payload);
blockEntity.loadWithComponents(TagValueInput.create(ProblemReporter.DISCARDING, level.registryAccess(), merged));
blockEntity.setChanged();
level.sendBlockUpdated(blockEntity.getBlockPos(), blockEntity.getBlockState(), blockEntity.getBlockState(), Block.UPDATE_CLIENTS);
return true;
}
public boolean isApplicable(BlockState state, BlockEntity blockEntity) {
if (expectedBlockKey != null) {
return expectedBlockKey.equals(normalizeBlockKey(BuiltInRegistries.BLOCK.getKey(state.getBlock()).toString()));
}
return switch (legacyType) {
case 0 -> blockEntity instanceof SignBlockEntity;
case 1 -> blockEntity instanceof SpawnerBlockEntity;
case 2 -> blockEntity instanceof BannerBlockEntity;
case 3 -> blockEntity instanceof RandomizableContainerBlockEntity;
default -> false;
};
}
CompoundTag payload() throws Exception {
String snbt = snbt();
if (snbt != null && !snbt.isBlank()) {
return NbtUtils.snbtToStructure(snbt);
}
Tag converted = toTag(tileProperties);
return converted instanceof CompoundTag compound ? compound : null;
}
public BlockState adjustBlockState(BlockState state) {
Object colorValue = tileProperties.get(LEGACY_BANNER_COLOR_PROPERTY);
if (colorValue == null) {
return state;
}
Identifier currentId = BuiltInRegistries.BLOCK.getKey(state.getBlock());
String currentPath = currentId.getPath();
if (!currentPath.endsWith("_banner")) {
return state;
}
String suffix = currentPath.endsWith("_wall_banner") ? "_wall_banner" : "_banner";
Identifier targetId = Identifier.tryParse("minecraft:" + colorValue + suffix);
if (targetId == null || !BuiltInRegistries.BLOCK.containsKey(targetId)) {
return state;
}
BlockState adjusted = BuiltInRegistries.BLOCK.getValue(targetId).defaultBlockState();
for (Property<?> property : state.getProperties()) {
if (adjusted.hasProperty(property)) {
adjusted = copyProperty(adjusted, state, property);
}
}
return adjusted;
}
private static Tag toTag(Object value) {
if (value == null) {
return null;
}
if (value instanceof Map<?, ?> map) {
CompoundTag compound = new CompoundTag();
for (Map.Entry<?, ?> entry : map.entrySet()) {
if (LEGACY_BANNER_COLOR_PROPERTY.equals(String.valueOf(entry.getKey()))) {
continue;
}
Tag child = toTag(entry.getValue());
if (child != null) {
compound.put(String.valueOf(entry.getKey()), child);
}
}
return compound;
}
if (value instanceof List<?> values) {
ListTag list = new ListTag();
for (Object entry : values) {
Tag child = toTag(entry);
if (child != null) {
list.add(child);
}
}
return list;
}
if (value instanceof Boolean bool) {
return ByteTag.valueOf(bool);
}
if (value instanceof Byte number) {
return ByteTag.valueOf(number);
}
if (value instanceof Short number) {
return ShortTag.valueOf(number);
}
if (value instanceof Integer number) {
return IntTag.valueOf(number);
}
if (value instanceof Long number) {
return LongTag.valueOf(number);
}
if (value instanceof Float number) {
return FloatTag.valueOf(number);
}
if (value instanceof Number number) {
return DoubleTag.valueOf(number.doubleValue());
}
return StringTag.valueOf(String.valueOf(value));
}
private static <T extends Comparable<T>> BlockState copyProperty(BlockState target, BlockState source, Property<T> property) {
return target.setValue(property, source.getValue(property));
}
static String normalizeBlockKey(String blockKey) {
if (blockKey == null || blockKey.isBlank()) {
return null;
}
String normalized = blockKey.trim().toLowerCase(java.util.Locale.ROOT);
int bracket = normalized.indexOf('[');
if (bracket >= 0) {
normalized = normalized.substring(0, bracket);
}
if (!normalized.contains(":")) {
normalized = "minecraft:" + normalized.replaceAll("\\s+", "_");
}
Identifier identifier = Identifier.tryParse(normalized);
return identifier == null ? null : identifier.toString();
}
@Override @Override
public KMap<String, Object> getProperties() { public KMap<String, Object> getProperties() {
return tileProperties; return tileProperties;
@@ -28,18 +28,68 @@ import net.minecraft.core.registries.BuiltInRegistries;
import net.minecraft.core.registries.Registries; import net.minecraft.core.registries.Registries;
import net.minecraft.resources.Identifier; import net.minecraft.resources.Identifier;
import net.minecraft.server.MinecraftServer; import net.minecraft.server.MinecraftServer;
import net.minecraft.world.item.DyeColor;
import net.minecraft.world.level.block.entity.BannerPattern; import net.minecraft.world.level.block.entity.BannerPattern;
import java.io.DataInputStream; import java.io.DataInputStream;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.List;
import java.util.Locale; import java.util.Locale;
import java.util.function.Supplier; import java.util.function.Supplier;
public final class ModdedTileReader implements TileData.TileReader { public final class ModdedTileReader implements TileData.TileReader {
private static final Gson GSON = new GsonBuilder().disableHtmlEscaping().setStrictness(Strictness.LENIENT).create(); private static final Gson GSON = new GsonBuilder().disableHtmlEscaping().setStrictness(Strictness.LENIENT).create();
private static final int DYE_COLOR_COUNT = 16; private static final int DYE_COLOR_COUNT = 16;
private static final Identifier DEFAULT_SPAWNER_ENTITY = Identifier.parse("minecraft:pig");
private static final Identifier DEFAULT_BANNER_PATTERN = Identifier.parse("minecraft:base");
private static final List<Identifier> LEGACY_BUKKIT_ENTITY_TYPES = createLegacyBukkitEntityTypes();
private static final List<Identifier> PAPER_26_2_BANNER_PATTERNS = List.of(
Identifier.parse("minecraft:small_stripes"),
Identifier.parse("minecraft:stripe_right"),
Identifier.parse("minecraft:diagonal_left"),
Identifier.parse("minecraft:stripe_middle"),
Identifier.parse("minecraft:square_bottom_right"),
Identifier.parse("minecraft:half_horizontal"),
Identifier.parse("minecraft:skull"),
Identifier.parse("minecraft:flow"),
Identifier.parse("minecraft:rhombus"),
Identifier.parse("minecraft:border"),
Identifier.parse("minecraft:gradient_up"),
Identifier.parse("minecraft:guster"),
Identifier.parse("minecraft:square_top_left"),
Identifier.parse("minecraft:triangle_bottom"),
Identifier.parse("minecraft:triangles_bottom"),
Identifier.parse("minecraft:half_horizontal_bottom"),
Identifier.parse("minecraft:gradient"),
Identifier.parse("minecraft:triangle_top"),
Identifier.parse("minecraft:piglin"),
Identifier.parse("minecraft:stripe_center"),
Identifier.parse("minecraft:circle"),
Identifier.parse("minecraft:stripe_left"),
Identifier.parse("minecraft:stripe_bottom"),
Identifier.parse("minecraft:square_top_right"),
Identifier.parse("minecraft:curly_border"),
Identifier.parse("minecraft:creeper"),
Identifier.parse("minecraft:square_bottom_left"),
Identifier.parse("minecraft:triangles_top"),
Identifier.parse("minecraft:half_vertical"),
Identifier.parse("minecraft:mojang"),
Identifier.parse("minecraft:diagonal_right"),
Identifier.parse("minecraft:cross"),
Identifier.parse("minecraft:straight_cross"),
Identifier.parse("minecraft:bricks"),
Identifier.parse("minecraft:diagonal_up_left"),
Identifier.parse("minecraft:base"),
Identifier.parse("minecraft:flower"),
Identifier.parse("minecraft:stripe_downleft"),
Identifier.parse("minecraft:diagonal_up_right"),
Identifier.parse("minecraft:stripe_downright"),
Identifier.parse("minecraft:stripe_top"),
Identifier.parse("minecraft:globe"),
Identifier.parse("minecraft:half_vertical_right"));
private final Supplier<MinecraftServer> server; private final Supplier<MinecraftServer> server;
@@ -135,7 +185,8 @@ public final class ModdedTileReader implements TileData.TileReader {
if (properties == null) { if (properties == null) {
throw new NullPointerException("properties is marked non-null but is null"); throw new NullPointerException("properties is marked non-null but is null");
} }
return new ModdedTileData(replay.consumed(), properties); return new ModdedTileData(replay.consumed(), properties,
ModdedTileData.normalizeBlockKey(materialKey), -1);
} catch (Throwable e) { } catch (Throwable e) {
replay.rewind(); replay.rewind();
return parseLegacy(din, replay); return parseLegacy(din, replay);
@@ -149,102 +200,162 @@ public final class ModdedTileReader implements TileData.TileReader {
private TileData parseLegacy(DataInputStream din, ReplayInputStream replay) throws IOException { private TileData parseLegacy(DataInputStream din, ReplayInputStream replay) throws IOException {
int id = din.readShort(); int id = din.readShort();
switch (id) { String expectedBlockKey = null;
KMap<String, Object> properties = switch (id) {
case 0 -> readSign(din); case 0 -> readSign(din);
case 1 -> readSpawner(din, replay); case 1 -> readSpawner(din, replay);
case 2 -> readBanner(din, replay); case 2 -> readBanner(din, replay);
case 3 -> readLootable(din); case 3 -> {
expectedBlockKey = ModdedTileData.normalizeBlockKey(din.readUTF());
yield readLootable(din);
}
default -> throw new IOException("Unknown tile type: " + id); default -> throw new IOException("Unknown tile type: " + id);
} };
return new ModdedTileData(replay.consumed(), new KMap<>()); return new ModdedTileData(replay.consumed(), properties, expectedBlockKey, id);
} }
private static void readSign(DataInputStream din) throws IOException { private static KMap<String, Object> readSign(DataInputStream din) throws IOException {
din.readUTF(); List<String> messages = List.of(din.readUTF(), din.readUTF(), din.readUTF(), din.readUTF());
din.readUTF();
din.readUTF();
din.readUTF();
byte dye = din.readByte(); byte dye = din.readByte();
if (dye < 0 || dye >= DYE_COLOR_COUNT) { if (dye < 0 || dye >= DYE_COLOR_COUNT) {
throw new ArrayIndexOutOfBoundsException("Index " + dye + " out of bounds for length " + DYE_COLOR_COUNT); throw new ArrayIndexOutOfBoundsException("Index " + dye + " out of bounds for length " + DYE_COLOR_COUNT);
} }
KMap<String, Object> text = new KMap<>();
text.put("messages", messages);
text.put("color", DyeColor.byId(dye).getName());
text.put("has_glowing_text", false);
KMap<String, Object> properties = new KMap<>();
properties.put("front_text", text);
properties.put("back_text", text.copy());
return properties;
} }
private static void readSpawner(DataInputStream din, ReplayInputStream replay) throws IOException { private static KMap<String, Object> readSpawner(DataInputStream din, ReplayInputStream replay) throws IOException {
boolean resolved = false; Identifier entityId = null;
replay.mark(Integer.MAX_VALUE); replay.mark(Integer.MAX_VALUE);
try { try {
String keyString = din.readUTF(); String keyString = din.readUTF();
Identifier key = Identifier.tryParse(keyString); Identifier key = Identifier.tryParse(keyString);
resolved = key != null && BuiltInRegistries.ENTITY_TYPE.containsKey(key); if (key != null && BuiltInRegistries.ENTITY_TYPE.containsKey(key)) {
if (!resolved) { entityId = key;
} else {
replay.reset(); replay.reset();
} }
} catch (Throwable ignored) { } catch (Throwable ignored) {
replay.reset(); replay.reset();
} }
if (!resolved) { if (entityId == null) {
din.readShort(); entityId = legacySpawnerEntityId(din.readShort());
} }
KMap<String, Object> entity = new KMap<>();
entity.put("id", entityId.toString());
KMap<String, Object> spawnData = new KMap<>();
spawnData.put("entity", entity);
KMap<String, Object> properties = new KMap<>();
properties.put("SpawnData", spawnData);
return properties;
} }
private void readBanner(DataInputStream din, ReplayInputStream replay) throws IOException { static Identifier legacySpawnerEntityId(int legacyOrdinal) {
din.readUnsignedByte(); if (legacyOrdinal < 0 || legacyOrdinal >= LEGACY_BUKKIT_ENTITY_TYPES.size()) {
return DEFAULT_SPAWNER_ENTITY;
}
return LEGACY_BUKKIT_ENTITY_TYPES.get(legacyOrdinal);
}
private static List<Identifier> createLegacyBukkitEntityTypes() {
List<Identifier> entityTypes = new ArrayList<>();
for (Identifier key : BuiltInRegistries.ENTITY_TYPE.keySet()) {
if (Identifier.DEFAULT_NAMESPACE.equals(key.getNamespace())) {
entityTypes.add(key);
}
}
entityTypes.sort(Identifier::compareTo);
return List.copyOf(entityTypes);
}
private KMap<String, Object> readBanner(DataInputStream din, ReplayInputStream replay) throws IOException {
int baseColor = din.readUnsignedByte();
int listSize = din.readUnsignedByte(); int listSize = din.readUnsignedByte();
replay.mark(Integer.MAX_VALUE); replay.mark(Integer.MAX_VALUE);
boolean parsedKeyed = false; List<Object> layers = new ArrayList<>(listSize);
try { try {
for (int i = 0; i < listSize; i++) { for (int i = 0; i < listSize; i++) {
din.readUnsignedByte(); int color = din.readUnsignedByte();
Identifier patternKey = Identifier.tryParse(din.readUTF()); Identifier patternKey = Identifier.tryParse(din.readUTF());
if (patternKey == null || !bannerPatternExists(patternKey)) { if (patternKey == null || !bannerPatternExists(patternKey)) {
throw new IOException("Unknown banner pattern key"); throw new IOException("Unknown banner pattern key");
} }
layers.add(bannerLayer(patternKey, color));
} }
parsedKeyed = true;
} catch (Throwable ignored) { } catch (Throwable ignored) {
replay.reset(); replay.reset();
layers.clear();
} }
if (parsedKeyed) { if (layers.isEmpty() && listSize > 0) {
return; for (int i = 0; i < listSize; i++) {
int color = din.readUnsignedByte();
int pattern = din.readUnsignedByte();
layers.add(bannerLayer(legacyBannerPatternKey(pattern), color));
}
} }
KMap<String, Object> properties = new KMap<>();
properties.put("patterns", layers);
properties.put(ModdedTileData.LEGACY_BANNER_COLOR_PROPERTY, DyeColor.byId(baseColor).getName());
return properties;
}
for (int i = 0; i < listSize; i++) { private static KMap<String, Object> bannerLayer(Identifier pattern, int color) {
din.readUnsignedByte(); KMap<String, Object> layer = new KMap<>();
din.readUnsignedByte(); layer.put("pattern", pattern.toString());
layer.put("color", DyeColor.byId(color).getName());
return layer;
}
static Identifier legacyBannerPatternKey(int legacyOrdinal) {
if (legacyOrdinal < 0 || legacyOrdinal >= PAPER_26_2_BANNER_PATTERNS.size()) {
return DEFAULT_BANNER_PATTERN;
} }
return PAPER_26_2_BANNER_PATTERNS.get(legacyOrdinal);
} }
private boolean bannerPatternExists(Identifier key) { private boolean bannerPatternExists(Identifier key) {
MinecraftServer instance = server.get(); MinecraftServer instance = server.get();
if (instance == null) { if (instance == null) {
return false; return true;
} }
Registry<BannerPattern> registry = instance.registryAccess().lookupOrThrow(Registries.BANNER_PATTERN); Registry<BannerPattern> registry = instance.registryAccess().lookupOrThrow(Registries.BANNER_PATTERN);
return registry.containsKey(key); return registry.containsKey(key);
} }
private static void readLootable(DataInputStream din) throws IOException { private static KMap<String, Object> readLootable(DataInputStream din) throws IOException {
din.readUTF(); String lootTable = din.readUTF();
din.readUTF(); long seed = din.readLong();
din.readLong(); KMap<String, Object> properties = new KMap<>();
if (!lootTable.isBlank()) {
properties.put("LootTable", lootTable);
properties.put("LootTableSeed", seed);
}
return properties;
} }
private static boolean matchMaterial(String name) { private static boolean matchMaterial(String name) {
String filtered = name; String filtered = name.trim().toLowerCase(Locale.ROOT);
if (filtered.startsWith("minecraft:")) { int bracket = filtered.indexOf('[');
filtered = filtered.substring("minecraft:".length()); if (bracket >= 0) {
filtered = filtered.substring(0, bracket);
} }
filtered = filtered.toUpperCase(Locale.ROOT); if (!filtered.contains(":")) {
filtered = filtered.replaceAll("\\s+", "_").replaceAll("\\W", ""); filtered = "minecraft:" + filtered.replaceAll("\\s+", "_");
Identifier identifier = Identifier.tryParse("minecraft:" + filtered.toLowerCase(Locale.ROOT)); }
Identifier identifier = Identifier.tryParse(filtered);
if (identifier == null) { if (identifier == null) {
return false; return false;
} }
return BuiltInRegistries.ITEM.containsKey(identifier) || BuiltInRegistries.BLOCK.containsKey(identifier); return BuiltInRegistries.BLOCK.containsKey(identifier);
} }
} }
File diff suppressed because it is too large. Load diff
@@ -23,6 +23,7 @@ import art.arcane.iris.engine.IrisEngine;
import art.arcane.iris.engine.framework.Engine; import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.framework.EngineTarget; import art.arcane.iris.engine.framework.EngineTarget;
import art.arcane.iris.engine.object.IrisDimension; 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.engine.object.IrisWorld;
import net.minecraft.server.level.ServerLevel; import net.minecraft.server.level.ServerLevel;
import net.minecraft.world.level.dimension.DimensionType; import net.minecraft.world.level.dimension.DimensionType;
@@ -82,6 +83,7 @@ public final class ModdedWorldEngines {
} }
long seed = seedOverride == Long.MIN_VALUE ? level.getSeed() : seedOverride; long seed = seedOverride == Long.MIN_VALUE ? level.getSeed() : seedOverride;
validateDimensionContract(dimension, level);
File worldFolder = DimensionType.getStorageFolder(level.dimension(), level.getServer().getWorldPath(LevelResource.ROOT)).toFile(); File worldFolder = DimensionType.getStorageFolder(level.dimension(), level.getServer().getWorldPath(LevelResource.ROOT)).toFile();
IrisWorld world = IrisWorld.builder() IrisWorld world = IrisWorld.builder()
.platformIdentity(level.dimension().identifier().toString()) .platformIdentity(level.dimension().identifier().toString())
@@ -90,21 +92,31 @@ public final class ModdedWorldEngines {
.worldFolder(worldFolder) .worldFolder(worldFolder)
.minHeight(dimension.getMinHeight()) .minHeight(dimension.getMinHeight())
.maxHeight(dimension.getMaxHeight()) .maxHeight(dimension.getMaxHeight())
.platformWorld(new ModdedPlatformWorld(level))
.build(); .build();
Engine engine = new IrisEngine(new EngineTarget(world, dimension, data), false); Engine engine = new IrisEngine(new EngineTarget(world, dimension, data), false);
int levelMinY = level.getMinY();
int levelMaxY = level.getMinY() + level.getHeight();
if (dimension.getMinHeight() < levelMinY || dimension.getMaxHeight() > levelMaxY) {
LOGGER.warn("Iris pack height range for {} exceeds the level: pack generates {}..{} but the level is {}..{}. Terrain outside the level range will be clipped; restart so the forced datapack registers the pack dimension type.",
level.dimension().identifier(), dimension.getMinHeight(), dimension.getMaxHeight(), levelMinY, levelMaxY);
}
LOGGER.info("Iris engine up for {}: pack={} dim={} seed={} height={}..{}", LOGGER.info("Iris engine up for {}: pack={} dim={} seed={} height={}..{}",
level.dimension().identifier(), packDir.getAbsolutePath(), dimension.getLoadKey(), seed, dimension.getMinHeight(), dimension.getMaxHeight()); level.dimension().identifier(), packDir.getAbsolutePath(), dimension.getLoadKey(), seed, dimension.getMinHeight(), dimension.getMaxHeight());
return engine; return engine;
} }
private static void validateDimensionContract(IrisDimension dimension, ServerLevel level) {
DimensionType actualType = level.dimensionType();
String actualTypeKey = level.dimensionTypeRegistration().unwrapKey()
.map(key -> key.identifier().toString())
.orElse("<unregistered>");
IrisDimensionRuntimeContract expected = IrisDimensionRuntimeContract.expected(dimension, "irisworldgen");
IrisDimensionRuntimeContract actual = new IrisDimensionRuntimeContract(
actualTypeKey,
actualType.minY(),
actualType.height(),
actualType.logicalHeight());
String runtimeName = "Modded level '" + level.dimension().identifier() + "'";
expected.requireExact(runtimeName, actual);
expected.requireHeight(runtimeName, level.getMinY(), level.getHeight());
}
public static File packFolder(String pack) { public static File packFolder(String pack) {
return ModdedEngineBootstrap.loader().configDir() return ModdedEngineBootstrap.loader().configDir()
.resolve("irisworldgen") .resolve("irisworldgen")
@@ -113,7 +125,7 @@ public final class ModdedWorldEngines {
.toFile(); .toFile();
} }
private static File resolvePack(String pack, String dimensionKey) { static File resolvePack(String pack, String dimensionKey) {
File packDir = packFolder(pack); File packDir = packFolder(pack);
if (packDir.isDirectory()) { if (packDir.isDirectory()) {
return packDir; return packDir;
@@ -30,7 +30,6 @@ import art.arcane.iris.engine.object.IrisEntitySpawn;
import art.arcane.iris.engine.object.IrisMarker; import art.arcane.iris.engine.object.IrisMarker;
import art.arcane.iris.engine.object.IrisPosition; import art.arcane.iris.engine.object.IrisPosition;
import art.arcane.iris.engine.object.IrisRange; import art.arcane.iris.engine.object.IrisRange;
import art.arcane.iris.engine.object.IrisRate;
import art.arcane.iris.engine.object.IrisRegion; import art.arcane.iris.engine.object.IrisRegion;
import art.arcane.iris.engine.object.IrisSpawnGroup; import art.arcane.iris.engine.object.IrisSpawnGroup;
import art.arcane.iris.engine.object.IrisSpawner; import art.arcane.iris.engine.object.IrisSpawner;
@@ -43,18 +42,16 @@ import art.arcane.volmlib.util.mantle.runtime.MantleChunk;
import art.arcane.volmlib.util.math.RNG; import art.arcane.volmlib.util.math.RNG;
import art.arcane.volmlib.util.matter.Matter; import art.arcane.volmlib.util.matter.Matter;
import art.arcane.volmlib.util.matter.MatterMarker; import art.arcane.volmlib.util.matter.MatterMarker;
import art.arcane.volmlib.util.matter.slices.MarkerMatter;
import it.unimi.dsi.fastutil.longs.LongArrayList;
import net.minecraft.core.BlockPos; import net.minecraft.core.BlockPos;
import net.minecraft.server.level.ServerLevel; import net.minecraft.server.level.ServerLevel;
import net.minecraft.server.level.ServerPlayer; import net.minecraft.server.level.ServerPlayer;
import net.minecraft.world.entity.Entity; import net.minecraft.world.entity.Entity;
import net.minecraft.world.entity.Mob; import net.minecraft.world.entity.LivingEntity;
import net.minecraft.world.level.block.Blocks; import net.minecraft.world.level.block.Blocks;
import net.minecraft.world.level.block.state.BlockState; import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.phys.AABB; import net.minecraft.world.phys.AABB;
import org.bukkit.Chunk;
import org.bukkit.event.block.BlockBreakEvent;
import org.bukkit.event.block.BlockPlaceEvent;
import org.bukkit.event.player.PlayerTeleportEvent;
import java.util.HashSet; import java.util.HashSet;
import java.util.Set; import java.util.Set;
@@ -66,15 +63,13 @@ import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
public final class ModdedWorldManager implements EngineWorldManager { public final class ModdedWorldManager implements EngineWorldManager {
static final MantleFlag INITIAL_SPAWN_COMPLETION_FLAG = MantleFlag.INITIAL_SPAWNED_MARKER;
private static final int MAX_INITIAL_QUEUE = 8192; private static final int MAX_INITIAL_QUEUE = 8192;
private static final int MAX_INITIAL_DRAIN_PER_TICK = 8; private static final int MAX_INITIAL_DRAIN_PER_TICK = 8;
private static final int MAX_INITIAL_RECOVERY_PER_PASS = 128; private static final int MAX_INITIAL_RECOVERY_PER_PASS = 128;
private static final int MANTLE_WARMUP_QUEUE_CAPACITY = 256; private static final int MANTLE_WARMUP_QUEUE_CAPACITY = 256;
private static final long INITIAL_RECOVERY_INTERVAL_MS = 1_000L; private static final long INITIAL_RECOVERY_INTERVAL_MS = 1_000L;
private static final long COUNT_INTERVAL_MS = 3_000L; private static final long COUNT_INTERVAL_MS = 3_000L;
private static final int ENTITY_SCAN_RADIUS = 64;
private static final int PLAYER_CHUNK_RADIUS = 4;
private static final int MIN_TICK_INTERVAL_MS = 1_000;
private final Engine engine; private final Engine engine;
private final InitialSpawnQueue initialSpawnQueue; private final InitialSpawnQueue initialSpawnQueue;
@@ -123,6 +118,9 @@ public final class ModdedWorldManager implements EngineWorldManager {
if (closed || engine.isClosed() || engine.getMantle().getMantle().isClosed()) { if (closed || engine.isClosed() || engine.getMantle().getMantle().isClosed()) {
return; return;
} }
if (!isEntitySpawningEnabledForCurrentWorld()) {
return;
}
if (isPregenActive()) { if (isPregenActive()) {
return; return;
} }
@@ -132,7 +130,7 @@ public final class ModdedWorldManager implements EngineWorldManager {
} }
private void recoverLoadedInitialSpawns(ServerLevel level) { private void recoverLoadedInitialSpawns(ServerLevel level) {
if (!markerSystemEnabled() && !ambientSystemEnabled()) { if (!markerSystemEnabled()) {
return; return;
} }
long now = System.currentTimeMillis(); long now = System.currentTimeMillis();
@@ -161,7 +159,7 @@ public final class ModdedWorldManager implements EngineWorldManager {
if (level.getChunkSource().getChunkNow(chunkX, chunkZ) == null) { if (level.getChunkSource().getChunkNow(chunkX, chunkZ) == null) {
continue; continue;
} }
if (mantle.isChunkLoaded(chunkX, chunkZ) && mantle.hasFlag(chunkX, chunkZ, MantleFlag.INITIAL_SPAWNED)) { if (mantle.isChunkLoaded(chunkX, chunkZ) && mantle.hasFlag(chunkX, chunkZ, INITIAL_SPAWN_COMPLETION_FLAG)) {
continue; continue;
} }
if (initialSpawnQueue.offer(key) && ++recovered >= MAX_INITIAL_RECOVERY_PER_PASS) { if (initialSpawnQueue.offer(key) && ++recovered >= MAX_INITIAL_RECOVERY_PER_PASS) {
@@ -174,7 +172,7 @@ public final class ModdedWorldManager implements EngineWorldManager {
if (initialSpawnQueue.isEmpty()) { if (initialSpawnQueue.isEmpty()) {
return; return;
} }
if (!markerSystemEnabled() && !ambientSystemEnabled()) { if (!markerSystemEnabled()) {
initialSpawnQueue.clear(); initialSpawnQueue.clear();
return; return;
} }
@@ -211,23 +209,22 @@ public final class ModdedWorldManager implements EngineWorldManager {
} }
private boolean initialSpawnChunk(ServerLevel level, int chunkX, int chunkZ) { private boolean initialSpawnChunk(ServerLevel level, int chunkX, int chunkZ) {
if (!ModdedEntitySpawner.chunksSafe(level, chunkX, chunkZ)) {
return false;
}
Mantle<Matter> mantle = engine.getMantle().getMantle(); Mantle<Matter> mantle = engine.getMantle().getMantle();
if (!mantle.isChunkLoaded(chunkX, chunkZ)) { if (!mantle.isChunkLoaded(chunkX, chunkZ)) {
return false; return false;
} }
if (mantle.hasFlag(chunkX, chunkZ, MantleFlag.INITIAL_SPAWNED)) { if (mantle.hasFlag(chunkX, chunkZ, INITIAL_SPAWN_COMPLETION_FLAG)) {
return true; return true;
} }
MantleChunk<Matter> chunk = mantle.getChunk(chunkX, chunkZ).use(); MantleChunk<Matter> chunk = mantle.getChunk(chunkX, chunkZ).use();
try { try {
chunk.raiseFlagUnchecked(MantleFlag.INITIAL_SPAWNED, () -> { chunk.raiseFlagUnchecked(INITIAL_SPAWN_COMPLETION_FLAG, () -> {
if (markerSystemEnabled()) { spawnMarkerSpawners(level, chunkX, chunkZ, chunk, true);
spawnMarkerSpawners(level, chunkX, chunkZ, chunk, true); scheduleInitialFollowUp(level, chunkX, chunkZ);
}
if (ambientSystemEnabled()) {
spawnAmbient(level, chunkX, chunkZ, true);
}
}); });
} finally { } finally {
chunk.release(); chunk.release();
@@ -235,6 +232,40 @@ public final class ModdedWorldManager implements EngineWorldManager {
return true; return true;
} }
private void scheduleInitialFollowUp(ServerLevel level, int chunkX, int chunkZ) {
ModdedScheduler scheduler = ModdedEngineBootstrap.schedulerOrNull();
if (scheduler == null) {
IrisLogging.error("Iris could not schedule the initial entity-spawn follow-up because the modded scheduler is unavailable.");
return;
}
scheduler.laterGlobal(() -> runInitialFollowUp(level, chunkX, chunkZ), RNG.r.i(5, 200));
}
private void runInitialFollowUp(ServerLevel level, int chunkX, int chunkZ) {
Mantle<Matter> mantle = engine.getMantle().getMantle();
if (closed || engine.isClosed() || mantle.isClosed() || !isEntitySpawningEnabledForCurrentWorld()) {
return;
}
if (level.getChunkSource().getChunkNow(chunkX, chunkZ) == null || !mantle.isChunkLoaded(chunkX, chunkZ)) {
return;
}
if (engine.getComplex() == null) {
return;
}
MantleChunk<Matter> chunk = mantle.getChunk(chunkX, chunkZ).use();
try {
if (markerSystemEnabled()) {
spawnMarkerSpawners(level, chunkX, chunkZ, chunk, false);
}
if (ambientSystemEnabled()) {
spawnAmbient(level, chunkX, chunkZ, true);
}
} finally {
chunk.release();
}
}
private void warmupMantleChunkAsync(long key, int chunkX, int chunkZ) { private void warmupMantleChunkAsync(long key, int chunkX, int chunkZ) {
if (closed || !mantleWarmups.add(key)) { if (closed || !mantleWarmups.add(key)) {
return; return;
@@ -263,25 +294,25 @@ public final class ModdedWorldManager implements EngineWorldManager {
private void ambientTick(ServerLevel level) { private void ambientTick(ServerLevel level) {
long now = System.currentTimeMillis(); long now = System.currentTimeMillis();
long interval = Math.max((long) MIN_TICK_INTERVAL_MS, IrisSettings.get().getWorld().getAsyncTickIntervalMS()); long interval = IrisSettings.get().getWorld().getAsyncTickIntervalMS();
if (now - lastAmbientAt < interval) { if (now - lastAmbientAt < interval) {
return; return;
} }
lastAmbientAt = now; lastAmbientAt = now;
if (level.players().isEmpty()) {
return;
}
if (!markerSystemEnabled() && !ambientSystemEnabled()) { if (!markerSystemEnabled() && !ambientSystemEnabled()) {
return; return;
} }
if (level.players().isEmpty()) {
return;
}
refreshEntityCount(level, now); long[] candidates = loadedChunkPositionsSnapshot(level);
refreshEntityCount(level, now, candidates.length);
if (cachedSaturation > IrisSettings.get().getWorld().getTargetSpawnEntitiesPerChunk()) { if (cachedSaturation > IrisSettings.get().getWorld().getTargetSpawnEntitiesPerChunk()) {
return; return;
} }
long[] candidates = loadedChunksNearPlayers(level);
if (candidates.length == 0) { if (candidates.length == 0) {
return; return;
} }
@@ -298,6 +329,9 @@ public final class ModdedWorldManager implements EngineWorldManager {
} }
private void ambientSpawnChunk(ServerLevel level, int chunkX, int chunkZ) { private void ambientSpawnChunk(ServerLevel level, int chunkX, int chunkZ) {
if (!ModdedEntitySpawner.chunksSafe(level, chunkX, chunkZ)) {
return;
}
Mantle<Matter> mantle = engine.getMantle().getMantle(); Mantle<Matter> mantle = engine.getMantle().getMantle();
if (!mantle.isChunkLoaded(chunkX, chunkZ)) { if (!mantle.isChunkLoaded(chunkX, chunkZ)) {
return; return;
@@ -318,6 +352,7 @@ public final class ModdedWorldManager implements EngineWorldManager {
private void spawnMarkerSpawners(ServerLevel level, int chunkX, int chunkZ, MantleChunk<Matter> chunk, boolean initial) { private void spawnMarkerSpawners(ServerLevel level, int chunkX, int chunkZ, MantleChunk<Matter> chunk, boolean initial) {
int minHeight = engine.getWorld().minHeight(); int minHeight = engine.getWorld().minHeight();
KList<IrisPosition> obstructed = new KList<>();
chunk.iterate(MatterMarker.class, (Integer x, Integer yf, Integer z, MatterMarker marker) -> { chunk.iterate(MatterMarker.class, (Integer x, Integer yf, Integer z, MatterMarker marker) -> {
String tag = marker.getTag(); String tag = marker.getTag();
if (tag.equals("cave_floor") || tag.equals("cave_ceiling")) { if (tag.equals("cave_floor") || tag.equals("cave_ceiling")) {
@@ -333,6 +368,7 @@ public final class ModdedWorldManager implements EngineWorldManager {
int worldY = yf + minHeight; int worldY = yf + minHeight;
if (resolved.isEmptyAbove() && aboveObstructed(level, worldX, worldY, worldZ)) { if (resolved.isEmptyAbove() && aboveObstructed(level, worldX, worldY, worldZ)) {
obstructed.add(new IrisPosition(worldX, yf, worldZ));
return; return;
} }
@@ -346,6 +382,10 @@ public final class ModdedWorldManager implements EngineWorldManager {
} }
spawnFromSpawner(level, new IrisPosition(worldX, worldY, worldZ), chosen, initial); spawnFromSpawner(level, new IrisPosition(worldX, worldY, worldZ), chosen, initial);
}); });
Mantle<Matter> mantle = engine.getMantle().getMantle();
for (IrisPosition position : obstructed) {
mantle.remove(position.getX(), position.getY(), position.getZ(), MatterMarker.class);
}
} }
private KList<IrisSpawner> resolveMarkerSpawners(IrisMarker marker) { private KList<IrisSpawner> resolveMarkerSpawners(IrisMarker marker) {
@@ -397,7 +437,7 @@ public final class ModdedWorldManager implements EngineWorldManager {
int blockZ = chunkZ << 4; int blockZ = chunkZ << 4;
IrisBiome biome = engine.getSurfaceBiome(blockX, blockZ); IrisBiome biome = engine.getSurfaceBiome(blockX, blockZ);
IrisRegion region = engine.getRegion(blockX, blockZ); IrisRegion region = engine.getRegion(blockX, blockZ);
int chunkMobs = countChunkMobs(level, chunkX, chunkZ); int chunkMobs = countChunkLivingEntities(level, chunkX, chunkZ);
KList<IrisEntitySpawn> pool = new KList<>(); KList<IrisEntitySpawn> pool = new KList<>();
collectSpawns(pool, engine.getData().getSpawnerLoader().loadAll(engine.getDimension().getEntitySpawners()), biome, chunkX, chunkZ, chunkMobs, initial); collectSpawns(pool, engine.getData().getSpawnerLoader().loadAll(engine.getDimension().getEntitySpawners()), biome, chunkX, chunkZ, chunkMobs, initial);
@@ -457,19 +497,33 @@ public final class ModdedWorldManager implements EngineWorldManager {
return 0; return 0;
} }
RNG entityRng = new RNG(engine.getSeedManager().getEntity()); RNG entityRng = entry.getRng().aquire(() -> new RNG(engine.getSeedManager().getEntity()));
IrisSpawnGroup group = spawner.getGroup(); IrisSpawnGroup group = spawner.getGroup();
KList<IrisPosition> caveFloors = group == IrisSpawnGroup.CAVE
? engine.getMantle().findMarkers(chunkX, chunkZ, MarkerMatter.CAVE_FLOOR)
: new KList<>();
int spawned = 0; int spawned = 0;
for (int i = 0; i < count; i++) { for (int i = 0; i < count; i++) {
int worldX = (chunkX << 4) + RNG.r.i(15); int worldX;
int worldZ = (chunkZ << 4) + RNG.r.i(15); int worldY;
int surfaceY = level.getMinY() + engine.getHeight(worldX, worldZ, false); int worldZ;
int solidY = level.getMinY() + engine.getHeight(worldX, worldZ, true); if (group == IrisSpawnGroup.CAVE) {
int worldY = switch (group) { if (caveFloors.isEmpty()) {
case NORMAL -> surfaceY + 1; continue;
case CAVE -> solidY + 1; }
case UNDERWATER, BEACH -> surfaceY > solidY + 1 ? RNG.r.i(solidY + 1, surfaceY) : surfaceY; IrisPosition caveFloor = caveFloors.getRandom(RNG.r);
}; worldX = caveFloor.getX();
worldY = caveFloor.getY() + 1;
worldZ = caveFloor.getZ();
} else {
worldX = (chunkX << 4) + RNG.r.i(15);
worldZ = (chunkZ << 4) + RNG.r.i(15);
int surfaceY = level.getMinY() + engine.getHeight(worldX, worldZ, false);
int solidY = level.getMinY() + engine.getHeight(worldX, worldZ, true);
worldY = group == IrisSpawnGroup.NORMAL
? surfaceY + 1
: RNG.r.i(solidY + 1, surfaceY);
}
if (worldY <= level.getMinY() || worldY >= level.getMaxY()) { if (worldY <= level.getMinY() || worldY >= level.getMaxY()) {
continue; continue;
} }
@@ -479,7 +533,7 @@ public final class ModdedWorldManager implements EngineWorldManager {
if (!surfaceMatches(irisEntity.getSurface(), level, worldX, worldY, worldZ)) { if (!surfaceMatches(irisEntity.getSurface(), level, worldX, worldY, worldZ)) {
continue; continue;
} }
if (!clearForSpawn(level, worldX, worldY, worldZ)) { if (!ModdedEntitySpawner.isAreaClearForSpawn(level, irisEntity, worldX, worldY, worldZ)) {
continue; continue;
} }
if (ModdedEntitySpawner.spawn(engine, irisEntity, level, worldX, worldY, worldZ, entityRng) != null) { if (ModdedEntitySpawner.spawn(engine, irisEntity, level, worldX, worldY, worldZ, entityRng) != null) {
@@ -504,7 +558,7 @@ public final class ModdedWorldManager implements EngineWorldManager {
exhaustMarker(spawner, position); exhaustMarker(spawner, position);
RNG entityRng = new RNG(engine.getSeedManager().getEntity()); RNG entityRng = entry.getRng().aquire(() -> new RNG(engine.getSeedManager().getEntity()));
int worldX = position.getX(); int worldX = position.getX();
int worldY = position.getY() + 1; int worldY = position.getY() + 1;
int worldZ = position.getZ(); int worldZ = position.getZ();
@@ -529,12 +583,7 @@ public final class ModdedWorldManager implements EngineWorldManager {
} }
private boolean canSpawn(IrisSpawner spawner, int chunkX, int chunkZ) { private boolean canSpawn(IrisSpawner spawner, int chunkX, int chunkZ) {
IrisRate rate = spawner.getMaximumRate(); return spawner.canSpawn(engine, chunkX, chunkZ);
if (!rate.isInfinite() && !engine.getEngineData().getCooldown(spawner).canSpawn(rate)) {
return false;
}
IrisRate chunkRate = spawner.getMaximumRatePerChunk();
return chunkRate.isInfinite() || engine.getEngineData().getChunk(chunkX, chunkZ).getCooldown(spawner).canSpawn(chunkRate);
} }
private boolean lightAllowed(IrisSpawner spawner, ServerLevel level, int worldX, int worldY, int worldZ) { private boolean lightAllowed(IrisSpawner spawner, ServerLevel level, int worldX, int worldY, int worldZ) {
@@ -575,68 +624,51 @@ public final class ModdedWorldManager implements EngineWorldManager {
|| state.is(Blocks.KELP) || state.is(Blocks.KELP_PLANT); || state.is(Blocks.KELP) || state.is(Blocks.KELP_PLANT);
} }
private boolean clearForSpawn(ServerLevel level, int worldX, int worldY, int worldZ) {
BlockState at = level.getBlockState(new BlockPos(worldX, worldY, worldZ));
BlockState above = level.getBlockState(new BlockPos(worldX, worldY + 1, worldZ));
return !ModdedBlockResolution.isSolid(at) && !ModdedBlockResolution.isSolid(above);
}
private boolean aboveObstructed(ServerLevel level, int worldX, int worldY, int worldZ) { private boolean aboveObstructed(ServerLevel level, int worldX, int worldY, int worldZ) {
return ModdedBlockResolution.isSolid(level.getBlockState(new BlockPos(worldX, worldY + 1, worldZ))) return worldY + 2 >= level.getMaxY()
|| ModdedBlockResolution.isSolid(level.getBlockState(new BlockPos(worldX, worldY + 1, worldZ)))
|| ModdedBlockResolution.isSolid(level.getBlockState(new BlockPos(worldX, worldY + 2, worldZ))); || ModdedBlockResolution.isSolid(level.getBlockState(new BlockPos(worldX, worldY + 2, worldZ)));
} }
private int countChunkMobs(ServerLevel level, int chunkX, int chunkZ) { private int countChunkLivingEntities(ServerLevel level, int chunkX, int chunkZ) {
int baseX = chunkX << 4; int baseX = chunkX << 4;
int baseZ = chunkZ << 4; int baseZ = chunkZ << 4;
AABB box = new AABB(baseX, level.getMinY(), baseZ, baseX + 16, level.getMaxY(), baseZ + 16); AABB box = new AABB(baseX, level.getMinY(), baseZ, baseX + 16, level.getMaxY(), baseZ + 16);
return level.getEntities((Entity) null, box, (Entity e) -> e instanceof Mob && e.isAlive()).size(); return level.getEntities((Entity) null, box,
(Entity entity) -> isLivingEntityInChunk(entity, chunkX, chunkZ)).size();
} }
private void refreshEntityCount(ServerLevel level, long now) { private static boolean isLivingEntityInChunk(Entity entity, int chunkX, int chunkZ) {
if (!(entity instanceof LivingEntity)) {
return false;
}
BlockPos position = entity.blockPosition();
return (position.getX() >> 4) == chunkX && (position.getZ() >> 4) == chunkZ;
}
private void refreshEntityCount(ServerLevel level, long now, int loadedChunks) {
cachedConsideredChunks = loadedChunks;
cachedSaturation = cachedEntityCount / (loadedChunks + 1.0) * 1.28;
if (now - lastCountAt < COUNT_INTERVAL_MS) { if (now - lastCountAt < COUNT_INTERVAL_MS) {
return; return;
} }
lastCountAt = now; lastCountAt = now;
int mobs = 0; int livingEntities = 0;
int chunks = 0; for (Entity entity : level.getAllEntities()) {
for (ServerPlayer player : level.players()) { if (entity instanceof LivingEntity && entity.isAlive()) {
int px = player.blockPosition().getX(); livingEntities++;
int pz = player.blockPosition().getZ();
AABB box = new AABB(px - ENTITY_SCAN_RADIUS, level.getMinY(), pz - ENTITY_SCAN_RADIUS,
px + ENTITY_SCAN_RADIUS, level.getMaxY(), pz + ENTITY_SCAN_RADIUS);
mobs += level.getEntities((Entity) null, box, (Entity e) -> e instanceof Mob && e.isAlive()).size();
chunks += (PLAYER_CHUNK_RADIUS * 2 + 1) * (PLAYER_CHUNK_RADIUS * 2 + 1);
}
cachedEntityCount = mobs;
cachedConsideredChunks = chunks;
cachedSaturation = mobs / (chunks + 1.0) * 1.28;
}
private long[] loadedChunksNearPlayers(ServerLevel level) {
Set<Long> keys = new HashSet<>();
for (ServerPlayer player : level.players()) {
int centerX = player.blockPosition().getX() >> 4;
int centerZ = player.blockPosition().getZ() >> 4;
for (int dx = -PLAYER_CHUNK_RADIUS; dx <= PLAYER_CHUNK_RADIUS; dx++) {
for (int dz = -PLAYER_CHUNK_RADIUS; dz <= PLAYER_CHUNK_RADIUS; dz++) {
int chunkX = centerX + dx;
int chunkZ = centerZ + dz;
if (level.getChunkSource().getChunkNow(chunkX, chunkZ) != null) {
keys.add(pack(chunkX, chunkZ));
}
}
} }
} }
long[] result = new long[keys.size()]; cachedEntityCount = livingEntities;
int index = 0; cachedSaturation = livingEntities / (loadedChunks + 1.0) * 1.28;
for (long key : keys) { }
result[index++] = key;
} private long[] loadedChunkPositionsSnapshot(ServerLevel level) {
return result; LongArrayList positions = new LongArrayList(level.getChunkSource().getLoadedChunksCount());
level.getChunkSource().chunkMap.forEachReadyToSendChunk(chunk -> positions.add(chunk.getPos().pack()));
return positions.toLongArray();
} }
private boolean isPregenActive() { private boolean isPregenActive() {
@@ -652,6 +684,14 @@ public final class ModdedWorldManager implements EngineWorldManager {
return IrisSettings.get().getWorld().isAmbientEntitySpawningSystem(); return IrisSettings.get().getWorld().isAmbientEntitySpawningSystem();
} }
private boolean isEntitySpawningEnabledForCurrentWorld() {
return entitySpawningEnabled(engine.isStudio(), IrisSettings.get().getStudio().isEnableEntitySpawning());
}
static boolean entitySpawningEnabled(boolean studio, boolean studioSetting) {
return !studio || studioSetting;
}
private IrisEntitySpawn rarityPick(KList<IrisEntitySpawn> entries) { private IrisEntitySpawn rarityPick(KList<IrisEntitySpawn> entries) {
int totalRarity = 0; int totalRarity = 0;
for (IrisEntitySpawn entry : entries) { for (IrisEntitySpawn entry : entries) {
@@ -710,24 +750,4 @@ public final class ModdedWorldManager implements EngineWorldManager {
public void onSave() { public void onSave() {
engine.getMantle().save(); engine.getMantle().save();
} }
@Override
public void onBlockBreak(BlockBreakEvent e) {
}
@Override
public void onBlockPlace(BlockPlaceEvent e) {
}
@Override
public void onChunkLoad(Chunk e, boolean generated) {
}
@Override
public void onChunkUnload(Chunk e) {
}
@Override
public void teleportAsync(PlayerTeleportEvent e) {
}
} }
@@ -16,20 +16,22 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
package art.arcane.iris.fabric.mixin; package art.arcane.iris.modded.api;
import art.arcane.iris.modded.ModdedDeathLoot; import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.damagesource.DamageSource;
import net.minecraft.world.entity.LivingEntity;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
@Mixin(LivingEntity.class) import java.util.Objects;
public class LivingEntityMixin {
@Inject(method = "die", at = @At("TAIL")) public record ModdedBlockData(BlockState state, boolean deferredPlacement) {
private void iris$dropIrisDeathLoot(DamageSource damageSource, CallbackInfo info) { public ModdedBlockData {
ModdedDeathLoot.handle((LivingEntity) (Object) this); Objects.requireNonNull(state);
}
public static ModdedBlockData direct(BlockState state) {
return new ModdedBlockData(state, false);
}
public static ModdedBlockData deferred(BlockState state) {
return new ModdedBlockData(state, true);
} }
} }
@@ -0,0 +1,45 @@
/*
* Iris is a World Generator for Minecraft 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.modded.api;
import art.arcane.iris.engine.framework.Engine;
import net.minecraft.core.BlockPos;
import net.minecraft.resources.Identifier;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.world.level.block.state.BlockState;
import java.util.Map;
import java.util.Objects;
public record ModdedBlockPlacementContext(
Engine engine,
ServerLevel level,
BlockPos position,
Identifier blockId,
Map<String, String> state,
BlockState blockState) {
public ModdedBlockPlacementContext {
Objects.requireNonNull(engine);
Objects.requireNonNull(level);
Objects.requireNonNull(position);
Objects.requireNonNull(blockId);
state = Map.copyOf(state);
Objects.requireNonNull(blockState);
}
}
@@ -18,7 +18,9 @@
package art.arcane.iris.modded.api; package art.arcane.iris.modded.api;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.modded.ModdedBlockResolution; import art.arcane.iris.modded.ModdedBlockResolution;
import net.minecraft.core.BlockPos;
import net.minecraft.resources.Identifier; import net.minecraft.resources.Identifier;
import net.minecraft.server.level.ServerLevel; import net.minecraft.server.level.ServerLevel;
import net.minecraft.world.entity.Entity; import net.minecraft.world.entity.Entity;
@@ -96,7 +98,7 @@ public final class ModdedCustomContentRegistry {
return !PROVIDERS.isEmpty() || !CUSTOM_BLOCKS.isEmpty(); return !PROVIDERS.isEmpty() || !CUSTOM_BLOCKS.isEmpty();
} }
public static BlockState resolveBlock(String key) { public static ModdedBlockData resolveBlock(String key) {
if (key == null || (PROVIDERS.isEmpty() && CUSTOM_BLOCKS.isEmpty())) { if (key == null || (PROVIDERS.isEmpty() && CUSTOM_BLOCKS.isEmpty())) {
return null; return null;
} }
@@ -106,7 +108,7 @@ public final class ModdedCustomContentRegistry {
} }
BlockState custom = CUSTOM_BLOCKS.get(base.toString()); BlockState custom = CUSTOM_BLOCKS.get(base.toString());
if (custom != null) { if (custom != null) {
return custom; return ModdedBlockData.direct(custom);
} }
if (PROVIDERS.isEmpty()) { if (PROVIDERS.isEmpty()) {
return null; return null;
@@ -117,7 +119,7 @@ public final class ModdedCustomContentRegistry {
continue; continue;
} }
try { try {
BlockState resolved = provider.getBlockData(base, state); ModdedBlockData resolved = provider.getBlockData(base, state);
if (resolved != null) { if (resolved != null) {
return resolved; return resolved;
} }
@@ -128,6 +130,28 @@ public final class ModdedCustomContentRegistry {
return null; return null;
} }
public static void processBlockPlacement(Engine engine, ServerLevel level, BlockPos position, String key) {
Identifier base = parseIdentifier(key);
if (base == null) {
LOGGER.warn("Iris deferred custom block placement rejected invalid id {}", key);
return;
}
Map<String, String> state = parseState(key);
for (ModdedDataProvider provider : PROVIDERS) {
if (!provider.isReady() || !provider.isValidProvider(base, ModdedDataType.BLOCK)) {
continue;
}
try {
provider.processBlockPlacement(new ModdedBlockPlacementContext(
engine, level, position.immutable(), base, state, level.getBlockState(position)));
} catch (Throwable error) {
LOGGER.error("Iris custom content provider '{}' failed post-placement for {} at {}", provider.modId(), key, position, error);
}
return;
}
LOGGER.warn("Iris deferred custom block placement has no provider for {}", key);
}
public static Entity spawnMob(ServerLevel level, double x, double y, double z, String key) { public static Entity spawnMob(ServerLevel level, double x, double y, double z, String key) {
if (PROVIDERS.isEmpty() || level == null || key == null) { if (PROVIDERS.isEmpty() || level == null || key == null) {
return null; return null;
@@ -21,7 +21,6 @@ package art.arcane.iris.modded.api;
import net.minecraft.resources.Identifier; import net.minecraft.resources.Identifier;
import net.minecraft.server.level.ServerLevel; import net.minecraft.server.level.ServerLevel;
import net.minecraft.world.entity.Entity; import net.minecraft.world.entity.Entity;
import net.minecraft.world.level.block.state.BlockState;
import java.util.Collection; import java.util.Collection;
import java.util.Map; import java.util.Map;
@@ -37,10 +36,13 @@ public interface ModdedDataProvider {
boolean isValidProvider(Identifier id, ModdedDataType type); boolean isValidProvider(Identifier id, ModdedDataType type);
default BlockState getBlockData(Identifier blockId, Map<String, String> state) { default ModdedBlockData getBlockData(Identifier blockId, Map<String, String> state) {
return null; return null;
} }
default void processBlockPlacement(ModdedBlockPlacementContext context) {
}
default Entity spawnMob(ServerLevel level, double x, double y, double z, Identifier entityId) { default Entity spawnMob(ServerLevel level, double x, double y, double z, Identifier entityId) {
return null; return null;
} }
@@ -27,6 +27,7 @@ import art.arcane.iris.engine.framework.IrisStructureLocator;
import art.arcane.iris.engine.framework.Locator; import art.arcane.iris.engine.framework.Locator;
import art.arcane.iris.engine.framework.WrongEngineBroException; import art.arcane.iris.engine.framework.WrongEngineBroException;
import art.arcane.iris.engine.object.IrisBiome; import art.arcane.iris.engine.object.IrisBiome;
import art.arcane.iris.engine.object.IrisImportedStructureControl;
import art.arcane.iris.engine.object.IrisPosition; import art.arcane.iris.engine.object.IrisPosition;
import art.arcane.iris.engine.object.IrisRegion; import art.arcane.iris.engine.object.IrisRegion;
import art.arcane.iris.modded.IrisModdedChunkGenerator; import art.arcane.iris.modded.IrisModdedChunkGenerator;
@@ -40,6 +41,7 @@ import art.arcane.iris.spi.PlatformBlockState;
import art.arcane.volmlib.util.collection.KMap; import art.arcane.volmlib.util.collection.KMap;
import art.arcane.volmlib.util.math.Position2; import art.arcane.volmlib.util.math.Position2;
import art.arcane.volmlib.util.matter.MatterMarker; import art.arcane.volmlib.util.matter.MatterMarker;
import com.mojang.datafixers.util.Pair;
import com.mojang.brigadier.CommandDispatcher; import com.mojang.brigadier.CommandDispatcher;
import com.mojang.brigadier.arguments.IntegerArgumentType; import com.mojang.brigadier.arguments.IntegerArgumentType;
import com.mojang.brigadier.arguments.LongArgumentType; import com.mojang.brigadier.arguments.LongArgumentType;
@@ -58,10 +60,14 @@ import net.minecraft.commands.Commands;
import net.minecraft.commands.SharedSuggestionProvider; import net.minecraft.commands.SharedSuggestionProvider;
import net.minecraft.commands.arguments.DimensionArgument; import net.minecraft.commands.arguments.DimensionArgument;
import net.minecraft.commands.arguments.EntityArgument; import net.minecraft.commands.arguments.EntityArgument;
import net.minecraft.network.chat.Component;
import net.minecraft.core.BlockPos; import net.minecraft.core.BlockPos;
import net.minecraft.core.Holder;
import net.minecraft.core.HolderSet;
import net.minecraft.core.Registry;
import net.minecraft.core.particles.DustParticleOptions; import net.minecraft.core.particles.DustParticleOptions;
import net.minecraft.core.registries.BuiltInRegistries; import net.minecraft.core.registries.BuiltInRegistries;
import net.minecraft.core.registries.Registries;
import net.minecraft.network.chat.Component;
import net.minecraft.resources.Identifier; import net.minecraft.resources.Identifier;
import net.minecraft.server.MinecraftServer; import net.minecraft.server.MinecraftServer;
import net.minecraft.server.level.ServerLevel; import net.minecraft.server.level.ServerLevel;
@@ -71,6 +77,7 @@ import net.minecraft.world.item.ItemStack;
import net.minecraft.world.level.block.state.BlockState; import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.chunk.ChunkGenerator; import net.minecraft.world.level.chunk.ChunkGenerator;
import net.minecraft.world.level.levelgen.Heightmap; import net.minecraft.world.level.levelgen.Heightmap;
import net.minecraft.world.level.levelgen.structure.Structure;
import net.minecraft.world.phys.BlockHitResult; import net.minecraft.world.phys.BlockHitResult;
import net.minecraft.world.phys.HitResult; import net.minecraft.world.phys.HitResult;
import org.slf4j.Logger; import org.slf4j.Logger;
@@ -79,10 +86,13 @@ import org.slf4j.LoggerFactory;
import java.awt.Desktop; import java.awt.Desktop;
import java.io.File; import java.io.File;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collection;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Optional;
import java.util.Set; import java.util.Set;
import java.util.TreeMap; import java.util.TreeMap;
import java.util.TreeSet;
import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutionException;
import java.util.function.Predicate; import java.util.function.Predicate;
@@ -91,6 +101,7 @@ public final class IrisModdedCommands {
private static final Logger LOGGER = LoggerFactory.getLogger("Iris"); private static final Logger LOGGER = LoggerFactory.getLogger("Iris");
private static final Predicate<CommandSourceStack> GATE = Commands.hasPermission(Commands.LEVEL_GAMEMASTERS); private static final Predicate<CommandSourceStack> GATE = Commands.hasPermission(Commands.LEVEL_GAMEMASTERS);
private static final long LOCATE_TIMEOUT_MS = 120000L; private static final long LOCATE_TIMEOUT_MS = 120000L;
private static final int NATIVE_STRUCTURE_LOCATE_RADIUS = 100;
private static final SuggestionProvider<CommandSourceStack> BIOME_KEYS = (CommandContext<CommandSourceStack> context, SuggestionsBuilder builder) -> suggestBiomeKeys(context, builder); private static final SuggestionProvider<CommandSourceStack> BIOME_KEYS = (CommandContext<CommandSourceStack> context, SuggestionsBuilder builder) -> suggestBiomeKeys(context, builder);
private static final SuggestionProvider<CommandSourceStack> REGION_KEYS = (CommandContext<CommandSourceStack> context, SuggestionsBuilder builder) -> suggestRegionKeys(context, builder); private static final SuggestionProvider<CommandSourceStack> REGION_KEYS = (CommandContext<CommandSourceStack> context, SuggestionsBuilder builder) -> suggestRegionKeys(context, builder);
@@ -869,36 +880,58 @@ public final class IrisModdedCommands {
return 0; return 0;
} }
String key = keyRaw.trim(); String key = keyRaw.trim();
Set<String> placed = IrisStructureLocator.placedKeys(engine); if (key.isEmpty()) {
if (!IrisStructureLocator.isPlaced(engine, key)) { fail(source, "Name an Iris or native structure to locate.");
fail(source, "Structure " + key + " is not placed by this pack. Placed keys (" + placed.size() + "): "
+ String.join(", ", placed.stream().limit(8).toList()) + (placed.size() > 8 ? ", ..." : ""));
return 0; return 0;
} }
ServerPlayer player = source.getPlayer(); ServerPlayer player = source.getPlayer();
if (player == null) { if (player == null) {
fail(source, "This command can only be used by players. (structure key '" + key + "' resolved; " fail(source, "This command can only be used by players.");
+ placed.size() + " placed structure keys loaded)");
return 0; return 0;
} }
if (IrisStructureLocator.isPlaced(engine, key)) {
locateIrisStructure(source, level, engine, player, key);
return 1;
}
Optional<NativeStructureTarget> resolved = resolveNativeStructure(source, level, engine, key);
if (resolved.isEmpty()) {
fail(source, "Unknown structure '" + key + "'. Use tab completion to choose an Iris placement or a registered native/datapack structure.");
return 0;
}
NativeStructureTarget target = resolved.get();
if (target.availability() != NativeStructureAvailability.AVAILABLE) {
fail(source, nativeUnavailableMessage(target.key(), target.availability()));
return 0;
}
ok(source, "Searching for native structure " + target.key() + " within " + NATIVE_STRUCTURE_LOCATE_RADIUS + " chunks...");
runNativeStructureLocate(source, level, player, target);
return 1;
}
private static void locateIrisStructure(CommandSourceStack source, ServerLevel level, Engine engine,
ServerPlayer player, String key) {
MinecraftServer server = source.getServer(); MinecraftServer server = source.getServer();
int blockX = player.blockPosition().getX(); int blockX = player.blockPosition().getX();
int blockZ = player.blockPosition().getZ(); int blockZ = player.blockPosition().getZ();
ok(source, "Searching for structure " + key + "..."); ok(source, "Searching for Iris-placed structure " + key + "...");
Thread thread = new Thread(() -> { Thread thread = new Thread(() -> {
try { try {
int[] at = IrisStructureLocator.locate(engine, key, blockX, blockZ, 1024); IrisStructureLocator.LocateResult result =
if (at == null) { IrisStructureLocator.locate(engine, key, blockX, blockZ, 1024);
server.execute(() -> fail(source, "Could not find structure " + key + " within 1024 chunks.")); if (result.status() == IrisStructureLocator.LocateStatus.SEARCH_LIMIT_REACHED) {
server.execute(() -> fail(source, "Unable to locate Iris-placed structure " + key
+ ": the density search safety limit was reached before the full 1024-chunk radius was searched."));
return; return;
} }
int targetX = at[0] + 8; if (!result.found()) {
int targetY = at[1] + 2; server.execute(() -> fail(source, "Could not find Iris-placed structure " + key + " within 1024 chunks."));
int targetZ = at[2] + 8; return;
server.execute(() -> { }
player.teleportTo(level, targetX + 0.5D, targetY, targetZ + 0.5D, Set.<Relative>of(), player.getYRot(), player.getXRot(), false); int targetX = result.originX();
ok(source, "Teleported to structure " + key + " at " + targetX + " " + targetY + " " + targetZ); int targetY = result.baseY() + 2;
}); int targetZ = result.originZ();
server.execute(() -> teleportToStructure(source, level, player, targetX, targetY, targetZ,
"Iris-placed structure " + key));
} catch (Throwable e) { } catch (Throwable e) {
LOGGER.error("Iris structure locate failed for {}", key, e); LOGGER.error("Iris structure locate failed for {}", key, e);
server.execute(() -> fail(source, "Search failed: " + e.getClass().getSimpleName())); server.execute(() -> fail(source, "Search failed: " + e.getClass().getSimpleName()));
@@ -906,9 +939,196 @@ public final class IrisModdedCommands {
}, "Iris Structure Locator"); }, "Iris Structure Locator");
thread.setDaemon(true); thread.setDaemon(true);
thread.start(); thread.start();
}
private static void runNativeStructureLocate(CommandSourceStack source, ServerLevel level,
ServerPlayer player, NativeStructureTarget target) {
MinecraftServer server = source.getServer();
Runnable locateTask = () -> locateNativeStructure(source, level, player, target);
if (Thread.currentThread() == server.getRunningThread()) {
locateTask.run();
return;
}
server.execute(locateTask);
}
private static void locateNativeStructure(CommandSourceStack source, ServerLevel level,
ServerPlayer player, NativeStructureTarget target) {
try {
ChunkGenerator generator = level.getChunkSource().getGenerator();
Pair<BlockPos, Holder<Structure>> found = generator.findNearestMapStructure(
level,
HolderSet.direct(target.holder()),
player.blockPosition(),
NATIVE_STRUCTURE_LOCATE_RADIUS,
false);
if (found == null) {
fail(source, "Could not find native structure " + target.key() + " within "
+ NATIVE_STRUCTURE_LOCATE_RADIUS + " chunks.");
return;
}
BlockPos position = found.getFirst();
int targetX = position.getX();
int targetZ = position.getZ();
level.getChunk(targetX >> 4, targetZ >> 4);
int surfaceY = level.getHeight(Heightmap.Types.MOTION_BLOCKING_NO_LEAVES, targetX, targetZ) + 1;
int targetY = Math.max(level.getMinY() + 1, Math.min(level.getMaxY() - 1, surfaceY));
teleportToStructure(source, level, player, targetX, targetY, targetZ,
"native structure " + target.key());
} catch (Throwable e) {
LOGGER.error("Native structure locate failed for {}", target.key(), e);
fail(source, "Search for native structure " + target.key() + " failed: " + e.getClass().getSimpleName());
}
}
private static void teleportToStructure(CommandSourceStack source, ServerLevel level, ServerPlayer player,
int targetX, int targetY, int targetZ, String label) {
if (player.hasDisconnected() || player.isRemoved()) {
fail(source, "The player disconnected before the structure search completed.");
return;
}
if (player.level() != level) {
fail(source, "You changed dimensions before the structure search completed; run the command again.");
return;
}
level.getChunk(targetX >> 4, targetZ >> 4);
int clampedY = Math.max(level.getMinY() + 1, Math.min(level.getMaxY() - 1, targetY));
boolean teleported = player.teleportTo(level, targetX + 0.5D, clampedY, targetZ + 0.5D,
Set.<Relative>of(), player.getYRot(), player.getXRot(), false);
if (!teleported) {
fail(source, "Found " + label + " at " + targetX + " " + clampedY + " " + targetZ + ", but teleportation failed.");
return;
}
ok(source, "Teleported to " + label + " at " + targetX + " " + clampedY + " " + targetZ);
}
static int verifyStructures(CommandSourceStack source, String keyRaw) {
ServerLevel level = source.getLevel();
Engine engine = engineFor(level);
if (engine == null) {
fail(source, "This dimension is not generated by Iris.");
return 0;
}
String key = keyRaw == null ? "" : keyRaw.trim();
if (!key.isEmpty()) {
return verifyStructure(source, level, engine, key);
}
Registry<Structure> registry = source.getServer().registryAccess().lookupOrThrow(Registries.STRUCTURE);
int available = 0;
int disabled = 0;
int suppressed = 0;
int unreachableBiomes = 0;
int unsupported = 0;
for (Identifier identifier : registry.keySet()) {
Optional<Holder.Reference<Structure>> holder = registry.get(identifier);
if (holder.isEmpty()) {
continue;
}
NativeStructureAvailability availability = nativeAvailability(source, level, engine,
identifier.toString(), holder.get());
switch (availability) {
case AVAILABLE -> available++;
case WORLD_DISABLED, FILTERED -> disabled++;
case IRIS_SUPPRESSED -> suppressed++;
case BIOME_UNREACHABLE -> unreachableBiomes++;
case NO_PLACEMENT -> unsupported++;
}
}
int irisPlaced = IrisStructureLocator.placedKeys(engine).size();
ok(source, "Structure reachability: " + available + " native locatable, " + irisPlaced
+ " Iris-placed, " + disabled + " native disabled, " + suppressed
+ " native replaced by Iris placements, " + unreachableBiomes
+ " native excluded by this pack's biomes, and " + unsupported
+ " registered native structures unsupported in this dimension. Use /iris structure verify <key> for one structure.");
return 1; return 1;
} }
private static int verifyStructure(CommandSourceStack source, ServerLevel level, Engine engine, String key) {
if (IrisStructureLocator.isPlaced(engine, key)) {
ok(source, "Structure " + key + " is Iris-placed and locatable with /iris goto structure " + key + ".");
return 1;
}
Optional<NativeStructureTarget> target = resolveNativeStructure(source, level, engine, key);
if (target.isEmpty()) {
fail(source, "Unknown structure '" + key + "'. It is neither Iris-placed nor registered by vanilla or a datapack.");
return 0;
}
NativeStructureTarget resolved = target.get();
if (resolved.availability() != NativeStructureAvailability.AVAILABLE) {
fail(source, nativeUnavailableMessage(resolved.key(), resolved.availability()));
return 0;
}
ok(source, "Native structure " + resolved.key() + " is enabled, supported by this dimension's generator state, and locatable with /iris goto structure " + resolved.key() + ".");
return 1;
}
private static Optional<NativeStructureTarget> resolveNativeStructure(CommandSourceStack source,
ServerLevel level,
Engine engine,
String keyRaw) {
Identifier identifier = Identifier.tryParse(keyRaw);
if (identifier == null) {
return Optional.empty();
}
Registry<Structure> registry = source.getServer().registryAccess().lookupOrThrow(Registries.STRUCTURE);
Optional<Holder.Reference<Structure>> holder = registry.get(identifier);
if (holder.isEmpty()) {
return Optional.empty();
}
String key = identifier.toString();
NativeStructureAvailability availability = nativeAvailability(source, level, engine, key, holder.get());
return Optional.of(new NativeStructureTarget(key, holder.get(), availability));
}
private static NativeStructureAvailability nativeAvailability(CommandSourceStack source, ServerLevel level,
Engine engine, String key,
Holder.Reference<Structure> holder) {
boolean worldEnabled = source.getServer().getWorldGenSettings().options().generateStructures();
IrisImportedStructureControl control = engine.getDimension().getImportedStructures();
boolean selected = control != null && control.active() && control.shouldGenerate(key);
boolean suppressed = IrisStructureLocator.suppressesVanilla(engine, key);
ChunkGenerator chunkGenerator = level.getChunkSource().getGenerator();
boolean biomeReachable = chunkGenerator instanceof IrisModdedChunkGenerator irisGenerator
&& irisGenerator.isNativeStructureReachable(holder);
boolean hasPlacement = false;
if (worldEnabled && selected && !suppressed && biomeReachable) {
hasPlacement = !level.getChunkSource().getGeneratorState().getPlacementsForStructure(holder).isEmpty();
}
return classifyNativeAvailability(worldEnabled, selected, suppressed, biomeReachable, hasPlacement);
}
static NativeStructureAvailability classifyNativeAvailability(boolean worldEnabled, boolean selected,
boolean suppressed, boolean biomeReachable,
boolean hasPlacement) {
if (!worldEnabled) {
return NativeStructureAvailability.WORLD_DISABLED;
}
if (!selected) {
return NativeStructureAvailability.FILTERED;
}
if (suppressed) {
return NativeStructureAvailability.IRIS_SUPPRESSED;
}
if (!biomeReachable) {
return NativeStructureAvailability.BIOME_UNREACHABLE;
}
if (!hasPlacement) {
return NativeStructureAvailability.NO_PLACEMENT;
}
return NativeStructureAvailability.AVAILABLE;
}
private static String nativeUnavailableMessage(String key, NativeStructureAvailability availability) {
return switch (availability) {
case WORLD_DISABLED -> "Native structure generation is disabled for this world, so " + key + " cannot generate or be located.";
case FILTERED -> "Native structure " + key + " is disabled by this dimension's importedStructures settings.";
case IRIS_SUPPRESSED -> "Native structure " + key + " is replaced by an Iris placement in this pack; locate the Iris key instead.";
case BIOME_UNREACHABLE -> "Native structure " + key + " cannot generate because none of its required biomes are produced by this Iris pack.";
case NO_PLACEMENT -> "Native structure " + key + " is registered, but its structure set has no placement supported by this dimension's generator state.";
case AVAILABLE -> "Native structure " + key + " is available.";
};
}
private static int gotoPoi(CommandSourceStack source, String typeRaw) { private static int gotoPoi(CommandSourceStack source, String typeRaw) {
ServerLevel level = source.getLevel(); ServerLevel level = source.getLevel();
Engine engine = engineFor(level); Engine engine = engineFor(level);
@@ -991,7 +1211,7 @@ public final class IrisModdedCommands {
boolean installed = ModdedPackInstaller.install(ModdedEngineBootstrap.loader().configDir(), pack, branch, boolean installed = ModdedPackInstaller.install(ModdedEngineBootstrap.loader().configDir(), pack, branch,
(String message) -> server.execute(() -> ok(source, message))); (String message) -> server.execute(() -> ok(source, message)));
if (installed) { if (installed) {
server.execute(() -> ok(source, "Pack '" + pack + "' installed. Its dimension types and custom biomes join the forced Iris datapack on the next server restart; worlds created before restarting run with fallback heights until then.")); server.execute(() -> ok(source, "Pack '" + pack + "' installed. Its exact dimension types and custom biomes join the forced Iris datapack on the next server restart; restart before creating worlds from this pack."));
} else { } else {
server.execute(() -> fail(source, "Pack download failed for " + pack + " (" + downloadSource + "; see console).")); server.execute(() -> fail(source, "Pack download failed for " + pack + " (" + downloadSource + "; see console)."));
} }
@@ -1079,18 +1299,29 @@ public final class IrisModdedCommands {
return builder.buildFuture(); return builder.buildFuture();
} }
private static CompletableFuture<Suggestions> suggestStructureKeys(CommandContext<CommandSourceStack> context, SuggestionsBuilder builder) { static CompletableFuture<Suggestions> suggestStructureKeys(CommandContext<CommandSourceStack> context, SuggestionsBuilder builder) {
ModdedCommandFeedback.tab(context.getSource()); ModdedCommandFeedback.tab(context.getSource());
try { try {
Engine engine = engineFor(context.getSource().getLevel()); Engine engine = engineFor(context.getSource().getLevel());
if (engine != null) { Collection<String> irisKeys = engine == null ? List.of() : IrisStructureLocator.placedKeys(engine);
return SharedSuggestionProvider.suggest(IrisStructureLocator.placedKeys(engine), builder); Registry<Structure> registry = context.getSource().getServer().registryAccess().lookupOrThrow(Registries.STRUCTURE);
List<String> nativeKeys = new ArrayList<>(registry.keySet().size());
for (Identifier identifier : registry.keySet()) {
nativeKeys.add(identifier.toString());
} }
return SharedSuggestionProvider.suggest(combineStructureKeys(irisKeys, nativeKeys), builder);
} catch (Throwable ignored) { } catch (Throwable ignored) {
} }
return builder.buildFuture(); return builder.buildFuture();
} }
static List<String> combineStructureKeys(Collection<String> irisKeys, Collection<String> nativeKeys) {
Set<String> combined = new TreeSet<>();
combined.addAll(irisKeys);
combined.addAll(nativeKeys);
return List.copyOf(combined);
}
private static CompletableFuture<Suggestions> suggestPackNames(CommandContext<CommandSourceStack> context, SuggestionsBuilder builder) { private static CompletableFuture<Suggestions> suggestPackNames(CommandContext<CommandSourceStack> context, SuggestionsBuilder builder) {
ModdedCommandFeedback.tab(context.getSource()); ModdedCommandFeedback.tab(context.getSource());
List<String> names = new ArrayList<>(); List<String> names = new ArrayList<>();
@@ -1132,4 +1363,17 @@ public final class IrisModdedCommands {
static void fail(CommandSourceStack source, String message) { static void fail(CommandSourceStack source, String message) {
ModdedCommandFeedback.fail(source, message); ModdedCommandFeedback.fail(source, message);
} }
enum NativeStructureAvailability {
AVAILABLE,
WORLD_DISABLED,
FILTERED,
IRIS_SUPPRESSED,
BIOME_UNREACHABLE,
NO_PLACEMENT
}
private record NativeStructureTarget(String key, Holder.Reference<Structure> holder,
NativeStructureAvailability availability) {
}
} }
@@ -53,7 +53,7 @@ final class ModdedCommandHelp {
Entry.command("version", "", "Print version information"), Entry.command("version", "", "Print version information"),
Entry.command("info", "[dimension]", "List loaded Iris dimensions and pack details"), Entry.command("info", "[dimension]", "List loaded Iris dimensions and pack details"),
Entry.command("what", "[block|hand|markers]", "Inspect the Iris biome, region, cave biome, surface and chunk at your position, the block you look at, your held item, or nearby markers"), Entry.command("what", "[block|hand|markers]", "Inspect the Iris biome, region, cave biome, surface and chunk at your position, the block you look at, your held item, or nearby markers"),
Entry.group("find", "Find and teleport to Iris biomes, regions, objects, structures and points of interest", "goto"), Entry.group("find", "Find and teleport to Iris biomes, regions, objects, Iris structures, native structures and points of interest", "goto"),
Entry.command("tp", "<dimension> [player]", "Teleport yourself or a named player into a loaded Iris dimension"), Entry.command("tp", "<dimension> [player]", "Teleport yourself or a named player into a loaded Iris dimension"),
Entry.command("evacuate", "[dimension]", "Teleport every player out of an Iris dimension to the primary world spawn"), Entry.command("evacuate", "[dimension]", "Teleport every player out of an Iris dimension to the primary world spawn"),
Entry.command("seed", "", "Print world and engine seed information"), Entry.command("seed", "", "Print world and engine seed information"),
@@ -79,7 +79,7 @@ final class ModdedCommandHelp {
Entry.command("biome", "<key>", "Find an Iris biome"), Entry.command("biome", "<key>", "Find an Iris biome"),
Entry.command("region", "<key>", "Find an Iris region"), Entry.command("region", "<key>", "Find an Iris region"),
Entry.command("object", "<key>", "Find an object placement"), Entry.command("object", "<key>", "Find an object placement"),
Entry.command("structure", "<key>", "Find an Iris-placed structure"), Entry.command("structure", "<key>", "Find an Iris-placed or native/datapack structure"),
Entry.command("poi", "<type>", "Find a supported point of interest") Entry.command("poi", "<type>", "Find a supported point of interest")
)); ));
SECTIONS.put("goto", SECTIONS.get("find")); SECTIONS.put("goto", SECTIONS.get("find"));
@@ -160,7 +160,7 @@ final class ModdedCommandHelp {
Entry.command("place", "<key>", "Assemble and place an Iris structure at your location", "p"), Entry.command("place", "<key>", "Assemble and place an Iris structure at your location", "p"),
Entry.command("import", "", "Explain Bukkit structure import workflow", "import-all", "reimport", "imp", "all"), Entry.command("import", "", "Explain Bukkit structure import workflow", "import-all", "reimport", "imp", "all"),
Entry.command("capture", "", "Explain Bukkit structure capture workflow", "cap"), Entry.command("capture", "", "Explain Bukkit structure capture workflow", "cap"),
Entry.command("verify", "", "Explain modded structure locate behavior", "locateall") Entry.command("verify", "[key]", "Report native and Iris structure reachability in the current dimension", "locateall")
)); ));
SECTIONS.put("struct", SECTIONS.get("structure")); SECTIONS.put("struct", SECTIONS.get("structure"));
SECTIONS.put("str", SECTIONS.get("structure")); SECTIONS.put("str", SECTIONS.get("structure"));
@@ -69,8 +69,8 @@ public final class ModdedDatapackCommands {
root.then(Commands.literal("ls") root.then(Commands.literal("ls")
.executes((CommandContext<CommandSourceStack> context) -> list(context.getSource()))); .executes((CommandContext<CommandSourceStack> context) -> list(context.getSource())));
root.then(message("ingest", "Modrinth datapack ingest requires the Bukkit plugin: its post-restart structure import into editable Iris resources uses Bukkit registries, and Iris modded dimensions do not run vanilla structure placement, so ingested structure datapacks would not generate. Drop datapacks into world/datapacks manually for non-structure content.")); root.then(message("ingest", "Modrinth datapack ingest requires the Bukkit plugin because its editable-resource import and manifest workflow use Bukkit tooling. Iris modded dimensions do run native vanilla and datapack structure placement; install the datapack in world/datapacks and restart to generate its registered structures."));
root.then(message("pull", "Modrinth datapack ingest requires the Bukkit plugin: its post-restart structure import into editable Iris resources uses Bukkit registries, and Iris modded dimensions do not run vanilla structure placement, so ingested structure datapacks would not generate. Drop datapacks into world/datapacks manually for non-structure content.")); root.then(message("pull", "Modrinth datapack ingest requires the Bukkit plugin because its editable-resource import and manifest workflow use Bukkit tooling. Iris modded dimensions do run native vanilla and datapack structure placement; install the datapack in world/datapacks and restart to generate its registered structures."));
root.then(message("remove", "Datapack removal manages the Bukkit ingest manifest. On modded servers delete the datapack folder from world/datapacks and restart.")); root.then(message("remove", "Datapack removal manages the Bukkit ingest manifest. On modded servers delete the datapack folder from world/datapacks and restart."));
root.then(message("rm", "Datapack removal manages the Bukkit ingest manifest. On modded servers delete the datapack folder from world/datapacks and restart.")); root.then(message("rm", "Datapack removal manages the Bukkit ingest manifest. On modded servers delete the datapack folder from world/datapacks and restart."));
@@ -135,8 +135,7 @@ public final class ModdedDatapackCommands {
+ (override.isFile() ? " (world datapack override installed)" : "")); + (override.isFile() ? " (world datapack override installed)" : ""));
if (!matches) { if (!matches) {
mismatches++; mismatches++;
IrisModdedCommands.fail(source, " WARNING: the active dimension type does not match the pack. Terrain outside " IrisModdedCommands.fail(source, " WARNING: the active dimension type does not match the pack. Iris will refuse to start this world engine instead of clipping terrain. Run /iris world enable <dimension> <pack> for new worlds, or /iris datapack install for already-loaded Iris dimensions, then restart.");
+ activeMin + ".." + activeMax + " will be clipped. Run /iris world enable <dimension> <pack> for new worlds, or /iris datapack install for already-loaded Iris dimensions, then restart.");
} }
} }
if (irisLevels == 0) { if (irisLevels == 0) {
@@ -27,11 +27,10 @@ import art.arcane.iris.modded.ModdedBlockState;
import art.arcane.iris.modded.ModdedTileData; import art.arcane.iris.modded.ModdedTileData;
import art.arcane.iris.spi.PlatformBlockState; import art.arcane.iris.spi.PlatformBlockState;
import net.minecraft.core.BlockPos; import net.minecraft.core.BlockPos;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.nbt.NbtUtils;
import net.minecraft.server.level.ServerLevel; import net.minecraft.server.level.ServerLevel;
import net.minecraft.world.level.block.Block; import net.minecraft.world.level.block.Block;
import net.minecraft.world.level.block.Blocks; import net.minecraft.world.level.block.Blocks;
import net.minecraft.world.level.block.EntityBlock;
import net.minecraft.world.level.block.entity.BlockEntity; import net.minecraft.world.level.block.entity.BlockEntity;
import net.minecraft.world.level.block.state.BlockState; import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.levelgen.Heightmap; import net.minecraft.world.level.levelgen.Heightmap;
@@ -148,26 +147,31 @@ final class ModdedObjectPlacer implements IObjectPlacer {
skippedTiles++; skippedTiles++;
return; return;
} }
String snbt = moddedTile.snbt();
if (snbt == null || snbt.isBlank()) {
skippedTiles++;
return;
}
BlockPos pos = new BlockPos(xx, yy, zz); BlockPos pos = new BlockPos(xx, yy, zz);
BlockState state = level.getBlockState(pos); BlockState state = level.getBlockState(pos);
BlockState adjusted = moddedTile.adjustBlockState(state);
if (adjusted != state) {
level.setBlock(pos, adjusted, Block.UPDATE_CLIENTS | Block.UPDATE_KNOWN_SHAPE);
state = adjusted;
}
if (!state.hasBlockEntity()) { if (!state.hasBlockEntity()) {
skippedTiles++; skippedTiles++;
return; return;
} }
try { try {
CompoundTag tag = NbtUtils.snbtToStructure(snbt); BlockEntity restored = level.getBlockEntity(pos);
BlockEntity restored = BlockEntity.loadStatic(pos, state, tag, level.registryAccess()); if (restored == null && state.getBlock() instanceof EntityBlock entityBlock) {
restored = entityBlock.newBlockEntity(pos, state);
}
if (restored == null) { if (restored == null) {
skippedTiles++; skippedTiles++;
return; return;
} }
level.setBlockEntity(restored); level.setBlockEntity(restored);
restored.setChanged(); if (!moddedTile.apply(restored, level)) {
skippedTiles++;
return;
}
restoredTiles++; restoredTiles++;
} catch (Throwable e) { } catch (Throwable e) {
LOGGER.error("Iris tile restore failed at {} {} {}", xx, yy, zz, e); LOGGER.error("Iris tile restore failed at {} {} {}", xx, yy, zz, e);
@@ -23,6 +23,8 @@ import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.mantle.EngineMantle; import art.arcane.iris.engine.mantle.EngineMantle;
import art.arcane.iris.modded.IrisModdedChunkGenerator; import art.arcane.iris.modded.IrisModdedChunkGenerator;
import art.arcane.iris.modded.ModdedBlockBuffer; import art.arcane.iris.modded.ModdedBlockBuffer;
import art.arcane.iris.modded.ModdedEngineBootstrap;
import art.arcane.iris.modded.service.ModdedChunkUpdateService;
import art.arcane.iris.spi.IrisPlatforms; import art.arcane.iris.spi.IrisPlatforms;
import art.arcane.iris.spi.PlatformBiome; import art.arcane.iris.spi.PlatformBiome;
import art.arcane.iris.spi.PlatformBlockState; import art.arcane.iris.spi.PlatformBlockState;
@@ -33,16 +35,13 @@ import art.arcane.volmlib.util.format.Form;
import art.arcane.volmlib.util.math.M; import art.arcane.volmlib.util.math.M;
import net.minecraft.commands.CommandSourceStack; import net.minecraft.commands.CommandSourceStack;
import net.minecraft.core.BlockPos; import net.minecraft.core.BlockPos;
import net.minecraft.core.Registry;
import net.minecraft.core.SectionPos; import net.minecraft.core.SectionPos;
import net.minecraft.core.registries.Registries;
import net.minecraft.server.MinecraftServer; import net.minecraft.server.MinecraftServer;
import net.minecraft.server.level.ServerLevel; import net.minecraft.server.level.ServerLevel;
import net.minecraft.server.level.ServerPlayer; import net.minecraft.server.level.ServerPlayer;
import net.minecraft.server.level.ThreadedLevelLightEngine; import net.minecraft.server.level.ThreadedLevelLightEngine;
import net.minecraft.world.entity.Entity; import net.minecraft.world.entity.Entity;
import net.minecraft.world.level.ChunkPos; import net.minecraft.world.level.ChunkPos;
import net.minecraft.world.level.biome.Biome;
import net.minecraft.world.level.block.Blocks; import net.minecraft.world.level.block.Blocks;
import net.minecraft.world.level.block.EntityBlock; import net.minecraft.world.level.block.EntityBlock;
import net.minecraft.world.level.block.entity.BlockEntity; import net.minecraft.world.level.block.entity.BlockEntity;
@@ -240,9 +239,13 @@ public final class ModdedRegen {
lightEngine.updateSectionStatus(SectionPos.of(pos, chunk.getSectionYFromSectionIndex(i)), section.hasOnlyAir()); lightEngine.updateSectionStatus(SectionPos.of(pos, chunk.getSectionYFromSectionIndex(i)), section.hasOnlyAir());
} }
ModdedChunkUpdateService updateService = ModdedEngineBootstrap.services().service(ModdedChunkUpdateService.class);
if (updateService == null) {
throw new IllegalStateException("Iris chunk update service is unavailable during regeneration");
}
updateService.updateRegeneratedChunk(engine, level, chunkX, chunkZ);
Heightmap.primeHeightmaps(chunk, ChunkStatus.FULL.heightmapsAfter()); Heightmap.primeHeightmaps(chunk, ChunkStatus.FULL.heightmapsAfter());
Registry<Biome> biomeRegistry = level.registryAccess().lookupOrThrow(Registries.BIOME); chunk.fillBiomesFromNoise(generator.regenBiomeResolver(), level.getChunkSource().randomState().sampler());
chunk.fillBiomesFromNoise(generator.regenBiomeResolver(biomeRegistry, biomes, pos), level.getChunkSource().randomState().sampler());
chunk.markUnsaved(); chunk.markUnsaved();
for (BlockPos check : lightChecks) { for (BlockPos check : lightChecks) {
@@ -52,6 +52,7 @@ public final class ModdedStructureCommands {
private static final Predicate<CommandSourceStack> GATE = Commands.hasPermission(Commands.LEVEL_GAMEMASTERS); private static final Predicate<CommandSourceStack> GATE = Commands.hasPermission(Commands.LEVEL_GAMEMASTERS);
private static final SuggestionProvider<CommandSourceStack> IRIS_STRUCTURE_KEYS = (CommandContext<CommandSourceStack> context, SuggestionsBuilder builder) -> suggestIrisStructureKeys(context, builder); private static final SuggestionProvider<CommandSourceStack> IRIS_STRUCTURE_KEYS = (CommandContext<CommandSourceStack> context, SuggestionsBuilder builder) -> suggestIrisStructureKeys(context, builder);
private static final SuggestionProvider<CommandSourceStack> ALL_STRUCTURE_KEYS = (CommandContext<CommandSourceStack> context, SuggestionsBuilder builder) -> IrisModdedCommands.suggestStructureKeys(context, builder);
private ModdedStructureCommands() { private ModdedStructureCommands() {
} }
@@ -84,12 +85,20 @@ public final class ModdedStructureCommands {
root.then(message("all", "Structure import rebuilds vanilla & datapack structures as editable Iris resources through Bukkit/NMS template managers; run /iris structure import on a Bukkit server against this pack, then copy the pack folder over.")); root.then(message("all", "Structure import rebuilds vanilla & datapack structures as editable Iris resources through Bukkit/NMS template managers; run /iris structure import on a Bukkit server against this pack, then copy the pack folder over."));
root.then(message("capture", "Structure capture generates each structure in a throwaway Bukkit scratch world to read its blocks; it requires the Bukkit plugin (v26 NMS binding).")); root.then(message("capture", "Structure capture generates each structure in a throwaway Bukkit scratch world to read its blocks; it requires the Bukkit plugin (v26 NMS binding)."));
root.then(message("cap", "Structure capture generates each structure in a throwaway Bukkit scratch world to read its blocks; it requires the Bukkit plugin (v26 NMS binding).")); root.then(message("cap", "Structure capture generates each structure in a throwaway Bukkit scratch world to read its blocks; it requires the Bukkit plugin (v26 NMS binding)."));
root.then(message("verify", "Vanilla structure locate is meaningless here: Iris modded dimensions do not run vanilla structure placement (Iris places structures itself). Use /iris goto structure <key> to locate Iris-placed structures.")); root.then(verifyTree("verify"));
root.then(message("locateall", "Vanilla structure locate is meaningless here: Iris modded dimensions do not run vanilla structure placement (Iris places structures itself). Use /iris goto structure <key> to locate Iris-placed structures.")); root.then(verifyTree("locateall"));
return root; return root;
} }
private static LiteralArgumentBuilder<CommandSourceStack> verifyTree(String name) {
return Commands.literal(name)
.executes((CommandContext<CommandSourceStack> context) -> IrisModdedCommands.verifyStructures(context.getSource(), null))
.then(Commands.argument("key", StringArgumentType.greedyString()).suggests(ALL_STRUCTURE_KEYS)
.executes((CommandContext<CommandSourceStack> context) -> IrisModdedCommands.verifyStructures(
context.getSource(), StringArgumentType.getString(context, "key"))));
}
private static LiteralArgumentBuilder<CommandSourceStack> message(String name, String text) { private static LiteralArgumentBuilder<CommandSourceStack> message(String name, String text) {
return Commands.literal(name) return Commands.literal(name)
.executes((CommandContext<CommandSourceStack> context) -> { .executes((CommandContext<CommandSourceStack> context) -> {
@@ -0,0 +1,17 @@
package art.arcane.iris.modded.mixin;
import art.arcane.iris.modded.ModdedEntityPersistence;
import net.minecraft.world.entity.Entity;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
@Mixin(Entity.class)
public abstract class EntityPersistenceMixin {
@Inject(method = "shouldBeSaved", at = @At("RETURN"), cancellable = true)
private void iris$applyGeneratedPersistence(CallbackInfoReturnable<Boolean> info) {
Entity entity = (Entity) (Object) this;
info.setReturnValue(ModdedEntityPersistence.shouldSave(entity, info.getReturnValue()));
}
}
@@ -0,0 +1,20 @@
package art.arcane.iris.modded.mixin;
import art.arcane.iris.modded.ModdedDeathLoot;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.world.damagesource.DamageSource;
import net.minecraft.world.entity.LivingEntity;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
@Mixin(LivingEntity.class)
public abstract class LivingEntityLootMixin {
@Inject(method = "dropFromLootTable(Lnet/minecraft/server/level/ServerLevel;Lnet/minecraft/world/damagesource/DamageSource;Z)V", at = @At("HEAD"), cancellable = true)
private void iris$replaceBaseLoot(ServerLevel level, DamageSource damageSource, boolean playerKilled, CallbackInfo info) {
if (ModdedDeathLoot.replaceBaseLoot((LivingEntity) (Object) this)) {
info.cancel();
}
}
}
@@ -0,0 +1,39 @@
package art.arcane.iris.modded.mixin;
import art.arcane.iris.modded.ModdedEntityAwareness;
import net.minecraft.world.entity.Mob;
import net.minecraft.world.entity.ai.goal.FloatGoal;
import net.minecraft.world.entity.ai.goal.WrappedGoal;
import org.objectweb.asm.Opcodes;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
@Mixin(Mob.class)
public abstract class MobAwarenessMixin {
@Inject(
method = "serverAiStep",
at = @At(
value = "FIELD",
target = "Lnet/minecraft/world/entity/Mob;noActionTime:I",
opcode = Opcodes.PUTFIELD,
shift = At.Shift.AFTER),
cancellable = true)
private void iris$tickUnawareMob(CallbackInfo info) {
Mob mob = (Mob) (Object) this;
if (ModdedEntityAwareness.isAware(mob)) {
return;
}
for (WrappedGoal wrappedGoal : mob.getGoalSelector().getAvailableGoals()) {
if (wrappedGoal.getGoal() instanceof FloatGoal floatGoal) {
if (floatGoal.canUse()) {
floatGoal.tick();
}
mob.getJumpControl().tick();
break;
}
}
info.cancel();
}
}
@@ -20,6 +20,7 @@ package art.arcane.iris.modded.service;
import art.arcane.iris.core.IrisSettings; import art.arcane.iris.core.IrisSettings;
import art.arcane.iris.core.gui.PregeneratorJob; import art.arcane.iris.core.gui.PregeneratorJob;
import art.arcane.iris.core.link.Identifier;
import art.arcane.iris.engine.data.cache.Cache; import art.arcane.iris.engine.data.cache.Cache;
import art.arcane.iris.engine.framework.Engine; import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.object.TileData; import art.arcane.iris.engine.object.TileData;
@@ -27,6 +28,7 @@ import art.arcane.iris.modded.IrisModdedChunkGenerator;
import art.arcane.iris.modded.ModdedBlockResolution; import art.arcane.iris.modded.ModdedBlockResolution;
import art.arcane.iris.modded.ModdedLootApplier; import art.arcane.iris.modded.ModdedLootApplier;
import art.arcane.iris.modded.ModdedTileData; import art.arcane.iris.modded.ModdedTileData;
import art.arcane.iris.modded.api.ModdedCustomContentRegistry;
import art.arcane.iris.spi.IrisLogging; import art.arcane.iris.spi.IrisLogging;
import art.arcane.iris.util.project.matter.TileWrapper; import art.arcane.iris.util.project.matter.TileWrapper;
import art.arcane.volmlib.util.mantle.flag.MantleFlag; import art.arcane.volmlib.util.mantle.flag.MantleFlag;
@@ -39,14 +41,13 @@ import art.arcane.volmlib.util.matter.MatterCavern;
import art.arcane.volmlib.util.matter.MatterUpdate; import art.arcane.volmlib.util.matter.MatterUpdate;
import art.arcane.volmlib.util.scheduling.PrecisionStopwatch; import art.arcane.volmlib.util.scheduling.PrecisionStopwatch;
import net.minecraft.core.BlockPos; import net.minecraft.core.BlockPos;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.nbt.NbtUtils;
import net.minecraft.server.MinecraftServer; import net.minecraft.server.MinecraftServer;
import net.minecraft.server.level.ServerLevel; import net.minecraft.server.level.ServerLevel;
import net.minecraft.server.level.ServerPlayer; import net.minecraft.server.level.ServerPlayer;
import net.minecraft.world.level.ChunkPos; import net.minecraft.world.level.ChunkPos;
import net.minecraft.world.level.block.Block; import net.minecraft.world.level.block.Block;
import net.minecraft.world.level.block.Blocks; import net.minecraft.world.level.block.Blocks;
import net.minecraft.world.level.block.EntityBlock;
import net.minecraft.world.level.block.entity.BlockEntity; import net.minecraft.world.level.block.entity.BlockEntity;
import net.minecraft.world.level.block.state.BlockState; import net.minecraft.world.level.block.state.BlockState;
@@ -148,11 +149,21 @@ public final class ModdedChunkUpdateService implements ModdedTickableService {
} }
} }
public void updateRegeneratedChunk(Engine engine, ServerLevel level, int chunkX, int chunkZ) {
updateChunk(engine, level, chunkX, chunkZ, false);
}
private void updateChunk(Engine engine, ServerLevel level, int chunkX, int chunkZ) { private void updateChunk(Engine engine, ServerLevel level, int chunkX, int chunkZ) {
for (int x = -1; x <= 1; x++) { updateChunk(engine, level, chunkX, chunkZ, true);
for (int z = -1; z <= 1; z++) { }
if (level.getChunkSource().getChunkNow(chunkX + x, chunkZ + z) == null) {
return; private void updateChunk(Engine engine, ServerLevel level, int chunkX, int chunkZ, boolean requireNeighbors) {
if (requireNeighbors) {
for (int x = -1; x <= 1; x++) {
for (int z = -1; z <= 1; z++) {
if (level.getChunkSource().getChunkNow(chunkX + x, chunkZ + z) == null) {
return;
}
} }
} }
} }
@@ -170,6 +181,7 @@ public final class ModdedChunkUpdateService implements ModdedTickableService {
try { try {
chunk.raiseFlagUnchecked(MantleFlag.ETCHED, () -> { chunk.raiseFlagUnchecked(MantleFlag.ETCHED, () -> {
chunk.raiseFlagUnchecked(MantleFlag.TILE, () -> runTilePass(engine, level, chunkX, chunkZ, chunk)); chunk.raiseFlagUnchecked(MantleFlag.TILE, () -> runTilePass(engine, level, chunkX, chunkZ, chunk));
chunk.raiseFlagUnchecked(MantleFlag.CUSTOM, () -> runCustomPass(engine, level, chunkX, chunkZ, chunk));
chunk.raiseFlagUnchecked(MantleFlag.UPDATE, () -> runUpdatePass(engine, level, chunkX, chunkZ, chunk)); chunk.raiseFlagUnchecked(MantleFlag.UPDATE, () -> runUpdatePass(engine, level, chunkX, chunkZ, chunk));
}); });
} finally { } finally {
@@ -183,34 +195,57 @@ public final class ModdedChunkUpdateService implements ModdedTickableService {
int baseZ = chunkZ << 4; int baseZ = chunkZ << 4;
chunk.iterate(TileWrapper.class, (Integer x, Integer yf, Integer z, TileWrapper v) -> { chunk.iterate(TileWrapper.class, (Integer x, Integer yf, Integer z, TileWrapper v) -> {
int y = yf + minHeight; int y = yf + minHeight;
if (y < level.getMinY() || y > level.getMaxY()) { if (y < level.getMinY() || y >= level.getMaxY()) {
return; return;
} }
applyTile(level, baseX + (x & 15), y, baseZ + (z & 15), v.getData()); applyTile(level, baseX + (x & 15), y, baseZ + (z & 15), v.getData());
}); });
} }
private void runCustomPass(Engine engine, ServerLevel level, int chunkX, int chunkZ, MantleChunk<Matter> chunk) {
int minHeight = engine.getWorld().minHeight();
int baseX = chunkX << 4;
int baseZ = chunkZ << 4;
chunk.iterate(Identifier.class, (Integer x, Integer yf, Integer z, Identifier identifier) -> {
int y = yf + minHeight;
if (y < level.getMinY() || y >= level.getMaxY()) {
return;
}
BlockPos position = new BlockPos(baseX + (x & 15), y, baseZ + (z & 15));
ModdedCustomContentRegistry.processBlockPlacement(engine, level, position, identifier.toString());
});
}
private void applyTile(ServerLevel level, int x, int y, int z, TileData tile) { private void applyTile(ServerLevel level, int x, int y, int z, TileData tile) {
if (!(tile instanceof ModdedTileData moddedTile)) { if (!(tile instanceof ModdedTileData moddedTile)) {
return; return;
} }
String snbt = moddedTile.snbt();
if (snbt == null || snbt.isBlank()) {
return;
}
BlockPos pos = new BlockPos(x, y, z); BlockPos pos = new BlockPos(x, y, z);
BlockState state = level.getBlockState(pos); BlockState state = level.getBlockState(pos);
if (!state.hasBlockEntity()) { BlockState adjusted = moddedTile.adjustBlockState(state);
if (adjusted != state) {
level.setBlock(pos, adjusted, SILENT_FLAGS);
state = adjusted;
}
if (!state.hasBlockEntity() || !(state.getBlock() instanceof EntityBlock entityBlock)) {
return; return;
} }
try { try {
CompoundTag tag = NbtUtils.snbtToStructure(snbt); BlockEntity blockEntity = level.getBlockEntity(pos);
BlockEntity restored = BlockEntity.loadStatic(pos, state, tag, level.registryAccess()); if (blockEntity == null) {
if (restored == null) { blockEntity = entityBlock.newBlockEntity(pos, state);
}
if (blockEntity == null) {
IrisLogging.warn("Iris could not create block entity at " + pos + " for " + state);
return; return;
} }
level.setBlockEntity(restored); if (!moddedTile.isApplicable(state, blockEntity)) {
restored.setChanged(); return;
}
level.setBlockEntity(blockEntity);
if (!moddedTile.apply(blockEntity, level)) {
IrisLogging.warn("Iris tile payload was empty at " + pos + " for " + state);
}
} catch (Throwable e) { } catch (Throwable e) {
IrisLogging.reportError(e); IrisLogging.reportError(e);
} }
@@ -255,7 +290,7 @@ public final class ModdedChunkUpdateService implements ModdedTickableService {
RNG rng = new RNG(Cache.key(chunkX, chunkZ)); RNG rng = new RNG(Cache.key(chunkX, chunkZ));
chunk.iterate(MatterCavern.class, (Integer x, Integer yf, Integer z, MatterCavern v) -> { chunk.iterate(MatterCavern.class, (Integer x, Integer yf, Integer z, MatterCavern v) -> {
int y = yf + minHeight; int y = yf + minHeight;
if (y < level.getMinY() || y > level.getMaxY()) { if (y < level.getMinY() || y >= level.getMaxY()) {
return; return;
} }
int lx = x & 15; int lx = x & 15;
@@ -294,7 +329,7 @@ public final class ModdedChunkUpdateService implements ModdedTickableService {
} }
private void update(Engine engine, ServerLevel level, int x, int y, int z, int baseX, int baseZ, MantleChunk<Matter> chunk, RNG rng) { private void update(Engine engine, ServerLevel level, int x, int y, int z, int baseX, int baseZ, MantleChunk<Matter> chunk, RNG rng) {
if (y < level.getMinY() || y > level.getMaxY()) { if (y < level.getMinY() || y >= level.getMaxY()) {
return; return;
} }
BlockPos pos = new BlockPos(baseX + (x & 15), y, baseZ + (z & 15)); BlockPos pos = new BlockPos(baseX + (x & 15), y, baseZ + (z & 15));
@@ -31,9 +31,10 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import java.util.Collection; import java.util.Collection;
import java.util.Collections;
import java.util.IdentityHashMap;
import java.util.Locale; import java.util.Locale;
import java.util.Set; import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutorService; import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors; import java.util.concurrent.Executors;
import java.util.concurrent.RejectedExecutionException; import java.util.concurrent.RejectedExecutionException;
@@ -47,7 +48,7 @@ public final class ModdedEngineMaintenanceService implements ModdedTickableServi
private static final long SAVE_PERIOD_MILLIS = 60_000L; private static final long SAVE_PERIOD_MILLIS = 60_000L;
private final AtomicInteger tectonicLimit = new AtomicInteger(30); private final AtomicInteger tectonicLimit = new AtomicInteger(30);
private final Set<Engine> inFlight = ConcurrentHashMap.newKeySet(); private final Set<Engine> inFlight = Collections.synchronizedSet(Collections.newSetFromMap(new IdentityHashMap<>()));
private volatile ExecutorService service; private volatile ExecutorService service;
private long lastMaintenanceAt; private long lastMaintenanceAt;
private long lastSaveAt; private long lastSaveAt;
@@ -20,9 +20,15 @@ package art.arcane.iris.modded.service;
import art.arcane.iris.core.gui.PregeneratorJob; import art.arcane.iris.core.gui.PregeneratorJob;
import art.arcane.iris.core.loader.IrisData; import art.arcane.iris.core.loader.IrisData;
import art.arcane.iris.core.tools.WorldMaintenance;
import art.arcane.iris.engine.framework.Engine; import art.arcane.iris.engine.framework.Engine;
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.modded.IrisModdedChunkGenerator; import art.arcane.iris.modded.IrisModdedChunkGenerator;
import art.arcane.iris.modded.ModdedDimensionManager; import art.arcane.iris.modded.ModdedDimensionManager;
import art.arcane.iris.modded.ModdedForcedDatapack;
import art.arcane.iris.modded.ModdedWorkspaceGenerator; import art.arcane.iris.modded.ModdedWorkspaceGenerator;
import art.arcane.volmlib.util.collection.KList; import art.arcane.volmlib.util.collection.KList;
import art.arcane.volmlib.util.io.ReactiveFolder; import art.arcane.volmlib.util.io.ReactiveFolder;
@@ -41,7 +47,7 @@ import java.util.concurrent.Executors;
import java.util.concurrent.RejectedExecutionException; import java.util.concurrent.RejectedExecutionException;
import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicBoolean;
public final class ModdedStudioHotloadService implements ModdedTickableService { public final class ModdedStudioHotloadService implements ModdedTickableService, EnginePlatformHooks {
private static final Logger LOGGER = LoggerFactory.getLogger("Iris"); private static final Logger LOGGER = LoggerFactory.getLogger("Iris");
private static final String STUDIO_DIMENSION_PREFIX = "irisworldgen:studio_"; private static final String STUDIO_DIMENSION_PREFIX = "irisworldgen:studio_";
private static final long POLL_MILLIS = 250L; private static final long POLL_MILLIS = 250L;
@@ -76,6 +82,34 @@ public final class ModdedStudioHotloadService implements ModdedTickableService {
watches.clear(); watches.clear();
} }
@Override
public void refreshWorkspace(Engine engine) {
writeWorkspace(engine, "workspace refresh");
}
@Override
public void refreshDatapackWorkspace(Engine engine) {
IrisData data = engine.getData();
KList<IrisDimension> dimensions = data.getDimensionLoader().loadAll(data.getDimensionLoader().getPossibleKeys());
if (hasDatapackImports(dimensions)) {
writeWorkspace(engine, "datapack workspace refresh");
}
}
@Override
public void reloadDatapacks(Engine engine) {
ModdedForcedDatapack.regenerate();
}
@Override
public void validateDimensionHotload(Engine engine, IrisDimension replacement) {
IrisDimensionRuntimeContract.requireHotloadCompatible(
"Modded Studio level '" + engine.getWorld().identity() + "'",
engine.getDimension(),
replacement,
"irisworldgen");
}
@Override @Override
public void onServerTick(MinecraftServer server) { public void onServerTick(MinecraftServer server) {
ExecutorService active = executor; ExecutorService active = executor;
@@ -134,12 +168,34 @@ public final class ModdedStudioHotloadService implements ModdedTickableService {
} }
} }
@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 shouldSkipMantleCleanup(Engine engine) {
IrisWorld world = engine.getWorld();
return world != null
&& WorldMaintenance.isWorldMaintenanceActive(world.identity())
&& !isPregeneratorActive(engine);
}
private boolean throttled(IrisModdedChunkGenerator generator, Engine engine, long now) { private boolean throttled(IrisModdedChunkGenerator generator, Engine engine, long now) {
if (now - generator.lastChunkGenAt() < RECENT_GENERATION_HOLDOFF_MILLIS) { if (now - generator.lastChunkGenAt() < RECENT_GENERATION_HOLDOFF_MILLIS) {
return true; return true;
} }
PregeneratorJob job = PregeneratorJob.getInstance(); return isPregeneratorActive(engine);
return job != null && job.targetsWorldIdentity(engine.getWorld().identity());
} }
private void poll(String dimensionId, Watch watch, IrisModdedChunkGenerator generator, Engine engine) { private void poll(String dimensionId, Watch watch, IrisModdedChunkGenerator generator, Engine engine) {
@@ -171,19 +227,32 @@ public final class ModdedStudioHotloadService implements ModdedTickableService {
try { try {
engine.hotloadSilently(); engine.hotloadSilently();
generator.onHotload(); generator.onHotload();
regenerateSchemas(dimensionId, engine);
LOGGER.info("Iris studio hotload {} pack={} {}ms", dimensionId, engine.getDimension().getLoadKey(), System.currentTimeMillis() - start); LOGGER.info("Iris studio hotload {} pack={} {}ms", dimensionId, engine.getDimension().getLoadKey(), System.currentTimeMillis() - start);
} catch (Throwable e) { } catch (Throwable e) {
LOGGER.error("Iris studio hotload failed for {}", dimensionId, e); LOGGER.error("Iris studio hotload failed for {}", dimensionId, e);
} }
} }
private void regenerateSchemas(String dimensionId, Engine engine) { static boolean hasDatapackImports(Iterable<IrisDimension> dimensions) {
if (dimensions == null) {
return false;
}
for (IrisDimension dimension : dimensions) {
if (dimension != null
&& dimension.getDatapackImports() != null
&& !dimension.getDatapackImports().isEmpty()) {
return true;
}
}
return false;
}
private void writeWorkspace(Engine engine, String operation) {
try { try {
IrisData data = engine.getData(); IrisData data = engine.getData();
ModdedWorkspaceGenerator.writeWorkspace(data, data.getDataFolder()); ModdedWorkspaceGenerator.writeWorkspace(data, data.getDataFolder());
} catch (Throwable e) { } catch (Throwable e) {
LOGGER.error("Iris studio schema regeneration failed for {}", dimensionId, e); LOGGER.error("Iris {} failed for {}", operation, engine.getDimension().getLoadKey(), e);
} }
} }
@@ -1,55 +0,0 @@
{
"ambient_light": 0.0,
"attributes": {
"minecraft:audio/ambient_sounds": {
"mood": {
"block_search_extent": 8,
"offset": 2.0,
"sound": "minecraft:ambient.cave",
"tick_delay": 6000
}
},
"minecraft:audio/background_music": {
"creative": {
"max_delay": 24000,
"min_delay": 12000,
"sound": "minecraft:music.creative"
},
"default": {
"max_delay": 24000,
"min_delay": 12000,
"sound": "minecraft:music.game"
}
},
"minecraft:gameplay/bed_rule": {
"can_set_spawn": "always",
"can_sleep": "when_dark",
"error_message": {
"translate": "block.minecraft.bed.no_sleep"
}
},
"minecraft:gameplay/nether_portal_spawns_piglin": true,
"minecraft:gameplay/respawn_anchor_works": false,
"minecraft:visual/ambient_light_color": "#0a0a0a",
"minecraft:visual/cloud_color": "#ccffffff",
"minecraft:visual/cloud_height": 192.33,
"minecraft:visual/fog_color": "#c0d8ff",
"minecraft:visual/sky_color": "#78a7ff"
},
"coordinate_scale": 1.0,
"default_clock": "minecraft:overworld",
"has_ceiling": false,
"has_ender_dragon_fight": false,
"has_skylight": true,
"height": 768,
"infiniburn": "#minecraft:infiniburn_overworld",
"logical_height": 512,
"min_y": -256,
"monster_spawn_block_light_limit": 0,
"monster_spawn_light_level": {
"type": "minecraft:uniform",
"max_inclusive": 7,
"min_inclusive": 0
},
"timelines": "#minecraft:in_overworld"
}
@@ -0,0 +1,14 @@
{
"required": true,
"minVersion": "0.8",
"package": "art.arcane.iris.modded.mixin",
"compatibilityLevel": "JAVA_21",
"mixins": [
"EntityPersistenceMixin",
"LivingEntityLootMixin",
"MobAwarenessMixin"
],
"injectors": {
"defaultRequire": 1
}
}
@@ -0,0 +1,152 @@
package art.arcane.iris.modded;
import art.arcane.volmlib.util.math.RNG;
import com.mojang.serialization.Codec;
import net.minecraft.SharedConstants;
import net.minecraft.core.BlockPos;
import net.minecraft.core.Holder;
import net.minecraft.core.IdMapper;
import net.minecraft.core.RegistryAccess;
import net.minecraft.core.component.DataComponentMap;
import net.minecraft.core.registries.BuiltInRegistries;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.resources.Identifier;
import net.minecraft.server.Bootstrap;
import net.minecraft.util.ProblemReporter;
import net.minecraft.world.Container;
import net.minecraft.world.item.Item;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.item.Items;
import net.minecraft.world.level.ChunkPos;
import net.minecraft.world.level.LevelHeightAccessor;
import net.minecraft.world.level.biome.Biome;
import net.minecraft.world.level.biome.BiomeGenerationSettings;
import net.minecraft.world.level.biome.BiomeSpecialEffects;
import net.minecraft.world.level.biome.MobSpawnSettings;
import net.minecraft.world.level.block.Block;
import net.minecraft.world.level.block.Blocks;
import net.minecraft.world.level.block.entity.BannerBlockEntity;
import net.minecraft.world.level.block.entity.BlockEntity;
import net.minecraft.world.level.block.entity.ChestBlockEntity;
import net.minecraft.world.level.block.entity.SignBlockEntity;
import net.minecraft.world.level.block.entity.SpawnerBlockEntity;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.chunk.PalettedContainerFactory;
import net.minecraft.world.level.chunk.PalettedContainer;
import net.minecraft.world.level.chunk.PalettedContainerRO;
import net.minecraft.world.level.chunk.ProtoChunk;
import net.minecraft.world.level.chunk.Strategy;
import net.minecraft.world.level.chunk.UpgradeData;
import net.minecraft.world.level.storage.TagValueInput;
import org.junit.BeforeClass;
import org.junit.Test;
import java.util.List;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
public class IrisModdedBlockEntityParityTest {
private static RegistryAccess registries;
@BeforeClass
public static void bootstrapMinecraftRegistries() {
SharedConstants.tryDetectVersion();
Bootstrap.bootStrap();
registries = RegistryAccess.fromRegistryOfRegistries(BuiltInRegistries.REGISTRY);
if (!Items.DIAMOND.builtInRegistryHolder().areComponentsBound()) {
Items.DIAMOND.builtInRegistryHolder().bindComponents(DataComponentMap.EMPTY);
}
}
@Test
public void generatedContainerGetsBlockEntityAndPersistsFilledLoot() {
ProtoChunk chunk = newChunk();
BlockPos position = new BlockPos(3, 70, 5);
chunk.setBlockState(position, Blocks.CHEST.defaultBlockState(), 0);
IrisModdedChunkGenerator.createDefaultBlockEntity(chunk, position, Blocks.CHEST.defaultBlockState());
BlockEntity blockEntity = chunk.getBlockEntity(position);
assertNotNull(blockEntity);
assertTrue(blockEntity instanceof Container);
Container container = (Container) blockEntity;
ModdedLootApplier.fillContainer(container, List.of(new ItemStack(Items.DIAMOND)), new RNG(17L));
assertEquals(1, countItem(container, Items.DIAMOND));
CompoundTag saved = blockEntity.saveWithoutMetadata(registries);
ChestBlockEntity restored = new ChestBlockEntity(position, Blocks.CHEST.defaultBlockState());
restored.loadWithComponents(TagValueInput.create(ProblemReporter.DISCARDING, registries, saved));
assertEquals(1, countItem(restored, Items.DIAMOND));
}
@Test
public void generatedSignSpawnerAndBannerGetNativeBlockEntities() {
ProtoChunk chunk = newChunk();
BlockState signState = Blocks.OAK_SIGN.defaultBlockState();
BlockState spawnerState = Blocks.SPAWNER.defaultBlockState();
BlockState bannerState = BuiltInRegistries.BLOCK.getValue(
Identifier.parse("minecraft:white_banner")).defaultBlockState();
BlockPos signPosition = new BlockPos(1, 70, 1);
BlockPos spawnerPosition = new BlockPos(2, 70, 1);
BlockPos bannerPosition = new BlockPos(3, 70, 1);
chunk.setBlockState(signPosition, signState, 0);
chunk.setBlockState(spawnerPosition, spawnerState, 0);
chunk.setBlockState(bannerPosition, bannerState, 0);
IrisModdedChunkGenerator.createDefaultBlockEntity(chunk, signPosition, signState);
IrisModdedChunkGenerator.createDefaultBlockEntity(chunk, spawnerPosition, spawnerState);
IrisModdedChunkGenerator.createDefaultBlockEntity(chunk, bannerPosition, bannerState);
assertTrue(chunk.getBlockEntity(signPosition) instanceof SignBlockEntity);
assertTrue(chunk.getBlockEntity(spawnerPosition) instanceof SpawnerBlockEntity);
assertTrue(chunk.getBlockEntity(bannerPosition) instanceof BannerBlockEntity);
}
private static ProtoChunk newChunk() {
return new ProtoChunk(
new ChunkPos(0, 0),
UpgradeData.EMPTY,
LevelHeightAccessor.create(-64, 384),
palettedContainerFactory(),
null);
}
private static PalettedContainerFactory palettedContainerFactory() {
Strategy<BlockState> blockStrategy = Strategy.createForBlockStates(Block.BLOCK_STATE_REGISTRY);
Codec<PalettedContainer<BlockState>> blockCodec = PalettedContainer.codecRW(
BlockState.CODEC, blockStrategy, Blocks.AIR.defaultBlockState());
Biome biome = new Biome.BiomeBuilder()
.hasPrecipitation(false)
.temperature(0.8F)
.downfall(0.4F)
.specialEffects(new BiomeSpecialEffects.Builder().waterColor(0x3F76E4).build())
.mobSpawnSettings(MobSpawnSettings.EMPTY)
.generationSettings(BiomeGenerationSettings.EMPTY)
.build();
Holder<Biome> biomeHolder = Holder.direct(biome);
IdMapper<Holder<Biome>> biomeIds = new IdMapper<>(1);
biomeIds.add(biomeHolder);
Strategy<Holder<Biome>> biomeStrategy = Strategy.createForBiomes(biomeIds);
Codec<PalettedContainerRO<Holder<Biome>>> biomeCodec = PalettedContainer.codecRO(
Biome.CODEC, biomeStrategy, biomeHolder);
return new PalettedContainerFactory(
blockStrategy,
Blocks.AIR.defaultBlockState(),
blockCodec,
biomeStrategy,
biomeHolder,
biomeCodec);
}
private static int countItem(Container container, Item item) {
int count = 0;
for (int slot = 0; slot < container.getContainerSize(); slot++) {
ItemStack stack = container.getItem(slot);
if (stack.getItem() == item) {
count += stack.getCount();
}
}
return count;
}
}
Loaded 100 of 234 files, more files were not shown because too many files have changed in this diff. Show more