Gate pools, placements and structures

- Filter excluded biomes, regions, spawners, loot tables, jigsaw pieces and structures out of every pool through CompatPools and cascade empty pools to the container
- Evaluate each object placement per object from the .iob palette header, honouring chance-1 edit rules and reporting every missing palette key
- Treat edit find lists and marker mark lists as match-only
- Exclude jigsaw pieces on missing palette keys, drop them from pools and exclude structures whose start pool is gone
- Drop custom biome spawns whose entity is missing before the datapack JSON is written
- Skip excluded placements in the object, floating object, tree and locator paths while keeping the per-chunk RNG draws unchanged
- Keep the engine focus biome and region null when the gate excluded them
This commit is contained in:
Brian Neumann-Fopiano
2026-09-03 04:54:35 -04:00
parent 516ef71c9c
commit 0d464aabf3
44 changed files with 2774 additions and 93 deletions
@@ -2,6 +2,7 @@ package art.arcane.iris.core;
import art.arcane.iris.BuildConstants; import art.arcane.iris.BuildConstants;
import art.arcane.iris.core.lifecycle.BukkitWorldConfiguration.IrisGeneratorBinding; import art.arcane.iris.core.lifecycle.BukkitWorldConfiguration.IrisGeneratorBinding;
import art.arcane.iris.core.compat.ContentGate;
import art.arcane.iris.core.loader.IrisData; import art.arcane.iris.core.loader.IrisData;
import art.arcane.iris.core.loader.ResourceLoader; import art.arcane.iris.core.loader.ResourceLoader;
import art.arcane.iris.core.nms.datapack.DataVersion; import art.arcane.iris.core.nms.datapack.DataVersion;
@@ -235,6 +236,7 @@ public final class IrisDatapackCompiler {
fingerprintContent(dimension.getDimensionType().toJson(fixer))); fingerprintContent(dimension.getDimensionType().toJson(fixer)));
String namespace = dimension.getLoadKey().toLowerCase(Locale.ROOT); String namespace = dimension.getLoadKey().toLowerCase(Locale.ROOT);
ContentGate contentGate = dimension.getLoader() == null ? null : dimension.getLoader().getContentGate();
for (IrisBiome biome : dimension.getAllBiomes(dimension::getLoader)) { for (IrisBiome biome : dimension.getAllBiomes(dimension::getLoader)) {
if (biome == null || !biome.isCustom()) { if (biome == null || !biome.isCustom()) {
continue; continue;
@@ -250,7 +252,7 @@ public final class IrisDatapackCompiler {
} }
requirements.put( requirements.put(
"worldgen/biome/" + biomeKey, "worldgen/biome/" + biomeKey,
fingerprintContent(customBiome.generateJson(fixer))); fingerprintContent(customBiome.generateJson(fixer, contentGate)));
TreeSet<String> tags = new TreeSet<>(); TreeSet<String> tags = new TreeSet<>();
for (String tag : customBiome.getEffectiveTags(derivativeKey)) { for (String tag : customBiome.getEffectiveTags(derivativeKey)) {
String normalizedTag = normalizeRegistryKey(tag); String normalizedTag = normalizeRegistryKey(tag);
@@ -142,8 +142,19 @@ public class TreeSVC implements IrisService {
return; return;
} }
KList<String> pool = placement.compatPlace(worldAccess.getData());
if (pool.isEmpty()) {
IrisLogging.debug("Tree placement has no object available on this Minecraft version");
return;
}
saplingPlane.forEach(block -> block.setType(Material.AIR)); saplingPlane.forEach(block -> block.setType(Material.AIR));
IrisObject object = worldAccess.getData().getObjectLoader().load(placement.getPlace().getRandom(RNG.r)); IrisObject object = worldAccess.getData().getObjectLoader().load(pool.getRandom(RNG.r));
if (object == null) {
return;
}
String treeMarker = object.getLoadKey() + "@" + ThreadLocalRandom.current().nextInt(Integer.MAX_VALUE); String treeMarker = object.getLoadKey() + "@" + ThreadLocalRandom.current().nextInt(Integer.MAX_VALUE);
List<BlockState> blockStateList = new KList<>(); List<BlockState> blockStateList = new KList<>();
KMap<Location, BlockData> dataCache = new KMap<>(); KMap<Location, BlockData> dataCache = new KMap<>();
@@ -311,12 +322,12 @@ public class TreeSVC implements IrisService {
// Retrieve objectPlacements of type `species` from biome // Retrieve objectPlacements of type `species` from biome
IrisBiome biome = worldAccess.getEngine().getBiome(location.getBlockX(), location.getBlockY()-worldAccess.getTarget().getWorld().minHeight(), location.getBlockZ()); IrisBiome biome = worldAccess.getEngine().getBiome(location.getBlockX(), location.getBlockY()-worldAccess.getTarget().getWorld().minHeight(), location.getBlockZ());
placements.addAll(matchObjectPlacements(biome.getObjects(), size, type)); placements.addAll(matchObjectPlacements(biome.getObjects(), size, type, worldAccess.getData()));
// Add more or find any in the region // Add more or find any in the region
if (isUseAll || placements.isEmpty()) { if (isUseAll || placements.isEmpty()) {
IrisRegion region = worldAccess.getEngine().getRegion(location.getBlockX(), location.getBlockZ()); IrisRegion region = worldAccess.getEngine().getRegion(location.getBlockX(), location.getBlockZ());
placements.addAll(matchObjectPlacements(region.getObjects(), size, type)); placements.addAll(matchObjectPlacements(region.getObjects(), size, type, worldAccess.getData()));
} }
// Check if no matches were found, return a random one if they are // Check if no matches were found, return a random one if they are
@@ -329,14 +340,15 @@ public class TreeSVC implements IrisService {
* @param objects The object placements to check * @param objects The object placements to check
* @param size The size of the sapling area to filter with * @param size The size of the sapling area to filter with
* @param type The type of the tree to filter with * @param type The type of the tree to filter with
* @param data The pack, for the version-content gate
* @return A list of objectPlacements that matched. May be empty. * @return A list of objectPlacements that matched. May be empty.
*/ */
private KList<IrisObjectPlacement> matchObjectPlacements(KList<IrisObjectPlacement> objects, IrisTreeSize size, TreeType type) { private KList<IrisObjectPlacement> matchObjectPlacements(KList<IrisObjectPlacement> objects, IrisTreeSize size, TreeType type, IrisData data) {
KList<IrisObjectPlacement> p = new KList<>(); KList<IrisObjectPlacement> p = new KList<>();
for (IrisObjectPlacement i : objects) { for (IrisObjectPlacement i : objects) {
if (i.matches(size, type)) { if (i.matches(size, type) && !i.isCompatExcluded(data)) {
p.add(i); p.add(i);
} }
} }
@@ -20,6 +20,7 @@ package art.arcane.iris.engine;
import art.arcane.iris.core.IrisSettings; import art.arcane.iris.core.IrisSettings;
import art.arcane.iris.core.loader.IrisData; import art.arcane.iris.core.loader.IrisData;
import art.arcane.iris.core.loader.IrisRegistrant;
import art.arcane.iris.engine.data.cache.Cache; import art.arcane.iris.engine.data.cache.Cache;
import art.arcane.iris.engine.framework.Engine; import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.hydrology.HydrologyColumnLayer; import art.arcane.iris.engine.hydrology.HydrologyColumnLayer;
@@ -178,8 +179,9 @@ public class IrisComplex implements DataProvider {
fluidHeight = engine.getDimension().getFluidHeight(); fluidHeight = engine.getDimension().getFluidHeight();
generators = new HashMap<>(); generators = new HashMap<>();
generatorBiomes = Collections.newSetFromMap(new IdentityHashMap<>()); generatorBiomes = Collections.newSetFromMap(new IdentityHashMap<>());
focusBiome = engine.getFocus(); // A registrant the version-content gate excluded never enters a pool, focus included.
focusRegion = engine.getFocusRegion(); focusBiome = compatUsable(engine.getFocus());
focusRegion = compatUsable(engine.getFocusRegion());
Map<InferredType, ProceduralStream<IrisBiome>> inferredStreams = new HashMap<>(); Map<InferredType, ProceduralStream<IrisBiome>> inferredStreams = new HashMap<>();
KList<IrisRegion> preparedRegions = new KList<>(); KList<IrisRegion> preparedRegions = new KList<>();
@@ -195,7 +197,7 @@ public class IrisComplex implements DataProvider {
} else { } else {
engine.getDimension().getRegions().forEach(regionKey -> { engine.getDimension().getRegions().forEach(regionKey -> {
IrisRegion region = data.getRegionLoader().load(regionKey); IrisRegion region = data.getRegionLoader().load(regionKey);
if (region == null) { if (region == null || region.isCompatExcluded()) {
return; return;
} }
prepareInferredBiomes(region, preparedRegions); prepareInferredBiomes(region, preparedRegions);
@@ -244,7 +246,7 @@ public class IrisComplex implements DataProvider {
ProceduralStream.of((x, z) -> focusRegion, ProceduralStream.of((x, z) -> focusRegion,
Interpolated.of(a -> 0D, a -> focusRegion)) Interpolated.of(a -> 0D, a -> focusRegion))
: regionStyleStream : regionStyleStream
.selectRarity(data.getRegionLoader().loadAll(engine.getDimension().getRegions())) .selectRarity(compatRegionPool(engine))
.cache2D("regionStream", engine, cacheSize); .cache2D("regionStream", engine, cacheSize);
regionStream = focusRegion != null ? proceduralRegionStream : proceduralRegionStream regionStream = focusRegion != null ? proceduralRegionStream : proceduralRegionStream
.convertAware2D((region, x, z) -> { .convertAware2D((region, x, z) -> {
@@ -1083,11 +1085,30 @@ public class IrisComplex implements DataProvider {
private KList<IrisBiome> loadInferredBiomes(KList<String> keys, InferredType type) { private KList<IrisBiome> loadInferredBiomes(KList<String> keys, InferredType type) {
KList<IrisBiome> inferred = new KList<>(); KList<IrisBiome> inferred = new KList<>();
for (IrisBiome biome : data.getBiomeLoader().loadAll(keys)) { for (IrisBiome biome : data.getBiomeLoader().loadAll(keys)) {
// Excluded biomes cannot generate on this Minecraft version, so they leave the selection pool.
if (biome.isCompatExcluded()) {
continue;
}
inferred.add(biome.withInferredType(type)); inferred.add(biome.withInferredType(type));
} }
return inferred; return inferred;
} }
/** The dimension's region pool with regions the version-content gate excluded removed. */
private KList<IrisRegion> compatRegionPool(Engine engine) {
KList<IrisRegion> pool = new KList<>();
for (IrisRegion region : data.getRegionLoader().loadAll(engine.getDimension().getRegions())) {
if (!region.isCompatExcluded()) {
pool.add(region);
}
}
return pool;
}
private static <T extends IrisRegistrant> T compatUsable(T registrant) {
return registrant == null || registrant.isCompatExcluded() ? null : registrant;
}
private void registerGenerators(IrisBiome biome) { private void registerGenerators(IrisBiome biome) {
generatorBiomes.add(biome); generatorBiomes.add(biome);
biome.getGenerators().forEach(c -> registerGenerator(c.getCachedGenerator(this))); biome.getGenerators().forEach(c -> registerGenerator(c.getCachedGenerator(this)));
@@ -83,6 +83,9 @@ final class MarkerSpawnScanner {
IrisLogging.error("Cannot load spawner: " + i + " for marker on " + manager.getName()); IrisLogging.error("Cannot load spawner: " + i + " for marker on " + manager.getName());
continue; continue;
} }
if (m.isCompatExcluded()) {
continue;
}
m.setReferenceMarker(mark); m.setReferenceMarker(mark);
// This is so fucking incorrect its a joke // This is so fucking incorrect its a joke
@@ -182,6 +185,9 @@ final class MarkerSpawnScanner {
IrisLogging.error("Cannot load spawner: " + i + " for marker on " + manager.getName()); IrisLogging.error("Cannot load spawner: " + i + " for marker on " + manager.getName());
continue; continue;
} }
if (spawner.isCompatExcluded()) {
continue;
}
spawner.setReferenceMarker(mark); spawner.setReferenceMarker(mark);
data.spawners.add(spawner); data.spawners.add(spawner);
} }
@@ -299,7 +299,8 @@ final class WorldEntitySpawner {
} }
//@builder //@builder
Predicate<IrisSpawner> filter = i -> i.canSpawn(manager.getEngine(), c.getX(), c.getZ()); // Excluded spawners cannot spawn anything on this Minecraft version, so they never enter the selection pool.
Predicate<IrisSpawner> filter = i -> !i.isCompatExcluded() && i.canSpawn(manager.getEngine(), c.getX(), c.getZ());
ChunkCounter counter = new ChunkCounter(c.getEntities()); ChunkCounter counter = new ChunkCounter(c.getEntities());
IrisBiome biome = EngineBukkitOps.getSurfaceBiome(manager.getEngine(), c); IrisBiome biome = EngineBukkitOps.getSurfaceBiome(manager.getEngine(), c);
@@ -502,7 +502,7 @@ public interface Engine extends DataProvider, Fallible, BlockUpdater, Renderer,
IrisRegion region = getRegion(x, z); IrisRegion region = getRegion(x, z);
for (IrisObjectPlacement i : region.getObjects()) { for (IrisObjectPlacement i : region.getObjects()) {
if (i.getPlace().contains(object)) { if (i.compatPlace(getData()).contains(object)) {
return new PlacedObject(i, getData().getObjectLoader().load(object), id, x, z); return new PlacedObject(i, getData().getObjectLoader().load(object), id, x, z);
} }
} }
@@ -510,7 +510,7 @@ public interface Engine extends DataProvider, Fallible, BlockUpdater, Renderer,
IrisBiome biome = getSurfaceBiome(x, z); IrisBiome biome = getSurfaceBiome(x, z);
for (IrisObjectPlacement i : biome.getObjects()) { for (IrisObjectPlacement i : biome.getObjects()) {
if (i.getPlace().contains(object)) { if (i.compatPlace(getData()).contains(object)) {
return new PlacedObject(i, getData().getObjectLoader().load(object), id, x, z); return new PlacedObject(i, getData().getObjectLoader().load(object), id, x, z);
} }
} }
@@ -526,19 +526,20 @@ public interface Engine extends DataProvider, Fallible, BlockUpdater, Renderer,
return false; return false;
} }
IrisData data = getData();
Set<String> biomeKeys = getDimension().getReachableBiomes(this).stream() Set<String> biomeKeys = getDimension().getReachableBiomes(this).stream()
.filter((i) -> containsObjectPlacement(i.getObjects(), normalizedObjectKey)) .filter((i) -> containsObjectPlacement(i.getObjects(), normalizedObjectKey, data))
.map(IrisRegistrant::getLoadKey) .map(IrisRegistrant::getLoadKey)
.collect(Collectors.toSet()); .collect(Collectors.toSet());
Set<String> regionKeys = getDimension().getAllRegions(this).stream() Set<String> regionKeys = getDimension().getAllRegions(this).stream()
.filter((i) -> i.getAllBiomeIds().stream().anyMatch(biomeKeys::contains) .filter((i) -> i.getAllBiomeIds().stream().anyMatch(biomeKeys::contains)
|| containsObjectPlacement(i.getObjects(), normalizedObjectKey)) || containsObjectPlacement(i.getObjects(), normalizedObjectKey, data))
.map(IrisRegistrant::getLoadKey) .map(IrisRegistrant::getLoadKey)
.collect(Collectors.toSet()); .collect(Collectors.toSet());
return !regionKeys.isEmpty(); return !regionKeys.isEmpty();
} }
private static boolean containsObjectPlacement(KList<IrisObjectPlacement> placements, String normalizedObjectKey) { private static boolean containsObjectPlacement(KList<IrisObjectPlacement> placements, String normalizedObjectKey, IrisData data) {
if (placements == null || placements.isEmpty() || normalizedObjectKey.isBlank()) { if (placements == null || placements.isEmpty() || normalizedObjectKey.isBlank()) {
return false; return false;
} }
@@ -548,7 +549,7 @@ public interface Engine extends DataProvider, Fallible, BlockUpdater, Renderer,
continue; continue;
} }
for (String placedObject : placement.getPlace()) { for (String placedObject : placement.compatPlace(data)) {
String normalizedPlacedObject = normalizeObjectPlacementKey(placedObject); String normalizedPlacedObject = normalizeObjectPlacementKey(placedObject);
if (!normalizedPlacedObject.isBlank() && normalizedPlacedObject.equals(normalizedObjectKey)) { if (!normalizedPlacedObject.isBlank() && normalizedPlacedObject.equals(normalizedObjectKey)) {
return true; return true;
@@ -20,6 +20,7 @@ package art.arcane.iris.engine.framework;
import art.arcane.iris.core.IrisSettings; import art.arcane.iris.core.IrisSettings;
import art.arcane.iris.engine.IrisComplex; import art.arcane.iris.engine.IrisComplex;
import art.arcane.iris.core.loader.IrisData;
import art.arcane.iris.engine.object.IrisBiome; import art.arcane.iris.engine.object.IrisBiome;
import art.arcane.iris.engine.object.IrisObjectPlacement; import art.arcane.iris.engine.object.IrisObjectPlacement;
import art.arcane.iris.engine.object.IrisRegion; import art.arcane.iris.engine.object.IrisRegion;
@@ -410,7 +411,7 @@ public final class HintedLocator<T> implements Locator<T> {
continue; continue;
} }
if (placesObject(region.getSurfaceObjects(), objectKey)) { if (placesObject(region.getSurfaceObjects(), objectKey, engine.getData())) {
directRegions.add(region.getLoadKey()); directRegions.add(region.getLoadKey());
} }
@@ -511,9 +512,9 @@ public final class HintedLocator<T> implements Locator<T> {
}; };
} }
private static boolean placesObject(KList<IrisObjectPlacement> placements, String objectKey) { private static boolean placesObject(KList<IrisObjectPlacement> placements, String objectKey, IrisData data) {
for (IrisObjectPlacement placement : placements) { for (IrisObjectPlacement placement : placements) {
if (placement != null && placement.getPlace().contains(objectKey)) { if (placement != null && placement.compatPlace(data).contains(objectKey)) {
return true; return true;
} }
} }
@@ -637,7 +638,7 @@ public final class HintedLocator<T> implements Locator<T> {
continue; continue;
} }
if (placesObject(biome.getSurfaceObjects(), objectKey)) { if (placesObject(biome.getSurfaceObjects(), objectKey, engine.getData())) {
return true; return true;
} }
@@ -507,6 +507,10 @@ public final class IrisStructureLocator {
throw new IllegalStateException("Iris structure placement references missing structure '" throw new IllegalStateException("Iris structure placement references missing structure '"
+ selectedKey + "'"); + selectedKey + "'");
} }
if (structure.isCompatExcluded()) {
// Gated out on this Minecraft version; the pack report already says why.
return null;
}
StructureGraphCompilation compilation = StructureGraphCatalog.compile(engine.getData(), structure); StructureGraphCompilation compilation = StructureGraphCatalog.compile(engine.getData(), structure);
if (!compilation.isAssemblyViable()) { if (!compilation.isAssemblyViable()) {
throw new IllegalStateException("Iris structure '" + selectedKey throw new IllegalStateException("Iris structure '" + selectedKey
@@ -1179,6 +1179,11 @@ public final class StructureAssembler {
if (pool.getPieces() == null) { if (pool.getPieces() == null) {
throw assemblyFailure("pool has no piece list"); throw assemblyFailure("pool has no piece list");
} }
// A pool the version-content gate excluded has nothing left to place: treat it as empty so the branch
// terminates or falls back, instead of failing the whole assembly.
if (pool.isCompatExcluded()) {
return List.of();
}
List<IrisJigsawPieceEntry> eligible = new ArrayList<>(); List<IrisJigsawPieceEntry> eligible = new ArrayList<>();
List<IrisJigsawPieceEntry> required = new ArrayList<>(); List<IrisJigsawPieceEntry> required = new ArrayList<>();
for (IrisJigsawPieceEntry entry : pool.getPieces()) { for (IrisJigsawPieceEntry entry : pool.getPieces()) {
@@ -1199,6 +1204,9 @@ public final class StructureAssembler {
if (piece == null) { if (piece == null) {
throw assemblyFailure("pool references missing piece '" + pieceKey + "'"); throw assemblyFailure("pool references missing piece '" + pieceKey + "'");
} }
if (piece.isCompatExcluded()) {
continue;
}
int placements = placementCounts.getOrDefault(pieceKey, 0); int placements = placementCounts.getOrDefault(pieceKey, 0);
if (!pieceEnabled(piece) if (!pieceEnabled(piece)
|| terminalOnly && !piece.resolvedRules().isTerminal() || terminalOnly && !piece.resolvedRules().isTerminal()
@@ -1,5 +1,7 @@
package art.arcane.iris.engine.hydrology.policy; package art.arcane.iris.engine.hydrology.policy;
import art.arcane.iris.core.loader.IrisData;
import art.arcane.iris.core.loader.IrisRegistrant;
import art.arcane.iris.engine.object.IrisBiome; import art.arcane.iris.engine.object.IrisBiome;
import art.arcane.iris.engine.object.IrisDimension; import art.arcane.iris.engine.object.IrisDimension;
import art.arcane.iris.engine.object.IrisRegion; import art.arcane.iris.engine.object.IrisRegion;
@@ -17,7 +19,8 @@ public final class RiverPolicyResolver {
return resolve( return resolve(
dimension == null ? null : dimension.getRiverPolicy(), dimension == null ? null : dimension.getRiverPolicy(),
region == null ? null : region.getRiverPolicy(), region == null ? null : region.getRiverPolicy(),
biome == null ? null : biome.getRiverPolicy() biome == null ? null : biome.getRiverPolicy(),
loaderOf(biome, region, dimension)
); );
} }
@@ -25,14 +28,37 @@ public final class RiverPolicyResolver {
IrisRiverPolicy dimensionPolicy, IrisRiverPolicy dimensionPolicy,
IrisRiverPolicy regionPolicy, IrisRiverPolicy regionPolicy,
IrisRiverPolicy biomePolicy IrisRiverPolicy biomePolicy
) {
return resolve(dimensionPolicy, regionPolicy, biomePolicy, null);
}
/**
* @param data pack the policies came from, used to drop river biome references the version-content gate excluded.
* Null skips that filtering.
*/
public static EffectiveRiverPolicy resolve(
IrisRiverPolicy dimensionPolicy,
IrisRiverPolicy regionPolicy,
IrisRiverPolicy biomePolicy,
IrisData data
) { ) {
State state = new State(); State state = new State();
state.apply(dimensionPolicy); state.apply(dimensionPolicy, data);
state.apply(regionPolicy); state.apply(regionPolicy, data);
state.apply(biomePolicy); state.apply(biomePolicy, data);
return state.build(); return state.build();
} }
private static IrisData loaderOf(IrisRegistrant... registrants) {
for (IrisRegistrant registrant : registrants) {
if (registrant != null && registrant.getLoader() != null) {
return registrant.getLoader();
}
}
return null;
}
private static final class State { private static final class State {
private IrisRiverPlacementMode placement = IrisRiverPlacementMode.NATURAL; private IrisRiverPlacementMode placement = IrisRiverPlacementMode.NATURAL;
private IrisRiverRoutingMode routing = IrisRiverRoutingMode.ALLOW; private IrisRiverRoutingMode routing = IrisRiverRoutingMode.ALLOW;
@@ -50,7 +76,7 @@ public final class RiverPolicyResolver {
private double routingMultiplier = 1D; private double routingMultiplier = 1D;
private double bankMultiplier = 1D; private double bankMultiplier = 1D;
private void apply(IrisRiverPolicy policy) { private void apply(IrisRiverPolicy policy, IrisData data) {
if (policy == null) { if (policy == null) {
return; return;
} }
@@ -67,19 +93,19 @@ public final class RiverPolicyResolver {
profiles = List.copyOf(policy.getProfiles()); profiles = List.copyOf(policy.getProfiles());
} }
if (policy.getSurfaceBiomes() != null) { if (policy.getSurfaceBiomes() != null) {
surfaceBiomes = List.copyOf(policy.getSurfaceBiomes()); surfaceBiomes = List.copyOf(policy.compatBiomes(policy.getSurfaceBiomes(), data, "surfaceBiomes"));
} }
if (policy.getMouthBiomes() != null) { if (policy.getMouthBiomes() != null) {
mouthBiomes = List.copyOf(policy.getMouthBiomes()); mouthBiomes = List.copyOf(policy.compatBiomes(policy.getMouthBiomes(), data, "mouthBiomes"));
} }
if (policy.getShoreBiomes() != null) { if (policy.getShoreBiomes() != null) {
shoreBiomes = List.copyOf(policy.getShoreBiomes()); shoreBiomes = List.copyOf(policy.compatBiomes(policy.getShoreBiomes(), data, "shoreBiomes"));
} }
if (policy.getBankBiomes() != null) { if (policy.getBankBiomes() != null) {
bankBiomes = List.copyOf(policy.getBankBiomes()); bankBiomes = List.copyOf(policy.compatBiomes(policy.getBankBiomes(), data, "bankBiomes"));
} }
if (policy.getFloodedCaveBiomes() != null) { if (policy.getFloodedCaveBiomes() != null) {
floodedCaveBiomes = List.copyOf(policy.getFloodedCaveBiomes()); floodedCaveBiomes = List.copyOf(policy.compatBiomes(policy.getFloodedCaveBiomes(), data, "floodedCaveBiomes"));
} }
if (policy.getSurfacePools() != null) { if (policy.getSurfacePools() != null) {
surfacePools = List.copyOf(policy.getSurfacePools()); surfacePools = List.copyOf(policy.getSurfacePools());
@@ -171,6 +171,10 @@ public class MantleFloatingObjectComponent extends IrisMantleComponent {
if (placement == null || columns == null || columns.isEmpty()) { if (placement == null || columns == null || columns.isEmpty()) {
return; return;
} }
if (placement.isCompatExcluded(data)) {
return;
}
int density = placement.getDensity(rng, minX, minZ, data); int density = placement.getDensity(rng, minX, minZ, data);
double perAttempt = placement.getChance(); double perAttempt = placement.getChance();
for (int i = 0; i < density; i++) { for (int i = 0; i < density; i++) {
@@ -217,6 +221,10 @@ public class MantleFloatingObjectComponent extends IrisMantleComponent {
if (placement == null || columns.isEmpty()) { if (placement == null || columns.isEmpty()) {
return; return;
} }
if (placement.isCompatExcluded(data)) {
return;
}
int density = placement.getDensity(rng, minX, minZ, data); int density = placement.getDensity(rng, minX, minZ, data);
double perAttempt = placement.getChance(); double perAttempt = placement.getChance();
@@ -302,6 +310,10 @@ public class MantleFloatingObjectComponent extends IrisMantleComponent {
if (placement == null || columns.isEmpty()) { if (placement == null || columns.isEmpty()) {
return; return;
} }
if (placement.isCompatExcluded(data)) {
return;
}
int density = placement.getDensity(rng, minX, minZ, data); int density = placement.getDensity(rng, minX, minZ, data);
double perAttempt = placement.getChance(); double perAttempt = placement.getChance();
@@ -417,7 +417,7 @@ public class MantleObjectComponent extends IrisMantleComponent {
for (IrisObjectPlacement i : surfaceBiome.getSurfaceObjects()) { for (IrisObjectPlacement i : surfaceBiome.getSurfaceObjects()) {
biomeSurfaceChecked++; biomeSurfaceChecked++;
boolean chance = rng.chance(i.getChance() + rng.d(-0.005, 0.005)); boolean chance = rng.chance(i.getChance() + rng.d(-0.005, 0.005)) && !i.isCompatExcluded(getData());
if (traceRegen) { if (traceRegen) {
IrisLogging.debug("Regen object placer chance: chunk=" + x + "," + z IrisLogging.debug("Regen object placer chance: chunk=" + x + "," + z
+ " scope=biome-surface" + " scope=biome-surface"
@@ -450,7 +450,7 @@ public class MantleObjectComponent extends IrisMantleComponent {
continue; continue;
} }
biomeCaveChecked++; biomeCaveChecked++;
boolean chance = rng.chance(i.getChance()); boolean chance = rng.chance(i.getChance()) && !i.isCompatExcluded(getData());
if (traceRegen) { if (traceRegen) {
IrisLogging.debug("Regen object placer chance: chunk=" + x + "," + z IrisLogging.debug("Regen object placer chance: chunk=" + x + "," + z
+ " scope=biome-cave" + " scope=biome-cave"
@@ -480,7 +480,7 @@ public class MantleObjectComponent extends IrisMantleComponent {
for (IrisObjectPlacement i : region.getSurfaceObjects()) { for (IrisObjectPlacement i : region.getSurfaceObjects()) {
regionSurfaceChecked++; regionSurfaceChecked++;
boolean chance = rng.chance(i.getChance() + rng.d(-0.005, 0.005)); boolean chance = rng.chance(i.getChance() + rng.d(-0.005, 0.005)) && !i.isCompatExcluded(getData());
if (traceRegen) { if (traceRegen) {
IrisLogging.debug("Regen object placer chance: chunk=" + x + "," + z IrisLogging.debug("Regen object placer chance: chunk=" + x + "," + z
+ " scope=region-surface" + " scope=region-surface"
@@ -513,7 +513,7 @@ public class MantleObjectComponent extends IrisMantleComponent {
continue; continue;
} }
regionCaveChecked++; regionCaveChecked++;
boolean chance = rng.chance(i.getChance()); boolean chance = rng.chance(i.getChance()) && !i.isCompatExcluded(getData());
if (traceRegen) { if (traceRegen) {
IrisLogging.debug("Regen object placer chance: chunk=" + x + "," + z IrisLogging.debug("Regen object placer chance: chunk=" + x + "," + z
+ " scope=region-cave" + " scope=region-cave"
@@ -1182,7 +1182,7 @@ public class MantleObjectComponent extends IrisMantleComponent {
boolean forcePlace = dimension.isUpperObjectsForcePlace(); boolean forcePlace = dimension.isUpperObjectsForcePlace();
if (upperBiome != null) { if (upperBiome != null) {
for (IrisObjectPlacement i : upperBiome.getSurfaceObjects()) { for (IrisObjectPlacement i : upperBiome.getSurfaceObjects()) {
if (!rng.chance(i.getChance() + rng.d(-0.005, 0.005))) { if (!rng.chance(i.getChance() + rng.d(-0.005, 0.005)) || i.isCompatExcluded(getData())) {
continue; continue;
} }
try { try {
@@ -1197,7 +1197,7 @@ public class MantleObjectComponent extends IrisMantleComponent {
if (upperRegion != null) { if (upperRegion != null) {
for (IrisObjectPlacement i : upperRegion.getSurfaceObjects()) { for (IrisObjectPlacement i : upperRegion.getSurfaceObjects()) {
if (!rng.chance(i.getChance() + rng.d(-0.005, 0.005))) { if (!rng.chance(i.getChance() + rng.d(-0.005, 0.005)) || i.isCompatExcluded(getData())) {
continue; continue;
} }
try { try {
@@ -1635,13 +1635,13 @@ public class MantleObjectComponent extends IrisMantleComponent {
IrisRegion region = getEngineMantle().getEngine().getRegion((x << 4) + 8, (z << 4) + 8); IrisRegion region = getEngineMantle().getEngine().getRegion((x << 4) + 8, (z << 4) + 8);
Set<String> v = new KSet<>(); Set<String> v = new KSet<>();
for (IrisObjectPlacement i : biome.getSurfaceObjects()) { for (IrisObjectPlacement i : biome.getSurfaceObjects()) {
if (rng.chance(i.getChance() + rng.d(-0.005, 0.005))) { if (rng.chance(i.getChance() + rng.d(-0.005, 0.005)) && !i.isCompatExcluded(getData())) {
v.addAll(guessPlacedKeys(rng, x, z, i)); v.addAll(guessPlacedKeys(rng, x, z, i));
} }
} }
for (IrisObjectPlacement i : region.getSurfaceObjects()) { for (IrisObjectPlacement i : region.getSurfaceObjects()) {
if (rng.chance(i.getChance() + rng.d(-0.005, 0.005))) { if (rng.chance(i.getChance() + rng.d(-0.005, 0.005)) && !i.isCompatExcluded(getData())) {
v.addAll(guessPlacedKeys(rng, x, z, i)); v.addAll(guessPlacedKeys(rng, x, z, i));
} }
} }
@@ -0,0 +1,219 @@
/*
* Iris is a World Generator for Minecraft Bukkit Servers
* Copyright (c) 2026 Arcane Arts (Volmit Software)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package art.arcane.iris.engine.object;
import art.arcane.iris.core.compat.CompatAction;
import art.arcane.iris.core.compat.CompatFinding;
import art.arcane.iris.core.compat.CompatStatus;
import art.arcane.iris.core.loader.IrisData;
import art.arcane.iris.core.loader.IrisRegistrant;
import art.arcane.iris.core.loader.ResourceLoader;
import art.arcane.volmlib.util.collection.KList;
import java.util.ArrayList;
import java.util.List;
/**
* Registrant reference pools filtered by the version-content gate. A reference to a registrant the gate EXCLUDED is
* dropped from every pool that could pick it and reported once against the containing registrant; a container whose
* required pool ends up empty cascades to EXCLUDED through {@link #cascade}. Cascade and drop findings reuse the
* registry and key of the cause so the console groups everything under the one missing content key.
* <p>
* Surviving entries keep their declaration order, so only the removed entries change what a pool picks.
*/
final class CompatPools {
private CompatPools() {
}
static boolean excluded(IrisRegistrant registrant) {
return registrant != null && registrant.isCompatExcluded();
}
/**
* Loads a registrant reference pool, skipping entries that fail to load and entries the gate excluded. Each
* excluded reference produces one DROPPED finding in {@code sink} (when given) and in the pack report.
*
* @param field the JSON field the keys came from, used for the finding detail ({@code landBiomes[2] key})
*/
static <T extends IrisRegistrant> KList<T> load(ResourceLoader<T> loader,
List<String> keys,
IrisData data,
String subjectType,
String subjectKey,
String field,
List<CompatFinding> sink) {
KList<T> kept = new KList<>();
if (loader == null || keys == null) {
return kept;
}
for (int index = 0; index < keys.size(); index++) {
String key = keys.get(index);
T loaded = loader.load(key);
if (loaded == null) {
continue;
}
if (loaded.isCompatExcluded()) {
drop(data, loaded, subjectType, subjectKey, field + "[" + index + "] " + key, sink);
continue;
}
kept.add(loaded);
}
return kept;
}
/** The surviving keys of a reference pool, in declaration order. */
static <T extends IrisRegistrant> KList<String> surviving(ResourceLoader<T> loader,
List<String> keys,
IrisData data,
String subjectType,
String subjectKey,
String field) {
KList<String> kept = new KList<>();
if (keys == null) {
return kept;
}
if (loader == null) {
kept.addAll(keys);
return kept;
}
for (int index = 0; index < keys.size(); index++) {
String key = keys.get(index);
T loaded = loader.load(key);
if (loaded != null && loaded.isCompatExcluded()) {
drop(data, loaded, subjectType, subjectKey, field + "[" + index + "] " + key, null);
continue;
}
kept.add(key);
}
return kept;
}
/**
* Records one DROPPED finding for a reference to an excluded registrant, reusing the registry and key of the
* child's exclusion cause. Returns null when the child carries no cause to attribute the drop to.
*/
static CompatFinding drop(IrisData data,
IrisRegistrant child,
String subjectType,
String subjectKey,
String detail,
List<CompatFinding> sink) {
CompatFinding cause = causeOf(child == null ? null : child.getCompat());
if (cause == null) {
return null;
}
CompatFinding finding = new CompatFinding(cause.registry(), cause.key(), CompatAction.DROPPED,
subjectType, subjectKey, detail);
record(data, finding);
if (sink != null) {
sink.add(finding);
}
return finding;
}
/**
* The EXCLUDED status for a container whose required pool lost every entry. Returns {@code base} untouched when no
* cause was collected, so a container that never declared the pool is not excluded for being empty.
*/
static CompatStatus cascade(IrisData data,
CompatStatus base,
List<CompatFinding> causes,
String subjectType,
String subjectKey,
String detail) {
if (causes == null || causes.isEmpty()) {
return base;
}
CompatFinding cause = causes.getFirst();
CompatFinding finding = new CompatFinding(cause.registry(), cause.key(), CompatAction.EXCLUDED,
subjectType, subjectKey, detail);
record(data, finding);
List<CompatFinding> reasons = new ArrayList<>(base == null ? List.of() : base.reasons());
reasons.add(finding);
return CompatStatus.excludedBy(reasons);
}
/** The findings on a status that dropped an entry, used as the cause set of a cascade. */
static List<CompatFinding> droppedReasons(CompatStatus status) {
List<CompatFinding> dropped = new ArrayList<>();
if (status == null) {
return dropped;
}
for (CompatFinding reason : status.reasons()) {
if (reason.action() == CompatAction.DROPPED) {
dropped.add(reason);
}
}
return dropped;
}
static void record(IrisData data, CompatFinding finding) {
if (data == null || finding == null) {
return;
}
try {
data.getCompatReport().record(finding);
} catch (Throwable ignored) {
// A report is best effort: never let reporting break a pool build.
}
}
/** The finding that explains an exclusion: the EXCLUDED reason if there is one, otherwise the first reason. */
private static CompatFinding causeOf(CompatStatus status) {
if (status == null) {
return null;
}
CompatFinding fallback = null;
for (CompatFinding reason : status.reasons()) {
if (reason.action() == CompatAction.EXCLUDED) {
return reason;
}
if (fallback == null) {
fallback = reason;
}
}
return fallback;
}
}
@@ -559,13 +559,9 @@ public class IrisBiome extends IrisRegistrant implements IRare {
public KList<IrisBiome> getRealChildren(DataProvider g) { public KList<IrisBiome> getRealChildren(DataProvider g) {
return realChildren.aquire(() -> return realChildren.aquire(() ->
{ {
KList<IrisBiome> realChildren = new KList<>(); IrisData data = g == null ? null : g.getData();
return CompatPools.load(data == null ? null : data.getBiomeLoader(), getChildren(), data,
for (String i : getChildren()) { "biome", getLoadKey(), "children", null);
realChildren.add(g.getData().getBiomeLoader().load(i));
}
return realChildren;
}); });
} }
@@ -18,6 +18,8 @@
package art.arcane.iris.engine.object; package art.arcane.iris.engine.object;
import art.arcane.iris.core.compat.ContentGate;
import art.arcane.iris.core.compat.KeyStatus;
import art.arcane.iris.core.nms.datapack.IDataFixer; import art.arcane.iris.core.nms.datapack.IDataFixer;
import art.arcane.iris.engine.object.annotations.ArrayType; import art.arcane.iris.engine.object.annotations.ArrayType;
import art.arcane.iris.engine.object.annotations.DependsOn; import art.arcane.iris.engine.object.annotations.DependsOn;
@@ -132,6 +134,16 @@ public class IrisBiomeCustom {
} }
public String generateJson(IDataFixer fixer) { public String generateJson(IDataFixer fixer) {
return generateJson(fixer, null);
}
/**
* @param gate version-content gate used to keep spawns whose entity type is missing on this server out of the
* generated datapack (the game rejects a datapack that names an unknown entity). Null consults the
* live platform registries; an unreadable registry keeps every spawn.
*/
public String generateJson(IDataFixer fixer, ContentGate gate) {
ContentGate contentGate = gate == null ? ContentGate.forData(null) : gate;
JSONObject effects = new JSONObject(); JSONObject effects = new JSONObject();
effects.put("sky_color", parseColor(getSkyColor())); effects.put("sky_color", parseColor(getSkyColor()));
effects.put("fog_color", parseColor(getFogColor())); effects.put("fog_color", parseColor(getFogColor()));
@@ -196,6 +208,10 @@ public class IrisBiomeCustom {
IrisLogging.warn("Skipping custom biome spawn with null entity type in biome " + getId()); IrisLogging.warn("Skipping custom biome spawn with null entity type in biome " + getId());
continue; continue;
} }
if (contentGate.entity(key) == KeyStatus.MISSING) {
IrisLogging.debug("Dropping custom biome spawn " + key + " in biome " + getId() + ": entity missing on this server");
continue;
}
IrisBiomeCustomSpawnType group = i.getGroup() == null ? IrisBiomeCustomSpawnType.MISC : i.getGroup(); IrisBiomeCustomSpawnType group = i.getGroup() == null ? IrisBiomeCustomSpawnType.MISC : i.getGroup();
JSONArray g = groups.computeIfAbsent(group, (k) -> new JSONArray()); JSONArray g = groups.computeIfAbsent(group, (k) -> new JSONArray());
JSONObject o = new JSONObject(); JSONObject o = new JSONObject();
@@ -63,7 +63,14 @@ public class IrisDimensionCarvingEntry {
return null; return null;
} }
return data.getBiomeLoader().load(biomeKey.trim()); IrisBiome loaded = data.getBiomeLoader().load(biomeKey.trim());
if (loaded != null && loaded.isCompatExcluded()) {
CompatPools.drop(data, loaded, "carving entry", getId(), "biome " + biomeKey.trim(), null);
return null;
}
return loaded;
}); });
} }
@@ -18,6 +18,7 @@
package art.arcane.iris.engine.object; package art.arcane.iris.engine.object;
import art.arcane.iris.core.loader.IrisData;
import art.arcane.iris.platform.bukkit.BukkitWorldBinding; import art.arcane.iris.platform.bukkit.BukkitWorldBinding;
import art.arcane.iris.platform.bukkit.BukkitPlatform; import art.arcane.iris.platform.bukkit.BukkitPlatform;
import art.arcane.iris.engine.data.cache.AtomicCache; import art.arcane.iris.engine.data.cache.AtomicCache;
@@ -147,7 +148,17 @@ public class IrisEntitySpawn implements IRare {
} }
public IrisEntity getRealEntity(Engine g) { public IrisEntity getRealEntity(Engine g) {
return ent.aquire(() -> g.getData().getEntityLoader().load(getEntity())); return ent.aquire(() -> resolveEntity(g.getData()));
}
/** The referenced entity, or null when it does not load or the version-content gate excluded it. */
IrisEntity resolveEntity(IrisData data) {
if (data == null || data.getEntityLoader() == null) {
return null;
}
IrisEntity entity = data.getEntityLoader().load(getEntity());
return entity == null || entity.isCompatExcluded() ? null : entity;
} }
public Entity spawn(Engine g, Location at) { public Entity spawn(Engine g, Location at) {
@@ -358,6 +358,11 @@ public class IrisFloatingChildBiomes implements IRare {
return parent; return parent;
} }
if (loaded.isCompatExcluded()) {
CompatPools.drop(data, loaded, "floating child biome", parent.getLoadKey(), "biome " + biome, null);
return parent;
}
return loaded; return loaded;
}); });
} }
@@ -18,6 +18,12 @@
package art.arcane.iris.engine.object; package art.arcane.iris.engine.object;
import art.arcane.iris.core.compat.CompatAction;
import art.arcane.iris.core.compat.CompatFinding;
import art.arcane.iris.core.compat.CompatRegistry;
import art.arcane.iris.core.compat.CompatStatus;
import art.arcane.iris.core.compat.ContentGate;
import art.arcane.iris.core.loader.IrisData;
import art.arcane.iris.core.loader.IrisRegistrant; import art.arcane.iris.core.loader.IrisRegistrant;
import art.arcane.iris.engine.object.annotations.ArrayType; import art.arcane.iris.engine.object.annotations.ArrayType;
import art.arcane.iris.engine.object.annotations.Desc; import art.arcane.iris.engine.object.annotations.Desc;
@@ -81,6 +87,42 @@ public class IrisJigsawPiece extends IrisRegistrant {
return rules == null ? new IrisJigsawPieceRules() : rules; return rules == null ? new IrisJigsawPieceRules() : rules;
} }
/**
* A piece is one object stamped verbatim - nothing rewrites its blocks - so a palette key the server does not
* have (after the pack's declared fallbacks) takes the whole piece out of every pool that lists it.
*/
@Override
public CompatStatus evaluateCompat(ContentGate gate) {
CompatStatus base = super.evaluateCompat(gate);
if (base.excluded() || gate == null || !gate.ready()) {
return base;
}
IrisData data = getLoader();
if (data == null || object == null || object.isBlank()) {
return base;
}
for (String paletteKey : IrisObjectIO.readPaletteKeysCached(data.getObjectLoader().findFile(object.trim()))) {
String key = ContentGate.baseKey(ContentGate.normalizeState(paletteKey));
if (key == null || gate.resolveBlock(key) != null) {
continue;
}
CompatFinding finding = new CompatFinding(CompatRegistry.BLOCK, key, CompatAction.EXCLUDED,
"jigsaw piece", getLoadKey(), "object " + object.trim());
gate.report().record(finding);
KList<CompatFinding> reasons = new KList<>(base.reasons());
reasons.add(finding);
return CompatStatus.excludedBy(reasons);
}
return base;
}
@Override @Override
public String getFolderName() { public String getFolderName() {
return "jigsaw-pieces"; return "jigsaw-pieces";
@@ -18,6 +18,12 @@
package art.arcane.iris.engine.object; package art.arcane.iris.engine.object;
import art.arcane.iris.core.compat.CompatAction;
import art.arcane.iris.core.compat.CompatFinding;
import art.arcane.iris.core.compat.CompatRegistry;
import art.arcane.iris.core.compat.CompatStatus;
import art.arcane.iris.core.compat.ContentGate;
import art.arcane.iris.core.loader.IrisData;
import art.arcane.iris.core.loader.IrisRegistrant; import art.arcane.iris.core.loader.IrisRegistrant;
import art.arcane.iris.engine.object.annotations.ArrayType; import art.arcane.iris.engine.object.annotations.ArrayType;
import art.arcane.iris.engine.object.annotations.Desc; import art.arcane.iris.engine.object.annotations.Desc;
@@ -51,6 +57,75 @@ public class IrisJigsawPool extends IrisRegistrant {
return structureRequiresCaps || mandatoryFallback; return structureRequiresCaps || mandatoryFallback;
} }
/**
* Excluded pieces drop out of the pool; a pool with nothing left to place is excluded in turn so every connector
* that targets it terminates instead of failing assembly.
*/
@Override
public CompatStatus evaluateCompat(ContentGate gate) {
CompatStatus base = super.evaluateCompat(gate);
if (base.excluded() || gate == null || !gate.ready()) {
return base;
}
IrisData data = getLoader();
if (data == null || pieces == null || pieces.isEmpty()) {
return base;
}
KList<CompatFinding> reasons = new KList<>(base.reasons());
CompatFinding cause = null;
int usable = 0;
for (int i = 0; i < pieces.size(); i++) {
IrisJigsawPieceEntry entry = pieces.get(i);
if (entry == null) {
continue;
}
String key = entry.getPiece() == null ? "" : entry.getPiece().trim();
// An empty membership terminates a branch on purpose, and a piece key that does not resolve at all is a
// pack error the loader already reports - neither is a version-content problem.
if (entry.isEmpty() || key.isEmpty()) {
usable++;
continue;
}
IrisJigsawPiece piece = data.load(IrisJigsawPiece.class, key, false);
if (piece == null || !piece.isCompatExcluded()) {
usable++;
continue;
}
CompatFinding reason = piece.getCompat().reasons().isEmpty() ? null : piece.getCompat().reasons().getFirst();
CompatFinding dropped = new CompatFinding(
reason == null ? CompatRegistry.BLOCK : reason.registry(),
reason == null ? key : reason.key(),
CompatAction.DROPPED, "jigsaw pool", getLoadKey(), "pieces[" + i + "] " + key);
gate.report().record(dropped);
reasons.add(dropped);
if (cause == null) {
cause = dropped;
}
}
if (usable > 0 || cause == null) {
return new CompatStatus(false, reasons);
}
CompatFinding excluded = new CompatFinding(cause.registry(), cause.key(), CompatAction.EXCLUDED,
"jigsaw pool", getLoadKey(), "no pieces remain");
gate.report().record(excluded);
reasons.add(excluded);
return CompatStatus.excludedBy(reasons);
}
@Override @Override
public String getFolderName() { public String getFolderName() {
return "jigsaw-pools"; return "jigsaw-pools";
@@ -18,6 +18,7 @@
package art.arcane.iris.engine.object; package art.arcane.iris.engine.object;
import art.arcane.iris.core.loader.IrisData;
import art.arcane.iris.engine.data.cache.AtomicCache; import art.arcane.iris.engine.data.cache.AtomicCache;
import art.arcane.iris.engine.object.annotations.ArrayType; import art.arcane.iris.engine.object.annotations.ArrayType;
import art.arcane.iris.engine.object.annotations.Desc; import art.arcane.iris.engine.object.annotations.Desc;
@@ -57,9 +58,25 @@ public class IrisLootReference {
return tt.aquire(() -> return tt.aquire(() ->
{ {
KList<IrisLootTable> t = new KList<>(); KList<IrisLootTable> t = new KList<>();
IrisData data = g == null ? null : g.getData();
if (data == null || data.getLootLoader() == null) {
return t;
}
for (String i : tables) { for (String i : tables) {
t.add(g.getData().getLootLoader().load(i)); IrisLootTable table = data.getLootLoader().load(i);
if (table == null) {
continue;
}
if (table.isCompatExcluded()) {
CompatPools.drop(data, table, "loot reference", table.getLoadKey(), "tables " + i, null);
continue;
}
t.add(table);
} }
return t; return t;
@@ -18,6 +18,8 @@
package art.arcane.iris.engine.object; package art.arcane.iris.engine.object;
import art.arcane.iris.core.compat.CompatStatus;
import art.arcane.iris.core.compat.ContentGate;
import art.arcane.iris.core.loader.IrisRegistrant; import art.arcane.iris.core.loader.IrisRegistrant;
import art.arcane.iris.engine.framework.LootResolver; import art.arcane.iris.engine.framework.LootResolver;
import art.arcane.iris.engine.object.annotations.ArrayType; import art.arcane.iris.engine.object.annotations.ArrayType;
@@ -103,6 +105,25 @@ public class IrisLootTable extends IrisRegistrant {
return lootf; return lootf;
} }
/**
* Cascade: the gate walker drops loot entries whose item is missing on this server. A table with nothing left to
* roll leaves every reference pool that could pick it.
*/
@Override
public CompatStatus evaluateCompat(ContentGate gate) {
return cascadeEmptyLoot(super.evaluateCompat(gate));
}
/** EXCLUDED when the walker dropped every loot entry; the walker's verdict is passed in as {@code base}. */
CompatStatus cascadeEmptyLoot(CompatStatus base) {
if (base.excluded() || !loot.isEmpty()) {
return base;
}
return CompatPools.cascade(getLoader(), base, CompatPools.droppedReasons(base),
"loot", getLoadKey(), "no loot entries remain");
}
@Override @Override
public String getFolderName() { public String getFolderName() {
return "loot"; return "loot";
@@ -18,6 +18,9 @@
package art.arcane.iris.engine.object; package art.arcane.iris.engine.object;
import art.arcane.iris.core.compat.CompatStatus;
import art.arcane.iris.core.compat.ContentGate;
import art.arcane.iris.core.loader.IrisData;
import art.arcane.iris.core.loader.IrisRegistrant; import art.arcane.iris.core.loader.IrisRegistrant;
import art.arcane.iris.engine.object.annotations.ArrayType; import art.arcane.iris.engine.object.annotations.ArrayType;
import art.arcane.iris.engine.object.annotations.Desc; import art.arcane.iris.engine.object.annotations.Desc;
@@ -104,6 +107,49 @@ public class IrisMod extends IrisRegistrant {
@Desc("Replace noise styles with other styles") @Desc("Replace noise styles with other styles")
private KList<IrisModNoiseStyleReplacer> styleReplacers = new KList<>(); private KList<IrisModNoiseStyleReplacer> styleReplacers = new KList<>();
/**
* A pack modification only references other registrants, so it never cascades: excluded biome and region
* references are dropped from its lists and reported, and the modification keeps applying whatever is left.
*/
@Override
public CompatStatus evaluateCompat(ContentGate gate) {
CompatStatus base = super.evaluateCompat(gate);
IrisData data = getLoader();
if (data == null) {
return base;
}
removeBiomes = biomes(data, removeBiomes, "removeBiomes");
removeRegions = regions(data, removeRegions, "removeRegions");
injectRegions = regions(data, injectRegions, "injectRegions");
for (int index = 0; index < biomeInjectors.size(); index++) {
IrisModBiomeInjector injector = biomeInjectors.get(index);
injector.setInject(biomes(data, injector.getInject(), "biomeInjectors[" + index + "].inject"));
}
for (int index = 0; index < biomeReplacers.size(); index++) {
IrisModBiomeReplacer replacer = biomeReplacers.get(index);
replacer.setFind(biomes(data, replacer.getFind(), "biomeReplacers[" + index + "].find"));
}
for (int index = 0; index < regionReplacers.size(); index++) {
IrisModRegionReplacer replacer = regionReplacers.get(index);
replacer.setFind(regions(data, replacer.getFind(), "regionReplacers[" + index + "].find"));
}
return base;
}
private KList<String> biomes(IrisData data, KList<String> keys, String field) {
return CompatPools.surviving(data.getBiomeLoader(), keys, data, "mod", getLoadKey(), field);
}
private KList<String> regions(IrisData data, KList<String> keys, String field) {
return CompatPools.surviving(data.getRegionLoader(), keys, data, "mod", getLoadKey(), field);
}
@Override @Override
public String getFolderName() { public String getFolderName() {
return "mods"; return "mods";
@@ -63,7 +63,7 @@ public class IrisObjectMarker {
KList<PlatformBlockState> b = new KList<>(); KList<PlatformBlockState> b = new KList<>();
for (IrisBlockData i : mark) { for (IrisBlockData i : mark) {
PlatformBlockState bx = i.getBlockData(rdata); PlatformBlockState bx = i.getBlockDataOrPlaceholder(rdata);
if (bx != null) { if (bx != null) {
b.add(bx); b.add(bx);
@@ -20,6 +20,12 @@ package art.arcane.iris.engine.object;
import art.arcane.iris.platform.bukkit.BukkitBlockResolution; import art.arcane.iris.platform.bukkit.BukkitBlockResolution;
import art.arcane.iris.core.compat.CompatAction;
import art.arcane.iris.core.compat.CompatFinding;
import art.arcane.iris.core.compat.CompatRegistry;
import art.arcane.iris.core.compat.CompatStatus;
import art.arcane.iris.core.compat.ContentGate;
import art.arcane.iris.core.compat.PackCompatReport;
import art.arcane.iris.core.loader.IrisData; import art.arcane.iris.core.loader.IrisData;
import art.arcane.iris.engine.data.cache.AtomicCache; import art.arcane.iris.engine.data.cache.AtomicCache;
import art.arcane.iris.engine.data.cache.LazyBoundedCache; import art.arcane.iris.engine.data.cache.LazyBoundedCache;
@@ -181,6 +187,274 @@ public class IrisObjectPlacement {
@SerializedName(value = "forcePlace", alternate = {"force"}) @SerializedName(value = "forcePlace", alternate = {"force"})
private boolean forcePlace = false; private boolean forcePlace = false;
private transient AtomicCache<TableCache> cache = new AtomicCache<>(); private transient AtomicCache<TableCache> cache = new AtomicCache<>();
private transient AtomicCache<CompatStatus> compatCache = new AtomicCache<>();
@Getter(AccessLevel.NONE)
@Setter(AccessLevel.NONE)
private transient volatile KList<String> compatPlaceCache;
/**
* Version-content gate verdict for this placement on the running server. The placement's own generated blocks
* (every {@code edit[].replace} palette option) exclude the whole placement when one of them is unavailable; each
* object in {@code place} is then dropped when its .iob palette carries a key that neither resolves nor is
* rewritten by a matching {@code edit} rule with chance 1. Match sides ({@code edit[].find}, {@code markers[].mark})
* never count. Cached per placement.
*/
public CompatStatus evaluateCompat(IrisData data) {
return compatCache.aquire(() -> computeCompat(data));
}
/** True when nothing in this placement can generate on the running server. */
public boolean isCompatExcluded(IrisData data) {
return evaluateCompat(data).excluded();
}
/** The {@code place} pool with objects that cannot generate on the running server removed. */
public KList<String> compatPlace(IrisData data) {
evaluateCompat(data);
KList<String> survivors = compatPlaceCache;
return survivors == null ? place : survivors;
}
private CompatStatus computeCompat(IrisData data) {
if (data == null) {
compatPlaceCache = place;
return CompatStatus.OK;
}
ContentGate gate = data.getContentGate();
PackCompatReport report = data.getCompatReport();
if (gate == null || !gate.ready()) {
// Registry not bound or not ready: UNKNOWN is never MISSING, so nothing is gated.
report.markIncomplete("registry not ready while evaluating object placements");
compatPlaceCache = place;
return CompatStatus.OK;
}
String subject = compatSubject();
KList<CompatFinding> reasons = new KList<>();
CompatFinding blocked = evaluatePlacementBlocks(data, gate, subject, reasons);
record(report, reasons);
if (blocked != null) {
compatPlaceCache = new KList<>();
return CompatStatus.excludedBy(reasons);
}
KList<String> survivors = new KList<>(place.size());
CompatFinding firstDrop = null;
for (int i = 0; i < place.size(); i++) {
String objectKey = place.get(i);
KList<CompatFinding> objectReasons = new KList<>();
KList<String> missing = unplaceableKeys(objectKey, data, gate, subject + " place[" + i + "]", objectReasons);
record(report, objectReasons);
reasons.addAll(objectReasons);
if (missing.isEmpty()) {
survivors.add(objectKey);
continue;
}
// One finding per missing key, so the operator sees every fallback the object needs at once.
for (String key : missing) {
CompatFinding dropped = new CompatFinding(CompatRegistry.BLOCK, key, CompatAction.DROPPED,
"object", objectKey, subject + " place[" + i + "]");
report.record(dropped);
reasons.add(dropped);
if (firstDrop == null) {
firstDrop = dropped;
}
}
}
if (firstDrop != null && survivors.isEmpty()) {
CompatFinding excluded = new CompatFinding(firstDrop.registry(), firstDrop.key(), CompatAction.EXCLUDED,
"placement", subject, "no objects remain");
report.record(excluded);
reasons.add(excluded);
compatPlaceCache = new KList<>();
return CompatStatus.excludedBy(reasons);
}
compatPlaceCache = survivors.size() == place.size() ? place : survivors;
return new CompatStatus(false, reasons);
}
/** The subject a finding names: the single object, or the whole place list when there is more than one. */
private String compatSubject() {
if (place.isEmpty()) {
return "";
}
return place.size() == 1 ? place.getFirst() : place.toString(", ");
}
/**
* The placement's own composition blocks: the {@code edit[].replace} palettes. {@code edit[].find} and
* {@code markers[].mark} are match sides and never count - matching a block the server does not have is harmless.
*
* @return the finding that excludes this placement, or null when every declared block resolves
*/
private CompatFinding evaluatePlacementBlocks(IrisData data, ContentGate gate, String subject, KList<CompatFinding> reasons) {
CompatFinding blocked = null;
for (int i = 0; i < edit.size(); i++) {
IrisObjectReplace rule = edit.get(i);
if (rule == null || rule.getReplace() == null) {
continue;
}
KList<IrisBlockData> palette = rule.getReplace().getPalette();
for (int j = 0; j < palette.size(); j++) {
CompatFinding finding = evaluateBlockEntry(palette.get(j), data, gate, subject,
"edit[" + i + "].replace.palette[" + j + "]", reasons);
if (finding != null && blocked == null) {
blocked = finding;
}
}
}
return blocked;
}
/** Records what happened to one declared block entry; returns the finding when it excludes the placement. */
private CompatFinding evaluateBlockEntry(IrisBlockData block, IrisData data, ContentGate gate, String subject,
String detail, KList<CompatFinding> reasons) {
if (block == null) {
return null;
}
BlockCompat resolution = resolveBlockEntry(block, data, gate, new KList<>());
if (resolution.missing()) {
CompatFinding finding = new CompatFinding(CompatRegistry.BLOCK, resolution.key(), CompatAction.EXCLUDED,
"placement", subject, detail);
reasons.add(finding);
return finding;
}
if (resolution.substituted()) {
reasons.add(new CompatFinding(CompatRegistry.BLOCK, resolution.key(), CompatAction.SUBSTITUTED,
"placement", subject, detail + " (using " + resolution.replacement() + ")"));
}
return null;
}
/**
* Every distinct palette key of {@code objectKey} that cannot reach the world, in palette order; empty when the
* object still places. A key survives when the gate resolves it (dimension fallbacks count) or when an
* {@code edit} rule with chance 1 matches it - that rule's replace palette was already verified by
* {@link #evaluatePlacementBlocks}. A rule below chance 1 does not cover the key: the original block can still be
* rolled.
*/
private KList<String> unplaceableKeys(String objectKey, IrisData data, ContentGate gate, String detail, KList<CompatFinding> reasons) {
KList<String> missing = new KList<>();
// Palette header only - no IrisObject is built and no state is resolved. A legacy (V1) object reads as an
// empty palette and is therefore never dropped; V1 has no palette block to scan without a full load.
for (String paletteKey : IrisObjectIO.readPaletteKeysCached(data.getObjectLoader().findFile(objectKey))) {
String base = ContentGate.baseKey(ContentGate.normalizeState(paletteKey));
if (base == null) {
continue;
}
// Existence is a registry question about the block, never about its properties: a property that was
// renamed on a block the server still has is out of scope for the gate.
ContentGate.BlockResolution resolution = gate.resolveBlock(base);
if (resolution != null) {
if (resolution.substituted()) {
reasons.add(new CompatFinding(CompatRegistry.BLOCK, base, CompatAction.SUBSTITUTED,
"object", objectKey, detail + " (using " + resolution.resolvedKey() + ")"));
}
continue;
}
if (coveredByEdit(paletteKey)) {
continue;
}
missing.addIfMissing(base);
}
return missing;
}
/** True when an {@code edit} rule certainly rewrites this palette state before it is written. */
private boolean coveredByEdit(String paletteKey) {
for (IrisObjectReplace rule : edit) {
if (rule == null || rule.getChance() < 1F) {
continue;
}
if (rule.matchesState(paletteKey)) {
return true;
}
}
return false;
}
/**
* Mirrors {@code IrisBlockData.getBlockData} against the gate: pack custom block first, then the registry with
* dimension fallbacks, then the entry's own {@code backup}.
*/
private static BlockCompat resolveBlockEntry(IrisBlockData block, IrisData data, ContentGate gate, KList<IrisBlockData> seen) {
for (IrisBlockData visited : seen) {
if (visited == block) {
return new BlockCompat(String.valueOf(block.getBlock()), null, true, false);
}
}
seen.add(block);
String declared = block.getBlock();
IrisBlockData custom = data.getBlockLoader() == null ? null : data.getBlockLoader().load(declared, false);
if (custom != null && custom != block) {
return resolveBlockEntry(custom, data, gate, seen);
}
String base = ContentGate.baseKey(ContentGate.normalizeState(declared));
if (base == null) {
return new BlockCompat(String.valueOf(declared), null, true, false);
}
ContentGate.BlockResolution resolution = gate.resolveBlock(base);
if (resolution != null) {
return new BlockCompat(base, resolution.resolvedKey(), false, resolution.substituted());
}
IrisBlockData backup = block.getBackup();
if (backup != null) {
BlockCompat viaBackup = resolveBlockEntry(backup, data, gate, seen);
if (!viaBackup.missing()) {
return new BlockCompat(base, viaBackup.replacement(), false, true);
}
}
return new BlockCompat(base, null, true, false);
}
private static void record(PackCompatReport report, KList<CompatFinding> findings) {
for (CompatFinding finding : findings) {
report.record(finding);
}
}
/** One block entry's verdict: the key asked for, what will actually generate, and how it got there. */
private record BlockCompat(String key, String replacement, boolean missing, boolean substituted) {
}
public IrisObjectPlacement toPlacement(String... place) { public IrisObjectPlacement toPlacement(String... place) {
IrisObjectPlacement p = new IrisObjectPlacement(); IrisObjectPlacement p = new IrisObjectPlacement();
@@ -244,11 +518,14 @@ public class IrisObjectPlacement {
} }
public IrisObject getObject(DataProvider g, RNG random) { public IrisObject getObject(DataProvider g, RNG random) {
if (place.isEmpty()) { IrisData data = g.getData();
KList<String> pool = compatPlace(data);
if (pool.isEmpty()) {
return null; return null;
} }
return g.getData().getObjectLoader().load(place.get(random.nextInt(place.size()))); return data.getObjectLoader().load(pool.get(random.nextInt(pool.size())));
} }
public boolean matches(IrisTreeSize size, TreeType type) { public boolean matches(IrisTreeSize size, TreeType type) {
@@ -298,6 +575,9 @@ public class IrisObjectPlacement {
IrisLogging.warn("Couldn't find loot table " + loot.getName()); IrisLogging.warn("Couldn't find loot table " + loot.getName());
continue; continue;
} }
if (table.isCompatExcluded()) {
continue;
}
if (loot.getFilter().isEmpty()) //Table applies to all containers if (loot.getFilter().isEmpty()) //Table applies to all containers
{ {
@@ -424,7 +424,7 @@ final class IrisObjectPlacementRunner {
IrisMarker marker = self.getLoader().getMarkerLoader().load(j.getMarker()); IrisMarker marker = self.getLoader().getMarkerLoader().load(j.getMarker());
int markerSalt = markerIndex++; int markerSalt = markerIndex++;
if (marker == null) { if (marker == null || marker.isCompatExcluded()) {
continue; continue;
} }
@@ -18,6 +18,7 @@
package art.arcane.iris.engine.object; package art.arcane.iris.engine.object;
import art.arcane.iris.core.compat.ContentGate;
import art.arcane.iris.core.loader.IrisData; import art.arcane.iris.core.loader.IrisData;
import art.arcane.iris.engine.data.cache.AtomicCache; import art.arcane.iris.engine.data.cache.AtomicCache;
import art.arcane.iris.engine.object.annotations.ArrayType; import art.arcane.iris.engine.object.annotations.ArrayType;
@@ -35,6 +36,9 @@ import lombok.NoArgsConstructor;
import art.arcane.iris.spi.PlatformBlockState; import art.arcane.iris.spi.PlatformBlockState;
import lombok.experimental.Accessors; import lombok.experimental.Accessors;
import java.util.LinkedHashMap;
import java.util.Map;
@Snippet("object-block-replacer") @Snippet("object-block-replacer")
@Accessors(chain = true) @Accessors(chain = true)
@NoArgsConstructor @NoArgsConstructor
@@ -65,7 +69,7 @@ public class IrisObjectReplace {
KList<PlatformBlockState> b = new KList<>(); KList<PlatformBlockState> b = new KList<>();
for (IrisBlockData i : find) { for (IrisBlockData i : find) {
PlatformBlockState bx = i.getBlockData(rdata); PlatformBlockState bx = i.getBlockDataOrPlaceholder(rdata);
if (bx != null) { if (bx != null) {
b.add(bx); b.add(bx);
@@ -79,4 +83,89 @@ public class IrisObjectReplace {
public PlatformBlockState getReplace(RNG seed, double x, double y, double z, IrisData rdata) { public PlatformBlockState getReplace(RNG seed, double x, double y, double z, IrisData rdata) {
return getReplace().get(seed, x, y, z, rdata); return getReplace().get(seed, x, y, z, rdata);
} }
/**
* Key-level mirror of the runtime find match, for the version-content gate: it has to decide whether a palette
* key the server does not have is rewritten by this rule, and a missing key has no resolved state to compare.
* Non-exact compares the base block; exact requires every property named by the find entry to match, the same
* partial-match contract {@code PlatformBlockState.matches} uses at runtime.
*/
public boolean matchesState(String stateKey) {
String state = ContentGate.normalizeState(stateKey);
if (state == null) {
return false;
}
String stateBase = ContentGate.baseKey(state);
for (IrisBlockData f : find) {
if (f == null) {
continue;
}
String findState = ContentGate.normalizeState(declaredState(f));
if (findState == null || !ContentGate.baseKey(findState).equals(stateBase)) {
continue;
}
if (!exact || propertiesMatch(findState, state)) {
return true;
}
}
return false;
}
/** The find entry as one state string; {@code block} already carrying properties wins over the data map. */
private static String declaredState(IrisBlockData block) {
String declared = block.getBlock();
if (declared == null) {
return null;
}
return declared.indexOf('[') < 0 ? declared + block.computeProperties() : declared;
}
/** Every property the find side names must be present with the same value on the candidate state. */
private static boolean propertiesMatch(String findState, String candidateState) {
Map<String, String> required = properties(findState);
if (required.isEmpty()) {
return true;
}
Map<String, String> candidate = properties(candidateState);
for (Map.Entry<String, String> entry : required.entrySet()) {
if (!entry.getValue().equals(candidate.get(entry.getKey()))) {
return false;
}
}
return true;
}
private static Map<String, String> properties(String state) {
int open = state.indexOf('[');
int close = state.lastIndexOf(']');
if (open < 0 || close < open) {
return Map.of();
}
Map<String, String> properties = new LinkedHashMap<>();
for (String part : state.substring(open + 1, close).split(",")) {
int split = part.indexOf('=');
if (split > 0) {
properties.put(part.substring(0, split).trim(), part.substring(split + 1).trim());
}
}
return properties;
}
} }
@@ -19,6 +19,9 @@
package art.arcane.iris.engine.object; package art.arcane.iris.engine.object;
import art.arcane.iris.engine.framework.render.RenderType; import art.arcane.iris.engine.framework.render.RenderType;
import art.arcane.iris.core.compat.CompatFinding;
import art.arcane.iris.core.compat.CompatStatus;
import art.arcane.iris.core.compat.ContentGate;
import art.arcane.iris.core.loader.IrisData; import art.arcane.iris.core.loader.IrisData;
import art.arcane.iris.spi.IrisLogging; import art.arcane.iris.spi.IrisLogging;
import art.arcane.iris.spi.PlatformBlockState; import art.arcane.iris.spi.PlatformBlockState;
@@ -44,6 +47,8 @@ import lombok.NoArgsConstructor;
import lombok.experimental.Accessors; import lombok.experimental.Accessors;
import java.awt.Color; import java.awt.Color;
import java.util.ArrayList;
import java.util.List;
import java.util.Random; import java.util.Random;
@@ -321,6 +326,11 @@ public class IrisRegion extends IrisRegistrant implements IRare {
continue; continue;
} }
if (biome.isCompatExcluded()) {
CompatPools.drop(g.getData(), biome, "region", getLoadKey(), "biomes " + i, null);
continue;
}
names.add(biome.getCarvingBiome()); names.add(biome.getCarvingBiome());
b.put(biome.getLoadKey(), biome); b.put(biome.getLoadKey(), biome);
names.addAll(biome.getChildren()); names.addAll(biome.getChildren());
@@ -345,55 +355,26 @@ public class IrisRegion extends IrisRegistrant implements IRare {
} }
public KList<IrisBiome> getRealCaveBiomes(DataProvider g) { public KList<IrisBiome> getRealCaveBiomes(DataProvider g) {
return realCaveBiomes.aquire(() -> return realCaveBiomes.aquire(() -> resolvePool(g, getCaveBiomes(), "caveBiomes", null));
{
KList<IrisBiome> realCaveBiomes = new KList<>();
for (String i : getCaveBiomes()) {
realCaveBiomes.add(g.getData().getBiomeLoader().load(i));
}
return realCaveBiomes;
});
} }
public KList<IrisBiome> getRealShoreBiomes(DataProvider g) { public KList<IrisBiome> getRealShoreBiomes(DataProvider g) {
return realShoreBiomes.aquire(() -> return realShoreBiomes.aquire(() -> resolvePool(g, getShoreBiomes(), "shoreBiomes", null));
{
KList<IrisBiome> realShoreBiomes = new KList<>();
for (String i : getShoreBiomes()) {
realShoreBiomes.add(g.getData().getBiomeLoader().load(i));
}
return realShoreBiomes;
});
} }
public KList<IrisBiome> getRealSeaBiomes(DataProvider g) { public KList<IrisBiome> getRealSeaBiomes(DataProvider g) {
return realSeaBiomes.aquire(() -> return realSeaBiomes.aquire(() -> resolvePool(g, getSeaBiomes(), "seaBiomes", null));
{
KList<IrisBiome> realSeaBiomes = new KList<>();
for (String i : getSeaBiomes()) {
realSeaBiomes.add(g.getData().getBiomeLoader().load(i));
}
return realSeaBiomes;
});
} }
public KList<IrisBiome> getRealLandBiomes(DataProvider g) { public KList<IrisBiome> getRealLandBiomes(DataProvider g) {
return realLandBiomes.aquire(() -> return realLandBiomes.aquire(() -> resolvePool(g, getLandBiomes(), "landBiomes", null));
{ }
KList<IrisBiome> realLandBiomes = new KList<>();
for (String i : getLandBiomes()) { /** A biome reference pool with unloadable and gate-excluded biomes removed; drops are reported once. */
realLandBiomes.add(g.getData().getBiomeLoader().load(i)); private KList<IrisBiome> resolvePool(DataProvider g, KList<String> keys, String field, List<CompatFinding> sink) {
} IrisData data = g == null ? null : g.getData();
return CompatPools.load(data == null ? null : data.getBiomeLoader(), keys, data,
return realLandBiomes; "region", getLoadKey(), field, sink);
});
} }
public KList<IrisBiome> getAllAnyBiomes() { public KList<IrisBiome> getAllAnyBiomes() {
@@ -468,6 +449,42 @@ public class IrisRegion extends IrisRegistrant implements IRare {
} }
/**
* Cascade: a region that lost every land biome to the gate cannot generate, so it leaves the dimension's region
* pool too. Loading biomes here is safe - biomes never load regions.
*/
@Override
public CompatStatus evaluateCompat(ContentGate gate) {
CompatStatus base = super.evaluateCompat(gate);
if (base.excluded()) {
return base;
}
IrisData data = getLoader();
if (data == null) {
return base;
}
for (int index = 0; index < structures.size(); index++) {
structures.get(index).reportExcludedCaveBiomes(data, "region", getLoadKey(),
"structures[" + index + "].caveBiomes");
}
if (getLandBiomes().isEmpty()) {
return base;
}
List<CompatFinding> drops = new ArrayList<>();
if (!resolvePool(() -> data, getLandBiomes(), "landBiomes", drops).isEmpty()) {
return base;
}
return CompatPools.cascade(data, base, drops, "region", getLoadKey(), "no land biomes remain");
}
@Override @Override
public String getFolderName() { public String getFolderName() {
return "regions"; return "regions";
@@ -4,6 +4,7 @@ import art.arcane.iris.engine.object.annotations.ArrayType;
import art.arcane.iris.engine.object.annotations.Desc; import art.arcane.iris.engine.object.annotations.Desc;
import art.arcane.iris.engine.object.annotations.MaxNumber; import art.arcane.iris.engine.object.annotations.MaxNumber;
import art.arcane.iris.engine.object.annotations.MinNumber; import art.arcane.iris.engine.object.annotations.MinNumber;
import art.arcane.iris.core.loader.IrisData;
import art.arcane.iris.engine.object.annotations.RegistryListResource; import art.arcane.iris.engine.object.annotations.RegistryListResource;
import art.arcane.volmlib.util.collection.KList; import art.arcane.volmlib.util.collection.KList;
import art.arcane.volmlib.util.collection.KSet; import art.arcane.volmlib.util.collection.KSet;
@@ -83,6 +84,19 @@ public class IrisRiverPolicy {
@Desc("Multiplier applied to the eroded bank width outside the shore band.") @Desc("Multiplier applied to the eroded bank width outside the shore band.")
private Double bankMultiplier = null; private Double bankMultiplier = null;
/**
* A declared river biome selection with biomes the version-content gate excluded removed. A declared-but-now-empty
* selection keeps its "explicitly clears the inherited selection" meaning; null (undeclared) stays null.
*/
public KList<String> compatBiomes(KList<String> declared, IrisData data, String field) {
if (declared == null) {
return null;
}
return CompatPools.surviving(data == null ? null : data.getBiomeLoader(), declared, data,
"river policy", "", field);
}
public KSet<String> getAllBiomeIds() { public KSet<String> getAllBiomeIds() {
KSet<String> biomeIds = new KSet<>(); KSet<String> biomeIds = new KSet<>();
addAll(biomeIds, surfaceBiomes); addAll(biomeIds, surfaceBiomes);
@@ -18,6 +18,10 @@
package art.arcane.iris.engine.object; package art.arcane.iris.engine.object;
import art.arcane.iris.core.compat.CompatFinding;
import art.arcane.iris.core.compat.CompatStatus;
import art.arcane.iris.core.compat.ContentGate;
import art.arcane.iris.core.loader.IrisData;
import art.arcane.iris.core.loader.IrisRegistrant; import art.arcane.iris.core.loader.IrisRegistrant;
import art.arcane.iris.engine.framework.Engine; import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.object.annotations.ArrayType; import art.arcane.iris.engine.object.annotations.ArrayType;
@@ -30,6 +34,9 @@ import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor; import lombok.NoArgsConstructor;
import lombok.experimental.Accessors; import lombok.experimental.Accessors;
import java.util.ArrayList;
import java.util.List;
@EqualsAndHashCode(callSuper = true) @EqualsAndHashCode(callSuper = true)
@Accessors(chain = true) @Accessors(chain = true)
@NoArgsConstructor @NoArgsConstructor
@@ -123,6 +130,64 @@ public class IrisSpawner extends IrisRegistrant {
engine.getEngineData().getChunk(x, z).getCooldown(this).spawn(engine); engine.getEngineData().getChunk(x, z).getCooldown(this).spawn(engine);
} }
/**
* Cascade: a spawn whose entity the gate excluded can never spawn, so it leaves this spawner's lists; a spawner
* with nothing left to spawn leaves every pool that references it. Loading entities here is safe - entities never
* load spawners.
*/
@Override
public CompatStatus evaluateCompat(ContentGate gate) {
CompatStatus base = super.evaluateCompat(gate);
if (base.excluded()) {
return base;
}
IrisData data = getLoader();
if (data == null || data.getEntityLoader() == null) {
return base;
}
boolean declared = !spawns.isEmpty() || !initialSpawns.isEmpty();
List<CompatFinding> drops = new ArrayList<>();
spawns = dropExcludedSpawns(data, spawns, "spawns", drops);
initialSpawns = dropExcludedSpawns(data, initialSpawns, "initialSpawns", drops);
if (!declared || !spawns.isEmpty() || !initialSpawns.isEmpty()) {
return base;
}
return CompatPools.cascade(data, base, drops, "spawner", getLoadKey(), "no entity spawns remain");
}
private KList<IrisEntitySpawn> dropExcludedSpawns(IrisData data,
KList<IrisEntitySpawn> declared,
String field,
List<CompatFinding> drops) {
KList<IrisEntitySpawn> kept = new KList<>();
for (int index = 0; index < declared.size(); index++) {
IrisEntitySpawn spawn = declared.get(index);
if (spawn == null) {
continue;
}
IrisEntity entity = data.getEntityLoader().load(spawn.getEntity());
if (entity != null && entity.isCompatExcluded()) {
CompatPools.drop(data, entity, "spawner", getLoadKey(),
field + "[" + index + "] " + spawn.getEntity(), drops);
continue;
}
kept.add(spawn);
}
return kept;
}
@Override @Override
public String getFolderName() { public String getFolderName() {
return "spawners"; return "spawners";
@@ -18,6 +18,12 @@
package art.arcane.iris.engine.object; package art.arcane.iris.engine.object;
import art.arcane.iris.core.compat.CompatAction;
import art.arcane.iris.core.compat.CompatFinding;
import art.arcane.iris.core.compat.CompatRegistry;
import art.arcane.iris.core.compat.CompatStatus;
import art.arcane.iris.core.compat.ContentGate;
import art.arcane.iris.core.loader.IrisData;
import art.arcane.iris.core.loader.IrisRegistrant; import art.arcane.iris.core.loader.IrisRegistrant;
import art.arcane.iris.engine.object.annotations.ArrayType; import art.arcane.iris.engine.object.annotations.ArrayType;
import art.arcane.iris.engine.object.annotations.Desc; import art.arcane.iris.engine.object.annotations.Desc;
@@ -126,6 +132,42 @@ public class IrisStructure extends IrisRegistrant {
return placement; return placement;
} }
/**
* The start pool is the only mandatory entry point into an assembly: when it has nothing left to place, the
* structure cannot generate on this server and every placement that lists it skips it.
*/
@Override
public CompatStatus evaluateCompat(ContentGate gate) {
CompatStatus base = super.evaluateCompat(gate);
if (base.excluded() || gate == null || !gate.ready()) {
return base;
}
IrisData data = getLoader();
if (data == null || startPool == null || startPool.isBlank()) {
return base;
}
IrisJigsawPool pool = data.load(IrisJigsawPool.class, startPool.trim(), false);
if (pool == null || !pool.isCompatExcluded()) {
return base;
}
CompatFinding reason = pool.getCompat().reasons().isEmpty() ? null : pool.getCompat().reasons().getLast();
CompatFinding excluded = new CompatFinding(
reason == null ? CompatRegistry.BLOCK : reason.registry(),
reason == null ? startPool.trim() : reason.key(),
CompatAction.EXCLUDED, "structure", getLoadKey(),
"start pool " + startPool.trim() + " is unavailable");
gate.report().record(excluded);
KList<CompatFinding> reasons = new KList<>(base.reasons());
reasons.add(excluded);
return CompatStatus.excludedBy(reasons);
}
@Override @Override
public String getFolderName() { public String getFolderName() {
return "structures"; return "structures";
@@ -18,6 +18,7 @@
package art.arcane.iris.engine.object; package art.arcane.iris.engine.object;
import art.arcane.iris.core.loader.IrisData;
import art.arcane.iris.engine.object.annotations.ArrayType; import art.arcane.iris.engine.object.annotations.ArrayType;
import art.arcane.iris.engine.object.annotations.Desc; import art.arcane.iris.engine.object.annotations.Desc;
import art.arcane.iris.engine.object.annotations.MaxNumber; import art.arcane.iris.engine.object.annotations.MaxNumber;
@@ -147,4 +148,24 @@ public class IrisStructurePlacement {
IrisStructureAnchorMode activeAnchor = resolvedAnchor(); IrisStructureAnchorMode activeAnchor = resolvedAnchor();
return activeAnchor == IrisStructureAnchorMode.HEIGHT_BAND || activeAnchor.isCave(); return activeAnchor == IrisStructureAnchorMode.HEIGHT_BAND || activeAnchor.isCave();
} }
/**
* Reports cave biome references the version-content gate excluded. The list is deliberately not filtered: it is an
* allowlist whose emptiness means "any cave biome", and it feeds the placement determinism signature. An excluded
* biome never generates, so the entry is inert - this only makes it visible in the pack compat report.
*/
void reportExcludedCaveBiomes(IrisData data, String subjectType, String subjectKey, String field) {
if (data == null || data.getBiomeLoader() == null || caveBiomes == null) {
return;
}
for (int index = 0; index < caveBiomes.size(); index++) {
String key = caveBiomes.get(index);
IrisBiome biome = data.getBiomeLoader().load(key);
if (biome != null && biome.isCompatExcluded()) {
CompatPools.drop(data, biome, subjectType, subjectKey, field + "[" + index + "] " + key, null);
}
}
}
} }
@@ -0,0 +1,145 @@
/*
* Iris is a World Generator for Minecraft Bukkit Servers
* Copyright (c) 2026 Arcane Arts (Volmit Software)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package art.arcane.iris.engine.framework;
import art.arcane.iris.core.compat.CompatAction;
import art.arcane.iris.core.compat.CompatFinding;
import art.arcane.iris.core.compat.CompatRegistry;
import art.arcane.iris.core.compat.CompatStatus;
import art.arcane.iris.core.loader.IrisData;
import art.arcane.iris.engine.framework.structure.StructureAssemblyResult;
import art.arcane.iris.engine.framework.structure.StructureAssemblyStatus;
import art.arcane.iris.engine.object.IrisDirection;
import art.arcane.iris.engine.object.IrisJigsawConnector;
import art.arcane.iris.engine.object.IrisJigsawPiece;
import art.arcane.iris.engine.object.IrisJigsawPieceEntry;
import art.arcane.iris.engine.object.IrisJigsawPool;
import art.arcane.iris.engine.object.IrisObject;
import art.arcane.iris.engine.object.IrisPosition;
import art.arcane.iris.engine.object.IrisStructure;
import art.arcane.iris.engine.object.JigsawJoint;
import art.arcane.iris.spi.IrisPlatform;
import art.arcane.iris.spi.IrisPlatforms;
import art.arcane.iris.spi.PlatformBlockState;
import art.arcane.iris.spi.PlatformRegistries;
import art.arcane.volmlib.util.math.RNG;
import org.junit.AfterClass;
import org.junit.BeforeClass;
import org.junit.Test;
import java.util.List;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
public class StructureAssemblerCompatTest {
@BeforeClass
public static void bindPlatform() {
IrisPlatforms.unbind();
PlatformBlockState block = mock(PlatformBlockState.class);
PlatformRegistries registries = mock(PlatformRegistries.class);
when(registries.block(anyString())).thenReturn(block);
IrisPlatform platform = mock(IrisPlatform.class);
when(platform.registries()).thenReturn(registries);
IrisPlatforms.bind(platform);
}
@AfterClass
public static void unbindPlatform() {
IrisPlatforms.unbind();
}
@Test
public void anExcludedPieceIsNeverDrawnFromItsPool() {
IrisData data = mock(IrisData.class);
IrisJigsawPiece good = piece("good-object");
IrisJigsawPiece bad = excluded(piece("bad-object"));
when(data.load(IrisJigsawPool.class, "start", false)).thenReturn(pool("good-piece", "bad-piece"));
when(data.load(IrisJigsawPool.class, "terminal", false)).thenReturn(new IrisJigsawPool());
when(data.load(IrisJigsawPiece.class, "good-piece", false)).thenReturn(good);
when(data.load(IrisJigsawPiece.class, "bad-piece", false)).thenReturn(bad);
when(data.load(IrisObject.class, "good-object", false)).thenReturn(new IrisObject(1, 1, 1));
when(data.load(IrisObject.class, "bad-object", false)).thenReturn(new IrisObject(1, 1, 1));
for (int seed = 0; seed < 32; seed++) {
StructureAssemblyResult result = StructureAssembler
.forData(data, structure("start"), new IrisPosition(0, 0, 0))
.assemble(new RNG(seed));
assertTrue(result.detail(), result.hasOutput());
assertTrue(result.pieces().toString(), result.pieces().stream()
.allMatch(placed -> "good-object".equals(placed.getPiece().getObject())));
}
}
@Test
public void aPoolWithOnlyExcludedPiecesStopsTheAssemblyWithoutThrowing() {
IrisData data = mock(IrisData.class);
IrisJigsawPool startPool = pool("bad-piece");
startPool.setCompat(CompatStatus.excludedBy(List.of(finding(CompatAction.EXCLUDED))));
when(data.load(IrisJigsawPool.class, "start", false)).thenReturn(startPool);
when(data.load(IrisJigsawPool.class, "terminal", false)).thenReturn(new IrisJigsawPool());
when(data.load(IrisJigsawPiece.class, "bad-piece", false)).thenReturn(excluded(piece("bad-object")));
when(data.load(IrisObject.class, "bad-object", false)).thenReturn(new IrisObject(1, 1, 1));
StructureAssemblyResult result = StructureAssembler
.forData(data, structure("start"), new IrisPosition(0, 0, 0))
.assemble(new RNG(3));
assertFalse(result.hasOutput());
assertEquals(StructureAssemblyStatus.FAILED_RULES, result.status());
}
private static IrisJigsawPiece excluded(IrisJigsawPiece piece) {
piece.setCompat(CompatStatus.excludedBy(List.of(finding(CompatAction.EXCLUDED))));
return piece;
}
private static CompatFinding finding(CompatAction action) {
return new CompatFinding(CompatRegistry.BLOCK, "minecraft:sulfur", action, "jigsaw piece", "bad-piece", "object bad-object");
}
private static IrisJigsawPiece piece(String object) {
IrisJigsawPiece piece = new IrisJigsawPiece().setObject(object).setRotatable(false);
piece.getConnectors().add(new IrisJigsawConnector()
.setPosition(new IrisPosition())
.setDirection(IrisDirection.EAST_POSITIVE_X)
.setPool("terminal")
.setName("start")
.setTargetName("door")
.setJoint(JigsawJoint.ROLLABLE));
return piece;
}
private static IrisJigsawPool pool(String... pieces) {
IrisJigsawPool pool = new IrisJigsawPool();
for (String piece : pieces) {
pool.getPieces().add(new IrisJigsawPieceEntry().setPiece(piece).setWeight(1));
}
return pool;
}
private static IrisStructure structure(String startPool) {
return new IrisStructure().setStartPool(startPool).setMaxDepth(1).setMaxSizeChunks(8);
}
}
@@ -0,0 +1,84 @@
package art.arcane.iris.engine.object;
import art.arcane.iris.core.compat.CompatAction;
import art.arcane.iris.core.compat.CompatFinding;
import art.arcane.iris.core.compat.CompatRegistry;
import art.arcane.iris.core.compat.CompatStatus;
import art.arcane.iris.core.compat.PackCompatReport;
import art.arcane.iris.core.loader.IrisData;
import art.arcane.iris.core.loader.IrisRegistrant;
import java.util.List;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
/**
* Shared fixtures for the version-content pool cascade tests. The gate walker (lane L1) is not exercised here: the
* tests stamp {@code compat} on a registrant directly, which is exactly what the walker stores.
*/
final class CompatFixtures {
static final String MISSING_BLOCK = "minecraft:sulfur";
static final String MISSING_ENTITY = "minecraft:camel";
private CompatFixtures() {
}
static IrisData data(PackCompatReport report) {
IrisData data = mock(IrisData.class);
when(data.getCompatReport()).thenReturn(report);
return data;
}
/** Marks a registrant excluded the way the gate walker would for a missing composed block. */
static <T extends IrisRegistrant> T excludeBlock(T registrant) {
return exclude(registrant, CompatRegistry.BLOCK, MISSING_BLOCK);
}
/** Marks a registrant excluded the way the gate walker would for a missing entity type. */
static <T extends IrisRegistrant> T excludeEntity(T registrant) {
return exclude(registrant, CompatRegistry.ENTITY, MISSING_ENTITY);
}
static <T extends IrisRegistrant> T exclude(T registrant, CompatRegistry registry, String key) {
registrant.setCompat(CompatStatus.excludedBy(List.of(new CompatFinding(registry, key,
CompatAction.EXCLUDED, registrant.getTypeName().toLowerCase(java.util.Locale.ROOT),
registrant.getLoadKey(), "composition"))));
return registrant;
}
static IrisBiome biome(String key) {
IrisBiome biome = new IrisBiome();
biome.setLoadKey(key);
return biome;
}
static IrisRegion region(String key) {
IrisRegion region = new IrisRegion();
region.setLoadKey(key);
return region;
}
static IrisEntity entity(String key) {
IrisEntity entity = new IrisEntity();
entity.setLoadKey(key);
return entity;
}
static IrisLootTable lootTable(String key) {
IrisLootTable table = new IrisLootTable();
table.setLoadKey(key);
return table;
}
static CompatFinding find(PackCompatReport report, CompatAction action, String subjectType, String subjectKey) {
for (CompatFinding finding : report.findings()) {
if (finding.action() == action
&& finding.subjectType().equals(subjectType)
&& finding.subjectKey().equals(subjectKey)) {
return finding;
}
}
return null;
}
}
@@ -0,0 +1,254 @@
/*
* Iris is a World Generator for Minecraft Bukkit Servers
* Copyright (c) 2026 Arcane Arts (Volmit Software)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package art.arcane.iris.engine.object;
import art.arcane.iris.engine.framework.MeteredCache;
import art.arcane.iris.engine.framework.PreservationRegistry;
import art.arcane.iris.spi.IrisPlatform;
import art.arcane.iris.spi.IrisPlatforms;
import art.arcane.iris.spi.IrisServices;
import art.arcane.iris.spi.PlatformBiome;
import art.arcane.iris.spi.PlatformBlockProperty;
import art.arcane.iris.spi.PlatformBlockState;
import art.arcane.iris.spi.PlatformEntityType;
import art.arcane.iris.spi.PlatformItem;
import art.arcane.iris.spi.PlatformRegistries;
import org.mockito.Answers;
import java.io.DataOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.concurrent.ExecutorService;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
/**
* Binds a platform whose block registry holds exactly the keys a compat test declares, so the gate can be exercised
* without a server. Also writes the .iob and JSON fixtures those tests need.
*/
final class CompatTestPlatform {
private final IrisPlatform previous;
private CompatTestPlatform(IrisPlatform previous) {
this.previous = previous;
}
static CompatTestPlatform bind(File dataFolder, List<String> blockKeys) {
IrisPlatform previous = IrisPlatforms.isBound() ? IrisPlatforms.get() : null;
IrisPlatforms.unbind();
IrisPlatform platform = mock(IrisPlatform.class, Answers.CALLS_REAL_METHODS);
when(platform.dataFolder()).thenReturn(dataFolder);
when(platform.registries()).thenReturn(new FixedRegistries(blockKeys));
IrisPlatforms.bind(platform);
IrisServices.register(PreservationRegistry.class, new NoOpPreservationRegistry());
return new CompatTestPlatform(previous);
}
void unbind() {
IrisServices.clear();
IrisPlatforms.unbind();
if (previous != null) {
IrisPlatforms.bind(previous);
}
}
/** A minimal V2 .iob under {@code <pack>/objects/<key>.iob} carrying only the given palette. */
static File writeObject(File pack, String key, String... palette) throws IOException {
File file = new File(pack, "objects/" + key + ".iob");
file.getParentFile().mkdirs();
try (DataOutputStream out = new DataOutputStream(new FileOutputStream(file))) {
out.writeInt(1);
out.writeInt(1);
out.writeInt(1);
out.writeUTF("Iris V2 IOB;");
out.writeShort(palette.length);
for (String entry : palette) {
out.writeUTF(entry);
}
out.writeInt(0);
out.writeInt(0);
}
return file;
}
static File write(File pack, String path, String content) throws IOException {
File file = new File(pack, path);
file.getParentFile().mkdirs();
Files.write(file.toPath(), content.getBytes(StandardCharsets.UTF_8));
return file;
}
private record FixedRegistries(List<String> blocks) implements PlatformRegistries {
private static String base(String key) {
if (key == null) {
return null;
}
String trimmed = key.trim().toLowerCase(Locale.ROOT);
int props = trimmed.indexOf('[');
String head = props < 0 ? trimmed : trimmed.substring(0, props);
return head.indexOf(':') < 0 ? "minecraft:" + head : head;
}
private boolean has(String key) {
String normalized = base(key);
if (normalized == null) {
return false;
}
for (String block : blocks) {
if (base(block).equals(normalized)) {
return true;
}
}
return false;
}
@Override
public PlatformBlockState block(String key) {
PlatformBlockState state = blockOrNull(key, false);
return state == null ? air() : state;
}
@Override
public PlatformBlockState blockOrNull(String key) {
return blockOrNull(key, false);
}
@Override
public PlatformBlockState blockOrNull(String key, boolean warn) {
return has(key) ? StateCache.of(base(key)) : null;
}
@Override
public PlatformBlockState air() {
return StateCache.of("minecraft:air");
}
@Override
public PlatformBlockState deepSlateOre(PlatformBlockState block, PlatformBlockState ore) {
return ore;
}
@Override
public PlatformBiome biome(String key) {
return null;
}
@Override
public PlatformItem item(String key) {
return null;
}
@Override
public PlatformEntityType entity(String key) {
return null;
}
@Override
public List<String> blockKeys() {
return blocks;
}
@Override
public List<String> biomeKeys() {
return List.of();
}
@Override
public List<String> structureKeys() {
return List.of();
}
@Override
public List<String> itemKeys() {
return List.of();
}
@Override
public List<String> entityKeys() {
return List.of();
}
@Override
public List<String> blockTypeKeys() {
return blocks;
}
@Override
public List<String> enchantmentKeys() {
return List.of();
}
@Override
public List<String> potionEffectKeys() {
return List.of();
}
@Override
public List<String> lootTableKeys() {
return List.of();
}
@Override
public Map<String, List<PlatformBlockProperty>> blockStateProperties() {
return Map.of();
}
}
/** One interned mock per key so {@code matches} and identity behave like a real adapter. */
private static final class StateCache {
private static final Map<String, PlatformBlockState> STATES = new LinkedHashMap<>();
private StateCache() {
}
static synchronized PlatformBlockState of(String key) {
return STATES.computeIfAbsent(key, k -> {
PlatformBlockState state = mock(PlatformBlockState.class);
when(state.key()).thenReturn(k);
return state;
});
}
}
private static final class NoOpPreservationRegistry implements PreservationRegistry {
@Override
public void register(Thread thread) {
}
@Override
public void register(ExecutorService service) {
}
@Override
public void registerCache(MeteredCache cache) {
}
@Override
public void dereference() {
}
}
}
@@ -0,0 +1,95 @@
package art.arcane.iris.engine.object;
import art.arcane.iris.core.compat.CompatAction;
import art.arcane.iris.core.compat.CompatFinding;
import art.arcane.iris.core.compat.PackCompatReport;
import art.arcane.iris.core.loader.IrisData;
import art.arcane.iris.core.loader.ResourceLoader;
import art.arcane.volmlib.util.collection.KList;
import org.junit.Test;
import static art.arcane.iris.engine.object.CompatFixtures.MISSING_BLOCK;
import static art.arcane.iris.engine.object.CompatFixtures.biome;
import static art.arcane.iris.engine.object.CompatFixtures.excludeBlock;
import static art.arcane.iris.engine.object.CompatFixtures.find;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertTrue;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
public class IrisBiomeCompatChildrenTest {
@SuppressWarnings("unchecked")
private static IrisData dataWith(PackCompatReport report, IrisBiome... biomes) {
IrisData data = CompatFixtures.data(report);
ResourceLoader<IrisBiome> biomeLoader = mock(ResourceLoader.class);
when(data.getBiomeLoader()).thenReturn(biomeLoader);
for (IrisBiome biome : biomes) {
when(biomeLoader.load(biome.getLoadKey())).thenReturn(biome);
}
return data;
}
@Test
public void realChildrenDropsExcludedChildAndRecordsFinding() {
PackCompatReport report = new PackCompatReport();
IrisBiome parent = biome("land/plains");
IrisBiome hills = biome("land/hills");
IrisBiome sulfur = excludeBlock(biome("land/sulfur-flats"));
parent.setChildren(new KList<>("land/hills", "land/sulfur-flats"));
IrisData data = dataWith(report, parent, hills, sulfur);
parent.setLoader(data);
KList<IrisBiome> children = parent.getRealChildren(() -> data);
assertEquals(1, children.size());
assertEquals("land/hills", children.get(0).getLoadKey());
CompatFinding dropped = find(report, CompatAction.DROPPED, "biome", "land/plains");
assertNotNull(dropped);
assertEquals(MISSING_BLOCK, dropped.key());
assertEquals("children[1] land/sulfur-flats", dropped.detail());
}
@Test
public void realChildrenSkipsUnloadableChild() {
PackCompatReport report = new PackCompatReport();
IrisBiome parent = biome("land/plains");
parent.setChildren(new KList<>("land/gone"));
IrisData data = dataWith(report, parent);
parent.setLoader(data);
assertTrue(parent.getRealChildren(() -> data).isEmpty());
}
@Test
public void floatingChildBiomeFallsBackToParentWhenTargetExcluded() {
PackCompatReport report = new PackCompatReport();
IrisBiome parent = biome("land/plains");
IrisBiome sulfur = excludeBlock(biome("sky/sulfur-isles"));
IrisData data = dataWith(report, parent, sulfur);
parent.setLoader(data);
IrisFloatingChildBiomes floating = new IrisFloatingChildBiomes().setBiome("sky/sulfur-isles");
assertSame(parent, floating.getRealBiome(parent, data));
CompatFinding dropped = find(report, CompatAction.DROPPED, "floating child biome", "land/plains");
assertNotNull(dropped);
assertEquals(MISSING_BLOCK, dropped.key());
}
@Test
public void carvingEntryResolvesNullWhenBiomeExcluded() {
PackCompatReport report = new PackCompatReport();
IrisBiome sulfur = excludeBlock(biome("cave/sulfur-grotto"));
IrisData data = dataWith(report, sulfur);
IrisDimensionCarvingEntry entry = new IrisDimensionCarvingEntry()
.setId("deep-band")
.setBiome("cave/sulfur-grotto");
assertNull(entry.getRealBiome(data));
CompatFinding dropped = find(report, CompatAction.DROPPED, "carving entry", "deep-band");
assertNotNull(dropped);
assertEquals(MISSING_BLOCK, dropped.key());
}
}
@@ -0,0 +1,91 @@
package art.arcane.iris.engine.object;
import art.arcane.iris.core.compat.ContentGate;
import art.arcane.iris.core.compat.PackCompatReport;
import art.arcane.iris.core.nms.datapack.IDataFixer;
import art.arcane.iris.spi.PlatformRegistries;
import art.arcane.volmlib.util.collection.KList;
import art.arcane.volmlib.util.json.JSONArray;
import art.arcane.volmlib.util.json.JSONObject;
import org.junit.Test;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
public class IrisBiomeCustomCompatTest {
private static final IDataFixer PASSTHROUGH = new IDataFixer() {
@Override
public JSONObject resolve(IDataFixer.Dimension dimension, art.arcane.iris.engine.object.IrisDimensionTypeOptions options) {
return new JSONObject();
}
@Override
public void fixDimension(IDataFixer.Dimension dimension, JSONObject json) {
}
};
private static ContentGate gateWithEntities(String... present) {
List<String> entities = new ArrayList<>();
entities.add("minecraft:pig");
entities.addAll(List.of(present));
PlatformRegistries registries = mock(PlatformRegistries.class);
when(registries.blockKeys()).thenReturn(List.of("minecraft:stone"));
when(registries.entityKeys()).thenReturn(entities);
return new ContentGate(registries, Map.of(), new PackCompatReport());
}
private static IrisBiomeCustom customBiome() {
IrisBiomeCustom biome = new IrisBiomeCustom();
biome.setId("sulfur_grotto");
biome.setSpawnRarity(1);
biome.setSpawns(new KList<>(
new IrisBiomeCustomSpawn().setType("minecraft:zombie"),
new IrisBiomeCustomSpawn().setType("minecraft:camel")));
return biome;
}
@Test
public void generateJsonOmitsSpawnWhoseEntityTypeIsMissing() {
JSONObject json = new JSONObject(customBiome().generateJson(PASSTHROUGH, gateWithEntities("minecraft:zombie")));
JSONArray monsters = json.getJSONObject("spawners").getJSONArray("misc");
assertEquals(1, monsters.length());
assertEquals("minecraft:zombie", monsters.getJSONObject(0).getString("type"));
assertFalse(json.getJSONObject("spawners").toString().contains("minecraft:camel"));
}
@Test
public void generateJsonKeepsEverySpawnWhenAllEntitiesArePresent() {
JSONObject json = new JSONObject(customBiome()
.generateJson(PASSTHROUGH, gateWithEntities("minecraft:zombie", "minecraft:camel")));
assertEquals(2, json.getJSONObject("spawners").getJSONArray("misc").length());
}
@Test
public void generateJsonKeepsEverySpawnWhenRegistryCannotBeConsulted() {
// Registry not ready (modded early boot): UNKNOWN must never behave like MISSING.
PlatformRegistries registries = mock(PlatformRegistries.class);
when(registries.blockKeys()).thenReturn(List.of());
when(registries.entityKeys()).thenReturn(List.of());
ContentGate gate = new ContentGate(registries, Map.of(), new PackCompatReport());
JSONObject json = new JSONObject(customBiome().generateJson(PASSTHROUGH, gate));
assertEquals(2, json.getJSONObject("spawners").getJSONArray("misc").length());
}
@Test
public void generateJsonWritesNoSpawnersWhenEveryEntityIsMissing() {
JSONObject json = new JSONObject(customBiome().generateJson(PASSTHROUGH, gateWithEntities()));
assertEquals(0, json.getJSONObject("spawners").length());
}
}
@@ -0,0 +1,34 @@
package art.arcane.iris.engine.object;
import org.junit.Test;
import static org.junit.Assert.assertEquals;
/**
* The gate walker drops a {@code vanillaDerivative} whose biome key is missing (policy table: entry DROPPED, falls back
* to derivative). This pins the fallback the drop relies on; the drop itself is simulated by clearing the field, which
* is exactly what the walker does.
*/
public class IrisBiomeDerivativeCompatTest {
@Test
public void droppedVanillaDerivativeFallsBackToDerivative() {
IrisBiome biome = CompatFixtures.biome("cave/sulfur-grotto");
biome.setDerivative("minecraft:dripstone_caves");
biome.setVanillaDerivative("minecraft:sulfur_caves");
assertEquals("minecraft:sulfur_caves", biome.getVanillaDerivativeKey());
biome.setVanillaDerivative(null);
assertEquals("minecraft:dripstone_caves", biome.getVanillaDerivativeKey());
}
@Test
public void blankVanillaDerivativeFallsBackToDerivative() {
IrisBiome biome = CompatFixtures.biome("cave/sulfur-grotto");
biome.setDerivative("minecraft:dripstone_caves");
biome.setVanillaDerivative("");
assertEquals("minecraft:dripstone_caves", biome.getVanillaDerivativeKey());
}
}
@@ -0,0 +1,92 @@
package art.arcane.iris.engine.object;
import art.arcane.iris.core.compat.CompatAction;
import art.arcane.iris.core.compat.CompatFinding;
import art.arcane.iris.core.compat.CompatStatus;
import art.arcane.iris.core.compat.ContentGate;
import art.arcane.iris.core.compat.PackCompatReport;
import art.arcane.iris.core.loader.IrisData;
import art.arcane.iris.core.loader.ResourceLoader;
import art.arcane.volmlib.util.collection.KList;
import org.junit.Test;
import java.util.Map;
import static art.arcane.iris.engine.object.CompatFixtures.MISSING_BLOCK;
import static art.arcane.iris.engine.object.CompatFixtures.excludeBlock;
import static art.arcane.iris.engine.object.CompatFixtures.find;
import static art.arcane.iris.engine.object.CompatFixtures.region;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
public class IrisDimensionCompatRegionTest {
@SuppressWarnings("unchecked")
private static IrisData dataWith(PackCompatReport report, IrisRegion... regions) {
IrisData data = CompatFixtures.data(report);
ResourceLoader<IrisRegion> regionLoader = mock(ResourceLoader.class);
when(data.getRegionLoader()).thenReturn(regionLoader);
for (IrisRegion region : regions) {
when(regionLoader.load(region.getLoadKey())).thenReturn(region);
}
return data;
}
private static ContentGate gate(PackCompatReport report) {
return new ContentGate(null, Map.of(), report);
}
@Test
public void allRegionsDropsExcludedRegionAndRecordsFinding() {
PackCompatReport report = new PackCompatReport();
IrisRegion plains = region("overworld");
IrisRegion sulfur = excludeBlock(region("sulfur-lands"));
IrisData data = dataWith(report, plains, sulfur);
IrisDimension dimension = new IrisDimension().setRegions(new KList<>("overworld", "sulfur-lands"));
dimension.setLoadKey("overworld");
dimension.setLoader(data);
KList<IrisRegion> pool = dimension.getAllRegions(() -> data);
assertEquals(1, pool.size());
assertEquals("overworld", pool.get(0).getLoadKey());
CompatFinding dropped = find(report, CompatAction.DROPPED, "dimension", "overworld");
assertNotNull(dropped);
assertEquals(MISSING_BLOCK, dropped.key());
assertEquals("regions[1] sulfur-lands", dropped.detail());
}
@Test
public void evaluateCompatExcludesDimensionWithNoRegionsLeft() {
PackCompatReport report = new PackCompatReport();
IrisRegion sulfur = excludeBlock(region("sulfur-lands"));
IrisData data = dataWith(report, sulfur);
IrisDimension dimension = new IrisDimension().setRegions(new KList<>("sulfur-lands"));
dimension.setLoadKey("sulfurworld");
dimension.setLoader(data);
CompatStatus status = dimension.evaluateCompat(gate(report));
assertTrue(status.excluded());
CompatFinding cascade = find(report, CompatAction.EXCLUDED, "dimension", "sulfurworld");
assertNotNull(cascade);
assertEquals(MISSING_BLOCK, cascade.key());
assertEquals("no regions remain", cascade.detail());
}
@Test
public void evaluateCompatKeepsDimensionWithSurvivingRegion() {
PackCompatReport report = new PackCompatReport();
IrisRegion plains = region("overworld");
IrisRegion sulfur = excludeBlock(region("sulfur-lands"));
IrisData data = dataWith(report, plains, sulfur);
IrisDimension dimension = new IrisDimension().setRegions(new KList<>("overworld", "sulfur-lands"));
dimension.setLoadKey("overworld");
dimension.setLoader(data);
assertFalse(dimension.evaluateCompat(gate(report)).excluded());
}
}
@@ -0,0 +1,158 @@
/*
* Iris is a World Generator for Minecraft Bukkit Servers
* Copyright (c) 2026 Arcane Arts (Volmit Software)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package art.arcane.iris.engine.object;
import art.arcane.iris.core.IrisSettings;
import art.arcane.iris.core.compat.CompatAction;
import art.arcane.iris.core.loader.IrisData;
import org.junit.After;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TemporaryFolder;
import java.io.File;
import java.util.List;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
public class IrisJigsawCompatTest {
@Rule
public TemporaryFolder temporaryFolder = new TemporaryFolder();
private CompatTestPlatform platform;
private IrisSettings previousSettings;
private IrisData data;
@Before
public void bindPlatform() {
previousSettings = IrisSettings.settings;
IrisSettings.settings = new IrisSettings();
platform = CompatTestPlatform.bind(temporaryFolder.getRoot(),
List.of("minecraft:stone", "minecraft:cobblestone", "minecraft:air"));
}
@After
public void restorePlatform() {
if (data != null) {
data.close();
data = null;
}
if (platform != null) {
platform.unbind();
platform = null;
}
IrisSettings.settings = previousSettings;
}
@Test
public void aPieceWhoseObjectNeedsAMissingBlockIsExcluded() throws Exception {
File pack = pack("piece");
CompatTestPlatform.writeObject(pack, "bad", "minecraft:sulfur");
CompatTestPlatform.write(pack, "jigsaw-pieces/bad.json", "{\"object\":\"bad\"}");
data = IrisData.get(pack);
IrisJigsawPiece piece = data.load(IrisJigsawPiece.class, "bad", false);
assertNotNull(piece);
assertTrue(piece.isCompatExcluded());
assertTrue(data.getCompatReport().findings().stream()
.anyMatch(finding -> "minecraft:sulfur".equals(finding.key())
&& finding.action() == CompatAction.EXCLUDED));
}
@Test
public void aPieceWhoseObjectResolvesIsKept() throws Exception {
File pack = pack("piece-ok");
CompatTestPlatform.writeObject(pack, "good", "minecraft:stone");
CompatTestPlatform.write(pack, "jigsaw-pieces/good.json", "{\"object\":\"good\"}");
data = IrisData.get(pack);
IrisJigsawPiece piece = data.load(IrisJigsawPiece.class, "good", false);
assertNotNull(piece);
assertFalse(piece.isCompatExcluded());
}
@Test
public void aPoolKeepsGeneratingWhileOnePieceSurvives() throws Exception {
File pack = pack("pool-partial");
CompatTestPlatform.writeObject(pack, "good", "minecraft:stone");
CompatTestPlatform.writeObject(pack, "bad", "minecraft:sulfur");
CompatTestPlatform.write(pack, "jigsaw-pieces/good.json", "{\"object\":\"good\"}");
CompatTestPlatform.write(pack, "jigsaw-pieces/bad.json", "{\"object\":\"bad\"}");
CompatTestPlatform.write(pack, "jigsaw-pools/pool.json",
"{\"pieces\":[{\"piece\":\"good\",\"weight\":1},{\"piece\":\"bad\",\"weight\":1}]}");
data = IrisData.get(pack);
IrisJigsawPool pool = data.load(IrisJigsawPool.class, "pool", false);
assertNotNull(pool);
assertFalse(pool.isCompatExcluded());
assertTrue(data.getCompatReport().findings().stream()
.anyMatch(finding -> finding.action() == CompatAction.DROPPED
&& "jigsaw pool".equals(finding.subjectType())));
}
@Test
public void pieceExclusionCascadesToThePoolAndTheStructure() throws Exception {
File pack = pack("cascade");
CompatTestPlatform.writeObject(pack, "bad", "minecraft:sulfur");
CompatTestPlatform.write(pack, "jigsaw-pieces/bad.json", "{\"object\":\"bad\"}");
CompatTestPlatform.write(pack, "jigsaw-pools/pool.json",
"{\"pieces\":[{\"piece\":\"bad\",\"weight\":1}]}");
CompatTestPlatform.write(pack, "structures/keep.json", "{\"startPool\":\"pool\"}");
data = IrisData.get(pack);
IrisJigsawPool pool = data.load(IrisJigsawPool.class, "pool", false);
IrisStructure structure = data.load(IrisStructure.class, "keep", false);
assertNotNull(pool);
assertNotNull(structure);
assertTrue(pool.isCompatExcluded());
assertTrue(structure.isCompatExcluded());
assertTrue(data.getCompatReport().findings().stream()
.anyMatch(finding -> "structure".equals(finding.subjectType())
&& finding.action() == CompatAction.EXCLUDED
&& "minecraft:sulfur".equals(finding.key())));
}
@Test
public void aStructureOverAHealthyPoolIsKept() throws Exception {
File pack = pack("structure-ok");
CompatTestPlatform.writeObject(pack, "good", "minecraft:stone");
CompatTestPlatform.write(pack, "jigsaw-pieces/good.json", "{\"object\":\"good\"}");
CompatTestPlatform.write(pack, "jigsaw-pools/pool.json",
"{\"pieces\":[{\"piece\":\"good\",\"weight\":1}]}");
CompatTestPlatform.write(pack, "structures/keep.json", "{\"startPool\":\"pool\"}");
data = IrisData.get(pack);
IrisStructure structure = data.load(IrisStructure.class, "keep", false);
assertNotNull(structure);
assertFalse(structure.isCompatExcluded());
assertTrue(data.getCompatReport().isEmpty());
}
private File pack(String name) throws Exception {
return temporaryFolder.newFolder(name);
}
}
@@ -0,0 +1,101 @@
package art.arcane.iris.engine.object;
import art.arcane.iris.core.compat.CompatAction;
import art.arcane.iris.core.compat.CompatFinding;
import art.arcane.iris.core.compat.CompatRegistry;
import art.arcane.iris.core.compat.CompatStatus;
import art.arcane.iris.core.compat.ContentGate;
import art.arcane.iris.core.compat.PackCompatReport;
import art.arcane.iris.core.loader.IrisData;
import art.arcane.iris.core.loader.ResourceLoader;
import art.arcane.volmlib.util.collection.KList;
import org.junit.Test;
import java.util.List;
import java.util.Map;
import static art.arcane.iris.engine.object.CompatFixtures.find;
import static art.arcane.iris.engine.object.CompatFixtures.lootTable;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
public class IrisLootTableCompatTest {
@SuppressWarnings("unchecked")
private static IrisData dataWith(PackCompatReport report, IrisLootTable... tables) {
IrisData data = CompatFixtures.data(report);
ResourceLoader<IrisLootTable> lootLoader = mock(ResourceLoader.class);
when(data.getLootLoader()).thenReturn(lootLoader);
for (IrisLootTable table : tables) {
when(lootLoader.load(table.getLoadKey())).thenReturn(table);
}
return data;
}
private static ContentGate gate(PackCompatReport report) {
return new ContentGate(null, Map.of(), report);
}
@Test
public void evaluateCompatExcludesTableWhoseEntriesWereAllDropped() {
PackCompatReport report = new PackCompatReport();
IrisLootTable table = lootTable("chest/sulfur-cache");
table.setLoader(CompatFixtures.data(report));
// The gate walker drops every entry with a missing item and hands the cascade its reasons.
CompatStatus walked = new CompatStatus(false, List.of(new CompatFinding(CompatRegistry.ITEM,
"minecraft:sulfur_dust", CompatAction.DROPPED, "loot", "chest/sulfur-cache", "loot[0]")));
CompatStatus status = table.cascadeEmptyLoot(walked);
assertTrue(status.excluded());
CompatFinding cascade = find(report, CompatAction.EXCLUDED, "loot", "chest/sulfur-cache");
assertNotNull(cascade);
assertEquals("minecraft:sulfur_dust", cascade.key());
assertEquals(CompatRegistry.ITEM, cascade.registry());
assertEquals("no loot entries remain", cascade.detail());
}
@Test
public void evaluateCompatKeepsTableWithSurvivingEntries() {
PackCompatReport report = new PackCompatReport();
IrisLootTable table = lootTable("chest/mixed");
table.setLoader(CompatFixtures.data(report));
table.setLoot(new KList<>(new IrisLoot()));
CompatStatus walked = new CompatStatus(false, List.of(new CompatFinding(CompatRegistry.ITEM,
"minecraft:sulfur_dust", CompatAction.DROPPED, "loot", "chest/mixed", "loot[1]")));
assertFalse(table.cascadeEmptyLoot(walked).excluded());
}
@Test
public void evaluateCompatKeepsTableThatNeverDeclaredLoot() {
PackCompatReport report = new PackCompatReport();
IrisLootTable table = lootTable("chest/empty");
table.setLoader(CompatFixtures.data(report));
assertFalse(table.evaluateCompat(gate(report)).excluded());
assertTrue(report.isEmpty());
}
@Test
public void lootReferenceSkipsExcludedAndUnloadableTables() {
PackCompatReport report = new PackCompatReport();
IrisLootTable kept = lootTable("chest/common");
IrisLootTable excluded = CompatFixtures.exclude(lootTable("chest/sulfur-cache"),
CompatRegistry.ITEM, "minecraft:sulfur_dust");
IrisData data = dataWith(report, kept, excluded);
IrisLootReference reference = new IrisLootReference()
.setTables(new KList<>("chest/common", "chest/sulfur-cache", "chest/gone"));
KList<IrisLootTable> tables = reference.getLootTables(() -> data);
assertEquals(1, tables.size());
assertEquals("chest/common", tables.get(0).getLoadKey());
CompatFinding dropped = find(report, CompatAction.DROPPED, "loot reference", "chest/sulfur-cache");
assertNotNull(dropped);
assertEquals("minecraft:sulfur_dust", dropped.key());
}
}
@@ -0,0 +1,282 @@
/*
* Iris is a World Generator for Minecraft Bukkit Servers
* Copyright (c) 2026 Arcane Arts (Volmit Software)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package art.arcane.iris.engine.object;
import art.arcane.iris.core.IrisSettings;
import art.arcane.iris.core.compat.CompatAction;
import art.arcane.iris.core.compat.CompatFinding;
import art.arcane.iris.core.loader.IrisData;
import art.arcane.iris.util.common.data.DataProvider;
import art.arcane.volmlib.util.collection.KList;
import art.arcane.volmlib.util.math.RNG;
import org.junit.After;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TemporaryFolder;
import java.io.File;
import java.util.List;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
public class IrisObjectPlacementCompatTest {
@Rule
public TemporaryFolder temporaryFolder = new TemporaryFolder();
private CompatTestPlatform platform;
private IrisSettings previousSettings;
private IrisData data;
@Before
public void bindPlatform() {
previousSettings = IrisSettings.settings;
IrisSettings.settings = new IrisSettings();
platform = CompatTestPlatform.bind(temporaryFolder.getRoot(),
List.of("minecraft:stone", "minecraft:cobblestone", "minecraft:air", "minecraft:oak_log"));
}
@After
public void restorePlatform() {
if (data != null) {
data.close();
data = null;
}
if (platform != null) {
platform.unbind();
platform = null;
}
IrisSettings.settings = previousSettings;
}
@Test
public void missingPaletteKeyDropsTheObjectAndRecordsAFinding() throws Exception {
File pack = pack("drop");
CompatTestPlatform.writeObject(pack, "good", "minecraft:stone");
CompatTestPlatform.writeObject(pack, "bad", "minecraft:stone", "minecraft:sulfur");
data = IrisData.get(pack);
IrisObjectPlacement placement = placement("good", "bad");
assertFalse(placement.isCompatExcluded(data));
assertEquals(new KList<String>().qadd("good"), placement.compatPlace(data));
List<CompatFinding> findings = data.getCompatReport().findings();
assertEquals(findings.toString(), 1, findings.size());
assertEquals("minecraft:sulfur", findings.getFirst().key());
assertEquals(CompatAction.DROPPED, findings.getFirst().action());
assertEquals("object", findings.getFirst().subjectType());
assertEquals("bad", findings.getFirst().subjectKey());
}
@Test
public void editRuleWithFullChanceKeepsTheObject() throws Exception {
File pack = pack("edit-keeps");
CompatTestPlatform.writeObject(pack, "bad", "minecraft:sulfur");
data = IrisData.get(pack);
IrisObjectPlacement placement = placement("bad");
placement.getEdit().add(replace(1F, false, "minecraft:sulfur", "minecraft:stone"));
assertFalse(placement.isCompatExcluded(data));
assertEquals(new KList<String>().qadd("bad"), placement.compatPlace(data));
assertTrue(data.getCompatReport().findings().toString(), data.getCompatReport().isEmpty());
}
@Test
public void editRuleBelowFullChanceDoesNotCoverTheMissingKey() throws Exception {
File pack = pack("edit-chance");
CompatTestPlatform.writeObject(pack, "bad", "minecraft:sulfur");
data = IrisData.get(pack);
IrisObjectPlacement placement = placement("bad");
placement.getEdit().add(replace(0.5F, false, "minecraft:sulfur", "minecraft:stone"));
assertTrue(placement.isCompatExcluded(data));
assertTrue(placement.compatPlace(data).isEmpty());
}
@Test
public void exactEditRuleOnlyCoversTheMatchingState() throws Exception {
File pack = pack("edit-exact");
CompatTestPlatform.writeObject(pack, "axisY", "minecraft:sulfur[axis=y]");
CompatTestPlatform.writeObject(pack, "axisX", "minecraft:sulfur[axis=x]");
data = IrisData.get(pack);
IrisObjectPlacement placement = placement("axisY", "axisX");
IrisObjectReplace rule = replace(1F, true, "minecraft:sulfur", "minecraft:stone");
rule.getFind().getFirst().getData().put("axis", "y");
placement.getEdit().add(rule);
assertEquals(new KList<String>().qadd("axisY"), placement.compatPlace(data));
}
@Test
public void nonExactEditRuleCoversEveryStateOfTheSameBlock() throws Exception {
File pack = pack("edit-non-exact");
CompatTestPlatform.writeObject(pack, "axisX", "minecraft:sulfur[axis=x]");
data = IrisData.get(pack);
IrisObjectPlacement placement = placement("axisX");
placement.getEdit().add(replace(1F, false, "minecraft:sulfur", "minecraft:stone"));
assertFalse(placement.isCompatExcluded(data));
}
@Test
public void missingReplacePaletteOptionExcludesTheWholePlacement() throws Exception {
File pack = pack("replace-missing");
CompatTestPlatform.writeObject(pack, "good", "minecraft:stone");
data = IrisData.get(pack);
IrisObjectPlacement placement = placement("good");
placement.getEdit().add(replace(1F, false, "minecraft:stone", "minecraft:sulfur"));
assertTrue(placement.isCompatExcluded(data));
assertTrue(placement.compatPlace(data).isEmpty());
List<CompatFinding> findings = data.getCompatReport().findings();
assertEquals(findings.toString(), 1, findings.size());
assertEquals(CompatAction.EXCLUDED, findings.getFirst().action());
assertEquals("placement", findings.getFirst().subjectType());
assertTrue(findings.getFirst().detail(), findings.getFirst().detail().contains("edit[0].replace.palette[0]"));
}
@Test
public void missingFindKeyOnTheMatchSideIsIgnored() throws Exception {
File pack = pack("find-missing");
CompatTestPlatform.writeObject(pack, "good", "minecraft:stone");
data = IrisData.get(pack);
IrisObjectPlacement placement = placement("good");
placement.getEdit().add(replace(1F, false, "minecraft:sulfur", "minecraft:cobblestone"));
assertFalse(placement.isCompatExcluded(data));
assertTrue(data.getCompatReport().findings().toString(), data.getCompatReport().isEmpty());
}
@Test
public void missingMarkerBlockIsMatchOnly() throws Exception {
File pack = pack("marker-missing");
CompatTestPlatform.writeObject(pack, "good", "minecraft:stone");
data = IrisData.get(pack);
IrisObjectPlacement placement = placement("good");
IrisObjectMarker marker = new IrisObjectMarker();
marker.setMarker("thing");
marker.getMark().add(new IrisBlockData("minecraft:sulfur"));
placement.getMarkers().add(marker);
assertFalse(placement.isCompatExcluded(data));
assertEquals(new KList<String>().qadd("good"), placement.compatPlace(data));
assertTrue(data.getCompatReport().isEmpty());
}
@Test
public void dimensionFallbackRescuesTheObjectAndRecordsASubstitution() throws Exception {
File pack = pack("fallback");
CompatTestPlatform.write(pack, "dimensions/main.json",
"{\"blockFallbacks\":{\"minecraft:sulfur\":\"minecraft:cobblestone\"}}");
CompatTestPlatform.writeObject(pack, "bad", "minecraft:sulfur");
data = IrisData.get(pack);
IrisObjectPlacement placement = placement("bad");
assertFalse(placement.isCompatExcluded(data));
assertEquals(new KList<String>().qadd("bad"), placement.compatPlace(data));
List<CompatFinding> findings = data.getCompatReport().findings();
assertEquals(findings.toString(), 1, findings.size());
assertEquals(CompatAction.SUBSTITUTED, findings.getFirst().action());
}
@Test
public void blockBackupRescuesAReplacePaletteEntry() throws Exception {
File pack = pack("backup");
CompatTestPlatform.writeObject(pack, "good", "minecraft:stone");
data = IrisData.get(pack);
IrisObjectPlacement placement = placement("good");
IrisObjectReplace rule = replace(1F, false, "minecraft:stone", "minecraft:sulfur");
rule.getReplace().getPalette().getFirst().setBackup(new IrisBlockData("minecraft:cobblestone"));
placement.getEdit().add(rule);
assertFalse(placement.isCompatExcluded(data));
assertEquals(CompatAction.SUBSTITUTED, data.getCompatReport().findings().getFirst().action());
}
@Test
public void everyObjectDroppedExcludesThePlacement() throws Exception {
File pack = pack("all-dropped");
CompatTestPlatform.writeObject(pack, "bad", "minecraft:sulfur");
data = IrisData.get(pack);
IrisObjectPlacement placement = placement("bad");
assertTrue(placement.isCompatExcluded(data));
assertTrue(placement.compatPlace(data).isEmpty());
assertTrue(data.getCompatReport().findings().stream()
.anyMatch(finding -> finding.action() == CompatAction.EXCLUDED
&& "placement".equals(finding.subjectType())));
}
@Test
public void survivingPoolKeepsAuthoredOrderAndGetObjectOnlyDrawsSurvivors() throws Exception {
File pack = pack("draw");
CompatTestPlatform.writeObject(pack, "a", "minecraft:stone");
CompatTestPlatform.writeObject(pack, "bad", "minecraft:sulfur");
CompatTestPlatform.writeObject(pack, "b", "minecraft:cobblestone");
data = IrisData.get(pack);
IrisObjectPlacement placement = placement("a", "bad", "b");
assertEquals(new KList<String>().qadd("a").qadd("b"), placement.compatPlace(data));
IrisData loaded = data;
DataProvider provider = () -> loaded;
for (int seed = 0; seed < 64; seed++) {
IrisObject drawn = placement.getObject(provider, new RNG(seed));
assertTrue("Draw must never return the dropped object",
drawn != null && !"bad".equals(drawn.getLoadKey()));
}
}
@Test
public void aCompletePackLeavesThePoolIdentical() throws Exception {
File pack = pack("complete");
CompatTestPlatform.writeObject(pack, "a", "minecraft:stone");
CompatTestPlatform.writeObject(pack, "b", "minecraft:cobblestone");
data = IrisData.get(pack);
IrisObjectPlacement placement = placement("a", "b");
assertTrue("A complete pack must hand back the authored pool untouched",
placement.getPlace() == placement.compatPlace(data));
assertTrue(data.getCompatReport().isEmpty());
}
private File pack(String name) throws Exception {
return temporaryFolder.newFolder(name);
}
private static IrisObjectPlacement placement(String... place) {
IrisObjectPlacement placement = new IrisObjectPlacement();
placement.setPlace(new KList<>(place));
return placement;
}
private static IrisObjectReplace replace(float chance, boolean exact, String find, String to) {
IrisObjectReplace rule = new IrisObjectReplace();
rule.setChance(chance);
rule.setExact(exact);
rule.getFind().add(new IrisBlockData(find));
rule.getReplace().qclear();
rule.getReplace().add(to);
return rule;
}
}
@@ -0,0 +1,142 @@
package art.arcane.iris.engine.object;
import art.arcane.iris.core.compat.CompatAction;
import art.arcane.iris.core.compat.CompatFinding;
import art.arcane.iris.core.compat.CompatRegistry;
import art.arcane.iris.core.compat.CompatStatus;
import art.arcane.iris.core.compat.ContentGate;
import art.arcane.iris.core.compat.PackCompatReport;
import art.arcane.iris.core.loader.IrisData;
import art.arcane.iris.core.loader.ResourceLoader;
import art.arcane.volmlib.util.collection.KList;
import org.junit.Test;
import java.util.Map;
import static art.arcane.iris.engine.object.CompatFixtures.MISSING_BLOCK;
import static art.arcane.iris.engine.object.CompatFixtures.biome;
import static art.arcane.iris.engine.object.CompatFixtures.excludeBlock;
import static art.arcane.iris.engine.object.CompatFixtures.find;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
public class IrisRegionCompatPoolTest {
@SuppressWarnings("unchecked")
private static IrisData dataWith(PackCompatReport report, IrisBiome... biomes) {
IrisData data = CompatFixtures.data(report);
ResourceLoader<IrisBiome> biomeLoader = mock(ResourceLoader.class);
when(data.getBiomeLoader()).thenReturn(biomeLoader);
for (IrisBiome biome : biomes) {
when(biomeLoader.load(biome.getLoadKey())).thenReturn(biome);
}
return data;
}
private static ContentGate gate(PackCompatReport report) {
return new ContentGate(null, Map.of(), report);
}
@Test
public void realLandBiomesDropsExcludedReferenceAndRecordsFinding() {
PackCompatReport report = new PackCompatReport();
IrisBiome plains = biome("land/plains");
IrisBiome sulfur = excludeBlock(biome("cave/sulfur-grotto"));
IrisData data = dataWith(report, plains, sulfur);
IrisRegion region = CompatFixtures.region("overworld");
region.setLoader(data);
region.setLandBiomes(new KList<>("land/plains", "cave/sulfur-grotto"));
KList<IrisBiome> pool = region.getRealLandBiomes(() -> data);
assertEquals(1, pool.size());
assertEquals("land/plains", pool.get(0).getLoadKey());
CompatFinding dropped = find(report, CompatAction.DROPPED, "region", "overworld");
assertNotNull(dropped);
assertEquals(CompatRegistry.BLOCK, dropped.registry());
assertEquals(MISSING_BLOCK, dropped.key());
assertEquals("landBiomes[1] cave/sulfur-grotto", dropped.detail());
}
@Test
public void realPoolsSkipUnloadableReferences() {
PackCompatReport report = new PackCompatReport();
IrisBiome shore = biome("shore/sand");
IrisData data = dataWith(report, shore);
IrisRegion region = CompatFixtures.region("overworld");
region.setLoader(data);
region.setSeaBiomes(new KList<>("sea/gone"));
region.setShoreBiomes(new KList<>("shore/sand"));
region.setCaveBiomes(new KList<>("cave/gone"));
assertTrue(region.getRealSeaBiomes(() -> data).isEmpty());
assertEquals(1, region.getRealShoreBiomes(() -> data).size());
assertTrue(region.getRealCaveBiomes(() -> data).isEmpty());
assertTrue(report.isEmpty());
}
@Test
public void naturalBiomesSkipExcludedBiomesAndTheirChildren() {
PackCompatReport report = new PackCompatReport();
IrisBiome plains = biome("land/plains");
IrisBiome sulfur = excludeBlock(biome("cave/sulfur-grotto"));
sulfur.setChildren(new KList<>("cave/sulfur-deep"));
IrisBiome sulfurDeep = biome("cave/sulfur-deep");
IrisData data = dataWith(report, plains, sulfur, sulfurDeep);
IrisRegion region = CompatFixtures.region("overworld");
region.setLoader(data);
region.setLandBiomes(new KList<>("land/plains"));
region.setCaveBiomes(new KList<>("cave/sulfur-grotto"));
KList<IrisBiome> natural = region.getNaturalBiomes(() -> data);
assertEquals(1, natural.size());
assertEquals("land/plains", natural.get(0).getLoadKey());
}
@Test
public void evaluateCompatExcludesRegionWithNoLandBiomesLeft() {
PackCompatReport report = new PackCompatReport();
IrisBiome sulfur = excludeBlock(biome("cave/sulfur-flats"));
IrisData data = dataWith(report, sulfur);
IrisRegion region = CompatFixtures.region("sulfur-lands");
region.setLoader(data);
region.setLandBiomes(new KList<>("cave/sulfur-flats"));
CompatStatus status = region.evaluateCompat(gate(report));
assertTrue(status.excluded());
CompatFinding cascade = find(report, CompatAction.EXCLUDED, "region", "sulfur-lands");
assertNotNull(cascade);
assertEquals(MISSING_BLOCK, cascade.key());
assertEquals(CompatRegistry.BLOCK, cascade.registry());
assertEquals("no land biomes remain", cascade.detail());
}
@Test
public void evaluateCompatKeepsRegionWithSurvivingLandBiome() {
PackCompatReport report = new PackCompatReport();
IrisBiome plains = biome("land/plains");
IrisBiome sulfur = excludeBlock(biome("cave/sulfur-flats"));
IrisData data = dataWith(report, plains, sulfur);
IrisRegion region = CompatFixtures.region("overworld");
region.setLoader(data);
region.setLandBiomes(new KList<>("land/plains", "cave/sulfur-flats"));
assertFalse(region.evaluateCompat(gate(report)).excluded());
}
@Test
public void evaluateCompatDoesNotExcludeRegionThatNeverDeclaredLandBiomes() {
PackCompatReport report = new PackCompatReport();
IrisData data = dataWith(report);
IrisRegion region = CompatFixtures.region("empty");
region.setLoader(data);
assertFalse(region.evaluateCompat(gate(report)).excluded());
assertTrue(report.isEmpty());
}
}
@@ -0,0 +1,126 @@
package art.arcane.iris.engine.object;
import art.arcane.iris.core.compat.CompatAction;
import art.arcane.iris.core.compat.CompatFinding;
import art.arcane.iris.core.compat.CompatRegistry;
import art.arcane.iris.core.compat.CompatStatus;
import art.arcane.iris.core.compat.ContentGate;
import art.arcane.iris.core.compat.PackCompatReport;
import art.arcane.iris.core.loader.IrisData;
import art.arcane.iris.core.loader.ResourceLoader;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.volmlib.util.collection.KList;
import org.junit.Test;
import java.util.Map;
import static art.arcane.iris.engine.object.CompatFixtures.MISSING_ENTITY;
import static art.arcane.iris.engine.object.CompatFixtures.entity;
import static art.arcane.iris.engine.object.CompatFixtures.excludeEntity;
import static art.arcane.iris.engine.object.CompatFixtures.find;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertTrue;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
public class IrisSpawnerCompatTest {
@SuppressWarnings("unchecked")
private static IrisData dataWith(PackCompatReport report, IrisEntity... entities) {
IrisData data = CompatFixtures.data(report);
ResourceLoader<IrisEntity> entityLoader = mock(ResourceLoader.class);
when(data.getEntityLoader()).thenReturn(entityLoader);
for (IrisEntity entity : entities) {
when(entityLoader.load(entity.getLoadKey())).thenReturn(entity);
}
return data;
}
private static ContentGate gate(PackCompatReport report) {
return new ContentGate(null, Map.of(), report);
}
@Test
public void evaluateCompatDropsSpawnsWhoseEntityIsExcluded() {
PackCompatReport report = new PackCompatReport();
IrisEntity zombie = entity("zombie");
IrisEntity camel = excludeEntity(entity("camel"));
IrisData data = dataWith(report, zombie, camel);
IrisSpawner spawner = new IrisSpawner();
spawner.setLoadKey("desert");
spawner.setLoader(data);
spawner.setSpawns(new KList<>(
new IrisEntitySpawn().setEntity("zombie"),
new IrisEntitySpawn().setEntity("camel")));
CompatStatus status = spawner.evaluateCompat(gate(report));
assertFalse(status.excluded());
assertEquals(1, spawner.getSpawns().size());
assertEquals("zombie", spawner.getSpawns().get(0).getEntity());
CompatFinding dropped = find(report, CompatAction.DROPPED, "spawner", "desert");
assertNotNull(dropped);
assertEquals(CompatRegistry.ENTITY, dropped.registry());
assertEquals(MISSING_ENTITY, dropped.key());
assertEquals("spawns[1] camel", dropped.detail());
}
@Test
public void evaluateCompatExcludesSpawnerWithNoSpawnsLeft() {
PackCompatReport report = new PackCompatReport();
IrisEntity camel = excludeEntity(entity("camel"));
IrisData data = dataWith(report, camel);
IrisSpawner spawner = new IrisSpawner();
spawner.setLoadKey("desert");
spawner.setLoader(data);
spawner.setSpawns(new KList<>(new IrisEntitySpawn().setEntity("camel")));
spawner.setInitialSpawns(new KList<>(new IrisEntitySpawn().setEntity("camel")));
CompatStatus status = spawner.evaluateCompat(gate(report));
assertTrue(status.excluded());
assertTrue(spawner.getSpawns().isEmpty());
assertTrue(spawner.getInitialSpawns().isEmpty());
CompatFinding cascade = find(report, CompatAction.EXCLUDED, "spawner", "desert");
assertNotNull(cascade);
assertEquals(MISSING_ENTITY, cascade.key());
assertEquals("no entity spawns remain", cascade.detail());
}
@Test
public void evaluateCompatKeepsSpawnerThatNeverDeclaredSpawns() {
PackCompatReport report = new PackCompatReport();
IrisData data = dataWith(report);
IrisSpawner spawner = new IrisSpawner();
spawner.setLoadKey("empty");
spawner.setLoader(data);
assertFalse(spawner.evaluateCompat(gate(report)).excluded());
assertTrue(report.isEmpty());
}
@Test
public void entitySpawnResolvesNoEntityWhenExcluded() {
PackCompatReport report = new PackCompatReport();
IrisEntity camel = excludeEntity(entity("camel"));
IrisData data = dataWith(report, camel);
Engine engine = mock(Engine.class);
when(engine.getData()).thenReturn(data);
assertNull(new IrisEntitySpawn().setEntity("camel").getRealEntity(engine));
}
@Test
public void entitySpawnResolvesPresentEntity() {
PackCompatReport report = new PackCompatReport();
IrisEntity zombie = entity("zombie");
IrisData data = dataWith(report, zombie);
Engine engine = mock(Engine.class);
when(engine.getData()).thenReturn(data);
assertSame(zombie, new IrisEntitySpawn().setEntity("zombie").getRealEntity(engine));
}
}