mirror of
https://github.com/VolmitSoftware/Iris.git
synced 2026-08-28 13:00:43 +00:00
Vwoop
This commit is contained in:
+25
@@ -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));
|
||||
}
|
||||
}
|
||||
+33
-7
@@ -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);
|
||||
|
||||
+66
@@ -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)) {
|
||||
|
||||
+249
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
+58
-8
@@ -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;
|
||||
|
||||
|
||||
+93
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+44
@@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
+30
@@ -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"));
|
||||
}
|
||||
}
|
||||
+2
-2
@@ -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
|
||||
|
||||
+26
-9
@@ -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));
|
||||
|
||||
+89
@@ -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"));
|
||||
}
|
||||
}
|
||||
+84
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+56
-7
@@ -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
|
||||
|
||||
+69
@@ -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));
|
||||
}
|
||||
}
|
||||
+86
@@ -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
|
||||
);
|
||||
}
|
||||
}
|
||||
+184
@@ -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));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user