This commit is contained in:
Brian Neumann-Fopiano
2026-07-16 12:26:01 -04:00
parent 55b7460b9a
commit 699247b1de
327 changed files with 30138 additions and 3934 deletions
@@ -0,0 +1,25 @@
package art.arcane.iris.modded;
import art.arcane.iris.nativegen.NativeStructureLocateResults;
import com.mojang.datafixers.util.Pair;
import net.minecraft.core.BlockPos;
import net.minecraft.core.Holder;
import net.minecraft.world.level.levelgen.structure.Structure;
import org.junit.Test;
import static org.junit.Assert.assertSame;
public class IrisModdedLocateRoutingTest {
@Test
public void mixedLocateSelectsNearestProviderAndPrefersNativeOnTie() {
BlockPos origin = BlockPos.ZERO;
Pair<BlockPos, Holder<Structure>> irisNear = Pair.of(new BlockPos(4, 70, 0), null);
Pair<BlockPos, Holder<Structure>> nativeFar = Pair.of(new BlockPos(8, 70, 0), null);
Pair<BlockPos, Holder<Structure>> nativeNear = Pair.of(new BlockPos(2, 70, 0), null);
Pair<BlockPos, Holder<Structure>> nativeTie = Pair.of(new BlockPos(0, 70, 4), null);
assertSame(irisNear, NativeStructureLocateResults.nearest(origin, irisNear, nativeFar));
assertSame(nativeNear, NativeStructureLocateResults.nearest(origin, irisNear, nativeNear));
assertSame(nativeTie, NativeStructureLocateResults.nearest(origin, irisNear, nativeTie));
}
}
@@ -23,6 +23,7 @@ import java.util.concurrent.TimeUnit;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertTrue;
public class IrisModdedStructureParityTest {
@@ -120,14 +121,18 @@ public class IrisModdedStructureParityTest {
}
@Test
public void possibleBiomeFallbackIsOnlyRequiredForMissingOrEmptyConfigurations() {
Set<String> registered = Set.of("minecraft:deep_ocean", "minecraft:dark_forest", "minecraft:plains");
public void structureBiomeContractRejectsAnEmptyConfiguredSet() {
IllegalStateException error = assertThrows(IllegalStateException.class,
() -> IrisModdedBiomeSource.requireConfiguredStructureBiomeKeys(Set.of()));
assertFalse(IrisModdedBiomeSource.requiresPossibleBiomeFallback(
Set.of("minecraft:deep_ocean", "minecraft:dark_forest"), registered));
assertTrue(IrisModdedBiomeSource.requiresPossibleBiomeFallback(
Set.of("minecraft:deep_ocean", "overworld:missing"), registered));
assertTrue(IrisModdedBiomeSource.requiresPossibleBiomeFallback(Set.of(), registered));
assertEquals("Iris has no configured structure biomes", error.getMessage());
}
@Test
public void structureBiomeContractPreservesConfiguredKeysDuringBootstrap() {
Set<String> configured = Set.of("minecraft:deep_ocean", "minecraft:dark_forest");
assertSame(configured, IrisModdedBiomeSource.requireConfiguredStructureBiomeKeys(configured));
}
@Test
@@ -185,6 +190,27 @@ public class IrisModdedStructureParityTest {
throw new AssertionError("Expected failed engine binding to propagate");
}
@Test
public void structureBiomeBootstrapAllowsOnlyPendingBindingsToUseMetadata() {
IrisModdedChunkGenerator.EngineBinding<String> binding =
new IrisModdedChunkGenerator.EngineBinding<>(1L, TimeUnit.SECONDS);
binding.throwIfFailed("overworld:overworld");
}
@Test
public void structureBiomeBootstrapPropagatesBindingFailure() {
IrisModdedChunkGenerator.EngineBinding<String> binding =
new IrisModdedChunkGenerator.EngineBinding<>(1L, TimeUnit.SECONDS);
IllegalArgumentException failure = new IllegalArgumentException("broken pack");
binding.fail(failure);
IllegalStateException error = assertThrows(IllegalStateException.class,
() -> binding.throwIfFailed("overworld:overworld"));
assertSame(failure, error.getCause());
}
@Test
public void initialEntitySpawnsUseThePaperCompletionMarker() {
assertSame(MantleFlag.INITIAL_SPAWNED_MARKER, ModdedWorldManager.INITIAL_SPAWN_COMPLETION_FLAG);
@@ -35,6 +35,7 @@ import java.util.stream.Stream;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotSame;
import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertTrue;
public class ModdedForcedDatapackTest {
@@ -75,6 +76,71 @@ public class ModdedForcedDatapackTest {
}
}
@Test
public void publishesCompleteStagingDirectoryOverExistingPack() throws IOException {
Path root = Files.createTempDirectory("iris-forced-pack-publish");
try {
Path published = Files.createDirectory(root.resolve("iris"));
Files.writeString(published.resolve("old.txt"), "old", StandardCharsets.UTF_8);
Path staging = Files.createDirectory(root.resolve("staging"));
Files.writeString(staging.resolve("new.txt"), "new", StandardCharsets.UTF_8);
ModdedForcedDatapack.publishDirectory(staging, published);
assertFalse(Files.exists(staging));
assertFalse(Files.exists(published.resolve("old.txt")));
assertEquals("new", Files.readString(published.resolve("new.txt"), StandardCharsets.UTF_8));
assertEquals(List.of("iris"), directoryEntries(root));
} finally {
deleteTree(root);
}
}
@Test
public void publishesStagingDirectoryWhenNoPriorPackExists() throws IOException {
Path root = Files.createTempDirectory("iris-forced-pack-first-publish");
try {
Path published = root.resolve("iris");
Path staging = Files.createDirectory(root.resolve("staging"));
Files.writeString(staging.resolve("pack.mcmeta"), "first", StandardCharsets.UTF_8);
ModdedForcedDatapack.publishDirectory(staging, published);
assertEquals("first",
Files.readString(published.resolve("pack.mcmeta"), StandardCharsets.UTF_8));
assertEquals(List.of("iris"), directoryEntries(root));
} finally {
deleteTree(root);
}
}
@Test
public void restoresPublishedPackWhenAtomicStagingMoveFails() throws IOException {
Path root = Files.createTempDirectory("iris-forced-pack-rollback");
try {
Path published = Files.createDirectory(root.resolve("iris"));
Files.writeString(published.resolve("pack.mcmeta"), "known-good", StandardCharsets.UTF_8);
Path missingStaging = root.resolve("missing-staging");
assertThrows(IOException.class,
() -> ModdedForcedDatapack.publishDirectory(missingStaging, published));
assertEquals("known-good",
Files.readString(published.resolve("pack.mcmeta"), StandardCharsets.UTF_8));
assertEquals(List.of("iris"), directoryEntries(root));
} finally {
deleteTree(root);
}
}
private List<String> directoryEntries(Path root) throws IOException {
try (Stream<Path> entries = Files.list(root)) {
return entries.map((Path path) -> path.getFileName().toString())
.sorted()
.toList();
}
}
private void deleteTree(Path root) throws IOException {
List<Path> paths = new ArrayList<>();
try (Stream<Path> walk = Files.walk(root)) {
@@ -0,0 +1,249 @@
package art.arcane.iris.modded;
import org.junit.Test;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
public class ModdedLifecycleFailureContractTest {
private static final String SOURCE_ROOT_PROPERTY = "iris.moddedCommonSources";
@Test
public void existingDimensionRepointBindsBeforeHandlePublication() throws IOException {
String managerSource = source("ModdedDimensionManager.java");
String create = method(managerSource, "public static Handle create(");
int existingStart = requiredIndex(create, "if (existing != null");
int presentStart = requiredIndex(create, "if (serverAccess.hasLevel(server, key))", existingStart + 1);
int injectionStart = requiredIndex(create, "try {", presentStart);
assertRepointBeforePublication(create.substring(existingStart, presentStart));
assertRepointBeforePublication(create.substring(presentStart, injectionStart));
String generatorSource = source("IrisModdedChunkGenerator.java");
String repointAndBind = method(generatorSource, "void repointAndBind(");
assertBefore(repointAndBind, "ModdedWorldEngines.prepareReplacement(",
"ModdedWorldEngines.installReplacement(");
assertBefore(repointAndBind, "ModdedWorldEngines.installReplacement(", "this.activePack = pack;");
String failure = catchBlock(repointAndBind);
assertTrue(failure.contains("ModdedWorldEngines.closeUnregistered(replacement);"));
assertTrue(failure.contains("addSuppressed("));
String worldEnginesSource = source("ModdedWorldEngines.java");
String installReplacement = method(worldEnginesSource, "static void installReplacement(");
assertTrue(installReplacement.contains("ENGINES.compute("));
assertBefore(installReplacement, "close(current);", "return activeReplacement;");
assertFalse(installReplacement.contains("catch ("));
}
@Test
public void injectionBindsBeforeRegistrationAndRollsBackEveryFailure() throws IOException {
String source = source("ModdedDimensionManager.java");
String injection = method(source, "private static Handle inject(");
assertBefore(injection, "generator.bindLevel(level);", "serverAccess.putLevelIfAbsent(server, key, level);");
assertBefore(injection, "serverAccess.putLevelIfAbsent(server, key, level);",
"server.getPlayerList().addWorldborderListener(level);");
assertFalse(injection.contains("serverAccess.putLevel(server, key, previous);"));
String failure = catchBlock(injection);
assertBefore(failure, "rollbackInjection(", "throw ");
String rollback = method(source, "void rollbackInjection(");
assertBefore(rollback, "serverAccess.hasLevel(server, key)", "serverAccess.removeLevel(server, key);");
assertTrue(rollback.contains("generator.unbindEngine("));
assertTrue(rollback.contains("level.close();"));
assertTrue(rollback.contains("addSuppressed("));
}
@Test
public void persistentReinjectionRethrowsTheOriginalCause() throws IOException {
String source = source("ModdedStartup.java");
String reinjection = method(source, "private static void reinjectPersistentDimensions(");
String failure = catchBlock(reinjection);
assertTrue(failure.contains("LOGGER.error("));
assertFalse(failure.contains("e.toString()"));
assertFalse(failure.contains("continue;"));
assertTrue(failure.contains("throw new IllegalStateException("));
assertTrue(failure.contains(", e);"));
}
@Test
public void packDimensionLoadingNeverSuppressesTheFailureAsNull() throws IOException {
String source = source("ModdedDimensionManager.java");
String loading = method(source, "private static IrisDimension loadPackDimension(");
assertFalse(loading.contains("return null;"));
assertFalse(loading.contains("e.toString()"));
int catchIndex = loading.indexOf("catch (");
if (catchIndex >= 0) {
String failure = blockAt(loading, requiredIndex(loading, "{", catchIndex));
assertTrue(failure.contains("throw "));
assertTrue(failure.contains(", e);") || failure.contains("throw e;"));
}
}
@Test
public void productionPackResolutionNeverUsesTheParityProbeAsData() throws IOException {
String source = source("ModdedWorldEngines.java");
String resolvePack = method(source, "static File resolvePack(");
assertFalse(source.contains("System.getProperty(\"iris.parity\")"));
assertFalse(source.contains("parityPack"));
assertFalse(source.contains("falling back to parity pack"));
assertTrue(resolvePack.contains("throw new IllegalStateException("));
}
@Test
public void engineBootstrapResetsStartupExactlyOnceDuringStop() throws IOException {
String source = source("ModdedEngineBootstrap.java");
String stop = method(source, "public static void stop(");
assertEquals(1, occurrences(stop, "ModdedStartup.reset();"));
}
@Test
public void engineEvictionRetainsTheMappingUntilCloseSucceeds() throws IOException {
String source = source("ModdedWorldEngines.java");
String eviction = method(source, "static void evictOrThrow(");
assertTrue(eviction.contains("ENGINES.computeIfPresent("));
assertFalse(eviction.contains("ENGINES.remove("));
assertBefore(eviction, "close(current);", "return null;");
}
@Test
public void generatorUnbindRetainsOwnershipWhenEvictionFails() throws IOException {
String source = source("IrisModdedChunkGenerator.java");
String unbind = method(source, "synchronized void unbindEngine(ServerLevel level)");
assertFalse(unbind.contains("finally"));
assertBefore(unbind, "ModdedWorldEngines.evictOrThrow(level);", "clearEngineBinding();");
}
@Test
public void engineBootstrapRollsBackEveryPublishedBindingBeforeRethrowing() throws IOException {
String source = source("ModdedEngineBootstrap.java");
String bind = method(source, "public static ModdedPlatform bind(");
String failure = catchBlock(bind);
assertBefore(bind, "createdServices.enableAll();", "runtime = new BoundRuntime(");
assertTrue(bind.contains("rollback.add(() -> GuiHost.suppressDesktop("));
assertTrue(bind.contains("rollback.add(() -> restorePlatform("));
assertTrue(bind.contains("rollback.add(() -> ModdedDimensionManager.restoreAccess("));
assertTrue(bind.contains("rollback.add(() -> IrisObjectRotation.restorePlatformRotator("));
assertTrue(bind.contains("rollback.add(() -> BlockDataMergeSupport.restorePlatformMerger("));
assertTrue(bind.contains("rollback.add(() -> TileData.restorePlatformReader("));
assertTrue(bind.contains("rollback.add(() -> TileData.restorePlatformFactory("));
assertTrue(bind.contains("rollback.add(() -> GuiHost.set("));
assertTrue(bind.contains("rollback.add(() -> DecoratorPlatformHooks.restore("));
assertTrue(bind.contains("bindService(PreservationRegistry.class"));
assertTrue(bind.contains("bindService(EngineEffectsProvider.class"));
assertTrue(bind.contains("bindService(EnginePlatformHooks.class"));
assertTrue(bind.contains("bindService(EngineWorldManagerProvider.class"));
assertTrue(bind.contains("rollback.add(customContentDiscovery::rollback);"));
assertBefore(failure, "createdServices.rollback(failure);", "rollback.restore(failure);");
assertBefore(failure, "rollback.restore(failure);", "throw ");
}
private static void assertRepointBeforePublication(String branch) {
assertBefore(branch, "repointAndBind(", "new Handle(");
assertBefore(branch, "repointAndBind(", "HANDLES.put(");
assertBefore(branch, "repointAndBind(", "return ");
}
private static void assertBefore(String source, String first, String second) {
int firstIndex = requiredIndex(source, first);
int secondIndex = requiredIndex(source, second);
assertTrue(first + " must occur before " + second, firstIndex < secondIndex);
}
private static int requiredIndex(String source, String needle) {
return requiredIndex(source, needle, 0);
}
private static int requiredIndex(String source, String needle, int start) {
int index = source.indexOf(needle, start);
assertTrue("Missing source contract token: " + needle, index >= 0);
return index;
}
private static String source(String fileName) throws IOException {
String sourceRoot = System.getProperty(SOURCE_ROOT_PROPERTY);
assertTrue("Missing system property " + SOURCE_ROOT_PROPERTY,
sourceRoot != null && !sourceRoot.isBlank());
Path sourcePath = Path.of(sourceRoot, "art", "arcane", "iris", "modded", fileName);
return Files.readString(sourcePath);
}
private static String method(String source, String signature) {
int start = requiredIndex(source, signature);
int openBrace = requiredIndex(source, "{", start);
return source.substring(start, matchingBrace(source, openBrace) + 1);
}
private static String catchBlock(String source) {
int catchIndex = requiredIndex(source, "catch (");
int openBrace = requiredIndex(source, "{", catchIndex);
return blockAt(source, openBrace);
}
private static String blockAt(String source, int openBrace) {
return source.substring(openBrace, matchingBrace(source, openBrace) + 1);
}
private static int matchingBrace(String source, int openBrace) {
int depth = 0;
boolean quoted = false;
boolean character = false;
boolean escaped = false;
for (int i = openBrace; i < source.length(); i++) {
char current = source.charAt(i);
if (escaped) {
escaped = false;
continue;
}
if ((quoted || character) && current == '\\') {
escaped = true;
continue;
}
if (!character && current == '"') {
quoted = !quoted;
continue;
}
if (!quoted && current == '\'') {
character = !character;
continue;
}
if (quoted || character) {
continue;
}
if (current == '{') {
depth++;
} else if (current == '}') {
depth--;
if (depth == 0) {
return i;
}
}
}
throw new IllegalArgumentException("Unclosed source block");
}
private static int occurrences(String source, String needle) {
int count = 0;
int offset = 0;
while (true) {
int index = source.indexOf(needle, offset);
if (index < 0) {
return count;
}
count++;
offset = index + needle.length();
}
}
}
@@ -1,9 +1,13 @@
package art.arcane.iris.modded;
import art.arcane.iris.engine.framework.LootResolver;
import art.arcane.iris.engine.object.IrisLootMode;
import art.arcane.volmlib.util.math.RNG;
import net.minecraft.resources.Identifier;
import net.minecraft.resources.ResourceKey;
import net.minecraft.core.BlockPos;
import net.minecraft.core.registries.Registries;
import net.minecraft.world.RandomizableContainer;
import net.minecraft.world.SimpleContainer;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.level.storage.loot.LootTable;
@@ -12,6 +16,7 @@ import org.junit.Test;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import java.lang.reflect.Proxy;
import java.util.concurrent.atomic.AtomicBoolean;
import static org.junit.Assert.assertEquals;
@@ -25,7 +30,7 @@ public class ModdedLootApplierTest {
public void addAppendsSourcesInOrder() {
List<String> sources = new ArrayList<>(List.of("placement-native"));
ModdedLootApplier.injectSources(sources, List.of("dimension-iris", "region-iris"), IrisLootMode.ADD, false);
LootResolver.injectSources(sources, List.of("dimension-iris", "region-iris"), IrisLootMode.ADD, false);
assertEquals(List.of("placement-native", "dimension-iris", "region-iris"), sources);
}
@@ -34,7 +39,7 @@ public class ModdedLootApplierTest {
public void clearRemovesNativeAndIrisSourcesBeforeAdding() {
List<String> sources = new ArrayList<>(List.of("placement-native", "dimension-iris"));
ModdedLootApplier.injectSources(sources, List.of("biome-iris"), IrisLootMode.CLEAR, false);
LootResolver.injectSources(sources, List.of("biome-iris"), IrisLootMode.CLEAR, false);
assertEquals(List.of("biome-iris"), sources);
}
@@ -43,7 +48,7 @@ public class ModdedLootApplierTest {
public void replaceRemovesNativeAndIrisSourcesBeforeAdding() {
List<String> sources = new ArrayList<>(List.of("placement-native", "dimension-iris"));
ModdedLootApplier.injectSources(sources, List.of("biome-iris"), IrisLootMode.REPLACE, false);
LootResolver.injectSources(sources, List.of("biome-iris"), IrisLootMode.REPLACE, false);
assertEquals(List.of("biome-iris"), sources);
}
@@ -58,7 +63,7 @@ public class ModdedLootApplierTest {
public void fallbackDoesNotOverrideExistingNativeOrIrisSources() {
List<String> sources = new ArrayList<>(List.of("placement-native", "dimension-iris"));
ModdedLootApplier.injectSources(sources, List.of("fallback-iris"), IrisLootMode.FALLBACK, false);
LootResolver.injectSources(sources, List.of("fallback-iris"), IrisLootMode.FALLBACK, false);
assertEquals(List.of("placement-native", "dimension-iris"), sources);
}
@@ -67,7 +72,7 @@ public class ModdedLootApplierTest {
public void fallbackAddsSourcesWhenPlacementIsEmpty() {
List<String> sources = new ArrayList<>();
ModdedLootApplier.injectSources(sources, List.of("fallback-iris"), IrisLootMode.FALLBACK, true);
LootResolver.injectSources(sources, List.of("fallback-iris"), IrisLootMode.FALLBACK, true);
assertEquals(List.of("fallback-iris"), sources);
}
@@ -76,7 +81,7 @@ public class ModdedLootApplierTest {
public void zeroMultiplierRemovesNativeAndIrisSources() {
List<String> sources = new ArrayList<>(List.of("placement-native", "dimension-iris"));
ModdedLootApplier.scaleSources(sources, 0D, new RNG(17L));
LootResolver.scaleSources(sources, 0D, new RNG(17L));
assertTrue(sources.isEmpty());
}
@@ -85,7 +90,7 @@ public class ModdedLootApplierTest {
public void unitMultiplierPreservesNativeAndIrisSources() {
List<String> sources = new ArrayList<>(List.of("placement-native", "dimension-iris"));
ModdedLootApplier.scaleSources(sources, 1D, new RNG(17L));
LootResolver.scaleSources(sources, 1D, new RNG(17L));
assertEquals(List.of("placement-native", "dimension-iris"), sources);
}
@@ -94,7 +99,7 @@ public class ModdedLootApplierTest {
public void doubleMultiplierScalesTheUnifiedSourceList() {
List<String> sources = new ArrayList<>(List.of("placement-native", "dimension-iris"));
ModdedLootApplier.scaleSources(sources, 2D, new RNG(17L));
LootResolver.scaleSources(sources, 2D, new RNG(17L));
assertEquals(4, sources.size());
assertEquals("placement-native", sources.get(0));
@@ -142,6 +147,51 @@ public class ModdedLootApplierTest {
assertTrue(container.changed);
}
@Test
public void doubleChestHasExactlyOneCanonicalHalf() {
BlockPos leftPos = new BlockPos(0, 64, 0);
BlockPos rightPos = new BlockPos(1, 64, 0);
assertTrue(ModdedLootApplier.isCanonicalPair(leftPos, rightPos));
assertFalse(ModdedLootApplier.isCanonicalPair(rightPos, leftPos));
}
@Test
public void nativeLootTableIsDetectedBeforeContainerAccess() {
ResourceKey<LootTable> key = ResourceKey.create(
Registries.LOOT_TABLE,
Identifier.parse("minecraft:chests/simple_dungeon")
);
RandomizableContainer empty = containerWithLootTable(null);
RandomizableContainer nativeLoot = containerWithLootTable(key);
assertFalse(ModdedLootApplier.hasNativeLootTable(empty));
assertTrue(ModdedLootApplier.hasNativeLootTable(nativeLoot));
}
private RandomizableContainer containerWithLootTable(ResourceKey<LootTable> key) {
return (RandomizableContainer) Proxy.newProxyInstance(
RandomizableContainer.class.getClassLoader(),
new Class<?>[]{RandomizableContainer.class},
(proxy, method, args) -> {
if (method.getName().equals("getLootTable")) {
return key;
}
Class<?> returnType = method.getReturnType();
if (returnType == boolean.class) {
return false;
}
if (returnType == int.class) {
return 0;
}
if (returnType == long.class) {
return 0L;
}
return null;
}
);
}
private static final class TrackingContainer extends SimpleContainer {
private boolean changed;
@@ -0,0 +1,93 @@
package art.arcane.iris.modded;
import art.arcane.iris.modded.service.ModdedService;
import org.junit.Test;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertThrows;
public class ModdedServiceManagerTest {
@Test
public void enablesAndDisablesEveryRegisteredService() {
ModdedServiceManager manager = new ModdedServiceManager();
FirstService first = manager.register(FirstService.class, new FirstService());
SecondService second = manager.register(SecondService.class, new SecondService(null, null));
manager.enableAll();
manager.disableAll();
assertEquals(1, first.enableCount);
assertEquals(1, first.disableCount);
assertEquals(1, second.enableCount);
assertEquals(1, second.disableCount);
}
@Test
public void rollsBackEnabledServicesAndPreservesCleanupFailures() {
ModdedServiceManager manager = new ModdedServiceManager();
FirstService first = manager.register(FirstService.class, new FirstService());
RuntimeException original = new RuntimeException("enable failed");
RuntimeException cleanup = new RuntimeException("cleanup failed");
SecondService second = manager.register(
SecondService.class, new SecondService(original, cleanup));
RuntimeException thrown = assertThrows(RuntimeException.class, manager::enableAll);
assertSame(original, thrown);
assertEquals(1, first.enableCount);
assertEquals(1, first.disableCount);
assertEquals(1, second.enableCount);
assertEquals(1, second.disableCount);
assertEquals(1, thrown.getSuppressed().length);
assertSame(cleanup, thrown.getSuppressed()[0]);
manager.rollback(thrown);
assertNull(manager.service(FirstService.class));
assertNull(manager.service(SecondService.class));
}
private static final class FirstService implements ModdedService {
private int enableCount;
private int disableCount;
@Override
public void onEnable() {
enableCount++;
}
@Override
public void onDisable() {
disableCount++;
}
}
private static final class SecondService implements ModdedService {
private final RuntimeException enableFailure;
private final RuntimeException disableFailure;
private int enableCount;
private int disableCount;
private SecondService(RuntimeException enableFailure, RuntimeException disableFailure) {
this.enableFailure = enableFailure;
this.disableFailure = disableFailure;
}
@Override
public void onEnable() {
enableCount++;
if (enableFailure != null) {
throw enableFailure;
}
}
@Override
public void onDisable() {
disableCount++;
if (disableFailure != null) {
throw disableFailure;
}
}
}
}
@@ -11,6 +11,7 @@ import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertTrue;
public class ModdedStructureHooksTest {
@@ -51,4 +52,47 @@ public class ModdedStructureHooksTest {
assertEquals(320, ModdedPlatformWorld.exclusiveMaxHeight(-64, 384));
assertEquals(384, ModdedPlatformWorld.exclusiveMaxHeight(0, 384));
}
@Test
public void structureRegistryHooksRejectUnavailableServer() {
ModdedStructureHooks hooks = new ModdedStructureHooks(() -> null);
IllegalStateException structureKeys = assertThrows(
IllegalStateException.class, hooks::structureKeys);
IllegalStateException structureSetKeys = assertThrows(
IllegalStateException.class, hooks::structureSetKeys);
IllegalStateException structureBiomeKeys = assertThrows(
IllegalStateException.class, () -> hooks.structureBiomeKeys("minecraft:village"));
assertTrue(structureKeys.getMessage().contains("before the Minecraft server is available"));
assertTrue(structureSetKeys.getMessage().contains("before the Minecraft server is available"));
assertTrue(structureBiomeKeys.getMessage().contains("before the Minecraft server is available"));
}
@Test
public void structureReachabilityHooksRejectUnavailableLevel() {
ModdedStructureHooks hooks = new ModdedStructureHooks(() -> null);
IllegalStateException reachable = assertThrows(
IllegalStateException.class, () -> hooks.reachableStructureKeys(null));
IllegalStateException possibleBiomes = assertThrows(
IllegalStateException.class, () -> hooks.possibleBiomeKeys(null));
assertTrue(reachable.getMessage().contains("without a bound modded ServerLevel"));
assertTrue(possibleBiomes.getMessage().contains("without a bound modded ServerLevel"));
}
@Test
public void structureRegistryHooksPreserveServerSupplierFailure() {
IllegalStateException cause = new IllegalStateException("server supplier failed");
ModdedStructureHooks hooks = new ModdedStructureHooks(() -> {
throw cause;
});
IllegalStateException failure = assertThrows(
IllegalStateException.class, hooks::structureKeys);
assertTrue(failure.getMessage().contains("access the Minecraft server"));
assertEquals(cause, failure.getCause());
}
}
@@ -0,0 +1,30 @@
package art.arcane.iris.modded;
import org.junit.Test;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
public class ModdedStructurePlacementFailureContractTest {
@Test
public void structurePlacementPropagatesRuntimeFailuresWithContext() throws IOException {
Path sourcePath = Path.of(System.getProperty("iris.moddedCommonSources"),
"art/arcane/iris/modded/ModdedStructureHooks.java");
String source = Files.readString(sourcePath);
int methodStart = source.indexOf("public int[] placeStructure(");
int methodEnd = source.indexOf("\n @Override", methodStart + 1);
String method = source.substring(methodStart, methodEnd);
int catchStart = method.indexOf("catch (RuntimeException error)");
assertTrue(catchStart >= 0);
String failurePath = method.substring(catchStart);
assertTrue(failurePath.contains("throw NativeStructureGenerationException.failure("));
assertTrue(failurePath.contains("\"capture placement\", structureKey, chunkX, chunkZ, error"));
assertFalse(failurePath.contains("return null;"));
assertFalse(failurePath.contains("catch (Throwable"));
}
}
@@ -39,8 +39,8 @@ public class ModdedTileParityTest {
SharedConstants.tryDetectVersion();
Bootstrap.bootStrap();
registries = RegistryAccess.fromRegistryOfRegistries(BuiltInRegistries.REGISTRY);
TileData.bindFallbackReader(new ModdedTileReader(() -> null));
TileData.bindFallbackFactory(ModdedTileData::fromProperties);
TileData.bindPlatformReader(new ModdedTileReader(() -> null));
TileData.bindPlatformFactory(ModdedTileData::fromProperties);
}
@Test
@@ -5,6 +5,9 @@ import net.minecraft.world.level.ChunkPos;
import net.minecraft.world.level.levelgen.structure.BoundingBox;
import org.junit.Test;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.atomic.AtomicBoolean;
@@ -23,6 +26,29 @@ public class ModdedWorldCheckTest {
assertFalse(thread.isDaemon());
}
@Test
public void poiAuditRunsInASecondServerTaskAfterVillageGeneration() throws IOException {
Path sourcePath = Path.of(System.getProperty("iris.moddedCommonSources"),
"art/arcane/iris/modded/ModdedWorldCheck.java");
String source = Files.readString(sourcePath);
int preparationSubmit = source.indexOf(
"WorldCheckPreparation preparation = serverRef.submit(() -> run(serverRef)).join();");
int completionSubmit = source.indexOf(
"exitCode = serverRef.submit(() -> runAndRequestStop(", preparationSubmit);
int completionMethod = source.indexOf("private static boolean completeWorldCheck");
int deferredAudit = source.indexOf("PoiAudit poi = auditStructurePois", completionMethod);
int structureMethod = source.indexOf("private static StructureCheckResult checkNativeStructure");
int structureMethodEnd = source.indexOf("private static StructureStart resolveStructureStart",
structureMethod);
String structureSource = source.substring(structureMethod, structureMethodEnd);
assertTrue(preparationSubmit >= 0);
assertTrue(completionSubmit > preparationSubmit);
assertTrue(deferredAudit > completionMethod);
assertFalse(structureSource.contains("auditStructurePois"));
assertFalse(source.contains("prepareDeferredAudits"));
}
@Test
public void validStructureStartIsGenerationEvidence() {
assertTrue(ModdedWorldCheck.hasNativeStructureEvidence(true, 0));
@@ -115,15 +141,6 @@ public class ModdedWorldCheckTest {
assertFalse(ModdedWorldCheck.villageFoundationPass(1));
}
@Test
public void villageFoundationAuditRejectsMissingBaseAndInvalidSupport() {
assertTrue(ModdedWorldCheck.villageFoundationSupported(true, false, true, false));
assertTrue(ModdedWorldCheck.villageFoundationSupported(true, true, false, false));
assertFalse(ModdedWorldCheck.villageFoundationSupported(false, false, true, false));
assertFalse(ModdedWorldCheck.villageFoundationSupported(true, false, false, false));
assertFalse(ModdedWorldCheck.villageFoundationSupported(true, false, true, true));
}
@Test
public void villagePoiGateRequiresInBoundsPoiWithoutOutOfBoundsRecords() {
assertTrue(ModdedWorldCheck.villagePoiPass(1, 0));
@@ -0,0 +1,89 @@
package art.arcane.iris.modded;
import art.arcane.iris.nativegen.NativeStructureGenerationException;
import net.minecraft.SharedConstants;
import net.minecraft.server.Bootstrap;
import org.junit.BeforeClass;
import org.junit.Test;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertTrue;
public class NativeStructureFailureContractTest {
@BeforeClass
public static void bootstrapMinecraftRegistries() {
SharedConstants.tryDetectVersion();
Bootstrap.bootStrap();
}
@Test
public void structureLocateDoesNotCatchAndFallThroughToAnotherImplementation() throws IOException {
Path sourcePath = Path.of(System.getProperty("iris.moddedCommonSources"),
"art/arcane/iris/modded/IrisModdedChunkGenerator.java");
String source = Files.readString(sourcePath);
int locateStart = source.indexOf("public Pair<BlockPos, Holder<Structure>> findNearestMapStructure");
int locateEnd = source.indexOf("public boolean isNativeStructureReachable", locateStart);
String locate = source.substring(locateStart, locateEnd);
int filterStart = source.indexOf("private HolderSet<Structure> filterReachableNativeStructures");
int filterEnd = source.indexOf("private ServerLevel boundLevel", filterStart);
String filter = source.substring(filterStart, filterEnd);
assertTrue(locate.contains("Engine current = engine();"));
assertFalse(locate.contains("catch (Throwable"));
assertFalse(locate.contains("return null;\n } catch"));
assertTrue(filter.contains("unregistered structure holder"));
assertFalse(filter.contains("catch (Throwable"));
assertFalse(filter.contains("failed closed"));
}
@Test
public void globalStructureDisableRefusesGeneratorBinding() {
IrisModdedChunkGenerator.requireGlobalStructureGeneration(true, "overworld:overworld");
IllegalStateException error = assertThrows(
IllegalStateException.class,
() -> IrisModdedChunkGenerator.requireGlobalStructureGeneration(
false, "overworld:overworld"));
assertTrue(error.getMessage().contains("overworld:overworld"));
assertTrue(error.getMessage().contains("generate-structures=false"));
assertTrue(error.getMessage().contains("importedStructures.disabled"));
}
@Test
public void structureTerrainPreparationPrecedesVegetationAndPlacement() throws IOException {
Path sourcePath = Path.of(System.getProperty("iris.moddedCommonSources"),
"art/arcane/iris/modded/IrisModdedChunkGenerator.java");
String source = Files.readString(sourcePath);
int placementStart = source.indexOf("private void placeVanillaStructures");
int placementEnd = source.indexOf("private static String nativeStructureBatchContext", placementStart);
String placement = source.substring(placementStart, placementEnd);
assertTrue(placement.contains("\"terrain integration\""));
assertTrue(placement.contains("prepareSurfaceStructures"));
assertTrue(placement.contains("clearIntersectingVegetation"));
assertTrue(placement.indexOf("prepareSurfaceStructures")
< placement.indexOf("clearIntersectingVegetation"));
assertTrue(placement.indexOf("clearIntersectingVegetation")
< placement.indexOf("for (NativePlacementGroup group"));
}
@Test
public void structureFailurePreservesPhaseIdentityChunkAndCause() {
IllegalArgumentException cause = new IllegalArgumentException("broken placement");
NativeStructureGenerationException error = NativeStructureGenerationException.failure(
"placement", "minecraft:monument", 12, -8, cause);
assertSame(cause, error.getCause());
assertTrue(error.getMessage().contains("placement"));
assertTrue(error.getMessage().contains("minecraft:monument"));
assertTrue(error.getMessage().contains("12,-8"));
assertTrue(error.getMessage().contains("aborted"));
}
}
@@ -0,0 +1,84 @@
package art.arcane.iris.modded.api;
import net.minecraft.resources.Identifier;
import org.junit.Test;
import java.util.Collection;
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.assertThrows;
import static org.junit.Assert.assertTrue;
public class ModdedCustomContentRegistryTest {
@Test
public void publishesProvidersOnlyAfterCompleteDiscoveryAndCanRollBack() {
String modId = "iris_discovery_success";
TestProvider provider = new TestProvider(modId, null);
boolean previousDiscoveryComplete = ModdedCustomContentRegistry.discoveryComplete();
ModdedCustomContentRegistry.Discovery discovery =
ModdedCustomContentRegistry.discover(List.of(provider));
try {
assertTrue(ModdedCustomContentRegistry.discoveryComplete());
assertTrue(ModdedCustomContentRegistry.hasProvider(modId));
} finally {
discovery.rollback();
}
assertEquals(previousDiscoveryComplete, ModdedCustomContentRegistry.discoveryComplete());
assertFalse(ModdedCustomContentRegistry.hasProvider(modId));
}
@Test
public void failedDiscoveryPublishesNothingAndPreservesTheCause() {
String firstModId = "iris_discovery_staged";
String failingModId = "iris_discovery_failure";
RuntimeException original = new RuntimeException("provider init failed");
boolean previousDiscoveryComplete = ModdedCustomContentRegistry.discoveryComplete();
RuntimeException thrown = assertThrows(RuntimeException.class,
() -> ModdedCustomContentRegistry.discover(List.of(
new TestProvider(firstModId, null),
new TestProvider(failingModId, original))));
assertSame(original, thrown);
assertEquals(previousDiscoveryComplete, ModdedCustomContentRegistry.discoveryComplete());
assertFalse(ModdedCustomContentRegistry.hasProvider(firstModId));
assertFalse(ModdedCustomContentRegistry.hasProvider(failingModId));
}
private static final class TestProvider implements ModdedDataProvider {
private final String modId;
private final RuntimeException failure;
private TestProvider(String modId, RuntimeException failure) {
this.modId = modId;
this.failure = failure;
}
@Override
public String modId() {
return modId;
}
@Override
public Collection<Identifier> getTypes(ModdedDataType type) {
return List.of();
}
@Override
public boolean isValidProvider(Identifier id, ModdedDataType type) {
return false;
}
@Override
public void init() {
if (failure != null) {
throw failure;
}
}
}
}
@@ -20,6 +20,7 @@ public class IrisModdedStructureCommandTest {
assertTrue(source.contains("generator.findNearestMapStructure("));
assertTrue(source.contains("NATIVE_STRUCTURE_LOCATE_RADIUS = 100"));
assertTrue(source.contains("HolderSet.direct(target.holder())"));
assertFalse(source.contains("NativeStructureLocateCapability"));
assertTrue(source.contains("boolean teleported = player.teleportTo("));
assertTrue(source.contains("combineStructureKeys(irisKeys, nativeKeys)"));
assertTrue(source.contains("irisGenerator.isNativeStructureReachable(holder)"));
@@ -33,16 +34,64 @@ public class IrisModdedStructureCommandTest {
}
@Test
public void generatorLocatePrefersIrisPlacementsAndRejectsDormantNativeStarts() throws IOException {
public void generatorLocateUsesIrisOnlyForExplicitReplacement() throws IOException {
String source = moddedSource("IrisModdedChunkGenerator.java");
int methodStart = source.indexOf("private Pair<BlockPos, Holder<Structure>> findNearestIrisStructure(");
int methodEnd = source.indexOf("private HolderSet<Structure> filterReachableNativeStructures(", methodStart);
String method = source.substring(methodStart, methodEnd);
int unexploredGuard = method.indexOf("if (findUnexplored)");
int registryLookup = method.indexOf("level.registryAccess().lookupOrThrow(Registries.STRUCTURE)");
int policyResolution = method.indexOf("NativeStructureGenerationPolicy.resolve(current,");
int replacementCheck = method.indexOf(
"decision.status() != NativeStructureGenerationStatus.REPLACED_BY_IRIS", policyResolution);
int irisLocate = method.indexOf("IrisStructureLocator.locate(", replacementCheck);
assertTrue(source.contains("public Pair<BlockPos, Holder<Structure>> findNearestMapStructure("));
assertTrue(source.contains("findNearestIrisStructure("));
assertTrue(source.contains("filterReachableNativeStructures("));
assertTrue(source.contains("IrisStructureLocator.suppressesVanilla(current, key)"));
assertTrue(unexploredGuard >= 0);
assertTrue(registryLookup > unexploredGuard);
assertTrue(policyResolution > registryLookup);
assertTrue(replacementCheck > policyResolution);
assertTrue(irisLocate > replacementCheck);
assertTrue(method.contains("LocateStatus.SEARCH_LIMIT_REACHED"));
assertTrue(method.contains("new BlockPos(result.originX(), result.baseY(), result.originZ())"));
assertFalse(method.contains("NativeStructureLocateCapability"));
assertTrue(source.contains("structureBiomeSource.isStructureReachable(holder)"));
assertTrue(source.contains("LocateStatus.SEARCH_LIMIT_REACHED"));
assertTrue(source.contains("new BlockPos(result.originX(), result.baseY(), result.originZ())"));
assertFalse(source.contains("isPaperUnavailable"));
}
@Test
public void commandResolvesNativePolicyBeforeAnyVanillaAliasLookup() throws IOException {
String source = source("IrisModdedCommands.java");
int methodStart = source.indexOf("private static int gotoStructure(");
int methodEnd = source.indexOf("private static void locateIrisStructure(", methodStart);
String method = source.substring(methodStart, methodEnd);
int nativeResolution = method.indexOf("resolveNativeStructure(source, level, engine, key)");
int genericIrisLookup = method.indexOf("IrisStructureLocator.isPlaced(engine, key)", nativeResolution);
int policyResolution = method.indexOf("NativeStructureGenerationPolicy.resolve(engine, target.key(), false)", genericIrisLookup);
int replacementCheck = method.indexOf(
"decision.status() == NativeStructureGenerationStatus.REPLACED_BY_IRIS", policyResolution);
int replacementLocate = method.indexOf("locateIrisStructure(source, level, engine, player, target.key())",
replacementCheck);
assertTrue(nativeResolution >= 0);
assertTrue(genericIrisLookup > nativeResolution);
assertTrue(policyResolution > genericIrisLookup);
assertTrue(replacementCheck > policyResolution);
assertTrue(replacementLocate > replacementCheck);
}
@Test
public void verifyResolvesRegisteredNativeBeforeGenericIrisAliases() throws IOException {
String source = source("IrisModdedCommands.java");
int methodStart = source.indexOf("private static int verifyStructure(");
int methodEnd = source.indexOf("private static Optional<NativeStructureTarget> resolveNativeStructure(",
methodStart);
String method = source.substring(methodStart, methodEnd);
int nativeResolution = method.indexOf("resolveNativeStructure(source, level, engine, key)");
int genericIrisLookup = method.indexOf("IrisStructureLocator.isPlaced(engine, key)", nativeResolution);
assertTrue(nativeResolution >= 0);
assertTrue(genericIrisLookup > nativeResolution);
assertTrue(method.contains("NativeStructureAvailability.IRIS_SUPPRESSED"));
}
@Test
@@ -0,0 +1,69 @@
/*
* Iris is a World Generator for Minecraft Servers
* Copyright (c) 2026 Arcane Arts (Volmit Software)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package art.arcane.iris.modded.structure;
import net.minecraft.core.Direction;
import org.junit.Test;
import java.util.Map;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotEquals;
public class ModdedJigsawStructureCaptureTest {
@Test
public void namespacedSourcePathsRemainDistinctAndPortable() {
String nested = ModdedJigsawStructureCapture.pieceName("village", "mod:a/b");
String underscored = ModdedJigsawStructureCapture.pieceName("village", "mod_a:b");
assertEquals("village/piece/mod/a/b", nested);
assertEquals("village/piece/mod_a/b", underscored);
assertNotEquals(nested, underscored);
assertEquals("village/pool/mod/a/b", ModdedJigsawStructureCapture.poolName("village", "mod:a/b"));
assertEquals(
"village/piece/generated/legacy/mod/a/b",
ModdedJigsawStructureCapture.legacyPieceName("village", "mod:a/b")
);
}
@Test
public void rootJsonRetainsGraphLimitsAndSourceIdentity() {
Map<String, Object> root = ModdedJigsawStructureCapture.structureJson(
"minecraft:village_plains",
"village/pool/minecraft/village/plains/town_centers",
6,
81
);
assertEquals("minecraft:village_plains", root.get("vanillaSource"));
assertEquals(6, root.get("maxDepth"));
assertEquals(6, root.get("maxSizeChunks"));
assertEquals("STRUCTURE_PIECE", root.get("placeMode"));
}
@Test
public void connectorDirectionsUseIrisAxisNames() {
assertEquals("UP_POSITIVE_Y", ModdedJigsawStructureCapture.directionName(Direction.UP));
assertEquals("DOWN_NEGATIVE_Y", ModdedJigsawStructureCapture.directionName(Direction.DOWN));
assertEquals("NORTH_NEGATIVE_Z", ModdedJigsawStructureCapture.directionName(Direction.NORTH));
assertEquals("SOUTH_POSITIVE_Z", ModdedJigsawStructureCapture.directionName(Direction.SOUTH));
assertEquals("EAST_POSITIVE_X", ModdedJigsawStructureCapture.directionName(Direction.EAST));
assertEquals("WEST_NEGATIVE_X", ModdedJigsawStructureCapture.directionName(Direction.WEST));
}
}
@@ -0,0 +1,86 @@
/*
* Iris is a World Generator for Minecraft Servers
* Copyright (c) 2026 Arcane Arts (Volmit Software)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package art.arcane.iris.modded.structure;
import art.arcane.iris.core.structure.authoring.StructureBackend;
import art.arcane.iris.core.structure.authoring.StructureCapability;
import art.arcane.iris.core.structure.authoring.StructureKey;
import art.arcane.iris.core.structure.authoring.StructureResourceBundle;
import art.arcane.iris.core.structure.authoring.StructureSource;
import art.arcane.iris.core.structure.authoring.StructureWriteMode;
import art.arcane.iris.core.structure.authoring.StructureWriteOptions;
import art.arcane.iris.core.structure.authoring.StructureWriteResult;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TemporaryFolder;
import java.nio.file.Path;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
public class ModdedStructureImportServiceTest {
@Rule
public TemporaryFolder temporaryFolder = new TemporaryFolder();
@Test
public void writesThroughOwnedAddOnlyAndOverwriteTransactions() throws Exception {
Path root = temporaryFolder.newFolder("modded-import").toPath();
ModdedStructureImportService service = new ModdedStructureImportService(() -> null);
ModdedStructureImportService.PreparedImport first = prepared(root, "one", StructureWriteMode.ADD_ONLY);
ModdedStructureImportService.ImportResult added = service.write(first);
ModdedStructureImportService.ImportResult conflict = service.write(first);
ModdedStructureImportService.ImportResult overwritten = service.write(
prepared(root, "two", StructureWriteMode.OVERWRITE)
);
assertTrue(added.success());
assertEquals(StructureWriteResult.Status.ADDED, added.writeResult().orElseThrow().status());
assertFalse(conflict.success());
assertEquals(StructureWriteResult.Status.ADD_ONLY_CONFLICT, conflict.writeResult().orElseThrow().status());
assertTrue(overwritten.success());
assertEquals(StructureWriteResult.Status.OVERWRITTEN, overwritten.writeResult().orElseThrow().status());
assertTrue(overwritten.capabilities().contains(StructureCapability.BLOCKS));
}
private static ModdedStructureImportService.PreparedImport prepared(
Path root,
String content,
StructureWriteMode mode
) {
StructureKey key = StructureKey.parse("iris:test_structure");
StructureResourceBundle bundle = StructureResourceBundle.builder(key)
.source(StructureSource.of(StructureSource.Kind.DATAPACK, StructureKey.parse("test:source")))
.backend(StructureBackend.SNAPSHOT)
.capability(StructureCapability.BLOCKS)
.textResource("structures/test_structure.json", content)
.build();
return new ModdedStructureImportService.PreparedImport(
root,
new StructureWriteOptions(mode, false),
ModdedStructureImportService.ImportKind.TEMPLATE,
bundle,
1,
1,
1
);
}
}
@@ -0,0 +1,184 @@
/*
* Iris is a World Generator for Minecraft Servers
* Copyright (c) 2026 Arcane Arts (Volmit Software)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package art.arcane.iris.modded.structure;
import art.arcane.iris.core.structure.authoring.StructureCapability;
import art.arcane.iris.core.structure.authoring.StructureKey;
import net.minecraft.SharedConstants;
import net.minecraft.core.registries.BuiltInRegistries;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.nbt.IntTag;
import net.minecraft.nbt.ListTag;
import net.minecraft.nbt.NbtUtils;
import net.minecraft.world.level.block.Blocks;
import net.minecraft.world.level.levelgen.structure.templatesystem.StructureTemplate;
import net.minecraft.server.Bootstrap;
import org.junit.BeforeClass;
import org.junit.Test;
import java.io.ByteArrayOutputStream;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
public class ModdedStructureTemplateCaptureTest {
@BeforeClass
public static void bootstrap() {
SharedConstants.tryDetectVersion();
Bootstrap.bootStrap();
}
@Test
public void capturesBlocksTilesAndExplicitStandaloneLosses() throws Exception {
CompoundTag template = templateTag();
ModdedStructureTemplateCapture.Capture capture = ModdedStructureTemplateCapture.captureTag(
StructureKey.parse("minecraft:test/template"),
template,
BuiltInRegistries.BLOCK,
false
);
assertEquals(4, capture.width());
assertEquals(1, capture.height());
assertEquals(1, capture.depth());
assertEquals(3, capture.blocks());
assertEquals(1, capture.tiles());
assertEquals(1, capture.jigsaws());
assertEquals(1, capture.dataMarkers());
assertEquals(3, capture.object().getBlocks().size());
assertEquals(1, capture.object().getStates().size());
assertTrue(capture.capabilities().contains(StructureCapability.BLOCKS));
assertTrue(capture.capabilities().contains(StructureCapability.BLOCK_ENTITIES));
assertFalse(capture.capabilities().contains(StructureCapability.CONNECTORS));
assertTrue(hasLoss(capture, "connectors_not_imported"));
assertTrue(hasLoss(capture, "data_markers_not_imported"));
assertTrue(hasLoss(capture, "entities_not_imported"));
ByteArrayOutputStream serialized = new ByteArrayOutputStream();
capture.object().write(serialized);
assertTrue(serialized.size() > 0);
}
@Test
public void graphCaptureReportsConnectorCapabilityWithoutStandaloneConnectorLoss() {
ModdedStructureTemplateCapture.Capture capture = ModdedStructureTemplateCapture.captureTag(
StructureKey.parse("minecraft:test/template"),
templateTag(),
BuiltInRegistries.BLOCK,
true
);
assertTrue(capture.capabilities().contains(StructureCapability.CONNECTORS));
assertFalse(hasLoss(capture, "connectors_not_imported"));
}
@Test
public void reportsAdditionalNativePalettes() {
CompoundTag template = templateTag();
ListTag palettes = new ListTag();
ListTag first = template.getListOrEmpty(StructureTemplate.PALETTE_TAG);
palettes.add(first.copy());
palettes.add(first.copy());
template.remove(StructureTemplate.PALETTE_TAG);
template.put(StructureTemplate.PALETTE_LIST_TAG, palettes);
ModdedStructureTemplateCapture.Capture capture = ModdedStructureTemplateCapture.captureTag(
StructureKey.parse("minecraft:test/template"),
template,
BuiltInRegistries.BLOCK,
true
);
assertTrue(hasLoss(capture, "palette_variants_not_imported"));
}
@Test
public void legacyCaptureOmitsAirBlocks() {
CompoundTag template = new CompoundTag();
template.put(StructureTemplate.SIZE_TAG, intList(1, 1, 1));
ListTag palette = new ListTag();
palette.add(NbtUtils.writeBlockState(Blocks.AIR.defaultBlockState()));
template.put(StructureTemplate.PALETTE_TAG, palette);
ListTag blocks = new ListTag();
blocks.add(block(0, 0, 0, 0, null));
template.put(StructureTemplate.BLOCKS_TAG, blocks);
ModdedStructureTemplateCapture.Capture capture = ModdedStructureTemplateCapture.captureTag(
StructureKey.parse("minecraft:test/legacy"),
template,
BuiltInRegistries.BLOCK,
true,
false
);
assertEquals(0, capture.blocks());
assertTrue(capture.object().getBlocks().isEmpty());
}
private static CompoundTag templateTag() {
CompoundTag template = new CompoundTag();
template.put(StructureTemplate.SIZE_TAG, intList(4, 1, 1));
ListTag palette = new ListTag();
palette.add(NbtUtils.writeBlockState(Blocks.STONE.defaultBlockState()));
palette.add(NbtUtils.writeBlockState(Blocks.CHEST.defaultBlockState()));
palette.add(NbtUtils.writeBlockState(Blocks.JIGSAW.defaultBlockState()));
palette.add(NbtUtils.writeBlockState(Blocks.STRUCTURE_BLOCK.defaultBlockState()));
template.put(StructureTemplate.PALETTE_TAG, palette);
ListTag blocks = new ListTag();
blocks.add(block(0, 0, 0, 0, null));
CompoundTag chest = new CompoundTag();
chest.putString("id", "minecraft:chest");
chest.putString("CustomName", "test");
blocks.add(block(1, 0, 0, 1, chest));
CompoundTag jigsaw = new CompoundTag();
jigsaw.putString("final_state", "minecraft:oak_planks");
blocks.add(block(2, 0, 0, 2, jigsaw));
blocks.add(block(3, 0, 0, 3, new CompoundTag()));
template.put(StructureTemplate.BLOCKS_TAG, blocks);
ListTag entities = new ListTag();
entities.add(new CompoundTag());
template.put(StructureTemplate.ENTITIES_TAG, entities);
return template;
}
private static CompoundTag block(int x, int y, int z, int state, CompoundTag nbt) {
CompoundTag block = new CompoundTag();
block.put(StructureTemplate.BLOCK_TAG_POS, intList(x, y, z));
block.putInt(StructureTemplate.BLOCK_TAG_STATE, state);
if (nbt != null) {
block.put(StructureTemplate.BLOCK_TAG_NBT, nbt);
}
return block;
}
private static ListTag intList(int... values) {
ListTag list = new ListTag();
for (int value : values) {
list.add(IntTag.valueOf(value));
}
return list;
}
private static boolean hasLoss(ModdedStructureTemplateCapture.Capture capture, String code) {
return capture.losses().stream().anyMatch(loss -> loss.code().equals(code));
}
}