Merge feature/version-compat: gate pack content on the running Minecraft version

- Every registrant is evaluated on load (IrisData.preprocessObject ->
  IrisRegistrant.evaluateCompat) by a cached per-class reflective walker over the
  annotated key fields; the verdict is stored on the registrant and pool sites filter
  with isCompatExcluded(). Block keys resolve through the live registry, the legacy
  rename table, IrisDimension.blockFallbacks and the entry backup, in that order.
- Unit of exclusion is the compat unit that composes the missing content (registrant
  or object placement). A referencing unit drops the reference; an emptied required
  pool excludes the container (CompatPools cascade through regions, biomes, dimension,
  spawners, loot tables, jigsaw pools and structures). A cascade that reaches the
  dimension is a blocking validation error and world creation is refused.
- Object placements are evaluated per object from the .iob palette header: an object
  is excluded from a placement's pool unless a chance-1 type-replace (edit rule
  matching the key) or a fallback covers the key. edit find lists and marker mark
  lists are match-only. Missing palette keys stay MissingBlockState placeholders
  (air everywhere, real air nativeHandle) so a chance-1 edit rule can still rewrite
  them at placement time.
- Approved decisions: exclude the composing unit; console-only reporting; no version
  fields on packs and no cross-version pre-check (authors build on their version,
  older servers gate at load).
- IrisDimension.blockFallbacks (base key -> full block state) applies to every
  palette, decorator, deposit, replace rule and object in the pack.
- IrisBlockData.backup is revived on every platform (it was dead on Bukkit because
  B.getState returned air).
- The legacy rename table now lives in LegacyBlockRenames and is applied identically
  on Bukkit and the mod loaders.

Operator surface
- Boot listing per pack after the validation lines (IrisWorldGeneratorResolver on
  Bukkit, ModdedStartup on modded), grouped by key, capped at three subjects per key.
- Validation summary line on /iris pack validate and /iris pack status, findings
  persisted in PackValidationResult through PackValidationCache.
- One summary line per world engine when its pack has findings.
- /iris pack compat [pack] (alias cp) on Bukkit and modded, uncapped, from the
  persisted validation so no pack is reloaded; catalog keys in all 17 locales.

Verification on the branch
- Full build green in the .compatwt worktree; modded adapters compile.
- Golden hash unchanged on Paper 26.2, seed 1337, radius 8.
- Live boots on Paper 26.1.2 and 26.2: the stock Overworld lists 6 unavailable keys
  on 26.1.2 and none on 26.2; the world creates and generates without the excluded
  content on 26.1.2.

Integration on top of master (df34f328e..fc3a26dd9)
- Conflicts: PackValidator keeps both import sets (JSONObject for static object
  validation plus the compat and loader types); RiverPolicyResolver.State.apply takes
  the confinement scope and the pack data (confined rivers and gate-filtered river
  biome selections); IrisRiverPolicy keeps shoreBiomeWidth and confined next to
  compatBiomes.
- Static objects honour the gate. The per-object palette evaluation moved out of
  IrisObjectPlacement into the shared IrisObjectCompat helper; IrisStaticObject
  .evaluateCompat mirrors a one-object placement, records findings with subject type
  "static object" and the object key (DROPPED for uncovered palette keys, EXCLUDED
  for a missing edit replace palette), IrisStaticObjectLayer.compile skips gated
  entries instead of failing, and PackValidator.gateContent asks every dimension for
  its static objects (IrisDimension.evaluateStaticObjectsCompat) so the boot listing
  is complete. CompatWalker treats IrisStaticObject like IrisObjectPlacement: never
  descended, so a static object's edit palette excludes only that entry, never the
  dimension. Covered by IrisStaticObjectCompatTest.
- The modded compat and cp subcommands run inside ModdedCommandTree.localized like
  the rest of the pack tree and are documented in the modded help; the parity test
  asserts both.
- Locale keys stay in core/src/main/resources/languages: master's on-demand download
  reads those files from the repository and the build only keeps them out of the jar,
  so the compat keys sit next to the debugdump keys in the same form.
- Placeholder audit of master's new code: the only native casts added are the
  isFaceSturdy fallbacks in IrisSpeleothems and ModdedDecoratorHooks, both behind
  B.isAir guards, and MissingBlockState.nativeHandle is the real air anyway;
  IrisObjectTransforms only asks B.isAir; decorators resolve palettes through
  IrisBlockData.getBlockData, which never yields a placeholder. No new guards needed.
- core/purity-allowlist.txt gains IrisSpeleothems: 5053a644c split the Bukkit
  isFaceSturdy fallback out of the already allowlisted DecoratorCore without moving
  the allowlist entry, so master's own build failed the Bukkit purity ratchet.
- CompatTestPlatform states now answer isAir, isSolid, isFluid and materialKey like a
  real adapter: B caches the first registries it sees per JVM, so an interned mock
  air that never said isAir leaked into the core.compat tests once the static layer
  ran under that platform.

Gate commands on the merged tree
- ./gradlew build (default VolmLib composite, ../VolmLib at 7b8029e): BUILD SUCCESSFUL;
  core 3499 (8 skipped) tests, bukkit plugin 343 tests, nms v26_2_R1 255 (1 skipped) tests,
  probe 78 tests, buildSrc 10 tests, 0 failures.
- cd adapters/fabric && ../../gradlew compileJava: BUILD SUCCESSFUL (:compileJava executed)
- cd adapters/forge && ../../gradlew compileJava: BUILD SUCCESSFUL (:compileJava executed)
- cd adapters/neoforge && ../../gradlew compileJava: BUILD SUCCESSFUL (:compileJava executed)
- cd adapters/forge && ../../gradlew test --tests IrisModdedCommandParityTest --tests
  ModdedLanguageCommandsTest: BUILD SUCCESSFUL, 11 tests, 0 failures.
This commit is contained in:
Brian Neumann-Fopiano committed 2026-09-03 10:48:37 -04:00
commit e4ca7d4e70
112 files changed
+8037 -295

No files matched your search

@@ -0,0 +1,410 @@
package art.arcane.iris.core.compat;
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.LogLevel;
import art.arcane.iris.spi.PlatformBiome;
import art.arcane.iris.spi.PlatformBiomeWriter;
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 art.arcane.iris.spi.PlatformScheduler;
import art.arcane.iris.spi.PlatformStructureHooks;
import art.arcane.iris.spi.PlatformWorld;
import java.io.File;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ExecutorService;
/** Registry fakes for the gate tests: every block is present unless listed as missing; other registries are explicit. */
final class CompatFixtures {
private CompatFixtures() {
}
static FakeRegistries registries(String... missingBlocks) {
FakeRegistries registries = new FakeRegistries();
registries.missingBlocks.addAll(List.of(missingBlocks));
return registries;
}
static FakePlatform bind(FakeRegistries registries) {
IrisPlatforms.unbind();
FakePlatform platform = new FakePlatform(registries);
IrisPlatforms.bind(platform);
IrisServices.register(PreservationRegistry.class, new NoPreservation());
return platform;
}
static void unbind() {
IrisPlatforms.unbind();
IrisServices.remove(PreservationRegistry.class);
}
/** Loaders register their caches for shutdown; nothing to preserve in a test JVM. */
private static final class NoPreservation implements PreservationRegistry {
@Override
public void register(Thread thread) {
}
@Override
public void register(ExecutorService service) {
}
@Override
public void registerCache(MeteredCache cache) {
}
@Override
public void dereference() {
}
}
static final class FakeRegistries implements PlatformRegistries {
final Set<String> missingBlocks = new HashSet<>();
final List<String> blockKeys = new ArrayList<>(List.of("minecraft:stone", "minecraft:air", "minecraft:sand"));
final Set<String> items = new HashSet<>(List.of("minecraft:diamond", "minecraft:stone"));
final Set<String> entities = new HashSet<>(List.of("minecraft:cow", "minecraft:zombie"));
final Set<String> biomes = new HashSet<>(List.of("minecraft:plains", "minecraft:the_void", "minecraft:desert"));
final Set<String> structures = new HashSet<>(List.of("minecraft:village_plains"));
final Set<String> enchantments = new HashSet<>(List.of("minecraft:sharpness"));
final Set<String> potionEffects = new HashSet<>(List.of("minecraft:speed", "minecraft:strength"));
int blockLookups = 0;
@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) {
blockLookups++;
String normalized = key.trim().toLowerCase(Locale.ROOT);
int props = normalized.indexOf('[');
String base = props < 0 ? normalized : normalized.substring(0, props);
if (base.indexOf(':') < 0) {
base = "minecraft:" + base;
normalized = base + (props < 0 ? "" : normalized.substring(props));
}
if (missingBlocks.contains(base)) {
return null;
}
return new FakeBlockState(normalized);
}
@Override
public PlatformBlockState air() {
return new FakeBlockState("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 blockKeys;
}
@Override
public List<String> biomeKeys() {
return new ArrayList<>(biomes);
}
@Override
public List<String> structureKeys() {
return new ArrayList<>(structures);
}
@Override
public List<String> itemKeys() {
return new ArrayList<>(items);
}
@Override
public List<String> entityKeys() {
return new ArrayList<>(entities);
}
@Override
public List<String> blockTypeKeys() {
return blockKeys;
}
@Override
public List<String> enchantmentKeys() {
return new ArrayList<>(enchantments);
}
@Override
public List<String> potionEffectKeys() {
return new ArrayList<>(potionEffects);
}
@Override
public List<String> lootTableKeys() {
return List.of();
}
@Override
public Map<String, List<PlatformBlockProperty>> blockStateProperties() {
return Map.of();
}
}
record FakeBlockState(String key) implements PlatformBlockState {
private static final Map<String, Object> HANDLES = new java.util.concurrent.ConcurrentHashMap<>();
@Override
public String namespace() {
return key.substring(0, key.indexOf(':'));
}
@Override
public boolean isAir() {
return key.equals("minecraft:air");
}
@Override
public boolean isSolid() {
return !isAir();
}
@Override
public boolean isOccluding() {
return false;
}
@Override
public boolean isCustom() {
return false;
}
@Override
public boolean isFluid() {
return false;
}
@Override
public boolean isWater() {
return false;
}
@Override
public boolean isWaterLogged() {
return false;
}
@Override
public boolean isLit() {
return false;
}
@Override
public boolean isUpdatable() {
return false;
}
@Override
public boolean isFoliage() {
return false;
}
@Override
public boolean isTreeBlock() {
return false;
}
@Override
public boolean isFoliagePlantable() {
return false;
}
@Override
public boolean isDecorant() {
return false;
}
@Override
public boolean isStorage() {
return false;
}
@Override
public boolean isStorageChest() {
return false;
}
@Override
public boolean isOre() {
return false;
}
@Override
public boolean isDeepSlate() {
return false;
}
@Override
public boolean isVineBlock() {
return false;
}
@Override
public boolean canPlaceOnto(PlatformBlockState onto) {
return true;
}
@Override
public boolean matches(PlatformBlockState state) {
return state != null && key.equals(state.key());
}
@Override
public boolean hasTileEntity() {
return false;
}
@Override
public PlatformBlockState withProperty(String name, String value) {
return this;
}
/** One mocked {@code BlockData} per key: the Bukkit rotation path casts the handle before it inspects it. */
@Override
public Object nativeHandle() {
return HANDLES.computeIfAbsent(key, ignored -> org.mockito.Mockito.mock(org.bukkit.block.data.BlockData.class));
}
}
static final class FakePlatform implements IrisPlatform {
private final PlatformRegistries registries;
private final File dataFolder;
FakePlatform(PlatformRegistries registries) {
this.registries = registries;
File folder;
try {
folder = java.nio.file.Files.createTempDirectory("iris-compat-platform").toFile();
} catch (java.io.IOException e) {
folder = new File(System.getProperty("java.io.tmpdir"), "iris-compat-platform");
}
this.dataFolder = folder;
}
@Override
public String platformName() {
return "fake";
}
@Override
public String minecraftVersion() {
return "26.1.2";
}
@Override
public PlatformRegistries registries() {
return registries;
}
@Override
public PlatformScheduler scheduler() {
return null;
}
@Override
public PlatformStructureHooks structureHooks() {
return null;
}
@Override
public PlatformBiomeWriter biomeWriter() {
return null;
}
@Override
public File dataFolder() {
return dataFolder;
}
@Override
public File dataFile(String... path) {
return new File(dataFolder, String.join(File.separator, path));
}
@Override
public File pluginJar() {
return new File(".");
}
@Override
public int irisVersionNumber() {
return 0;
}
@Override
public int minecraftVersionNumber() {
return 0;
}
@Override
public void callEvent(Object event) {
}
@Override
public void dispatchConsoleCommand(String command) {
}
@Override
public boolean spawnEntity(PlatformWorld world, String entityKey, double x, double y, double z) {
return false;
}
@Override
public void log(LogLevel level, String message) {
if (level == LogLevel.WARN || level == LogLevel.ERROR) {
System.err.println("[compat-test " + level + "] " + message);
}
}
@Override
public void msg(String message) {
}
@Override
public void reportError(Throwable error) {
error.printStackTrace(System.err);
}
}
}
@@ -0,0 +1,256 @@
package art.arcane.iris.core.compat;
import art.arcane.iris.core.loader.IrisData;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.object.IObjectPlacer;
import art.arcane.iris.engine.object.IrisBlockData;
import art.arcane.iris.engine.object.IrisObject;
import art.arcane.iris.engine.object.IrisObjectPlacement;
import art.arcane.iris.engine.object.IrisObjectReplace;
import art.arcane.iris.engine.object.ObjectPlaceMode;
import art.arcane.iris.engine.object.TileData;
import art.arcane.iris.spi.PlatformBlockState;
import art.arcane.volmlib.util.collection.KList;
import art.arcane.volmlib.util.math.RNG;
import org.junit.AfterClass;
import org.junit.BeforeClass;
import org.junit.Test;
import java.io.DataOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.nio.file.Files;
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.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertTrue;
public class ContentGatePlaceholderTest {
private static CompatFixtures.FakeRegistries registries;
@BeforeClass
public static void bindPlatform() {
registries = CompatFixtures.registries("minecraft:sulfur", "minecraft:brimstone");
CompatFixtures.bind(registries);
}
@AfterClass
public static void unbindPlatform() {
CompatFixtures.unbind();
}
private static ContentGate gate() {
return new ContentGate(registries, Map.of(), new PackCompatReport());
}
@Test
public void placeholderOnlyForMissingKeysAndOnlyWhenReady() {
ContentGate gate = gate();
PlatformBlockState present = gate.resolveBlockOrPlaceholder("STONE");
assertFalse(MissingBlockState.isPlaceholder(present));
assertEquals("minecraft:stone", present.key());
PlatformBlockState missing = gate.resolveBlockOrPlaceholder("minecraft:sulfur[lit=true]");
assertTrue(MissingBlockState.isPlaceholder(missing));
assertEquals("minecraft:sulfur[lit=true]", missing.key());
assertEquals("minecraft:sulfur", missing.materialKey());
assertEquals("minecraft", missing.namespace());
assertSame(missing, gate.resolveBlockOrPlaceholder("minecraft:sulfur[lit=true]"));
assertNull(gate.resolveBlockOrPlaceholder(" "));
ContentGate unbound = new ContentGate(null, Map.of(), new PackCompatReport());
assertNull(unbound.resolveBlockOrPlaceholder("minecraft:sulfur"));
}
@Test
public void placeholderBehavesAsAirButKeepsItsKey() {
PlatformBlockState air = registries.air();
MissingBlockState placeholder = MissingBlockState.of("minecraft:sulfur[lit=true,axis=y]", air);
assertTrue(placeholder.isAir());
assertFalse(placeholder.isSolid());
assertFalse(placeholder.isCustom());
assertFalse(placeholder.hasTileEntity());
assertEquals(air.nativeHandle(), placeholder.nativeHandle());
assertEquals("minecraft:sulfur", placeholder.missingKey());
MissingBlockState exact = MissingBlockState.of("minecraft:sulfur[lit=true]", air);
MissingBlockState other = MissingBlockState.of("minecraft:brimstone", air);
assertTrue(exact.matches(placeholder));
assertFalse(placeholder.matches(exact));
assertFalse(exact.matches(other));
assertFalse(exact.matches(air));
assertFalse(air.matches(exact));
assertEquals(MissingBlockState.of("minecraft:sulfur[lit=true]", air), exact);
assertEquals(exact.hashCode(), MissingBlockState.of("minecraft:sulfur[lit=true]", air).hashCode());
assertEquals("minecraft:sulfur[lit=true,axis=y]", exact.withProperty("axis", "y").key());
}
@Test
public void blockDataPlaceholderFollowsTheChainBeforeGivingUp() throws Exception {
IrisData data = IrisData.get(Files.createTempDirectory("iris-compat-placeholder").toFile());
PlatformBlockState present = new IrisBlockData("minecraft:stone").getBlockDataOrPlaceholder(data);
assertFalse(MissingBlockState.isPlaceholder(present));
assertEquals("minecraft:stone", present.key());
PlatformBlockState revived = new IrisBlockData("minecraft:sulfur").setBackup(new IrisBlockData("minecraft:sand")).getBlockDataOrPlaceholder(data);
assertEquals("minecraft:sand", revived.key());
assertFalse(MissingBlockState.isPlaceholder(revived));
IrisBlockData missing = new IrisBlockData("minecraft:sulfur");
missing.getData().put("lit", true);
PlatformBlockState placeholder = missing.getBlockDataOrPlaceholder(data);
assertTrue(MissingBlockState.isPlaceholder(placeholder));
assertEquals("minecraft:sulfur[lit=true]", placeholder.key());
assertTrue(missing.getBlockData(data).isAir());
assertFalse(MissingBlockState.isPlaceholder(missing.getBlockData(data)));
}
@Test
public void fullChanceEditRuleRewritesAMissingPaletteKeyDuringPlacement() throws Exception {
File pack = Files.createTempDirectory("iris-compat-rescue").toFile();
writeObject(pack, "rescue", "minecraft:sulfur[lit=true]");
IrisData data = IrisData.get(pack);
IrisObject object = data.getObjectLoader().load("rescue");
assertNotNull(object);
PlatformBlockState loaded = object.getBlocks().values().iterator().next();
assertTrue(loaded.toString(), MissingBlockState.isPlaceholder(loaded));
IrisObjectPlacement placement = placement();
placement.getEdit().add(replace(1F, false, "minecraft:sulfur", "minecraft:stone"));
RecordingPlacer placer = new RecordingPlacer();
object.place(0, 64, 0, placer, placement, new RNG(7L), data);
assertEquals(1, placer.writes.size());
assertEquals("minecraft:stone", placer.writes.getFirst().key());
IrisObjectPlacement exact = placement();
exact.getEdit().add(replace(1F, true, "minecraft:sulfur[lit=true]", "minecraft:sand"));
RecordingPlacer exactPlacer = new RecordingPlacer();
object.place(0, 64, 0, exactPlacer, exact, new RNG(7L), data);
assertEquals("minecraft:sand", exactPlacer.writes.getFirst().key());
IrisObjectPlacement mismatch = placement();
mismatch.getEdit().add(replace(1F, true, "minecraft:sulfur[lit=false]", "minecraft:sand"));
RecordingPlacer mismatchPlacer = new RecordingPlacer();
object.place(0, 64, 0, mismatchPlacer, mismatch, new RNG(7L), data);
PlatformBlockState untouched = mismatchPlacer.writes.getFirst();
assertTrue(MissingBlockState.isPlaceholder(untouched));
assertTrue(untouched.isAir());
assertEquals(registries.air().nativeHandle(), untouched.nativeHandle());
}
private static IrisObjectPlacement placement() {
IrisObjectPlacement placement = new IrisObjectPlacement();
placement.setPlace(new KList<>(List.of("rescue")));
placement.setMode(ObjectPlaceMode.CENTER_HEIGHT);
placement.setRequireSurfaceSupport(false);
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(IrisBlockData.from(find));
rule.getReplace().qclear();
rule.getReplace().add(to);
return rule;
}
/** A one-block V2 .iob whose only palette entry is {@code state}. */
private static void writeObject(File pack, String key, String state) throws Exception {
File file = new File(pack, "objects/" + key + ".iob");
assertTrue(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(1);
out.writeUTF(state);
out.writeInt(1);
out.writeShort(0);
out.writeShort(0);
out.writeShort(0);
out.writeShort(0);
out.writeInt(0);
}
}
private static final class RecordingPlacer implements IObjectPlacer {
private final List<PlatformBlockState> writes = new ArrayList<>();
@Override
public int getHighest(int x, int z, IrisData data) {
return 64;
}
@Override
public int getHighest(int x, int z, IrisData data, boolean ignoreFluid) {
return 64;
}
@Override
public void set(int x, int y, int z, PlatformBlockState d) {
writes.add(d);
}
@Override
public PlatformBlockState get(int x, int y, int z) {
return null;
}
@Override
public boolean isPreventingDecay() {
return false;
}
@Override
public boolean isCarved(int x, int y, int z) {
return false;
}
@Override
public boolean isSolid(int x, int y, int z) {
return false;
}
@Override
public boolean isUnderwater(int x, int z) {
return false;
}
@Override
public int getFluidHeight() {
return 0;
}
@Override
public boolean isDebugSmartBore() {
return false;
}
@Override
public void setTile(int xx, int yy, int zz, TileData tile) {
}
@Override
public <T> void setData(int xx, int yy, int zz, T data) {
}
@Override
public <T> T getData(int xx, int yy, int zz, Class<T> t) {
return null;
}
@Override
public Engine getEngine() {
return null;
}
}
}
@@ -0,0 +1,190 @@
package art.arcane.iris.core.compat;
import art.arcane.iris.core.compat.ContentGate.BlockResolution;
import art.arcane.iris.core.compat.ContentGate.BlockResolution.Source;
import org.junit.Test;
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.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertTrue;
public class ContentGateTest {
private static ContentGate gate(CompatFixtures.FakeRegistries registries, Map<String, String> fallbacks) {
return new ContentGate(registries, fallbacks, new PackCompatReport());
}
@Test
public void presentBlockResolvesFromRegistry() {
ContentGate gate = gate(CompatFixtures.registries(), Map.of());
BlockResolution resolution = gate.resolveBlock("STONE");
assertNotNull(resolution);
assertEquals("minecraft:stone", resolution.resolvedKey());
assertEquals("minecraft:stone", resolution.state().key());
assertFalse(resolution.substituted());
assertNull(resolution.substitutedFrom());
assertEquals(Source.REGISTRY, resolution.source());
assertEquals(KeyStatus.PRESENT, gate.block("minecraft:stone[axis=y]"));
}
@Test
public void missingBlockResolvesToNull() {
ContentGate gate = gate(CompatFixtures.registries("minecraft:sulfur"), Map.of());
assertNull(gate.resolveBlock("minecraft:sulfur"));
assertNull(gate.resolveBlock("sulfur[lit=true]"));
assertEquals(KeyStatus.MISSING, gate.block("minecraft:sulfur"));
}
@Test
public void unknownWhenRegistriesAbsentOrEmpty() {
ContentGate unbound = gate(null, Map.of());
assertFalse(unbound.ready());
assertNull(unbound.resolveBlock("minecraft:stone"));
assertEquals(KeyStatus.UNKNOWN, unbound.block("minecraft:stone"));
assertEquals(KeyStatus.UNKNOWN, unbound.entity("minecraft:cow"));
CompatFixtures.FakeRegistries empty = CompatFixtures.registries();
empty.blockKeys.clear();
ContentGate notReady = gate(empty, Map.of());
assertFalse(notReady.ready());
assertEquals(KeyStatus.UNKNOWN, notReady.block("minecraft:stone"));
assertEquals(KeyStatus.UNKNOWN, notReady.item("minecraft:diamond"));
}
@Test
public void perRegistryUnknownWhenThatKeyListIsEmpty() {
CompatFixtures.FakeRegistries registries = CompatFixtures.registries();
registries.biomes.clear();
registries.structures.clear();
ContentGate gate = gate(registries, Map.of());
assertTrue(gate.ready());
assertEquals(KeyStatus.UNKNOWN, gate.biome("minecraft:plains"));
assertEquals(KeyStatus.UNKNOWN, gate.structure("minecraft:village_plains"));
assertEquals(KeyStatus.PRESENT, gate.entity("COW"));
assertEquals(KeyStatus.MISSING, gate.entity("minecraft:camel"));
}
@Test
public void nonBlockRegistriesNormalizeKeys() {
ContentGate gate = gate(CompatFixtures.registries(), Map.of());
assertEquals(KeyStatus.PRESENT, gate.item("DIAMOND"));
assertEquals(KeyStatus.MISSING, gate.item("minecraft:netherite_upgrade_template"));
assertEquals(KeyStatus.PRESENT, gate.biome("minecraft:plains"));
assertEquals(KeyStatus.MISSING, gate.biome("minecraft:pale_garden"));
assertEquals(KeyStatus.PRESENT, gate.enchantment("sharpness"));
assertEquals(KeyStatus.MISSING, gate.enchantment("minecraft:wind_burst"));
assertEquals(KeyStatus.PRESENT, gate.potionEffect("SPEED"));
assertEquals(KeyStatus.PRESENT, gate.potionEffect("INCREASE_DAMAGE"));
assertEquals(KeyStatus.MISSING, gate.potionEffect("minecraft:infested"));
assertEquals(KeyStatus.MISSING, gate.item(" "));
}
@Test
public void dimensionFallbackSubstitutesAndIsReported() {
ContentGate gate = gate(CompatFixtures.registries("minecraft:sulfur"), Map.of("minecraft:sulfur", "minecraft:stone"));
BlockResolution resolution = gate.resolveBlock("minecraft:sulfur[lit=true]");
assertNotNull(resolution);
assertTrue(resolution.substituted());
assertEquals(Source.FALLBACK, resolution.source());
assertEquals("minecraft:stone", resolution.resolvedKey());
assertEquals("minecraft:sulfur[lit=true]", resolution.substitutedFrom());
assertEquals("minecraft:stone", gate.fallbacks().get("minecraft:sulfur"));
}
@Test
public void fallbackThatIsItselfMissingCountsAsMissing() {
ContentGate gate = gate(CompatFixtures.registries("minecraft:sulfur", "minecraft:brimstone"),
Map.of("minecraft:sulfur", "minecraft:brimstone"));
assertNull(gate.resolveBlock("minecraft:sulfur"));
}
@Test
public void legacyRenameHitIsSubstitutedButNotReported() {
ContentGate gate = gate(CompatFixtures.registries("minecraft:grass"), Map.of());
BlockResolution resolution = gate.resolveBlock("minecraft:grass");
assertNotNull(resolution);
assertEquals("minecraft:short_grass", resolution.resolvedKey());
assertEquals(Source.RENAME, resolution.source());
assertFalse(resolution.substituted());
assertNull(resolution.substitutedFrom());
assertTrue(gate.report().isEmpty());
}
@Test
public void legacyRenameFollowsChainsAndGivesUpOnCycles() {
ContentGate chained = gate(CompatFixtures.registries("minecraft:soul_soil", "minecraft:soulsand"), Map.of());
assertNull(chained.resolveBlock("minecraft:soul_soil"));
ContentGate cyclic = gate(CompatFixtures.registries("minecraft:grass", "minecraft:short_grass"), Map.of());
assertNull(cyclic.resolveBlock("minecraft:grass"));
}
@Test
public void legacyRenameExactEntriesMatchFullState() {
ContentGate gate = gate(CompatFixtures.registries("minecraft:barrel"), Map.of());
BlockResolution resolution = gate.resolveBlock("minecraft:barrel[facing=east]");
assertNotNull(resolution);
assertEquals("minecraft:hay_bale[axis=x]", resolution.resolvedKey());
assertEquals(Source.RENAME, resolution.source());
}
@Test
public void renameRunsBeforeFallback() {
ContentGate gate = gate(CompatFixtures.registries("minecraft:grass"), Map.of("minecraft:grass", "minecraft:sand"));
BlockResolution resolution = gate.resolveBlock("minecraft:grass");
assertNotNull(resolution);
assertEquals(Source.RENAME, resolution.source());
assertEquals("minecraft:short_grass", resolution.resolvedKey());
}
@Test
public void resolutionsAreCachedPerNormalizedState() {
CompatFixtures.FakeRegistries registries = CompatFixtures.registries("minecraft:sulfur");
ContentGate gate = gate(registries, Map.of());
BlockResolution first = gate.resolveBlock("minecraft:stone");
int after = registries.blockLookups;
BlockResolution second = gate.resolveBlock("STONE");
assertSame(first, second);
assertEquals(after, registries.blockLookups);
assertNull(gate.resolveBlock("minecraft:sulfur"));
int misses = registries.blockLookups;
assertNull(gate.resolveBlock("minecraft:sulfur"));
assertEquals(misses, registries.blockLookups);
gate.invalidate();
gate.resolveBlock("minecraft:stone");
assertTrue(registries.blockLookups > after);
}
@Test
public void normalizeStateAndBaseKey() {
assertEquals("minecraft:stone", ContentGate.normalizeState(" STONE "));
assertEquals("minecraft:oak_log[axis=y]", ContentGate.normalizeState("Oak_Log[AXIS=Y]"));
assertEquals("create:cogwheel", ContentGate.normalizeState("create:cogwheel"));
assertNull(ContentGate.normalizeState(null));
assertNull(ContentGate.normalizeState(" "));
assertEquals("minecraft:oak_log", ContentGate.baseKey("minecraft:oak_log[axis=y]"));
assertEquals("minecraft:oak_log", ContentGate.baseKey("minecraft:oak_log"));
assertNull(ContentGate.baseKey(null));
}
@Test
public void readBlockFallbacksMergesDimensionFiles() throws Exception {
java.io.File pack = java.nio.file.Files.createTempDirectory("iris-compat-fallbacks").toFile();
java.io.File dimensions = new java.io.File(pack, "dimensions");
assertTrue(dimensions.mkdirs());
java.nio.file.Files.writeString(new java.io.File(dimensions, "overworld.json").toPath(),
"{\"name\":\"x\",\"blockFallbacks\":{\"minecraft:sulfur\":\"minecraft:stone\"}}");
java.nio.file.Files.writeString(new java.io.File(dimensions, "other.json").toPath(),
"{\"name\":\"y\",\"blockFallbacks\":{\"minecraft:sulfur\":\"minecraft:sand\",\"minecraft:x\":\"minecraft:dirt\"}}");
Map<String, String> read = ContentGate.readBlockFallbacks(pack);
assertEquals(2, read.size());
assertTrue(List.of("minecraft:stone", "minecraft:sand").contains(read.get("minecraft:sulfur")));
assertEquals("minecraft:dirt", read.get("minecraft:x"));
assertTrue(ContentGate.readBlockFallbacks(new java.io.File(pack, "nope")).isEmpty());
}
}
@@ -0,0 +1,576 @@
package art.arcane.iris.core.compat;
import art.arcane.iris.core.loader.IrisRegistrant;
import art.arcane.iris.engine.object.IrisBiome;
import art.arcane.iris.engine.object.IrisBiomeCustom;
import art.arcane.iris.engine.object.IrisBiomeCustomSpawn;
import art.arcane.iris.engine.object.IrisBiomePaletteLayer;
import art.arcane.iris.engine.object.IrisBlockData;
import art.arcane.iris.engine.object.IrisDecorator;
import art.arcane.iris.engine.object.IrisEffect;
import art.arcane.iris.engine.object.IrisEnchantment;
import art.arcane.iris.engine.object.IrisEntity;
import art.arcane.iris.engine.object.IrisLoot;
import art.arcane.iris.engine.object.IrisLootTable;
import art.arcane.iris.engine.object.IrisMaterialPalette;
import art.arcane.iris.engine.object.IrisObjectPlacement;
import art.arcane.iris.engine.object.IrisObjectReplace;
import art.arcane.iris.engine.object.annotations.RegistryListBlockType;
import art.arcane.iris.engine.object.annotations.RegistryListItemType;
import art.arcane.volmlib.util.collection.KList;
import org.junit.Test;
import java.util.List;
import java.util.Map;
import java.util.function.Function;
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.assertTrue;
public class ContentGateWalkerTest {
private static ContentGate gate(String... missingBlocks) {
return new ContentGate(CompatFixtures.registries(missingBlocks), Map.of(), new PackCompatReport());
}
private static ContentGate gate(CompatFixtures.FakeRegistries registries) {
return new ContentGate(registries, Map.of(), new PackCompatReport());
}
private static IrisBiome biome(String key) {
IrisBiome biome = new IrisBiome();
biome.setLoadKey(key);
return biome;
}
private static IrisBiomePaletteLayer layer(String... blocks) {
KList<IrisBlockData> palette = new KList<>();
for (String block : blocks) {
palette.add(new IrisBlockData(block));
}
return new IrisBiomePaletteLayer().setPalette(palette);
}
private static IrisLoot loot(String item) {
return new IrisLoot().setType(item);
}
/** Reads a field directly; several registrant getters resolve through Bukkit registries that are absent here. */
private static Object rawField(Object owner, String name) throws Exception {
java.lang.reflect.Field field = owner.getClass().getDeclaredField(name);
field.setAccessible(true);
return field.get(owner);
}
private static CompatFinding only(CompatStatus status) {
assertEquals(status.reasons().toString(), 1, status.reasons().size());
return status.reasons().getFirst();
}
private static void assertFinding(CompatFinding finding, CompatRegistry registry, String key, CompatAction action,
String subjectType, String subjectKey, String detail) {
assertEquals(registry, finding.registry());
assertEquals(key, finding.key());
assertEquals(action, finding.action());
assertEquals(subjectType, finding.subjectType());
assertEquals(subjectKey, finding.subjectKey());
assertEquals(detail, finding.detail());
}
public static final class Probe extends IrisRegistrant {
@RegistryListBlockType
private String block = "minecraft:stone";
@RegistryListItemType
private KList<String> items = new KList<>();
private KList<IrisObjectReplace> edit = new KList<>();
private IrisObjectPlacement placement = null;
private IrisLoot gear = null;
private Probe self = null;
private KList<Probe> children = new KList<>();
private KList<IrisDecorator> decorators = new KList<>();
@Override
public String getFolderName() {
return "probes";
}
@Override
public String getTypeName() {
return "Probe";
}
void setBlock(String block) {
this.block = block;
}
KList<String> getItems() {
return items;
}
void setItems(KList<String> items) {
this.items = items;
}
void setEdit(KList<IrisObjectReplace> edit) {
this.edit = edit;
}
void setPlacement(IrisObjectPlacement placement) {
this.placement = placement;
}
IrisLoot getGear() {
return gear;
}
void setGear(IrisLoot gear) {
this.gear = gear;
}
void setSelf(Probe self) {
this.self = self;
}
void setChildren(KList<Probe> children) {
this.children = children;
}
void setDecorators(KList<IrisDecorator> decorators) {
this.decorators = decorators;
}
}
@Test
public void missingLayerBlockExcludesBiomeWithFieldPath() {
ContentGate gate = gate("minecraft:sulfur");
IrisBiome biome = biome("cave/sulfur-grotto");
biome.setLayers(new KList<>(List.of(layer("minecraft:stone"), layer("minecraft:stone", "minecraft:dirt", "minecraft:sulfur"))));
CompatStatus status = gate.evaluate(biome);
assertTrue(status.excluded());
assertFinding(only(status), CompatRegistry.BLOCK, "minecraft:sulfur", CompatAction.EXCLUDED, "biome", "cave/sulfur-grotto", "layers[1].palette[2]");
assertEquals(1, gate.report().size());
assertEquals(status.reasons().getFirst(), gate.report().findings().getFirst());
}
@Test
public void blockPropertiesTravelWithTheKey() {
ContentGate gate = gate("minecraft:sulfur");
IrisBiome biome = biome("b");
IrisBlockData entry = new IrisBlockData("sulfur");
entry.getData().put("lit", true);
biome.setLayers(new KList<>(List.of(new IrisBiomePaletteLayer().setPalette(new KList<>(List.of(entry))))));
CompatStatus status = gate.evaluate(biome);
assertTrue(status.excluded());
assertEquals("minecraft:sulfur", only(status).key());
}
@Test
public void backupRevivesEntryAsSubstitution() {
ContentGate gate = gate("minecraft:sulfur");
IrisBiome biome = biome("desert/dunes");
IrisBlockData entry = new IrisBlockData("minecraft:sulfur").setBackup(new IrisBlockData("minecraft:sand"));
biome.setLayers(new KList<>(List.of(new IrisBiomePaletteLayer().setPalette(new KList<>(List.of(entry))))));
CompatStatus status = gate.evaluate(biome);
assertFalse(status.excluded());
assertFinding(only(status), CompatRegistry.BLOCK, "minecraft:sulfur", CompatAction.SUBSTITUTED, "biome", "desert/dunes", "layers[0].palette[0] (backup minecraft:sand)");
assertEquals(1, gate.report().size());
}
@Test
public void backupChainsAndMissingBackupStillExcludes() {
ContentGate gate = gate("minecraft:sulfur", "minecraft:brimstone");
IrisBiome chained = biome("chained");
IrisBlockData deep = new IrisBlockData("minecraft:sulfur")
.setBackup(new IrisBlockData("minecraft:brimstone").setBackup(new IrisBlockData("minecraft:sand")));
chained.setLayers(new KList<>(List.of(new IrisBiomePaletteLayer().setPalette(new KList<>(List.of(deep))))));
CompatStatus revived = gate.evaluate(chained);
assertFalse(revived.excluded());
assertEquals("layers[0].palette[0] (backup minecraft:sand)", only(revived).detail());
IrisBiome dead = biome("dead");
IrisBlockData deadEntry = new IrisBlockData("minecraft:sulfur").setBackup(new IrisBlockData("minecraft:brimstone"));
dead.setLayers(new KList<>(List.of(new IrisBiomePaletteLayer().setPalette(new KList<>(List.of(deadEntry))))));
CompatStatus excluded = gate.evaluate(dead);
assertTrue(excluded.excluded());
assertFinding(only(excluded), CompatRegistry.BLOCK, "minecraft:sulfur", CompatAction.EXCLUDED, "biome", "dead", "layers[0].palette[0]");
}
@Test
public void dimensionFallbackSubstitutesEntry() {
ContentGate gate = new ContentGate(CompatFixtures.registries("minecraft:sulfur"), Map.of("minecraft:sulfur", "minecraft:stone"), new PackCompatReport());
IrisBiome biome = biome("desert/flats");
biome.setLayers(new KList<>(List.of(layer("minecraft:sulfur"))));
CompatStatus status = gate.evaluate(biome);
assertFalse(status.excluded());
assertFinding(only(status), CompatRegistry.BLOCK, "minecraft:sulfur", CompatAction.SUBSTITUTED, "biome", "desert/flats", "layers[0].palette[0] (fallback minecraft:stone)");
}
@Test
public void legacyRenameHitIsNeitherExcludedNorReported() {
ContentGate gate = gate("minecraft:grass");
IrisBiome biome = biome("plains");
biome.setLayers(new KList<>(List.of(layer("minecraft:grass"))));
CompatStatus status = gate.evaluate(biome);
assertFalse(status.excluded());
assertTrue(status.reasons().isEmpty());
assertTrue(gate.report().isEmpty());
}
@Test
public void missingEntityTypeExcludesEntity() {
ContentGate gate = gate();
IrisEntity entity = new IrisEntity().setType("minecraft:camel");
entity.setLoadKey("camel");
CompatStatus status = gate.evaluate(entity);
assertTrue(status.excluded());
assertFinding(only(status), CompatRegistry.ENTITY, "minecraft:camel", CompatAction.EXCLUDED, "entity", "camel", "type");
assertEquals("minecraft:camel", entity.getType());
}
@Test
public void inlinePassengerWithMissingTypeIsDroppedFromItsList() {
ContentGate gate = gate();
IrisEntity entity = new IrisEntity().setType("minecraft:zombie");
entity.setLoadKey("rider");
entity.setPassengers(new KList<>(List.of(new IrisEntity().setType("minecraft:cow"), new IrisEntity().setType("minecraft:camel"))));
CompatStatus status = gate.evaluate(entity);
assertFalse(status.excluded());
assertEquals(1, entity.getPassengers().size());
assertEquals("minecraft:cow", entity.getPassengers().getFirst().getType());
assertFinding(only(status), CompatRegistry.ENTITY, "minecraft:camel", CompatAction.DROPPED, "entity", "rider", "passengers[1].type");
}
@Test
public void customBiomeSpawnIsDroppedBeforeDatapackWrite() {
ContentGate gate = gate();
IrisBiome biome = biome("custom/lush");
IrisBiomeCustom custom = new IrisBiomeCustom().setId("lush");
custom.setSpawns(new KList<>(List.of(new IrisBiomeCustomSpawn().setType("minecraft:cow"), new IrisBiomeCustomSpawn().setType("camel"))));
biome.setCustomDerivitives(new KList<>(List.of(custom)));
CompatStatus status = gate.evaluate(biome);
assertFalse(status.excluded());
assertEquals(1, custom.getSpawns().size());
assertEquals("minecraft:cow", custom.getSpawns().getFirst().getTypeKey());
assertFinding(only(status), CompatRegistry.ENTITY, "minecraft:camel", CompatAction.DROPPED, "biome", "custom/lush", "customDerivitives[0].spawns[1].type");
}
@Test
public void lootItemIsDroppedFromTable() {
ContentGate gate = gate();
IrisLootTable table = new IrisLootTable();
table.setLoadKey("chests/common");
table.setLoot(new KList<>(List.of(loot("minecraft:diamond"), loot("minecraft:mace"))));
CompatStatus status = gate.evaluate(table);
assertFalse(status.excluded());
assertEquals(1, table.getLoot().size());
assertEquals("minecraft:diamond", table.getLoot().getFirst().getTypeKey());
assertFinding(only(status), CompatRegistry.ITEM, "minecraft:mace", CompatAction.DROPPED, "loot", "chests/common", "loot[1].type");
}
@Test
public void missingEnchantmentIsDroppedButLootEntryStays() {
ContentGate gate = gate();
IrisLootTable table = new IrisLootTable();
table.setLoadKey("chests/rare");
IrisLoot sword = loot("minecraft:diamond");
sword.setEnchantments(new KList<>(List.of(new IrisEnchantment().setEnchantment("sharpness"), new IrisEnchantment().setEnchantment("minecraft:wind_burst"))));
table.setLoot(new KList<>(List.of(sword)));
CompatStatus status = gate.evaluate(table);
assertFalse(status.excluded());
assertEquals(1, table.getLoot().size());
assertEquals(1, sword.getEnchantments().size());
assertEquals("sharpness", sword.getEnchantments().getFirst().getEnchantment());
assertFinding(only(status), CompatRegistry.ENCHANTMENT, "minecraft:wind_burst", CompatAction.DROPPED, "loot", "chests/rare", "loot[0].enchantments[1].enchantment");
}
@Test
public void missingPotionEffectDropsEffectAndBlankEffectStays() {
ContentGate gate = gate();
IrisBiome biome = biome("swamp");
biome.setEffects(new KList<>(List.of(new IrisEffect().setPotionEffect("SPEED"), new IrisEffect().setPotionEffect("minecraft:infested"), new IrisEffect().setPotionEffect(""))));
CompatStatus status = gate.evaluate(biome);
assertFalse(status.excluded());
assertEquals(2, biome.getEffects().size());
assertEquals("SPEED", biome.getEffects().get(0).getPotionEffect());
assertEquals("", biome.getEffects().get(1).getPotionEffect());
assertFinding(only(status), CompatRegistry.POTION_EFFECT, "minecraft:infested", CompatAction.DROPPED, "biome", "swamp", "effects[1].potionEffect");
}
@Test
public void scatterEntriesAreDroppedAndBiomeKeepsGenerating() {
ContentGate gate = gate();
IrisBiome biome = biome("plains");
biome.setBiomeScatter(new KList<>(List.of("minecraft:plains", "minecraft:pale_garden")));
biome.setBiomeSkyScatter(new KList<>(List.of("minecraft:pale_garden", "minecraft:desert")));
CompatStatus status = gate.evaluate(biome);
assertFalse(status.excluded());
assertEquals(List.of("minecraft:plains"), biome.getBiomeScatter());
assertEquals(List.of("minecraft:desert"), biome.getBiomeSkyScatter());
assertEquals(2, status.reasons().size());
assertFinding(status.reasons().get(0), CompatRegistry.BIOME, "minecraft:pale_garden", CompatAction.DROPPED, "biome", "plains", "biomeScatter[1]");
assertFinding(status.reasons().get(1), CompatRegistry.BIOME, "minecraft:pale_garden", CompatAction.DROPPED, "biome", "plains", "biomeSkyScatter[0]");
assertEquals(1, gate.report().size());
}
@Test
public void missingDerivativeExcludesBiome() {
ContentGate gate = gate();
IrisBiome biome = biome("pale");
biome.setDerivative("minecraft:pale_garden");
CompatStatus status = gate.evaluate(biome);
assertTrue(status.excluded());
assertFinding(only(status), CompatRegistry.BIOME, "minecraft:pale_garden", CompatAction.EXCLUDED, "biome", "pale", "derivative");
assertEquals("minecraft:pale_garden", biome.getDerivativeKey());
}
@Test
public void missingVanillaDerivativeIsNulled() throws Exception {
ContentGate gate = gate();
IrisBiome biome = biome("pale");
biome.setVanillaDerivative("minecraft:pale_garden");
CompatStatus status = gate.evaluate(biome);
assertFalse(status.excluded());
assertNull(rawField(biome, "vanillaDerivative"));
assertFinding(only(status), CompatRegistry.BIOME, "minecraft:pale_garden", CompatAction.DROPPED, "biome", "pale", "vanillaDerivative");
}
@Test
public void objectPlacementsAreNotDescended() {
ContentGate gate = gate("minecraft:sulfur");
IrisBiome biome = biome("plains");
IrisObjectReplace replace = new IrisObjectReplace()
.setFind(new KList<>(List.of(new IrisBlockData("minecraft:stone"))))
.setReplace(new IrisMaterialPalette().setPalette(new KList<>(List.of(new IrisBlockData("minecraft:sulfur")))));
IrisObjectPlacement placement = new IrisObjectPlacement().setPlace(new KList<>(List.of("trees/oak"))).setEdit(new KList<>(List.of(replace)));
biome.setObjects(new KList<>(List.of(placement)));
CompatStatus status = gate.evaluate(biome);
assertFalse(status.excluded());
assertTrue(status.reasons().isEmpty());
assertTrue(gate.report().isEmpty());
assertEquals(1, biome.getObjects().size());
assertEquals(1, replace.getReplace().getPalette().size());
}
@Test
public void replaceFindIsMatchOnlyButReplacePaletteCounts() {
ContentGate gate = gate("minecraft:sulfur");
Probe matchOnly = new Probe();
matchOnly.setLoadKey("match");
matchOnly.setEdit(new KList<>(List.of(new IrisObjectReplace()
.setFind(new KList<>(List.of(new IrisBlockData("minecraft:sulfur"))))
.setReplace(new IrisMaterialPalette().setPalette(new KList<>(List.of(new IrisBlockData("minecraft:stone"))))))));
assertFalse(gate.evaluate(matchOnly).excluded());
assertTrue(gate.report().isEmpty());
Probe generated = new Probe();
generated.setLoadKey("gen");
generated.setEdit(new KList<>(List.of(new IrisObjectReplace()
.setFind(new KList<>(List.of(new IrisBlockData("minecraft:stone"))))
.setReplace(new IrisMaterialPalette().setPalette(new KList<>(List.of(new IrisBlockData("minecraft:sulfur"))))))));
CompatStatus status = gate.evaluate(generated);
assertTrue(status.excluded());
assertFinding(only(status), CompatRegistry.BLOCK, "minecraft:sulfur", CompatAction.EXCLUDED, "probe", "gen", "edit[0].replace.palette[0]");
}
@Test
public void decoratorMatchListsAreIgnoredButPaletteCounts() {
ContentGate gate = gate("minecraft:sulfur");
Probe whitelisted = new Probe();
whitelisted.setLoadKey("white");
whitelisted.setDecorators(new KList<>(List.of(new IrisDecorator()
.setWhitelist(new KList<>(List.of(new IrisBlockData("minecraft:sulfur"))))
.setBlacklist(new KList<>(List.of(new IrisBlockData("minecraft:sulfur")))))));
assertFalse(gate.evaluate(whitelisted).excluded());
assertTrue(gate.report().isEmpty());
Probe placing = new Probe();
placing.setLoadKey("place");
placing.setDecorators(new KList<>(List.of(new IrisDecorator().setPalette(new KList<>(List.of(new IrisBlockData("minecraft:sulfur")))))));
CompatStatus status = gate.evaluate(placing);
assertTrue(status.excluded());
assertEquals("decorators[0].palette[0]", only(status).detail());
}
@Test
public void cyclicGraphsTerminate() {
ContentGate gate = gate();
Probe probe = new Probe();
probe.setLoadKey("loop");
probe.setSelf(probe);
probe.setChildren(new KList<>(List.of(probe)));
CompatStatus status = gate.evaluate(probe);
assertFalse(status.excluded());
assertTrue(status.reasons().isEmpty());
}
@Test
public void notReadyProducesNoFindingsNoMutationAndMarksIncomplete() {
ContentGate gate = new ContentGate(null, Map.of(), new PackCompatReport());
IrisBiome biome = biome("custom/lush");
IrisBiomeCustom custom = new IrisBiomeCustom().setId("lush");
custom.setSpawns(new KList<>(List.of(new IrisBiomeCustomSpawn().setType("minecraft:camel"))));
biome.setCustomDerivitives(new KList<>(List.of(custom)));
biome.setLayers(new KList<>(List.of(layer("minecraft:sulfur"))));
CompatStatus status = gate.evaluate(biome);
assertFalse(status.excluded());
assertTrue(status.reasons().isEmpty());
assertEquals(1, custom.getSpawns().size());
assertTrue(gate.report().isEmpty());
assertTrue(gate.report().isIncomplete());
}
@Test
public void unknownSingleRegistryLeavesItsKeysAlone() {
CompatFixtures.FakeRegistries registries = CompatFixtures.registries("minecraft:sulfur");
registries.biomes.clear();
ContentGate gate = gate(registries);
IrisBiome biome = biome("plains");
biome.setDerivative("minecraft:pale_garden");
biome.setBiomeScatter(new KList<>(List.of("minecraft:pale_garden")));
biome.setLayers(new KList<>(List.of(layer("minecraft:sulfur"))));
CompatStatus status = gate.evaluate(biome);
assertTrue(status.excluded());
assertEquals(1, status.reasons().size());
assertEquals(CompatRegistry.BLOCK, only(status).registry());
assertEquals(List.of("minecraft:pale_garden"), biome.getBiomeScatter());
assertTrue(gate.report().isIncomplete());
assertTrue(gate.report().incompleteReason(), gate.report().incompleteReason().contains("biome"));
}
@Test
public void annotatedStringFieldsOnTheRegistrantFollowTheTable() {
ContentGate gate = gate("minecraft:sulfur");
Probe blocked = new Probe();
blocked.setLoadKey("blocked");
blocked.setBlock("minecraft:sulfur");
CompatStatus status = gate.evaluate(blocked);
assertTrue(status.excluded());
assertFinding(only(status), CompatRegistry.BLOCK, "minecraft:sulfur", CompatAction.EXCLUDED, "probe", "blocked", "block");
Probe items = new Probe();
items.setLoadKey("items");
items.setItems(new KList<>(List.of("minecraft:diamond", "minecraft:mace")));
CompatStatus dropped = gate.evaluate(items);
assertFalse(dropped.excluded());
assertEquals(List.of("minecraft:diamond"), items.getItems());
assertFinding(only(dropped), CompatRegistry.ITEM, "minecraft:mace", CompatAction.DROPPED, "probe", "items", "items[1]");
}
@Test
public void droppedObjectHeldByDirectFieldIsNulled() {
ContentGate gate = gate();
Probe probe = new Probe();
probe.setLoadKey("gear");
probe.setGear(loot("minecraft:mace"));
CompatStatus status = gate.evaluate(probe);
assertFalse(status.excluded());
assertNull(probe.getGear());
assertFinding(only(status), CompatRegistry.ITEM, "minecraft:mace", CompatAction.DROPPED, "probe", "gear", "gear.type");
}
@Test
public void everyFindingReachesReasonsAndReportDedups() {
ContentGate gate = gate("minecraft:sulfur", "minecraft:brimstone");
IrisBiome biome = biome("multi");
biome.setLayers(new KList<>(List.of(layer("minecraft:sulfur", "minecraft:brimstone"), layer("minecraft:sulfur"))));
CompatStatus status = gate.evaluate(biome);
assertTrue(status.excluded());
assertEquals(3, status.reasons().size());
assertEquals(2, gate.report().size());
assertEquals("layers[0].palette[0]", gate.report().byKey().get("BLOCK:minecraft:sulfur").getFirst().detail());
}
@Test
public void customPackBlocksResolveThroughTheLoaderStatus() {
IrisBlockData fine = new IrisBlockData("minecraft:stone");
fine.setCompat(CompatStatus.OK);
IrisBlockData broken = new IrisBlockData("minecraft:sulfur");
broken.setCompat(CompatStatus.excludedBy(List.of(new CompatFinding(CompatRegistry.BLOCK, "minecraft:sulfur", CompatAction.EXCLUDED, "block", "broken", "block"))));
Function<String, IrisBlockData> customBlocks = key -> switch (key) {
case "fancy" -> fine;
case "broken" -> broken;
default -> null;
};
ContentGate gate = new ContentGate(CompatFixtures.registries("minecraft:sulfur"), Map::of, new PackCompatReport(), customBlocks);
IrisBiome ok = biome("ok");
ok.setLayers(new KList<>(List.of(layer("fancy"))));
assertFalse(gate.evaluate(ok).excluded());
assertTrue(gate.report().isEmpty());
IrisBiome bad = biome("bad");
bad.setLayers(new KList<>(List.of(layer("broken"))));
CompatStatus status = gate.evaluate(bad);
assertTrue(status.excluded());
assertFinding(only(status), CompatRegistry.BLOCK, "minecraft:sulfur", CompatAction.EXCLUDED, "biome", "bad", "layers[0].palette[0]");
}
@Test
public void blockRegistrantEvaluatesItsOwnStateWithoutTheLoader() {
Function<String, IrisBlockData> customBlocks = key -> {
throw new IllegalStateException("loader must not be consulted for a block registrant");
};
ContentGate gate = new ContentGate(CompatFixtures.registries("minecraft:sulfur"), Map::of, new PackCompatReport(), customBlocks);
IrisBlockData registrant = new IrisBlockData("minecraft:sulfur");
registrant.setLoadKey("sulfur-variant");
CompatStatus status = gate.evaluate(registrant);
assertTrue(status.excluded());
assertFinding(only(status), CompatRegistry.BLOCK, "minecraft:sulfur", CompatAction.EXCLUDED, "block", "sulfur-variant", "block");
IrisBlockData revived = new IrisBlockData("minecraft:sulfur").setBackup(new IrisBlockData("minecraft:sand"));
revived.setLoadKey("sulfur-backup");
CompatStatus substituted = gate.evaluate(revived);
assertFalse(substituted.excluded());
assertEquals("block (backup minecraft:sand)", only(substituted).detail());
}
@Test
public void nullRegistrantIsOk() {
assertNotNull(gate().evaluate(null));
assertFalse(gate().evaluate(null).excluded());
}
}
@@ -0,0 +1,96 @@
package art.arcane.iris.core.compat;
import art.arcane.iris.core.loader.IrisData;
import art.arcane.iris.engine.object.IrisBlockData;
import org.junit.AfterClass;
import org.junit.BeforeClass;
import org.junit.Test;
import java.io.File;
import java.nio.file.Files;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
public class IrisBlockDataResolutionTest {
private static CompatFixtures.FakeRegistries registries;
@BeforeClass
public static void bindPlatform() {
registries = CompatFixtures.registries("minecraft:sulfur", "minecraft:brimstone", "minecraft:grass");
CompatFixtures.bind(registries);
}
@AfterClass
public static void unbindPlatform() {
CompatFixtures.unbind();
}
private static IrisData pack(String dimensionJson, String customBlockJson) throws Exception {
File folder = Files.createTempDirectory("iris-compat-blockdata").toFile();
if (dimensionJson != null) {
File dimensions = new File(folder, "dimensions");
assertTrue(dimensions.mkdirs());
Files.writeString(new File(dimensions, "overworld.json").toPath(), dimensionJson);
}
if (customBlockJson != null) {
File blocks = new File(folder, "blocks");
assertTrue(blocks.mkdirs());
Files.writeString(new File(blocks, "fancy.json").toPath(), customBlockJson);
}
return IrisData.get(folder);
}
@Test
public void presentBlockResolvesWithProperties() throws Exception {
IrisData data = pack(null, null);
IrisBlockData entry = new IrisBlockData("minecraft:oak_log");
entry.getData().put("axis", "y");
assertEquals("minecraft:oak_log[axis=y]", entry.getBlockData(data).key());
assertEquals("minecraft:stone", new IrisBlockData("STONE").getBlockData(data).key());
}
@Test
public void backupRevivesMissingBlock() throws Exception {
IrisData data = pack(null, null);
IrisBlockData entry = new IrisBlockData("minecraft:sulfur").setBackup(new IrisBlockData("minecraft:sand"));
assertEquals("minecraft:sand", entry.getBlockData(data).key());
IrisBlockData chained = new IrisBlockData("minecraft:sulfur")
.setBackup(new IrisBlockData("minecraft:brimstone").setBackup(new IrisBlockData("minecraft:sand")));
assertEquals("minecraft:sand", chained.getBlockData(data).key());
}
@Test
public void missingBlockWithoutBackupFallsBackToAir() throws Exception {
IrisData data = pack(null, null);
IrisBlockData entry = new IrisBlockData("minecraft:sulfur");
assertTrue(entry.getBlockData(data).isAir());
assertTrue(new IrisBlockData("minecraft:sulfur").setBackup(new IrisBlockData("minecraft:brimstone")).getBlockData(data).isAir());
}
@Test
public void dimensionFallbackAppliesBeforeBackup() throws Exception {
IrisData data = pack("{\"name\":\"overworld\",\"blockFallbacks\":{\"minecraft:sulfur\":\"minecraft:stone\"}}", null);
IrisBlockData entry = new IrisBlockData("minecraft:sulfur").setBackup(new IrisBlockData("minecraft:sand"));
assertEquals("minecraft:stone", entry.getBlockData(data).key());
}
@Test
public void legacyRenameAppliesOnEveryPlatform() throws Exception {
IrisData data = pack(null, null);
assertEquals("minecraft:short_grass", new IrisBlockData("minecraft:grass").getBlockData(data).key());
assertTrue(data.getCompatReport().isEmpty());
}
@Test
public void customPackBlockStillResolvesFirstAndUsesItsOwnBackup() throws Exception {
IrisData data = pack(null, "{\"block\":\"minecraft:sulfur\",\"backup\":{\"block\":\"minecraft:sand\"}}");
IrisBlockData entry = new IrisBlockData("fancy");
assertEquals("minecraft:sand", entry.getBlockData(data).key());
assertFalse(data.getCompatReport().isEmpty());
assertEquals(CompatAction.SUBSTITUTED, data.getCompatReport().findings().getFirst().action());
assertEquals("block", data.getCompatReport().findings().getFirst().subjectType());
}
}
@@ -0,0 +1,71 @@
package art.arcane.iris.core.compat;
import org.junit.Test;
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.assertNull;
import static org.junit.Assert.assertTrue;
public class PackCompatReportTest {
private static CompatFinding finding(CompatAction action, String subjectKey, String detail) {
return new CompatFinding(CompatRegistry.BLOCK, "minecraft:sulfur", action, "Biome", subjectKey, detail);
}
@Test
public void recordDedupsOnRegistryKeyActionAndSubject() {
PackCompatReport report = new PackCompatReport();
report.record(finding(CompatAction.EXCLUDED, "cave/sulfur", "layers[0].palette[1]"));
report.record(finding(CompatAction.EXCLUDED, "cave/sulfur", "layers[2].palette[0]"));
report.record(finding(CompatAction.SUBSTITUTED, "cave/sulfur", "layers[1].palette[0] (backup minecraft:sand)"));
report.record(finding(CompatAction.EXCLUDED, "desert/flats", "layers[0].palette[0]"));
report.record(null);
assertEquals(3, report.size());
List<CompatFinding> findings = report.findings();
assertEquals("layers[0].palette[1]", findings.get(0).detail());
assertEquals(CompatAction.SUBSTITUTED, findings.get(1).action());
assertEquals("desert/flats", findings.get(2).subjectKey());
Map<CompatAction, Integer> counts = report.countsByAction();
assertEquals(Integer.valueOf(2), counts.get(CompatAction.EXCLUDED));
assertEquals(Integer.valueOf(1), counts.get(CompatAction.SUBSTITUTED));
assertEquals(1, report.distinctKeys());
assertEquals(3, report.byKey().get("BLOCK:minecraft:sulfur").size());
}
@Test
public void markIncompleteKeepsFirstReason() {
PackCompatReport report = new PackCompatReport();
assertFalse(report.isIncomplete());
report.markIncomplete("biome registry not ready");
report.markIncomplete("structure registry not ready");
assertTrue(report.isIncomplete());
assertEquals("biome registry not ready", report.incompleteReason());
report.clear();
assertFalse(report.isIncomplete());
assertNull(report.incompleteReason());
}
@Test
public void clearForgetsFindingsForHotload() {
PackCompatReport report = new PackCompatReport();
report.record(finding(CompatAction.DROPPED, "cave/sulfur", "x"));
assertFalse(report.isEmpty());
report.clear();
assertTrue(report.isEmpty());
assertEquals(0, report.size());
assertTrue(report.bootLines("overworld", "26.1.2", 3).isEmpty());
assertEquals("", report.summaryLine("26.1.2"));
}
@Test
public void findingLineAndDedupKey() {
CompatFinding finding = finding(CompatAction.EXCLUDED, "cave/sulfur", "layers[0].palette[1]");
assertEquals("minecraft:sulfur (block): excluded Biome cave/sulfur at layers[0].palette[1]", finding.line());
assertEquals("BLOCK:minecraft:sulfur|EXCLUDED|Biome|cave/sulfur", finding.dedupKey());
CompatFinding blank = new CompatFinding(CompatRegistry.ENTITY, "minecraft:camel", CompatAction.DROPPED, null, null, null);
assertEquals("minecraft:camel (entity): dropped", blank.line());
}
}
@@ -1,5 +1,8 @@
package art.arcane.iris.core.pack;
import art.arcane.iris.core.compat.CompatAction;
import art.arcane.iris.core.compat.CompatFinding;
import art.arcane.iris.core.compat.CompatRegistry;
import org.junit.Rule;
import org.junit.Test;
import org.junit.Assume;
@@ -16,6 +19,7 @@ import java.util.Optional;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotEquals;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
@@ -61,6 +65,55 @@ public class PackValidationCacheTest {
cache, "content", "context", List.of("other")).isEmpty());
}
@Test
public void cacheRoundTripsCompatFindingsAndMinecraftVersion() throws Exception {
Path cache = new File(temporaryFolder.newFolder("compat-round-trip"), "validation.json").toPath();
CompatFinding excluded = new CompatFinding(
CompatRegistry.BLOCK, "minecraft:sulfur", CompatAction.EXCLUDED,
"Biome", "cave/sulfur-grotto", "layers[0].palette[1]");
CompatFinding dropped = new CompatFinding(
CompatRegistry.ENTITY, "minecraft:camel", CompatAction.DROPPED, "Spawner", "desert", "spawns[0]");
PackValidationCache.save(cache, "content", "context", List.of(new PackValidationResult(
"overworld", List.of(), List.of("warning"), 7L, List.of(excluded, dropped), "26.1.2")));
PackValidationResult loaded = PackValidationCache.load(
cache, "content", "context", List.of("overworld")).orElseThrow().getFirst();
assertEquals("26.1.2", loaded.getMinecraftVersion());
assertEquals(List.of(excluded, dropped), loaded.getCompatFindings());
}
@Test
public void resultsWithoutGatingRoundTripAsEmptyFindings() throws Exception {
Path cache = new File(temporaryFolder.newFolder("compat-empty"), "validation.json").toPath();
PackValidationCache.save(cache, "content", "context", List.of(
new PackValidationResult("overworld", List.of(), List.of(), 1L)));
PackValidationResult loaded = PackValidationCache.load(
cache, "content", "context", List.of("overworld")).orElseThrow().getFirst();
assertTrue(loaded.getCompatFindings().isEmpty());
assertNull(loaded.getMinecraftVersion());
}
@Test
public void cacheWithAnUnreadableFindingIsIgnored() throws Exception {
Path cache = new File(temporaryFolder.newFolder("compat-corrupt"), "validation.json").toPath();
Files.writeString(cache, """
{
"schemaVersion": 3,
"contentFingerprint": "content",
"contextFingerprint": "context",
"results": [
{"packName":"overworld","blockingErrors":[],"warnings":[],"validatedAtMillis":1,
"compatFindings":[{"registry":"NOT_A_REGISTRY","key":"minecraft:sulfur","action":"EXCLUDED"}]}
]
}
""", StandardCharsets.UTF_8);
assertTrue(PackValidationCache.load(cache, "content", "context", List.of("overworld")).isEmpty());
}
@Test
public void corruptCacheIsIgnored() throws Exception {
Path cache = new File(temporaryFolder.newFolder("corrupt"), "validation.json").toPath();
@@ -77,7 +130,7 @@ public class PackValidationCacheTest {
Path cache = new File(temporaryFolder.newFolder("duplicate"), "validation.json").toPath();
Files.writeString(cache, """
{
"schemaVersion": 2,
"schemaVersion": 3,
"contentFingerprint": "content",
"contextFingerprint": "context",
"results": [
@@ -0,0 +1,130 @@
package art.arcane.iris.core.pack;
import art.arcane.iris.core.compat.CompatAction;
import art.arcane.iris.core.compat.CompatFinding;
import art.arcane.iris.core.compat.CompatRegistry;
import org.junit.Test;
import java.util.ArrayList;
import java.util.List;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
public class PackValidatorCompatTest {
private static CompatFinding block(CompatAction action, String subjectKey) {
return new CompatFinding(
CompatRegistry.BLOCK, "minecraft:sulfur", action, "Biome", subjectKey, "layers[0].palette[1]");
}
private static CompatFinding entity(CompatAction action, String subjectKey) {
return new CompatFinding(
CompatRegistry.ENTITY, "minecraft:camel", action, "Spawner", subjectKey, "spawns[0]");
}
@Test
public void excludedDimensionBecomesABlockingError() {
List<String> blockingErrors = new ArrayList<>();
List<String> warnings = new ArrayList<>();
PackValidator.applyCompatFindings(
"26.1.2",
true,
List.of(block(CompatAction.EXCLUDED, "cave/sulfur"), entity(CompatAction.DROPPED, "desert")),
blockingErrors,
warnings);
assertEquals(1, blockingErrors.size());
assertEquals(
"Pack cannot generate on Minecraft 26.1.2: minecraft:sulfur (block), minecraft:camel (entity)",
blockingErrors.getFirst());
}
@Test
public void unexcludedDimensionAddsNoBlockingError() {
List<String> blockingErrors = new ArrayList<>();
List<String> warnings = new ArrayList<>();
PackValidator.applyCompatFindings(
"26.1.2", false, List.of(block(CompatAction.DROPPED, "cave/sulfur")), blockingErrors, warnings);
assertTrue(blockingErrors.toString(), blockingErrors.isEmpty());
}
@Test
public void excludedDimensionWithoutFindingsStillNamesTheVersion() {
List<String> blockingErrors = new ArrayList<>();
PackValidator.applyCompatFindings("26.1.2", true, List.of(), blockingErrors, new ArrayList<>());
assertEquals(
"Pack cannot generate on Minecraft 26.1.2: the dimension composes content this server does not have",
blockingErrors.getFirst());
assertTrue(PackValidator.excludedDimensionError(null, List.of()).contains("Minecraft unknown"));
}
@Test
public void contentKeyWarningsForGatedKeysAreSuppressed() {
List<String> warnings = new ArrayList<>(List.of(
"Unknown block key 'minecraft:sulfur' (missing from the block registry)",
"Unknown entity key 'minecraft:camel' (missing from the entity registry)",
"Unknown block key 'minecraft:cheese' (missing from the block registry)",
"Duplicate generator 'overworld'"));
PackValidator.applyCompatFindings(
"26.1.2",
false,
List.of(block(CompatAction.EXCLUDED, "cave/sulfur"), entity(CompatAction.DROPPED, "desert")),
new ArrayList<>(),
warnings);
assertEquals(List.of(
"Unknown block key 'minecraft:cheese' (missing from the block registry)",
"Duplicate generator 'overworld'"), warnings);
}
@Test
public void bootLinesCapSubjectsPerKeyAndSummaryCountsActions() {
PackValidationResult result = new PackValidationResult(
"overworld",
List.of(),
List.of(),
1L,
List.of(
block(CompatAction.EXCLUDED, "cave/sulfur-grotto"),
block(CompatAction.EXCLUDED, "desert/sulfur-flats"),
block(CompatAction.DROPPED, "cave/sulfur-grotto"),
block(CompatAction.SUBSTITUTED, "desert/dunes"),
entity(CompatAction.EXCLUDED, "camel")),
"26.1.2");
List<String> lines = PackValidator.compatBootLines(result, null, 3);
assertEquals(4, lines.size());
assertEquals("Pack 'overworld': content unavailable on Minecraft 26.1.2", lines.get(0));
assertTrue(lines.get(1), lines.get(1).startsWith(" minecraft:sulfur (block): "));
assertTrue(lines.get(1), lines.get(1).endsWith("; +1 more"));
assertEquals(3, lines.get(1).split("; ").length - 1);
assertTrue(lines.get(2), lines.get(2).startsWith(" minecraft:camel (entity): "));
assertFalse(lines.get(2), lines.get(2).contains("more"));
assertTrue(lines.get(3), lines.get(3).contains("/iris pack compat overworld"));
assertEquals(
"2 content keys unavailable on Minecraft 26.1.2: 3 excluded, 1 dropped, 1 substituted.",
PackValidator.compatSummary(result, null));
assertEquals(
"2 content keys unavailable on Minecraft 26.2: 3 excluded, 1 dropped, 1 substituted.",
PackValidator.compatSummary(result, "26.2"));
}
@Test
public void bootLinesAndSummaryAreEmptyWithoutFindings() {
PackValidationResult result = new PackValidationResult("overworld", List.of(), List.of(), 1L);
assertTrue(PackValidator.compatBootLines(result, "26.1.2", 3).isEmpty());
assertEquals("", PackValidator.compatSummary(result, "26.1.2"));
assertTrue(PackValidator.compatBootLines(null, "26.1.2", 3).isEmpty());
assertEquals("", PackValidator.compatSummary(null, "26.1.2"));
}
}
@@ -252,6 +252,17 @@ public class SchemaBuilderParityTest {
assertEquals("boolean", properties.getJSONObject("requireObjectSurfaceSupport").getString("type"));
}
@Test
public void dimensionSchemaListsBlockFallbacksAsPlainObjectProperty() {
JSONObject properties = rootProperties(
new SchemaBuilder(IrisDimension.class, structureSchemaData()).construct());
JSONObject fallbacks = properties.getJSONObject("blockFallbacks");
assertEquals("object", fallbacks.getString("type"));
assertFalse(fallbacks.has("!top"));
assertFalse(fallbacks.has("anyOf"));
}
@Test
public void jigsawStructureSchemaExposesBranchFailurePolicy() {
JSONObject schema = new SchemaBuilder(IrisStructure.class, structureSchemaData()).construct();
@@ -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,294 @@
/*
* 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;
}
/**
* A V2 .iob whose palette is {@code palette} and which carries one block per palette entry, entry {@code i} at
* {@code (i, 0, 0)} relative to the object origin, so a placer actually has something to stamp.
*/
static File writeObjectWithBlocks(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(palette.length);
out.writeInt(1);
out.writeInt(1);
out.writeUTF("Iris V2 IOB;");
out.writeShort(palette.length);
for (String entry : palette) {
out.writeUTF(entry);
}
out.writeInt(palette.length);
for (int index = 0; index < palette.length; index++) {
out.writeShort(index);
out.writeShort(0);
out.writeShort(0);
out.writeShort(index);
}
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. The physical predicates
* answer like the real thing too: {@code B} caches the first registries it sees for the whole JVM, so a state
* handed out here can outlive the test that bound this platform.
*/
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 -> {
boolean air = k.endsWith("air");
PlatformBlockState state = mock(PlatformBlockState.class);
when(state.key()).thenReturn(k);
when(state.materialKey()).thenReturn(k);
when(state.isAir()).thenReturn(air);
when(state.isSolid()).thenReturn(!air && !k.endsWith("water"));
when(state.isOccluding()).thenReturn(!air && !k.endsWith("water"));
when(state.isFluid()).thenReturn(k.endsWith("water"));
when(state.isWater()).thenReturn(k.endsWith("water"));
when(state.placementBaseState()).thenReturn(state);
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));
}
}
@@ -0,0 +1,238 @@
/*
* 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.compat.CompatStatus;
import art.arcane.iris.core.loader.IrisData;
import art.arcane.iris.core.pack.PackValidationResult;
import art.arcane.iris.core.pack.PackValidator;
import art.arcane.volmlib.util.collection.KList;
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.assertSame;
import static org.junit.Assert.assertTrue;
/**
* Static objects (absolute-coordinate placements on the dimension) honour the version-content gate the same way a
* one-object placement does: a palette key the server does not have keeps the object out of the static layer and
* is reported against the static object, while a chance-1 edit rule that rewrites the key rescues it.
*/
public class IrisStaticObjectCompatTest {
/** World Y of the entries below, as the static layer stores it (offset from the default dimension floor). */
private static final int LAYER_Y = 100 + 64;
@Rule
public TemporaryFolder temporaryFolder = new TemporaryFolder();
private CompatTestPlatform platform;
private IrisSettings previousSettings;
private IrisObjectRotation.StateRotator previousRotator;
private IrisData data;
@Before
public void bindPlatform() {
previousSettings = IrisSettings.settings;
IrisSettings.settings = new IrisSettings();
// Water, clay and dirt are what a default dimension composes (fluid palette, river bed padding); without
// them the dimension itself is excluded and the static object is never the deciding unit.
platform = CompatTestPlatform.bind(temporaryFolder.getRoot(), List.of("minecraft:stone",
"minecraft:cobblestone", "minecraft:air", "minecraft:water", "minecraft:clay", "minecraft:dirt"));
previousRotator = IrisObjectRotation.bindPlatformRotator((rotation, block, x, y, z) -> block);
}
@After
public void restorePlatform() {
IrisObjectRotation.restorePlatformRotator(previousRotator);
if (data != null) {
data.close();
data = null;
}
if (platform != null) {
platform.unbind();
platform = null;
}
IrisSettings.settings = previousSettings;
}
@Test
public void missingPaletteKeyKeepsTheEntryOutOfTheLayerAndReportsItAsDropped() throws Exception {
File pack = pack("static-drop");
CompatTestPlatform.writeObjectWithBlocks(pack, "landmarks/bad", "minecraft:sulfur");
CompatTestPlatform.writeObjectWithBlocks(pack, "landmarks/good", "minecraft:stone");
data = IrisData.get(pack);
IrisDimension dimension = dimension(entry("landmarks/bad", 0, 100, 0), entry("landmarks/good", 32, 100, 32));
IrisStaticObjectLayer layer = IrisStaticObjectLayer.compile(dimension, data);
assertFalse("the gated entry must not be stamped", layer.contains(0, LAYER_Y, 0));
assertTrue("the entry that resolves still compiles", layer.contains(32, LAYER_Y, 32));
List<CompatFinding> findings = data.getCompatReport().findings();
assertEquals(findings.toString(), 1, findings.size());
CompatFinding finding = findings.getFirst();
assertEquals("minecraft:sulfur", finding.key());
assertEquals(CompatAction.DROPPED, finding.action());
assertEquals("static object", finding.subjectType());
assertEquals("landmarks/bad", finding.subjectKey());
assertTrue(finding.detail(), finding.detail().contains("main staticObjects[0]"));
}
@Test
public void chanceOneEditRuleRescuesTheEntryAndTheRewrittenBlockIsStamped() throws Exception {
File pack = pack("static-edit");
CompatTestPlatform.writeObjectWithBlocks(pack, "bad", "minecraft:sulfur");
data = IrisData.get(pack);
IrisStaticObject entry = entry("bad", 0, 100, 0)
.setEdit(new KList<>(replace(1F, "minecraft:sulfur", "minecraft:stone")));
assertFalse(entry.evaluateCompat(data, "main", 0).excluded());
IrisStaticObjectLayer layer = IrisStaticObjectLayer.compile(dimension(entry), data);
assertTrue(layer.contains(0, LAYER_Y, 0));
assertEquals("minecraft:stone", layer.blocks(0, 0).getFirst().state().key());
assertTrue(data.getCompatReport().findings().toString(), data.getCompatReport().isEmpty());
}
@Test
public void editRuleBelowChanceOneDoesNotCoverTheKey() throws Exception {
File pack = pack("static-edit-chance");
CompatTestPlatform.writeObjectWithBlocks(pack, "bad", "minecraft:sulfur");
data = IrisData.get(pack);
IrisStaticObject entry = entry("bad", 0, 100, 0)
.setEdit(new KList<>(replace(0.5F, "minecraft:sulfur", "minecraft:stone")));
assertTrue(entry.evaluateCompat(data, "main", 0).excluded());
assertTrue(IrisStaticObjectLayer.compile(dimension(entry), data).isEmpty());
}
@Test
public void missingEditPaletteExcludesOnlyTheEntryNeverTheDimension() throws Exception {
File pack = pack("static-edit-missing");
CompatTestPlatform.writeObjectWithBlocks(pack, "good", "minecraft:stone");
data = IrisData.get(pack);
IrisStaticObject entry = entry("good", 0, 100, 0)
.setEdit(new KList<>(replace(1F, "minecraft:stone", "minecraft:sulfur")));
IrisDimension dimension = dimension(entry);
dimension.setLoader(data);
CompatStatus dimensionStatus = dimension.evaluateCompat(data.getContentGate());
assertFalse("a static object's own palette is the static object's problem, not the pack's: "
+ dimensionStatus.reasons(), dimensionStatus.excluded());
assertTrue(dimensionStatus.reasons().toString(), dimensionStatus.reasons().isEmpty());
CompatStatus status = entry.evaluateCompat(data, "main", 0);
assertTrue(status.excluded());
CompatFinding finding = status.reasons().getFirst();
assertEquals(CompatAction.EXCLUDED, finding.action());
assertEquals("static object", finding.subjectType());
assertEquals("good", finding.subjectKey());
assertTrue(finding.detail(), finding.detail().contains("main staticObjects[0] edit[0].replace.palette[0]"));
assertTrue(IrisStaticObjectLayer.compile(dimension, data).isEmpty());
}
@Test
public void dimensionFallbackRescuesTheEntryAndRecordsASubstitution() throws Exception {
File pack = pack("static-fallback");
CompatTestPlatform.write(pack, "dimensions/main.json",
"{\"blockFallbacks\":{\"minecraft:sulfur\":\"minecraft:cobblestone\"}}");
CompatTestPlatform.writeObjectWithBlocks(pack, "bad", "minecraft:sulfur");
data = IrisData.get(pack);
IrisStaticObject entry = entry("bad", 0, 100, 0);
assertFalse(entry.evaluateCompat(data, "main", 0).excluded());
IrisStaticObjectLayer layer = IrisStaticObjectLayer.compile(dimension(entry), data);
assertTrue(layer.contains(0, LAYER_Y, 0));
assertEquals("minecraft:cobblestone", layer.blocks(0, 0).getFirst().state().key());
List<CompatFinding> findings = data.getCompatReport().findings();
assertEquals(findings.toString(), 1, findings.size());
assertEquals(CompatAction.SUBSTITUTED, findings.getFirst().action());
assertEquals("static object", findings.getFirst().subjectType());
}
@Test
public void aCompletePackRecordsNothingAndPlacesEveryEntry() throws Exception {
File pack = pack("static-complete");
CompatTestPlatform.writeObjectWithBlocks(pack, "a", "minecraft:stone");
CompatTestPlatform.writeObjectWithBlocks(pack, "b", "minecraft:cobblestone");
data = IrisData.get(pack);
IrisStaticObjectLayer layer = IrisStaticObjectLayer.compile(
dimension(entry("a", 0, 100, 0), entry("b", 16, 100, 16)), data);
assertTrue(layer.contains(0, LAYER_Y, 0));
assertTrue(layer.contains(16, LAYER_Y, 16));
assertSame(CompatStatus.OK, entry("a", 0, 100, 0).evaluateCompat(null, "main", 0));
assertTrue(data.getCompatReport().isEmpty());
}
@Test
public void packValidationListsStaticObjectsTheServerCannotPlace() throws Exception {
File pack = pack("static-validate");
CompatTestPlatform.writeObjectWithBlocks(pack, "landmarks/bad", "minecraft:sulfur");
CompatTestPlatform.write(pack, "dimensions/main.json",
"{\"staticObjects\":[{\"object\":\"landmarks/bad\",\"position\":{\"x\":0,\"y\":100,\"z\":0}}]}");
PackValidationResult result = PackValidator.validate(pack);
assertTrue(result.getCompatFindings().toString(), result.getCompatFindings().stream().anyMatch(
finding -> finding.action() == CompatAction.DROPPED
&& "static object".equals(finding.subjectType())
&& "landmarks/bad".equals(finding.subjectKey())
&& "minecraft:sulfur".equals(finding.key())));
assertFalse("a static object never makes the pack unusable: " + result.getBlockingErrors(),
result.getBlockingErrors().stream().anyMatch(error -> error.contains("cannot generate on Minecraft")));
assertTrue(result.getCompatFindings().toString(), result.getCompatFindings().stream()
.noneMatch(finding -> "dimension".equals(finding.subjectType())));
}
private File pack(String name) throws Exception {
return temporaryFolder.newFolder(name);
}
private static IrisDimension dimension(IrisStaticObject... entries) {
IrisDimension dimension = new IrisDimension().setStaticObjects(new KList<>(entries));
dimension.setLoadKey("main");
return dimension;
}
private static IrisStaticObject entry(String key, int x, int y, int z) {
return new IrisStaticObject().setObject(key).setPosition(new IrisPosition(x, y, z));
}
private static IrisObjectReplace replace(float chance, String find, String to) {
IrisObjectReplace rule = new IrisObjectReplace();
rule.setChance(chance);
rule.getFind().add(new IrisBlockData(find));
rule.getReplace().qclear();
rule.getReplace().add(to);
return rule;
}
}