mirror of
https://github.com/VolmitSoftware/Iris.git
synced 2026-10-21 07:46:55 +00:00
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:
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();
|
||||
|
||||
+145
@@ -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;
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user