This commit is contained in:
Brian Neumann-Fopiano
2026-07-30 12:31:39 -04:00
parent 0afbfcb7a2
commit 324cf9e095
374 changed files with 22705 additions and 25650 deletions
@@ -49,8 +49,10 @@ import java.util.stream.Stream;
final class IrisModdedBiomeSource extends BiomeSource {
private static final int BIOME_CACHE_MAX = 262144;
private static final int UNRESOLVED_WARN_KEYS_MAX = 256;
private final BiomeSource serializedSource;
private final Set<String> warnedUnresolvedBiomeKeys = ConcurrentHashMap.newKeySet();
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<>();
@@ -70,6 +72,7 @@ final class IrisModdedBiomeSource extends BiomeSource {
visibleBiomeCache.clear();
structureBiomeCache.clear();
surfaceStructureBiomeCache.clear();
warnedUnresolvedBiomeKeys.clear();
possibleStructureBiomeKeys = null;
for (StructureStateBiomeSource source : structureStateSources) {
source.clearCache();
@@ -332,7 +335,8 @@ final class IrisModdedBiomeSource extends BiomeSource {
IrisBiomeCustom customBiome = resolution.irisBiome().getCustomBiome(
resolution.rng(), engine, resolution.blockX(), resolution.blockY(), resolution.blockZ());
if (customBiome == null) {
return fallbackBiome(registry, quartX, quartY, quartZ, sampler);
return fallbackBiome(registry, "custom derivative of '"
+ resolution.irisBiome().getLoadKey() + "'", quartX, quartY, quartZ, sampler);
}
biomeKey = ModdedWorldgenIds.biomeRef(engine, customBiome.getId());
} else if (resolution.underground()) {
@@ -344,7 +348,7 @@ final class IrisModdedBiomeSource extends BiomeSource {
}
Holder<Biome> resolved = resolveHolder(registry, biomeKey);
return resolved == null
? fallbackBiome(registry, quartX, quartY, quartZ, sampler)
? fallbackBiome(registry, biomeKey, quartX, quartY, quartZ, sampler)
: resolved;
}
@@ -567,12 +571,29 @@ final class IrisModdedBiomeSource extends BiomeSource {
return identifier == null ? null : identifier.toString().toLowerCase(Locale.ROOT);
}
private Holder<Biome> fallbackBiome(Registry<Biome> registry, int quartX, int quartY, int quartZ,
private Holder<Biome> fallbackBiome(Registry<Biome> registry, String unresolvedKey,
int quartX, int quartY, int quartZ,
Climate.Sampler sampler) {
Holder<Biome> plains = resolveHolder(registry, "minecraft:plains");
warnUnresolvedBiome(unresolvedKey, plains == null ? "the serialized biome source" : "minecraft:plains",
quartX, quartY, quartZ);
return plains == null ? serializedSource.getNoiseBiome(quartX, quartY, quartZ, sampler) : plains;
}
private void warnUnresolvedBiome(String unresolvedKey, String fallback,
int quartX, int quartY, int quartZ) {
String key = unresolvedKey == null || unresolvedKey.isBlank() ? "<blank>" : unresolvedKey;
if (!warnedUnresolvedBiomeKeys.add(key)) {
return;
}
if (warnedUnresolvedBiomeKeys.size() > UNRESOLVED_WARN_KEYS_MAX) {
warnedUnresolvedBiomeKeys.clear();
}
ModdedIrisLog.warn("Iris biome " + key + " is not registered; using " + fallback
+ " at quart " + quartX + "," + quartY + "," + quartZ
+ " (wrong biome generates; regenerate the forced datapack and restart)");
}
private static long packNoiseKey(int x, int y, int z) {
return (((long) x & 67108863L) << 38)
| (((long) z & 67108863L) << 12)
@@ -23,27 +23,15 @@ import art.arcane.iris.core.pack.PackValidationRegistry;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.framework.GenerationSessionException;
import art.arcane.iris.engine.framework.GenerationSessionLease;
import art.arcane.iris.engine.framework.IrisStructureLocator;
import art.arcane.iris.engine.framework.NativeStructurePlacementPlanner;
import art.arcane.iris.engine.framework.NativeStructureStartPlan;
import art.arcane.iris.engine.framework.NativeStructureGenerationPolicy;
import art.arcane.iris.engine.object.IrisBiome;
import art.arcane.iris.engine.object.IrisBiomeCustom;
import art.arcane.iris.engine.object.IrisDimension;
import art.arcane.iris.engine.object.IrisMaterialPalette;
import art.arcane.iris.engine.object.IrisNativeStructureDecision;
import art.arcane.iris.engine.object.IrisRegion;
import art.arcane.iris.nativegen.NativeStructureGenerationException;
import art.arcane.iris.nativegen.NativeStructureStartInjector;
import art.arcane.iris.nativegen.NativeStructureReferenceEnvelope;
import art.arcane.iris.nativegen.NativeStructureLocateResults;
import art.arcane.iris.nativegen.NativeStructurePostProcessor;
import art.arcane.iris.spi.IrisPlatforms;
import art.arcane.iris.spi.PlatformBiome;
import art.arcane.iris.spi.PlatformBlockState;
import art.arcane.iris.util.project.context.IrisContext;
import art.arcane.iris.util.project.hunk.Hunk;
import art.arcane.volmlib.util.math.RNG;
import com.mojang.datafixers.util.Pair;
import com.mojang.serialization.Codec;
import com.mojang.serialization.MapCodec;
@@ -54,9 +42,7 @@ import net.minecraft.core.HolderLookup;
import net.minecraft.core.HolderSet;
import net.minecraft.core.Registry;
import net.minecraft.core.RegistryAccess;
import net.minecraft.core.SectionPos;
import net.minecraft.core.registries.Registries;
import net.minecraft.resources.Identifier;
import net.minecraft.resources.ResourceKey;
import net.minecraft.server.MinecraftServer;
import net.minecraft.server.level.ServerLevel;
@@ -83,15 +69,12 @@ import net.minecraft.world.level.chunk.ChunkAccess;
import net.minecraft.world.level.chunk.ChunkGenerator;
import net.minecraft.world.level.chunk.ChunkGeneratorStructureState;
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.LegacyRandomSource;
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.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;
@@ -100,98 +83,41 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.File;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionException;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.IntBinaryOperator;
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");
public static final MapCodec<IrisModdedChunkGenerator> CODEC = RecordCodecBuilder.mapCodec((RecordCodecBuilder.Instance<IrisModdedChunkGenerator> instance) -> instance.group(
BiomeSource.CODEC.fieldOf("biome_source").forGetter((IrisModdedChunkGenerator generator) -> generator.serializedBiomeSource),
Codec.STRING.fieldOf("dimension").forGetter((IrisModdedChunkGenerator generator) -> generator.dimensionKey)
).apply(instance, IrisModdedChunkGenerator::new));
private static final AtomicInteger GEN_THREAD_SEQ = new AtomicInteger();
private static final boolean PARALLEL_CHUNK_SYSTEM = detectParallelChunkSystem();
private static volatile ExecutorService genPool = createGenPool();
public static void startGenPool() {
ExecutorService pool = genPool;
if (pool == null || pool.isShutdown()) {
genPool = createGenPool();
}
ModdedGenPool.start();
}
public static void shutdownGenPool() {
ExecutorService pool = genPool;
if (pool != null) {
pool.shutdownNow();
}
}
private static boolean detectParallelChunkSystem() {
String[] markers = {
"com.ishland.c2me.base.ModProperties",
"com.ishland.c2me.base.common.config.C2MEConfig",
"com.ishland.c2me.opts.chunkio.ModProperties",
"ca.spottedleaf.moonrise.common.util.MoonriseCommon"
};
for (String marker : markers) {
try {
Class.forName(marker, false, IrisModdedChunkGenerator.class.getClassLoader());
return true;
} catch (Throwable ignored) {
}
}
return false;
}
private static ExecutorService createGenPool() {
int threads = Math.max(2, Runtime.getRuntime().availableProcessors());
ThreadPoolExecutor pool = new ThreadPoolExecutor(
threads, threads, 30L, TimeUnit.SECONDS,
new LinkedBlockingQueue<>(),
runnable -> {
Thread thread = new Thread(runnable, "Iris ModGen-" + GEN_THREAD_SEQ.incrementAndGet());
thread.setDaemon(true);
return thread;
});
pool.allowCoreThreadTimeOut(true);
return pool;
ModdedGenPool.shutdown();
}
private final String dimensionKey;
private final String defaultPack;
private final String defaultDimensionKey;
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<SpawnTableKey, WeightedList<MobSpawnSettings.SpawnerData>> mergedSpawnTables = new ConcurrentHashMap<>();
private final ConcurrentHashMap<NativeStructureStartKey, Integer> worldCheckStructureShifts = new ConcurrentHashMap<>();
final IrisModdedBiomeSource structureBiomeSource;
private final ModdedEngineBinding<Engine> engineBinding = new ModdedEngineBinding<>(60L, TimeUnit.SECONDS);
private final ModdedNativeStructureStage nativeStructures = new ModdedNativeStructureStage(this);
private final ModdedSpawnTableMerger spawnTables = new ModdedSpawnTableMerger(this);
private final AtomicBoolean announced = new AtomicBoolean(false);
private volatile boolean vanillaSpawnBiomesInitialized;
private volatile boolean unloading;
private volatile Engine engine;
private volatile String activePack;
@@ -199,8 +125,7 @@ public final class IrisModdedChunkGenerator extends ChunkGenerator {
private volatile long seedOverride = Long.MIN_VALUE;
private volatile long lastChunkGenAt = 0L;
private volatile Set<String> configuredStructureBiomeKeys;
private volatile ConfiguredPack configuredPack;
private volatile StructureStepCache structureStepCache;
private volatile ModdedDimensionMetadata.ConfiguredPack configuredPack;
public IrisModdedChunkGenerator(BiomeSource biomeSource, String dimensionKey) {
this(biomeSource, dimensionKey, new IrisModdedBiomeSource(biomeSource));
@@ -262,8 +187,8 @@ public final class IrisModdedChunkGenerator extends ChunkGenerator {
this.engineBinding.complete(replacement);
this.announced.set(false);
this.structureBiomeSource.clearCaches();
this.worldCheckStructureShifts.clear();
resetVanillaSpawnBiomes();
this.nativeStructures.clearWorldCheckStructureShifts();
this.spawnTables.resetVanillaSpawnBiomes();
}
public synchronized void unbindEngine() {
@@ -287,8 +212,8 @@ public final class IrisModdedChunkGenerator extends ChunkGenerator {
this.engineBinding.reset();
this.announced.set(false);
this.structureBiomeSource.clearCaches();
this.worldCheckStructureShifts.clear();
resetVanillaSpawnBiomes();
this.nativeStructures.clearWorldCheckStructureShifts();
this.spawnTables.resetVanillaSpawnBiomes();
}
private void applyUnboundConfiguration(String pack, String packDimensionKey, long seed) {
@@ -301,8 +226,8 @@ public final class IrisModdedChunkGenerator extends ChunkGenerator {
this.engineBinding.reset();
this.announced.set(false);
this.structureBiomeSource.clearCaches();
this.worldCheckStructureShifts.clear();
resetVanillaSpawnBiomes();
this.nativeStructures.clearWorldCheckStructureShifts();
this.spawnTables.resetVanillaSpawnBiomes();
}
public synchronized void resetToDefault() {
@@ -335,9 +260,10 @@ public final class IrisModdedChunkGenerator extends ChunkGenerator {
Engine current = engine();
try (GenerationSessionLease lease = requireGenerationLease(current, "modded_structure_locate");
IrisContext.Scope ignored = IrisContext.open(current, lease.sessionId(), null)) {
Pair<BlockPos, Holder<Structure>> irisPlaced = findNearestIrisStructure(
Pair<BlockPos, Holder<Structure>> irisPlaced = nativeStructures.findNearestIrisStructure(
level, holders, pos, Math.max(1, radius), findUnexplored, current);
HolderSet<Structure> reachable = filterReachableNativeStructures(level, holders, current);
HolderSet<Structure> reachable = nativeStructures.filterReachableNativeStructures(
level, holders, current);
Pair<BlockPos, Holder<Structure>> nativeLocated = reachable.size() == 0
? null
: super.findNearestMapStructure(level, reachable, pos, radius, findUnexplored);
@@ -349,67 +275,6 @@ public final class IrisModdedChunkGenerator extends ChunkGenerator {
return structure != null && structureBiomeSource.isStructureReachable(structure);
}
private Pair<BlockPos, Holder<Structure>> findNearestIrisStructure(ServerLevel level,
HolderSet<Structure> holders,
BlockPos pos, int radius, boolean findUnexplored,
Engine current) {
if (findUnexplored) {
return null;
}
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) {
throw new IllegalStateException("Native structure locate received an unregistered structure holder");
}
String structureId = id.toString();
if (!IrisStructureLocator.isPlaced(current, structureId)) {
continue;
}
IrisStructureLocator.LocateResult result = IrisStructureLocator.locate(
current, structureId, pos.getX(), pos.getZ(), radius);
if (result.status() == IrisStructureLocator.LocateStatus.SEARCH_LIMIT_REACHED) {
throw new IllegalStateException("Iris structure locate reached its safety limit for "
+ structureId + " within " + radius + " chunks");
}
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) {
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());
if (id == null) {
throw new IllegalStateException("Native structure filtering received an unregistered structure holder");
}
String key = id.toString();
IrisNativeStructureDecision decision = NativeStructureGenerationPolicy.resolve(current,
key, NativeStructurePostProcessor.isUndergroundStep(holder.value().step()));
if (!decision.generate() || !structureBiomeSource.isStructureReachable(holder)) {
continue;
}
kept.add(holder);
}
return kept.size() == holders.size() ? holders : HolderSet.direct(kept);
}
private ServerLevel boundLevel() {
MinecraftServer server = ModdedEngineBootstrap.currentServer();
if (server == null) {
@@ -423,7 +288,7 @@ public final class IrisModdedChunkGenerator extends ChunkGenerator {
return null;
}
private Engine engine() {
Engine engine() {
requireBindingAllowed();
Engine cached = engine;
requireCompletedShutdown(cached);
@@ -516,7 +381,7 @@ public final class IrisModdedChunkGenerator extends ChunkGenerator {
+ "and deny individual structures through importedStructures.disabled");
}
private Engine engineOrNull() {
Engine engineOrNull() {
requireBindingAllowed();
Engine cached = engine;
requireCompletedShutdown(cached);
@@ -607,7 +472,7 @@ public final class IrisModdedChunkGenerator extends ChunkGenerator {
if (current != null && !current.isClosed() && !current.isClosing()) {
try (GenerationSessionLease lease = current.acquireGenerationLease("modded_configured_biome_keys");
IrisContext.Scope ignored = IrisContext.open(current, lease.sessionId(), null)) {
Set<String> resolved = collectConfiguredBiomeKeys(
Set<String> resolved = ModdedDimensionMetadata.collectConfiguredBiomeKeys(
current.getAllBiomes(), current.getDimension().getLoadKey());
configuredStructureBiomeKeys = resolved;
return resolved;
@@ -617,15 +482,16 @@ public final class IrisModdedChunkGenerator extends ChunkGenerator {
}
}
}
ConfiguredPack configured = configuredPack();
Set<String> resolved = collectConfiguredBiomeKeys(configured.dimension(), configured.data());
ModdedDimensionMetadata.ConfiguredPack configured = configuredPack();
Set<String> resolved = ModdedDimensionMetadata.collectConfiguredBiomeKeys(
configured.dimension(), configured.data());
configuredStructureBiomeKeys = resolved;
return resolved;
}
}
private ConfiguredPack configuredPack() {
ConfiguredPack cached = configuredPack;
private ModdedDimensionMetadata.ConfiguredPack configuredPack() {
ModdedDimensionMetadata.ConfiguredPack cached = configuredPack;
if (cached != null) {
return cached;
}
@@ -641,60 +507,13 @@ public final class IrisModdedChunkGenerator extends ChunkGenerator {
throw new IllegalStateException("Iris dimension '" + activeDimensionKey
+ "' missing from pack " + packDirectory.getAbsolutePath());
}
ConfiguredPack resolved = new ConfiguredPack(data, dimension, dimensionMetadata(dimension));
ModdedDimensionMetadata.ConfiguredPack resolved = new ModdedDimensionMetadata.ConfiguredPack(
data, dimension, ModdedDimensionMetadata.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) {
LinkedHashSet<String> keys = new LinkedHashSet<>(
collectConfiguredBiomeKeys(dimension.getReachableBiomes(() -> data), dimension.getLoadKey()));
for (IrisRegion region : dimension.getAllRegions(() -> data)) {
if (region == null) {
continue;
}
if (!region.getSeaBiomes().isEmpty()) {
keys.add("minecraft:the_void");
}
if (!region.getShoreBiomes().isEmpty()) {
keys.add("minecraft:beach");
}
}
return Set.copyOf(keys);
}
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.getStructureDerivativeKey());
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() {
return dimensionKey;
}
@@ -715,8 +534,8 @@ public final class IrisModdedChunkGenerator extends ChunkGenerator {
public void onHotload() {
configuredStructureBiomeKeys = null;
structureBiomeSource.clearCaches();
worldCheckStructureShifts.clear();
resetVanillaSpawnBiomes();
nativeStructures.clearWorldCheckStructureShifts();
spawnTables.resetVanillaSpawnBiomes();
}
@Override
@@ -730,8 +549,8 @@ public final class IrisModdedChunkGenerator extends ChunkGenerator {
}
Registry<Biome> registry = structureManager.registryAccess().lookupOrThrow(Registries.BIOME);
initializeVanillaSpawnBiomes(registry);
Holder<Biome> vanillaSpawnBiome = vanillaSpawnBiomes.get(biome.value());
spawnTables.initializeVanillaSpawnBiomes(registry);
Holder<Biome> vanillaSpawnBiome = spawnTables.vanillaSpawnBiome(biome.value());
if (vanillaSpawnBiome == null) {
return explicitSpawns;
}
@@ -744,57 +563,7 @@ public final class IrisModdedChunkGenerator extends ChunkGenerator {
return explicitSpawns;
}
SpawnTableKey key = new SpawnTableKey(biome.value(), category);
return mergedSpawnTables.computeIfAbsent(key, ignored -> NativeSpawnTableMerger.merge(vanillaSpawns, explicitSpawns));
}
private synchronized void initializeVanillaSpawnBiomes(Registry<Biome> registry) {
if (vanillaSpawnBiomesInitialized) {
return;
}
Engine current = engineOrNull();
if (current == null) {
return;
}
try (GenerationSessionLease lease = requireGenerationLease(current, "modded_spawn_biomes");
IrisContext.Scope ignored = IrisContext.open(current, lease.sessionId(), null)) {
String namespace = current.getDimension().getLoadKey().toLowerCase(Locale.ROOT);
for (IrisBiome irisBiome : current.getDimension().getReachableBiomes(current)) {
if (irisBiome == null || !irisBiome.isCustom()) {
continue;
}
Holder<Biome> vanillaHolder = resolveBiomeHolder(registry, irisBiome.getVanillaDerivativeKey());
if (vanillaHolder == null) {
continue;
}
for (IrisBiomeCustom customBiome : irisBiome.getCustomDerivitives()) {
Holder<Biome> customHolder = resolveBiomeHolder(registry, namespace + ":" + customBiome.getId());
if (customHolder != null) {
vanillaSpawnBiomes.putIfAbsent(customHolder.value(), vanillaHolder);
}
}
}
vanillaSpawnBiomesInitialized = true;
}
}
private Holder<Biome> resolveBiomeHolder(Registry<Biome> registry, String key) {
if (key == null || key.isBlank()) {
return null;
}
Identifier identifier = Identifier.tryParse(key);
if (identifier == null) {
return null;
}
Optional<Holder.Reference<Biome>> reference = registry.get(identifier);
return reference.<Holder<Biome>>map((Holder.Reference<Biome> value) -> value).orElse(null);
}
private synchronized void resetVanillaSpawnBiomes() {
vanillaSpawnBiomes.clear();
mergedSpawnTables.clear();
vanillaSpawnBiomesInitialized = false;
return spawnTables.mergedSpawnTable(biome.value(), category, vanillaSpawns, explicitSpawns);
}
@Override
@@ -817,13 +586,13 @@ public final class IrisModdedChunkGenerator extends ChunkGenerator {
PlatformBlockState air = IrisPlatforms.get().registries().air();
if (PARALLEL_CHUNK_SYSTEM) {
if (ModdedGenPool.parallelChunkSystem()) {
return CompletableFuture.completedFuture(
generateTerrain(chunk, generationEngine, pos, air));
}
return CompletableFuture.supplyAsync(
() -> generateTerrain(chunk, generationEngine, pos, air),
genPool);
ModdedGenPool.pool());
}
private ChunkAccess generateTerrain(ChunkAccess chunk, Engine generationEngine, ChunkPos pos,
@@ -944,7 +713,7 @@ public final class IrisModdedChunkGenerator extends ChunkGenerator {
Engine current = engine();
try (GenerationSessionLease lease = requireGenerationLease(current, "modded_biome_decoration");
IrisContext.Scope ignored = IrisContext.open(current, lease.sessionId(), null)) {
placeVanillaStructures(level, chunk, structureManager);
nativeStructures.placeVanillaStructures(level, chunk, structureManager);
}
}
@@ -967,7 +736,7 @@ public final class IrisModdedChunkGenerator extends ChunkGenerator {
this,
structureBiomeSource
));
adjustGeneratedStructures(
nativeStructures.adjustGeneratedStructures(
registryAccess, chunk, previousStarts, configuredStarts, current, templateManager);
}
}
@@ -981,269 +750,17 @@ public final class IrisModdedChunkGenerator extends ChunkGenerator {
}
}
private void adjustGeneratedStructures(RegistryAccess registryAccess, ChunkAccess chunk,
Map<Structure, StructureStart> previousStarts,
Map<Structure, NativeStructureStartPlan> configuredStarts,
Engine current,
StructureTemplateManager templateManager) {
Registry<Structure> registry = registryAccess.lookupOrThrow(Registries.STRUCTURE);
ChunkPos chunkPos = chunk.getPos();
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;
}
if (configuredStarts.containsKey(structure)) {
recordWorldCheckStructureShift(
configuredStarts.get(structure).source().getStructure(), start.getChunkPos(), 0);
continue;
}
Identifier id = registry.getKey(structure);
String structureId = id == null ? null : id.toString();
if (structureId == null) {
throw NativeStructureGenerationException.failure(
"resolution", null, chunkPos.x(), chunkPos.z());
}
boolean undergroundStep = NativeStructurePostProcessor.isUndergroundStep(structure.step());
IrisNativeStructureDecision decision;
try {
decision = NativeStructureGenerationPolicy.resolve(current,
structureId, undergroundStep);
} catch (Throwable error) {
throw NativeStructureGenerationException.failure(
"policy resolution", structureId, chunkPos.x(), chunkPos.z(), error);
}
if (!decision.generate()) {
chunk.setStartForStructure(structure, StructureStart.INVALID_START);
continue;
}
int offsetY;
try {
offsetY = NativeStructurePostProcessor.applyVerticalPlacement(
start,
structureId,
decision.yShift(),
getSeaLevel(),
chunk.getMinY(),
chunk.getMinY() + chunk.getHeight(),
undergroundStep,
decision.preserveSourceY(),
decision.yBand(),
(x, z) -> current.getHeight(x, z, true) + current.getMinHeight());
StructureStart wrapped = NativeStructureReferenceEnvelope.wrap(
start, structure, start.getReferences(), templateManager,
NativeStructurePostProcessor.resolveNativeTerrain(start, decision.terrain()));
chunk.setStartForStructure(structure, wrapped);
} catch (Throwable error) {
throw NativeStructureGenerationException.failure(
"vertical adjustment", structureId, chunkPos.x(), chunkPos.z(), error);
}
recordWorldCheckStructureShift(structureId, start.getChunkPos(), offsetY);
}
}
private void placeVanillaStructures(WorldGenLevel world, ChunkAccess chunk, StructureManager structureManager) {
if (!structureManager.shouldGenerateStructures()) {
ChunkPos disabledChunk = chunk.getPos();
throw new IllegalStateException("Iris cannot generate native structures in chunk "
+ disabledChunk.x() + "," + disabledChunk.z()
+ " because generate-structures=false disables them outside the pack; set generate-structures=true, "
+ "restart the server, and deny individual structures through importedStructures.disabled");
}
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;
Engine current = engine();
List<NativePlacementGroup> placementGroups = new ArrayList<>();
List<StructureStart> nativeStarts = new ArrayList<>();
List<NativeStructurePostProcessor.VegetationTarget> vegetationTargets = new ArrayList<>();
List<NativeStructurePostProcessor.TerrainTarget> terrainTargets = 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();
if (structureId == null) {
throw NativeStructureGenerationException.failure(
"resolution", null, chunkPos.x(), chunkPos.z());
}
try {
IrisNativeStructureDecision sourceDecision = NativeStructureGenerationPolicy.resolve(current,
structureId, NativeStructurePostProcessor.isUndergroundStep(structure.step()));
List<StructureStart> starts = structureManager.startsForStructure(sectionPos, structure);
List<NativePlacement> resolvedPlacements = new ArrayList<>(starts.size());
for (StructureStart start : starts) {
NativeStructureStartPlan plan = NativeStructurePlacementPlanner.matchingPlan(
current, structureId, start.getChunkPos().x(), start.getChunkPos().z());
IrisNativeStructureDecision decision = plan == null
? sourceDecision : NativeStructurePlacementPlanner.decisionFor(plan);
if (!decision.generate()) {
continue;
}
resolvedPlacements.add(new NativePlacement(start, decision));
terrainTargets.add(new NativeStructurePostProcessor.TerrainTarget(
structureId, start,
NativeStructurePostProcessor.resolveNativeTerrain(
start, decision.terrain())));
if (plan == null || !plan.placement().isUnderground()) {
nativeStarts.add(start);
}
boolean clearEntireFootprint = NativeStructurePostProcessor
.shouldClearEntireVegetationFootprint(
structure.step(), decision.clearVegetation());
vegetationTargets.add(new NativeStructurePostProcessor.VegetationTarget(
start, clearEntireFootprint));
}
if (!resolvedPlacements.isEmpty()) {
placementGroups.add(new NativePlacementGroup(
structureId, index, step, List.copyOf(resolvedPlacements)));
}
} catch (Throwable error) {
throw NativeStructureGenerationException.failure(
"resolution", structureId, chunkPos.x(), chunkPos.z(), error);
}
index++;
}
}
try {
NativeStructurePostProcessor.prepareSurfaceStructures(
world, area, nativeStarts,
(x, z) -> current.getHeight(x, z, true) + current.getMinHeight());
} catch (Throwable error) {
throw NativeStructureGenerationException.failure(
"terrain integration", nativeStructureBatchContext(placementGroups),
chunkPos.x(), chunkPos.z(), error);
}
try {
NativeStructurePostProcessor.clearIntersectingVegetation(
world, chunk, area, vegetationTargets);
} catch (Throwable error) {
throw NativeStructureGenerationException.failure(
"vegetation cleanup", nativeStructureBatchContext(placementGroups),
chunkPos.x(), chunkPos.z(), error);
}
try {
NativeStructurePostProcessor.prepareTerrain(
world, area, terrainTargets, this::resolvePaletteBlock);
} catch (Throwable error) {
throw NativeStructureGenerationException.failure(
"terrain carving", nativeStructureBatchContext(placementGroups),
chunkPos.x(), chunkPos.z(), error);
}
for (NativePlacementGroup group : placementGroups) {
random.setFeatureSeed(decorationSeed, group.featureIndex(), group.step());
try {
for (NativePlacement placement : group.placements()) {
placeVanillaStructure(world, structureManager, random, area, chunkPos,
group.structureId(), placement.start(), placement.decision());
}
} catch (Throwable error) {
throw NativeStructureGenerationException.failure(
"placement", group.structureId(), chunkPos.x(), chunkPos.z(), error);
}
}
}
private static String nativeStructureBatchContext(List<NativePlacementGroup> placementGroups) {
if (placementGroups.isEmpty()) {
return "<no resolved native structures>";
}
StringBuilder context = new StringBuilder("[");
for (int i = 0; i < placementGroups.size(); i++) {
if (i > 0) {
context.append(", ");
}
context.append(placementGroups.get(i).structureId());
}
return context.append(']').toString();
}
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::resolvePaletteBlock,
(x, z) -> engine().getHeight(x, z, true) + engine().getMinHeight());
}
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 resolvePaletteBlock(IrisMaterialPalette palette, RNG rng,
int x, int y, int z) {
PlatformBlockState platformState = palette.get(rng, x, y, z, engine().getData());
if (platformState == null || !(platformState.nativeHandle() instanceof BlockState blockState)) {
throw new IllegalStateException("Configured native structure palette did not resolve a Minecraft block at "
+ x + "," + y + "," + z);
}
return blockState;
}
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);
return nativeStructures.worldCheckStructureShift(structureId, startChunk);
}
@Override
public void spawnOriginalMobs(WorldGenRegion region) {
Registry<Biome> registry = region.registryAccess().lookupOrThrow(Registries.BIOME);
initializeVanillaSpawnBiomes(registry);
spawnTables.initializeVanillaSpawnBiomes(registry);
ChunkPos center = region.getCenter();
Holder<Biome> visibleBiome = region.getBiome(center.getWorldPosition().atY(region.getMaxY()));
Holder<Biome> vanillaBiome = vanillaSpawnBiomes.get(visibleBiome.value());
Holder<Biome> vanillaBiome = spawnTables.vanillaSpawnBiome(visibleBiome.value());
WorldgenRandom random = new WorldgenRandom(new LegacyRandomSource(RandomSupport.generateUniqueSeed()));
random.setDecorationSeed(region.getSeed(), center.getMinBlockX(), center.getMinBlockZ());
NaturalSpawner.spawnMobsForChunkGeneration(
@@ -1276,13 +793,7 @@ public final class IrisModdedChunkGenerator extends ChunkGenerator {
@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));
return ModdedDimensionMetadata.clampSpawnHeight(heightAccessor.getMinY(), heightAccessor.getHeight());
}
@Override
@@ -1320,7 +831,7 @@ public final class IrisModdedChunkGenerator extends ChunkGenerator {
}
}
private GenerationSessionLease requireGenerationLease(Engine current, String operation) {
GenerationSessionLease requireGenerationLease(Engine current, String operation) {
try {
return current.acquireGenerationLease(operation);
} catch (GenerationSessionException exception) {
@@ -1333,80 +844,4 @@ public final class IrisModdedChunkGenerator extends ChunkGenerator {
info.add("Iris dimension: " + dimensionKey);
}
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 NativePlacement(StructureStart start, IrisNativeStructureDecision decision) {
}
private record NativePlacementGroup(String structureId, int featureIndex, int step,
List<NativePlacement> placements) {
}
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 throwIfFailed(String dimensionKey) {
CompletableFuture<T> current = future;
if (!current.isCompletedExceptionally()) {
return;
}
try {
current.join();
} catch (CompletionException error) {
throw new IllegalStateException("Iris generator '" + dimensionKey + "' failed to bind",
error.getCause());
}
}
void reset() {
future = new CompletableFuture<>();
}
}
}
@@ -45,6 +45,7 @@ import java.util.HashSet;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.stream.Collectors;
public final class ModdedBlockResolution {
@@ -86,6 +87,8 @@ public final class ModdedBlockResolution {
"acacia_leaves", "birch_leaves", "dark_oak_leaves", "jungle_leaves", "oak_leaves", "spruce_leaves");
private static final BlockState AIR = Blocks.AIR.defaultBlockState();
private static final UnresolvedKeyLog UNRESOLVED = new UnresolvedKeyLog("Iris modded block resolution", 30_000L);
private static final int REPORTED_FAILURE_KEYS_MAX = 256;
private static final Set<String> REPORTED_FAILURE_KEYS = ConcurrentHashMap.newKeySet();
private ModdedBlockResolution() {
}
@@ -152,6 +155,17 @@ public final class ModdedBlockResolution {
return map;
}
private static void reportResolveFailure(String key, Throwable error) {
String failureKey = key == null ? "<null>" : key;
if (!REPORTED_FAILURE_KEYS.add(failureKey)) {
return;
}
if (REPORTED_FAILURE_KEYS.size() > REPORTED_FAILURE_KEYS_MAX) {
REPORTED_FAILURE_KEYS.clear();
}
IrisLogging.reportError("Iris block data '" + failureKey + "' failed to resolve", error);
}
private static void warnUnresolved(String key, String message) {
if (UNRESOLVED.firstOccurrence(key)) {
IrisLogging.warn(message);
@@ -239,7 +253,7 @@ public final class ModdedBlockResolution {
return bdx;
} catch (Throwable e) {
e.printStackTrace();
reportResolveFailure(bdxf, e);
if (warn) {
warnUnresolved(bdxf, "Unknown Block Data '" + bdxf + "'");
}
@@ -38,6 +38,7 @@ public final class ModdedBlockState implements PlatformBlockState {
private final String key;
private final String namespace;
private final String deferredPlacementKey;
private volatile String materialKey;
private volatile Boolean air;
private volatile Boolean solid;
private volatile Boolean occluding;
@@ -148,6 +149,17 @@ public final class ModdedBlockState implements PlatformBlockState {
return namespace;
}
@Override
public String materialKey() {
String cached = materialKey;
if (cached == null) {
int bracket = key.indexOf('[');
cached = bracket < 0 ? key : key.substring(0, bracket);
materialKey = cached;
}
return cached;
}
@Override
public boolean isAir() {
Boolean cached = air;
@@ -0,0 +1,98 @@
/*
* 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.object.IrisBiome;
import art.arcane.iris.engine.object.IrisBiomeCustom;
import art.arcane.iris.engine.object.IrisDimension;
import art.arcane.iris.engine.object.IrisRegion;
import net.minecraft.resources.Identifier;
import java.util.LinkedHashSet;
import java.util.Locale;
import java.util.Set;
final class ModdedDimensionMetadata {
private ModdedDimensionMetadata() {
}
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) {
LinkedHashSet<String> keys = new LinkedHashSet<>(
collectConfiguredBiomeKeys(dimension.getReachableBiomes(() -> data), dimension.getLoadKey()));
for (IrisRegion region : dimension.getAllRegions(() -> data)) {
if (region == null) {
continue;
}
if (!region.getSeaBiomes().isEmpty()) {
keys.add("minecraft:the_void");
}
if (!region.getShoreBiomes().isEmpty()) {
keys.add("minecraft:beach");
}
}
return Set.copyOf(keys);
}
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.getStructureDerivativeKey());
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);
}
static int clampSpawnHeight(int minY, int height) {
int minimum = minY + 1;
int maximum = minY + height - 2;
return Math.max(minimum, Math.min(maximum, 96));
}
record DimensionMetadata(int minY, int maxY, int seaLevel) {
int depth() {
return maxY - minY;
}
}
record ConfiguredPack(IrisData data, IrisDimension dimension, DimensionMetadata metadata) {
}
}
@@ -50,8 +50,12 @@ public final class ModdedDimensionRegistryStore {
}
static List<PersistentDimension> load(Path file) {
return contents(file).dimensions();
}
private static Contents contents(Path file) {
if (!Files.isRegularFile(file)) {
return new ArrayList<>();
return new Contents(new ArrayList<>(), new ArrayList<>());
}
try {
JSONObject root = new JSONObject(Files.readString(file, StandardCharsets.UTF_8));
@@ -60,7 +64,9 @@ public final class ModdedDimensionRegistryStore {
throw new IllegalArgumentException("registry root has no dimensions array");
}
Map<String, PersistentDimension> deduplicated = new LinkedHashMap<>();
List<Object> unparsed = new ArrayList<>();
for (int index = 0; index < entries.length(); index++) {
Object raw = entries.opt(index);
try {
JSONObject entry = entries.getJSONObject(index);
String id = required(entry, "id", index, file);
@@ -72,14 +78,18 @@ public final class ModdedDimensionRegistryStore {
PersistentDimension previous = deduplicated.putIfAbsent(
id, new PersistentDimension(id, pack, dimension, entry.getLong("seed")));
if (previous != null) {
throw new IllegalArgumentException("duplicate id '" + id + "'");
LOGGER.warn("Iris persistent dimension registry entry {} in {} duplicates id '{}'; keeping the first",
index, file, id);
}
} catch (RuntimeException invalidEntry) {
LOGGER.error("Iris persistent dimension registry entry {} in {} is invalid; skipping only that entry",
index, file, invalidEntry);
if (raw != null) {
unparsed.add(raw);
}
LOGGER.warn("Iris persistent dimension registry entry {} in {} is invalid ({}); kept verbatim: {}",
index, file, invalidEntry.getMessage(), raw);
}
}
return new ArrayList<>(deduplicated.values());
return new Contents(new ArrayList<>(deduplicated.values()), unparsed);
} catch (RuntimeException | IOException e) {
throw new IllegalStateException("Iris persistent dimension registry at " + file
+ " could not be read; refusing to discard persistent worlds", e);
@@ -91,15 +101,19 @@ public final class ModdedDimensionRegistryStore {
}
public static synchronized void put(MinecraftServer server, PersistentDimension dimension) {
Map<String, PersistentDimension> current = index(load(server));
Path file = storeFile(server);
Contents contents = contents(file);
Map<String, PersistentDimension> current = index(contents.dimensions());
current.put(dimension.id(), dimension);
write(server, new ArrayList<>(current.values()));
write(file, new ArrayList<>(current.values()), contents.unparsed());
}
public static synchronized void remove(MinecraftServer server, String id) {
Map<String, PersistentDimension> current = index(load(server));
Path file = storeFile(server);
Contents contents = contents(file);
Map<String, PersistentDimension> current = index(contents.dimensions());
if (current.remove(id) != null) {
write(server, new ArrayList<>(current.values()));
write(file, new ArrayList<>(current.values()), contents.unparsed());
}
}
@@ -119,11 +133,11 @@ public final class ModdedDimensionRegistryStore {
return value;
}
private static void write(MinecraftServer server, List<PersistentDimension> dimensions) {
write(storeFile(server), dimensions);
static void write(Path file, List<PersistentDimension> dimensions) {
write(file, dimensions, List.of());
}
static void write(Path file, List<PersistentDimension> dimensions) {
private static void write(Path file, List<PersistentDimension> dimensions, List<Object> unparsed) {
JSONArray entries = new JSONArray();
for (PersistentDimension dimension : dimensions) {
JSONObject entry = new JSONObject();
@@ -133,6 +147,9 @@ public final class ModdedDimensionRegistryStore {
entry.put("seed", dimension.seed());
entries.put(entry);
}
for (Object entry : unparsed) {
entries.put(entry);
}
JSONObject root = new JSONObject();
root.put("dimensions", entries);
Path temp = file.resolveSibling(FILE_NAME + ".tmp");
@@ -167,4 +184,7 @@ public final class ModdedDimensionRegistryStore {
public record PersistentDimension(String id, String pack, String dimension, long seed) {
}
private record Contents(List<PersistentDimension> dimensions, List<Object> unparsed) {
}
}
@@ -0,0 +1,77 @@
/*
* 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 java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionException;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
final class ModdedEngineBinding<T> {
private final long timeout;
private final TimeUnit timeoutUnit;
private volatile CompletableFuture<T> future = new CompletableFuture<>();
ModdedEngineBinding(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 throwIfFailed(String dimensionKey) {
CompletableFuture<T> current = future;
if (!current.isCompletedExceptionally()) {
return;
}
try {
current.join();
} catch (CompletionException error) {
throw new IllegalStateException("Iris generator '" + dimensionKey + "' failed to bind",
error.getCause());
}
}
void reset() {
future = new CompletableFuture<>();
}
}
@@ -342,7 +342,7 @@ public final class ModdedEntitySpawner {
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 particleY = entity.getY() + 0.25 + addition - subtraction + 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();
@@ -55,6 +55,7 @@ import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.UUID;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.function.Consumer;
import java.util.stream.Stream;
@@ -63,6 +64,7 @@ public final class ModdedForcedDatapack {
private static final String PACK_ID = "iris_worldgen";
private static final String PACK_FOLDER = "iris";
private static final Object LOCK = new Object();
private static final AtomicBoolean LOADED = new AtomicBoolean(false);
private ModdedForcedDatapack() {
}
@@ -71,9 +73,26 @@ public final class ModdedForcedDatapack {
return (Consumer<Pack> consumer) -> {
Pack pack = buildPack();
consumer.accept(pack);
LOADED.set(true);
};
}
public static void verifyInjected() {
if (LOADED.get()) {
return;
}
Path packsRoot = packsRoot();
File[] packs = packsRoot.toFile().listFiles(File::isDirectory);
if (packs == null || packs.length == 0) {
return;
}
LOGGER.error("===============================================================");
LOGGER.error("Iris forced datapack '{}' was never loaded by this server.", PACK_ID);
LOGGER.error("{} installed pack(s) at {} contributed no dimension types or custom biomes.", packs.length, packsRoot);
LOGGER.error("Datapack source injection failed for this loader (mixin/event not applied), so world creation will fail and restarting will not fix it.");
LOGGER.error("===============================================================");
}
public static Path datapackRoot() {
return ModdedEngineBootstrap.loader().configDir().resolve("irisworldgen").resolve("generated").resolve("datapack");
}
@@ -310,7 +329,9 @@ public final class ModdedForcedDatapack {
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."));
+ packDimensionKey + "' is not loaded. Restart the server so the forced Iris datapack registers it before creating the world."
+ (LOADED.get() ? "" : " The forced Iris datapack has not been loaded by this server at all"
+ " (datapack source injection failed; see the Iris boot ERROR), so a restart alone will not register it.")));
}
private static void writeWorldPreset(KList<File> folders, String packName, String dimensionKey,
@@ -0,0 +1,87 @@
/*
* 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 java.util.concurrent.ExecutorService;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
final class ModdedGenPool {
private static final AtomicInteger GEN_THREAD_SEQ = new AtomicInteger();
private static final boolean PARALLEL_CHUNK_SYSTEM = detectParallelChunkSystem();
private static volatile ExecutorService genPool = createGenPool();
private ModdedGenPool() {
}
static boolean parallelChunkSystem() {
return PARALLEL_CHUNK_SYSTEM;
}
static ExecutorService pool() {
return genPool;
}
static void start() {
ExecutorService pool = genPool;
if (pool == null || pool.isShutdown()) {
genPool = createGenPool();
}
}
static void shutdown() {
ExecutorService pool = genPool;
if (pool != null) {
pool.shutdownNow();
}
}
private static boolean detectParallelChunkSystem() {
String[] markers = {
"com.ishland.c2me.base.ModProperties",
"com.ishland.c2me.base.common.config.C2MEConfig",
"com.ishland.c2me.opts.chunkio.ModProperties",
"ca.spottedleaf.moonrise.common.util.MoonriseCommon"
};
for (String marker : markers) {
try {
Class.forName(marker, false, IrisModdedChunkGenerator.class.getClassLoader());
return true;
} catch (Throwable ignored) {
}
}
return false;
}
private static ExecutorService createGenPool() {
int threads = Math.max(2, Runtime.getRuntime().availableProcessors());
ThreadPoolExecutor pool = new ThreadPoolExecutor(
threads, threads, 30L, TimeUnit.SECONDS,
new LinkedBlockingQueue<>(),
runnable -> {
Thread thread = new Thread(runnable, "Iris ModGen-" + GEN_THREAD_SEQ.incrementAndGet());
thread.setDaemon(true);
return thread;
});
pool.allowCoreThreadTimeOut(true);
return pool;
}
}
@@ -43,10 +43,11 @@ public final class ModdedIrisLog {
public static void debug(String message) {
if (!debugEnabled()) {
LOGGER.debug(clean(message));
return;
}
LOGGER.debug(clean(message));
LOGGER.info("[Iris/DEBUG] " + clean(message));
}
public static void info(String message) {
@@ -267,15 +267,21 @@ public final class ModdedLootApplier {
for (int i = 0; i < container.getContainerSize() && !stack.isEmpty(); i++) {
ItemStack existing = container.getItem(i);
if (existing.isEmpty()) {
container.setItem(i, stack);
return;
container.setItem(i, stack.split(container.getMaxStackSize(stack)));
continue;
}
if (ItemStack.isSameItemSameComponents(existing, stack) && existing.getCount() < existing.getMaxStackSize()) {
int move = Math.min(stack.getCount(), existing.getMaxStackSize() - existing.getCount());
existing.grow(move);
stack.shrink(move);
if (ItemStack.isSameItemSameComponents(existing, stack)) {
int limit = container.getMaxStackSize(existing);
if (existing.getCount() < limit) {
int move = Math.min(stack.getCount(), limit - existing.getCount());
existing.grow(move);
stack.shrink(move);
}
}
}
if (!stack.isEmpty()) {
IrisLogging.debug("Iris loot: container full, dropped " + stack.getCount() + "x " + stack.getItem());
}
}
private static void scramble(Container container, RNG rng) {
@@ -0,0 +1,439 @@
/*
* 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.framework.IrisStructureLocator;
import art.arcane.iris.engine.framework.NativeStructureGenerationPolicy;
import art.arcane.iris.engine.framework.NativeStructurePlacementPlanner;
import art.arcane.iris.engine.framework.NativeStructureStartPlan;
import art.arcane.iris.engine.object.IrisMaterialPalette;
import art.arcane.iris.engine.object.IrisNativeStructureDecision;
import art.arcane.iris.nativegen.NativeStructureGenerationException;
import art.arcane.iris.nativegen.NativeStructurePostProcessor;
import art.arcane.iris.nativegen.NativeStructureReferenceEnvelope;
import art.arcane.iris.nativegen.NativeStructureSurfaceFitter;
import art.arcane.iris.nativegen.NativeStructureTerrainIntegrator;
import art.arcane.iris.nativegen.NativeStructureVegetationClearer;
import art.arcane.iris.nativegen.NativeStructureVerticalPlacer;
import art.arcane.iris.nativegen.WorldgenTerrainHeightmaps;
import art.arcane.iris.spi.PlatformBlockState;
import art.arcane.volmlib.util.math.RNG;
import com.mojang.datafixers.util.Pair;
import net.minecraft.core.BlockPos;
import net.minecraft.core.Holder;
import net.minecraft.core.HolderSet;
import net.minecraft.core.Registry;
import net.minecraft.core.RegistryAccess;
import net.minecraft.core.SectionPos;
import net.minecraft.core.registries.Registries;
import net.minecraft.resources.Identifier;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.world.level.ChunkPos;
import net.minecraft.world.level.StructureManager;
import net.minecraft.world.level.WorldGenLevel;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.chunk.ChunkAccess;
import net.minecraft.world.level.levelgen.GenerationStep;
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.structure.BoundingBox;
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.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.IntBinaryOperator;
/**
* Native (vanilla registry) structure stage for {@link IrisModdedChunkGenerator}. The generator keeps the
* {@link net.minecraft.world.level.chunk.ChunkGenerator} overrides because they issue {@code super} calls;
* everything they do beyond that lives here.
*/
final class ModdedNativeStructureStage {
private static final int WORLD_CHECK_SHIFT_RECORD_LIMIT = 4096;
private static final boolean WORLD_CHECK_ENABLED = Boolean.getBoolean("iris.worldcheck");
private final IrisModdedChunkGenerator generator;
private final ConcurrentHashMap<NativeStructureStartKey, Integer> worldCheckStructureShifts = new ConcurrentHashMap<>();
private volatile StructureStepCache structureStepCache;
ModdedNativeStructureStage(IrisModdedChunkGenerator generator) {
this.generator = generator;
}
Pair<BlockPos, Holder<Structure>> findNearestIrisStructure(ServerLevel level,
HolderSet<Structure> holders,
BlockPos pos, int radius, boolean findUnexplored,
Engine current) {
if (findUnexplored) {
return null;
}
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) {
throw new IllegalStateException("Native structure locate received an unregistered structure holder");
}
String structureId = id.toString();
if (!IrisStructureLocator.isPlaced(current, structureId)) {
continue;
}
IrisStructureLocator.LocateResult result = IrisStructureLocator.locate(
current, structureId, pos.getX(), pos.getZ(), radius);
if (result.status() == IrisStructureLocator.LocateStatus.SEARCH_LIMIT_REACHED) {
throw new IllegalStateException("Iris structure locate reached its safety limit for "
+ structureId + " within " + radius + " chunks");
}
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);
}
HolderSet<Structure> filterReachableNativeStructures(ServerLevel level, HolderSet<Structure> holders,
Engine current) {
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());
if (id == null) {
throw new IllegalStateException("Native structure filtering received an unregistered structure holder");
}
String key = id.toString();
IrisNativeStructureDecision decision = NativeStructureGenerationPolicy.resolve(current,
key, NativeStructureVegetationClearer.isUndergroundStep(holder.value().step()));
if (!decision.generate() || !generator.structureBiomeSource.isStructureReachable(holder)) {
continue;
}
kept.add(holder);
}
return kept.size() == holders.size() ? holders : HolderSet.direct(kept);
}
void adjustGeneratedStructures(RegistryAccess registryAccess, ChunkAccess chunk,
Map<Structure, StructureStart> previousStarts,
Map<Structure, NativeStructureStartPlan> configuredStarts,
Engine current,
StructureTemplateManager templateManager) {
Registry<Structure> registry = registryAccess.lookupOrThrow(Registries.STRUCTURE);
ChunkPos chunkPos = chunk.getPos();
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;
}
if (configuredStarts.containsKey(structure)) {
recordWorldCheckStructureShift(
configuredStarts.get(structure).source().getStructure(), start.getChunkPos(), 0);
continue;
}
Identifier id = registry.getKey(structure);
String structureId = id == null ? null : id.toString();
if (structureId == null) {
throw NativeStructureGenerationException.failure(
"resolution", null, chunkPos.x(), chunkPos.z());
}
boolean undergroundStep = NativeStructureVegetationClearer.isUndergroundStep(structure.step());
IrisNativeStructureDecision decision;
try {
decision = NativeStructureGenerationPolicy.resolve(current,
structureId, undergroundStep);
} catch (Throwable error) {
throw NativeStructureGenerationException.failure(
"policy resolution", structureId, chunkPos.x(), chunkPos.z(), error);
}
if (!decision.generate()) {
chunk.setStartForStructure(structure, StructureStart.INVALID_START);
continue;
}
int offsetY;
try {
offsetY = NativeStructureVerticalPlacer.applyVerticalPlacement(
start,
structureId,
decision.yShift(),
generator.getSeaLevel(),
chunk.getMinY(),
chunk.getMinY() + chunk.getHeight(),
undergroundStep,
decision.preserveSourceY(),
decision.yBand(),
(x, z) -> current.getHeight(x, z, true) + current.getMinHeight());
StructureStart wrapped = NativeStructureReferenceEnvelope.wrap(
start, structure, start.getReferences(), templateManager,
NativeStructureTerrainIntegrator.resolveNativeTerrain(start, decision.terrain()));
chunk.setStartForStructure(structure, wrapped);
} catch (Throwable error) {
throw NativeStructureGenerationException.failure(
"vertical adjustment", structureId, chunkPos.x(), chunkPos.z(), error);
}
recordWorldCheckStructureShift(structureId, start.getChunkPos(), offsetY);
}
}
void placeVanillaStructures(WorldGenLevel world, ChunkAccess chunk, StructureManager structureManager) {
if (!structureManager.shouldGenerateStructures()) {
ChunkPos disabledChunk = chunk.getPos();
throw new IllegalStateException("Iris cannot generate native structures in chunk "
+ disabledChunk.x() + "," + disabledChunk.z()
+ " because generate-structures=false disables them outside the pack; set generate-structures=true, "
+ "restart the server, and deny individual structures through importedStructures.disabled");
}
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;
Engine current = generator.engine();
List<NativePlacementGroup> placementGroups = new ArrayList<>();
List<StructureStart> heightmapStarts = new ArrayList<>();
List<StructureStart> nativeStarts = new ArrayList<>();
List<NativeStructureVegetationClearer.VegetationTarget> vegetationTargets = new ArrayList<>();
List<NativeStructureTerrainIntegrator.TerrainTarget> terrainTargets = new ArrayList<>();
for (int step = 0; step < steps; step++) {
int index = 0;
for (Structure structure : byStep.get(step)) {
Identifier id = registry.getKey(structure);
String structureId = id == null ? null : id.toString();
if (structureId == null) {
throw NativeStructureGenerationException.failure(
"resolution", null, chunkPos.x(), chunkPos.z());
}
try {
IrisNativeStructureDecision sourceDecision = NativeStructureGenerationPolicy.resolve(current,
structureId, NativeStructureVegetationClearer.isUndergroundStep(structure.step()));
List<StructureStart> starts = structureManager.startsForStructure(sectionPos, structure);
List<NativePlacement> resolvedPlacements = new ArrayList<>(starts.size());
for (StructureStart start : starts) {
NativeStructureStartPlan plan = NativeStructurePlacementPlanner.matchingPlan(
current, structureId, start.getChunkPos().x(), start.getChunkPos().z());
IrisNativeStructureDecision decision = plan == null
? sourceDecision : NativeStructurePlacementPlanner.decisionFor(plan);
if (!decision.generate()) {
continue;
}
resolvedPlacements.add(new NativePlacement(start, decision));
heightmapStarts.add(start);
terrainTargets.add(new NativeStructureTerrainIntegrator.TerrainTarget(
structureId, start,
NativeStructureTerrainIntegrator.resolveNativeTerrain(
start, decision.terrain())));
if (plan == null || !plan.placement().isUnderground()) {
nativeStarts.add(start);
}
boolean clearEntireFootprint = NativeStructureVegetationClearer
.shouldClearEntireVegetationFootprint(
structure.step(), decision.clearVegetation());
vegetationTargets.add(new NativeStructureVegetationClearer.VegetationTarget(
start, clearEntireFootprint));
}
if (!resolvedPlacements.isEmpty()) {
placementGroups.add(new NativePlacementGroup(
structureId, index, step, List.copyOf(resolvedPlacements)));
}
} catch (Throwable error) {
throw NativeStructureGenerationException.failure(
"resolution", structureId, chunkPos.x(), chunkPos.z(), error);
}
index++;
}
}
try {
int runtimeMinY = world.getMinY();
WorldgenTerrainHeightmaps.primeStructurePlacement(
world, heightmapStarts,
worldgenSurfaceHeight(current, runtimeMinY),
worldgenFloorHeight(current, runtimeMinY));
} catch (Throwable error) {
throw NativeStructureGenerationException.failure(
"heightmap priming", nativeStructureBatchContext(placementGroups),
chunkPos.x(), chunkPos.z(), error);
}
try {
NativeStructureSurfaceFitter.prepareSurfaceStructures(
world, area, nativeStarts,
(x, z) -> current.getHeight(x, z, true) + current.getMinHeight());
} catch (Throwable error) {
throw NativeStructureGenerationException.failure(
"terrain integration", nativeStructureBatchContext(placementGroups),
chunkPos.x(), chunkPos.z(), error);
}
try {
NativeStructureVegetationClearer.clearIntersectingVegetation(
world, chunk, area, vegetationTargets);
} catch (Throwable error) {
throw NativeStructureGenerationException.failure(
"vegetation cleanup", nativeStructureBatchContext(placementGroups),
chunkPos.x(), chunkPos.z(), error);
}
try {
NativeStructurePostProcessor.prepareTerrain(
world, area, terrainTargets, this::resolvePaletteBlock);
} catch (Throwable error) {
throw NativeStructureGenerationException.failure(
"terrain carving", nativeStructureBatchContext(placementGroups),
chunkPos.x(), chunkPos.z(), error);
}
for (NativePlacementGroup group : placementGroups) {
random.setFeatureSeed(decorationSeed, group.featureIndex(), group.step());
try {
for (NativePlacement placement : group.placements()) {
placeVanillaStructure(world, structureManager, random, area, chunkPos,
group.structureId(), placement.start(), placement.decision());
}
} catch (Throwable error) {
throw NativeStructureGenerationException.failure(
"placement", group.structureId(), chunkPos.x(), chunkPos.z(), error);
}
}
}
private static String nativeStructureBatchContext(List<NativePlacementGroup> placementGroups) {
if (placementGroups.isEmpty()) {
return "<no resolved native structures>";
}
StringBuilder context = new StringBuilder("[");
for (int i = 0; i < placementGroups.size(); i++) {
if (i > 0) {
context.append(", ");
}
context.append(placementGroups.get(i).structureId());
}
return context.append(']').toString();
}
private void placeVanillaStructure(WorldGenLevel world, StructureManager structureManager,
WorldgenRandom random, BoundingBox area, ChunkPos chunkPos,
String structureId, StructureStart start,
IrisNativeStructureDecision decision) {
NativeStructurePostProcessor.place(world, structureManager, generator, random, area, chunkPos,
structureId, start, decision, this::resolvePaletteBlock,
(x, z) -> generator.engine().getHeight(x, z, true) + generator.engine().getMinHeight());
}
private List<List<Structure>> structuresByStep(Registry<Structure> registry) {
StructureStepCache cached = structureStepCache;
if (cached != null && cached.registry() == registry) {
return cached.structures();
}
synchronized (generator) {
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()));
}
void clearWorldCheckStructureShifts() {
worldCheckStructureShifts.clear();
}
private BlockState resolvePaletteBlock(IrisMaterialPalette palette, RNG rng,
int x, int y, int z) {
PlatformBlockState platformState = palette.get(rng, x, y, z, generator.engine().getData());
if (platformState == null || !(platformState.nativeHandle() instanceof BlockState blockState)) {
throw new IllegalStateException("Configured native structure palette did not resolve a Minecraft block at "
+ x + "," + y + "," + z);
}
return blockState;
}
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);
}
private IntBinaryOperator worldgenSurfaceHeight(Engine generationEngine, int runtimeMinY) {
return (x, z) -> generationEngine.getHeight(x, z, false) + runtimeMinY + 1;
}
private IntBinaryOperator worldgenFloorHeight(Engine generationEngine, int runtimeMinY) {
return (x, z) -> generationEngine.getHeight(x, z, true) + runtimeMinY + 1;
}
private record NativeStructureStartKey(String structureId, long chunkPosition) {
}
private record NativePlacement(StructureStart start, IrisNativeStructureDecision decision) {
}
private record NativePlacementGroup(String structureId, int featureIndex, int step,
List<NativePlacement> placements) {
}
private record StructureStepCache(Registry<Structure> registry, List<List<Structure>> structures) {
}
}
@@ -26,6 +26,7 @@ import art.arcane.iris.spi.PlatformEntityType;
import art.arcane.iris.spi.PlatformRegistries;
import art.arcane.iris.spi.PlatformScheduler;
import art.arcane.iris.spi.PlatformStructureHooks;
import art.arcane.iris.spi.PlatformWorld;
import net.minecraft.core.BlockPos;
import net.minecraft.server.MinecraftServer;
import net.minecraft.server.level.ServerLevel;
@@ -141,8 +142,8 @@ public final class ModdedPlatform implements IrisPlatform {
}
@Override
public boolean spawnEntity(Object world, String entityKey, double x, double y, double z) {
if (!(world instanceof ServerLevel level) || entityKey == null) {
public boolean spawnEntity(PlatformWorld world, String entityKey, double x, double y, double z) {
if (world == null || entityKey == null || !(world.nativeHandle() instanceof ServerLevel level)) {
return false;
}
PlatformEntityType resolved = registries.entity(entityKey);
@@ -0,0 +1,119 @@
/*
* 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.framework.GenerationSessionLease;
import art.arcane.iris.engine.object.IrisBiome;
import art.arcane.iris.engine.object.IrisBiomeCustom;
import art.arcane.iris.util.project.context.IrisContext;
import net.minecraft.core.Holder;
import net.minecraft.core.Registry;
import net.minecraft.resources.Identifier;
import net.minecraft.util.random.WeightedList;
import net.minecraft.world.entity.MobCategory;
import net.minecraft.world.level.biome.Biome;
import net.minecraft.world.level.biome.MobSpawnSettings;
import java.util.Locale;
import java.util.Optional;
import java.util.concurrent.ConcurrentHashMap;
/**
* Vanilla-derivative spawn table state for {@link IrisModdedChunkGenerator}. Every mutator locks the
* generator monitor because the repoint/bind/reset paths already hold it while resetting this state.
*/
final class ModdedSpawnTableMerger {
private final IrisModdedChunkGenerator generator;
private final ConcurrentHashMap<Biome, Holder<Biome>> vanillaSpawnBiomes = new ConcurrentHashMap<>();
private final ConcurrentHashMap<SpawnTableKey, WeightedList<MobSpawnSettings.SpawnerData>> mergedSpawnTables = new ConcurrentHashMap<>();
private volatile boolean vanillaSpawnBiomesInitialized;
ModdedSpawnTableMerger(IrisModdedChunkGenerator generator) {
this.generator = generator;
}
void initializeVanillaSpawnBiomes(Registry<Biome> registry) {
synchronized (generator) {
if (vanillaSpawnBiomesInitialized) {
return;
}
Engine current = generator.engineOrNull();
if (current == null) {
return;
}
try (GenerationSessionLease lease = generator.requireGenerationLease(current, "modded_spawn_biomes");
IrisContext.Scope ignored = IrisContext.open(current, lease.sessionId(), null)) {
String namespace = current.getDimension().getLoadKey().toLowerCase(Locale.ROOT);
for (IrisBiome irisBiome : current.getDimension().getReachableBiomes(current)) {
if (irisBiome == null || !irisBiome.isCustom()) {
continue;
}
Holder<Biome> vanillaHolder = resolveBiomeHolder(registry, irisBiome.getVanillaDerivativeKey());
if (vanillaHolder == null) {
continue;
}
for (IrisBiomeCustom customBiome : irisBiome.getCustomDerivitives()) {
Holder<Biome> customHolder = resolveBiomeHolder(registry, namespace + ":" + customBiome.getId());
if (customHolder != null) {
vanillaSpawnBiomes.putIfAbsent(customHolder.value(), vanillaHolder);
}
}
}
vanillaSpawnBiomesInitialized = true;
}
}
}
Holder<Biome> vanillaSpawnBiome(Biome biome) {
return vanillaSpawnBiomes.get(biome);
}
WeightedList<MobSpawnSettings.SpawnerData> mergedSpawnTable(
Biome biome, MobCategory category,
WeightedList<MobSpawnSettings.SpawnerData> vanillaSpawns,
WeightedList<MobSpawnSettings.SpawnerData> explicitSpawns) {
SpawnTableKey key = new SpawnTableKey(biome, category);
return mergedSpawnTables.computeIfAbsent(key, ignored -> NativeSpawnTableMerger.merge(vanillaSpawns, explicitSpawns));
}
private Holder<Biome> resolveBiomeHolder(Registry<Biome> registry, String key) {
if (key == null || key.isBlank()) {
return null;
}
Identifier identifier = Identifier.tryParse(key);
if (identifier == null) {
return null;
}
Optional<Holder.Reference<Biome>> reference = registry.get(identifier);
return reference.<Holder<Biome>>map((Holder.Reference<Biome> value) -> value).orElse(null);
}
void resetVanillaSpawnBiomes() {
synchronized (generator) {
vanillaSpawnBiomes.clear();
mergedSpawnTables.clear();
vanillaSpawnBiomesInitialized = false;
}
}
private record SpawnTableKey(Biome biome, MobCategory category) {
}
}
@@ -73,6 +73,7 @@ public final class ModdedStartup {
if (!STARTED.compareAndSet(false, true)) {
return;
}
ModdedForcedDatapack.verifyInjected();
reinjectPersistentDimensions(server);
ModdedScheduler scheduler = ModdedEngineBootstrap.schedulerOrNull();
@@ -88,6 +89,8 @@ public final class ModdedStartup {
File[] packDirs = packsRoot.listFiles(File::isDirectory);
PackValidationRegistry.clear();
if (packDirs == null || packDirs.length == 0) {
LOGGER.info("Iris found no packs to validate under {}; install one with /iris download <pack>",
packsRoot.getAbsolutePath());
return;
}
for (File packDir : packDirs) {
@@ -182,13 +182,28 @@ public final class ModdedStateRotator implements IrisObjectRotation.StateRotator
private static Property<?> findRotation(BlockState state) {
for (Property<?> property : state.getProperties()) {
if (property.getName().equals("rotation") && property instanceof IntegerProperty) {
if (property.getName().equals("rotation")
&& property instanceof IntegerProperty integer
&& isFullRotationCycle(integer)) {
return property;
}
}
return null;
}
private static boolean isFullRotationCycle(IntegerProperty property) {
List<Integer> values = property.getPossibleValues();
if (values.size() != ROTATION_CYCLE_MODS.length) {
return false;
}
for (int value : values) {
if (value < 0 || value >= ROTATION_CYCLE_MODS.length) {
return false;
}
}
return true;
}
private static Property<?> findAxis(BlockState state) {
for (Property<?> property : state.getProperties()) {
if (property.getName().equals("axis") && property.getValueClass() == Direction.Axis.class) {
@@ -41,7 +41,7 @@ import java.util.Locale;
import java.util.function.Supplier;
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).setObjectToNumberStrategy(com.google.gson.ToNumberPolicy.LONG_OR_DOUBLE).create();
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");
File diff suppressed because it is too large Load Diff
@@ -100,8 +100,8 @@ public final class ModdedWorldEngines {
private static Engine create(ServerLevel level, String pack, String dimensionKey, long seedOverride) {
ModdedEngineBootstrap.bind();
PackValidationRegistry.requireLoadable(pack);
File packDir = resolvePack(pack, dimensionKey);
PackValidationRegistry.requireLoadable(pack);
IrisData data = IrisData.openRuntime(packDir);
IrisDimension dimension = data.getDimensionLoader().load(dimensionKey);
if (dimension == null) {
@@ -0,0 +1,114 @@
/*
* 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.nms.datapack.DataVersion;
import art.arcane.iris.engine.object.IrisDimension;
import art.arcane.volmlib.util.json.JSONObject;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.world.entity.item.ItemEntity;
import net.minecraft.world.item.Items;
import net.minecraft.world.level.dimension.DimensionType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
final class WorldCheckDimensionContract {
private static final Logger LOGGER = LoggerFactory.getLogger("Iris");
private WorldCheckDimensionContract() {
}
static boolean checkDimensionType(ServerLevel level, IrisModdedChunkGenerator generator) {
try {
IrisDimension dimension = generator.commandEngine().getDimension();
DimensionContract expected = expectedDimensionContract(dimension);
DimensionContract actual = runtimeDimensionContract(level.dimensionType());
boolean pass = matchesDimensionContract(level.getMinY(), level.getHeight(), expected, actual);
String detail = "expected=" + expected + ",actual=" + actual
+ ",levelMinY=" + level.getMinY() + ",levelHeight=" + level.getHeight();
WorldCheckPredicates.qaEvent("dimension_type", dimension.getLoadKey(), pass, detail);
if (!pass) {
LOGGER.error("[worldcheck] dimension type mismatch for {}: {}", dimension.getLoadKey(), detail);
} else {
LOGGER.info("[worldcheck] dimension type contract: {}", detail);
}
return pass;
} catch (Throwable error) {
LOGGER.error("[worldcheck] could not validate the Iris dimension type contract", error);
WorldCheckPredicates.qaEvent("dimension_type", generator.activeDimensionKey(), false,
"validationError=" + error.getClass().getSimpleName() + ":" + error.getMessage());
return false;
}
}
static DimensionContract expectedDimensionContract(IrisDimension dimension) {
JSONObject json = new JSONObject(dimension.getDimensionType().toJson(DataVersion.getLatest().get()));
return new DimensionContract(
json.getInt("min_y"),
json.getInt("height"),
json.getInt("logical_height"),
json.getDouble("coordinate_scale"),
(float) json.getDouble("ambient_light"),
json.getBoolean("has_skylight"),
json.getBoolean("has_ceiling"),
json.getBoolean("has_ender_dragon_fight"),
json.getInt("monster_spawn_block_light_limit"));
}
static DimensionContract runtimeDimensionContract(DimensionType dimensionType) {
return new DimensionContract(
dimensionType.minY(),
dimensionType.height(),
dimensionType.logicalHeight(),
dimensionType.coordinateScale(),
dimensionType.ambientLight(),
dimensionType.hasSkyLight(),
dimensionType.hasCeiling(),
dimensionType.hasEnderDragonFight(),
dimensionType.monsterSpawnBlockLightLimit());
}
static boolean matchesDimensionContract(int levelMinY, int levelHeight,
DimensionContract expected, DimensionContract actual) {
return levelMinY == expected.minY()
&& levelHeight == expected.height()
&& actual.equals(expected);
}
static boolean checkEntityMixins(ServerLevel level) {
ItemEntity item = new ItemEntity(level, 0D, level.getMinY(), 0D, Items.COBBLESTONE.getDefaultInstance());
boolean vanillaSave = item.shouldBeSaved();
ModdedEntityPersistence.configure(item, false);
boolean suppressed = !item.shouldBeSaved();
ModdedEntityPersistence.configure(item, true);
boolean restored = item.shouldBeSaved();
boolean pass = vanillaSave && suppressed && restored;
WorldCheckPredicates.qaEvent("entity_mixin", "persistence", pass,
"vanilla=" + vanillaSave + ",suppressed=" + suppressed + ",restored=" + restored);
if (!pass) {
LOGGER.error("[worldcheck] shared entity mixins are not active on this loader");
}
return pass;
}
record DimensionContract(int minY, int height, int logicalHeight, double coordinateScale,
float ambientLight, boolean hasSkyLight, boolean hasCeiling,
boolean hasEnderDragonFight, int monsterSpawnBlockLightLimit) {
}
}
@@ -0,0 +1,130 @@
/*
* 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 net.minecraft.resources.Identifier;
final class WorldCheckMaterials {
private WorldCheckMaterials() {
}
static boolean isCharacteristicMaterial(String structureLabel, Identifier structureKey, Identifier blockKey) {
if (structureKey == null || blockKey == null || !blockKey.getNamespace().equals("minecraft")) {
return false;
}
String block = blockKey.getPath();
return switch (structureLabel) {
case "stronghold" -> isStrongholdMaterial(block);
case "trial_chambers" -> isTrialChamberMaterial(block);
case "mansion" -> isWoodConstructionMaterial(block, "dark_oak")
|| isWoodConstructionMaterial(block, "birch")
|| isCobblestoneConstructionMaterial(block);
case "village" -> isVillageMaterial(structureKey.getPath(), block);
case "monument" -> isMonumentMaterial(block);
default -> false;
};
}
private static boolean isStrongholdMaterial(String block) {
return block.equals("stone_bricks")
|| block.equals("cracked_stone_bricks")
|| block.equals("mossy_stone_bricks")
|| block.equals("infested_stone_bricks")
|| block.equals("infested_cracked_stone_bricks")
|| block.equals("infested_mossy_stone_bricks")
|| block.equals("stone_brick_stairs")
|| block.equals("stone_brick_slab")
|| block.equals("stone_brick_wall");
}
private static boolean isTrialChamberMaterial(String block) {
return block.contains("tuff_brick")
|| block.equals("polished_tuff")
|| block.equals("chiseled_tuff")
|| block.endsWith("copper_grate")
|| block.equals("trial_spawner")
|| block.equals("vault");
}
private static boolean isVillageMaterial(String structure, String block) {
if (isCobblestoneConstructionMaterial(block)) {
return true;
}
return switch (structure) {
case "village_plains" -> isWoodConstructionMaterial(block, "oak");
case "village_desert" -> block.equals("cut_sandstone")
|| block.equals("smooth_sandstone")
|| block.equals("cut_sandstone_slab")
|| block.equals("smooth_sandstone_slab")
|| block.equals("smooth_sandstone_stairs")
|| block.equals("sandstone_stairs")
|| block.equals("sandstone_slab")
|| block.equals("sandstone_wall");
case "village_savanna" -> isWoodConstructionMaterial(block, "acacia");
case "village_snowy", "village_taiga" -> isWoodConstructionMaterial(block, "spruce");
default -> false;
};
}
private static boolean isWoodConstructionMaterial(String block, String wood) {
if (block.startsWith(wood)) {
int suffixOffset = wood.length();
if (matchesSuffix(block, suffixOffset, "_planks")
|| matchesSuffix(block, suffixOffset, "_stairs")
|| matchesSuffix(block, suffixOffset, "_slab")
|| matchesSuffix(block, suffixOffset, "_fence")
|| matchesSuffix(block, suffixOffset, "_fence_gate")
|| matchesSuffix(block, suffixOffset, "_door")
|| matchesSuffix(block, suffixOffset, "_trapdoor")) {
return true;
}
}
int strippedOffset = "stripped_".length();
if (!block.startsWith("stripped_")
|| !block.regionMatches(strippedOffset, wood, 0, wood.length())) {
return false;
}
int suffixOffset = strippedOffset + wood.length();
return matchesSuffix(block, suffixOffset, "_log")
|| matchesSuffix(block, suffixOffset, "_wood");
}
private static boolean isCobblestoneConstructionMaterial(String block) {
return block.equals("cobblestone")
|| block.equals("cobblestone_stairs")
|| block.equals("cobblestone_slab")
|| block.equals("cobblestone_wall")
|| block.equals("mossy_cobblestone")
|| block.equals("mossy_cobblestone_stairs")
|| block.equals("mossy_cobblestone_slab")
|| block.equals("mossy_cobblestone_wall");
}
private static boolean matchesSuffix(String value, int offset, String suffix) {
return value.length() == offset + suffix.length()
&& value.regionMatches(offset, suffix, 0, suffix.length());
}
private static boolean isMonumentMaterial(String block) {
return block.equals("prismarine")
|| block.equals("prismarine_bricks")
|| block.equals("dark_prismarine")
|| block.equals("sea_lantern");
}
}
@@ -0,0 +1,130 @@
/*
* 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.StructureVerticalBounds;
import art.arcane.iris.modded.WorldCheckStructureAudit.StructureCheck;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
final class WorldCheckPredicates {
private static final Logger LOGGER = LoggerFactory.getLogger("Iris");
private WorldCheckPredicates() {
}
static void emitSkipped(StructureCheck check, String reason, String... events) {
for (String event : events) {
qaEvent(event, check.label(), false, "skipped=" + reason);
}
}
static void qaEvent(String event, String structure, boolean pass, String detail) {
LOGGER.info(qaEventJson(event, structure, pass, detail));
}
static String qaEventJson(String event, String structure, boolean pass, String detail) {
return "QA_EVT {\"event\":\"" + jsonEscape(event)
+ "\",\"structure\":\"" + jsonEscape(structure)
+ "\",\"pass\":" + pass
+ ",\"detail\":\"" + jsonEscape(detail) + "\"}";
}
static String jsonEscape(String value) {
StringBuilder escaped = new StringBuilder(value.length() + 16);
for (int i = 0; i < value.length(); i++) {
char character = value.charAt(i);
switch (character) {
case '"' -> escaped.append("\\\"");
case '\\' -> escaped.append("\\\\");
case '\b' -> escaped.append("\\b");
case '\f' -> escaped.append("\\f");
case '\n' -> escaped.append("\\n");
case '\r' -> escaped.append("\\r");
case '\t' -> escaped.append("\\t");
default -> {
if (character < 32) {
escaped.append("\\u");
String hex = Integer.toHexString(character);
escaped.append("0".repeat(4 - hex.length())).append(hex);
} else {
escaped.append(character);
}
}
}
}
return escaped.toString();
}
static boolean hasNativeStructureEvidence(boolean validStart, int references) {
return validStart || references > 0;
}
static boolean hasCharacteristicMaterialEvidence(int blocks, int chunksWithMaterial, int scannedChunks) {
if (blocks <= 0 || chunksWithMaterial <= 0 || scannedChunks <= 0
|| chunksWithMaterial > scannedChunks) {
return false;
}
return scannedChunks == 1 || chunksWithMaterial > 1;
}
static boolean verticalShiftMatches(int configuredShift, Integer appliedShift, int shiftedMinY,
int shiftedMaxY, int worldMinY, int worldMaxYExclusive) {
try {
if (appliedShift == null) {
return configuredShift == 0 && StructureVerticalBounds.clampOffset(
shiftedMinY, shiftedMaxY, 0, worldMinY, worldMaxYExclusive) == 0;
}
int originalMinY = Math.subtractExact(shiftedMinY, appliedShift);
int originalMaxY = Math.subtractExact(shiftedMaxY, appliedShift);
int expectedShift = StructureVerticalBounds.clampOffset(
originalMinY, originalMaxY, configuredShift, worldMinY, worldMaxYExclusive);
return appliedShift == expectedShift;
} catch (RuntimeException error) {
return false;
}
}
static boolean mansionVegetationPass(int remainingVegetationBlocks) {
return remainingVegetationBlocks == 0;
}
static boolean mansionVegetationAbovePiece(boolean vegetation, int blockY, int highestPieceY) {
return vegetation && blockY > highestPieceY;
}
static boolean villageFoundationPass(int unsupportedColumns) {
return unsupportedColumns == 0;
}
static boolean villagePoiPass(int inBounds, int outOfBounds) {
return inBounds > 0 && outOfBounds == 0;
}
static MessageDigest sha256() {
try {
return MessageDigest.getInstance("SHA-256");
} catch (NoSuchAlgorithmException e) {
throw new IllegalStateException(e);
}
}
}
@@ -0,0 +1,783 @@
/*
* 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.framework.NativeStructureGenerationPolicy;
import art.arcane.iris.engine.object.IrisNativeStructureDecision;
import art.arcane.iris.nativegen.NativeStructureFoundationBuilder;
import art.arcane.iris.nativegen.NativeStructureVegetationClearer;
import com.mojang.datafixers.util.Pair;
import net.minecraft.core.BlockPos;
import net.minecraft.core.Holder;
import net.minecraft.core.Registry;
import net.minecraft.core.registries.BuiltInRegistries;
import net.minecraft.core.registries.Registries;
import net.minecraft.resources.Identifier;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.tags.BlockTags;
import net.minecraft.world.entity.ai.village.poi.PoiManager;
import net.minecraft.world.entity.ai.village.poi.PoiRecord;
import net.minecraft.world.level.ChunkPos;
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.ChunkGeneratorStructureState;
import net.minecraft.world.level.chunk.ChunkGenerator;
import net.minecraft.world.level.chunk.LevelChunkSection;
import net.minecraft.world.level.levelgen.structure.BoundingBox;
import net.minecraft.world.level.levelgen.structure.Structure;
import net.minecraft.world.level.levelgen.structure.StructurePiece;
import net.minecraft.world.level.levelgen.structure.StructureStart;
import net.minecraft.world.level.levelgen.structure.placement.ConcentricRingsStructurePlacement;
import net.minecraft.world.level.levelgen.structure.placement.RandomSpreadStructurePlacement;
import net.minecraft.world.level.levelgen.structure.placement.StructurePlacement;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.BitSet;
import java.util.Comparator;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.function.IntBinaryOperator;
final class WorldCheckStructureAudit {
static final List<StructureCheck> STRUCTURE_CHECKS = List.of(
new StructureCheck("stronghold", List.of("minecraft:stronghold"), 256),
new StructureCheck("trial_chambers", List.of("minecraft:trial_chambers"), 128),
new StructureCheck("mansion", List.of("minecraft:mansion"), 256),
new StructureCheck("village", List.of(
"minecraft:village_plains",
"minecraft:village_desert",
"minecraft:village_savanna",
"minecraft:village_snowy",
"minecraft:village_taiga"), 128),
new StructureCheck("monument", List.of("minecraft:monument"), 128)
);
private static final int MAX_FOOTPRINT_CHUNKS = 96;
private static final int MAX_START_REFERENCE_CHUNKS = 16;
private static final int MAX_STRUCTURE_CANDIDATES = 1024;
private static final Logger LOGGER = LoggerFactory.getLogger("Iris");
private WorldCheckStructureAudit() {
}
static NativeStructureGate checkNativeStructures(ServerLevel level,
IrisModdedChunkGenerator generator,
BlockPos origin) {
boolean pass = true;
int nonVillagePassed = 0;
boolean villagePass = false;
PendingVillagePoi pendingPoi = null;
for (StructureCheck check : STRUCTURE_CHECKS) {
StructureCheckResult result = checkNativeStructure(level, generator, origin, check);
if (!result.pass()) {
pass = false;
}
if (check.label().equals("village")) {
villagePass = result.pass();
pendingPoi = result.pendingPoi();
} else if (result.pass()) {
nonVillagePassed++;
}
}
return new NativeStructureGate(pass, nonVillagePassed, villagePass, pendingPoi);
}
private static StructureCheckResult checkNativeStructure(ServerLevel level,
IrisModdedChunkGenerator generator,
BlockPos origin,
StructureCheck check) {
Registry<Structure> registry = level.registryAccess().lookupOrThrow(Registries.STRUCTURE);
List<Holder<Structure>> registered = new ArrayList<>(check.registryKeys().size());
LinkedHashSet<String> registeredKeys = new LinkedHashSet<>();
for (String key : check.registryKeys()) {
Identifier identifier = Identifier.tryParse(key);
if (identifier == null) {
continue;
}
Optional<Holder.Reference<Structure>> resolved = registry.get(identifier);
if (resolved.isPresent()) {
registered.add(resolved.get());
registeredKeys.add(identifier.toString());
}
}
boolean registryOk = registered.size() == check.registryKeys().size();
LOGGER.info("[worldcheck] {} registry: {}/{} resolved {}", check.label(), registered.size(),
check.registryKeys().size(), registeredKeys);
WorldCheckPredicates.qaEvent("structure_registry", check.label(), registryOk,
"resolved=" + registered.size() + ",expected=" + check.registryKeys().size()
+ ",keys=" + String.join("|", registeredKeys));
if (!registryOk) {
LOGGER.error("[worldcheck] {} registry resolution failed; expected {}", check.label(), check.registryKeys());
WorldCheckPredicates.emitSkipped(check, "registry", "structure_reachability", "structure_locate",
"structure_start_reference", "structure_footprint", "structure_material",
"structure_block_entity");
return new StructureCheckResult(false, null);
}
List<Holder<Structure>> reachable = new ArrayList<>(registered.size());
LinkedHashSet<String> reachableKeys = new LinkedHashSet<>();
for (Holder<Structure> holder : registered) {
if (!generator.isNativeStructureReachable(holder)) {
continue;
}
reachable.add(holder);
Identifier key = registry.getKey(holder.value());
if (key != null) {
reachableKeys.add(key.toString());
}
}
boolean reachableOk = !reachable.isEmpty();
LOGGER.info("[worldcheck] {} biome-reachable through Iris: {}", check.label(), reachableKeys);
WorldCheckPredicates.qaEvent("structure_reachability", check.label(), reachableOk,
"reachable=" + reachable.size() + ",registered=" + registered.size()
+ ",keys=" + String.join("|", reachableKeys));
if (!reachableOk) {
LOGGER.error("[worldcheck] {} cannot generate in any biome produced by this Iris pack", check.label());
WorldCheckPredicates.emitSkipped(check, "reachability", "structure_locate", "structure_start_reference",
"structure_footprint", "structure_material", "structure_block_entity");
return new StructureCheckResult(false, null);
}
long locateStart = System.nanoTime();
Pair<BlockPos, Holder<Structure>> found = findGeneratedStructureCandidate(
level, reachable, origin, check.locateRadius());
long locateMillis = (System.nanoTime() - locateStart) / 1_000_000L;
Identifier foundKey = found == null ? null : registry.getKey(found.getSecond().value());
boolean locateOk = found != null && foundKey != null && reachableKeys.contains(foundKey.toString());
WorldCheckPredicates.qaEvent("structure_locate", check.label(), locateOk,
"method=placement_candidates,millis=" + locateMillis + ",radius=" + check.locateRadius()
+ ",result=" + (foundKey == null ? "none" : foundKey));
if (found == null) {
LOGGER.error("[worldcheck] {} native placement candidates produced no valid start within {} rings after {}ms",
check.label(), check.locateRadius(), locateMillis);
WorldCheckPredicates.emitSkipped(check, "locate", "structure_start_reference", "structure_footprint",
"structure_material", "structure_block_entity");
return new StructureCheckResult(false, null);
}
BlockPos position = found.getFirst();
LOGGER.info("[worldcheck] {} generated candidate: {} {} {} in {}ms (radius={}, result={})",
check.label(), position.getX(), position.getY(), position.getZ(), locateMillis,
check.locateRadius(), foundKey);
if (!locateOk) {
LOGGER.error("[worldcheck] {} candidate scan returned unexpected structure {}", check.label(), foundKey);
WorldCheckPredicates.emitSkipped(check, "locate", "structure_start_reference", "structure_footprint",
"structure_material", "structure_block_entity");
return new StructureCheckResult(false, null);
}
int chunkX = position.getX() >> 4;
int chunkZ = position.getZ() >> 4;
ChunkAccess targetChunk = level.getChunk(chunkX, chunkZ);
Structure structure = found.getSecond().value();
StructureStart start = resolveStructureStart(level, targetChunk, structure);
boolean validStart = start != null && start.isValid();
int references = targetChunk.getReferencesForStructure(structure).size();
boolean startReferenceOk = WorldCheckPredicates.hasNativeStructureEvidence(validStart, references);
LOGGER.info("[worldcheck] {} target chunk {},{}: valid start={}, references={}",
check.label(), chunkX, chunkZ, validStart, references);
WorldCheckPredicates.qaEvent("structure_start_reference", check.label(), startReferenceOk,
"chunk=" + chunkX + "," + chunkZ + ",validStart=" + validStart
+ ",references=" + references);
if (!startReferenceOk || !validStart) {
LOGGER.error("[worldcheck] {} located at chunk {},{} but no resolvable valid start was generated",
check.label(), chunkX, chunkZ);
WorldCheckPredicates.emitSkipped(check, "start_reference", "structure_footprint", "structure_material",
"structure_block_entity");
return new StructureCheckResult(false, null);
}
IrisNativeStructureDecision decision = NativeStructureGenerationPolicy.resolve(
generator.commandEngine(), foundKey.toString(),
NativeStructureVegetationClearer.isUndergroundStep(structure.step()));
Integer appliedShift = generator.worldCheckStructureShift(foundKey.toString(), start.getChunkPos());
BoundingBox shiftedBounds = start.getBoundingBox();
boolean verticalShiftOk = WorldCheckPredicates.verticalShiftMatches(
decision.yShift(), appliedShift, shiftedBounds.minY(), shiftedBounds.maxY(),
level.getMinY(), level.getMaxY());
WorldCheckPredicates.qaEvent("structure_vertical_shift", check.label(), verticalShiftOk,
"configured=" + decision.yShift() + ",applied="
+ (appliedShift == null ? "unrecorded" : appliedShift));
if (!verticalShiftOk) {
LOGGER.error("[worldcheck] {} expected vertical shift {} but generation recorded {}",
check.label(), decision.yShift(), appliedShift);
}
FootprintAudit footprint = auditFootprint(level, structure, start, check, foundKey);
boolean footprintOk = footprint.inspectedChunks() > 0
&& footprint.evidenceChunks() == footprint.inspectedChunks()
&& footprint.coveredPieces() == footprint.totalPieces();
LOGGER.info("[worldcheck] {} footprint: chunks={}/{} evidence={} pieces={}/{}",
check.label(), footprint.inspectedChunks(), footprint.availableChunks(),
footprint.evidenceChunks(), footprint.coveredPieces(), footprint.totalPieces());
WorldCheckPredicates.qaEvent("structure_footprint", check.label(), footprintOk,
"inspected=" + footprint.inspectedChunks() + ",available=" + footprint.availableChunks()
+ ",evidence=" + footprint.evidenceChunks() + ",coveredPieces="
+ footprint.coveredPieces() + ",totalPieces=" + footprint.totalPieces());
boolean materialOk = WorldCheckPredicates.hasCharacteristicMaterialEvidence(footprint.characteristicBlocks(),
footprint.characteristicChunks(), footprint.materialScannedChunks());
LOGGER.info("[worldcheck] {} material: blocks={} chunks={}/{}",
check.label(), footprint.characteristicBlocks(), footprint.characteristicChunks(),
footprint.materialScannedChunks());
WorldCheckPredicates.qaEvent("structure_material", check.label(), materialOk,
"blocks=" + footprint.characteristicBlocks() + ",chunks=" + footprint.characteristicChunks()
+ ",scanned=" + footprint.materialScannedChunks());
boolean vegetationOk = true;
if (check.label().equals("mansion")) {
boolean overlap = footprint.vegetationBlocks() > 0;
vegetationOk = WorldCheckPredicates.mansionVegetationPass(footprint.vegetationBlocks());
LOGGER.info("[worldcheck] mansion vegetation metric: remaining log/leaf blocks={} columns={} overlap={}",
footprint.vegetationBlocks(), footprint.vegetationColumns(), overlap);
WorldCheckPredicates.qaEvent("mansion_vegetation_metric", check.label(), vegetationOk,
"remainingLogsOrLeaves=" + footprint.vegetationBlocks() + ",columns="
+ footprint.vegetationColumns() + ",overlap=" + overlap);
}
boolean foundationOk = true;
PendingVillagePoi pendingPoi = null;
if (check.label().equals("village")) {
foundationOk = WorldCheckPredicates.villageFoundationPass(footprint.foundationGapColumns());
LOGGER.info("[worldcheck] village foundation metric: bases={} cobblestone={} columns={} unsupported={}",
footprint.foundationBaseColumns(), footprint.foundationBlocks(),
footprint.foundationColumns(), footprint.foundationGapColumns());
WorldCheckPredicates.qaEvent("village_foundation_metric", check.label(), foundationOk,
"bases=" + footprint.foundationBaseColumns() + ",cobblestoneBelowBase="
+ footprint.foundationBlocks() + ",columns="
+ footprint.foundationColumns() + ",unsupported=" + footprint.foundationGapColumns());
pendingPoi = new PendingVillagePoi(level, start);
}
boolean blockEntityOk = footprint.blockEntityStates() == footprint.blockEntitiesPresent();
LOGGER.info("[worldcheck] {} block entities: state blocks={}, present={}, missing={}",
check.label(), footprint.blockEntityStates(), footprint.blockEntitiesPresent(),
footprint.blockEntityStates() - footprint.blockEntitiesPresent());
WorldCheckPredicates.qaEvent("structure_block_entity", check.label(), blockEntityOk,
"states=" + footprint.blockEntityStates() + ",present=" + footprint.blockEntitiesPresent()
+ ",missing=" + (footprint.blockEntityStates() - footprint.blockEntitiesPresent()));
if (!footprintOk) {
LOGGER.error("[worldcheck] {} structure footprint is incomplete", check.label());
}
if (!materialOk) {
LOGGER.error("[worldcheck] {} has no distributed characteristic structure material", check.label());
}
if (!blockEntityOk) {
LOGGER.error("[worldcheck] {} generated block-entity states without matching block entities", check.label());
}
if (!vegetationOk) {
LOGGER.error("[worldcheck] mansion vegetation still intersects the generated structure footprint");
}
if (!foundationOk) {
LOGGER.error("[worldcheck] village has unsupported foundation columns after stilt placement");
}
boolean pass = verticalShiftOk && footprintOk && materialOk && blockEntityOk
&& vegetationOk && foundationOk;
return new StructureCheckResult(pass, pass ? pendingPoi : null);
}
private static StructureStart resolveStructureStart(ServerLevel level, ChunkAccess targetChunk,
Structure structure) {
StructureStart direct = targetChunk.getStartForStructure(structure);
if (direct != null && direct.isValid()) {
return direct;
}
int checked = 0;
for (long packed : targetChunk.getReferencesForStructure(structure)) {
if (checked++ >= MAX_START_REFERENCE_CHUNKS) {
break;
}
ChunkAccess referencedChunk = level.getChunk(ChunkPos.getX(packed), ChunkPos.getZ(packed));
StructureStart referenced = referencedChunk.getStartForStructure(structure);
if (referenced != null && referenced.isValid()) {
return referenced;
}
}
return direct;
}
private static Pair<BlockPos, Holder<Structure>> findGeneratedStructureCandidate(
ServerLevel level, List<Holder<Structure>> structures, BlockPos origin, int maxRadius) {
ChunkGeneratorStructureState state = level.getChunkSource().getGeneratorState();
Set<Long> attempted = new LinkedHashSet<>();
for (Holder<Structure> structure : structures) {
for (StructurePlacement placement : state.getPlacementsForStructure(structure)) {
if (!(placement instanceof ConcentricRingsStructurePlacement rings)) {
continue;
}
List<ChunkPos> positions = state.getRingPositionsFor(rings);
if (positions == null) {
continue;
}
List<ChunkPos> sorted = new ArrayList<>(positions);
sorted.sort(Comparator.comparingLong(position -> distanceSquared(origin, position)));
for (ChunkPos position : sorted) {
Pair<BlockPos, Holder<Structure>> found = inspectStructureCandidate(
level, structures, placement, position, attempted);
if (found != null) {
return found;
}
if (attempted.size() >= MAX_STRUCTURE_CANDIDATES) {
return null;
}
}
}
}
int originChunkX = origin.getX() >> 4;
int originChunkZ = origin.getZ() >> 4;
for (int radius = 0; radius <= maxRadius; radius++) {
for (Holder<Structure> structure : structures) {
for (StructurePlacement placement : state.getPlacementsForStructure(structure)) {
if (!(placement instanceof RandomSpreadStructurePlacement randomSpread)) {
continue;
}
for (int x = -radius; x <= radius; x++) {
boolean xEdge = x == -radius || x == radius;
for (int z = -radius; z <= radius; z++) {
if (!xEdge && z != -radius && z != radius) {
continue;
}
int sectorX = originChunkX + randomSpread.spacing() * x;
int sectorZ = originChunkZ + randomSpread.spacing() * z;
ChunkPos candidate = randomSpread.getPotentialStructureChunk(
state.getLevelSeed(), sectorX, sectorZ);
if (!placement.isStructureChunk(state, candidate.x(), candidate.z())) {
continue;
}
Pair<BlockPos, Holder<Structure>> found = inspectStructureCandidate(
level, structures, placement, candidate, attempted);
if (found != null) {
return found;
}
if (attempted.size() >= MAX_STRUCTURE_CANDIDATES) {
return null;
}
}
}
}
}
}
return null;
}
private static Pair<BlockPos, Holder<Structure>> inspectStructureCandidate(
ServerLevel level, List<Holder<Structure>> structures, StructurePlacement placement,
ChunkPos candidate, Set<Long> attempted) {
if (!attempted.add(candidate.pack())) {
return null;
}
ChunkAccess chunk = level.getChunk(candidate.x(), candidate.z());
for (Holder<Structure> structure : structures) {
StructureStart start = resolveStructureStart(level, chunk, structure.value());
if (start == null || !start.isValid()) {
continue;
}
BlockPos locate = placement.getLocatePos(start.getChunkPos());
BlockPos resolved = new BlockPos(locate.getX(), start.getBoundingBox().minY(), locate.getZ());
return Pair.of(resolved, structure);
}
return null;
}
private static long distanceSquared(BlockPos origin, ChunkPos position) {
long x = (long) position.getMinBlockX() - origin.getX();
long z = (long) position.getMinBlockZ() - origin.getZ();
return x * x + z * z;
}
private static FootprintAudit auditFootprint(ServerLevel level, Structure structure, StructureStart start,
StructureCheck check, Identifier structureKey) {
List<StructurePiece> pieces = start.getPieces();
BoundingBox bounds = start.getBoundingBox();
int availableChunks = footprintChunkCount(bounds);
List<ChunkPos> selected = selectFootprintChunks(start, MAX_FOOTPRINT_CHUNKS);
int evidenceChunks = 0;
int blockEntityStates = 0;
int blockEntitiesPresent = 0;
int materialScannedChunks = 0;
int characteristicBlocks = 0;
int characteristicChunks = 0;
int vegetationBlocks = 0;
int vegetationColumns = 0;
int foundationBaseColumns = 0;
int foundationBlocks = 0;
int foundationColumns = 0;
int foundationGapColumns = 0;
BitSet visited = new BitSet(level.getHeight() << 8);
int[] maximumPieceY = new int[256];
for (ChunkPos chunkPos : selected) {
ChunkAccess chunk = level.getChunk(chunkPos.x(), chunkPos.z());
StructureStart localStart = chunk.getStartForStructure(structure);
boolean validStart = localStart != null && localStart.isValid();
int references = chunk.getReferencesForStructure(structure).size();
if (WorldCheckPredicates.hasNativeStructureEvidence(validStart, references)) {
evidenceChunks++;
}
BlockEntityAudit blockEntities = auditBlockEntities(level, chunk);
blockEntityStates += blockEntities.states();
blockEntitiesPresent += blockEntities.present();
StructureMaterialAudit material = auditStructureMaterial(level, chunk, start, check,
structureKey, visited, maximumPieceY);
if (material.scanned()) {
materialScannedChunks++;
}
characteristicBlocks += material.characteristicBlocks();
if (material.characteristicBlocks() > 0) {
characteristicChunks++;
}
vegetationBlocks += material.vegetationBlocks();
vegetationColumns += material.vegetationColumns();
foundationBaseColumns += material.foundationBaseColumns();
foundationBlocks += material.foundationBlocks();
foundationColumns += material.foundationColumns();
foundationGapColumns += material.foundationGapColumns();
}
int coveredPieces = 0;
for (StructurePiece piece : pieces) {
boolean covered = false;
for (ChunkPos chunkPos : selected) {
if (intersectsChunk(piece.getBoundingBox(), chunkPos)) {
covered = true;
break;
}
}
if (covered) {
coveredPieces++;
}
}
return new FootprintAudit(selected.size(), availableChunks, evidenceChunks,
coveredPieces, pieces.size(), blockEntityStates, blockEntitiesPresent,
materialScannedChunks, characteristicBlocks, characteristicChunks,
vegetationBlocks, vegetationColumns, foundationBaseColumns, foundationBlocks,
foundationColumns, foundationGapColumns);
}
private static StructureMaterialAudit auditStructureMaterial(ServerLevel level, ChunkAccess chunk,
StructureStart start,
StructureCheck check,
Identifier structureKey,
BitSet visited,
int[] maximumPieceY) {
List<StructurePiece> pieces = start.getPieces();
visited.clear();
Arrays.fill(maximumPieceY, Integer.MIN_VALUE);
int characteristicBlocks = 0;
int minimumWorldY = level.getMinY();
int maximumWorldY = level.getMaxY() - 1;
int minimumChunkX = chunk.getPos().getMinBlockX();
int maximumChunkX = chunk.getPos().getMaxBlockX();
int minimumChunkZ = chunk.getPos().getMinBlockZ();
int maximumChunkZ = chunk.getPos().getMaxBlockZ();
boolean scanned = false;
BlockPos.MutableBlockPos position = new BlockPos.MutableBlockPos();
for (StructurePiece piece : pieces) {
BoundingBox bounds = piece.getBoundingBox();
int minimumX = Math.max(minimumChunkX, bounds.minX());
int maximumX = Math.min(maximumChunkX, bounds.maxX());
int minimumY = Math.max(minimumWorldY, bounds.minY());
int maximumY = Math.min(maximumWorldY, bounds.maxY());
int minimumZ = Math.max(minimumChunkZ, bounds.minZ());
int maximumZ = Math.min(maximumChunkZ, bounds.maxZ());
if (minimumX > maximumX || minimumY > maximumY || minimumZ > maximumZ) {
continue;
}
scanned = true;
for (int z = minimumZ; z <= maximumZ; z++) {
int localZ = z - minimumChunkZ;
for (int x = minimumX; x <= maximumX; x++) {
int column = (localZ << 4) | (x - minimumChunkX);
maximumPieceY[column] = Math.max(maximumPieceY[column], maximumY);
}
}
for (int y = minimumY; y <= maximumY; y++) {
int verticalIndex = (y - minimumWorldY) << 8;
for (int z = minimumZ; z <= maximumZ; z++) {
int localZ = z - minimumChunkZ;
for (int x = minimumX; x <= maximumX; x++) {
int column = (localZ << 4) | (x - minimumChunkX);
int index = verticalIndex | column;
if (visited.get(index)) {
continue;
}
visited.set(index);
BlockState state = chunk.getBlockState(position.set(x, y, z));
Identifier blockKey = BuiltInRegistries.BLOCK.getKey(state.getBlock());
if (WorldCheckMaterials.isCharacteristicMaterial(check.label(), structureKey, blockKey)) {
characteristicBlocks++;
}
}
}
}
}
int vegetationBlocks = 0;
int vegetationColumns = 0;
if (check.label().equals("mansion")) {
for (int column = 0; column < maximumPieceY.length; column++) {
int highestPieceY = maximumPieceY[column];
if (highestPieceY == Integer.MIN_VALUE || highestPieceY >= maximumWorldY) {
continue;
}
boolean vegetationColumn = false;
int x = minimumChunkX + (column & 15);
int z = minimumChunkZ + (column >> 4);
for (int y = highestPieceY + 1; y <= maximumWorldY; y++) {
BlockState state = chunk.getBlockState(position.set(x, y, z));
boolean vegetation = state.is(BlockTags.LOGS) || state.is(BlockTags.LEAVES);
if (!WorldCheckPredicates.mansionVegetationAbovePiece(vegetation, y, highestPieceY)) {
continue;
}
vegetationBlocks++;
vegetationColumn = true;
}
if (vegetationColumn) {
vegetationColumns++;
}
}
}
int foundationBaseColumns = 0;
int foundationBlocks = 0;
int foundationColumns = 0;
int foundationGapColumns = 0;
if (check.label().equals("village")) {
BoundingBox area = new BoundingBox(
minimumChunkX, minimumWorldY, minimumChunkZ,
maximumChunkX, maximumWorldY, maximumChunkZ);
ChunkGenerator chunkGenerator = level.getChunkSource().getGenerator();
if (!(chunkGenerator instanceof IrisModdedChunkGenerator irisGenerator)) {
throw new IllegalStateException("Iris structure audit requires the Iris chunk generator");
}
Engine engine = irisGenerator.commandEngine();
IntBinaryOperator surfaceHeight = (x, z) ->
engine.getHeight(x, z, true) + engine.getMinHeight();
NativeStructureFoundationBuilder.StiltSupportAudit foundation =
NativeStructureFoundationBuilder.auditStiltSupport(
level, area, start, Blocks.COBBLESTONE.defaultBlockState(), surfaceHeight);
foundationBaseColumns = foundation.baseColumns();
foundationBlocks = foundation.stiltBlocks();
foundationColumns = foundation.stiltColumns();
foundationGapColumns = foundation.unsupportedColumns();
}
return new StructureMaterialAudit(scanned, characteristicBlocks, vegetationBlocks,
vegetationColumns, foundationBaseColumns, foundationBlocks, foundationColumns,
foundationGapColumns);
}
private static List<ChunkPos> selectFootprintChunks(StructureStart start, int limit) {
LinkedHashSet<ChunkPos> selected = new LinkedHashSet<>();
addBounded(selected, start.getChunkPos(), limit);
List<ChunkPos> pieceAnchors = new ArrayList<>();
for (StructurePiece piece : start.getPieces()) {
BoundingBox bounds = piece.getBoundingBox();
pieceAnchors.add(new ChunkPos((bounds.minX() + bounds.maxX()) >> 5,
(bounds.minZ() + bounds.maxZ()) >> 5));
pieceAnchors.add(new ChunkPos(bounds.minX() >> 4, bounds.minZ() >> 4));
pieceAnchors.add(new ChunkPos(bounds.maxX() >> 4, bounds.maxZ() >> 4));
}
pieceAnchors.sort(Comparator.comparingInt((ChunkPos chunkPos) ->
chunkPos.distanceSquared(start.getChunkPos())));
for (ChunkPos chunkPos : pieceAnchors) {
addBounded(selected, chunkPos, limit);
}
for (ChunkPos chunkPos : boundedFootprintChunks(start.getBoundingBox(), start.getChunkPos(), limit)) {
if (intersectsAnyPiece(start.getPieces(), chunkPos)) {
addBounded(selected, chunkPos, limit);
}
}
return List.copyOf(selected);
}
static List<ChunkPos> boundedFootprintChunks(BoundingBox bounds, ChunkPos origin, int limit) {
if (limit <= 0) {
return List.of();
}
int minChunkX = bounds.minX() >> 4;
int maxChunkX = bounds.maxX() >> 4;
int minChunkZ = bounds.minZ() >> 4;
int maxChunkZ = bounds.maxZ() >> 4;
long width = (long) maxChunkX - minChunkX + 1L;
long depth = (long) maxChunkZ - minChunkZ + 1L;
long total = width * depth;
LinkedHashSet<ChunkPos> chunks = new LinkedHashSet<>();
addBounded(chunks, origin, limit);
if (total <= limit) {
for (int chunkZ = minChunkZ; chunkZ <= maxChunkZ; chunkZ++) {
for (int chunkX = minChunkX; chunkX <= maxChunkX; chunkX++) {
addBounded(chunks, new ChunkPos(chunkX, chunkZ), limit);
}
}
return List.copyOf(chunks);
}
addBounded(chunks, new ChunkPos(minChunkX, minChunkZ), limit);
addBounded(chunks, new ChunkPos(maxChunkX, minChunkZ), limit);
addBounded(chunks, new ChunkPos(minChunkX, maxChunkZ), limit);
addBounded(chunks, new ChunkPos(maxChunkX, maxChunkZ), limit);
int samplesPerAxis = Math.max(2, (int) Math.floor(Math.sqrt(limit)));
for (int sampleZ = 0; sampleZ < samplesPerAxis; sampleZ++) {
int chunkZ = sampleCoordinate(minChunkZ, maxChunkZ, sampleZ, samplesPerAxis);
for (int sampleX = 0; sampleX < samplesPerAxis; sampleX++) {
int chunkX = sampleCoordinate(minChunkX, maxChunkX, sampleX, samplesPerAxis);
addBounded(chunks, new ChunkPos(chunkX, chunkZ), limit);
}
}
return List.copyOf(chunks);
}
private static int footprintChunkCount(BoundingBox bounds) {
long width = (long) (bounds.maxX() >> 4) - (bounds.minX() >> 4) + 1L;
long depth = (long) (bounds.maxZ() >> 4) - (bounds.minZ() >> 4) + 1L;
long total = width * depth;
return total > Integer.MAX_VALUE ? Integer.MAX_VALUE : (int) total;
}
private static int sampleCoordinate(int minimum, int maximum, int index, int samples) {
if (samples <= 1 || minimum == maximum) {
return minimum;
}
double progress = (double) index / (double) (samples - 1);
return minimum + (int) Math.round((maximum - minimum) * progress);
}
private static void addBounded(Set<ChunkPos> chunks, ChunkPos chunkPos, int limit) {
if (chunks.size() < limit) {
chunks.add(chunkPos);
}
}
private static boolean intersectsAnyPiece(List<StructurePiece> pieces, ChunkPos chunkPos) {
for (StructurePiece piece : pieces) {
if (intersectsChunk(piece.getBoundingBox(), chunkPos)) {
return true;
}
}
return false;
}
private static boolean intersectsChunk(BoundingBox bounds, ChunkPos chunkPos) {
return bounds.maxX() >= chunkPos.getMinBlockX()
&& bounds.minX() <= chunkPos.getMaxBlockX()
&& bounds.maxZ() >= chunkPos.getMinBlockZ()
&& bounds.minZ() <= chunkPos.getMaxBlockZ();
}
private static BlockEntityAudit auditBlockEntities(ServerLevel level, ChunkAccess chunk) {
int states = 0;
int present = 0;
BlockPos.MutableBlockPos position = new BlockPos.MutableBlockPos();
LevelChunkSection[] sections = chunk.getSections();
for (int sectionIndex = 0; sectionIndex < sections.length; sectionIndex++) {
LevelChunkSection section = sections[sectionIndex];
if (!section.maybeHas(BlockState::hasBlockEntity)) {
continue;
}
int sectionMinY = chunk.getSectionYFromSectionIndex(sectionIndex) << 4;
for (int localY = 0; localY < 16; localY++) {
for (int localZ = 0; localZ < 16; localZ++) {
for (int localX = 0; localX < 16; localX++) {
BlockState state = section.getBlockState(localX, localY, localZ);
if (!state.hasBlockEntity()) {
continue;
}
states++;
position.set(chunk.getPos().getBlockX(localX), sectionMinY + localY,
chunk.getPos().getBlockZ(localZ));
if (level.getBlockEntity(position) != null) {
present++;
}
}
}
}
}
return new BlockEntityAudit(states, present);
}
static PoiAudit auditStructurePois(ServerLevel level, StructureStart start) {
int inBounds = 0;
int outOfBounds = 0;
PoiManager poiManager = level.getPoiManager();
for (ChunkPos chunkPos : selectFootprintChunks(start, MAX_FOOTPRINT_CHUNKS)) {
List<PoiRecord> records = poiManager.getInChunk(
holder -> true, chunkPos, PoiManager.Occupancy.ANY).toList();
for (PoiRecord record : records) {
BlockPos position = record.getPos();
if (position.getY() < level.getMinY() || position.getY() >= level.getMaxY()) {
outOfBounds++;
continue;
}
if (insideAnyPiece(start.getPieces(), position)) {
inBounds++;
}
}
}
return new PoiAudit(inBounds, outOfBounds);
}
private static boolean insideAnyPiece(List<StructurePiece> pieces, BlockPos position) {
for (StructurePiece piece : pieces) {
if (piece.getBoundingBox().isInside(position)) {
return true;
}
}
return false;
}
record StructureCheck(String label, List<String> registryKeys, int locateRadius) {
}
record NativeStructureGate(boolean passBeforePoi, int nonVillagePassed,
boolean villagePassBeforePoi, PendingVillagePoi pendingPoi) {
}
private record StructureCheckResult(boolean pass, PendingVillagePoi pendingPoi) {
}
record PendingVillagePoi(ServerLevel level, StructureStart start) {
}
private record FootprintAudit(int inspectedChunks, int availableChunks, int evidenceChunks,
int coveredPieces, int totalPieces, int blockEntityStates,
int blockEntitiesPresent, int materialScannedChunks,
int characteristicBlocks, int characteristicChunks,
int vegetationBlocks, int vegetationColumns,
int foundationBaseColumns, int foundationBlocks, int foundationColumns,
int foundationGapColumns) {
}
private record BlockEntityAudit(int states, int present) {
}
private record StructureMaterialAudit(boolean scanned, int characteristicBlocks,
int vegetationBlocks, int vegetationColumns,
int foundationBaseColumns, int foundationBlocks,
int foundationColumns,
int foundationGapColumns) {
}
record PoiAudit(int inBounds, int outOfBounds) {
}
}
@@ -25,10 +25,33 @@ import art.arcane.iris.modded.command.ModdedPregenJob;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.world.level.chunk.ChunkGenerator;
/**
* Entry point for mods integrating with Iris on Fabric, Forge and NeoForge.
* <p>
* Everything here is static and null-tolerant: a null or non-Iris {@link ServerLevel} produces false, null, or a
* no-op rather than an exception, so a caller never has to pre-check whether a level is generated by Iris.
* <p>
* <b>Threading.</b> {@link #isIrisLevel(ServerLevel)}, {@link #isStudioLevel(ServerLevel)} and
* {@link #getEngine(ServerLevel)} read the level's chunk generator reference and are safe from any thread once
* the level is loaded. The mantle accessors are safe off the server thread but touch engine storage - see their
* own notes. {@link #pregenerate(ServerLevel, int)} and {@link #registerProvider(ModdedDataProvider)} mutate
* global state and belong on the server thread, during mod setup or from a command.
* <p>
* <b>Stability.</b> This class and the {@code Modded*} types beside it are the intended integration surface. The
* types they expose from {@code art.arcane.iris.engine.*} and {@code art.arcane.iris.core.*} - notably
* {@link Engine} - are internal to Iris and change without a deprecation cycle. Treat {@link Engine} as an opaque
* token to hand back to Iris, and prefer the wrappers here over reaching into it.
*
* @see ModdedDataProvider for supplying custom blocks, items and entities to the generator
*/
public final class IrisModdedAPI {
private IrisModdedAPI() {
}
/**
* Whether {@code level}'s chunk generator is an Iris generator. False for null and for every vanilla or
* third-party generated level. The cheapest available Iris check.
*/
public static boolean isIrisLevel(ServerLevel level) {
if (level == null) {
return false;
@@ -36,11 +59,22 @@ public final class IrisModdedAPI {
return level.getChunkSource().getGenerator() instanceof IrisModdedChunkGenerator;
}
/**
* Whether {@code level} is an Iris studio level - a throwaway world opened for pack authoring, which is
* deleted on shutdown. Persist nothing against one. False for null and for non-Iris levels.
*/
public static boolean isStudioLevel(ServerLevel level) {
Engine engine = getEngine(level);
return engine != null && engine.isStudio();
}
/**
* The Iris engine driving {@code level}, or null when the level is null, is not Iris-generated, or its engine
* is not currently available - during shutdown, or while the generator is still binding.
* <p>
* Never cached: resolve per use. Reloading a pack or unloading the level replaces the engine, and a stale
* reference goes inert. {@link Engine} is internal to Iris; see the stability note on this class.
*/
public static Engine getEngine(ServerLevel level) {
if (level == null) {
return null;
@@ -56,10 +90,27 @@ public final class IrisModdedAPI {
}
}
/**
* Starts a cached, asynchronous pregeneration of {@code radiusBlocks} around the world origin.
* Equivalent to {@code pregenerate(level, radiusBlocks, 0, 0, false, true)}.
*/
public static boolean pregenerate(ServerLevel level, int radiusBlocks) {
return pregenerate(level, radiusBlocks, 0, 0, false, true);
}
/**
* Starts a pregeneration job over a square region.
* <p>
* Returns as soon as the job is queued; progress is reported through Iris's own logging and boss bar, not to
* the caller. Only one job runs server-wide, so this returns false if one is already active. Call on the
* server thread.
*
* @param radiusBlocks half-extent of the square in blocks, measured from the centre
* @param sync write chunks synchronously; slower but avoids the async write queue
* @param cached reuse and update the on-disk pregeneration cache so an interrupted job resumes instead of
* regenerating
* @return false when {@code level} is not Iris-generated or another pregeneration job is already running
*/
public static boolean pregenerate(ServerLevel level, int radiusBlocks, int centerBlockX, int centerBlockZ, boolean sync, boolean cached) {
Engine engine = getEngine(level);
if (engine == null) {
@@ -68,6 +119,19 @@ public final class IrisModdedAPI {
return ModdedPregenJob.start(level.getServer(), level, engine, radiusBlocks, centerBlockX, centerBlockZ, false, sync, cached);
}
/**
* Reads a mantle value of {@code type} at world coordinates.
* <p>
* The mantle is Iris's own per-block sidecar storage, independent of chunk NBT, and it is how Iris carries
* data that must survive between generation stages. Coordinates are world-space: {@code y} is translated by
* the engine's minimum height internally.
* <p>
* Returns null when the level is not Iris-generated, no mantle region exists for that column yet - reads
* never create or load one - or nothing of {@code type} is stored there. A {@code y} outside the engine's
* height range reads as null rather than throwing.
*
* @throws IllegalStateException if the engine's mantle has already been closed
*/
public static <T> T getMantleData(ServerLevel level, int x, int y, int z, Class<T> type) {
Engine engine = getEngine(level);
if (engine == null) {
@@ -76,6 +140,19 @@ public final class IrisModdedAPI {
return engine.getMantle().getMantle().get(x, y - engine.getMinHeight(), z, type);
}
/**
* Writes a mantle value at world coordinates, replacing any previous value of the same type there.
* <p>
* Unlike {@link #getMantleData(ServerLevel, int, int, int, Class)}, a write creates the mantle region if it
* does not exist, which can touch disk - do not call it per block in a tick loop from the server thread. A
* null {@code data}, a non-Iris level, or a {@code y} outside the engine's height range is a silent no-op;
* remove values with {@link #deleteMantleData(ServerLevel, int, int, int, Class)}.
* <p>
* Values written under a custom type are discarded when Iris trims a mantle region unless the type is
* declared with {@link #retainMantleDataForSlice(Class)}.
*
* @throws IllegalStateException if the engine's mantle has already been closed
*/
public static <T> void setMantleData(ServerLevel level, int x, int y, int z, T data) {
Engine engine = getEngine(level);
if (engine == null || data == null) {
@@ -84,6 +161,12 @@ public final class IrisModdedAPI {
engine.getMantle().getMantle().set(x, y - engine.getMinHeight(), z, data);
}
/**
* Removes any mantle value of {@code type} at world coordinates. A non-Iris level or an out-of-range
* {@code y} is a silent no-op. Like a write, this creates the mantle region if it is absent.
*
* @throws IllegalStateException if the engine's mantle has already been closed
*/
public static <T> void deleteMantleData(ServerLevel level, int x, int y, int z, Class<T> type) {
Engine engine = getEngine(level);
if (engine == null) {
@@ -92,6 +175,14 @@ public final class IrisModdedAPI {
engine.getMantle().getMantle().remove(x, y - engine.getMinHeight(), z, type);
}
/**
* Declares that mantle slices of {@code sliceType} must be kept rather than discarded.
* <p>
* Iris drops slices it does not need once a region's generation data has served its purpose. Any type a mod
* writes with {@link #setMantleData(ServerLevel, int, int, int, Object)} and expects to read back later must be
* declared here first. Registration is by canonical class name, process-wide across every Iris world, and
* cannot be undone - declare it once during mod setup. A null {@code sliceType} is ignored.
*/
public static void retainMantleDataForSlice(Class<?> sliceType) {
if (sliceType == null) {
return;
@@ -99,10 +190,31 @@ public final class IrisModdedAPI {
WorldMaintenance.retainMantleDataForSlice(sliceType.getCanonicalName());
}
/**
* Registers a custom content provider imperatively, for mods that would rather call Iris than ship a
* {@link java.util.ServiceLoader} entry.
* <p>
* Providers are keyed by {@link ModdedDataProvider#modId()}; a second registration under an id already present
* is logged and ignored. {@link ModdedDataProvider#init()} runs during this call, and a throwable it raises is
* logged rather than propagated. A null {@code provider} is ignored.
* <p>
* Ordering matters: Iris only consults providers registered before a pack resolves the block in question, so
* register during mod setup. Registering after Iris's own {@link java.util.ServiceLoader} discovery is
* supported; registering after a world has generated is not - blocks already resolved are not revisited.
*/
public static void registerProvider(ModdedDataProvider provider) {
ModdedCustomContentRegistry.register(provider);
}
/**
* Maps a custom {@code namespace:key} onto a fixed vanilla block state, for mods that only need a static alias
* and no provider class.
* <p>
* {@code state} is a block state string in the same syntax packs use, for example
* {@code minecraft:oak_log[axis=y]}, and is parsed immediately: an unparseable state or an invalid identifier
* is logged and the registration is dropped, so a typo shows up at startup rather than as missing blocks.
* Aliases take precedence over provider lookups for the same key. Null arguments are ignored.
*/
public static void registerCustomBlockData(String namespace, String key, String state) {
ModdedCustomContentRegistry.registerCustomBlockData(namespace, key, state);
}
@@ -22,15 +22,39 @@ import net.minecraft.world.level.block.state.BlockState;
import java.util.Objects;
/**
* A provider's answer to a block lookup: the state to write, and whether the provider wants a second pass once the
* chunk is loaded.
* <p>
* Immutable. Returned from {@link ModdedDataProvider#getBlockData(net.minecraft.resources.Identifier, java.util.Map)};
* construct with {@link #direct(BlockState)} or {@link #deferred(BlockState)} rather than the canonical constructor.
*
* @param state the block state Iris writes. Never null
* @param deferredPlacement whether {@link ModdedDataProvider#processBlockPlacement(ModdedBlockPlacementContext)}
* should run for this position after the chunk is loaded
*/
public record ModdedBlockData(BlockState state, boolean deferredPlacement) {
/**
* @throws NullPointerException if {@code state} is null
*/
public ModdedBlockData {
Objects.requireNonNull(state);
}
/**
* The state is final - Iris writes it during generation and does nothing further.
*/
public static ModdedBlockData direct(BlockState state) {
return new ModdedBlockData(state, false);
}
/**
* {@code state} is a placeholder written during generation; the provider finishes the job in
* {@link ModdedDataProvider#processBlockPlacement(ModdedBlockPlacementContext)} once the chunk is loaded. Use
* when the real block needs a level - a block entity, neighbour state, or mod registries not available on a
* generation thread. Pick a placeholder with the same shape and occlusion as the final block so terrain around
* it generates correctly.
*/
public static ModdedBlockData deferred(BlockState state) {
return new ModdedBlockData(state, true);
}
@@ -27,6 +27,22 @@ import net.minecraft.world.level.block.state.BlockState;
import java.util.Map;
import java.util.Objects;
/**
* Everything a provider needs to finish a deferred block placement, handed to
* {@link ModdedDataProvider#processBlockPlacement(ModdedBlockPlacementContext)} on the server thread.
* <p>
* Immutable, and constructed by Iris rather than by mods. {@code state} is defensively copied; {@code position} is
* already immutable. Because delivery is on the server thread with the chunk loaded, it is safe to write blocks,
* attach block entities and read neighbours from here.
*
* @param engine the Iris engine for this level. Internal Iris type - treat it as an opaque token
* @param level the level to write into. Never null
* @param position the block the placeholder was written at. Never null
* @param blockId the identifier the pack named, without state properties. Never null
* @param blockState the state currently at {@code position} - normally the placeholder returned as deferred, though
* another provider or a later generation stage may have replaced it. Never null
* @param state the {@code [prop=value]} pairs from the pack's key, possibly empty. Never null; unmodifiable
*/
public record ModdedBlockPlacementContext(
Engine engine,
ServerLevel level,
@@ -34,6 +50,9 @@ public record ModdedBlockPlacementContext(
Identifier blockId,
Map<String, String> state,
BlockState blockState) {
/**
* @throws NullPointerException if any component is null
*/
public ModdedBlockPlacementContext {
Objects.requireNonNull(engine);
Objects.requireNonNull(level);
@@ -37,6 +37,22 @@ import java.util.ServiceLoader;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArrayList;
/**
* Registry of {@link ModdedDataProvider} instances and static block-data aliases, and the resolution path Iris
* itself calls into.
* <p>
* Mods should go through {@link IrisModdedAPI#registerProvider(ModdedDataProvider)} and
* {@link IrisModdedAPI#registerCustomBlockData(String, String, String)} rather than calling this class directly;
* the resolution methods here are Iris internals and are public only because the adapter's generation code lives in
* another package.
* <p>
* <b>Threading.</b> Mutation ({@link #register(ModdedDataProvider)},
* {@link #registerCustomBlockData(String, String, String)}, {@link #discover()}) is serialized on the class
* monitor. Resolution ({@link #resolveBlock(String)}, {@link #spawnMob}, {@link #processBlockPlacement}) is lock
* free over a copy-on-write provider list and a concurrent alias map, so it runs on generation threads. Every
* resolution method catches provider throwables, logs them against the provider's mod id, and continues with the
* next provider.
*/
public final class ModdedCustomContentRegistry {
private static final Logger LOGGER = LoggerFactory.getLogger("Iris");
private static final List<ModdedDataProvider> PROVIDERS = new CopyOnWriteArrayList<>();
@@ -47,6 +63,11 @@ public final class ModdedCustomContentRegistry {
private ModdedCustomContentRegistry() {
}
/**
* Registers a static {@code namespace:key} to block-state alias. Invalid identifiers and unparseable states are
* logged and dropped; null arguments are ignored. See
* {@link IrisModdedAPI#registerCustomBlockData(String, String, String)}.
*/
public static synchronized void registerCustomBlockData(String namespace, String key, String state) {
if (namespace == null || key == null || state == null) {
return;
@@ -72,6 +93,11 @@ public final class ModdedCustomContentRegistry {
LOGGER.info("Iris registered custom block data {}:{} -> {}", namespace, key, state);
}
/**
* Registers a provider, rejecting a duplicate {@link ModdedDataProvider#modId()} with a warning and ignoring
* null. {@link ModdedDataProvider#init()} runs here; a throwable it raises is logged, not propagated. See
* {@link IrisModdedAPI#registerProvider(ModdedDataProvider)}.
*/
public static synchronized void register(ModdedDataProvider provider) {
if (provider == null) {
return;
@@ -96,6 +122,15 @@ public final class ModdedCustomContentRegistry {
LOGGER.info("Iris registered custom content provider '{}'", provider.modId());
}
/**
* Runs {@link ServiceLoader} discovery for {@link ModdedDataProvider} against Iris's own class loader, once per
* process. Called during Iris mod initialization; a second call is a no-op that returns an inert handle.
* <p>
* All-or-nothing: a provider whose {@link ModdedDataProvider#init()} throws aborts the pass, restores the
* previous provider and alias state, logs the failing provider's identity, and rethrows.
*
* @return a handle whose {@link Discovery#rollback()} undoes this pass, used by the bootstrap's rollback chain
*/
public static synchronized Discovery discover() {
if (scanned) {
return Discovery.unchanged();
@@ -110,9 +145,12 @@ public final class ModdedCustomContentRegistry {
boolean previousDiscoveryComplete = scanned;
DiscoveryBatch batch = new DiscoveryBatch(previousProviders, previousCustomBlocks);
discoveryBatch = batch;
ModdedDataProvider failingProvider = null;
try {
for (ModdedDataProvider provider : discoveredProviders) {
failingProvider = provider;
batch.add(provider);
failingProvider = null;
}
PROVIDERS.addAll(batch.additions);
CUSTOM_BLOCKS.putAll(batch.customBlocks);
@@ -130,7 +168,8 @@ public final class ModdedCustomContentRegistry {
failure.addSuppressed(rollbackFailure);
}
}
LOGGER.error("Iris custom content provider discovery failed", failure);
LOGGER.warn("Iris custom content provider discovery failed at {}",
providerIdentity(failingProvider), failure);
if (failure instanceof RuntimeException runtimeException) {
throw runtimeException;
}
@@ -143,6 +182,19 @@ public final class ModdedCustomContentRegistry {
}
}
private static String providerIdentity(ModdedDataProvider provider) {
if (provider == null) {
return "the provider service loader";
}
String className = provider.getClass().getName();
try {
String modId = provider.modId();
return modId == null || modId.isBlank() ? className : "provider '" + modId + "' (" + className + ")";
} catch (Throwable identityFailure) {
return className;
}
}
static synchronized boolean hasProvider(String modId) {
for (ModdedDataProvider provider : PROVIDERS) {
if (Objects.equals(provider.modId(), modId)) {
@@ -156,10 +208,19 @@ public final class ModdedCustomContentRegistry {
return scanned;
}
/**
* Whether any provider or alias is registered. Iris checks this to skip custom resolution entirely on a server
* with no integrating mods.
*/
public static boolean hasProviders() {
return !PROVIDERS.isEmpty() || !CUSTOM_BLOCKS.isEmpty();
}
/**
* Resolves a pack block key against aliases first, then each ready provider that claims it, in registration
* order. {@code key} may carry {@code [prop=value]} properties, which are parsed and passed along. Returns null
* when nothing claims it, which lets the caller fall back to air. Called from generation threads.
*/
public static ModdedBlockData resolveBlock(String key) {
if (key == null || (PROVIDERS.isEmpty() && CUSTOM_BLOCKS.isEmpty())) {
return null;
@@ -192,6 +253,11 @@ public final class ModdedCustomContentRegistry {
return null;
}
/**
* Delivers a deferred placement to the first ready provider claiming {@code key}; later providers are not
* consulted for that position. An unparseable key or no matching provider is logged and skipped. Called on the
* server thread with the chunk loaded.
*/
public static void processBlockPlacement(Engine engine, ServerLevel level, BlockPos position, String key) {
Identifier base = parseIdentifier(key);
if (base == null) {
@@ -214,6 +280,10 @@ public final class ModdedCustomContentRegistry {
LOGGER.warn("Iris deferred custom block placement has no provider for {}", key);
}
/**
* Asks each ready provider claiming {@code key} to spawn a custom entity, returning the first non-null result.
* Null when no provider claims it or every attempt declined. Called on the server thread.
*/
public static Entity spawnMob(ServerLevel level, double x, double y, double z, String key) {
if (PROVIDERS.isEmpty() || level == null || key == null) {
return null;
@@ -276,6 +346,10 @@ public final class ModdedCustomContentRegistry {
scanned = discoveryComplete;
}
/**
* Undo handle for one {@link #discover()} pass, so a failure later in Iris's bootstrap can restore the registry
* to its pre-discovery state.
*/
public static final class Discovery {
private final List<ModdedDataProvider> providers;
private final Map<String, BlockState> customBlocks;
@@ -294,6 +368,10 @@ public final class ModdedCustomContentRegistry {
return new Discovery(List.of(), Map.of(), true, false);
}
/**
* Restores the providers and aliases captured before the pass. Idempotent; a no-op on a handle from a
* discovery that did not run.
*/
public synchronized void rollback() {
if (!active) {
return;
@@ -25,28 +25,91 @@ import net.minecraft.world.entity.Entity;
import java.util.Collection;
import java.util.Map;
/**
* Extension point letting a mod resolve its own blocks, items and entities for Iris packs, so a pack can name
* {@code yourmod:something} and have it placed.
* <p>
* Discovered through {@link java.util.ServiceLoader} at Iris mod initialization, or registered imperatively with
* {@link IrisModdedAPI#registerProvider(ModdedDataProvider)}. For ServiceLoader discovery, ship
* {@code META-INF/services/art.arcane.iris.modded.api.ModdedDataProvider} listing the implementation's binary
* name; the class needs a public no-argument constructor.
* <p>
* <b>Threading.</b> Implementations must be thread-safe.
* {@link #getBlockData(Identifier, Map)} is called from generation threads, potentially many at once, for every
* unresolved key a pack names - it must be fast and must not touch world state.
* {@link #processBlockPlacement(ModdedBlockPlacementContext)} and
* {@link #spawnMob(ServerLevel, double, double, double, Identifier)} are called on the server thread, where
* touching the level is safe.
* <p>
* <b>Failure handling.</b> Iris catches throwables from every callback except {@link #init()} during
* ServiceLoader discovery, logs them against {@link #modId()}, and carries on with the remaining providers - one
* broken provider does not stop world generation. A throwable from {@link #init()} during discovery aborts
* discovery and rolls back every provider registered in that pass.
*/
public interface ModdedDataProvider {
/**
* The owning mod's id. Used as the provider's identity: duplicates are rejected, and it labels every log line
* Iris emits about this provider. Must be non-null and stable; returning null aborts discovery.
*/
String modId();
/**
* Whether this provider can answer lookups yet. Iris skips a provider that reports false rather than treating
* it as absent, so a provider whose registries populate late can gate itself instead of returning wrong
* answers. Defaults to true.
*/
default boolean isReady() {
return true;
}
/**
* Every identifier this provider can supply for {@code type}. Used for command suggestion and pack tooling, not
* on the resolution path - {@link #isValidProvider(Identifier, ModdedDataType)} decides that. Return an empty
* collection rather than null.
*/
Collection<Identifier> getTypes(ModdedDataType type);
/**
* Whether this provider claims {@code id} for {@code type}. Called before every resolution callback, on
* generation threads, so keep it to a set lookup. A cheap namespace check is usually enough.
*/
boolean isValidProvider(Identifier id, ModdedDataType type);
/**
* Resolves a claimed block identifier into a concrete block state.
* <p>
* {@code state} holds the {@code [prop=value]} pairs from the pack's key, already parsed and possibly empty;
* never null. Return null to decline, in which case Iris tries the next provider and finally falls back to air.
* Return {@link ModdedBlockData#deferred(net.minecraft.world.level.block.state.BlockState)} when the real block
* needs a loaded level - Iris then writes the placeholder state and calls
* {@link #processBlockPlacement(ModdedBlockPlacementContext)} later. Called from generation threads.
*/
default ModdedBlockData getBlockData(Identifier blockId, Map<String, String> state) {
return null;
}
/**
* Finishes a deferred placement once the chunk is loaded: swap in the real block, attach a block entity, seed
* NBT.
* <p>
* Called on the server thread, once per deferred position, by the first provider that claims the identifier -
* later providers are not consulted for that position. Only fires for states returned as deferred.
*/
default void processBlockPlacement(ModdedBlockPlacementContext context) {
}
/**
* Spawns a claimed custom entity at the given position. Return null to decline and let the next provider try.
* Called on the server thread from Iris's entity spawning.
*/
default Entity spawnMob(ServerLevel level, double x, double y, double z, Identifier entityId) {
return null;
}
/**
* One-time setup, called by Iris immediately after this provider is accepted. A throwable raised here aborts
* ServiceLoader discovery; when registered imperatively it is logged and the provider stays registered.
*/
default void init() {
}
}
@@ -18,8 +18,16 @@
package art.arcane.iris.modded.api;
/**
* The kinds of custom content a {@link ModdedDataProvider} can claim.
* <p>
* Constants may be added. Switch expressions over this enum need a {@code default} arm.
*/
public enum ModdedDataType {
/** Block states, resolved through {@link ModdedDataProvider#getBlockData(net.minecraft.resources.Identifier, java.util.Map)}. */
BLOCK,
/** Item types, claimed for loot and pack tooling. */
ITEM,
/** Entity types, spawned through {@link ModdedDataProvider#spawnMob(net.minecraft.server.level.ServerLevel, double, double, double, net.minecraft.resources.Identifier)}. */
ENTITY
}
@@ -0,0 +1,191 @@
/*
* 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.command;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.framework.IrisStructureLocator;
import art.arcane.iris.modded.IrisModdedChunkGenerator;
import art.arcane.iris.modded.ModdedEngineBootstrap;
import com.mojang.brigadier.context.CommandContext;
import com.mojang.brigadier.suggestion.SuggestionProvider;
import com.mojang.brigadier.suggestion.Suggestions;
import com.mojang.brigadier.suggestion.SuggestionsBuilder;
import net.minecraft.commands.CommandSourceStack;
import net.minecraft.commands.SharedSuggestionProvider;
import net.minecraft.core.Registry;
import net.minecraft.core.registries.Registries;
import net.minecraft.resources.Identifier;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.world.level.levelgen.structure.Structure;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.File;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Set;
import java.util.TreeSet;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentHashMap;
final class ModdedCommandSuggestions {
static final SuggestionProvider<CommandSourceStack> BIOME_KEYS = (CommandContext<CommandSourceStack> context, SuggestionsBuilder builder) -> suggestBiomeKeys(context, builder);
static final SuggestionProvider<CommandSourceStack> REGION_KEYS = (CommandContext<CommandSourceStack> context, SuggestionsBuilder builder) -> suggestRegionKeys(context, builder);
static final SuggestionProvider<CommandSourceStack> OBJECT_KEYS = (CommandContext<CommandSourceStack> context, SuggestionsBuilder builder) -> suggestObjectKeys(context, builder);
static final SuggestionProvider<CommandSourceStack> STRUCTURE_KEYS = (CommandContext<CommandSourceStack> context, SuggestionsBuilder builder) -> suggestStructureKeys(context, builder);
static final SuggestionProvider<CommandSourceStack> POI_TYPES = (CommandContext<CommandSourceStack> context, SuggestionsBuilder builder) -> SharedSuggestionProvider.suggest(List.of("buried_treasure"), builder);
static final SuggestionProvider<CommandSourceStack> PACK_NAMES = (CommandContext<CommandSourceStack> context, SuggestionsBuilder builder) -> suggestPackNames(context, builder);
static final SuggestionProvider<CommandSourceStack> DIMENSION_NAMES = (CommandContext<CommandSourceStack> context, SuggestionsBuilder builder) -> suggestDimensionNames(context, builder);
private static final Logger LOGGER = LoggerFactory.getLogger("Iris");
private static final int TAB_FAILURE_KEYS_MAX = 256;
private static final Set<String> REPORTED_TAB_FAILURES = ConcurrentHashMap.newKeySet();
private ModdedCommandSuggestions() {
}
private static CompletableFuture<Suggestions> suggestBiomeKeys(CommandContext<CommandSourceStack> context, SuggestionsBuilder builder) {
ModdedCommandFeedback.tab(context.getSource());
try {
Engine engine = IrisModdedCommands.engineFor(context.getSource().getLevel());
if (engine != null) {
return SharedSuggestionProvider.suggest(engine.getData().getBiomeLoader().getPossibleKeys(), builder);
}
} catch (Throwable e) {
warnTabFailure("biome keys", context.getSource(), e);
}
return builder.buildFuture();
}
private static CompletableFuture<Suggestions> suggestRegionKeys(CommandContext<CommandSourceStack> context, SuggestionsBuilder builder) {
ModdedCommandFeedback.tab(context.getSource());
try {
Engine engine = IrisModdedCommands.engineFor(context.getSource().getLevel());
if (engine != null) {
return SharedSuggestionProvider.suggest(engine.getDimension().getRegions(), builder);
}
} catch (Throwable e) {
warnTabFailure("region keys", context.getSource(), e);
}
return builder.buildFuture();
}
private static CompletableFuture<Suggestions> suggestObjectKeys(CommandContext<CommandSourceStack> context, SuggestionsBuilder builder) {
ModdedCommandFeedback.tab(context.getSource());
try {
Engine engine = IrisModdedCommands.engineFor(context.getSource().getLevel());
if (engine != null) {
return SharedSuggestionProvider.suggest(engine.getData().getObjectLoader().getPossibleKeys(), builder);
}
} catch (Throwable e) {
warnTabFailure("object keys", context.getSource(), e);
}
return builder.buildFuture();
}
static CompletableFuture<Suggestions> suggestStructureKeys(CommandContext<CommandSourceStack> context, SuggestionsBuilder builder) {
ModdedCommandFeedback.tab(context.getSource());
try {
Engine engine = IrisModdedCommands.engineFor(context.getSource().getLevel());
Collection<String> irisKeys = engine == null ? List.of() : IrisStructureLocator.placedKeys(engine);
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 e) {
warnTabFailure("structure keys", context.getSource(), e);
}
return builder.buildFuture();
}
static void warnTabFailure(String suggestion, CommandSourceStack source, Throwable error) {
String origin = tabOrigin(source);
if (!REPORTED_TAB_FAILURES.add(suggestion + '|' + origin + '|' + error.getClass().getName())) {
return;
}
if (REPORTED_TAB_FAILURES.size() > TAB_FAILURE_KEYS_MAX) {
REPORTED_TAB_FAILURES.clear();
}
LOGGER.warn("Iris tab-complete for {} in {} failed; suggestions will be empty", suggestion, origin, error);
}
private static String tabOrigin(CommandSourceStack source) {
if (source == null) {
return "<no source>";
}
try {
return source.getLevel().dimension().identifier().toString();
} catch (Throwable originFailure) {
return "<no level>";
}
}
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) {
ModdedCommandFeedback.tab(context.getSource());
Set<String> names = new TreeSet<>();
names.add("overworld");
try {
File packs = ModdedEngineBootstrap.loader().configDir().resolve("irisworldgen").resolve("packs").toFile();
File[] children = packs.listFiles();
if (children != null) {
for (File child : children) {
if (!child.isDirectory()) {
continue;
}
String packName = child.getName();
names.add(packName);
File dimensions = new File(child, "dimensions");
File[] dimensionFiles = dimensions.listFiles(
(File directory, String name) -> name.endsWith(".json"));
if (dimensionFiles == null) {
continue;
}
for (File dimensionFile : dimensionFiles) {
String fileName = dimensionFile.getName();
names.add(packName + ":" + fileName.substring(0, fileName.length() - 5));
}
}
}
} catch (Throwable e) {
warnTabFailure("pack names", context.getSource(), e);
}
return SharedSuggestionProvider.suggest(names, builder);
}
private static CompletableFuture<Suggestions> suggestDimensionNames(CommandContext<CommandSourceStack> context, SuggestionsBuilder builder) {
ModdedCommandFeedback.tab(context.getSource());
List<String> names = new ArrayList<>();
for (ServerLevel level : context.getSource().getServer().getAllLevels()) {
if (level.getChunkSource().getGenerator() instanceof IrisModdedChunkGenerator) {
names.add(level.dimension().identifier().toString());
}
}
return SharedSuggestionProvider.suggest(names, builder);
}
}
@@ -0,0 +1,344 @@
/*
* 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.command;
import com.mojang.brigadier.arguments.BoolArgumentType;
import com.mojang.brigadier.arguments.IntegerArgumentType;
import com.mojang.brigadier.arguments.LongArgumentType;
import com.mojang.brigadier.arguments.StringArgumentType;
import com.mojang.brigadier.builder.ArgumentBuilder;
import com.mojang.brigadier.builder.LiteralArgumentBuilder;
import com.mojang.brigadier.builder.RequiredArgumentBuilder;
import com.mojang.brigadier.context.CommandContext;
import net.minecraft.commands.CommandSourceStack;
import net.minecraft.commands.Commands;
import net.minecraft.commands.arguments.DimensionArgument;
import net.minecraft.commands.arguments.EntityArgument;
import net.minecraft.resources.Identifier;
import java.util.function.Predicate;
final class ModdedCommandTree {
private static final Predicate<CommandSourceStack> GATE = Commands.hasPermission(Commands.LEVEL_GAMEMASTERS);
private ModdedCommandTree() {
}
static LiteralArgumentBuilder<CommandSourceStack> rootTree() {
LiteralArgumentBuilder<CommandSourceStack> root = Commands.literal("iris");
root.executes((CommandContext<CommandSourceStack> context) -> ModdedCommandHelp.send(context.getSource(), ""));
root.then(helpTree());
root.then(Commands.literal("version")
.executes((CommandContext<CommandSourceStack> context) -> IrisModdedCommands.version(context.getSource())));
root.then(Commands.literal("info").requires(GATE)
.executes((CommandContext<CommandSourceStack> context) -> IrisModdedCommands.info(context.getSource(), null))
.then(Commands.argument("dimension", StringArgumentType.greedyString()).suggests(ModdedCommandSuggestions.DIMENSION_NAMES)
.executes((CommandContext<CommandSourceStack> context) -> IrisModdedCommands.info(context.getSource(), StringArgumentType.getString(context, "dimension")))));
root.then(ModdedWhatCommands.tree());
root.then(teleportTree("teleport"));
root.then(teleportTree("tp"));
root.then(Commands.literal("evacuate").requires(GATE)
.executes((CommandContext<CommandSourceStack> context) -> IrisModdedCommands.evacuate(context.getSource(), null))
.then(Commands.argument("dimension", DimensionArgument.dimension()).suggests(ModdedCommandSuggestions.DIMENSION_NAMES)
.executes((CommandContext<CommandSourceStack> context) -> IrisModdedCommands.evacuate(context.getSource(), DimensionArgument.getDimension(context, "dimension")))));
root.then(Commands.literal("debug").requires(GATE)
.executes((CommandContext<CommandSourceStack> context) -> IrisModdedCommands.debug(context.getSource())));
root.then(Commands.literal("reload").requires(GATE)
.executes((CommandContext<CommandSourceStack> context) -> IrisModdedCommands.reload(context.getSource())));
root.then(Commands.literal("height").requires(GATE)
.executes((CommandContext<CommandSourceStack> context) -> IrisModdedCommands.height(context.getSource())));
root.then(Commands.literal("worlds").requires(GATE)
.executes((CommandContext<CommandSourceStack> context) -> IrisModdedCommands.info(context.getSource(), null)));
root.then(Commands.literal("accesslist").requires(GATE)
.executes((CommandContext<CommandSourceStack> context) -> IrisModdedCommands.info(context.getSource(), null)));
root.then(gotoTree("goto"));
root.then(gotoTree("find"));
root.then(Commands.literal("seed").requires(GATE)
.executes((CommandContext<CommandSourceStack> context) -> IrisModdedCommands.seed(context.getSource())));
root.then(goldenhashTree("goldenhash"));
root.then(goldenhashTree("gold"));
root.then(downloadTree("download"));
root.then(downloadTree("dl"));
root.then(metricsTree("metrics"));
root.then(metricsTree("measure"));
root.then(regenTree("regen"));
root.then(regenTree("rg"));
root.then(pregenTree("pregen"));
root.then(pregenTree("pregenerate"));
root.then(Commands.literal("wand").requires(GATE)
.executes((CommandContext<CommandSourceStack> context) -> ModdedObjectCommands.giveWand(context.getSource())));
root.then(Commands.literal("dust").requires(GATE)
.executes((CommandContext<CommandSourceStack> context) -> ModdedObjectCommands.giveDust(context.getSource())));
root.then(Commands.literal("d").requires(GATE)
.executes((CommandContext<CommandSourceStack> context) -> ModdedObjectCommands.giveDust(context.getSource())));
root.then(ModdedObjectCommands.tree("object"));
root.then(ModdedObjectCommands.tree("o"));
root.then(editTree());
root.then(createTree("create"));
root.then(createTree("c"));
root.then(ModdedStudioCommands.tree("studio"));
root.then(ModdedStudioCommands.tree("std"));
root.then(ModdedStudioCommands.tree("s"));
root.then(ModdedPackCommands.tree("pack"));
root.then(ModdedPackCommands.tree("pk"));
root.then(ModdedWorldCommands.tree("world"));
root.then(ModdedWorldCommands.tree("w"));
root.then(ModdedDatapackCommands.tree("datapack"));
root.then(ModdedDatapackCommands.tree("datapacks"));
root.then(ModdedDatapackCommands.tree("dp"));
root.then(ModdedStructureCommands.tree("structure"));
root.then(ModdedStructureCommands.tree("struct"));
root.then(ModdedStructureCommands.tree("str"));
root.then(ModdedDeveloperCommands.tree("developer"));
root.then(ModdedDeveloperCommands.tree("dev"));
return root;
}
private static LiteralArgumentBuilder<CommandSourceStack> createTree(String name) {
return Commands.literal(name).requires(GATE)
.then(Commands.argument("name", StringArgumentType.word())
.executes((CommandContext<CommandSourceStack> context) ->
ModdedWorldCommands.createWorld(
context.getSource(),
StringArgumentType.getString(context, "name"),
"overworld",
1337L))
.then(Commands.argument("pack", StringArgumentType.string()).suggests(ModdedCommandSuggestions.PACK_NAMES)
.executes((CommandContext<CommandSourceStack> context) -> ModdedWorldCommands.createWorld(context.getSource(),
StringArgumentType.getString(context, "name"),
StringArgumentType.getString(context, "pack"),
1337L))
.then(Commands.argument("seed", LongArgumentType.longArg())
.executes((CommandContext<CommandSourceStack> context) -> ModdedWorldCommands.createWorld(context.getSource(),
StringArgumentType.getString(context, "name"),
StringArgumentType.getString(context, "pack"),
LongArgumentType.getLong(context, "seed"))))));
}
private static LiteralArgumentBuilder<CommandSourceStack> teleportTree(String name) {
return Commands.literal(name).requires(GATE)
.then(Commands.argument("dimension", DimensionArgument.dimension()).suggests(ModdedCommandSuggestions.DIMENSION_NAMES)
.executes((CommandContext<CommandSourceStack> context) ->
IrisModdedCommands.tp(context.getSource(), DimensionArgument.getDimension(context, "dimension"), null))
.then(Commands.argument("player", EntityArgument.player())
.executes((CommandContext<CommandSourceStack> context) ->
IrisModdedCommands.tp(context.getSource(), DimensionArgument.getDimension(context, "dimension"),
EntityArgument.getPlayer(context, "player")))));
}
private static LiteralArgumentBuilder<CommandSourceStack> helpTree() {
return Commands.literal("help")
.executes((CommandContext<CommandSourceStack> context) -> ModdedCommandHelp.send(context.getSource(), ""))
.then(Commands.argument("section", StringArgumentType.greedyString())
.executes((CommandContext<CommandSourceStack> context) -> ModdedCommandHelp.send(context.getSource(), StringArgumentType.getString(context, "section"))));
}
private static LiteralArgumentBuilder<CommandSourceStack> downloadTree(String name) {
return Commands.literal(name).requires(GATE)
.then(Commands.argument("pack", StringArgumentType.word()).suggests(ModdedCommandSuggestions.PACK_NAMES)
.executes((CommandContext<CommandSourceStack> context) ->
IrisModdedCommands.download(context.getSource(),
StringArgumentType.getString(context, "pack"), "stable", false))
.then(Commands.literal("force")
.executes((CommandContext<CommandSourceStack> context) ->
IrisModdedCommands.download(context.getSource(),
StringArgumentType.getString(context, "pack"), "stable", true)))
.then(Commands.argument("overwrite", BoolArgumentType.bool())
.executes((CommandContext<CommandSourceStack> context) ->
IrisModdedCommands.download(context.getSource(),
StringArgumentType.getString(context, "pack"), "stable",
BoolArgumentType.getBool(context, "overwrite"))))
.then(Commands.argument("branch", StringArgumentType.word())
.executes((CommandContext<CommandSourceStack> context) ->
IrisModdedCommands.download(context.getSource(),
StringArgumentType.getString(context, "pack"),
StringArgumentType.getString(context, "branch"), false))
.then(Commands.literal("force")
.executes((CommandContext<CommandSourceStack> context) ->
IrisModdedCommands.download(context.getSource(),
StringArgumentType.getString(context, "pack"),
StringArgumentType.getString(context, "branch"), true)))
.then(Commands.argument("overwrite", BoolArgumentType.bool())
.executes((CommandContext<CommandSourceStack> context) ->
IrisModdedCommands.download(context.getSource(),
StringArgumentType.getString(context, "pack"),
StringArgumentType.getString(context, "branch"),
BoolArgumentType.getBool(context, "overwrite"))))));
}
private static LiteralArgumentBuilder<CommandSourceStack> metricsTree(String name) {
return Commands.literal(name).requires(GATE)
.executes((CommandContext<CommandSourceStack> context) -> IrisModdedCommands.metrics(context.getSource()));
}
private static LiteralArgumentBuilder<CommandSourceStack> regenTree(String name) {
return Commands.literal(name).requires(GATE)
.executes((CommandContext<CommandSourceStack> context) -> IrisModdedCommands.regen(context.getSource(), 0))
.then(Commands.argument("radius", IntegerArgumentType.integer(0, 64))
.executes((CommandContext<CommandSourceStack> context) -> IrisModdedCommands.regen(context.getSource(), IntegerArgumentType.getInteger(context, "radius"))));
}
private static LiteralArgumentBuilder<CommandSourceStack> gotoTree(String name) {
return Commands.literal(name).requires(GATE)
.executes((CommandContext<CommandSourceStack> context) -> ModdedCommandHelp.send(context.getSource(), name))
.then(Commands.literal("biome")
.then(Commands.argument("key", StringArgumentType.greedyString()).suggests(ModdedCommandSuggestions.BIOME_KEYS)
.executes((CommandContext<CommandSourceStack> context) -> ModdedLocateCommands.gotoBiome(context.getSource(), StringArgumentType.getString(context, "key")))))
.then(Commands.literal("region")
.then(Commands.argument("key", StringArgumentType.greedyString()).suggests(ModdedCommandSuggestions.REGION_KEYS)
.executes((CommandContext<CommandSourceStack> context) -> ModdedLocateCommands.gotoRegion(context.getSource(), StringArgumentType.getString(context, "key")))))
.then(Commands.literal("object")
.then(Commands.argument("key", StringArgumentType.greedyString()).suggests(ModdedCommandSuggestions.OBJECT_KEYS)
.executes((CommandContext<CommandSourceStack> context) -> ModdedLocateCommands.gotoObject(context.getSource(), StringArgumentType.getString(context, "key")))))
.then(Commands.literal("structure")
.then(Commands.argument("key", StringArgumentType.greedyString()).suggests(ModdedCommandSuggestions.STRUCTURE_KEYS)
.executes((CommandContext<CommandSourceStack> context) -> ModdedLocateCommands.gotoStructure(context.getSource(), StringArgumentType.getString(context, "key")))))
.then(Commands.literal("poi")
.then(Commands.argument("type", StringArgumentType.greedyString()).suggests(ModdedCommandSuggestions.POI_TYPES)
.executes((CommandContext<CommandSourceStack> context) -> ModdedLocateCommands.gotoPoi(context.getSource(), StringArgumentType.getString(context, "type")))));
}
private static LiteralArgumentBuilder<CommandSourceStack> pregenTree(String name) {
RequiredArgumentBuilder<CommandSourceStack, Integer> radius = Commands.argument("radius", IntegerArgumentType.integer(1, 100000))
.executes((CommandContext<CommandSourceStack> context) -> ModdedPregenCommands.pregenStart(context, false, false, false, false, false));
attachPregenCenter(radius, false);
attachPregenFlags(radius, false, false, false, false, false);
RequiredArgumentBuilder<CommandSourceStack, Identifier> dimension = Commands.argument("dimension", DimensionArgument.dimension()).suggests(ModdedCommandSuggestions.DIMENSION_NAMES)
.executes((CommandContext<CommandSourceStack> context) -> ModdedPregenCommands.pregenStart(context, true, false, false, false, false));
attachPregenCenter(dimension, true);
attachPregenFlags(dimension, true, false, false, false, false);
radius.then(dimension);
return Commands.literal(name).requires(GATE)
.executes((CommandContext<CommandSourceStack> context) -> ModdedCommandHelp.send(context.getSource(), name))
.then(Commands.literal("start")
.then(radius))
.then(Commands.literal("stop")
.executes((CommandContext<CommandSourceStack> context) -> ModdedPregenCommands.pregenStop(context.getSource())))
.then(Commands.literal("x")
.executes((CommandContext<CommandSourceStack> context) -> ModdedPregenCommands.pregenStop(context.getSource())))
.then(Commands.literal("pause")
.executes((CommandContext<CommandSourceStack> context) -> ModdedPregenCommands.pregenPause(context.getSource())))
.then(Commands.literal("resume")
.executes((CommandContext<CommandSourceStack> context) -> ModdedPregenCommands.pregenPause(context.getSource())))
.then(Commands.literal("status")
.executes((CommandContext<CommandSourceStack> context) -> ModdedPregenCommands.pregenStatus(context.getSource())));
}
private static void attachPregenCenter(ArgumentBuilder<CommandSourceStack, ?> node, boolean withDimension) {
RequiredArgumentBuilder<CommandSourceStack, Integer> z = Commands.argument("z", IntegerArgumentType.integer())
.executes((CommandContext<CommandSourceStack> context) -> ModdedPregenCommands.pregenStart(context, withDimension, true, false, false, false));
attachPregenFlags(z, withDimension, true, false, false, false);
node.then(Commands.literal("at")
.then(Commands.argument("x", IntegerArgumentType.integer())
.then(z)));
}
private static void attachPregenFlags(ArgumentBuilder<CommandSourceStack, ?> node, boolean withDimension, boolean withCenter, boolean gui, boolean sync, boolean nocache) {
if (!gui) {
node.then(pregenFlagNode("gui", withDimension, withCenter, true, sync, nocache));
}
if (!sync) {
node.then(pregenFlagNode("sync", withDimension, withCenter, gui, true, nocache));
}
if (!nocache) {
node.then(pregenFlagNode("nocache", withDimension, withCenter, gui, sync, true));
}
}
private static LiteralArgumentBuilder<CommandSourceStack> pregenFlagNode(String name, boolean withDimension, boolean withCenter, boolean gui, boolean sync, boolean nocache) {
LiteralArgumentBuilder<CommandSourceStack> flag = Commands.literal(name)
.executes((CommandContext<CommandSourceStack> context) -> ModdedPregenCommands.pregenStart(context, withDimension, withCenter, gui, sync, nocache));
attachPregenFlags(flag, withDimension, withCenter, gui, sync, nocache);
return flag;
}
private static LiteralArgumentBuilder<CommandSourceStack> goldenhashTree(String name) {
LiteralArgumentBuilder<CommandSourceStack> radiusAndThreads = Commands.literal(name).requires(GATE)
.executes((CommandContext<CommandSourceStack> context) -> IrisModdedCommands.goldenhash(context.getSource(), 8, 8, ModdedGoldenHash.Mode.AUTO));
attachModes(radiusAndThreads, (CommandContext<CommandSourceStack> context) -> 8, (CommandContext<CommandSourceStack> context) -> 8);
com.mojang.brigadier.builder.RequiredArgumentBuilder<CommandSourceStack, Integer> radius = Commands.argument("radius", IntegerArgumentType.integer(0, 256))
.executes((CommandContext<CommandSourceStack> context) -> IrisModdedCommands.goldenhash(context.getSource(), IntegerArgumentType.getInteger(context, "radius"), 8, ModdedGoldenHash.Mode.AUTO));
attachModes(radius, (CommandContext<CommandSourceStack> context) -> IntegerArgumentType.getInteger(context, "radius"), (CommandContext<CommandSourceStack> context) -> 8);
com.mojang.brigadier.builder.RequiredArgumentBuilder<CommandSourceStack, Integer> threads = Commands.argument("threads", IntegerArgumentType.integer(1, 64))
.executes((CommandContext<CommandSourceStack> context) -> IrisModdedCommands.goldenhash(context.getSource(), IntegerArgumentType.getInteger(context, "radius"), IntegerArgumentType.getInteger(context, "threads"), ModdedGoldenHash.Mode.AUTO));
attachModes(threads, (CommandContext<CommandSourceStack> context) -> IntegerArgumentType.getInteger(context, "radius"), (CommandContext<CommandSourceStack> context) -> IntegerArgumentType.getInteger(context, "threads"));
radius.then(threads);
radiusAndThreads.then(radius);
return radiusAndThreads;
}
private interface IntExtractor {
int extract(CommandContext<CommandSourceStack> context);
}
private static void attachModes(com.mojang.brigadier.builder.ArgumentBuilder<CommandSourceStack, ?> node, IntExtractor radius, IntExtractor threads) {
node.then(Commands.literal("capture")
.executes((CommandContext<CommandSourceStack> context) -> IrisModdedCommands.goldenhash(context.getSource(), radius.extract(context), threads.extract(context), ModdedGoldenHash.Mode.CAPTURE)));
node.then(Commands.literal("verify")
.executes((CommandContext<CommandSourceStack> context) -> IrisModdedCommands.goldenhash(context.getSource(), radius.extract(context), threads.extract(context), ModdedGoldenHash.Mode.VERIFY)));
}
private static LiteralArgumentBuilder<CommandSourceStack> editTree() {
return Commands.literal("edit").requires(GATE)
.executes((CommandContext<CommandSourceStack> context) -> ModdedCommandHelp.send(context.getSource(), "edit"))
.then(Commands.literal("biome")
.executes((CommandContext<CommandSourceStack> context) -> ModdedEditCommands.editBiome(context.getSource(), null))
.then(Commands.argument("key", StringArgumentType.greedyString()).suggests(ModdedCommandSuggestions.BIOME_KEYS)
.executes((CommandContext<CommandSourceStack> context) -> ModdedEditCommands.editBiome(context.getSource(), StringArgumentType.getString(context, "key")))))
.then(Commands.literal("b")
.executes((CommandContext<CommandSourceStack> context) -> ModdedEditCommands.editBiome(context.getSource(), null))
.then(Commands.argument("key", StringArgumentType.greedyString()).suggests(ModdedCommandSuggestions.BIOME_KEYS)
.executes((CommandContext<CommandSourceStack> context) -> ModdedEditCommands.editBiome(context.getSource(), StringArgumentType.getString(context, "key")))))
.then(Commands.literal("region")
.executes((CommandContext<CommandSourceStack> context) -> ModdedEditCommands.editRegion(context.getSource(), null))
.then(Commands.argument("key", StringArgumentType.greedyString()).suggests(ModdedCommandSuggestions.REGION_KEYS)
.executes((CommandContext<CommandSourceStack> context) -> ModdedEditCommands.editRegion(context.getSource(), StringArgumentType.getString(context, "key")))))
.then(Commands.literal("r")
.executes((CommandContext<CommandSourceStack> context) -> ModdedEditCommands.editRegion(context.getSource(), null))
.then(Commands.argument("key", StringArgumentType.greedyString()).suggests(ModdedCommandSuggestions.REGION_KEYS)
.executes((CommandContext<CommandSourceStack> context) -> ModdedEditCommands.editRegion(context.getSource(), StringArgumentType.getString(context, "key")))))
.then(Commands.literal("dimension")
.executes((CommandContext<CommandSourceStack> context) -> ModdedEditCommands.editDimension(context.getSource())))
.then(Commands.literal("d")
.executes((CommandContext<CommandSourceStack> context) -> ModdedEditCommands.editDimension(context.getSource())));
}
}
@@ -108,7 +108,7 @@ public final class ModdedDustRevealer {
pos.immutable(),
key,
level.getMinY(),
level.getMaxY(),
level.getMaxY() + 1,
new AtomicBoolean());
RevealRun previous = ACTIVE_RUNS.put(player.getUUID(), run);
if (previous != null) {
@@ -144,14 +144,14 @@ public final class ModdedDustRevealer {
run.key(),
run.engine().getMinHeight(),
run.minY(),
run.maxY(),
run.maxYExclusive(),
run.cancelled(),
(int x, int relativeY, int z) ->
run.engine().getObjectPlacementKey(x, relativeY, z));
}
static List<BlockPos> collect(BlockPos origin, String key, int engineMinY,
int minY, int maxY, AtomicBoolean cancelled,
int minY, int maxYExclusive, AtomicBoolean cancelled,
ObjectPlacementLookup lookup) {
List<BlockPos> hits = new ArrayList<>();
Set<BlockPos> visited = new HashSet<>();
@@ -169,7 +169,7 @@ public final class ModdedDustRevealer {
}
BlockPos next = current.offset(dx, dy, dz);
if (next.getY() < minY
|| next.getY() >= maxY
|| next.getY() >= maxYExclusive
|| !visited.add(next)) {
continue;
}
@@ -483,7 +483,7 @@ public final class ModdedDustRevealer {
BlockPos origin,
String key,
int minY,
int maxY,
int maxYExclusive,
AtomicBoolean cancelled
) {
}
@@ -0,0 +1,133 @@
/*
* 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.command;
import art.arcane.iris.core.gui.GuiHost;
import art.arcane.iris.core.loader.IrisRegistrant;
import art.arcane.iris.core.localization.IrisLanguage;
import art.arcane.iris.core.localization.ModdedCommandMessages;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.object.IrisBiome;
import art.arcane.iris.engine.object.IrisRegion;
import art.arcane.volmlib.util.localization.MessageArgument;
import net.minecraft.commands.CommandSourceStack;
import net.minecraft.core.BlockPos;
import net.minecraft.server.level.ServerPlayer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.awt.Desktop;
import java.io.File;
final class ModdedEditCommands {
private static final Logger LOGGER = LoggerFactory.getLogger("Iris");
private ModdedEditCommands() {
}
static int editBiome(CommandSourceStack source, String key) {
Engine engine = IrisModdedCommands.engineFor(source.getLevel());
if (engine == null) {
IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_THIS_DIMENSION_IS_NOT_GENERATED_BY_IRIS));
return 0;
}
IrisBiome biome;
if (key == null || key.isBlank()) {
ServerPlayer player = source.getPlayer();
if (player == null) {
IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_CONSOLE_MUST_NAME_BIOME_IRIS_EDIT_BIOME_KEY));
return 0;
}
BlockPos pos = player.blockPosition();
try {
biome = engine.getBiome(pos.getX(), pos.getY() - engine.getMinHeight(), pos.getZ());
} catch (Throwable e) {
IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_BIOME_LOOKUP_FAILED, MessageArgument.untrusted("value", e.getClass().getSimpleName())));
return 0;
}
} else {
biome = engine.getData().getBiomeLoader().load(key.trim());
if (biome == null) {
IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_UNKNOWN_BIOME, MessageArgument.untrusted("key", key)));
return 0;
}
}
return openJson(source, biome);
}
static int editRegion(CommandSourceStack source, String key) {
Engine engine = IrisModdedCommands.engineFor(source.getLevel());
if (engine == null) {
IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_THIS_DIMENSION_IS_NOT_GENERATED_BY_IRIS_2));
return 0;
}
IrisRegion region;
if (key == null || key.isBlank()) {
ServerPlayer player = source.getPlayer();
if (player == null) {
IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_CONSOLE_MUST_NAME_REGION_IRIS_EDIT_REGION_KEY));
return 0;
}
BlockPos pos = player.blockPosition();
try {
region = engine.getRegion(pos.getX(), pos.getZ());
} catch (Throwable e) {
IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_REGION_LOOKUP_FAILED, MessageArgument.untrusted("value", e.getClass().getSimpleName())));
return 0;
}
} else {
region = engine.getData().getRegionLoader().load(key.trim());
if (region == null) {
IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_UNKNOWN_REGION, MessageArgument.untrusted("key", key)));
return 0;
}
}
return openJson(source, region);
}
static int editDimension(CommandSourceStack source) {
Engine engine = IrisModdedCommands.engineFor(source.getLevel());
if (engine == null) {
IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_THIS_DIMENSION_IS_NOT_GENERATED_BY_IRIS_3));
return 0;
}
return openJson(source, engine.getDimension());
}
private static int openJson(CommandSourceStack source, IrisRegistrant registrant) {
if (!GuiHost.isAvailable() || !Desktop.isDesktopSupported()) {
IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_CANNOT_OPEN_FILES_HERE, MessageArgument.untrusted("value", ModdedGuiHost.guiUnavailableReason())));
return 0;
}
if (registrant == null || registrant.getLoadFile() == null || !registrant.getLoadFile().isFile()) {
IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_CANNOT_FIND_FILE_PERHAPS_IT_WAS_NOT_LOADED_DIRECTLY_FROM));
return 0;
}
File file = registrant.getLoadFile();
try {
Desktop.getDesktop().open(file);
} catch (Throwable e) {
LOGGER.error("Iris edit failed to open {}", file, e);
IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_COULD_NOT_OPEN, MessageArgument.untrusted("value", file.getName()), MessageArgument.untrusted("value2", e.getClass().getSimpleName())));
return 0;
}
IrisModdedCommands.ok(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_OPENING_YOUR_EDITOR, MessageArgument.untrusted("value", registrant.getTypeName()), MessageArgument.untrusted("value2", file.getName())));
return 1;
}
}
@@ -0,0 +1,535 @@
/*
* 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.command;
import art.arcane.iris.core.localization.IrisLanguage;
import art.arcane.iris.core.localization.ModdedCommandMessages;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.framework.GenerationSessionException;
import art.arcane.iris.engine.framework.GenerationSessionLease;
import art.arcane.iris.engine.framework.IrisStructureLocator;
import art.arcane.iris.engine.framework.Locator;
import art.arcane.iris.engine.framework.NativeStructureGenerationPolicy;
import art.arcane.iris.engine.framework.WrongEngineBroException;
import art.arcane.iris.engine.object.IrisBiome;
import art.arcane.iris.engine.object.IrisNativeStructureDecision;
import art.arcane.iris.engine.object.IrisRegion;
import art.arcane.iris.engine.object.NativeStructureGenerationStatus;
import art.arcane.iris.modded.IrisModdedChunkGenerator;
import art.arcane.iris.util.project.context.IrisContext;
import art.arcane.volmlib.util.localization.MessageArgument;
import art.arcane.volmlib.util.math.Position2;
import com.mojang.datafixers.util.Pair;
import net.minecraft.commands.CommandSourceStack;
import net.minecraft.core.BlockPos;
import net.minecraft.core.Holder;
import net.minecraft.core.HolderSet;
import net.minecraft.core.Registry;
import net.minecraft.core.registries.Registries;
import net.minecraft.resources.Identifier;
import net.minecraft.server.MinecraftServer;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.world.entity.Relative;
import net.minecraft.world.level.chunk.ChunkGenerator;
import net.minecraft.world.level.levelgen.Heightmap;
import net.minecraft.world.level.levelgen.structure.Structure;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.Optional;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.CancellationException;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionException;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutionException;
final class ModdedLocateCommands {
private static final Logger LOGGER = LoggerFactory.getLogger("Iris");
private static final long LOCATE_TIMEOUT_MS = 120000L;
private static final int NATIVE_STRUCTURE_LOCATE_RADIUS = 100;
private static final ConcurrentHashMap<UUID, CompletableFuture<Position2>> ACTIVE_LOCATE_REQUESTS = new ConcurrentHashMap<>();
private ModdedLocateCommands() {
}
static int gotoBiome(CommandSourceStack source, String key) {
ServerPlayer player = source.getPlayer();
if (player == null) {
IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_THIS_COMMAND_CAN_ONLY_BE_USED_BY_PLAYERS_3));
return 0;
}
ServerLevel level = source.getLevel();
Engine engine = IrisModdedCommands.engineFor(level);
if (engine == null) {
IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_THIS_DIMENSION_IS_NOT_GENERATED_BY_IRIS_8));
return 0;
}
IrisBiome biome = engine.getData().getBiomeLoader().load(key.trim());
if (biome == null) {
IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_UNKNOWN_BIOME_2, MessageArgument.untrusted("key", key)));
return 0;
}
locate(source, level, engine, player, Locator.surfaceBiome(biome.getLoadKey()), "biome " + biome.getLoadKey());
return 1;
}
static int gotoRegion(CommandSourceStack source, String key) {
ServerPlayer player = source.getPlayer();
if (player == null) {
IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_THIS_COMMAND_CAN_ONLY_BE_USED_BY_PLAYERS_4));
return 0;
}
ServerLevel level = source.getLevel();
Engine engine = IrisModdedCommands.engineFor(level);
if (engine == null) {
IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_THIS_DIMENSION_IS_NOT_GENERATED_BY_IRIS_9));
return 0;
}
IrisRegion region = engine.getData().getRegionLoader().load(key.trim());
if (region == null) {
IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_UNKNOWN_REGION_2, MessageArgument.untrusted("key", key)));
return 0;
}
if (!engine.getDimension().getRegions().contains(region.getLoadKey())) {
IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_IS_NOT_DEFINED_DIMENSION, MessageArgument.untrusted("value", region.getLoadKey())));
return 0;
}
locate(source, level, engine, player, Locator.region(region.getLoadKey()), "region " + region.getLoadKey());
return 1;
}
static int gotoObject(CommandSourceStack source, String keyRaw) {
ServerLevel level = source.getLevel();
Engine engine = IrisModdedCommands.engineFor(level);
if (engine == null) {
IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_THIS_DIMENSION_IS_NOT_GENERATED_BY_IRIS_10));
return 0;
}
String key = keyRaw.trim();
if (!engine.hasObjectPlacement(key)) {
IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_IS_NOT_CONFIGURED_ANY_REGION_BIOME_OBJECT_PLACEMENTS_OBJECT_KEYS, MessageArgument.untrusted("key", key), MessageArgument.untrusted("value", engine.getData().getObjectLoader().getPossibleKeys().length)));
return 0;
}
ServerPlayer player = source.getPlayer();
if (player == null) {
IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_THIS_COMMAND_CAN_ONLY_BE_USED_BY_PLAYERS_OBJECT_KEY, MessageArgument.untrusted("key", key), MessageArgument.untrusted("value", engine.getData().getObjectLoader().getPossibleKeys().length)));
return 0;
}
locate(source, level, engine, player, Locator.object(key), "object " + key);
return 1;
}
static int gotoStructure(CommandSourceStack source, String keyRaw) {
ServerLevel level = source.getLevel();
Engine engine = IrisModdedCommands.engineFor(level);
if (engine == null) {
IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_THIS_DIMENSION_IS_NOT_GENERATED_BY_IRIS_11));
return 0;
}
String key = keyRaw.trim();
if (key.isEmpty()) {
IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_NAME_IRIS_NATIVE_STRUCTURE_LOCATE));
return 0;
}
ServerPlayer player = source.getPlayer();
if (player == null) {
IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_THIS_COMMAND_CAN_ONLY_BE_USED_BY_PLAYERS_5));
return 0;
}
Optional<NativeStructureTarget> resolved = resolveNativeStructure(source, level, engine, key);
if (resolved.isEmpty()) {
if (IrisStructureLocator.isPlaced(engine, key)) {
locateIrisStructure(source, level, engine, player, key);
return 1;
}
IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_UNKNOWN_STRUCTURE_USE_TAB_COMPLETION_CHOOSE_IRIS_PLACEMENT_REGISTERED_NATIVE, MessageArgument.untrusted("key", key)));
return 0;
}
NativeStructureTarget target = resolved.get();
IrisNativeStructureDecision decision = NativeStructureGenerationPolicy.resolve(engine, target.key(), false);
if (!decision.generate()
&& decision.status() != NativeStructureGenerationStatus.REPLACED_BY_IRIS) {
IrisModdedCommands.fail(source, NativeStructureGenerationPolicy.generationStatusMessage(
target.key(), decision.status()));
return 0;
}
if (decision.status() == NativeStructureGenerationStatus.REPLACED_BY_IRIS) {
locateIrisStructure(source, level, engine, player, target.key());
return 1;
}
if (target.availability() != NativeStructureAvailability.AVAILABLE) {
IrisModdedCommands.fail(source, nativeUnavailableMessage(target.key(), target.availability()));
return 0;
}
IrisModdedCommands.ok(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_SEARCHING_NATIVE_STRUCTURE_WITHIN_CHUNKS, MessageArgument.untrusted("value", target.key()), MessageArgument.untrusted("NATIVESTRUCTURELOCATERADIUS", NATIVE_STRUCTURE_LOCATE_RADIUS)));
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();
int blockX = player.blockPosition().getX();
int blockZ = player.blockPosition().getZ();
IrisModdedCommands.ok(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_SEARCHING_IRIS_PLACED_STRUCTURE, MessageArgument.untrusted("key", key)));
Thread thread = new Thread(() -> {
try {
IrisStructureLocator.LocateResult result =
IrisStructureLocator.locate(engine, key, blockX, blockZ, 1024);
if (result.status() == IrisStructureLocator.LocateStatus.SEARCH_LIMIT_REACHED) {
server.execute(() -> IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_UNABLE_LOCATE_IRIS_PLACED_STRUCTURE_DENSITY_SEARCH_SAFETY_LIMIT_WAS, MessageArgument.untrusted("key", key))));
return;
}
if (!result.found()) {
server.execute(() -> IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_COULD_NOT_FIND_IRIS_PLACED_STRUCTURE_WITHIN_1024_CHUNKS, MessageArgument.untrusted("key", key))));
return;
}
int targetX = result.originX();
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) {
LOGGER.error("Iris structure locate failed for {}", key, e);
server.execute(() -> IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_SEARCH_FAILED, MessageArgument.untrusted("value", e.getClass().getSimpleName()))));
}
}, "Iris Structure Locator");
thread.setDaemon(true);
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) {
IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_COULD_NOT_FIND_NATIVE_STRUCTURE_WITHIN_CHUNKS, MessageArgument.untrusted("value", target.key()), MessageArgument.untrusted("NATIVESTRUCTURELOCATERADIUS", NATIVE_STRUCTURE_LOCATE_RADIUS)));
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);
IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_SEARCH_NATIVE_STRUCTURE_FAILED, MessageArgument.untrusted("value", target.key()), MessageArgument.untrusted("value2", 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()) {
IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_PLAYER_DISCONNECTED_BEFORE_STRUCTURE_SEARCH_COMPLETED));
return;
}
if (player.level() != level) {
IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_YOU_CHANGED_DIMENSIONS_BEFORE_STRUCTURE_SEARCH_COMPLETED_RUN_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) {
IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_FOUND_AT_BUT_TELEPORTATION_FAILED, MessageArgument.untrusted("label", label), MessageArgument.untrusted("targetX", targetX), MessageArgument.untrusted("clampedY", clampedY), MessageArgument.untrusted("targetZ", targetZ)));
return;
}
IrisModdedCommands.ok(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_TELEPORTED_AT, MessageArgument.untrusted("label", label), MessageArgument.untrusted("targetX", targetX), MessageArgument.untrusted("clampedY", clampedY), MessageArgument.untrusted("targetZ", targetZ)));
}
static int verifyStructures(CommandSourceStack source, String keyRaw) {
ServerLevel level = source.getLevel();
Engine engine = IrisModdedCommands.engineFor(level);
if (engine == null) {
IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_THIS_DIMENSION_IS_NOT_GENERATED_BY_IRIS_12));
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();
IrisModdedCommands.ok(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_STRUCTURE_REACHABILITY_NATIVE_GENERATION_ELIGIBLE_IRIS_PLACED_NATIVE_DISABLED_NATIVE, MessageArgument.untrusted("available", available), MessageArgument.untrusted("irisPlaced", irisPlaced), MessageArgument.untrusted("disabled", disabled), MessageArgument.untrusted("suppressed", suppressed), MessageArgument.untrusted("unreachableBiomes", unreachableBiomes), MessageArgument.untrusted("unsupported", unsupported)));
return 1;
}
private static int verifyStructure(CommandSourceStack source, ServerLevel level, Engine engine, String key) {
Optional<NativeStructureTarget> target = resolveNativeStructure(source, level, engine, key);
if (target.isEmpty()) {
if (IrisStructureLocator.isPlaced(engine, key)) {
IrisModdedCommands.ok(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_STRUCTURE_IS_IRIS_PLACED_LOCATABLE_WITH_IRIS_GOTO_STRUCTURE, MessageArgument.untrusted("key", key), MessageArgument.untrusted("key2", key)));
return 1;
}
IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_UNKNOWN_STRUCTURE_IT_IS_NEITHER_IRIS_PLACED_NOR_REGISTERED_BY, MessageArgument.untrusted("key", key)));
return 0;
}
NativeStructureTarget resolved = target.get();
if (resolved.availability() == NativeStructureAvailability.IRIS_SUPPRESSED) {
IrisModdedCommands.ok(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_STRUCTURE_IS_EXPLICITLY_REPLACED_BY_IRIS_PLACEMENT_LOCATABLE_WITH_IRIS, MessageArgument.untrusted("value", resolved.key()), MessageArgument.untrusted("value2", resolved.key())));
return 1;
}
if (resolved.availability() != NativeStructureAvailability.AVAILABLE) {
IrisModdedCommands.fail(source, nativeUnavailableMessage(resolved.key(), resolved.availability()));
return 0;
}
IrisModdedCommands.ok(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_NATIVE_STRUCTURE_IS_ENABLED_SUPPORTED_BY_THIS_DIMENSION_S_GENERATOR, MessageArgument.untrusted("value", resolved.key()), MessageArgument.untrusted("value2", 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();
IrisNativeStructureDecision decision = NativeStructureGenerationPolicy.resolve(engine, key, false);
boolean selected = decision.status() != NativeStructureGenerationStatus.DISABLED_BY_PACK;
boolean suppressed = decision.status() == NativeStructureGenerationStatus.REPLACED_BY_IRIS;
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 -> NativeStructureGenerationPolicy.generationStatusMessage(
key, NativeStructureGenerationStatus.DISABLED_BY_PACK);
case IRIS_SUPPRESSED -> NativeStructureGenerationPolicy.generationStatusMessage(
key, NativeStructureGenerationStatus.REPLACED_BY_IRIS);
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.";
};
}
static int gotoPoi(CommandSourceStack source, String typeRaw) {
ServerLevel level = source.getLevel();
Engine engine = IrisModdedCommands.engineFor(level);
if (engine == null) {
IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_THIS_DIMENSION_IS_NOT_GENERATED_BY_IRIS_13));
return 0;
}
String type = typeRaw.trim();
ServerPlayer player = source.getPlayer();
if (player == null) {
IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_THIS_COMMAND_CAN_ONLY_BE_USED_BY_PLAYERS_POI_TYPE, MessageArgument.untrusted("type", type)));
return 0;
}
locate(source, level, engine, player, Locator.poi(type), "POI " + type);
return 1;
}
private static void locate(CommandSourceStack source, ServerLevel level, Engine engine, ServerPlayer player, Locator<?> locator, String label) {
MinecraftServer server = source.getServer();
int chunkX = player.blockPosition().getX() >> 4;
int chunkZ = player.blockPosition().getZ() >> 4;
IrisModdedCommands.ok(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_SEARCHING, MessageArgument.untrusted("label", label)));
CompletableFuture<Position2> search;
try {
search = locator.find(engine, new Position2(chunkX, chunkZ), LOCATE_TIMEOUT_MS, (Integer checks) -> {
});
} catch (WrongEngineBroException e) {
IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_ENGINE_THIS_WORLD_HAS_BEEN_CLOSED_REJOIN_DIMENSION_TRY_AGAIN));
return;
}
UUID playerId = player.getUUID();
CompletableFuture<Position2> previous = ACTIVE_LOCATE_REQUESTS.put(playerId, search);
if (previous != null && previous != search) {
previous.cancel(true);
}
search.whenComplete((Position2 at, Throwable error) -> completeLocate(
source, level, engine, player, label, server, playerId, search, at, error));
}
private static void completeLocate(CommandSourceStack source, ServerLevel level, Engine engine,
ServerPlayer player, String label, MinecraftServer server, UUID playerId,
CompletableFuture<Position2> search, Position2 at, Throwable error) {
if (ACTIVE_LOCATE_REQUESTS.get(playerId) != search) {
return;
}
Throwable failure = unwrapCompletionFailure(error);
if (failure instanceof CancellationException) {
ACTIVE_LOCATE_REQUESTS.remove(playerId, search);
return;
}
if (failure != null) {
LOGGER.error("Iris locate failed for {}", label, failure);
server.execute(() -> {
if (ACTIVE_LOCATE_REQUESTS.remove(playerId, search)) {
IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_SEARCH_FAILED_2, MessageArgument.untrusted("failure", failure)));
}
});
return;
}
if (at == null) {
server.execute(() -> {
if (ACTIVE_LOCATE_REQUESTS.remove(playerId, search)) {
IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_COULD_NOT_FIND_WITHIN_SEARCH_TIMEOUT, MessageArgument.untrusted("label", label)));
}
});
return;
}
server.execute(() -> {
if (ACTIVE_LOCATE_REQUESTS.remove(playerId, search)) {
teleportToLocateResult(source, level, engine, player, label, at);
}
});
}
private static void teleportToLocateResult(CommandSourceStack source, ServerLevel level, Engine engine,
ServerPlayer player, String label, Position2 at) {
int blockX = (at.getX() << 4) + 8;
int blockZ = (at.getZ() << 4) + 8;
try (GenerationSessionLease lease = engine.acquireGenerationLease("modded_locator_teleport");
IrisContext.Scope ignored = IrisContext.open(engine, lease.sessionId(), null)) {
int blockY = engine.getMinHeight() + engine.getHeight(blockX, blockZ, false) + 2;
boolean teleported = player.teleportTo(
level,
blockX + 0.5D,
blockY,
blockZ + 0.5D,
Set.<Relative>of(),
player.getYRot(),
player.getXRot(),
false);
if (!teleported) {
IrisModdedCommands.fail(source, IrisLanguage.plain(
ModdedCommandMessages.IRIS_MODDED_COMMANDS_FOUND_AT_BUT_TELEPORTATION_FAILED,
MessageArgument.untrusted("label", label),
MessageArgument.trusted("targetX", blockX),
MessageArgument.trusted("clampedY", blockY),
MessageArgument.trusted("targetZ", blockZ)));
return;
}
IrisModdedCommands.ok(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_TELEPORTED_AT_2, MessageArgument.untrusted("label", label), MessageArgument.untrusted("blockX", blockX), MessageArgument.untrusted("blockY", blockY), MessageArgument.untrusted("blockZ", blockZ)));
} catch (GenerationSessionException e) {
IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_ENGINE_CHANGED_WHILE_LOCATING_TRY_AGAIN, MessageArgument.untrusted("label", label)));
}
}
private static Throwable unwrapCompletionFailure(Throwable error) {
Throwable failure = error;
while ((failure instanceof CompletionException || failure instanceof ExecutionException)
&& failure.getCause() != null) {
failure = failure.getCause();
}
return failure;
}
enum NativeStructureAvailability {
AVAILABLE,
WORLD_DISABLED,
FILTERED,
IRIS_SUPPRESSED,
BIOME_UNREACHABLE,
NO_PLACEMENT
}
private record NativeStructureTarget(String key, Holder.Reference<Structure> holder,
NativeStructureAvailability availability) {
}
}
@@ -87,7 +87,8 @@ public final class ModdedObjectCommands {
if (engine != null) {
return SharedSuggestionProvider.suggest(engine.getData().getObjectLoader().getPossibleKeys(), builder);
}
} catch (Throwable ignored) {
} catch (Throwable e) {
IrisModdedCommands.warnTabFailure("object keys", context.getSource(), e);
}
return builder.buildFuture();
};
@@ -0,0 +1,101 @@
/*
* 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.command;
import art.arcane.iris.core.localization.IrisLanguage;
import art.arcane.iris.core.localization.ModdedCommandMessages;
import art.arcane.iris.core.localization.RuntimeUiMessages;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.volmlib.util.localization.MessageArgument;
import com.mojang.brigadier.arguments.IntegerArgumentType;
import com.mojang.brigadier.context.CommandContext;
import com.mojang.brigadier.exceptions.CommandSyntaxException;
import net.minecraft.commands.CommandSourceStack;
import net.minecraft.commands.arguments.DimensionArgument;
import net.minecraft.network.chat.Component;
import net.minecraft.server.level.ServerLevel;
final class ModdedPregenCommands {
private ModdedPregenCommands() {
}
static int pregenStart(CommandContext<CommandSourceStack> context, boolean withDimension, boolean withCenter, boolean gui, boolean sync, boolean nocache) throws CommandSyntaxException {
CommandSourceStack source = context.getSource();
int radius = IntegerArgumentType.getInteger(context, "radius");
int centerX = withCenter ? IntegerArgumentType.getInteger(context, "x") : 0;
int centerZ = withCenter ? IntegerArgumentType.getInteger(context, "z") : 0;
ServerLevel level = withDimension ? DimensionArgument.getDimension(context, "dimension") : source.getLevel();
Engine engine = IrisModdedCommands.engineFor(level);
if (engine == null) {
if (withDimension) {
IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_IS_NOT_GENERATED_BY_IRIS_SEE_IRIS_INFO_LOADED_IRIS, MessageArgument.untrusted("value", level.dimension().identifier())));
} else {
IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_CURRENT_DIMENSION_IS_NOT_GENERATED_BY_IRIS_NAME_ONE_EXPLICITLY, MessageArgument.untrusted("value", level.dimension().identifier()), MessageArgument.untrusted("radius", radius)));
}
return 0;
}
boolean showGui = gui && ModdedGuiHost.isGuiLaunchable();
if (!ModdedPregenJob.start(source.getServer(), level, engine, radius, centerX, centerZ, showGui, sync, !nocache)) {
IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_PREGENERATION_TASK_IS_ALREADY_RUNNING_STOP_IT_FIRST_WITH_IRIS));
return 0;
}
ModdedPregenBossBar.begin(source.getPlayer());
String guiNote;
if (!gui) {
guiNote = "";
} else if (showGui) {
guiNote = " A progress map window is opening on the server display.";
} else {
guiNote = " (GUI requested but unavailable: " + ModdedGuiHost.guiUnavailableReason() + ")";
}
String modeNote = " Mode: " + (sync ? "sync" : "async") + (nocache ? ", cache disabled." : ", resumable (checkpoint cache).");
IrisModdedCommands.ok(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_PREGEN_STARTED_BY_BLOCKS_FROM_PROGRESS_LOGS_CONSOLE_SEE_IRIS, MessageArgument.untrusted("value", level.dimension().identifier()), MessageArgument.untrusted("value2", (radius * 2)), MessageArgument.untrusted("value3", (radius * 2)), MessageArgument.untrusted("centerX", centerX), MessageArgument.untrusted("centerZ", centerZ), MessageArgument.untrusted("modeNote", modeNote), MessageArgument.untrusted("guiNote", guiNote)));
return 1;
}
static int pregenStop(CommandSourceStack source) {
if (ModdedPregenJob.stop()) {
ModdedPregenBossBar.clear();
IrisModdedCommands.ok(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_STOPPING_PREGENERATION_FINISHING_UP_CURRENT_REGION));
return 1;
}
IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_NO_ACTIVE_PREGENERATION_TASK_STOP));
return 0;
}
static int pregenPause(CommandSourceStack source) {
Boolean paused = ModdedPregenJob.pauseResume();
if (paused == null) {
IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_NO_ACTIVE_PREGENERATION_TASK_PAUSE_RESUME));
return 0;
}
IrisModdedCommands.ok(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_PREGENERATION_IS_NOW, MessageArgument.trusted("value", IrisLanguage.plain(paused.booleanValue() ? RuntimeUiMessages.STATUS_PAUSED_LOWER : RuntimeUiMessages.STATUS_RUNNING_LOWER))));
return 1;
}
static int pregenStatus(CommandSourceStack source) {
Component status = ModdedPregenJob.statusComponent();
if (status == null) {
IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_NO_ACTIVE_PREGENERATION_TASK));
return 0;
}
IrisModdedCommands.ok(source, status);
return 1;
}
}
@@ -102,7 +102,8 @@ public final class ModdedStudioCommands {
if (engine != null) {
return SharedSuggestionProvider.suggest(engine.getData().getGeneratorLoader().getPossibleKeys(), builder);
}
} catch (Throwable ignored) {
} catch (Throwable e) {
IrisModdedCommands.warnTabFailure("generator keys", context.getSource(), e);
}
return builder.buildFuture();
};
@@ -20,6 +20,7 @@ package art.arcane.iris.modded.command;
import art.arcane.iris.core.loader.IrisData;
import art.arcane.iris.core.pack.BrokenPackException;
import art.arcane.iris.core.pack.PackValidationRegistry;
import art.arcane.iris.engine.object.IrisDimension;
import art.arcane.iris.modded.IrisModdedChunkGenerator;
import art.arcane.iris.modded.MainWorldService;
@@ -303,7 +304,14 @@ public final class ModdedWorldCommands {
}
} catch (Throwable e) {
LOGGER.error("Iris main world pack load failed for {} (dim={})", pack, packDimension, e);
IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.MODDED_WORLD_COMMANDS_PACK_IS_NOT_READY_YET_STILL_LOADING_VALIDATING_TRY_COMMAND, MessageArgument.untrusted("pack", pack)));
if (PackValidationRegistry.get(pack) == null) {
IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.MODDED_WORLD_COMMANDS_PACK_IS_NOT_READY_YET_STILL_LOADING_VALIDATING_TRY_COMMAND, MessageArgument.untrusted("pack", pack)));
return 0;
}
IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.MODDED_PACK_COMMANDS_VALIDATION_FAILED,
MessageArgument.untrusted("value", pack + ":" + packDimension),
MessageArgument.trusted("value2", e.getClass().getSimpleName() + IrisLanguage.errorDetail(e))));
IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.MODDED_WORLD_COMMANDS_FIX_PACK_RUN_IRIS_PACK_VALIDATE_REVALIDATE, MessageArgument.untrusted("pack", pack)));
return 0;
}
ModdedModConfig.setMainWorld(packRef, seed);
@@ -1,620 +0,0 @@
/*
* 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.structure;
import art.arcane.iris.core.structure.authoring.StructureBackend;
import art.arcane.iris.core.structure.authoring.StructureCapability;
import art.arcane.iris.core.structure.authoring.StructureKey;
import art.arcane.iris.core.structure.authoring.StructureLoss;
import art.arcane.iris.core.structure.authoring.StructureResourceBundle;
import art.arcane.iris.core.structure.authoring.StructureSource;
import art.arcane.iris.engine.object.IrisObject;
import art.arcane.iris.engine.object.TileData;
import art.arcane.iris.util.common.math.IrisBlockVector;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.mojang.datafixers.util.Pair;
import net.minecraft.SharedConstants;
import net.minecraft.core.BlockPos;
import net.minecraft.core.Direction;
import net.minecraft.core.Registry;
import net.minecraft.core.Vec3i;
import net.minecraft.core.registries.Registries;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.nbt.NbtOps;
import net.minecraft.nbt.NbtUtils;
import net.minecraft.nbt.Tag;
import net.minecraft.resources.Identifier;
import net.minecraft.resources.RegistryOps;
import net.minecraft.resources.ResourceKey;
import net.minecraft.server.MinecraftServer;
import net.minecraft.util.RandomSource;
import net.minecraft.world.level.block.Block;
import net.minecraft.world.level.block.JigsawBlock;
import net.minecraft.world.level.block.Rotation;
import net.minecraft.world.level.levelgen.structure.Structure;
import net.minecraft.world.level.levelgen.structure.TerrainAdjustment;
import net.minecraft.world.level.levelgen.structure.pools.EmptyPoolElement;
import net.minecraft.world.level.levelgen.structure.pools.FeaturePoolElement;
import net.minecraft.world.level.levelgen.structure.pools.LegacySinglePoolElement;
import net.minecraft.world.level.levelgen.structure.pools.ListPoolElement;
import net.minecraft.world.level.levelgen.structure.pools.SinglePoolElement;
import net.minecraft.world.level.levelgen.structure.pools.StructurePoolElement;
import net.minecraft.world.level.levelgen.structure.pools.StructureTemplatePool;
import net.minecraft.world.level.levelgen.structure.pools.alias.PoolAliasBinding;
import net.minecraft.world.level.levelgen.structure.structures.JigsawStructure;
import net.minecraft.world.level.levelgen.structure.templatesystem.StructureTemplate;
import net.minecraft.world.level.levelgen.structure.templatesystem.StructureTemplateManager;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Deque;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
final class ModdedJigsawStructureCapture {
private static final Gson GSON = new GsonBuilder().setPrettyPrinting().create();
private final MinecraftServer server;
private final StructureKey sourceKey;
private final StructureKey targetKey;
private final StructureSource.Kind sourceKind;
private final Registry<Structure> structureRegistry;
private final Registry<StructureTemplatePool> poolRegistry;
private final Registry<Block> blockRegistry;
private final StructureTemplateManager templateManager;
private final RegistryOps<Tag> registryOps;
private final Map<ResourceKey<StructureTemplatePool>, ResourceKey<StructureTemplatePool>> aliases;
private final Map<String, byte[]> objects;
private final Map<String, Map<String, Object>> pieces;
private final Map<String, Map<String, Object>> pools;
private final EnumSet<StructureCapability> capabilities;
private final List<StructureLoss> losses;
private final Set<String> recordedLosses;
private final Set<String> visitedPools;
private final Deque<String> pendingPools;
private int blocks;
private ModdedJigsawStructureCapture(
MinecraftServer server,
StructureKey sourceKey,
StructureKey targetKey,
StructureSource.Kind sourceKind
) {
this.server = Objects.requireNonNull(server);
this.sourceKey = Objects.requireNonNull(sourceKey);
this.targetKey = Objects.requireNonNull(targetKey);
this.sourceKind = Objects.requireNonNull(sourceKind);
structureRegistry = server.registryAccess().lookupOrThrow(Registries.STRUCTURE);
poolRegistry = server.registryAccess().lookupOrThrow(Registries.TEMPLATE_POOL);
blockRegistry = server.registryAccess().lookupOrThrow(Registries.BLOCK);
templateManager = server.getStructureManager();
registryOps = RegistryOps.create(NbtOps.INSTANCE, server.registryAccess());
aliases = new HashMap<>();
objects = new LinkedHashMap<>();
pieces = new LinkedHashMap<>();
pools = new LinkedHashMap<>();
capabilities = EnumSet.of(
StructureCapability.BLOCKS,
StructureCapability.CONNECTORS,
StructureCapability.IRIS_PLACEMENT
);
losses = new ArrayList<>();
recordedLosses = new HashSet<>();
visitedPools = new HashSet<>();
pendingPools = new ArrayDeque<>();
}
static Capture capture(
MinecraftServer server,
StructureKey sourceKey,
StructureKey targetKey,
StructureSource.Kind sourceKind
) throws IOException {
return new ModdedJigsawStructureCapture(server, sourceKey, targetKey, sourceKind).capture();
}
private Capture capture() throws IOException {
Identifier sourceIdentifier = identifier(sourceKey);
Structure structure = structureRegistry.getValue(sourceIdentifier);
if (structure == null) {
throw new IllegalArgumentException("No registered structure exists for " + sourceKey);
}
if (!(structure instanceof JigsawStructure jigsaw)) {
throw new UnsupportedStructureTypeException("Structure " + sourceKey + " uses "
+ structure.getClass().getSimpleName() + "; only jigsaw structure graphs can be converted to Iris assembly resources");
}
CompoundTag encodedStructure = encodeStructure(structure);
configureAliases(jigsaw);
recordRootLosses(jigsaw);
String startPoolKey = poolKey(jigsaw.getStartPool().value());
pendingPools.add(startPoolKey);
while (!pendingPools.isEmpty()) {
capturePool(pendingPools.removeFirst());
}
if (pieces.isEmpty()) {
throw new IllegalStateException("Structure " + sourceKey + " produced no importable pieces");
}
int maxDepth = Math.max(1, encodedStructure.getIntOr("size", 1));
int maxDistance = readHorizontalDistance(encodedStructure);
StructureSource source = StructureSource.identified(
sourceKind,
sourceKey,
SharedConstants.getCurrentVersion().name(),
NbtUtils.structureToSnbt(encodedStructure).getBytes(StandardCharsets.UTF_8)
);
StructureResourceBundle bundle = buildBundle(source, startPoolKey, maxDepth, maxDistance);
return new Capture(bundle, blocks, pieces.size(), pools.size());
}
private CompoundTag encodeStructure(Structure structure) {
Tag encoded = Structure.DIRECT_CODEC.encodeStart(registryOps, structure).getOrThrow();
if (!(encoded instanceof CompoundTag compound)) {
throw new IllegalStateException("Structure codec did not produce a compound for " + sourceKey);
}
return compound;
}
private void configureAliases(JigsawStructure structure) {
List<PoolAliasBinding> bindings = structure.getPoolAliases();
if (bindings.isEmpty()) {
return;
}
RandomSource random = RandomSource.create(stableSeed(sourceKey.value()));
for (PoolAliasBinding binding : bindings) {
binding.forEachResolved(random, aliases::put);
}
addLossOnce(
"pool_aliases_resolved_once",
StructureLoss.warning(
StructureCapability.CONNECTORS,
"pool_aliases_resolved_once",
bindings.size() + " native pool alias binding(s) were resolved deterministically for the imported graph; per-placement alias variation is not represented."
)
);
}
private void recordRootLosses(JigsawStructure structure) {
losses.add(StructureLoss.warning(
StructureCapability.NATIVE_PLACEMENT,
"native_placement_settings_not_imported",
"Native start height, heightmap projection, expansion, padding, and placement-set settings are not represented by Iris assembly placement."
));
losses.add(StructureLoss.warning(
StructureCapability.LIQUID_SETTINGS,
"native_liquid_settings_not_imported",
"Native structure liquid placement behavior is not represented beyond the captured block and waterlogged states."
));
if (structure.terrainAdaptation() != TerrainAdjustment.NONE) {
losses.add(StructureLoss.warning(
StructureCapability.TERRAIN_ADAPTATION,
"terrain_adaptation_not_imported",
"Native terrain adaptation '" + structure.terrainAdaptation().getSerializedName()
+ "' is not represented by the Iris assembly."
));
}
}
private void capturePool(String sourcePoolKey) throws IOException {
if (!visitedPools.add(sourcePoolKey)) {
return;
}
Identifier sourcePoolIdentifier = Identifier.tryParse(sourcePoolKey);
StructureTemplatePool pool = sourcePoolIdentifier == null ? null : poolRegistry.getValue(sourcePoolIdentifier);
if (pool == null) {
throw new IllegalStateException("Jigsaw graph references missing template pool " + sourcePoolKey);
}
String irisPoolName = poolName(targetKey.path(), sourcePoolKey);
List<Object> entries = new ArrayList<>();
List<Pair<StructurePoolElement, Integer>> templates = pool.getTemplates();
for (int index = 0; index < templates.size(); index++) {
Pair<StructurePoolElement, Integer> weighted = templates.get(index);
StructurePoolElement element = weighted.getFirst();
int weight = Math.max(1, weighted.getSecond());
Map<String, Object> entry = new LinkedHashMap<>();
if (element == EmptyPoolElement.INSTANCE) {
entry.put("empty", true);
} else {
entry.put("piece", captureElement(sourcePoolKey, index, element));
}
entry.put("weight", weight);
entries.add(entry);
}
Map<String, Object> poolJson = new LinkedHashMap<>();
poolJson.put("pieces", entries);
String fallback = resolvedPoolKey(pool.getFallback().value());
if (!fallback.equals(sourcePoolKey)) {
poolJson.put("fallback", poolName(targetKey.path(), fallback));
pendingPools.addLast(fallback);
}
pools.put(irisPoolName, poolJson);
}
private String captureElement(String sourcePoolKey, int index, StructurePoolElement element) throws IOException {
Objects.requireNonNull(element, "pool element");
if (element instanceof SinglePoolElement single) {
return captureSingle(single);
}
String generatedName = generatedPieceName(targetKey.path(), sourcePoolKey, index, element);
if (pieces.containsKey(generatedName)) {
return generatedName;
}
if (element instanceof ListPoolElement list) {
capabilities.add(StructureCapability.LIST_ELEMENTS);
CompositeCapture composite = captureList(list, generatedName);
blocks += composite.object().getBlocks().size();
emitPiece(generatedName, composite.object(), composite.losses(), element);
return generatedName;
}
IrisObject object = emptyObject(element);
StructureCapability unsupported = element instanceof FeaturePoolElement
? StructureCapability.FEATURE_ELEMENTS : StructureCapability.BLOCKS;
StructureLoss loss = StructureLoss.warning(
unsupported,
"unsupported_pool_element",
"Pool element " + element.getClass().getSimpleName() + " was represented as an empty Iris piece."
);
emitPiece(generatedName, object, List.of(loss), element);
return generatedName;
}
private String captureSingle(SinglePoolElement element) throws IOException {
Identifier templateIdentifier = element.getTemplateLocation();
StructureKey templateKey = StructureKey.parse(templateIdentifier.toString());
boolean legacy = element instanceof LegacySinglePoolElement;
String pieceName = legacy
? legacyPieceName(targetKey.path(), templateKey.value())
: pieceName(targetKey.path(), templateKey.value());
if (pieces.containsKey(pieceName)) {
return pieceName;
}
StructureTemplate template = templateManager.get(templateIdentifier)
.orElseThrow(() -> new IllegalStateException("Missing structure template " + templateIdentifier));
ModdedStructureTemplateCapture.Capture capture = ModdedStructureTemplateCapture.capture(
templateKey, template, blockRegistry, true, !legacy);
capabilities.addAll(capture.capabilities());
blocks += capture.blocks();
List<StructureLoss> pieceLosses = new ArrayList<>(capture.losses());
pieceLosses.addAll(elementLosses(element));
emitPiece(pieceName, capture.object(), pieceLosses, element);
return pieceName;
}
private CompositeCapture captureList(ListPoolElement list, String pieceName) throws IOException {
Vec3i size = list.getSize(templateManager, Rotation.NONE);
IrisObject composite = new IrisObject(
Math.max(1, size.getX()),
Math.max(1, size.getY()),
Math.max(1, size.getZ())
);
List<StructureLoss> compositeLosses = new ArrayList<>();
for (StructurePoolElement child : list.getElements()) {
if (child == EmptyPoolElement.INSTANCE) {
continue;
}
if (child instanceof SinglePoolElement single) {
Identifier templateIdentifier = single.getTemplateLocation();
StructureTemplate template = templateManager.get(templateIdentifier)
.orElseThrow(() -> new IllegalStateException("Missing structure template " + templateIdentifier));
ModdedStructureTemplateCapture.Capture capture = ModdedStructureTemplateCapture.capture(
StructureKey.parse(templateIdentifier.toString()),
template,
blockRegistry,
true,
!(single instanceof LegacySinglePoolElement)
);
merge(composite, capture.object());
capabilities.addAll(capture.capabilities());
compositeLosses.addAll(capture.losses());
compositeLosses.addAll(elementLosses(single));
continue;
}
if (child instanceof ListPoolElement nested) {
CompositeCapture nestedCapture = captureList(nested, pieceName);
merge(composite, nestedCapture.object());
compositeLosses.addAll(nestedCapture.losses());
continue;
}
StructureCapability unsupported = child instanceof FeaturePoolElement
? StructureCapability.FEATURE_ELEMENTS : StructureCapability.LIST_ELEMENTS;
compositeLosses.add(StructureLoss.warning(
unsupported,
"list_child_not_imported",
"List element child " + child.getClass().getSimpleName() + " could not be flattened into the Iris object."
));
}
compositeLosses.addAll(elementLosses(list));
return new CompositeCapture(composite, compositeLosses);
}
private List<StructureLoss> elementLosses(StructurePoolElement element) {
List<StructureLoss> elementLosses = new ArrayList<>();
if (element.getProjection() != StructureTemplatePool.Projection.RIGID) {
elementLosses.add(StructureLoss.warning(
StructureCapability.PROJECTION,
"terrain_matching_projection_not_imported",
"Pool projection '" + element.getProjection().getSerializedName()
+ "' was converted to rigid Iris piece placement."
));
}
Tag encoded = StructurePoolElement.CODEC.encodeStart(registryOps, element).getOrThrow();
if (encoded instanceof CompoundTag compound) {
String processors = compound.getStringOr("processors", "");
if (!processors.isEmpty() && !processors.equals("minecraft:empty")) {
elementLosses.add(StructureLoss.warning(
StructureCapability.PROCESSORS,
"native_processors_not_imported",
"Native processor list '" + processors + "' was not applied to the captured template."
));
}
if (compound.contains("override_liquid_settings")) {
elementLosses.add(StructureLoss.warning(
StructureCapability.LIQUID_SETTINGS,
"element_liquid_settings_not_imported",
"The pool element's liquid setting override is not represented by Iris placement."
));
}
}
return elementLosses;
}
private void emitPiece(
String pieceName,
IrisObject object,
List<StructureLoss> pieceLosses,
StructurePoolElement element
) throws IOException {
String objectResource = "objects/" + pieceName + ".iob";
objects.put(pieceName, serialize(object));
for (StructureLoss loss : pieceLosses) {
losses.add(loss.affecting(objectResource));
}
List<Map<String, Object>> connectors = connectors(element, pieceName);
Map<String, Object> pieceJson = new LinkedHashMap<>();
pieceJson.put("object", pieceName);
pieceJson.put("connectors", connectors);
pieceJson.put("rotatable", true);
pieces.put(pieceName, pieceJson);
}
private List<Map<String, Object>> connectors(StructurePoolElement element, String pieceName) {
List<StructureTemplate.JigsawBlockInfo> sourceConnectors = element.getShuffledJigsawBlocks(
templateManager,
BlockPos.ZERO,
Rotation.NONE,
RandomSource.create(stableSeed(sourceKey.value() + ":" + pieceName))
);
List<Map<String, Object>> connectors = new ArrayList<>(sourceConnectors.size());
for (StructureTemplate.JigsawBlockInfo source : sourceConnectors) {
recordConnectorLosses(source, pieceName);
ResourceKey<StructureTemplatePool> resolvedPool = aliases.getOrDefault(source.pool(), source.pool());
String sourcePoolKey = resolvedPool.identifier().toString();
pendingPools.addLast(sourcePoolKey);
Map<String, Object> position = new LinkedHashMap<>();
position.put("x", source.info().pos().getX());
position.put("y", source.info().pos().getY());
position.put("z", source.info().pos().getZ());
Map<String, Object> connector = new LinkedHashMap<>();
connector.put("position", position);
connector.put("direction", directionName(JigsawBlock.getFrontFacing(source.info().state())));
connector.put("top", directionName(JigsawBlock.getTopFacing(source.info().state())));
connector.put("pool", poolName(targetKey.path(), sourcePoolKey));
connector.put("name", source.name().toString());
connector.put("targetName", source.target().toString());
connector.put("joint", source.jointType().getSerializedName().equals("aligned") ? "ALIGNED" : "ROLLABLE");
connectors.add(connector);
}
return connectors;
}
private void recordConnectorLosses(StructureTemplate.JigsawBlockInfo connector, String pieceName) {
String pieceResource = "jigsaw-pieces/" + pieceName + ".json";
if (connector.placementPriority() != 0 || connector.selectionPriority() != 0) {
addLossOnce(
"connector-priority:" + pieceName,
StructureLoss.warning(
StructureCapability.CONNECTORS,
"connector_priorities_not_imported",
"Native connector placement and selection priorities are not represented by Iris assembly ordering."
).affecting(pieceResource)
);
}
}
private StructureResourceBundle buildBundle(
StructureSource source,
String startPoolKey,
int maxDepth,
int maxDistance
) {
StructureResourceBundle.Builder bundle = StructureResourceBundle.builder(targetKey)
.source(source)
.backend(StructureBackend.IRIS_ASSEMBLY)
.capabilities(capabilities)
.losses(losses);
for (Map.Entry<String, byte[]> entry : objects.entrySet()) {
bundle.resource("objects/" + entry.getKey() + ".iob", entry.getValue());
}
for (Map.Entry<String, Map<String, Object>> entry : pieces.entrySet()) {
bundle.textResource("jigsaw-pieces/" + entry.getKey() + ".json", GSON.toJson(entry.getValue()));
}
for (Map.Entry<String, Map<String, Object>> entry : pools.entrySet()) {
bundle.textResource("jigsaw-pools/" + entry.getKey() + ".json", GSON.toJson(entry.getValue()));
}
bundle.textResource(
"structures/" + targetKey.path() + ".json",
GSON.toJson(structureJson(sourceKey.value(), poolName(targetKey.path(), startPoolKey), maxDepth, maxDistance))
);
return bundle.build();
}
private String resolvedPoolKey(StructureTemplatePool pool) {
ResourceKey<StructureTemplatePool> raw = poolResourceKey(pool);
return aliases.getOrDefault(raw, raw).identifier().toString();
}
private String poolKey(StructureTemplatePool pool) {
return poolResourceKey(pool).identifier().toString();
}
private ResourceKey<StructureTemplatePool> poolResourceKey(StructureTemplatePool pool) {
Identifier identifier = poolRegistry.getKey(pool);
if (identifier == null) {
throw new IllegalStateException("Jigsaw structure references an unregistered template pool");
}
return ResourceKey.create(Registries.TEMPLATE_POOL, identifier);
}
private void addLossOnce(String key, StructureLoss loss) {
if (recordedLosses.add(key)) {
losses.add(loss);
}
}
private static void merge(IrisObject target, IrisObject source) {
for (IrisBlockVector position : source.getBlocks().keys()) {
int x = position.getBlockX() + source.getCenter().getX();
int y = position.getBlockY() + source.getCenter().getY();
int z = position.getBlockZ() + source.getCenter().getZ();
if (x < 0 || y < 0 || z < 0 || x >= target.getW() || y >= target.getH() || z >= target.getD()) {
continue;
}
target.setUnsignedTile(x, y, z, null);
target.setUnsigned(x, y, z, source.getBlocks().get(position));
TileData tile = source.getStates().get(position);
if (tile != null) {
target.setUnsignedTile(x, y, z, tile);
}
}
}
private static IrisObject emptyObject() {
return new IrisObject(1, 1, 1);
}
private IrisObject emptyObject(StructurePoolElement element) {
Vec3i size = element.getSize(templateManager, Rotation.NONE);
return new IrisObject(
Math.max(1, size.getX()),
Math.max(1, size.getY()),
Math.max(1, size.getZ())
);
}
private static byte[] serialize(IrisObject object) throws IOException {
ByteArrayOutputStream output = new ByteArrayOutputStream();
object.write(output);
return output.toByteArray();
}
private static int readHorizontalDistance(CompoundTag encodedStructure) {
int scalar = encodedStructure.getIntOr("max_distance_from_center", -1);
if (scalar > 0) {
return scalar;
}
CompoundTag compound = encodedStructure.getCompoundOrEmpty("max_distance_from_center");
return Math.max(1, compound.getIntOr("horizontal", 80));
}
static Map<String, Object> structureJson(String source, String startPool, int maxDepth, int maxDistance) {
Map<String, Object> root = new LinkedHashMap<>();
root.put("startPool", startPool);
root.put("maxDepth", Math.max(1, Math.min(30, maxDepth)));
root.put("maxSizeChunks", Math.max(1, Math.min(32, (Math.max(1, maxDistance) + 15) / 16)));
root.put("placeMode", "STRUCTURE_PIECE");
root.put("vanillaSource", source);
return root;
}
static String poolName(String base, String sourcePoolKey) {
StructureKey key = StructureKey.parse(sourcePoolKey);
return base + "/pool/" + key.namespace() + "/" + key.path();
}
static String pieceName(String base, String templateKey) {
StructureKey key = StructureKey.parse(templateKey);
return base + "/piece/" + key.namespace() + "/" + key.path();
}
static String legacyPieceName(String base, String templateKey) {
StructureKey key = StructureKey.parse(templateKey);
return base + "/piece/generated/legacy/" + key.namespace() + "/" + key.path();
}
static String directionName(Direction direction) {
return switch (direction) {
case UP -> "UP_POSITIVE_Y";
case DOWN -> "DOWN_NEGATIVE_Y";
case SOUTH -> "SOUTH_POSITIVE_Z";
case EAST -> "EAST_POSITIVE_X";
case WEST -> "WEST_NEGATIVE_X";
case NORTH -> "NORTH_NEGATIVE_Z";
};
}
private static String generatedPieceName(String base, String sourcePoolKey, int index, StructurePoolElement element) {
StructureKey poolKey = StructureKey.parse(sourcePoolKey);
String type = element instanceof ListPoolElement ? "list" : element instanceof FeaturePoolElement ? "feature" : "unsupported";
return base + "/piece/generated/" + type + "/" + poolKey.namespace() + "/" + poolKey.path() + "/" + index;
}
private static long stableSeed(String value) {
long hash = 0xcbf29ce484222325L;
for (int index = 0; index < value.length(); index++) {
hash ^= value.charAt(index);
hash *= 0x100000001b3L;
}
return hash;
}
private static Identifier identifier(StructureKey key) {
return Identifier.fromNamespaceAndPath(key.namespace(), key.path());
}
record Capture(StructureResourceBundle bundle, int blocks, int pieces, int pools) {
Capture {
Objects.requireNonNull(bundle);
}
}
static final class UnsupportedStructureTypeException extends IllegalArgumentException {
UnsupportedStructureTypeException(String message) {
super(message);
}
}
private record CompositeCapture(IrisObject object, List<StructureLoss> losses) {
CompositeCapture {
Objects.requireNonNull(object);
losses = List.copyOf(losses);
}
}
}
@@ -1,426 +0,0 @@
/*
* 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.structure;
import art.arcane.iris.core.loader.IrisData;
import art.arcane.iris.core.structure.authoring.IrisStructureBundleFactory;
import art.arcane.iris.core.structure.authoring.StructureCapability;
import art.arcane.iris.core.structure.authoring.StructureKey;
import art.arcane.iris.core.structure.authoring.StructureLoss;
import art.arcane.iris.core.structure.authoring.StructureResourceBundle;
import art.arcane.iris.core.structure.authoring.StructureSource;
import art.arcane.iris.core.structure.authoring.StructureTransactionWriter;
import art.arcane.iris.core.structure.authoring.StructureWriteMode;
import art.arcane.iris.core.structure.authoring.StructureWriteOptions;
import art.arcane.iris.core.structure.authoring.StructureWriteResult;
import art.arcane.iris.engine.framework.structure.StructureResourceBundleGraphCompiler;
import net.minecraft.SharedConstants;
import net.minecraft.core.Registry;
import net.minecraft.core.registries.Registries;
import net.minecraft.nbt.NbtUtils;
import net.minecraft.resources.Identifier;
import net.minecraft.server.MinecraftServer;
import net.minecraft.world.level.levelgen.structure.Structure;
import net.minecraft.world.level.levelgen.structure.structures.JigsawStructure;
import net.minecraft.world.level.levelgen.structure.templatesystem.StructureTemplate;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.function.Supplier;
public final class ModdedStructureImportService {
private static final Logger LOGGER = LoggerFactory.getLogger("Iris");
private final Supplier<MinecraftServer> server;
public ModdedStructureImportService(Supplier<MinecraftServer> server) {
this.server = Objects.requireNonNull(server);
}
public List<StructureKey> templateKeys() throws StructureImportException {
try {
MinecraftServer activeServer = requireServerThread();
return activeServer.getStructureManager().listTemplates()
.map((Identifier identifier) -> StructureKey.parse(identifier.toString()))
.sorted()
.toList();
} catch (RuntimeException failure) {
throw report("Failed to list native structure templates", failure);
}
}
public List<StructureKey> jigsawStructureKeys() throws StructureImportException {
try {
MinecraftServer activeServer = requireServerThread();
Registry<Structure> registry = activeServer.registryAccess().lookupOrThrow(Registries.STRUCTURE);
List<StructureKey> keys = new ArrayList<>();
for (Identifier identifier : registry.keySet()) {
if (registry.getValue(identifier) instanceof JigsawStructure) {
keys.add(StructureKey.parse(identifier.toString()));
}
}
keys.sort(Comparator.naturalOrder());
return List.copyOf(keys);
} catch (RuntimeException failure) {
throw report("Failed to list registered jigsaw structures", failure);
}
}
public PreparedImport prepareTemplate(TemplateImportOptions options) throws StructureImportException {
Objects.requireNonNull(options);
try {
MinecraftServer activeServer = requireServerThread();
Identifier sourceIdentifier = identifier(options.sourceKey());
StructureTemplate template = activeServer.getStructureManager().get(sourceIdentifier)
.orElseThrow(() -> new IllegalArgumentException("No structure template exists for " + options.sourceKey()));
ModdedStructureTemplateCapture.Capture capture = ModdedStructureTemplateCapture.capture(
options.sourceKey(),
template,
activeServer.registryAccess().lookupOrThrow(Registries.BLOCK),
false
);
StructureSource source = StructureSource.identified(
options.sourceKind(),
options.sourceKey(),
SharedConstants.getCurrentVersion().name(),
NbtUtils.structureToSnbt(capture.sourceTag()).getBytes(StandardCharsets.UTF_8)
);
IrisStructureBundleFactory.SinglePieceOptions bundleOptions = new IrisStructureBundleFactory.SinglePieceOptions(
options.targetKey(),
source,
options.targetKey().path(),
capture.object(),
Math.max(capture.width(), capture.depth()),
options.placeMode(),
options.objectOnly(),
capture.capabilities(),
capture.losses()
);
StructureResourceBundle bundle = IrisStructureBundleFactory.singlePiece(bundleOptions);
if (!options.objectOnly()) {
StructureResourceBundleGraphCompiler.requireViable(bundle);
}
return new PreparedImport(
options.packRoot(),
options.writeOptions(),
ImportKind.TEMPLATE,
bundle,
capture.blocks(),
1,
options.objectOnly() ? 0 : 1
);
} catch (IOException | RuntimeException failure) {
throw report("Failed to prepare structure template import " + options.sourceKey(), failure);
}
}
public PreparedImport prepareJigsawStructure(JigsawImportOptions options) throws StructureImportException {
Objects.requireNonNull(options);
try {
MinecraftServer activeServer = requireServerThread();
ModdedJigsawStructureCapture.Capture capture = ModdedJigsawStructureCapture.capture(
activeServer,
options.sourceKey(),
options.targetKey(),
options.sourceKind()
);
StructureResourceBundleGraphCompiler.requireViable(capture.bundle());
return new PreparedImport(
options.packRoot(),
options.writeOptions(),
ImportKind.JIGSAW_STRUCTURE,
capture.bundle(),
capture.blocks(),
capture.pieces(),
capture.pools()
);
} catch (ModdedJigsawStructureCapture.UnsupportedStructureTypeException failure) {
StructureLoss loss = StructureLoss.error(
StructureCapability.IRIS_PLACEMENT,
"unsupported_native_structure_type",
failure.getMessage()
);
throw report("Cannot import native structure graph " + options.sourceKey(), failure, List.of(loss));
} catch (IOException | RuntimeException failure) {
throw report("Failed to prepare jigsaw structure import " + options.sourceKey(), failure);
}
}
public ImportResult write(PreparedImport prepared) {
Objects.requireNonNull(prepared);
StructureWriteResult writeResult = new StructureTransactionWriter(prepared.packRoot())
.write(prepared.bundle(), prepared.writeOptions());
writeResult.failure().ifPresent((Throwable failure) -> LOGGER.error(
"Failed to commit modded structure import {} to {}",
prepared.bundle().key(),
prepared.packRoot(),
failure
));
if (writeResult.committed()) {
IrisData.getLoaded(prepared.packRoot().toFile()).ifPresent(IrisData::invalidateStructureResources);
}
String message = writeMessage(prepared, writeResult);
return new ImportResult(
writeResult.successful(),
message,
prepared.kind(),
prepared.bundle().key(),
prepared.blocks(),
prepared.pieces(),
prepared.pools(),
prepared.bundle().capabilities(),
prepared.bundle().losses(),
Optional.of(writeResult)
);
}
public ImportResult importTemplate(TemplateImportOptions options) {
try {
return write(prepareTemplate(options));
} catch (StructureImportException failure) {
return failed(options.targetKey(), ImportKind.TEMPLATE, failure.getMessage(), failure.losses());
}
}
public ImportResult importJigsawStructure(JigsawImportOptions options) {
try {
return write(prepareJigsawStructure(options));
} catch (StructureImportException failure) {
return failed(options.targetKey(), ImportKind.JIGSAW_STRUCTURE, failure.getMessage(), failure.losses());
}
}
private MinecraftServer requireServerThread() {
MinecraftServer activeServer = server.get();
if (activeServer == null) {
throw new IllegalStateException("Minecraft server is not available");
}
if (!activeServer.isSameThread()) {
throw new IllegalStateException("Structure registry capture must run on the logical server thread");
}
return activeServer;
}
private StructureImportException report(String context, Exception failure) {
return report(context, failure, List.of());
}
private StructureImportException report(String context, Exception failure, List<StructureLoss> losses) {
LOGGER.error(context, failure);
return new StructureImportException(context + ": " + failureDetail(failure), failure, losses);
}
private static ImportResult failed(
StructureKey targetKey,
ImportKind kind,
String message,
List<StructureLoss> losses
) {
return new ImportResult(
false,
message,
kind,
targetKey,
0,
0,
0,
Set.of(),
losses,
Optional.empty()
);
}
private static String writeMessage(PreparedImport prepared, StructureWriteResult result) {
if (result.successful()) {
return switch (result.status()) {
case DRY_RUN -> "Validated import of '" + prepared.bundle().key() + "' without writing files";
case ADDED -> "Imported '" + prepared.bundle().key() + "'";
case OVERWRITTEN -> "Overwrote owned import '" + prepared.bundle().key() + "'";
case UNCHANGED -> "Import '" + prepared.bundle().key() + "' is already current";
case COMMITTED_CLEANUP_REQUIRED -> "Imported '" + prepared.bundle().key()
+ "'; obsolete staging cleanup is still required";
default -> "Imported '" + prepared.bundle().key() + "'";
};
}
if (!result.conflicts().isEmpty()) {
StructureWriteResult.Conflict conflict = result.conflicts().getFirst();
return "Import conflict for '" + prepared.bundle().key() + "': " + conflict.relativePath()
+ " is " + conflict.reason().name().toLowerCase() + "; existing files were preserved";
}
return "Import failed for '" + prepared.bundle().key() + "': "
+ result.failure().map(ModdedStructureImportService::failureDetail).orElse(result.status().name());
}
private static String failureDetail(Throwable failure) {
String message = failure.getMessage();
return message == null || message.isBlank() ? failure.getClass().getSimpleName() : message;
}
private static Identifier identifier(StructureKey key) {
return Identifier.fromNamespaceAndPath(key.namespace(), key.path());
}
public enum ImportKind {
TEMPLATE,
JIGSAW_STRUCTURE
}
public record TemplateImportOptions(
Path packRoot,
StructureKey sourceKey,
StructureKey targetKey,
StructureSource.Kind sourceKind,
StructureWriteOptions writeOptions,
boolean objectOnly,
String placeMode
) {
public TemplateImportOptions {
packRoot = normalizedRoot(packRoot);
Objects.requireNonNull(sourceKey);
Objects.requireNonNull(targetKey);
Objects.requireNonNull(sourceKind);
Objects.requireNonNull(writeOptions);
Objects.requireNonNull(placeMode);
if (placeMode.isBlank()) {
throw new IllegalArgumentException("Structure place mode cannot be blank");
}
}
public static TemplateImportOptions create(
Path packRoot,
StructureKey sourceKey,
StructureKey targetKey,
StructureWriteMode mode
) {
return new TemplateImportOptions(
packRoot,
sourceKey,
targetKey,
inferredSourceKind(sourceKey),
new StructureWriteOptions(mode, false),
false,
"CENTER_HEIGHT"
);
}
}
public record JigsawImportOptions(
Path packRoot,
StructureKey sourceKey,
StructureKey targetKey,
StructureSource.Kind sourceKind,
StructureWriteOptions writeOptions
) {
public JigsawImportOptions {
packRoot = normalizedRoot(packRoot);
Objects.requireNonNull(sourceKey);
Objects.requireNonNull(targetKey);
Objects.requireNonNull(sourceKind);
Objects.requireNonNull(writeOptions);
}
public static JigsawImportOptions create(
Path packRoot,
StructureKey sourceKey,
StructureKey targetKey,
StructureWriteMode mode
) {
return new JigsawImportOptions(
packRoot,
sourceKey,
targetKey,
inferredSourceKind(sourceKey),
new StructureWriteOptions(mode, false)
);
}
}
public record PreparedImport(
Path packRoot,
StructureWriteOptions writeOptions,
ImportKind kind,
StructureResourceBundle bundle,
int blocks,
int pieces,
int pools
) {
public PreparedImport {
packRoot = normalizedRoot(packRoot);
Objects.requireNonNull(writeOptions);
Objects.requireNonNull(kind);
Objects.requireNonNull(bundle);
if (blocks < 0 || pieces < 0 || pools < 0) {
throw new IllegalArgumentException("Prepared import counts cannot be negative");
}
}
}
public record ImportResult(
boolean success,
String message,
ImportKind kind,
StructureKey targetKey,
int blocks,
int pieces,
int pools,
Set<StructureCapability> capabilities,
List<StructureLoss> losses,
Optional<StructureWriteResult> writeResult
) {
public ImportResult {
Objects.requireNonNull(message);
Objects.requireNonNull(kind);
Objects.requireNonNull(targetKey);
capabilities = Set.copyOf(capabilities);
losses = List.copyOf(losses);
Objects.requireNonNull(writeResult);
}
}
public static final class StructureImportException extends Exception {
private final List<StructureLoss> losses;
public StructureImportException(String message, Throwable cause, List<StructureLoss> losses) {
super(message, cause);
this.losses = List.copyOf(losses);
}
public List<StructureLoss> losses() {
return losses;
}
}
private static Path normalizedRoot(Path path) {
return Objects.requireNonNull(path).toAbsolutePath().normalize();
}
private static StructureSource.Kind inferredSourceKind(StructureKey sourceKey) {
return sourceKey.namespace().equals("minecraft")
? StructureSource.Kind.VANILLA : StructureSource.Kind.DATAPACK;
}
}
@@ -1,339 +0,0 @@
/*
* 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.structure;
import art.arcane.iris.core.structure.authoring.StructureCapability;
import art.arcane.iris.core.structure.authoring.StructureKey;
import art.arcane.iris.core.structure.authoring.StructureLoss;
import art.arcane.iris.engine.object.IrisObject;
import art.arcane.iris.modded.ModdedBlockResolution;
import art.arcane.iris.modded.ModdedBlockState;
import art.arcane.iris.modded.ModdedTileData;
import net.minecraft.core.HolderGetter;
import net.minecraft.core.Vec3i;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.nbt.ListTag;
import net.minecraft.nbt.NbtUtils;
import net.minecraft.world.level.block.Block;
import net.minecraft.world.level.block.Blocks;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.levelgen.structure.templatesystem.StructureTemplate;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.util.ArrayList;
import java.util.EnumSet;
import java.util.List;
import java.util.Objects;
import java.util.Set;
final class ModdedStructureTemplateCapture {
private static final Logger LOGGER = LoggerFactory.getLogger("Iris");
private ModdedStructureTemplateCapture() {
}
static Capture capture(
StructureKey sourceKey,
StructureTemplate template,
HolderGetter<Block> blockLookup,
boolean connectorsPreserved
) {
return capture(sourceKey, template, blockLookup, connectorsPreserved, true);
}
static Capture capture(
StructureKey sourceKey,
StructureTemplate template,
HolderGetter<Block> blockLookup,
boolean connectorsPreserved,
boolean includeAir
) {
Objects.requireNonNull(template);
CompoundTag sourceTag = template.save(new CompoundTag());
return captureTag(sourceKey, sourceTag, blockLookup, connectorsPreserved, includeAir);
}
static Capture captureTag(
StructureKey sourceKey,
CompoundTag sourceTag,
HolderGetter<Block> blockLookup,
boolean connectorsPreserved
) {
return captureTag(sourceKey, sourceTag, blockLookup, connectorsPreserved, true);
}
static Capture captureTag(
StructureKey sourceKey,
CompoundTag sourceTag,
HolderGetter<Block> blockLookup,
boolean connectorsPreserved,
boolean includeAir
) {
Objects.requireNonNull(sourceKey);
Objects.requireNonNull(sourceTag);
Objects.requireNonNull(blockLookup);
Vec3i size = readSize(sourceTag);
IrisObject object = new IrisObject(size.getX(), size.getY(), size.getZ());
List<StructureLoss> losses = new ArrayList<>();
ListTag palette = firstPalette(sourceTag, losses);
ListTag blocks = sourceTag.getListOrEmpty(StructureTemplate.BLOCKS_TAG);
CaptureCounts counts = captureBlocks(sourceKey, palette, blocks, blockLookup, object, losses, includeAir);
recordEntityLoss(sourceTag, losses);
recordMarkerLosses(counts, connectorsPreserved, losses);
EnumSet<StructureCapability> capabilities = EnumSet.of(StructureCapability.BLOCKS);
if (counts.tiles() > 0) {
capabilities.add(StructureCapability.BLOCK_ENTITIES);
}
if (connectorsPreserved && counts.jigsaws() > 0) {
capabilities.add(StructureCapability.CONNECTORS);
}
return new Capture(
object,
counts.blocks(),
counts.tiles(),
counts.jigsaws(),
counts.dataMarkers(),
size.getX(),
size.getY(),
size.getZ(),
capabilities,
losses,
sourceTag.copy()
);
}
private static Vec3i readSize(CompoundTag sourceTag) {
ListTag size = sourceTag.getListOrEmpty(StructureTemplate.SIZE_TAG);
int width = size.getIntOr(0, 0);
int height = size.getIntOr(1, 0);
int depth = size.getIntOr(2, 0);
if (width < 1 || height < 1 || depth < 1) {
throw new IllegalArgumentException("Structure template has invalid dimensions "
+ width + "x" + height + "x" + depth);
}
return new Vec3i(width, height, depth);
}
private static ListTag firstPalette(CompoundTag sourceTag, List<StructureLoss> losses) {
ListTag palettes = sourceTag.getList(StructureTemplate.PALETTE_LIST_TAG).orElse(null);
if (palettes != null) {
if (palettes.isEmpty()) {
throw new IllegalArgumentException("Structure template has no palettes");
}
if (palettes.size() > 1) {
losses.add(StructureLoss.warning(
StructureCapability.BLOCKS,
"palette_variants_not_imported",
"Only palette 0 was converted; " + (palettes.size() - 1)
+ " additional palette(s) remain native-only."
));
}
return palettes.getListOrEmpty(0);
}
ListTag palette = sourceTag.getListOrEmpty(StructureTemplate.PALETTE_TAG);
if (palette.isEmpty() && !sourceTag.getListOrEmpty(StructureTemplate.BLOCKS_TAG).isEmpty()) {
throw new IllegalArgumentException("Structure template has blocks but no palette");
}
return palette;
}
private static CaptureCounts captureBlocks(
StructureKey sourceKey,
ListTag palette,
ListTag blocks,
HolderGetter<Block> blockLookup,
IrisObject object,
List<StructureLoss> losses,
boolean includeAir
) {
int blockCount = 0;
int tiles = 0;
int jigsaws = 0;
int dataMarkers = 0;
for (int index = 0; index < blocks.size(); index++) {
CompoundTag blockTag = blocks.getCompoundOrEmpty(index);
BlockPosition position = readPosition(blockTag);
if (!withinObject(position, object)) {
losses.add(StructureLoss.warning(
StructureCapability.BLOCKS,
"out_of_bounds_block_not_imported",
"Block " + index + " at " + position.x() + "," + position.y() + "," + position.z()
+ " is outside the declared template bounds."
));
continue;
}
int paletteIndex = blockTag.getIntOr(StructureTemplate.BLOCK_TAG_STATE, 0);
BlockState state = NbtUtils.readBlockState(blockLookup, palette.getCompoundOrEmpty(paletteIndex));
CompoundTag blockEntityTag = blockTag.getCompound(StructureTemplate.BLOCK_TAG_NBT).orElse(null);
if (state.is(Blocks.STRUCTURE_VOID)) {
continue;
}
if (state.is(Blocks.STRUCTURE_BLOCK)) {
dataMarkers++;
continue;
}
if (state.is(Blocks.JIGSAW)) {
jigsaws++;
BlockState finalState = resolveJigsawFinalState(sourceKey, position, blockEntityTag, losses);
if (finalState == null || finalState.isAir()) {
continue;
}
state = finalState;
blockEntityTag = null;
}
if (!includeAir && state.isAir()) {
continue;
}
object.setUnsigned(position.x(), position.y(), position.z(), ModdedBlockState.of(state, null));
blockCount++;
if (blockEntityTag != null && captureTile(sourceKey, position, state, blockEntityTag, object, losses)) {
tiles++;
}
}
return new CaptureCounts(blockCount, tiles, jigsaws, dataMarkers);
}
private static BlockPosition readPosition(CompoundTag blockTag) {
ListTag position = blockTag.getListOrEmpty(StructureTemplate.BLOCK_TAG_POS);
return new BlockPosition(
position.getIntOr(0, Integer.MIN_VALUE),
position.getIntOr(1, Integer.MIN_VALUE),
position.getIntOr(2, Integer.MIN_VALUE)
);
}
private static boolean withinObject(BlockPosition position, IrisObject object) {
return position.x() >= 0 && position.x() < object.getW()
&& position.y() >= 0 && position.y() < object.getH()
&& position.z() >= 0 && position.z() < object.getD();
}
private static BlockState resolveJigsawFinalState(
StructureKey sourceKey,
BlockPosition position,
CompoundTag blockEntityTag,
List<StructureLoss> losses
) {
String finalState = blockEntityTag == null
? "minecraft:air"
: blockEntityTag.getStringOr("final_state", "minecraft:air");
try {
return ModdedBlockResolution.strictParse(finalState).handle();
} catch (IllegalArgumentException failure) {
LOGGER.error("Failed to parse jigsaw final state '{}' in {} at {},{},{}",
finalState, sourceKey, position.x(), position.y(), position.z(), failure);
losses.add(StructureLoss.warning(
StructureCapability.BLOCKS,
"jigsaw_final_state_not_imported",
"Jigsaw final state '" + finalState + "' at " + position.x() + "," + position.y() + ","
+ position.z() + " could not be parsed."
));
return null;
}
}
private static boolean captureTile(
StructureKey sourceKey,
BlockPosition position,
BlockState state,
CompoundTag blockEntityTag,
IrisObject object,
List<StructureLoss> losses
) {
try {
String blockKey = ModdedBlockState.serialize(state);
ModdedTileData tile = ModdedTileData.capture(blockKey, NbtUtils.structureToSnbt(blockEntityTag));
object.setUnsignedTile(position.x(), position.y(), position.z(), tile);
return true;
} catch (IOException | RuntimeException failure) {
LOGGER.error("Failed to capture block entity in {} at {},{},{}",
sourceKey, position.x(), position.y(), position.z(), failure);
losses.add(StructureLoss.warning(
StructureCapability.BLOCK_ENTITIES,
"block_entity_not_imported",
"Block entity data at " + position.x() + "," + position.y() + "," + position.z()
+ " could not be encoded."
));
return false;
}
}
private static void recordEntityLoss(CompoundTag sourceTag, List<StructureLoss> losses) {
int entityCount = sourceTag.getListOrEmpty(StructureTemplate.ENTITIES_TAG).size();
if (entityCount > 0) {
losses.add(StructureLoss.warning(
StructureCapability.ENTITIES,
"entities_not_imported",
entityCount + " structure entit" + (entityCount == 1 ? "y was" : "ies were")
+ " not converted into the Iris snapshot."
));
}
}
private static void recordMarkerLosses(
CaptureCounts counts,
boolean connectorsPreserved,
List<StructureLoss> losses
) {
if (counts.dataMarkers() > 0) {
losses.add(StructureLoss.warning(
StructureCapability.PROCESSORS,
"data_markers_not_imported",
counts.dataMarkers() + " structure data marker(s) require native pool-element handlers and were omitted."
));
}
if (!connectorsPreserved && counts.jigsaws() > 0) {
losses.add(StructureLoss.warning(
StructureCapability.CONNECTORS,
"connectors_not_imported",
counts.jigsaws() + " jigsaw connector(s) were resolved to final blocks without importing their pool graph."
));
}
}
record Capture(
IrisObject object,
int blocks,
int tiles,
int jigsaws,
int dataMarkers,
int width,
int height,
int depth,
Set<StructureCapability> capabilities,
List<StructureLoss> losses,
CompoundTag sourceTag
) {
Capture {
Objects.requireNonNull(object);
capabilities = Set.copyOf(capabilities);
losses = List.copyOf(losses);
sourceTag = sourceTag.copy();
}
}
private record CaptureCounts(int blocks, int tiles, int jigsaws, int dataMarkers) {
}
private record BlockPosition(int x, int y, int z) {
}
}
@@ -58,8 +58,8 @@ public class IrisModdedChunkGeneratorSpawnTest {
int spawnEnd = source.indexOf("@Override", spawnStart + 1);
String spawn = source.substring(spawnStart, spawnEnd);
assertTrue(spawn.contains("initializeVanillaSpawnBiomes(registry)"));
assertTrue(spawn.contains("vanillaSpawnBiomes.get(visibleBiome.value())"));
assertTrue(spawn.contains("spawnTables.initializeVanillaSpawnBiomes(registry)"));
assertTrue(spawn.contains("spawnTables.vanillaSpawnBiome(visibleBiome.value())"));
assertTrue(spawn.contains("NaturalSpawner.spawnMobsForChunkGeneration("));
assertTrue(spawn.contains("new LegacyRandomSource(RandomSupport.generateUniqueSeed())"));
assertTrue(spawn.contains("random.setDecorationSeed(region.getSeed()"));
@@ -58,10 +58,10 @@ public class IrisModdedStructureParityTest {
@Test
public void spawnHeightMatchesPaperFixedSpawnClamp() {
assertEquals(96, IrisModdedChunkGenerator.clampSpawnHeight(-64, 384));
assertEquals(96, IrisModdedChunkGenerator.clampSpawnHeight(0, 128));
assertEquals(88, IrisModdedChunkGenerator.clampSpawnHeight(80, 10));
assertEquals(101, IrisModdedChunkGenerator.clampSpawnHeight(100, 20));
assertEquals(96, ModdedDimensionMetadata.clampSpawnHeight(-64, 384));
assertEquals(96, ModdedDimensionMetadata.clampSpawnHeight(0, 128));
assertEquals(88, ModdedDimensionMetadata.clampSpawnHeight(80, 10));
assertEquals(101, ModdedDimensionMetadata.clampSpawnHeight(100, 20));
}
@Test
@@ -109,7 +109,7 @@ public class IrisModdedStructureParityTest {
.setVanillaDerivative("minecraft:plains")
.setInferredType(InferredType.SEA);
Set<String> keys = IrisModdedChunkGenerator.collectConfiguredBiomeKeys(
Set<String> keys = ModdedDimensionMetadata.collectConfiguredBiomeKeys(
List.of(ocean, custom, shore, unsafeSea), "OverWorld");
assertEquals(Set.of("minecraft:deep_ocean", "minecraft:forest", "minecraft:beach",
@@ -150,8 +150,8 @@ public class IrisModdedStructureParityTest {
.setFluidHeight(50);
dimension.setLoadKey("bootstrap_contract");
IrisModdedChunkGenerator.DimensionMetadata metadata =
IrisModdedChunkGenerator.dimensionMetadata(dimension);
ModdedDimensionMetadata.DimensionMetadata metadata =
ModdedDimensionMetadata.dimensionMetadata(dimension);
assertEquals(-256, metadata.minY());
assertEquals(512, metadata.maxY());
@@ -161,8 +161,8 @@ public class IrisModdedStructureParityTest {
@Test
public void structureRingWorkersWaitWithoutBlockingLifecycleBinding() throws Exception {
IrisModdedChunkGenerator.EngineBinding<String> binding =
new IrisModdedChunkGenerator.EngineBinding<>(5L, TimeUnit.SECONDS);
ModdedEngineBinding<String> binding =
new ModdedEngineBinding<>(5L, TimeUnit.SECONDS);
String exactEngine = "exact-engine";
CountDownLatch workerStarted = new CountDownLatch(1);
ExecutorService executor = Executors.newSingleThreadExecutor();
@@ -184,8 +184,8 @@ public class IrisModdedStructureParityTest {
@Test
public void structureRingBindingPropagatesBootstrapFailure() {
IrisModdedChunkGenerator.EngineBinding<String> binding =
new IrisModdedChunkGenerator.EngineBinding<>(1L, TimeUnit.SECONDS);
ModdedEngineBinding<String> binding =
new ModdedEngineBinding<>(1L, TimeUnit.SECONDS);
IllegalArgumentException failure = new IllegalArgumentException("broken pack");
binding.fail(failure);
@@ -200,16 +200,16 @@ public class IrisModdedStructureParityTest {
@Test
public void structureBiomeBootstrapAllowsOnlyPendingBindingsToUseMetadata() {
IrisModdedChunkGenerator.EngineBinding<String> binding =
new IrisModdedChunkGenerator.EngineBinding<>(1L, TimeUnit.SECONDS);
ModdedEngineBinding<String> binding =
new ModdedEngineBinding<>(1L, TimeUnit.SECONDS);
binding.throwIfFailed("overworld:overworld");
}
@Test
public void structureBiomeBootstrapPropagatesBindingFailure() {
IrisModdedChunkGenerator.EngineBinding<String> binding =
new IrisModdedChunkGenerator.EngineBinding<>(1L, TimeUnit.SECONDS);
ModdedEngineBinding<String> binding =
new ModdedEngineBinding<>(1L, TimeUnit.SECONDS);
IllegalArgumentException failure = new IllegalArgumentException("broken pack");
binding.fail(failure);
@@ -152,13 +152,13 @@ public class ModdedDimensionTypeParityTest {
.ceiling(TRUE);
IrisDimension dimension = dimension("runtime_contract", IrisEnvironment.CUSTOM,
-128, 384, 384, options);
ModdedWorldCheck.DimensionContract expected = ModdedWorldCheck.expectedDimensionContract(dimension);
ModdedWorldCheck.DimensionContract fallback = new ModdedWorldCheck.DimensionContract(
WorldCheckDimensionContract.DimensionContract expected = WorldCheckDimensionContract.expectedDimensionContract(dimension);
WorldCheckDimensionContract.DimensionContract fallback = new WorldCheckDimensionContract.DimensionContract(
-256, 768, 512, 1D, 0F, true, false, false, 0);
assertTrue(ModdedWorldCheck.matchesDimensionContract(-128, 512, expected, expected));
assertFalse(ModdedWorldCheck.matchesDimensionContract(-256, 768, expected, fallback));
assertFalse(ModdedWorldCheck.matchesDimensionContract(-128, 512, expected, fallback));
assertTrue(WorldCheckDimensionContract.matchesDimensionContract(-128, 512, expected, expected));
assertFalse(WorldCheckDimensionContract.matchesDimensionContract(-256, 768, expected, fallback));
assertFalse(WorldCheckDimensionContract.matchesDimensionContract(-128, 512, expected, fallback));
}
private static IrisDimension dimension(String key, IrisEnvironment environment, int minY, int maxY,
@@ -28,40 +28,43 @@ public class ModdedWorldCheckTest {
@Test
public void poiAuditRunsInASecondServerTaskAfterVillageGeneration() throws IOException {
Path sourcePath = Path.of(System.getProperty("iris.moddedCommonSources"),
"art/arcane/iris/modded/ModdedWorldCheck.java");
String source = Files.readString(sourcePath);
Path sourceRoot = Path.of(System.getProperty("iris.moddedCommonSources"));
String source = Files.readString(sourceRoot.resolve("art/arcane/iris/modded/ModdedWorldCheck.java"));
String auditSource = Files.readString(
sourceRoot.resolve("art/arcane/iris/modded/WorldCheckStructureAudit.java"));
int preparationSubmit = source.indexOf(
"WorldCheckPreparation preparation = serverRef.submit(() -> run(serverRef)).join();");
int completionSubmit = source.indexOf(
"exitCode = serverRef.submit(() -> runAndRequestStop(", preparationSubmit);
int completionMethod = source.indexOf("private static boolean completeWorldCheck");
int deferredAudit = source.indexOf("PoiAudit poi = auditStructurePois", completionMethod);
int structureMethod = source.indexOf("private static StructureCheckResult checkNativeStructure");
int structureMethodEnd = source.indexOf("private static StructureStart resolveStructureStart",
int deferredAudit = source.indexOf(
"PoiAudit poi = WorldCheckStructureAudit.auditStructurePois", completionMethod);
int structureMethod = auditSource.indexOf("private static StructureCheckResult checkNativeStructure");
int structureMethodEnd = auditSource.indexOf("private static StructureStart resolveStructureStart",
structureMethod);
String structureSource = source.substring(structureMethod, structureMethodEnd);
String structureSource = auditSource.substring(structureMethod, structureMethodEnd);
assertTrue(preparationSubmit >= 0);
assertTrue(completionSubmit > preparationSubmit);
assertTrue(deferredAudit > completionMethod);
assertFalse(structureSource.contains("auditStructurePois"));
assertFalse(source.contains("prepareDeferredAudits"));
assertFalse(auditSource.contains("prepareDeferredAudits"));
}
@Test
public void validStructureStartIsGenerationEvidence() {
assertTrue(ModdedWorldCheck.hasNativeStructureEvidence(true, 0));
assertTrue(WorldCheckPredicates.hasNativeStructureEvidence(true, 0));
}
@Test
public void structureReferenceIsGenerationEvidence() {
assertTrue(ModdedWorldCheck.hasNativeStructureEvidence(false, 1));
assertTrue(WorldCheckPredicates.hasNativeStructureEvidence(false, 1));
}
@Test
public void absentStartAndReferencesFailGenerationEvidence() {
assertFalse(ModdedWorldCheck.hasNativeStructureEvidence(false, 0));
assertFalse(WorldCheckPredicates.hasNativeStructureEvidence(false, 0));
}
@Test
@@ -91,68 +94,68 @@ public class ModdedWorldCheckTest {
@Test
public void materialEvidenceMustExist() {
assertFalse(ModdedWorldCheck.hasCharacteristicMaterialEvidence(0, 0, 1));
assertFalse(ModdedWorldCheck.hasCharacteristicMaterialEvidence(8, 0, 1));
assertFalse(ModdedWorldCheck.hasCharacteristicMaterialEvidence(8, 1, 0));
assertFalse(ModdedWorldCheck.hasCharacteristicMaterialEvidence(8, 2, 1));
assertFalse(WorldCheckPredicates.hasCharacteristicMaterialEvidence(0, 0, 1));
assertFalse(WorldCheckPredicates.hasCharacteristicMaterialEvidence(8, 0, 1));
assertFalse(WorldCheckPredicates.hasCharacteristicMaterialEvidence(8, 1, 0));
assertFalse(WorldCheckPredicates.hasCharacteristicMaterialEvidence(8, 2, 1));
}
@Test
public void singleChunkStructureAcceptsMaterialInItsOnlyChunk() {
assertTrue(ModdedWorldCheck.hasCharacteristicMaterialEvidence(8, 1, 1));
assertTrue(WorldCheckPredicates.hasCharacteristicMaterialEvidence(8, 1, 1));
}
@Test
public void multiChunkStructureRejectsMaterialConfinedToOneChunk() {
assertFalse(ModdedWorldCheck.hasCharacteristicMaterialEvidence(8, 1, 4));
assertTrue(ModdedWorldCheck.hasCharacteristicMaterialEvidence(8, 2, 4));
assertFalse(WorldCheckPredicates.hasCharacteristicMaterialEvidence(8, 1, 4));
assertTrue(WorldCheckPredicates.hasCharacteristicMaterialEvidence(8, 2, 4));
}
@Test
public void configuredVerticalShiftRequiresSafetyClampedGenerationEvidence() {
assertTrue(ModdedWorldCheck.verticalShiftMatches(0, null, -32, 20, -64, 320));
assertFalse(ModdedWorldCheck.verticalShiftMatches(0, null, -112, -80, -64, 320));
assertTrue(ModdedWorldCheck.verticalShiftMatches(0, 0, -32, 20, -64, 320));
assertTrue(ModdedWorldCheck.verticalShiftMatches(0, 48, -64, -32, -64, 320));
assertTrue(ModdedWorldCheck.verticalShiftMatches(-64, -64, -48, 4, -64, 320));
assertTrue(ModdedWorldCheck.verticalShiftMatches(-64, -16, -64, -12, -64, 320));
assertFalse(ModdedWorldCheck.verticalShiftMatches(-64, null, -48, 4, -64, 320));
assertFalse(ModdedWorldCheck.verticalShiftMatches(-64, -15, -63, -11, -64, 320));
assertFalse(ModdedWorldCheck.verticalShiftMatches(0, -1, -33, 19, -64, 320));
assertTrue(WorldCheckPredicates.verticalShiftMatches(0, null, -32, 20, -64, 320));
assertFalse(WorldCheckPredicates.verticalShiftMatches(0, null, -112, -80, -64, 320));
assertTrue(WorldCheckPredicates.verticalShiftMatches(0, 0, -32, 20, -64, 320));
assertTrue(WorldCheckPredicates.verticalShiftMatches(0, 48, -64, -32, -64, 320));
assertTrue(WorldCheckPredicates.verticalShiftMatches(-64, -64, -48, 4, -64, 320));
assertTrue(WorldCheckPredicates.verticalShiftMatches(-64, -16, -64, -12, -64, 320));
assertFalse(WorldCheckPredicates.verticalShiftMatches(-64, null, -48, 4, -64, 320));
assertFalse(WorldCheckPredicates.verticalShiftMatches(-64, -15, -63, -11, -64, 320));
assertFalse(WorldCheckPredicates.verticalShiftMatches(0, -1, -33, 19, -64, 320));
}
@Test
public void mansionVegetationGateRejectsRemainingLeaves() {
assertTrue(ModdedWorldCheck.mansionVegetationPass(0));
assertFalse(ModdedWorldCheck.mansionVegetationPass(1));
assertTrue(WorldCheckPredicates.mansionVegetationPass(0));
assertFalse(WorldCheckPredicates.mansionVegetationPass(1));
}
@Test
public void mansionVegetationAuditIgnoresTemplateBlocksAndRejectsVegetationAbovePieces() {
assertFalse(ModdedWorldCheck.mansionVegetationAbovePiece(true, 80, 80));
assertFalse(ModdedWorldCheck.mansionVegetationAbovePiece(true, 79, 80));
assertTrue(ModdedWorldCheck.mansionVegetationAbovePiece(true, 81, 80));
assertFalse(ModdedWorldCheck.mansionVegetationAbovePiece(false, 81, 80));
assertFalse(WorldCheckPredicates.mansionVegetationAbovePiece(true, 80, 80));
assertFalse(WorldCheckPredicates.mansionVegetationAbovePiece(true, 79, 80));
assertTrue(WorldCheckPredicates.mansionVegetationAbovePiece(true, 81, 80));
assertFalse(WorldCheckPredicates.mansionVegetationAbovePiece(false, 81, 80));
}
@Test
public void villageFoundationGateRejectsUnsupportedColumns() {
assertTrue(ModdedWorldCheck.villageFoundationPass(0));
assertFalse(ModdedWorldCheck.villageFoundationPass(1));
assertTrue(WorldCheckPredicates.villageFoundationPass(0));
assertFalse(WorldCheckPredicates.villageFoundationPass(1));
}
@Test
public void villagePoiGateRequiresInBoundsPoiWithoutOutOfBoundsRecords() {
assertTrue(ModdedWorldCheck.villagePoiPass(1, 0));
assertFalse(ModdedWorldCheck.villagePoiPass(0, 0));
assertFalse(ModdedWorldCheck.villagePoiPass(1, 1));
assertTrue(WorldCheckPredicates.villagePoiPass(1, 0));
assertFalse(WorldCheckPredicates.villagePoiPass(0, 0));
assertFalse(WorldCheckPredicates.villagePoiPass(1, 1));
}
@Test
public void smallStructureFootprintIncludesEveryChunk() {
BoundingBox bounds = new BoundingBox(-16, -20, -16, 31, 120, 31);
List<ChunkPos> chunks = ModdedWorldCheck.boundedFootprintChunks(bounds, ChunkPos.ZERO, 96);
List<ChunkPos> chunks = WorldCheckStructureAudit.boundedFootprintChunks(bounds, ChunkPos.ZERO, 96);
assertEquals(9, chunks.size());
assertTrue(chunks.contains(new ChunkPos(-1, -1)));
@@ -163,7 +166,7 @@ public class ModdedWorldCheckTest {
public void largeStructureFootprintIsBoundedAndSamplesEdges() {
BoundingBox bounds = new BoundingBox(-512, -64, -512, 511, 300, 511);
List<ChunkPos> chunks = ModdedWorldCheck.boundedFootprintChunks(bounds, ChunkPos.ZERO, 20);
List<ChunkPos> chunks = WorldCheckStructureAudit.boundedFootprintChunks(bounds, ChunkPos.ZERO, 20);
assertTrue(chunks.size() <= 20);
assertTrue(chunks.size() >= 16);
@@ -174,7 +177,7 @@ public class ModdedWorldCheckTest {
@Test
public void qaEventsEscapeStructuredValues() {
String event = ModdedWorldCheck.qaEventJson("locate\"", "village\n", false, "x\\y\t");
String event = WorldCheckPredicates.qaEventJson("locate\"", "village\n", false, "x\\y\t");
assertEquals("QA_EVT {\"event\":\"locate\\\"\",\"structure\":\"village\\n\","
+ "\"pass\":false,\"detail\":\"x\\\\y\\t\"}", event);
@@ -292,7 +295,7 @@ public class ModdedWorldCheckTest {
}
private static boolean characteristic(String structureLabel, String structureKey, String blockKey) {
return ModdedWorldCheck.isCharacteristicMaterial(structureLabel,
return WorldCheckMaterials.isCharacteristicMaterial(structureLabel,
Identifier.parse(structureKey), Identifier.parse(blockKey));
}
}
@@ -24,15 +24,14 @@ public class NativeStructureFailureContractTest {
@Test
public void structureLocateDoesNotCatchAndFallThroughToAnotherImplementation() throws IOException {
Path sourcePath = Path.of(System.getProperty("iris.moddedCommonSources"),
"art/arcane/iris/modded/IrisModdedChunkGenerator.java");
String source = Files.readString(sourcePath);
String source = moddedSource("IrisModdedChunkGenerator.java");
int locateStart = source.indexOf("public Pair<BlockPos, Holder<Structure>> findNearestMapStructure");
int locateEnd = source.indexOf("public boolean isNativeStructureReachable", locateStart);
String locate = source.substring(locateStart, locateEnd);
int filterStart = source.indexOf("private HolderSet<Structure> filterReachableNativeStructures");
int filterEnd = source.indexOf("private ServerLevel boundLevel", filterStart);
String filter = source.substring(filterStart, filterEnd);
String stage = moddedSource("ModdedNativeStructureStage.java");
int filterStart = stage.indexOf("HolderSet<Structure> filterReachableNativeStructures");
int filterEnd = stage.indexOf("void adjustGeneratedStructures", filterStart);
String filter = stage.substring(filterStart, filterEnd);
assertTrue(locate.contains("Engine current = engine();"));
assertFalse(locate.contains("catch (Throwable"));
@@ -58,10 +57,8 @@ public class NativeStructureFailureContractTest {
@Test
public void structureTerrainPreparationPrecedesVegetationAndPlacement() throws IOException {
Path sourcePath = Path.of(System.getProperty("iris.moddedCommonSources"),
"art/arcane/iris/modded/IrisModdedChunkGenerator.java");
String source = Files.readString(sourcePath);
int placementStart = source.indexOf("private void placeVanillaStructures");
String source = moddedSource("ModdedNativeStructureStage.java");
int placementStart = source.indexOf("void placeVanillaStructures");
int placementEnd = source.indexOf("private static String nativeStructureBatchContext", placementStart);
String placement = source.substring(placementStart, placementEnd);
@@ -74,6 +71,23 @@ public class NativeStructureFailureContractTest {
< placement.indexOf("for (NativePlacementGroup group"));
}
@Test
public void structurePlacementPrimesNeighbourWorldgenHeightmapsBeforeTerrainPreparation() throws IOException {
String source = moddedSource("ModdedNativeStructureStage.java");
int placementStart = source.indexOf("void placeVanillaStructures");
int placementEnd = source.indexOf("private static String nativeStructureBatchContext", placementStart);
String placement = source.substring(placementStart, placementEnd);
assertTrue(source.contains("import art.arcane.iris.nativegen.WorldgenTerrainHeightmaps;"));
assertTrue(placement.contains("WorldgenTerrainHeightmaps.primeStructurePlacement("));
assertTrue(placement.contains("\"heightmap priming\""));
assertTrue(placement.contains("heightmapStarts.add(start);"));
assertTrue(placement.indexOf("WorldgenTerrainHeightmaps.primeStructurePlacement(")
< placement.indexOf("prepareSurfaceStructures"));
assertTrue(source.contains("generationEngine.getHeight(x, z, false) + runtimeMinY + 1"));
assertTrue(source.contains("generationEngine.getHeight(x, z, true) + runtimeMinY + 1"));
}
@Test
public void structureFailurePreservesPhaseIdentityChunkAndCause() {
IllegalArgumentException cause = new IllegalArgumentException("broken placement");
@@ -86,4 +100,10 @@ public class NativeStructureFailureContractTest {
assertTrue(error.getMessage().contains("12,-8"));
assertTrue(error.getMessage().contains("aborted"));
}
private static String moddedSource(String fileName) throws IOException {
Path sourcePath = Path.of(System.getProperty("iris.moddedCommonSources"),
"art", "arcane", "iris", "modded", fileName);
return Files.readString(sourcePath);
}
}
@@ -12,7 +12,8 @@ import static org.junit.Assert.assertTrue;
public class IrisModdedStructureCommandTest {
@Test
public void gotoStructureSupportsIrisAndNativeRegistryTargets() throws IOException {
String source = source("IrisModdedCommands.java");
String source = source("ModdedLocateCommands.java");
String suggestions = source("ModdedCommandSuggestions.java");
assertTrue(source.contains("IrisStructureLocator.isPlaced(engine, key)"));
assertTrue(source.contains("registry.get(identifier)"));
@@ -22,7 +23,7 @@ public class IrisModdedStructureCommandTest {
assertTrue(source.contains("HolderSet.direct(target.holder())"));
assertFalse(source.contains("NativeStructureLocateCapability"));
assertTrue(source.contains("boolean teleported = player.teleportTo("));
assertTrue(source.contains("combineStructureKeys(irisKeys, nativeKeys)"));
assertTrue(suggestions.contains("combineStructureKeys(irisKeys, nativeKeys)"));
assertTrue(source.contains("irisGenerator.isNativeStructureReachable(holder)"));
assertTrue(source.contains("LocateStatus.SEARCH_LIMIT_REACHED"));
assertTrue(source.contains("IRIS_MODDED_COMMANDS_UNABLE_LOCATE_IRIS_PLACED_STRUCTURE_DENSITY_SEARCH_SAFETY_LIMIT_WAS"));
@@ -35,9 +36,9 @@ public class IrisModdedStructureCommandTest {
@Test
public void generatorLocateUsesEveryIrisPlacedNativeStructure() throws IOException {
String source = moddedSource("IrisModdedChunkGenerator.java");
int methodStart = source.indexOf("private Pair<BlockPos, Holder<Structure>> findNearestIrisStructure(");
int methodEnd = source.indexOf("private HolderSet<Structure> filterReachableNativeStructures(", methodStart);
String source = moddedSource("ModdedNativeStructureStage.java");
int methodStart = source.indexOf("Pair<BlockPos, Holder<Structure>> findNearestIrisStructure(");
int methodEnd = source.indexOf("HolderSet<Structure> filterReachableNativeStructures(", methodStart);
String method = source.substring(methodStart, methodEnd);
int unexploredGuard = method.indexOf("if (findUnexplored)");
int registryLookup = method.indexOf("level.registryAccess().lookupOrThrow(Registries.STRUCTURE)");
@@ -58,8 +59,8 @@ public class IrisModdedStructureCommandTest {
@Test
public void commandResolvesNativePolicyBeforeAnyVanillaAliasLookup() throws IOException {
String source = source("IrisModdedCommands.java");
int methodStart = source.indexOf("private static int gotoStructure(");
String source = source("ModdedLocateCommands.java");
int methodStart = source.indexOf("static int gotoStructure(");
int methodEnd = source.indexOf("private static void locateIrisStructure(", methodStart);
String method = source.substring(methodStart, methodEnd);
int nativeResolution = method.indexOf("resolveNativeStructure(source, level, engine, key)");
@@ -79,7 +80,7 @@ public class IrisModdedStructureCommandTest {
@Test
public void verifyResolvesRegisteredNativeBeforeGenericIrisAliases() throws IOException {
String source = source("IrisModdedCommands.java");
String source = source("ModdedLocateCommands.java");
int methodStart = source.indexOf("private static int verifyStructure(");
int methodEnd = source.indexOf("private static Optional<NativeStructureTarget> resolveNativeStructure(",
methodStart);
@@ -1,69 +0,0 @@
/*
* 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.structure;
import net.minecraft.core.Direction;
import org.junit.Test;
import java.util.Map;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotEquals;
public class ModdedJigsawStructureCaptureTest {
@Test
public void namespacedSourcePathsRemainDistinctAndPortable() {
String nested = ModdedJigsawStructureCapture.pieceName("village", "mod:a/b");
String underscored = ModdedJigsawStructureCapture.pieceName("village", "mod_a:b");
assertEquals("village/piece/mod/a/b", nested);
assertEquals("village/piece/mod_a/b", underscored);
assertNotEquals(nested, underscored);
assertEquals("village/pool/mod/a/b", ModdedJigsawStructureCapture.poolName("village", "mod:a/b"));
assertEquals(
"village/piece/generated/legacy/mod/a/b",
ModdedJigsawStructureCapture.legacyPieceName("village", "mod:a/b")
);
}
@Test
public void rootJsonRetainsGraphLimitsAndSourceIdentity() {
Map<String, Object> root = ModdedJigsawStructureCapture.structureJson(
"minecraft:village_plains",
"village/pool/minecraft/village/plains/town_centers",
6,
81
);
assertEquals("minecraft:village_plains", root.get("vanillaSource"));
assertEquals(6, root.get("maxDepth"));
assertEquals(6, root.get("maxSizeChunks"));
assertEquals("STRUCTURE_PIECE", root.get("placeMode"));
}
@Test
public void connectorDirectionsUseIrisAxisNames() {
assertEquals("UP_POSITIVE_Y", ModdedJigsawStructureCapture.directionName(Direction.UP));
assertEquals("DOWN_NEGATIVE_Y", ModdedJigsawStructureCapture.directionName(Direction.DOWN));
assertEquals("NORTH_NEGATIVE_Z", ModdedJigsawStructureCapture.directionName(Direction.NORTH));
assertEquals("SOUTH_POSITIVE_Z", ModdedJigsawStructureCapture.directionName(Direction.SOUTH));
assertEquals("EAST_POSITIVE_X", ModdedJigsawStructureCapture.directionName(Direction.EAST));
assertEquals("WEST_NEGATIVE_X", ModdedJigsawStructureCapture.directionName(Direction.WEST));
}
}
@@ -1,86 +0,0 @@
/*
* 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.structure;
import art.arcane.iris.core.structure.authoring.StructureBackend;
import art.arcane.iris.core.structure.authoring.StructureCapability;
import art.arcane.iris.core.structure.authoring.StructureKey;
import art.arcane.iris.core.structure.authoring.StructureResourceBundle;
import art.arcane.iris.core.structure.authoring.StructureSource;
import art.arcane.iris.core.structure.authoring.StructureWriteMode;
import art.arcane.iris.core.structure.authoring.StructureWriteOptions;
import art.arcane.iris.core.structure.authoring.StructureWriteResult;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TemporaryFolder;
import java.nio.file.Path;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
public class ModdedStructureImportServiceTest {
@Rule
public TemporaryFolder temporaryFolder = new TemporaryFolder();
@Test
public void writesThroughOwnedAddOnlyAndOverwriteTransactions() throws Exception {
Path root = temporaryFolder.newFolder("modded-import").toPath();
ModdedStructureImportService service = new ModdedStructureImportService(() -> null);
ModdedStructureImportService.PreparedImport first = prepared(root, "one", StructureWriteMode.ADD_ONLY);
ModdedStructureImportService.ImportResult added = service.write(first);
ModdedStructureImportService.ImportResult conflict = service.write(first);
ModdedStructureImportService.ImportResult overwritten = service.write(
prepared(root, "two", StructureWriteMode.OVERWRITE)
);
assertTrue(added.success());
assertEquals(StructureWriteResult.Status.ADDED, added.writeResult().orElseThrow().status());
assertFalse(conflict.success());
assertEquals(StructureWriteResult.Status.ADD_ONLY_CONFLICT, conflict.writeResult().orElseThrow().status());
assertTrue(overwritten.success());
assertEquals(StructureWriteResult.Status.OVERWRITTEN, overwritten.writeResult().orElseThrow().status());
assertTrue(overwritten.capabilities().contains(StructureCapability.BLOCKS));
}
private static ModdedStructureImportService.PreparedImport prepared(
Path root,
String content,
StructureWriteMode mode
) {
StructureKey key = StructureKey.parse("iris:test_structure");
StructureResourceBundle bundle = StructureResourceBundle.builder(key)
.source(StructureSource.of(StructureSource.Kind.DATAPACK, StructureKey.parse("test:source")))
.backend(StructureBackend.SNAPSHOT)
.capability(StructureCapability.BLOCKS)
.textResource("structures/test_structure.json", content)
.build();
return new ModdedStructureImportService.PreparedImport(
root,
new StructureWriteOptions(mode, false),
ModdedStructureImportService.ImportKind.TEMPLATE,
bundle,
1,
1,
1
);
}
}
@@ -1,184 +0,0 @@
/*
* 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.structure;
import art.arcane.iris.core.structure.authoring.StructureCapability;
import art.arcane.iris.core.structure.authoring.StructureKey;
import net.minecraft.SharedConstants;
import net.minecraft.core.registries.BuiltInRegistries;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.nbt.IntTag;
import net.minecraft.nbt.ListTag;
import net.minecraft.nbt.NbtUtils;
import net.minecraft.world.level.block.Blocks;
import net.minecraft.world.level.levelgen.structure.templatesystem.StructureTemplate;
import net.minecraft.server.Bootstrap;
import org.junit.BeforeClass;
import org.junit.Test;
import java.io.ByteArrayOutputStream;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
public class ModdedStructureTemplateCaptureTest {
@BeforeClass
public static void bootstrap() {
SharedConstants.tryDetectVersion();
Bootstrap.bootStrap();
}
@Test
public void capturesBlocksTilesAndExplicitStandaloneLosses() throws Exception {
CompoundTag template = templateTag();
ModdedStructureTemplateCapture.Capture capture = ModdedStructureTemplateCapture.captureTag(
StructureKey.parse("minecraft:test/template"),
template,
BuiltInRegistries.BLOCK,
false
);
assertEquals(4, capture.width());
assertEquals(1, capture.height());
assertEquals(1, capture.depth());
assertEquals(3, capture.blocks());
assertEquals(1, capture.tiles());
assertEquals(1, capture.jigsaws());
assertEquals(1, capture.dataMarkers());
assertEquals(3, capture.object().getBlocks().size());
assertEquals(1, capture.object().getStates().size());
assertTrue(capture.capabilities().contains(StructureCapability.BLOCKS));
assertTrue(capture.capabilities().contains(StructureCapability.BLOCK_ENTITIES));
assertFalse(capture.capabilities().contains(StructureCapability.CONNECTORS));
assertTrue(hasLoss(capture, "connectors_not_imported"));
assertTrue(hasLoss(capture, "data_markers_not_imported"));
assertTrue(hasLoss(capture, "entities_not_imported"));
ByteArrayOutputStream serialized = new ByteArrayOutputStream();
capture.object().write(serialized);
assertTrue(serialized.size() > 0);
}
@Test
public void graphCaptureReportsConnectorCapabilityWithoutStandaloneConnectorLoss() {
ModdedStructureTemplateCapture.Capture capture = ModdedStructureTemplateCapture.captureTag(
StructureKey.parse("minecraft:test/template"),
templateTag(),
BuiltInRegistries.BLOCK,
true
);
assertTrue(capture.capabilities().contains(StructureCapability.CONNECTORS));
assertFalse(hasLoss(capture, "connectors_not_imported"));
}
@Test
public void reportsAdditionalNativePalettes() {
CompoundTag template = templateTag();
ListTag palettes = new ListTag();
ListTag first = template.getListOrEmpty(StructureTemplate.PALETTE_TAG);
palettes.add(first.copy());
palettes.add(first.copy());
template.remove(StructureTemplate.PALETTE_TAG);
template.put(StructureTemplate.PALETTE_LIST_TAG, palettes);
ModdedStructureTemplateCapture.Capture capture = ModdedStructureTemplateCapture.captureTag(
StructureKey.parse("minecraft:test/template"),
template,
BuiltInRegistries.BLOCK,
true
);
assertTrue(hasLoss(capture, "palette_variants_not_imported"));
}
@Test
public void legacyCaptureOmitsAirBlocks() {
CompoundTag template = new CompoundTag();
template.put(StructureTemplate.SIZE_TAG, intList(1, 1, 1));
ListTag palette = new ListTag();
palette.add(NbtUtils.writeBlockState(Blocks.AIR.defaultBlockState()));
template.put(StructureTemplate.PALETTE_TAG, palette);
ListTag blocks = new ListTag();
blocks.add(block(0, 0, 0, 0, null));
template.put(StructureTemplate.BLOCKS_TAG, blocks);
ModdedStructureTemplateCapture.Capture capture = ModdedStructureTemplateCapture.captureTag(
StructureKey.parse("minecraft:test/legacy"),
template,
BuiltInRegistries.BLOCK,
true,
false
);
assertEquals(0, capture.blocks());
assertTrue(capture.object().getBlocks().isEmpty());
}
private static CompoundTag templateTag() {
CompoundTag template = new CompoundTag();
template.put(StructureTemplate.SIZE_TAG, intList(4, 1, 1));
ListTag palette = new ListTag();
palette.add(NbtUtils.writeBlockState(Blocks.STONE.defaultBlockState()));
palette.add(NbtUtils.writeBlockState(Blocks.CHEST.defaultBlockState()));
palette.add(NbtUtils.writeBlockState(Blocks.JIGSAW.defaultBlockState()));
palette.add(NbtUtils.writeBlockState(Blocks.STRUCTURE_BLOCK.defaultBlockState()));
template.put(StructureTemplate.PALETTE_TAG, palette);
ListTag blocks = new ListTag();
blocks.add(block(0, 0, 0, 0, null));
CompoundTag chest = new CompoundTag();
chest.putString("id", "minecraft:chest");
chest.putString("CustomName", "test");
blocks.add(block(1, 0, 0, 1, chest));
CompoundTag jigsaw = new CompoundTag();
jigsaw.putString("final_state", "minecraft:oak_planks");
blocks.add(block(2, 0, 0, 2, jigsaw));
blocks.add(block(3, 0, 0, 3, new CompoundTag()));
template.put(StructureTemplate.BLOCKS_TAG, blocks);
ListTag entities = new ListTag();
entities.add(new CompoundTag());
template.put(StructureTemplate.ENTITIES_TAG, entities);
return template;
}
private static CompoundTag block(int x, int y, int z, int state, CompoundTag nbt) {
CompoundTag block = new CompoundTag();
block.put(StructureTemplate.BLOCK_TAG_POS, intList(x, y, z));
block.putInt(StructureTemplate.BLOCK_TAG_STATE, state);
if (nbt != null) {
block.put(StructureTemplate.BLOCK_TAG_NBT, nbt);
}
return block;
}
private static ListTag intList(int... values) {
ListTag list = new ListTag();
for (int value : values) {
list.add(IntTag.valueOf(value));
}
return list;
}
private static boolean hasLoss(ModdedStructureTemplateCapture.Capture capture, String code) {
return capture.losses().stream().anyMatch(loss -> loss.code().equals(code));
}
}