diff --git a/adapters/bukkit/nms/v26_2_R1/logs/latest.log b/adapters/bukkit/nms/v26_2_R1/logs/latest.log index 82344e949..7ac5d39de 100644 --- a/adapters/bukkit/nms/v26_2_R1/logs/latest.log +++ b/adapters/bukkit/nms/v26_2_R1/logs/latest.log @@ -1,3 +1,3 @@ -[01:34:50] [Test worker/INFO]: [STDERR]: [Iris/WARN] Native structure terrain envelope at 0,0 was clipped to Minecraft's 8-chunk structure reference range -[01:34:50] [Test worker/INFO]: [STDERR]: [Iris/WARN] Native structure burial at 0,0 clamped to world floor: wanted -19, used -4 -[01:34:50] [Test worker/INFO]: [STDERR]: [Iris/WARN] Native structure burial at 0,0 clamped to world floor: wanted -5, used -2 +[10:47:09] [Test worker/INFO]: [STDERR]: [Iris/WARN] Native structure terrain envelope at 0,0 was clipped to Minecraft's 8-chunk structure reference range +[10:47:09] [Test worker/INFO]: [STDERR]: [Iris/WARN] Native structure burial at 0,0 clamped to world floor: wanted -19, used -4 +[10:47:09] [Test worker/INFO]: [STDERR]: [Iris/WARN] Native structure burial at 0,0 clamped to world floor: wanted -5, used -2 diff --git a/adapters/bukkit/nms/v26_2_R1/src/main/java/art/arcane/iris/core/nms/v26_2_R1/VanillaStructureBiomes.java b/adapters/bukkit/nms/v26_2_R1/src/main/java/art/arcane/iris/core/nms/v26_2_R1/VanillaStructureBiomes.java index d78966f38..7e0946386 100644 --- a/adapters/bukkit/nms/v26_2_R1/src/main/java/art/arcane/iris/core/nms/v26_2_R1/VanillaStructureBiomes.java +++ b/adapters/bukkit/nms/v26_2_R1/src/main/java/art/arcane/iris/core/nms/v26_2_R1/VanillaStructureBiomes.java @@ -53,15 +53,19 @@ final class VanillaStructureBiomes { if (structure == null) { throw new IllegalArgumentException("Registered structure does not exist: " + structureKey); } + boolean hasFilterEntries = false; for (Holder holder : structure.biomes()) { + hasFilterEntries = true; Optional> key = holder.unwrapKey(); if (key.isPresent()) { keys.add(key.get().identifier().toString()); } } - if (keys.isEmpty()) { + // An empty biome filter is legal datapack content (opt-in structures whose tags only + // reference absent modded biomes); the structure is unreachable, not an error state. + if (keys.isEmpty() && hasFilterEntries) { throw new IllegalStateException("Registered structure '" + structureKey - + "' exposes no registered biome keys"); + + "' has biome filter entries but none resolve to registered biome keys"); } return keys; } diff --git a/adapters/bukkit/nms/v26_2_R1/src/test/java/art/arcane/iris/core/nms/v26_2_R1/VanillaStructureBiomesEmptyFilterContractTest.java b/adapters/bukkit/nms/v26_2_R1/src/test/java/art/arcane/iris/core/nms/v26_2_R1/VanillaStructureBiomesEmptyFilterContractTest.java new file mode 100644 index 000000000..9112fff88 --- /dev/null +++ b/adapters/bukkit/nms/v26_2_R1/src/test/java/art/arcane/iris/core/nms/v26_2_R1/VanillaStructureBiomesEmptyFilterContractTest.java @@ -0,0 +1,38 @@ +package art.arcane.iris.core.nms.v26_2_R1; + +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; + +/** + * A registered structure with an EMPTY biome filter is legal datapack content: opt-in structures + * (e.g. Towns and Towers' towns_and_towers:exclusives/*) ship biome tags that only reference + * optional modded biomes, so the filter resolves to zero entries on a vanilla server. Resolving + * biome keys for such a structure must return an empty set (the structure is simply unreachable), + * never throw — a throw here aborts the whole /iris structure verify run and degrades /iris find. + * The defensive throw is only kept for a NON-empty filter whose holders all fail to resolve keys. + */ +public class VanillaStructureBiomesEmptyFilterContractTest { + @Test + public void emptyBiomeFilterReturnsEmptyKeysInsteadOfThrowing() throws IOException { + String source = Files.readString(Path.of(System.getProperty("iris.vanillaStructureBiomesSource"))); + + assertTrue(source.contains("if (keys.isEmpty() && hasFilterEntries)")); + assertTrue(source.contains("has biome filter entries but none resolve to registered biome keys")); + assertFalse(source.contains("exposes no registered biome keys")); + } + + @Test + public void moddedHookMatchesEmptyFilterContract() throws IOException { + String source = Files.readString(Path.of(System.getProperty("iris.moddedStructureHooksSource"))); + + assertTrue(source.contains("if (keys.isEmpty() && hasFilterEntries)")); + assertTrue(source.contains("has biome filter entries but none resolve to registered biome keys")); + assertFalse(source.contains("exposes no registered biome keys")); + } +} diff --git a/adapters/bukkit/plugin/src/main/java/art/arcane/iris/Iris.java b/adapters/bukkit/plugin/src/main/java/art/arcane/iris/Iris.java index 6fe36df87..1d5b0c7a2 100644 --- a/adapters/bukkit/plugin/src/main/java/art/arcane/iris/Iris.java +++ b/adapters/bukkit/plugin/src/main/java/art/arcane/iris/Iris.java @@ -581,6 +581,10 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware { watch::normalizeSettingsContent ); configHotloadEngine.configure(3_000L, List.of(watch.settingsFile()), List.of()); + // Stale-temp cleanup must complete before services enable: StudioSVC.onEnable downloads + // packs through cache/temp on an async thread, and a concurrent delete of that folder + // truncated pack imports mid-copy (partial packs/ without dimensions/). + IO.delete(getTemp()); services.values().forEach(IrisService::onEnable); services.values().forEach(this::registerListener); addShutdownHook(); @@ -593,7 +597,6 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware { } J.s(() -> { - J.a(() -> IO.delete(getTemp())); J.a(this::bstats); J.ar(() -> settingsHotloadWatch.checkConfigHotload(configHotloadEngine), 60); J.sr(this::tickQueue, 0); diff --git a/adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/BukkitWorldReconciler.java b/adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/BukkitWorldReconciler.java index 04508489d..9a968077c 100644 --- a/adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/BukkitWorldReconciler.java +++ b/adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/BukkitWorldReconciler.java @@ -57,7 +57,7 @@ public final class BukkitWorldReconciler { assert dim != null && gen != null; Iris.info(C.LIGHT_PURPLE + "Preparing Spawn for " + s + "' using Iris:" + generator + "..."); - WorldCreator c = WorldCreator.ofKey(worldKey) + WorldCreator c = WorldCreatorCompat.ofKey(worldKey) .generator(gen) .environment(BukkitEnvironment.from(dim.getEnvironment())); Long stagedSeed = IrisWorlds.readBukkitWorldSeed(s); diff --git a/adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/IrisWorldGeneratorResolver.java b/adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/IrisWorldGeneratorResolver.java index 5815631b9..30b90364b 100644 --- a/adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/IrisWorldGeneratorResolver.java +++ b/adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/IrisWorldGeneratorResolver.java @@ -22,9 +22,11 @@ import art.arcane.iris.Iris; import art.arcane.iris.core.lifecycle.WorldLifecycleStaging; import art.arcane.iris.core.loader.IrisData; import art.arcane.iris.core.pack.BrokenPackException; +import art.arcane.iris.core.pack.PackDownloader; import art.arcane.iris.core.pack.PackValidationRegistry; import art.arcane.iris.core.pack.PackValidationResult; import art.arcane.iris.core.pack.PackValidator; +import art.arcane.iris.spi.IrisPlatforms; import art.arcane.iris.core.service.StudioSVC; import art.arcane.iris.engine.object.IrisDimension; import art.arcane.iris.engine.object.IrisWorld; @@ -61,6 +63,9 @@ public final class IrisWorldGeneratorResolver { } PackValidationRegistry.clear(); for (File packDir : packDirs) { + if (packDir.getName().contains(".importing-")) { + continue; + } try { PackValidationResult result = PackValidator.validate(packDir); PackValidationRegistry.publish(result); @@ -90,6 +95,12 @@ public final class IrisWorldGeneratorResolver { IrisDimension dimension = pack.isDirectory() ? IrisData.get(pack).getDimensionLoader().load(id) : null; if (dimension == null) dimension = IrisData.loadAnyDimension(id, null); if (dimension == null) { + File packsRoot = IrisPlatforms.get().dataFolderNoCreate(StudioSVC.WORKSPACE_NAME); + if (PackDownloader.isPackPresent(packsRoot, id)) { + Iris.error("Pack '" + id + "' exists at " + new File(packsRoot, id).getPath() + + " but its dimension failed to load; not redownloading. Fix or delete the pack folder."); + return null; + } Iris.warn("Unable to find dimension type " + id + " Looking for online packs..."); Iris.service(StudioSVC.class).downloadSearch(new VolmitSender(Bukkit.getConsoleSender()), id, false); dimension = IrisData.loadAnyDimension(id, null); diff --git a/adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/commands/CommandFind.java b/adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/commands/CommandFind.java index edfbcdfec..3486e693a 100644 --- a/adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/commands/CommandFind.java +++ b/adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/commands/CommandFind.java @@ -40,8 +40,7 @@ import art.arcane.volmlib.util.collection.KList; import art.arcane.volmlib.util.director.DirectorOrigin; import art.arcane.volmlib.util.director.annotations.Director; import art.arcane.volmlib.util.director.annotations.Param; -import io.papermc.paper.registry.RegistryAccess; -import io.papermc.paper.registry.RegistryKey; +import org.bukkit.Bukkit; import org.bukkit.Location; import org.bukkit.NamespacedKey; import org.bukkit.Registry; @@ -187,7 +186,10 @@ public class CommandFind implements DirectorExecutor { } private static Structure resolveNativeStructure(String structureKey) { - Registry structureRegistry = RegistryAccess.registryAccess().getRegistry(RegistryKey.STRUCTURE); + Registry structureRegistry = Bukkit.getRegistry(Structure.class); + if (structureRegistry == null) { + return null; + } for (Structure candidate : structureRegistry) { NamespacedKey key = structureRegistry.getKey(candidate); if (key != null && key.toString().equalsIgnoreCase(structureKey)) { diff --git a/adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/commands/CommandStructure.java b/adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/commands/CommandStructure.java index d780eea28..8071c5502 100644 --- a/adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/commands/CommandStructure.java +++ b/adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/commands/CommandStructure.java @@ -49,8 +49,6 @@ import art.arcane.iris.engine.platform.PlatformChunkGenerator; import art.arcane.iris.util.common.plugin.VolmitSender; import art.arcane.volmlib.util.math.RNG; import art.arcane.iris.util.common.scheduling.J; -import io.papermc.paper.registry.RegistryAccess; -import io.papermc.paper.registry.RegistryKey; import org.bukkit.Bukkit; import org.bukkit.Location; import org.bukkit.NamespacedKey; @@ -146,11 +144,13 @@ public class CommandStructure implements DirectorExecutor { return; } KList structureKeys = new KList<>(); - Registry structureRegistry = RegistryAccess.registryAccess().getRegistry(RegistryKey.STRUCTURE); - for (Structure structure : structureRegistry) { - NamespacedKey key = structureRegistry.getKey(structure); - if (key != null) { - structureKeys.add(key.toString()); + Registry structureRegistry = Bukkit.getRegistry(Structure.class); + if (structureRegistry != null) { + for (Structure structure : structureRegistry) { + NamespacedKey key = structureRegistry.getKey(structure); + if (key != null) { + structureKeys.add(key.toString()); + } } } VolmitSender commandSender = sender(); diff --git a/adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/service/PaperCommandRegistrar.java b/adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/commands/PaperCommandRegistrar.java similarity index 71% rename from adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/service/PaperCommandRegistrar.java rename to adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/commands/PaperCommandRegistrar.java index 467cccf33..a08e7ed1b 100644 --- a/adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/service/PaperCommandRegistrar.java +++ b/adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/commands/PaperCommandRegistrar.java @@ -1,6 +1,7 @@ -package art.arcane.iris.core.service; +package art.arcane.iris.core.commands; import art.arcane.iris.Iris; +import art.arcane.iris.core.service.CommandSVC; import io.papermc.paper.command.brigadier.BasicCommand; import io.papermc.paper.command.brigadier.CommandSourceStack; import io.papermc.paper.plugin.lifecycle.event.types.LifecycleEvents; @@ -9,13 +10,19 @@ import org.bukkit.command.CommandSender; import java.util.Collection; import java.util.List; -final class PaperCommandRegistrar { +/** + * Paper-only command registration. Lives outside {@code art.arcane.iris.core.service} on purpose: + * that package is eagerly Class.forName-scanned by JarScanner during enable, and resolving this + * class on plain Spigot throws NoClassDefFoundError for the Paper lifecycle types. CommandSVC + * loads it reflectively only when the Bukkit plugin.yml command is unavailable (Paper path). + */ +public final class PaperCommandRegistrar { private static final String ROOT_COMMAND = "iris"; private PaperCommandRegistrar() { } - static void register(Iris plugin, CommandSVC commandService) { + public static void register(Iris plugin, CommandSVC commandService) { plugin.getLifecycleManager().registerEventHandler(LifecycleEvents.COMMANDS, event -> event.registrar().register( ROOT_COMMAND, diff --git a/adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/service/CommandSVC.java b/adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/service/CommandSVC.java index 1527ed6ef..ae72e1ad3 100644 --- a/adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/service/CommandSVC.java +++ b/adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/service/CommandSVC.java @@ -159,7 +159,7 @@ public class CommandSVC implements IrisService, CommandExecutor, TabCompleter, D return true; } - void executeRoot(CommandSender sender, String label, String[] args) { + public void executeRoot(CommandSender sender, String label, String[] args) { if (!sender.hasPermission(ROOT_PERMISSION)) { sender.sendMessage(IrisLanguage.text( IrisMessages.COMMAND_PERMISSION_DENIED, @@ -171,7 +171,7 @@ public class CommandSVC implements IrisService, CommandExecutor, TabCompleter, D J.aBukkit(() -> executeCommand(sender, label, args)); } - List tabCompleteRoot(CommandSender sender, String alias, String[] args) { + public List tabCompleteRoot(CommandSender sender, String alias, String[] args) { List suggestions = runDirectorTab(sender, alias, args); if (sender instanceof Player player && IrisSettings.get().getGeneral().isCommandSounds()) { player.playSound(player.getLocation(), Sound.BLOCK_AMETHYST_BLOCK_CHIME, 0.25f, RNG.r.f(0.125f, 1.95f)); @@ -190,7 +190,7 @@ public class CommandSVC implements IrisService, CommandExecutor, TabCompleter, D private void registerPaperCommand() { try { Class registrarType = Class.forName( - "art.arcane.iris.core.service.PaperCommandRegistrar", + "art.arcane.iris.core.commands.PaperCommandRegistrar", true, getClass().getClassLoader() ); diff --git a/adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/service/FellingRun.java b/adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/service/FellingRun.java index 6daebfb7c..088a40b64 100644 --- a/adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/service/FellingRun.java +++ b/adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/service/FellingRun.java @@ -25,6 +25,7 @@ final class FellingRun { volatile int effectStride = 1; volatile ItemStack expectedTool; volatile List work = List.of(); + volatile String abortReason; FellingRun( TreeClaim claim, diff --git a/adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/service/TreeFellingRunner.java b/adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/service/TreeFellingRunner.java index ee7bf83b3..a1360af20 100644 --- a/adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/service/TreeFellingRunner.java +++ b/adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/service/TreeFellingRunner.java @@ -67,6 +67,9 @@ final class TreeFellingRunner { ) ); List positions = positionsForFelling(discovery, run.candidate.trigger()); + if (!discovery.complete()) { + run.abortReason = "discovery-incomplete"; + } preflight(run, positions, discovery.complete()); } catch (Throwable error) { IrisLogging.reportError("Failed to discover an Iris tree for felling.", error); @@ -96,6 +99,7 @@ final class TreeFellingRunner { Runnable task = () -> { try { if (!run.candidate.world().isChunkLoaded(chunk.x(), chunk.z())) { + run.abortReason = "chunk-unloaded"; failed.set(true); return; } @@ -151,6 +155,7 @@ final class TreeFellingRunner { List ordered = orderMembers(run.candidate.trigger(), members); if (ordered.isEmpty() || !ordered.getFirst().position().equals(run.candidate.trigger())) { + run.abortReason = "trigger-drift"; finish(run); return; } @@ -212,8 +217,9 @@ final class TreeFellingRunner { "Failed to prepare an Iris tree-feller block.", () -> prepareBreak(run, member) ); - if (!J.runRegion(run.candidate.world(), chunk.x(), chunk.z(), task)) { + if (!runMemberRegion(run, chunk.x(), chunk.z(), task)) { if (member.log()) { + run.abortReason = "schedule-refused"; finish(run); } else { continueRun(run); @@ -221,10 +227,34 @@ final class TreeFellingRunner { } } + // On Paper, J.runRegion/J.runEntity always defer a full tick even when already on the + // main thread, which collapsed the pulse pacing to one block per tick and forced players + // to hold sneak for 10+ seconds on large trees. Folia keeps the scheduler hop. + static boolean shouldRunInline(boolean folia, boolean primaryThread) { + return !folia && primaryThread; + } + + private boolean runMemberRegion(FellingRun run, int chunkX, int chunkZ, Runnable task) { + if (shouldRunInline(J.isFolia(), Bukkit.isPrimaryThread())) { + task.run(); + return true; + } + return J.runRegion(run.candidate.world(), chunkX, chunkZ, task); + } + + private boolean runMemberEntity(FellingRun run, Runnable task) { + if (shouldRunInline(J.isFolia(), Bukkit.isPrimaryThread())) { + task.run(); + return true; + } + return J.runEntity(run.candidate.player(), task); + } + private void prepareBreak(FellingRun run, TreeMember member) { Block block = liveMemberBlock(run, member); if (block == null) { if (member.log()) { + run.abortReason = "member-drift"; finish(run); } else { continueRun(run); @@ -247,7 +277,8 @@ final class TreeFellingRunner { "Failed to reserve Iris tree-feller tool durability.", () -> reserveDamage(run, member) ); - if (!J.runEntity(run.candidate.player(), task)) { + if (!runMemberEntity(run, task)) { + run.abortReason = "schedule-refused"; finish(run); } } @@ -255,6 +286,7 @@ final class TreeFellingRunner { private void reserveDamage(FellingRun run, TreeMember member) { Player player = run.candidate.player(); if (!isRunControlActive(run, player)) { + run.abortReason = "control-released"; finish(run); return; } @@ -264,11 +296,13 @@ final class TreeFellingRunner { || inventory.getHeldItemSlot() != run.heldSlot || !current.isSimilar(run.expectedTool) || !TreeFellerSVC.isAxe(current)) { + run.abortReason = "tool-changed"; finish(run); return; } if (!reserveLogCost(run)) { + run.abortReason = "log-cost-refused"; finish(run); return; } @@ -304,8 +338,8 @@ final class TreeFellingRunner { Runnable task = () -> runMutationTask(run, member, reservation, mutationSucceeded); boolean scheduled; try { - scheduled = J.runRegion( - run.candidate.world(), + scheduled = runMemberRegion( + run, position.x() >> 4, position.z() >> 4, task @@ -368,8 +402,10 @@ final class TreeFellingRunner { try { if (probe.isCancelled()) { if (reservation.charged() || reservation.logCostReserved()) { + run.abortReason = "probe-cancelled"; refundAndFinish(run, reservation); } else if (member.log()) { + run.abortReason = "probe-cancelled"; finish(run); } else { continueRun(run); @@ -379,6 +415,9 @@ final class TreeFellingRunner { block = liveMemberBlock(run, member); if (run.finished.get() || block == null) { + if (block == null) { + run.abortReason = "member-drift"; + } probe.setCancelled(true); refundAndFinish(run, reservation); return; @@ -525,6 +564,7 @@ final class TreeFellingRunner { private void completeSuccessfulMutation(FellingRun run, DamageReservation reservation) { if (reservation.broke()) { + run.abortReason = "tool-broke"; finish(run); return; } @@ -564,6 +604,9 @@ final class TreeFellingRunner { } } run.presentation.finish(); + IrisLogging.debug("Tree feller run ended: members=" + run.work.size() + + " processed=" + run.processed.get() + + " reason=" + (run.abortReason == null ? "complete" : run.abortReason)); } } @@ -573,6 +616,9 @@ final class TreeFellingRunner { return; } for (FellingRun run : List.copyOf(runs)) { + if (run.abortReason == null) { + run.abortReason = "halted"; + } finish(run); } } diff --git a/adapters/bukkit/plugin/src/main/resources/paper-plugin.yml b/adapters/bukkit/plugin/src/main/resources/paper-plugin.yml index eebf7f462..347b7397f 100644 --- a/adapters/bukkit/plugin/src/main/resources/paper-plugin.yml +++ b/adapters/bukkit/plugin/src/main/resources/paper-plugin.yml @@ -6,7 +6,7 @@ folia-supported: true api-version: '${apiVersion}' load: STARTUP authors: [ cyberpwn, NextdoorPsycho, Vatuu ] -website: volmit.com +website: VolmitSoftware.com description: More than a Dimension! permissions: iris.treefeller: diff --git a/adapters/bukkit/plugin/src/main/resources/plugin.yml b/adapters/bukkit/plugin/src/main/resources/plugin.yml index f07654159..8ee16c61d 100644 --- a/adapters/bukkit/plugin/src/main/resources/plugin.yml +++ b/adapters/bukkit/plugin/src/main/resources/plugin.yml @@ -20,7 +20,7 @@ softdepend: - WorldEdit loadbefore: [ Multiverse-Core ] authors: [ cyberpwn, NextdoorPsycho, Vatuu ] -website: volmit.com +website: VolmitSoftware.com description: More than a Dimension! commands: iris: diff --git a/adapters/bukkit/plugin/src/test/java/art/arcane/iris/IrisDiagnosticsTest.java b/adapters/bukkit/plugin/src/test/java/art/arcane/iris/IrisDiagnosticsTest.java index ad7e0e0a3..279409e80 100644 --- a/adapters/bukkit/plugin/src/test/java/art/arcane/iris/IrisDiagnosticsTest.java +++ b/adapters/bukkit/plugin/src/test/java/art/arcane/iris/IrisDiagnosticsTest.java @@ -21,10 +21,15 @@ import static org.junit.Assert.assertTrue; public class IrisDiagnosticsTest { @Test public void serviceInitializationSkipsPackageHelperClasses() throws Exception { - Class registrar = Class.forName("art.arcane.iris.core.service.PaperCommandRegistrar"); + Class registrar = Class.forName("art.arcane.iris.core.commands.PaperCommandRegistrar"); assertFalse(Iris.isConcreteImplementation(registrar, IrisService.class)); assertTrue(Iris.isConcreteImplementation(CommandSVC.class, IrisService.class)); + // The service package is eagerly Class.forName-scanned by JarScanner at enable; a class + // referencing Paper-only types there prints an NCDFE stack trace on plain Spigot. + assertFalse( + "Paper-only command registrar must stay out of the JarScanner-scanned services package", + "art.arcane.iris.core.service".equals(registrar.getPackageName())); } @Test diff --git a/adapters/bukkit/plugin/src/test/java/art/arcane/iris/core/service/PaperCommandRegistrarTest.java b/adapters/bukkit/plugin/src/test/java/art/arcane/iris/core/commands/PaperCommandRegistrarTest.java similarity index 92% rename from adapters/bukkit/plugin/src/test/java/art/arcane/iris/core/service/PaperCommandRegistrarTest.java rename to adapters/bukkit/plugin/src/test/java/art/arcane/iris/core/commands/PaperCommandRegistrarTest.java index 6e9ec341a..30de6e3e6 100644 --- a/adapters/bukkit/plugin/src/test/java/art/arcane/iris/core/service/PaperCommandRegistrarTest.java +++ b/adapters/bukkit/plugin/src/test/java/art/arcane/iris/core/commands/PaperCommandRegistrarTest.java @@ -1,5 +1,6 @@ -package art.arcane.iris.core.service; +package art.arcane.iris.core.commands; +import art.arcane.iris.core.service.CommandSVC; import io.papermc.paper.command.brigadier.BasicCommand; import io.papermc.paper.command.brigadier.CommandSourceStack; import org.bukkit.Location; @@ -74,14 +75,14 @@ public class PaperCommandRegistrarTest { private String[] arguments; @Override - void executeRoot(CommandSender sender, String label, String[] args) { + public void executeRoot(CommandSender sender, String label, String[] args) { this.sender = sender; this.label = label; this.arguments = args; } @Override - List tabCompleteRoot(CommandSender sender, String alias, String[] args) { + public List tabCompleteRoot(CommandSender sender, String alias, String[] args) { return List.of("first", "second"); } } diff --git a/adapters/bukkit/plugin/src/test/java/art/arcane/iris/core/service/TreeFellingRunnerPacingTest.java b/adapters/bukkit/plugin/src/test/java/art/arcane/iris/core/service/TreeFellingRunnerPacingTest.java new file mode 100644 index 000000000..a3ab672e7 --- /dev/null +++ b/adapters/bukkit/plugin/src/test/java/art/arcane/iris/core/service/TreeFellingRunnerPacingTest.java @@ -0,0 +1,16 @@ +package art.arcane.iris.core.service; + +import org.junit.Test; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +public class TreeFellingRunnerPacingTest { + @Test + public void memberTasksRunInlineOnlyOnNonFoliaMainThread() { + assertTrue(TreeFellingRunner.shouldRunInline(false, true)); + assertFalse(TreeFellingRunner.shouldRunInline(false, false)); + assertFalse(TreeFellingRunner.shouldRunInline(true, true)); + assertFalse(TreeFellingRunner.shouldRunInline(true, false)); + } +} diff --git a/adapters/fabric/logs/latest.log b/adapters/fabric/logs/latest.log index 838498f44..e69de29bb 100644 --- a/adapters/fabric/logs/latest.log +++ b/adapters/fabric/logs/latest.log @@ -1,488 +0,0 @@ -[01:29:43] [Test worker/WARN]: Iris cannot resolve the biome id for 'minecraft:plains' before the Minecraft server is available; using biome id 0 -[01:29:43] [Test worker/WARN]: Iris cannot enumerate the biome registry before the Minecraft server is available; returning no biomes -[01:29:43] [Test worker/INFO]: [STDERR]: [Iris/WARN] Can't find block data for minecraft:definitely_not_a_real_block -[01:29:43] [Test worker/INFO]: [STDERR]: [Iris/WARN] Can't find block data for minecraft:minecraft:definitely_not_a_real_block -[01:29:43] [Test worker/INFO]: [STDERR]: [Iris/WARN] Block 'minecraft:oak_log' rejected state 'not_a_property=x'; using its default state -[01:29:43] [Test worker/INFO]: Iris registered custom content provider 'iris_deferred_test' -[01:29:43] [Test worker/WARN]: Iris persistent dimension registry entry 1 in /var/folders/l1/g6lr3c5x26z_mdmtfzt9gjbr0000gn/T/iris-dimension-registry-partial13614762119230589058/iris-dimensions.json is invalid (entry 1 in /var/folders/l1/g6lr3c5x26z_mdmtfzt9gjbr0000gn/T/iris-dimension-registry-partial13614762119230589058/iris-dimensions.json has no dimension); kept verbatim: {"id":"iris:broken","pack":"overworld"} -[01:29:43] [Test worker/ERROR]: Iris persistent dimension registry at /var/folders/l1/g6lr3c5x26z_mdmtfzt9gjbr0000gn/T/iris-dimension-registry-corrupt-boot13293805067262854379/iris-dimensions.json is corrupt; quarantining it and continuing boot -java.lang.IllegalStateException: Iris persistent dimension registry at /var/folders/l1/g6lr3c5x26z_mdmtfzt9gjbr0000gn/T/iris-dimension-registry-corrupt-boot13293805067262854379/iris-dimensions.json could not be read; refusing to discard persistent worlds - at art.arcane.iris.modded.ModdedDimensionRegistryStore.contents(ModdedDimensionRegistryStore.java:150) - at art.arcane.iris.modded.ModdedDimensionRegistryStore.load(ModdedDimensionRegistryStore.java:56) - at art.arcane.iris.modded.ModdedDimensionRegistryStore.loadForStartup(ModdedDimensionRegistryStore.java:69) - at art.arcane.iris.modded.ModdedDimensionRegistryStoreTest.startupLoadQuarantinesACorruptRegistryInsteadOfFailingBoot(ModdedDimensionRegistryStoreTest.java:99) - at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:104) - at java.base/java.lang.reflect.Method.invoke(Method.java:565) - at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:59) - at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12) - at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:56) - at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17) - at org.junit.runners.ParentRunner$3.evaluate(ParentRunner.java:306) - at org.junit.runners.BlockJUnit4ClassRunner$1.evaluate(BlockJUnit4ClassRunner.java:100) - at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:366) - at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:103) - at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:63) - at org.junit.runners.ParentRunner$4.run(ParentRunner.java:331) - at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:79) - at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:329) - at org.junit.runners.ParentRunner.access$100(ParentRunner.java:66) - at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:293) - at org.junit.runners.ParentRunner$3.evaluate(ParentRunner.java:306) - at org.junit.runners.ParentRunner.run(ParentRunner.java:413) - at org.junit.runner.JUnitCore.run(JUnitCore.java:137) - at org.junit.runner.JUnitCore.run(JUnitCore.java:115) - at org.gradle.api.internal.tasks.testing.junit.JUnitTestExecutor.runRequest(JUnitTestExecutor.java:175) - at org.gradle.api.internal.tasks.testing.junit.JUnitTestExecutor.accept(JUnitTestExecutor.java:84) - at org.gradle.api.internal.tasks.testing.junit.JUnitTestExecutor.accept(JUnitTestExecutor.java:47) - at org.gradle.api.internal.tasks.testing.junit.AbstractJUnitTestDefinitionProcessor.processTestDefinition(AbstractJUnitTestDefinitionProcessor.java:65) - at org.gradle.api.internal.tasks.testing.SuiteTestDefinitionProcessor.processTestDefinition(SuiteTestDefinitionProcessor.java:53) - at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:104) - at java.base/java.lang.reflect.Method.invoke(Method.java:565) - at org.gradle.internal.dispatch.MethodInvocation.invokeOn(MethodInvocation.java:77) - at org.gradle.internal.dispatch.ReflectionDispatch.dispatch(ReflectionDispatch.java:28) - at org.gradle.internal.dispatch.ReflectionDispatch.dispatch(ReflectionDispatch.java:19) - at org.gradle.internal.dispatch.ContextClassLoaderDispatch.dispatch(ContextClassLoaderDispatch.java:33) - at org.gradle.internal.dispatch.ProxyDispatchAdapter$DispatchingInvocationHandler.invoke(ProxyDispatchAdapter.java:88) - at jdk.proxy1/jdk.proxy1.$Proxy4.processTestDefinition(Unknown Source) - at org.gradle.api.internal.tasks.testing.worker.TestWorker$2.run(TestWorker.java:178) - at org.gradle.api.internal.tasks.testing.worker.TestWorker.executeAndMaintainThreadName(TestWorker.java:126) - at org.gradle.api.internal.tasks.testing.worker.TestWorker.execute(TestWorker.java:103) - at org.gradle.api.internal.tasks.testing.worker.TestWorker.execute(TestWorker.java:63) - at org.gradle.process.internal.worker.child.ActionExecutionWorker.execute(ActionExecutionWorker.java:56) - at org.gradle.process.internal.worker.child.SystemApplicationClassLoaderWorker.call(SystemApplicationClassLoaderWorker.java:122) - at org.gradle.process.internal.worker.child.SystemApplicationClassLoaderWorker.call(SystemApplicationClassLoaderWorker.java:72) - at worker.org.gradle.process.internal.worker.GradleWorkerMain.run(GradleWorkerMain.java:69) - at worker.org.gradle.process.internal.worker.GradleWorkerMain.main(GradleWorkerMain.java:74) -Caused by: art.arcane.volmlib.util.json.JSONException: A JSONObject text must end with '}' at 34 [character 35 line 1] - at art.arcane.volmlib.util.json.JSONTokener.syntaxError(JSONTokener.java:414) - at art.arcane.volmlib.util.json.JSONObject.(JSONObject.java:145) - at art.arcane.volmlib.util.json.JSONTokener.nextValue(JSONTokener.java:345) - at art.arcane.volmlib.util.json.JSONArray.(JSONArray.java:111) - at art.arcane.volmlib.util.json.JSONTokener.nextValue(JSONTokener.java:348) - at art.arcane.volmlib.util.json.JSONObject.(JSONObject.java:159) - at art.arcane.volmlib.util.json.JSONObject.(JSONObject.java:260) - at art.arcane.iris.modded.ModdedDimensionRegistryStore.contents(ModdedDimensionRegistryStore.java:117) - ... 45 more -[01:29:43] [Test worker/ERROR]: Iris lost 1 persistent dimension(s) from the corrupt registry: iris:lost -[01:29:43] [Test worker/ERROR]: Iris moved the corrupt persistent dimension registry to /var/folders/l1/g6lr3c5x26z_mdmtfzt9gjbr0000gn/T/iris-dimension-registry-corrupt-boot13293805067262854379/iris-dimensions.json.broken-1785648583220 -[01:29:43] [Test worker/ERROR]: Iris service onDisable failed for art.arcane.iris.modded.ModdedServiceManagerTest$SecondService -java.lang.RuntimeException: second disable failed - at art.arcane.iris.modded.ModdedServiceManagerTest.disableAttemptsEveryServiceInReverseOrderAndNeverRethrows(ModdedServiceManagerTest.java:55) - at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:104) - at java.base/java.lang.reflect.Method.invoke(Method.java:565) - at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:59) - at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12) - at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:56) - at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17) - at org.junit.runners.ParentRunner$3.evaluate(ParentRunner.java:306) - at org.junit.runners.BlockJUnit4ClassRunner$1.evaluate(BlockJUnit4ClassRunner.java:100) - at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:366) - at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:103) - at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:63) - at org.junit.runners.ParentRunner$4.run(ParentRunner.java:331) - at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:79) - at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:329) - at org.junit.runners.ParentRunner.access$100(ParentRunner.java:66) - at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:293) - at org.junit.runners.ParentRunner$3.evaluate(ParentRunner.java:306) - at org.junit.runners.ParentRunner.run(ParentRunner.java:413) - at org.junit.runner.JUnitCore.run(JUnitCore.java:137) - at org.junit.runner.JUnitCore.run(JUnitCore.java:115) - at org.gradle.api.internal.tasks.testing.junit.JUnitTestExecutor.runRequest(JUnitTestExecutor.java:175) - at org.gradle.api.internal.tasks.testing.junit.JUnitTestExecutor.accept(JUnitTestExecutor.java:84) - at org.gradle.api.internal.tasks.testing.junit.JUnitTestExecutor.accept(JUnitTestExecutor.java:47) - at org.gradle.api.internal.tasks.testing.junit.AbstractJUnitTestDefinitionProcessor.processTestDefinition(AbstractJUnitTestDefinitionProcessor.java:65) - at org.gradle.api.internal.tasks.testing.SuiteTestDefinitionProcessor.processTestDefinition(SuiteTestDefinitionProcessor.java:53) - at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:104) - at java.base/java.lang.reflect.Method.invoke(Method.java:565) - at org.gradle.internal.dispatch.MethodInvocation.invokeOn(MethodInvocation.java:77) - at org.gradle.internal.dispatch.ReflectionDispatch.dispatch(ReflectionDispatch.java:28) - at org.gradle.internal.dispatch.ReflectionDispatch.dispatch(ReflectionDispatch.java:19) - at org.gradle.internal.dispatch.ContextClassLoaderDispatch.dispatch(ContextClassLoaderDispatch.java:33) - at org.gradle.internal.dispatch.ProxyDispatchAdapter$DispatchingInvocationHandler.invoke(ProxyDispatchAdapter.java:88) - at jdk.proxy1/jdk.proxy1.$Proxy4.processTestDefinition(Unknown Source) - at org.gradle.api.internal.tasks.testing.worker.TestWorker$2.run(TestWorker.java:178) - at org.gradle.api.internal.tasks.testing.worker.TestWorker.executeAndMaintainThreadName(TestWorker.java:126) - at org.gradle.api.internal.tasks.testing.worker.TestWorker.execute(TestWorker.java:103) - at org.gradle.api.internal.tasks.testing.worker.TestWorker.execute(TestWorker.java:63) - at org.gradle.process.internal.worker.child.ActionExecutionWorker.execute(ActionExecutionWorker.java:56) - at org.gradle.process.internal.worker.child.SystemApplicationClassLoaderWorker.call(SystemApplicationClassLoaderWorker.java:122) - at org.gradle.process.internal.worker.child.SystemApplicationClassLoaderWorker.call(SystemApplicationClassLoaderWorker.java:72) - at worker.org.gradle.process.internal.worker.GradleWorkerMain.run(GradleWorkerMain.java:69) - at worker.org.gradle.process.internal.worker.GradleWorkerMain.main(GradleWorkerMain.java:74) -[01:29:43] [Test worker/ERROR]: Iris service onDisable failed for art.arcane.iris.modded.ModdedServiceManagerTest$FirstService -java.lang.RuntimeException: first disable failed - at art.arcane.iris.modded.ModdedServiceManagerTest.disableAttemptsEveryServiceInReverseOrderAndNeverRethrows(ModdedServiceManagerTest.java:54) - at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:104) - at java.base/java.lang.reflect.Method.invoke(Method.java:565) - at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:59) - at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12) - at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:56) - at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17) - at org.junit.runners.ParentRunner$3.evaluate(ParentRunner.java:306) - at org.junit.runners.BlockJUnit4ClassRunner$1.evaluate(BlockJUnit4ClassRunner.java:100) - at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:366) - at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:103) - at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:63) - at org.junit.runners.ParentRunner$4.run(ParentRunner.java:331) - at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:79) - at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:329) - at org.junit.runners.ParentRunner.access$100(ParentRunner.java:66) - at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:293) - at org.junit.runners.ParentRunner$3.evaluate(ParentRunner.java:306) - at org.junit.runners.ParentRunner.run(ParentRunner.java:413) - at org.junit.runner.JUnitCore.run(JUnitCore.java:137) - at org.junit.runner.JUnitCore.run(JUnitCore.java:115) - at org.gradle.api.internal.tasks.testing.junit.JUnitTestExecutor.runRequest(JUnitTestExecutor.java:175) - at org.gradle.api.internal.tasks.testing.junit.JUnitTestExecutor.accept(JUnitTestExecutor.java:84) - at org.gradle.api.internal.tasks.testing.junit.JUnitTestExecutor.accept(JUnitTestExecutor.java:47) - at org.gradle.api.internal.tasks.testing.junit.AbstractJUnitTestDefinitionProcessor.processTestDefinition(AbstractJUnitTestDefinitionProcessor.java:65) - at org.gradle.api.internal.tasks.testing.SuiteTestDefinitionProcessor.processTestDefinition(SuiteTestDefinitionProcessor.java:53) - at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:104) - at java.base/java.lang.reflect.Method.invoke(Method.java:565) - at org.gradle.internal.dispatch.MethodInvocation.invokeOn(MethodInvocation.java:77) - at org.gradle.internal.dispatch.ReflectionDispatch.dispatch(ReflectionDispatch.java:28) - at org.gradle.internal.dispatch.ReflectionDispatch.dispatch(ReflectionDispatch.java:19) - at org.gradle.internal.dispatch.ContextClassLoaderDispatch.dispatch(ContextClassLoaderDispatch.java:33) - at org.gradle.internal.dispatch.ProxyDispatchAdapter$DispatchingInvocationHandler.invoke(ProxyDispatchAdapter.java:88) - at jdk.proxy1/jdk.proxy1.$Proxy4.processTestDefinition(Unknown Source) - at org.gradle.api.internal.tasks.testing.worker.TestWorker$2.run(TestWorker.java:178) - at org.gradle.api.internal.tasks.testing.worker.TestWorker.executeAndMaintainThreadName(TestWorker.java:126) - at org.gradle.api.internal.tasks.testing.worker.TestWorker.execute(TestWorker.java:103) - at org.gradle.api.internal.tasks.testing.worker.TestWorker.execute(TestWorker.java:63) - at org.gradle.process.internal.worker.child.ActionExecutionWorker.execute(ActionExecutionWorker.java:56) - at org.gradle.process.internal.worker.child.SystemApplicationClassLoaderWorker.call(SystemApplicationClassLoaderWorker.java:122) - at org.gradle.process.internal.worker.child.SystemApplicationClassLoaderWorker.call(SystemApplicationClassLoaderWorker.java:72) - at worker.org.gradle.process.internal.worker.GradleWorkerMain.run(GradleWorkerMain.java:69) - at worker.org.gradle.process.internal.worker.GradleWorkerMain.main(GradleWorkerMain.java:74) -[01:29:43] [Test worker/ERROR]: Iris disabled all services with 2 failure(s) -java.lang.RuntimeException: second disable failed - at art.arcane.iris.modded.ModdedServiceManagerTest.disableAttemptsEveryServiceInReverseOrderAndNeverRethrows(ModdedServiceManagerTest.java:55) - at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:104) - at java.base/java.lang.reflect.Method.invoke(Method.java:565) - at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:59) - at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12) - at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:56) - at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17) - at org.junit.runners.ParentRunner$3.evaluate(ParentRunner.java:306) - at org.junit.runners.BlockJUnit4ClassRunner$1.evaluate(BlockJUnit4ClassRunner.java:100) - at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:366) - at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:103) - at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:63) - at org.junit.runners.ParentRunner$4.run(ParentRunner.java:331) - at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:79) - at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:329) - at org.junit.runners.ParentRunner.access$100(ParentRunner.java:66) - at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:293) - at org.junit.runners.ParentRunner$3.evaluate(ParentRunner.java:306) - at org.junit.runners.ParentRunner.run(ParentRunner.java:413) - at org.junit.runner.JUnitCore.run(JUnitCore.java:137) - at org.junit.runner.JUnitCore.run(JUnitCore.java:115) - at org.gradle.api.internal.tasks.testing.junit.JUnitTestExecutor.runRequest(JUnitTestExecutor.java:175) - at org.gradle.api.internal.tasks.testing.junit.JUnitTestExecutor.accept(JUnitTestExecutor.java:84) - at org.gradle.api.internal.tasks.testing.junit.JUnitTestExecutor.accept(JUnitTestExecutor.java:47) - at org.gradle.api.internal.tasks.testing.junit.AbstractJUnitTestDefinitionProcessor.processTestDefinition(AbstractJUnitTestDefinitionProcessor.java:65) - at org.gradle.api.internal.tasks.testing.SuiteTestDefinitionProcessor.processTestDefinition(SuiteTestDefinitionProcessor.java:53) - at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:104) - at java.base/java.lang.reflect.Method.invoke(Method.java:565) - at org.gradle.internal.dispatch.MethodInvocation.invokeOn(MethodInvocation.java:77) - at org.gradle.internal.dispatch.ReflectionDispatch.dispatch(ReflectionDispatch.java:28) - at org.gradle.internal.dispatch.ReflectionDispatch.dispatch(ReflectionDispatch.java:19) - at org.gradle.internal.dispatch.ContextClassLoaderDispatch.dispatch(ContextClassLoaderDispatch.java:33) - at org.gradle.internal.dispatch.ProxyDispatchAdapter$DispatchingInvocationHandler.invoke(ProxyDispatchAdapter.java:88) - at jdk.proxy1/jdk.proxy1.$Proxy4.processTestDefinition(Unknown Source) - at org.gradle.api.internal.tasks.testing.worker.TestWorker$2.run(TestWorker.java:178) - at org.gradle.api.internal.tasks.testing.worker.TestWorker.executeAndMaintainThreadName(TestWorker.java:126) - at org.gradle.api.internal.tasks.testing.worker.TestWorker.execute(TestWorker.java:103) - at org.gradle.api.internal.tasks.testing.worker.TestWorker.execute(TestWorker.java:63) - at org.gradle.process.internal.worker.child.ActionExecutionWorker.execute(ActionExecutionWorker.java:56) - at org.gradle.process.internal.worker.child.SystemApplicationClassLoaderWorker.call(SystemApplicationClassLoaderWorker.java:122) - at org.gradle.process.internal.worker.child.SystemApplicationClassLoaderWorker.call(SystemApplicationClassLoaderWorker.java:72) - at worker.org.gradle.process.internal.worker.GradleWorkerMain.run(GradleWorkerMain.java:69) - at worker.org.gradle.process.internal.worker.GradleWorkerMain.main(GradleWorkerMain.java:74) - Suppressed: java.lang.RuntimeException: first disable failed - at art.arcane.iris.modded.ModdedServiceManagerTest.disableAttemptsEveryServiceInReverseOrderAndNeverRethrows(ModdedServiceManagerTest.java:54) - ... 42 more -[01:29:43] [Test worker/ERROR]: Iris service rollback failed for art.arcane.iris.modded.ModdedServiceManagerTest$SecondService -java.lang.RuntimeException: cleanup failed - at art.arcane.iris.modded.ModdedServiceManagerTest.rollsBackEnabledServicesAndPreservesCleanupFailures(ModdedServiceManagerTest.java:32) - at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:104) - at java.base/java.lang.reflect.Method.invoke(Method.java:565) - at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:59) - at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12) - at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:56) - at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17) - at org.junit.runners.ParentRunner$3.evaluate(ParentRunner.java:306) - at org.junit.runners.BlockJUnit4ClassRunner$1.evaluate(BlockJUnit4ClassRunner.java:100) - at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:366) - at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:103) - at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:63) - at org.junit.runners.ParentRunner$4.run(ParentRunner.java:331) - at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:79) - at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:329) - at org.junit.runners.ParentRunner.access$100(ParentRunner.java:66) - at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:293) - at org.junit.runners.ParentRunner$3.evaluate(ParentRunner.java:306) - at org.junit.runners.ParentRunner.run(ParentRunner.java:413) - at org.junit.runner.JUnitCore.run(JUnitCore.java:137) - at org.junit.runner.JUnitCore.run(JUnitCore.java:115) - at org.gradle.api.internal.tasks.testing.junit.JUnitTestExecutor.runRequest(JUnitTestExecutor.java:175) - at org.gradle.api.internal.tasks.testing.junit.JUnitTestExecutor.accept(JUnitTestExecutor.java:84) - at org.gradle.api.internal.tasks.testing.junit.JUnitTestExecutor.accept(JUnitTestExecutor.java:47) - at org.gradle.api.internal.tasks.testing.junit.AbstractJUnitTestDefinitionProcessor.processTestDefinition(AbstractJUnitTestDefinitionProcessor.java:65) - at org.gradle.api.internal.tasks.testing.SuiteTestDefinitionProcessor.processTestDefinition(SuiteTestDefinitionProcessor.java:53) - at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:104) - at java.base/java.lang.reflect.Method.invoke(Method.java:565) - at org.gradle.internal.dispatch.MethodInvocation.invokeOn(MethodInvocation.java:77) - at org.gradle.internal.dispatch.ReflectionDispatch.dispatch(ReflectionDispatch.java:28) - at org.gradle.internal.dispatch.ReflectionDispatch.dispatch(ReflectionDispatch.java:19) - at org.gradle.internal.dispatch.ContextClassLoaderDispatch.dispatch(ContextClassLoaderDispatch.java:33) - at org.gradle.internal.dispatch.ProxyDispatchAdapter$DispatchingInvocationHandler.invoke(ProxyDispatchAdapter.java:88) - at jdk.proxy1/jdk.proxy1.$Proxy4.processTestDefinition(Unknown Source) - at org.gradle.api.internal.tasks.testing.worker.TestWorker$2.run(TestWorker.java:178) - at org.gradle.api.internal.tasks.testing.worker.TestWorker.executeAndMaintainThreadName(TestWorker.java:126) - at org.gradle.api.internal.tasks.testing.worker.TestWorker.execute(TestWorker.java:103) - at org.gradle.api.internal.tasks.testing.worker.TestWorker.execute(TestWorker.java:63) - at org.gradle.process.internal.worker.child.ActionExecutionWorker.execute(ActionExecutionWorker.java:56) - at org.gradle.process.internal.worker.child.SystemApplicationClassLoaderWorker.call(SystemApplicationClassLoaderWorker.java:122) - at org.gradle.process.internal.worker.child.SystemApplicationClassLoaderWorker.call(SystemApplicationClassLoaderWorker.java:72) - at worker.org.gradle.process.internal.worker.GradleWorkerMain.run(GradleWorkerMain.java:69) - at worker.org.gradle.process.internal.worker.GradleWorkerMain.main(GradleWorkerMain.java:74) -[01:29:43] [Test worker/ERROR]: Iris service onEnable failed for art.arcane.iris.modded.ModdedServiceManagerTest$SecondService -java.lang.RuntimeException: enable failed - at art.arcane.iris.modded.ModdedServiceManagerTest.rollsBackEnabledServicesAndPreservesCleanupFailures(ModdedServiceManagerTest.java:31) - at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:104) - at java.base/java.lang.reflect.Method.invoke(Method.java:565) - at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:59) - at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12) - at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:56) - at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17) - at org.junit.runners.ParentRunner$3.evaluate(ParentRunner.java:306) - at org.junit.runners.BlockJUnit4ClassRunner$1.evaluate(BlockJUnit4ClassRunner.java:100) - at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:366) - at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:103) - at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:63) - at org.junit.runners.ParentRunner$4.run(ParentRunner.java:331) - at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:79) - at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:329) - at org.junit.runners.ParentRunner.access$100(ParentRunner.java:66) - at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:293) - at org.junit.runners.ParentRunner$3.evaluate(ParentRunner.java:306) - at org.junit.runners.ParentRunner.run(ParentRunner.java:413) - at org.junit.runner.JUnitCore.run(JUnitCore.java:137) - at org.junit.runner.JUnitCore.run(JUnitCore.java:115) - at org.gradle.api.internal.tasks.testing.junit.JUnitTestExecutor.runRequest(JUnitTestExecutor.java:175) - at org.gradle.api.internal.tasks.testing.junit.JUnitTestExecutor.accept(JUnitTestExecutor.java:84) - at org.gradle.api.internal.tasks.testing.junit.JUnitTestExecutor.accept(JUnitTestExecutor.java:47) - at org.gradle.api.internal.tasks.testing.junit.AbstractJUnitTestDefinitionProcessor.processTestDefinition(AbstractJUnitTestDefinitionProcessor.java:65) - at org.gradle.api.internal.tasks.testing.SuiteTestDefinitionProcessor.processTestDefinition(SuiteTestDefinitionProcessor.java:53) - at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:104) - at java.base/java.lang.reflect.Method.invoke(Method.java:565) - at org.gradle.internal.dispatch.MethodInvocation.invokeOn(MethodInvocation.java:77) - at org.gradle.internal.dispatch.ReflectionDispatch.dispatch(ReflectionDispatch.java:28) - at org.gradle.internal.dispatch.ReflectionDispatch.dispatch(ReflectionDispatch.java:19) - at org.gradle.internal.dispatch.ContextClassLoaderDispatch.dispatch(ContextClassLoaderDispatch.java:33) - at org.gradle.internal.dispatch.ProxyDispatchAdapter$DispatchingInvocationHandler.invoke(ProxyDispatchAdapter.java:88) - at jdk.proxy1/jdk.proxy1.$Proxy4.processTestDefinition(Unknown Source) - at org.gradle.api.internal.tasks.testing.worker.TestWorker$2.run(TestWorker.java:178) - at org.gradle.api.internal.tasks.testing.worker.TestWorker.executeAndMaintainThreadName(TestWorker.java:126) - at org.gradle.api.internal.tasks.testing.worker.TestWorker.execute(TestWorker.java:103) - at org.gradle.api.internal.tasks.testing.worker.TestWorker.execute(TestWorker.java:63) - at org.gradle.process.internal.worker.child.ActionExecutionWorker.execute(ActionExecutionWorker.java:56) - at org.gradle.process.internal.worker.child.SystemApplicationClassLoaderWorker.call(SystemApplicationClassLoaderWorker.java:122) - at org.gradle.process.internal.worker.child.SystemApplicationClassLoaderWorker.call(SystemApplicationClassLoaderWorker.java:72) - at worker.org.gradle.process.internal.worker.GradleWorkerMain.run(GradleWorkerMain.java:69) - at worker.org.gradle.process.internal.worker.GradleWorkerMain.main(GradleWorkerMain.java:74) - Suppressed: java.lang.RuntimeException: cleanup failed - at art.arcane.iris.modded.ModdedServiceManagerTest.rollsBackEnabledServicesAndPreservesCleanupFailures(ModdedServiceManagerTest.java:32) - ... 42 more -[01:29:43] [Test worker/ERROR]: [worldcheck] server stop request failed -java.lang.IllegalStateException: stop request failed - at art.arcane.iris.modded.ModdedWorldCheckTest.lambda$stopRequestFailureForcesNonzeroResult$1(ModdedWorldCheckTest.java:238) - at art.arcane.iris.modded.ModdedWorldCheck.runAndRequestStop(ModdedWorldCheck.java:144) - at art.arcane.iris.modded.ModdedWorldCheckTest.stopRequestFailureForcesNonzeroResult(ModdedWorldCheckTest.java:235) - at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:104) - at java.base/java.lang.reflect.Method.invoke(Method.java:565) - at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:59) - at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12) - at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:56) - at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17) - at org.junit.runners.ParentRunner$3.evaluate(ParentRunner.java:306) - at org.junit.runners.BlockJUnit4ClassRunner$1.evaluate(BlockJUnit4ClassRunner.java:100) - at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:366) - at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:103) - at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:63) - at org.junit.runners.ParentRunner$4.run(ParentRunner.java:331) - at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:79) - at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:329) - at org.junit.runners.ParentRunner.access$100(ParentRunner.java:66) - at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:293) - at org.junit.runners.ParentRunner$3.evaluate(ParentRunner.java:306) - at org.junit.runners.ParentRunner.run(ParentRunner.java:413) - at org.junit.runner.JUnitCore.run(JUnitCore.java:137) - at org.junit.runner.JUnitCore.run(JUnitCore.java:115) - at org.gradle.api.internal.tasks.testing.junit.JUnitTestExecutor.runRequest(JUnitTestExecutor.java:175) - at org.gradle.api.internal.tasks.testing.junit.JUnitTestExecutor.accept(JUnitTestExecutor.java:84) - at org.gradle.api.internal.tasks.testing.junit.JUnitTestExecutor.accept(JUnitTestExecutor.java:47) - at org.gradle.api.internal.tasks.testing.junit.AbstractJUnitTestDefinitionProcessor.processTestDefinition(AbstractJUnitTestDefinitionProcessor.java:65) - at org.gradle.api.internal.tasks.testing.SuiteTestDefinitionProcessor.processTestDefinition(SuiteTestDefinitionProcessor.java:53) - at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:104) - at java.base/java.lang.reflect.Method.invoke(Method.java:565) - at org.gradle.internal.dispatch.MethodInvocation.invokeOn(MethodInvocation.java:77) - at org.gradle.internal.dispatch.ReflectionDispatch.dispatch(ReflectionDispatch.java:28) - at org.gradle.internal.dispatch.ReflectionDispatch.dispatch(ReflectionDispatch.java:19) - at org.gradle.internal.dispatch.ContextClassLoaderDispatch.dispatch(ContextClassLoaderDispatch.java:33) - at org.gradle.internal.dispatch.ProxyDispatchAdapter$DispatchingInvocationHandler.invoke(ProxyDispatchAdapter.java:88) - at jdk.proxy1/jdk.proxy1.$Proxy4.processTestDefinition(Unknown Source) - at org.gradle.api.internal.tasks.testing.worker.TestWorker$2.run(TestWorker.java:178) - at org.gradle.api.internal.tasks.testing.worker.TestWorker.executeAndMaintainThreadName(TestWorker.java:126) - at org.gradle.api.internal.tasks.testing.worker.TestWorker.execute(TestWorker.java:103) - at org.gradle.api.internal.tasks.testing.worker.TestWorker.execute(TestWorker.java:63) - at org.gradle.process.internal.worker.child.ActionExecutionWorker.execute(ActionExecutionWorker.java:56) - at org.gradle.process.internal.worker.child.SystemApplicationClassLoaderWorker.call(SystemApplicationClassLoaderWorker.java:122) - at org.gradle.process.internal.worker.child.SystemApplicationClassLoaderWorker.call(SystemApplicationClassLoaderWorker.java:72) - at worker.org.gradle.process.internal.worker.GradleWorkerMain.run(GradleWorkerMain.java:69) - at worker.org.gradle.process.internal.worker.GradleWorkerMain.main(GradleWorkerMain.java:74) -[01:29:43] [Test worker/ERROR]: [worldcheck] waiting for server shutdown failed -java.lang.IllegalStateException: shutdown wait failed - at art.arcane.iris.modded.ModdedWorldCheckTest.lambda$shutdownWaitFailureForcesNonzeroExit$0(ModdedWorldCheckTest.java:264) - at art.arcane.iris.modded.ModdedWorldCheck.awaitStopAndExit(ModdedWorldCheck.java:159) - at art.arcane.iris.modded.ModdedWorldCheckTest.shutdownWaitFailureForcesNonzeroExit(ModdedWorldCheckTest.java:262) - at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:104) - at java.base/java.lang.reflect.Method.invoke(Method.java:565) - at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:59) - at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12) - at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:56) - at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17) - at org.junit.runners.ParentRunner$3.evaluate(ParentRunner.java:306) - at org.junit.runners.BlockJUnit4ClassRunner$1.evaluate(BlockJUnit4ClassRunner.java:100) - at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:366) - at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:103) - at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:63) - at org.junit.runners.ParentRunner$4.run(ParentRunner.java:331) - at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:79) - at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:329) - at org.junit.runners.ParentRunner.access$100(ParentRunner.java:66) - at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:293) - at org.junit.runners.ParentRunner$3.evaluate(ParentRunner.java:306) - at org.junit.runners.ParentRunner.run(ParentRunner.java:413) - at org.junit.runner.JUnitCore.run(JUnitCore.java:137) - at org.junit.runner.JUnitCore.run(JUnitCore.java:115) - at org.gradle.api.internal.tasks.testing.junit.JUnitTestExecutor.runRequest(JUnitTestExecutor.java:175) - at org.gradle.api.internal.tasks.testing.junit.JUnitTestExecutor.accept(JUnitTestExecutor.java:84) - at org.gradle.api.internal.tasks.testing.junit.JUnitTestExecutor.accept(JUnitTestExecutor.java:47) - at org.gradle.api.internal.tasks.testing.junit.AbstractJUnitTestDefinitionProcessor.processTestDefinition(AbstractJUnitTestDefinitionProcessor.java:65) - at org.gradle.api.internal.tasks.testing.SuiteTestDefinitionProcessor.processTestDefinition(SuiteTestDefinitionProcessor.java:53) - at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:104) - at java.base/java.lang.reflect.Method.invoke(Method.java:565) - at org.gradle.internal.dispatch.MethodInvocation.invokeOn(MethodInvocation.java:77) - at org.gradle.internal.dispatch.ReflectionDispatch.dispatch(ReflectionDispatch.java:28) - at org.gradle.internal.dispatch.ReflectionDispatch.dispatch(ReflectionDispatch.java:19) - at org.gradle.internal.dispatch.ContextClassLoaderDispatch.dispatch(ContextClassLoaderDispatch.java:33) - at org.gradle.internal.dispatch.ProxyDispatchAdapter$DispatchingInvocationHandler.invoke(ProxyDispatchAdapter.java:88) - at jdk.proxy1/jdk.proxy1.$Proxy4.processTestDefinition(Unknown Source) - at org.gradle.api.internal.tasks.testing.worker.TestWorker$2.run(TestWorker.java:178) - at org.gradle.api.internal.tasks.testing.worker.TestWorker.executeAndMaintainThreadName(TestWorker.java:126) - at org.gradle.api.internal.tasks.testing.worker.TestWorker.execute(TestWorker.java:103) - at org.gradle.api.internal.tasks.testing.worker.TestWorker.execute(TestWorker.java:63) - at org.gradle.process.internal.worker.child.ActionExecutionWorker.execute(ActionExecutionWorker.java:56) - at org.gradle.process.internal.worker.child.SystemApplicationClassLoaderWorker.call(SystemApplicationClassLoaderWorker.java:122) - at org.gradle.process.internal.worker.child.SystemApplicationClassLoaderWorker.call(SystemApplicationClassLoaderWorker.java:72) - at worker.org.gradle.process.internal.worker.GradleWorkerMain.run(GradleWorkerMain.java:69) - at worker.org.gradle.process.internal.worker.GradleWorkerMain.main(GradleWorkerMain.java:74) -[01:29:43] [Test worker/ERROR]: [worldcheck] check failed -java.lang.IllegalStateException: check failed - at art.arcane.iris.modded.ModdedWorldCheckTest.lambda$thrownCheckStillRequestsStop$0(ModdedWorldCheckTest.java:224) - at art.arcane.iris.modded.ModdedWorldCheck.runAndRequestStop(ModdedWorldCheck.java:139) - at art.arcane.iris.modded.ModdedWorldCheckTest.thrownCheckStillRequestsStop(ModdedWorldCheckTest.java:222) - at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:104) - at java.base/java.lang.reflect.Method.invoke(Method.java:565) - at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:59) - at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12) - at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:56) - at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17) - at org.junit.runners.ParentRunner$3.evaluate(ParentRunner.java:306) - at org.junit.runners.BlockJUnit4ClassRunner$1.evaluate(BlockJUnit4ClassRunner.java:100) - at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:366) - at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:103) - at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:63) - at org.junit.runners.ParentRunner$4.run(ParentRunner.java:331) - at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:79) - at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:329) - at org.junit.runners.ParentRunner.access$100(ParentRunner.java:66) - at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:293) - at org.junit.runners.ParentRunner$3.evaluate(ParentRunner.java:306) - at org.junit.runners.ParentRunner.run(ParentRunner.java:413) - at org.junit.runner.JUnitCore.run(JUnitCore.java:137) - at org.junit.runner.JUnitCore.run(JUnitCore.java:115) - at org.gradle.api.internal.tasks.testing.junit.JUnitTestExecutor.runRequest(JUnitTestExecutor.java:175) - at org.gradle.api.internal.tasks.testing.junit.JUnitTestExecutor.accept(JUnitTestExecutor.java:84) - at org.gradle.api.internal.tasks.testing.junit.JUnitTestExecutor.accept(JUnitTestExecutor.java:47) - at org.gradle.api.internal.tasks.testing.junit.AbstractJUnitTestDefinitionProcessor.processTestDefinition(AbstractJUnitTestDefinitionProcessor.java:65) - at org.gradle.api.internal.tasks.testing.SuiteTestDefinitionProcessor.processTestDefinition(SuiteTestDefinitionProcessor.java:53) - at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:104) - at java.base/java.lang.reflect.Method.invoke(Method.java:565) - at org.gradle.internal.dispatch.MethodInvocation.invokeOn(MethodInvocation.java:77) - at org.gradle.internal.dispatch.ReflectionDispatch.dispatch(ReflectionDispatch.java:28) - at org.gradle.internal.dispatch.ReflectionDispatch.dispatch(ReflectionDispatch.java:19) - at org.gradle.internal.dispatch.ContextClassLoaderDispatch.dispatch(ContextClassLoaderDispatch.java:33) - at org.gradle.internal.dispatch.ProxyDispatchAdapter$DispatchingInvocationHandler.invoke(ProxyDispatchAdapter.java:88) - at jdk.proxy1/jdk.proxy1.$Proxy4.processTestDefinition(Unknown Source) - at org.gradle.api.internal.tasks.testing.worker.TestWorker$2.run(TestWorker.java:178) - at org.gradle.api.internal.tasks.testing.worker.TestWorker.executeAndMaintainThreadName(TestWorker.java:126) - at org.gradle.api.internal.tasks.testing.worker.TestWorker.execute(TestWorker.java:103) - at org.gradle.api.internal.tasks.testing.worker.TestWorker.execute(TestWorker.java:63) - at org.gradle.process.internal.worker.child.ActionExecutionWorker.execute(ActionExecutionWorker.java:56) - at org.gradle.process.internal.worker.child.SystemApplicationClassLoaderWorker.call(SystemApplicationClassLoaderWorker.java:122) - at org.gradle.process.internal.worker.child.SystemApplicationClassLoaderWorker.call(SystemApplicationClassLoaderWorker.java:72) - at worker.org.gradle.process.internal.worker.GradleWorkerMain.run(GradleWorkerMain.java:69) - at worker.org.gradle.process.internal.worker.GradleWorkerMain.main(GradleWorkerMain.java:74) -[01:29:43] [Test worker/INFO]: Iris registered custom content provider 'iris_discovery_success' -[01:29:43] [Test worker/WARN]: Iris custom content provider discovery failed at provider 'iris_discovery_failure' (art.arcane.iris.modded.api.ModdedCustomContentRegistryTest$TestProvider) -java.lang.RuntimeException: provider init failed - at art.arcane.iris.modded.api.ModdedCustomContentRegistryTest.failedDiscoveryPublishesNothingAndPreservesTheCause(ModdedCustomContentRegistryTest.java:39) - at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:104) - at java.base/java.lang.reflect.Method.invoke(Method.java:565) - at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:59) - at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12) - at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:56) - at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17) - at org.junit.runners.ParentRunner$3.evaluate(ParentRunner.java:306) - at org.junit.runners.BlockJUnit4ClassRunner$1.evaluate(BlockJUnit4ClassRunner.java:100) - at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:366) - at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:103) - at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:63) - at org.junit.runners.ParentRunner$4.run(ParentRunner.java:331) - at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:79) - at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:329) - at org.junit.runners.ParentRunner.access$100(ParentRunner.java:66) - at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:293) - at org.junit.runners.ParentRunner$3.evaluate(ParentRunner.java:306) - at org.junit.runners.ParentRunner.run(ParentRunner.java:413) - at org.junit.runner.JUnitCore.run(JUnitCore.java:137) - at org.junit.runner.JUnitCore.run(JUnitCore.java:115) - at org.gradle.api.internal.tasks.testing.junit.JUnitTestExecutor.runRequest(JUnitTestExecutor.java:175) - at org.gradle.api.internal.tasks.testing.junit.JUnitTestExecutor.accept(JUnitTestExecutor.java:84) - at org.gradle.api.internal.tasks.testing.junit.JUnitTestExecutor.accept(JUnitTestExecutor.java:47) - at org.gradle.api.internal.tasks.testing.junit.AbstractJUnitTestDefinitionProcessor.processTestDefinition(AbstractJUnitTestDefinitionProcessor.java:65) - at org.gradle.api.internal.tasks.testing.SuiteTestDefinitionProcessor.processTestDefinition(SuiteTestDefinitionProcessor.java:53) - at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:104) - at java.base/java.lang.reflect.Method.invoke(Method.java:565) - at org.gradle.internal.dispatch.MethodInvocation.invokeOn(MethodInvocation.java:77) - at org.gradle.internal.dispatch.ReflectionDispatch.dispatch(ReflectionDispatch.java:28) - at org.gradle.internal.dispatch.ReflectionDispatch.dispatch(ReflectionDispatch.java:19) - at org.gradle.internal.dispatch.ContextClassLoaderDispatch.dispatch(ContextClassLoaderDispatch.java:33) - at org.gradle.internal.dispatch.ProxyDispatchAdapter$DispatchingInvocationHandler.invoke(ProxyDispatchAdapter.java:88) - at jdk.proxy1/jdk.proxy1.$Proxy4.processTestDefinition(Unknown Source) - at org.gradle.api.internal.tasks.testing.worker.TestWorker$2.run(TestWorker.java:178) - at org.gradle.api.internal.tasks.testing.worker.TestWorker.executeAndMaintainThreadName(TestWorker.java:126) - at org.gradle.api.internal.tasks.testing.worker.TestWorker.execute(TestWorker.java:103) - at org.gradle.api.internal.tasks.testing.worker.TestWorker.execute(TestWorker.java:63) - at org.gradle.process.internal.worker.child.ActionExecutionWorker.execute(ActionExecutionWorker.java:56) - at org.gradle.process.internal.worker.child.SystemApplicationClassLoaderWorker.call(SystemApplicationClassLoaderWorker.java:122) - at org.gradle.process.internal.worker.child.SystemApplicationClassLoaderWorker.call(SystemApplicationClassLoaderWorker.java:72) - at worker.org.gradle.process.internal.worker.GradleWorkerMain.run(GradleWorkerMain.java:69) - at worker.org.gradle.process.internal.worker.GradleWorkerMain.main(GradleWorkerMain.java:74) -[01:29:43] [Test worker/INFO]: Iris object undo service ready (bounded to 32 paste(s) per player) -[01:29:43] [Test worker/INFO]: [STDOUT]: [Iris/INFO] Iris /iris command tree registered -[01:29:43] [Test worker/INFO]: [STDOUT]: [Iris/INFO] Iris /iris command tree registered diff --git a/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedPackInstaller.java b/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedPackInstaller.java index 41c406fd9..d5395649f 100644 --- a/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedPackInstaller.java +++ b/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedPackInstaller.java @@ -64,6 +64,7 @@ public final class ModdedPackInstaller { branch, forceOverwrite, false, + pack, feedback) != null; if (installed) { // Pack-install completion is one of the four forced-datapack regeneration triggers; every diff --git a/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedStructureHooks.java b/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedStructureHooks.java index dc33b73cc..4f14a0483 100644 --- a/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedStructureHooks.java +++ b/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedStructureHooks.java @@ -114,15 +114,19 @@ public final class ModdedStructureHooks implements PlatformStructureHooks { if (structure == null) { throw new IllegalArgumentException("Registered structure does not exist: " + structureKey); } + boolean hasFilterEntries = false; for (Holder holder : structure.biomes()) { + hasFilterEntries = true; Optional> key = holder.unwrapKey(); if (key.isPresent()) { keys.add(key.get().identifier().toString()); } } - if (keys.isEmpty()) { + // An empty biome filter is legal datapack content (opt-in structures whose tags only + // reference absent modded biomes); the structure is unreachable, not an error state. + if (keys.isEmpty() && hasFilterEntries) { throw new IllegalStateException("Registered structure '" + structureKey - + "' exposes no registered biome keys"); + + "' has biome filter entries but none resolve to registered biome keys"); } } catch (RuntimeException error) { throw new IllegalStateException("Iris failed to resolve biome keys for registered structure '" diff --git a/build.gradle b/build.gradle index 017fa3d76..70645215d 100644 --- a/build.gradle +++ b/build.gradle @@ -122,6 +122,10 @@ nmsBindings.each { key, value -> rootProject.file('adapters/minecraft-common/src/main/java/art/arcane/iris/nativegen/NativeStructurePostProcessor.java').absolutePath) systemProperty('iris.customBiomeSource', rootProject.file("adapters/bukkit/nms/${key}/src/main/java/art/arcane/iris/core/nms/${key}/CustomBiomeSource.java").absolutePath) + systemProperty('iris.vanillaStructureBiomesSource', + rootProject.file("adapters/bukkit/nms/${key}/src/main/java/art/arcane/iris/core/nms/${key}/VanillaStructureBiomes.java").absolutePath) + systemProperty('iris.moddedStructureHooksSource', + rootProject.file('adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedStructureHooks.java').absolutePath) } } } diff --git a/core/src/main/java/art/arcane/iris/core/IrisSettings.java b/core/src/main/java/art/arcane/iris/core/IrisSettings.java index 49295de4d..edcedc6ed 100644 --- a/core/src/main/java/art/arcane/iris/core/IrisSettings.java +++ b/core/src/main/java/art/arcane/iris/core/IrisSettings.java @@ -276,7 +276,13 @@ public class IrisSettings { public boolean useCustomColorsIngame = true; public boolean adjustVanillaHeight = false; public boolean autoIngestDatapacks = true; - public boolean autoImportDatapackStructures = true; + /** + * Converting every registered datapack structure into editable Iris resources writes + * thousands of objects/pools/pieces into the pack folder. Native generation and + * nativeStructures placements never need those copies, so this stays opt-in; run + * /iris structure import <dimension> when you actually want editable copies. + */ + public boolean autoImportDatapackStructures = false; /** Unresolved pack content keys and bad block-state properties become blocking pack errors. -Diris.strictContent overrides. */ public boolean strictContentKeys = false; public int spinh = -20; diff --git a/core/src/main/java/art/arcane/iris/core/IrisWorldStorage.java b/core/src/main/java/art/arcane/iris/core/IrisWorldStorage.java index b4d97b7fa..c3176cc60 100644 --- a/core/src/main/java/art/arcane/iris/core/IrisWorldStorage.java +++ b/core/src/main/java/art/arcane/iris/core/IrisWorldStorage.java @@ -7,19 +7,64 @@ import org.bukkit.World; import org.bukkit.generator.WorldInfo; import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; import java.nio.file.Path; import java.util.Locale; import java.util.Objects; import java.util.Optional; +import java.util.Properties; public final class IrisWorldStorage { private static final String IRIS_NAMESPACE = "iris"; + private static final String DEFAULT_LEVEL_NAME = "world"; + + /** + * Server#getLevelDirectory is Paper-API-only. Once a call throws NoSuchMethodError (plain + * Spigot/CraftBukkit) this flips and every later call goes straight to the fallback. + */ + private static volatile boolean levelDirectoryUnavailable; + private static volatile String cachedFallbackLevelName; private IrisWorldStorage() { } public static File levelRoot() { - return Bukkit.getServer().getLevelDirectory().toAbsolutePath().normalize().toFile(); + if (!levelDirectoryUnavailable) { + try { + return Bukkit.getServer().getLevelDirectory().toAbsolutePath().normalize().toFile(); + } catch (NoSuchMethodError e) { + levelDirectoryUnavailable = true; + } + } + return new File(Bukkit.getWorldContainer(), fallbackLevelName()).getAbsoluteFile(); + } + + private static String fallbackLevelName() { + String cached = cachedFallbackLevelName; + if (cached == null) { + cached = levelNameFromProperties(new File("server.properties")); + cachedFallbackLevelName = cached; + } + return cached; + } + + static String levelNameFromProperties(File serverProperties) { + Properties properties = new Properties(); + if (Objects.requireNonNull(serverProperties, "serverProperties").isFile()) { + try (InputStream in = new FileInputStream(serverProperties)) { + properties.load(in); + } catch (IOException ignored) { + // Unreadable server.properties: fall through to the default level name. + } + } + return levelNameFromProperties(properties); + } + + static String levelNameFromProperties(Properties properties) { + String levelName = Objects.requireNonNull(properties, "properties").getProperty("level-name", DEFAULT_LEVEL_NAME).trim(); + return levelName.isEmpty() ? DEFAULT_LEVEL_NAME : levelName; } public static File levelRoot(File dimensionRoot) { diff --git a/core/src/main/java/art/arcane/iris/core/IrisWorlds.java b/core/src/main/java/art/arcane/iris/core/IrisWorlds.java index 8bc84e4c8..cfc83287c 100644 --- a/core/src/main/java/art/arcane/iris/core/IrisWorlds.java +++ b/core/src/main/java/art/arcane/iris/core/IrisWorlds.java @@ -1,6 +1,7 @@ package art.arcane.iris.core; import art.arcane.iris.core.loader.IrisData; +import art.arcane.iris.core.pack.PackDownloader; import art.arcane.iris.core.service.StudioSVC; import art.arcane.iris.engine.data.cache.AtomicCache; import art.arcane.iris.engine.object.IrisDimension; @@ -159,6 +160,12 @@ public class IrisWorlds { dimension = IrisData.loadAnyDimension(id, null); } if (dimension == null) { + File packsRoot = IrisPlatforms.get().dataFolderNoCreate(StudioSVC.WORKSPACE_NAME); + if (PackDownloader.isPackPresent(packsRoot, id)) { + IrisLogging.error("Pack '" + id + "' exists at " + new File(packsRoot, id).getPath() + + " but its dimension failed to load; not redownloading. Fix or delete the pack folder."); + return null; + } IrisLogging.warn("Unable to find dimension type " + id + " Looking for online packs..."); IrisServices.get(StudioSVC.class).downloadSearch(new VolmitSender(Bukkit.getConsoleSender()), id, false); dimension = IrisData.loadAnyDimension(id, null); diff --git a/core/src/main/java/art/arcane/iris/core/ServerConfigurator.java b/core/src/main/java/art/arcane/iris/core/ServerConfigurator.java index 6d91b1ba3..8bc8654cf 100644 --- a/core/src/main/java/art/arcane/iris/core/ServerConfigurator.java +++ b/core/src/main/java/art/arcane/iris/core/ServerConfigurator.java @@ -363,6 +363,7 @@ public class ServerConfigurator { File[] packs = IrisPlatforms.get().dataFolder("packs").listFiles(File::isDirectory); Stream locals = packs == null ? Stream.empty() : Arrays.stream(packs); return Stream.concat(locals + .filter(base -> !base.getName().contains(".importing-")) .filter( base -> { var content = new File(base, "dimensions").listFiles(); return content != null && content.length > 0; diff --git a/core/src/main/java/art/arcane/iris/core/WorldCreatorCompat.java b/core/src/main/java/art/arcane/iris/core/WorldCreatorCompat.java new file mode 100644 index 000000000..f6f4cf4e1 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/core/WorldCreatorCompat.java @@ -0,0 +1,47 @@ +package art.arcane.iris.core; + +import org.bukkit.NamespacedKey; +import org.bukkit.WorldCreator; + +/** + * WorldCreator.ofKey and WorldCreator#key are Paper-API-only. Once a call throws + * NoSuchMethodError (plain Spigot/CraftBukkit) this flips and every later call goes + * straight to the fallback. The fallback derives names/keys through IrisWorldStorage's + * logical mapping so keyFromName(creator.name()) round-trips on Spigot. + */ +public final class WorldCreatorCompat { + private static volatile boolean keyedCreatorsUnavailable; + + private WorldCreatorCompat() { + } + + public static WorldCreator ofKey(NamespacedKey worldKey) { + if (!keyedCreatorsUnavailable) { + try { + return WorldCreator.ofKey(worldKey); + } catch (NoSuchMethodError e) { + keyedCreatorsUnavailable = true; + } + } + return new WorldCreator(IrisWorldStorage.logicalName(worldKey)); + } + + public static NamespacedKey keyOf(WorldCreator creator) { + if (!keyedCreatorsUnavailable) { + try { + return creator.key(); + } catch (NoSuchMethodError e) { + keyedCreatorsUnavailable = true; + } + } + return IrisWorldStorage.keyFromName(creator.name()); + } + + static String fallbackName(NamespacedKey worldKey, String levelName) { + return IrisWorldStorage.logicalName(worldKey, levelName); + } + + static NamespacedKey fallbackKey(String creatorName, String levelName) { + return IrisWorldStorage.keyFromName(creatorName, levelName); + } +} diff --git a/core/src/main/java/art/arcane/iris/core/datapack/DatapackIngestService.java b/core/src/main/java/art/arcane/iris/core/datapack/DatapackIngestService.java index 32ffc4917..38d246495 100644 --- a/core/src/main/java/art/arcane/iris/core/datapack/DatapackIngestService.java +++ b/core/src/main/java/art/arcane/iris/core/datapack/DatapackIngestService.java @@ -145,7 +145,7 @@ public final class DatapackIngestService { if (report.changed()) { message(sender, C.YELLOW + "New datapack structures were installed. A server restart is required for them to register and generate."); - message(sender, C.GRAY + "After the restart their jigsaw pools, pieces & objects are imported automatically (set general.autoImportDatapackStructures=false to disable), or run /iris structure import to import everything on demand. Reference an imported key from a 'structures' placement to position it manually."); + message(sender, C.GRAY + "After the restart they generate natively - no import needed. To get editable Iris copies (jigsaw pools, pieces & objects written into the pack) run /iris structure import , or set general.autoImportDatapackStructures=true to do it on every ingest. Place any registered key directly with a 'structures' placement using nativeStructures."); message(sender, C.GRAY + "Datapacks replace matching vanilla structure keys by default. Set 'importedStructures.datapackOverrides' to false to keep minecraft-namespaced structure definitions untouched; deny non-minecraft datapack and mod structures explicitly with importedStructures.disabled."); if (restart) { ServerConfigurator.restart(); diff --git a/core/src/main/java/art/arcane/iris/core/lifecycle/WorldLifecycleRequest.java b/core/src/main/java/art/arcane/iris/core/lifecycle/WorldLifecycleRequest.java index b2e32a530..aaf161602 100644 --- a/core/src/main/java/art/arcane/iris/core/lifecycle/WorldLifecycleRequest.java +++ b/core/src/main/java/art/arcane/iris/core/lifecycle/WorldLifecycleRequest.java @@ -1,5 +1,6 @@ package art.arcane.iris.core.lifecycle; +import art.arcane.iris.core.WorldCreatorCompat; import org.bukkit.NamespacedKey; import org.bukkit.World; import org.bukkit.WorldCreator; @@ -24,7 +25,7 @@ public record WorldLifecycleRequest( public static WorldLifecycleRequest fromCreator(WorldCreator creator, boolean studio, boolean benchmark, WorldLifecycleCaller callerKind) { return new WorldLifecycleRequest( creator.name(), - creator.key(), + WorldCreatorCompat.keyOf(creator), creator.environment(), creator.generator(), creator.biomeProvider(), @@ -39,7 +40,7 @@ public record WorldLifecycleRequest( } public WorldCreator toWorldCreator() { - WorldCreator creator = WorldCreator.ofKey(worldKey) + WorldCreator creator = WorldCreatorCompat.ofKey(worldKey) .environment(environment) .generateStructures(generateStructures) .hardcore(hardcore) diff --git a/core/src/main/java/art/arcane/iris/core/localization/PackDownloadMessages.java b/core/src/main/java/art/arcane/iris/core/localization/PackDownloadMessages.java index 9296c7de6..c24712a0a 100644 --- a/core/src/main/java/art/arcane/iris/core/localization/PackDownloadMessages.java +++ b/core/src/main/java/art/arcane/iris/core/localization/PackDownloadMessages.java @@ -94,6 +94,10 @@ public final class PackDownloadMessages { "iris.runtime.pack_download.acquired", "Successfully acquired {name}." ); + public static final TextKey ALREADY_INSTALLED = TextKey.of( + "iris.runtime.pack_download.already_installed", + "Pack {key} is already installed, skipping download." + ); public static final TextKey VALIDATION_FAILED = TextKey.of( "iris.runtime.pack_download.validation_failed", "Pack '{pack}' failed validation; world and Studio creation will be refused. Reasons:" @@ -136,6 +140,7 @@ public final class PackDownloadMessages { DIMENSION_KEY_CONFLICT, PACK_KEY_CONFLICT, ACQUIRED, + ALREADY_INSTALLED, VALIDATION_FAILED, VALIDATION_REASON, VALIDATED_WITH_WARNINGS, diff --git a/core/src/main/java/art/arcane/iris/core/pack/PackDownloader.java b/core/src/main/java/art/arcane/iris/core/pack/PackDownloader.java index a5ef9d11c..6c04d67e5 100644 --- a/core/src/main/java/art/arcane/iris/core/pack/PackDownloader.java +++ b/core/src/main/java/art/arcane/iris/core/pack/PackDownloader.java @@ -32,6 +32,7 @@ import org.zeroturnaround.zip.commons.FileUtils; import java.io.File; import java.io.IOException; import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; import java.util.function.Consumer; import java.util.regex.Pattern; @@ -42,6 +43,7 @@ public final class PackDownloader { private static final Pattern GITHUB_REPOSITORY = Pattern.compile("[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+"); private static final Pattern GITHUB_REF = Pattern.compile("[A-Za-z0-9][A-Za-z0-9._/-]*"); private static final Pattern COMMIT_SHA = Pattern.compile("[0-9a-fA-F]{40}"); + private static final ConcurrentHashMap DOWNLOAD_LOCKS = new ConcurrentHashMap<>(); private PackDownloader() { } @@ -50,11 +52,51 @@ public final class PackDownloader { return DEFAULT_OVERWORLD_PACK.equals(pack); } - public static String downloadDefaultOverworld(File packsFolder, boolean forceOverwrite, Consumer feedback) throws IOException { - return download(packsFolder, DEFAULT_OVERWORLD_REPOSITORY, defaultOverworldReleaseUrl(), forceOverwrite, true, feedback); + public static String defaultOverworldPack() { + return DEFAULT_OVERWORLD_PACK; } - public static String download(File packsFolder, String repo, String ref, boolean forceOverwrite, boolean directUrl, Consumer feedback) throws IOException { + /** + * Whether a pack folder for {@code key} already exists with at least one dimension file. + * Presence is judged on disk, not on loadability: a pack that exists but fails to parse must + * surface as an error, never as a redownload. A folder without any dimensions/*.json is a + * partial import (an interrupted copy) and counts as absent so it can be replaced. + */ + public static boolean isPackPresent(File packsFolder, String key) { + if (packsFolder == null || key == null || key.isBlank()) { + return false; + } + File[] dimensions = new File(new File(packsFolder, key), "dimensions") + .listFiles((File dir, String name) -> name.endsWith(".json")); + return dimensions != null && dimensions.length > 0; + } + + public static String downloadDefaultOverworld(File packsFolder, boolean forceOverwrite, Consumer feedback) throws IOException { + return download(packsFolder, DEFAULT_OVERWORLD_REPOSITORY, defaultOverworldReleaseUrl(), forceOverwrite, true, DEFAULT_OVERWORLD_PACK, feedback); + } + + /** + * Downloads and imports a pack. {@code expectedKey} is the pack key the caller is trying to + * obtain (null when unknown, e.g. arbitrary repo/branch downloads); when the key is already + * present on disk and {@code forceOverwrite} is false, the network is never touched. The + * per-repo lock keeps concurrent startup triggers (async default-pack install racing world + * resolution) from downloading the same archive twice. + */ + public static String download(File packsFolder, String repo, String ref, boolean forceOverwrite, boolean directUrl, String expectedKey, Consumer feedback) throws IOException { + // Lock on the destination pack key when known: concurrent triggers for the same pack can + // arrive with different refs (release URL vs listing branch) and must still serialize. + String lockKey = expectedKey != null && !expectedKey.isBlank() ? "key:" + expectedKey : "ref:" + repo + "|" + ref; + Object lock = DOWNLOAD_LOCKS.computeIfAbsent(lockKey, key -> new Object()); + synchronized (lock) { + if (!forceOverwrite && isPackPresent(packsFolder, expectedKey)) { + feedback.accept(IrisLanguage.plain(PackDownloadMessages.ALREADY_INSTALLED, MessageArgument.untrusted("key", expectedKey))); + return expectedKey; + } + return downloadLocked(packsFolder, repo, ref, forceOverwrite, directUrl, feedback); + } + } + + private static String downloadLocked(File packsFolder, String repo, String ref, boolean forceOverwrite, boolean directUrl, Consumer feedback) throws IOException { String url = directUrl ? ref : resolveGithubArchiveUrl(repo, ref); feedback.accept(IrisLanguage.plain(PackDownloadMessages.DOWNLOADING, MessageArgument.untrusted("url", url)) + " "); //The extra space stops a bug in adventure API from repeating the last letter of the URL File zip = WebCache.getNonCachedFile("pack-" + repo, url); @@ -122,6 +164,12 @@ public final class PackDownloader { String key = d.getLoadKey(); feedback.accept(IrisLanguage.plain(PackDownloadMessages.IMPORTING, MessageArgument.untrusted("name", d.getName()), MessageArgument.untrusted("key", key))); File packEntry = new File(packsFolder, key); + File[] staleStaging = packsFolder.listFiles((File parent, String name) -> name.startsWith(key + ".importing-")); + if (staleStaging != null) { + for (File stale : staleStaging) { + IO.delete(stale); + } + } if (forceOverwrite) { IO.delete(packEntry); @@ -134,11 +182,29 @@ public final class PackDownloader { File[] existingEntries = packEntry.listFiles(); if (packEntry.exists() && existingEntries != null && existingEntries.length > 0) { - feedback.accept(IrisLanguage.plain(PackDownloadMessages.PACK_KEY_CONFLICT, MessageArgument.untrusted("key", key))); - return null; + if (isPackPresent(packsFolder, key)) { + feedback.accept(IrisLanguage.plain(PackDownloadMessages.PACK_KEY_CONFLICT, MessageArgument.untrusted("key", key))); + return null; + } + // Non-empty but no dimension file: a partial import from an interrupted copy. + // Replace it instead of refusing forever. + IrisLogging.warn("Replacing partial pack folder " + packEntry.getPath() + " (no dimension files found)."); + IO.delete(packEntry); } - FileUtils.copyDirectory(dir, packEntry); + // Stage inside the packs folder and move into place so packs/ is never partial: + // an interrupted copy previously left a folder without dimensions/, which then blocked + // every future import as a key conflict. + File staging = new File(packsFolder, key + ".importing-" + UUID.randomUUID()); + try { + FileUtils.copyDirectory(dir, staging); + if (!staging.renameTo(packEntry)) { + throw new IOException("Unable to move imported pack into place: " + packEntry.getPath()); + } + } catch (IOException | RuntimeException e) { + IO.delete(staging); + throw e; + } IrisData.getLoaded(packEntry) .ifPresent(IrisData::hotloaded); diff --git a/core/src/main/java/art/arcane/iris/core/project/SchemaBuilder.java b/core/src/main/java/art/arcane/iris/core/project/SchemaBuilder.java index 207d56ba2..e084dde48 100644 --- a/core/src/main/java/art/arcane/iris/core/project/SchemaBuilder.java +++ b/core/src/main/java/art/arcane/iris/core/project/SchemaBuilder.java @@ -72,6 +72,8 @@ public class SchemaBuilder { private static final String SYMBOL_LIMIT__N = "*"; private static final String SYMBOL_TYPE__N = ""; private static final String MINECRAFT_NAMESPACE = "minecraft:"; + /** Namespaced key or family/namespace prefix: "minecraft:village_plains", "minecraft:village", "nova_structures:". */ + private static final String VANILLA_STRUCTURE_PREFIX_PATTERN = "^[a-z0-9_.-]+:[a-z0-9_./-]*$"; private static volatile JSONArray fontTypes; private final KMap definitions; private final Class root; @@ -249,6 +251,46 @@ public class SchemaBuilder { } } + /** + * A registry enum that ALSO accepts family/namespace prefixes ("minecraft:village", + * "nova_structures:") — the runtime prefix-matching contract of importedStructures.disabled and + * adjustments[].match. Emitted as anyOf(enum, pattern) so autocomplete still offers registered + * keys while prefix entries validate instead of being rejected. + */ + private void putRegistryEnumOrPrefixRef(JSONObject target, String definitionKey, + String enumDefinitionKey, Supplier values, + String pattern) { + if (!definitions.containsKey(definitionKey)) { + JSONArray anyOf = new JSONArray(); + JSONObject enumRef = new JSONObject(); + try { + putRegistryEnumRef(enumRef, enumDefinitionKey, values); + } catch (RuntimeException e) { + IrisLogging.debug("Schema enum '" + enumDefinitionKey + "' unavailable (" + + e.getMessage() + "); emitting prefix pattern only"); + } + if (enumRef.has("$ref")) { + anyOf.put(enumRef); + } + JSONObject prefix = new JSONObject(); + prefix.put("type", "string"); + prefix.put("pattern", pattern); + anyOf.put(prefix); + JSONObject definition = new JSONObject(); + definition.put("anyOf", anyOf); + definitions.put(definitionKey, definition); + } + target.put("$ref", "#/definitions/" + definitionKey); + } + + private void putRegistryEnumOrPrefixItems(JSONObject prop, String definitionKey, + String enumDefinitionKey, Supplier values, + String pattern) { + JSONObject items = new JSONObject(); + putRegistryEnumOrPrefixRef(items, definitionKey, enumDefinitionKey, values, pattern); + prop.put("items", items); + } + private JSONArray itemTypes() { JSONArray a = new JSONArray(); for (String key : IrisPlatforms.get().registries().itemKeys()) { @@ -427,8 +469,14 @@ public class SchemaBuilder { } else if (k.isAnnotationPresent(RegistryListVanillaStructure.class)) { fancyType = "Vanilla Structure"; - putRegistryEnumRef(prop, "enum-vanilla-structure", this::vanillaStructures); - description.add(SYMBOL_TYPE__N + " Must be a valid vanilla/datapack structure key (use ctrl+space for auto complete!)"); + if (k.getAnnotation(RegistryListVanillaStructure.class).prefixes()) { + putRegistryEnumOrPrefixRef(prop, "enum-vanilla-structure-or-prefix", + "enum-vanilla-structure", this::vanillaStructures, VANILLA_STRUCTURE_PREFIX_PATTERN); + description.add(SYMBOL_TYPE__N + " Must be a vanilla/datapack structure key or a family/namespace prefix like 'minecraft:village' or 'nova_structures:' (use ctrl+space for auto complete!)"); + } else { + putRegistryEnumRef(prop, "enum-vanilla-structure", this::vanillaStructures); + description.add(SYMBOL_TYPE__N + " Must be a valid vanilla/datapack structure key (use ctrl+space for auto complete!)"); + } } else if (k.isAnnotationPresent(RegistryListVanillaStructureSet.class)) { fancyType = "Vanilla Structure Set"; @@ -627,8 +675,14 @@ public class SchemaBuilder { description.add(SYMBOL_TYPE__N + " Must be a registered vanilla, datapack, or modded template pool key (use ctrl+space for auto complete!)"); } else if (k.isAnnotationPresent(RegistryListVanillaStructure.class)) { fancyType = "List"; - putRegistryEnumItems(prop, "enum-vanilla-structure", this::vanillaStructures); - description.add(SYMBOL_TYPE__N + " Must be a valid vanilla/datapack structure key (use ctrl+space for auto complete!)"); + if (k.getAnnotation(RegistryListVanillaStructure.class).prefixes()) { + putRegistryEnumOrPrefixItems(prop, "enum-vanilla-structure-or-prefix", + "enum-vanilla-structure", this::vanillaStructures, VANILLA_STRUCTURE_PREFIX_PATTERN); + description.add(SYMBOL_TYPE__N + " Must be a vanilla/datapack structure key or a family/namespace prefix like 'minecraft:village' or 'nova_structures:' (use ctrl+space for auto complete!)"); + } else { + putRegistryEnumItems(prop, "enum-vanilla-structure", this::vanillaStructures); + description.add(SYMBOL_TYPE__N + " Must be a valid vanilla/datapack structure key (use ctrl+space for auto complete!)"); + } } else if (k.isAnnotationPresent(RegistryListVanillaStructureSet.class)) { fancyType = "List"; putRegistryEnumItems(prop, "enum-vanilla-structure-set", this::vanillaStructureSets); diff --git a/core/src/main/java/art/arcane/iris/core/service/StudioSVC.java b/core/src/main/java/art/arcane/iris/core/service/StudioSVC.java index 0ae07e4f6..7870362c5 100644 --- a/core/src/main/java/art/arcane/iris/core/service/StudioSVC.java +++ b/core/src/main/java/art/arcane/iris/core/service/StudioSVC.java @@ -26,7 +26,6 @@ import art.arcane.iris.core.IrisWorldStorage; import art.arcane.iris.core.ServerConfigurator; import art.arcane.iris.core.lifecycle.WorldLifecycleService; import art.arcane.iris.core.loader.IrisData; -import art.arcane.iris.core.pack.IrisPack; import art.arcane.iris.core.pack.PackDownloader; import art.arcane.iris.core.pack.PackValidationRegistry; import art.arcane.iris.core.pack.PackValidationResult; @@ -63,6 +62,7 @@ import java.util.function.Consumer; import art.arcane.iris.core.localization.BukkitRuntimeMessages; import art.arcane.iris.core.localization.IrisLanguage; +import art.arcane.iris.core.localization.PackDownloadMessages; import art.arcane.volmlib.util.localization.MessageArgument; public class StudioSVC implements IrisService { public static final String LISTING = "https://raw.githubusercontent.com/IrisDimensions/_listing/main/listing-v2.json"; @@ -76,9 +76,10 @@ public class StudioSVC implements IrisService { public void onEnable() { J.a(() -> { String pack = IrisSettings.get().getGenerator().getDefaultWorldType(); - File f = IrisPack.packsPack(pack); - if (!f.exists()) { + // Presence means a non-empty pack folder: an empty leftover folder must still + // trigger the install instead of shadowing it forever. + if (!PackDownloader.isPackPresent(getWorkspaceFolder(), pack)) { if (PackDownloader.isDefaultOverworld(pack)) { IrisLogging.info("Downloading Default Pack " + pack + " (beta release)"); IrisServices.get(StudioSVC.class).downloadDefaultOverworld(BukkitPlatform.console(), false); @@ -203,8 +204,8 @@ public class StudioSVC implements IrisService { } } } - - IO.delete(downloaded); + // The downloaded pack stays in the packs workspace: deleting it here made the + // next startup see a missing pack and download it again. } } @@ -231,6 +232,14 @@ public class StudioSVC implements IrisService { } public void downloadSearch(VolmitSender sender, String key, boolean forceOverwrite) { + // The default overworld always comes from the pinned release + // (PackDownloader.DEFAULT_OVERWORLD_RELEASE_URL), never from the listing, + // so every code path ships the same pack build. + if (PackDownloader.isDefaultOverworld(key)) { + downloadDefaultOverworld(sender, forceOverwrite); + return; + } + try { String url = getListing(false).get(key); @@ -244,7 +253,8 @@ public class StudioSVC implements IrisService { String[] nodes = url.split("\\Q/\\E"); String repo = nodes.length == 1 ? "IrisDimensions/" + nodes[0] : nodes[0] + "/" + nodes[1]; String branch = nodes.length > 2 ? nodes[2] : "stable"; - download(sender, repo, branch, forceOverwrite, false); + String expectedKey = key.contains("/") ? null : key; + download(sender, repo, branch, forceOverwrite, false, expectedKey); } catch (Throwable e) { IrisLogging.reportError(e); e.printStackTrace(); @@ -253,6 +263,13 @@ public class StudioSVC implements IrisService { } public void downloadDefaultOverworld(VolmitSender sender, boolean forceOverwrite) { + // Same guard as download(): a present pack must not reach installDataPacks(true), + // which can trigger an automatic restart. + if (!forceOverwrite && PackDownloader.isPackPresent(getWorkspaceFolder(), PackDownloader.defaultOverworldPack())) { + sender.sendMessage(IrisLanguage.text(PackDownloadMessages.ALREADY_INSTALLED, MessageArgument.untrusted("key", PackDownloader.defaultOverworldPack()))); + return; + } + try { String key = PackDownloader.downloadDefaultOverworld(getWorkspaceFolder(), forceOverwrite, sender::sendMessage); if (key != null) { @@ -280,7 +297,18 @@ public class StudioSVC implements IrisService { } public void download(VolmitSender sender, String repo, String branch, boolean forceOverwrite, boolean directUrl) throws JsonSyntaxException, IOException { - String key = PackDownloader.download(getWorkspaceFolder(), repo, branch, forceOverwrite, directUrl, sender::sendMessage); + download(sender, repo, branch, forceOverwrite, directUrl, null); + } + + public void download(VolmitSender sender, String repo, String branch, boolean forceOverwrite, boolean directUrl, String expectedKey) throws JsonSyntaxException, IOException { + // Skip before PackDownloader so an already-present pack never reaches + // installDataPacks(true), which can trigger an automatic restart. + if (!forceOverwrite && PackDownloader.isPackPresent(getWorkspaceFolder(), expectedKey)) { + sender.sendMessage(IrisLanguage.text(PackDownloadMessages.ALREADY_INSTALLED, MessageArgument.untrusted("key", expectedKey))); + return; + } + + String key = PackDownloader.download(getWorkspaceFolder(), repo, branch, forceOverwrite, directUrl, expectedKey, sender::sendMessage); if (key == null) { return; diff --git a/core/src/main/java/art/arcane/iris/core/splash/IrisSplashComposer.java b/core/src/main/java/art/arcane/iris/core/splash/IrisSplashComposer.java index 1b7044cff..b90891afa 100644 --- a/core/src/main/java/art/arcane/iris/core/splash/IrisSplashComposer.java +++ b/core/src/main/java/art/arcane/iris/core/splash/IrisSplashComposer.java @@ -22,12 +22,12 @@ public final class IrisSplashComposer { prefix + style.title(" Iris, ") + style.subtitle("Dimension Engine ") + style.tag("[" + releaseTrain + "]"), prefix + style.label(" Version: ") + style.value(version), prefix + style.label(" By: ") + style.value("Volmit Software (Arcane Arts)"), + prefix + style.label(" Web: ") + style.value("VolmitSoftware.com"), prefix + style.label(" Server: ") + style.value(serverLine), prefix + style.label(" Java: ") + style.value(String.valueOf(javaVersion())) + style.label(" | Date: ") + style.value(startupDate()), prefix + style.label(" Commit: ") + style.value(BuildConstants.COMMIT) + style.label("/") + style.value(BuildConstants.ENVIRONMENT), "", "", - "", "" }; } diff --git a/core/src/main/java/art/arcane/iris/core/structure/FeatureImporter.java b/core/src/main/java/art/arcane/iris/core/structure/FeatureImporter.java index 58186ed89..634336561 100644 --- a/core/src/main/java/art/arcane/iris/core/structure/FeatureImporter.java +++ b/core/src/main/java/art/arcane/iris/core/structure/FeatureImporter.java @@ -19,6 +19,7 @@ package art.arcane.iris.core.structure; import art.arcane.iris.core.IrisWorldStorage; +import art.arcane.iris.core.WorldCreatorCompat; import art.arcane.iris.spi.IrisLogging; import art.arcane.iris.core.loader.IrisData; import art.arcane.iris.core.nms.INMS; @@ -301,7 +302,7 @@ public final class FeatureImporter { if (existing != null) { return existing; } - WorldCreator creator = WorldCreator.ofKey(IrisWorldStorage.keyFromName(SCRATCH_WORLD_NAME)) + WorldCreator creator = WorldCreatorCompat.ofKey(IrisWorldStorage.keyFromName(SCRATCH_WORLD_NAME)) .environment(World.Environment.NORMAL) .type(WorldType.FLAT) .generateStructures(false); diff --git a/core/src/main/java/art/arcane/iris/core/structure/VillageImporter.java b/core/src/main/java/art/arcane/iris/core/structure/VillageImporter.java index b6bff2465..a4d4dfb40 100644 --- a/core/src/main/java/art/arcane/iris/core/structure/VillageImporter.java +++ b/core/src/main/java/art/arcane/iris/core/structure/VillageImporter.java @@ -41,6 +41,7 @@ import java.io.ByteArrayOutputStream; import java.io.IOException; import java.lang.reflect.Field; import java.lang.reflect.Method; +import java.lang.reflect.Modifier; import java.util.ArrayDeque; import java.util.ArrayList; import java.util.Deque; @@ -49,9 +50,11 @@ import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; public final class VillageImporter { private static final Gson GSON = new GsonBuilder().setPrettyPrinting().create(); + private static final Set PRINTED_FAILURE_SIGNATURES = ConcurrentHashMap.newKeySet(); public record Result(boolean success, String message, int pools, int pieces, List losses) { public Result { @@ -686,25 +689,62 @@ public final class VillageImporter { return opt; } - private static int readIntMember(Object value, String memberName) throws Exception { + static int readIntMember(Object value, String memberName) throws Exception { Class type = value.getClass(); while (type != null) { try { Field field = type.getDeclaredField(memberName); field.setAccessible(true); - return field.getInt(value); + return coerceInt(field.get(value), memberName, value); } catch (NoSuchFieldException ignored) { type = type.getSuperclass(); } } Method method = findMethod(value.getClass(), memberName); - if (method != null && Number.class.isAssignableFrom(boxedType(method.getReturnType()))) { + if (method != null) { method.setAccessible(true); - return ((Number) method.invoke(value)).intValue(); + return coerceInt(method.invoke(value), memberName, value); } throw new NoSuchFieldException(memberName + " on " + value.getClass().getName()); } + /** + * Members that are plain numbers on one server build are wrapper objects on another + * (JigsawStructure.maxDistanceFromCenter became a MaxDistance{horizontal, vertical} record). + * Numbers pass through; a wrapper contributes its horizontal component, else its largest + * integral component, so a distance bound is never under-read. + */ + private static int coerceInt(Object member, String memberName, Object owner) throws Exception { + if (member instanceof Number n) { + return n.intValue(); + } + if (member == null) { + throw new NoSuchFieldException(memberName + " on " + owner.getClass().getName() + " is null"); + } + Method horizontal = findMethod(member.getClass(), "horizontal"); + if (horizontal != null && Number.class.isAssignableFrom(boxedType(horizontal.getReturnType()))) { + horizontal.setAccessible(true); + return ((Number) horizontal.invoke(member)).intValue(); + } + Integer widest = null; + for (Field component : member.getClass().getDeclaredFields()) { + if (Modifier.isStatic(component.getModifiers()) + || !Number.class.isAssignableFrom(boxedType(component.getType()))) { + continue; + } + component.setAccessible(true); + Object componentValue = component.get(member); + if (componentValue instanceof Number n && (widest == null || n.intValue() > widest)) { + widest = n.intValue(); + } + } + if (widest != null) { + return widest; + } + throw new NoSuchFieldException(memberName + " on " + owner.getClass().getName() + + " is a " + member.getClass().getName() + " with no integral component"); + } + private static Class boxedType(Class type) { return type == int.class ? Integer.class : type; } @@ -828,7 +868,28 @@ public final class VillageImporter { private static void reportFailure(Throwable failure) { IrisLogging.reportError(failure); - failure.printStackTrace(); + if (shouldPrintFullTrace(failure)) { + failure.printStackTrace(); + } + } + + /** + * True the first time a failure signature is seen. A bulk import repeats the same failure once + * per registered structure, so printing every trace buries the boot log in hundreds of copies + * of one problem; the per-structure "[fail] key: message" line still reports each occurrence. + */ + static boolean shouldPrintFullTrace(Throwable failure) { + if (failure == null) { + return false; + } + StackTraceElement[] trace = failure.getStackTrace(); + String signature = failure.getClass().getName() + '|' + failure.getMessage() + + '|' + (trace.length == 0 ? "" : trace[0].toString()); + return PRINTED_FAILURE_SIGNATURES.add(signature); + } + + static void resetFailureLogState() { + PRINTED_FAILURE_SIGNATURES.clear(); } private static void reportWriteFailure(StructureWriteResult result) { diff --git a/core/src/main/java/art/arcane/iris/core/tools/IrisCreator.java b/core/src/main/java/art/arcane/iris/core/tools/IrisCreator.java index c051e0fd8..32d9a8c7b 100644 --- a/core/src/main/java/art/arcane/iris/core/tools/IrisCreator.java +++ b/core/src/main/java/art/arcane/iris/core/tools/IrisCreator.java @@ -27,6 +27,7 @@ import art.arcane.iris.platform.bukkit.BukkitPlatform; import art.arcane.iris.core.link.MultiverseCoreLink; import art.arcane.iris.core.IrisRuntimeSchedulerMode; import art.arcane.iris.core.IrisWorldStorage; +import art.arcane.iris.core.WorldCreatorCompat; import art.arcane.iris.core.IrisWorlds; import art.arcane.iris.core.IrisSettings; import art.arcane.iris.core.ServerConfigurator; @@ -204,7 +205,7 @@ public class IrisCreator { .studio(studio) .create(); if (!studio()) { - IrisWorlds.get().put(wc.key().toString(), dimension()); + IrisWorlds.get().put(WorldCreatorCompat.keyOf(wc).toString(), dimension()); } ServerConfigurator.installDataPacksIfChanged(!studio()); IrisLogging.debug("[Studio timing] create.packPrep + datapacks = " + (System.currentTimeMillis() - createStart) + "ms (cumulative in create)"); diff --git a/core/src/main/java/art/arcane/iris/core/tools/IrisWorldCreator.java b/core/src/main/java/art/arcane/iris/core/tools/IrisWorldCreator.java index 7aaa3b878..669d0815a 100644 --- a/core/src/main/java/art/arcane/iris/core/tools/IrisWorldCreator.java +++ b/core/src/main/java/art/arcane/iris/core/tools/IrisWorldCreator.java @@ -19,6 +19,7 @@ package art.arcane.iris.core.tools; import art.arcane.iris.core.IrisWorldStorage; +import art.arcane.iris.core.WorldCreatorCompat; import art.arcane.iris.core.loader.IrisData; import art.arcane.iris.engine.object.IrisDimension; import art.arcane.iris.engine.object.IrisWorld; @@ -92,7 +93,7 @@ public class IrisWorldCreator { new File(w.worldFolder(), "iris/pack"), dimensionName); - return WorldCreator.ofKey(worldKey) + return WorldCreatorCompat.ofKey(worldKey) .environment(environment) .generateStructures(true) .generator(g).seed(seed); diff --git a/core/src/main/java/art/arcane/iris/engine/framework/NativeStructureGenerationPolicy.java b/core/src/main/java/art/arcane/iris/engine/framework/NativeStructureGenerationPolicy.java index 4889b5d9a..fac5bfc63 100644 --- a/core/src/main/java/art/arcane/iris/engine/framework/NativeStructureGenerationPolicy.java +++ b/core/src/main/java/art/arcane/iris/engine/framework/NativeStructureGenerationPolicy.java @@ -39,6 +39,13 @@ public final class NativeStructureGenerationPolicy { "Dimension importedStructures must not be null"); IrisNativeStructureDecision decision = control.resolve(structureKey, undergroundStep); if (!decision.generate()) { + // A disabled key with an active Iris placement is the "blanket-disable, re-place + // explicitly" pattern: the placement planner ignores the disable list, so generation + // places it — report REPLACED_BY_IRIS so find/goto/verify locate the placement. + if (decision.status() == NativeStructureGenerationStatus.DISABLED_BY_PACK + && IrisStructureLocator.isPlaced(activeEngine, structureKey)) { + return decision.withStatus(NativeStructureGenerationStatus.REPLACED_BY_IRIS); + } return decision; } if (IrisStructureLocator.suppressesVanilla(activeEngine, structureKey)) { diff --git a/core/src/main/java/art/arcane/iris/engine/object/IrisImportedStructureControl.java b/core/src/main/java/art/arcane/iris/engine/object/IrisImportedStructureControl.java index 3583d7668..3f094fb9f 100644 --- a/core/src/main/java/art/arcane/iris/engine/object/IrisImportedStructureControl.java +++ b/core/src/main/java/art/arcane/iris/engine/object/IrisImportedStructureControl.java @@ -39,7 +39,7 @@ import java.util.Objects; @Data public class IrisImportedStructureControl { @ArrayType(type = String.class, min = 1) - @RegistryListVanillaStructure + @RegistryListVanillaStructure(prefixes = true) @Desc("Structure keys to deny explicitly, e.g. 'minecraft:stronghold'. A namespace:path prefix also matches, so 'minecraft:village' disables every village variant and 'minecraft:ruined_portal' disables every ruined portal. Every key not matched here remains enabled.") private KList disabled = new KList<>(); diff --git a/core/src/main/java/art/arcane/iris/engine/object/IrisVanillaStructureAdjustment.java b/core/src/main/java/art/arcane/iris/engine/object/IrisVanillaStructureAdjustment.java index 7aae79b79..6730ed680 100644 --- a/core/src/main/java/art/arcane/iris/engine/object/IrisVanillaStructureAdjustment.java +++ b/core/src/main/java/art/arcane/iris/engine/object/IrisVanillaStructureAdjustment.java @@ -36,7 +36,7 @@ import lombok.experimental.Accessors; @Data public class IrisVanillaStructureAdjustment { @ArrayType(type = String.class, min = 1) - @RegistryListVanillaStructure + @RegistryListVanillaStructure(prefixes = true) @Desc("Structure keys this adjustment applies to, e.g. 'minecraft:stronghold'. A namespace:path prefix also matches, so 'minecraft:village' adjusts every village variant and 'minecraft:ruined_portal' adjusts every ruined portal. Empty matches nothing.") private KList match = new KList<>(); diff --git a/core/src/main/java/art/arcane/iris/engine/object/annotations/RegistryListVanillaStructure.java b/core/src/main/java/art/arcane/iris/engine/object/annotations/RegistryListVanillaStructure.java index 138eeb11c..58bd66442 100644 --- a/core/src/main/java/art/arcane/iris/engine/object/annotations/RegistryListVanillaStructure.java +++ b/core/src/main/java/art/arcane/iris/engine/object/annotations/RegistryListVanillaStructure.java @@ -29,5 +29,12 @@ import static java.lang.annotation.RetentionPolicy.RUNTIME; @Retention(RUNTIME) @Target({PARAMETER, TYPE, FIELD}) public @interface RegistryListVanillaStructure { - + /** + * When true the field accepts family/namespace prefixes in addition to exact registered keys + * (the {@code IrisImportedStructureControl.matchesKey} contract: "minecraft:village" matches + * every village variant, "nova_structures:" matches a whole namespace). The generated editor + * schema then validates entries against the registry enum OR a key/prefix pattern instead of + * rejecting anything that is not an exact registered key. + */ + boolean prefixes() default false; } diff --git a/core/src/main/resources/languages/de_DE.json b/core/src/main/resources/languages/de_DE.json index 22e6f31a7..66cc2079d 100644 --- a/core/src/main/resources/languages/de_DE.json +++ b/core/src/main/resources/languages/de_DE.json @@ -1295,6 +1295,7 @@ "iris.runtime.pack_download.dimension_key_conflict": "Eine andere Dimension im Pack-Ordner verwendet bereits den Schlüssel {key}. Import fehlgeschlagen!", "iris.runtime.pack_download.pack_key_conflict": "Ein anderer Pack verwendet bereits den Schlüssel {key}. Import fehlgeschlagen!", "iris.runtime.pack_download.acquired": "{name} erfolgreich abgerufen.", + "iris.runtime.pack_download.already_installed": "Pack {key} ist bereits installiert, Download wird übersprungen.", "iris.runtime.pack_download.validation_failed": "Pack '{pack}' hat die Validierung nicht bestanden; Welt- und Studio-Erstellung werden verweigert. Gründe:", "iris.runtime.pack_download.validation_reason": " - {reason}", "iris.runtime.pack_download.validated_with_warnings": { diff --git a/core/src/main/resources/languages/es_ES.json b/core/src/main/resources/languages/es_ES.json index b907396ec..ecf7a4594 100644 --- a/core/src/main/resources/languages/es_ES.json +++ b/core/src/main/resources/languages/es_ES.json @@ -1295,6 +1295,7 @@ "iris.runtime.pack_download.dimension_key_conflict": "Otra dimensión de la carpeta packs ya usa la clave {key}. ¡La importación falló!", "iris.runtime.pack_download.pack_key_conflict": "Otro pack usa la clave {key}. ¡La importación falló!", "iris.runtime.pack_download.acquired": "{name} se obtuvo correctamente.", + "iris.runtime.pack_download.already_installed": "El pack {key} ya está instalado, se omite la descarga.", "iris.runtime.pack_download.validation_failed": "El pack '{pack}' no superó la validación; se rechazará la creación de mundos y de Studio. Motivos:", "iris.runtime.pack_download.validation_reason": " - {reason}", "iris.runtime.pack_download.validated_with_warnings": { diff --git a/core/src/main/resources/languages/fi_FI.json b/core/src/main/resources/languages/fi_FI.json index 6b64aa1bb..a5aabf934 100644 --- a/core/src/main/resources/languages/fi_FI.json +++ b/core/src/main/resources/languages/fi_FI.json @@ -1295,6 +1295,7 @@ "iris.runtime.pack_download.dimension_key_conflict": "Toinen ulottuvuus pakkauskansiossa jo käyttää avainta {key}. Tuonti epäonnistui!", "iris.runtime.pack_download.pack_key_conflict": "Toinen pakkaus käyttää avainta {key}. Tuonti epäonnistui!", "iris.runtime.pack_download.acquired": "Onnistunut hankinta {name}.", + "iris.runtime.pack_download.already_installed": "Pack {key} on jo asennettu, lataus ohitetaan.", "iris.runtime.pack_download.validation_failed": "Pakkaus{pack}' Epäonnistunut validointi; maailma ja Studio luominen hylätään. Perusteet:", "iris.runtime.pack_download.validation_reason": " - {reason}", "iris.runtime.pack_download.validated_with_warnings": { diff --git a/core/src/main/resources/languages/fr_FR.json b/core/src/main/resources/languages/fr_FR.json index dec706ebe..46263dbd2 100644 --- a/core/src/main/resources/languages/fr_FR.json +++ b/core/src/main/resources/languages/fr_FR.json @@ -1295,6 +1295,7 @@ "iris.runtime.pack_download.dimension_key_conflict": "Une autre dimension du dossier packs utilise déjà la clé {key}. Échec de l'importation !", "iris.runtime.pack_download.pack_key_conflict": "Un autre pack utilise la clé {key}. Échec de l'importation !", "iris.runtime.pack_download.acquired": "{name} obtenu avec succès.", + "iris.runtime.pack_download.already_installed": "Le pack {key} est déjà installé, téléchargement ignoré.", "iris.runtime.pack_download.validation_failed": "Le pack '{pack}' a échoué à la validation ; la création de mondes et de Studio sera refusée. Raisons :", "iris.runtime.pack_download.validation_reason": " - {reason}", "iris.runtime.pack_download.validated_with_warnings": { diff --git a/core/src/main/resources/languages/he_IL.json b/core/src/main/resources/languages/he_IL.json index 7e4416e07..e67d90bd7 100644 --- a/core/src/main/resources/languages/he_IL.json +++ b/core/src/main/resources/languages/he_IL.json @@ -1295,6 +1295,7 @@ "iris.runtime.pack_download.dimension_key_conflict": "מימד נוסף בתיקיה של הלהקה כבר משתמש המפתח {key}. ייבוא נכשל!", "iris.runtime.pack_download.pack_key_conflict": "חבילה נוספת משתמשת במפתח {key}. ייבוא נכשל!", "iris.runtime.pack_download.acquired": "נרכשה בהצלחה {name}.", + "iris.runtime.pack_download.already_installed": "החבילה {key} כבר מותקנת, ההורדה מדולגת.", "iris.runtime.pack_download.validation_failed": "Pack »{pack}\"התאימות הכושל; יצירת העולם והסטודיו לא תסרב. סיבות:", "iris.runtime.pack_download.validation_reason": " - {reason}", "iris.runtime.pack_download.validated_with_warnings": { diff --git a/core/src/main/resources/languages/it_IT.json b/core/src/main/resources/languages/it_IT.json index 6100d6568..c8e69d3e1 100644 --- a/core/src/main/resources/languages/it_IT.json +++ b/core/src/main/resources/languages/it_IT.json @@ -1295,6 +1295,7 @@ "iris.runtime.pack_download.dimension_key_conflict": "Un'altra dimensione nella cartella dei Pack usa già la chiave {key}. Importazione non riuscita!", "iris.runtime.pack_download.pack_key_conflict": "Un altro Pack usa già la chiave {key}. Importazione non riuscita!", "iris.runtime.pack_download.acquired": "{name} acquisito correttamente.", + "iris.runtime.pack_download.already_installed": "Il pack {key} è già installato, download saltato.", "iris.runtime.pack_download.validation_failed": "Il Pack '{pack}' non ha superato la convalida; la creazione di mondi e Studio verrà rifiutata. Motivi:", "iris.runtime.pack_download.validation_reason": " - {reason}", "iris.runtime.pack_download.validated_with_warnings": { diff --git a/core/src/main/resources/languages/ja-JP.json b/core/src/main/resources/languages/ja-JP.json index 2c2347a0f..1673a68f6 100644 --- a/core/src/main/resources/languages/ja-JP.json +++ b/core/src/main/resources/languages/ja-JP.json @@ -1295,6 +1295,7 @@ "iris.runtime.pack_download.dimension_key_conflict": "packs フォルダー内の別のディメンションがキー {key} をすでに使用しています。インポートに失敗しました!", "iris.runtime.pack_download.pack_key_conflict": "別のパックがキー {key} を使用しています。インポートに失敗しました!", "iris.runtime.pack_download.acquired": "{name} を取得しました。", + "iris.runtime.pack_download.already_installed": "パック {key} は既にインストールされているため、ダウンロードをスキップします。", "iris.runtime.pack_download.validation_failed": "パック '{pack}' は検証に失敗しました。ワールドと Studio の作成を拒否します。理由:", "iris.runtime.pack_download.validation_reason": " - {reason}", "iris.runtime.pack_download.validated_with_warnings": { diff --git a/core/src/main/resources/languages/ko_KR.json b/core/src/main/resources/languages/ko_KR.json index 3c0da8a99..469a27108 100644 --- a/core/src/main/resources/languages/ko_KR.json +++ b/core/src/main/resources/languages/ko_KR.json @@ -1295,6 +1295,7 @@ "iris.runtime.pack_download.dimension_key_conflict": "packs 폴더의 다른 차원이 이미 키 {key}을(를) 사용합니다. 가져오기에 실패했습니다!", "iris.runtime.pack_download.pack_key_conflict": "다른 팩이 키 {key}을(를) 사용합니다. 가져오기에 실패했습니다!", "iris.runtime.pack_download.acquired": "성공적으로 취득 {name}.", + "iris.runtime.pack_download.already_installed": "팩 {key}이(가) 이미 설치되어 있어 다운로드를 건너뜁니다.", "iris.runtime.pack_download.validation_failed": "팩 '{pack}' 유효성 검사; 세계 및 스튜디오 생성은 거부됩니다. 이유:", "iris.runtime.pack_download.validation_reason": " - {reason}", "iris.runtime.pack_download.validated_with_warnings": { diff --git a/core/src/main/resources/languages/lt_LT.json b/core/src/main/resources/languages/lt_LT.json index 5f18952a4..4c6fec972 100644 --- a/core/src/main/resources/languages/lt_LT.json +++ b/core/src/main/resources/languages/lt_LT.json @@ -1295,6 +1295,7 @@ "iris.runtime.pack_download.dimension_key_conflict": "Kitas matmuo paketų aplanke jau naudoja raktą {key}. Importuoti nepavyko!", "iris.runtime.pack_download.pack_key_conflict": "Kita pakuotė naudoja raktą {key}. Importuoti nepavyko!", "iris.runtime.pack_download.acquired": "Sėkmingai įgyta {name}.", + "iris.runtime.pack_download.already_installed": "Paketas {key} jau įdiegtas, atsisiuntimas praleidžiamas.", "iris.runtime.pack_download.validation_failed": "Pakuotė \"{pack}\"nepavyko patvirtinimas; pasaulio ir Studio kūrimas bus atsisakyta. Motyvai:", "iris.runtime.pack_download.validation_reason": " - {reason}", "iris.runtime.pack_download.validated_with_warnings": { diff --git a/core/src/main/resources/languages/nl_NL.json b/core/src/main/resources/languages/nl_NL.json index 763da2a6a..3121c13ec 100644 --- a/core/src/main/resources/languages/nl_NL.json +++ b/core/src/main/resources/languages/nl_NL.json @@ -1295,6 +1295,7 @@ "iris.runtime.pack_download.dimension_key_conflict": "Een andere dimensie in de packs-map gebruikt al de sleutel {key}. Importeren mislukt!", "iris.runtime.pack_download.pack_key_conflict": "Een ander pakje gebruikt de sleutel {key}. Importeren mislukt!", "iris.runtime.pack_download.acquired": "Succesvol verworven {name}.", + "iris.runtime.pack_download.already_installed": "Pack {key} is al geïnstalleerd, download wordt overgeslagen.", "iris.runtime.pack_download.validation_failed": "Verpakking{pack}' mislukte validatie; wereld en Studio creatie zal worden geweigerd. Motivering:", "iris.runtime.pack_download.validation_reason": " - {reason}", "iris.runtime.pack_download.validated_with_warnings": { diff --git a/core/src/main/resources/languages/pl_PL.json b/core/src/main/resources/languages/pl_PL.json index b4894434d..9106cf3db 100644 --- a/core/src/main/resources/languages/pl_PL.json +++ b/core/src/main/resources/languages/pl_PL.json @@ -1295,6 +1295,7 @@ "iris.runtime.pack_download.dimension_key_conflict": "Inny wymiar w folderze pakietów już używa klucza {key}. Import nie powiódł się!", "iris.runtime.pack_download.pack_key_conflict": "Inny pakiet używa klucza {key}. Import nie powiódł się!", "iris.runtime.pack_download.acquired": "Udane nabycie {name}.", + "iris.runtime.pack_download.already_installed": "Pakiet {key} jest już zainstalowany, pomijanie pobierania.", "iris.runtime.pack_download.validation_failed": "Paczka \"{pack}\"nieudaną walidację; świat i tworzenie studia zostaną odrzucone. Uzasadnienie:", "iris.runtime.pack_download.validation_reason": " - {reason}", "iris.runtime.pack_download.validated_with_warnings": { diff --git a/core/src/main/resources/languages/pt_PT.json b/core/src/main/resources/languages/pt_PT.json index 4e0d8f386..8afee7d32 100644 --- a/core/src/main/resources/languages/pt_PT.json +++ b/core/src/main/resources/languages/pt_PT.json @@ -1295,6 +1295,7 @@ "iris.runtime.pack_download.dimension_key_conflict": "Outra dimensão na pasta pacotes já está usando a chave {key}. A importação falhou!", "iris.runtime.pack_download.pack_key_conflict": "Outro pacote está usando a chave {key}. A importação falhou!", "iris.runtime.pack_download.acquired": "Adquirido com sucesso {name}.", + "iris.runtime.pack_download.already_installed": "O pack {key} já está instalado, download ignorado.", "iris.runtime.pack_download.validation_failed": "Embalar '{pack}' validação falhada; a criação de mundo e estúdio será recusada. Motivos:", "iris.runtime.pack_download.validation_reason": " - {reason}", "iris.runtime.pack_download.validated_with_warnings": { diff --git a/core/src/main/resources/languages/ru_RU.json b/core/src/main/resources/languages/ru_RU.json index 28eb9f374..e202336ff 100644 --- a/core/src/main/resources/languages/ru_RU.json +++ b/core/src/main/resources/languages/ru_RU.json @@ -1295,6 +1295,7 @@ "iris.runtime.pack_download.dimension_key_conflict": "Еще одно измерение в папке пакетов уже использует ключ {key}. Импорт провалился!", "iris.runtime.pack_download.pack_key_conflict": "Другой пакет использует ключ. {key}. Импорт провалился!", "iris.runtime.pack_download.acquired": "Успешно приобретенный {name}.", + "iris.runtime.pack_download.already_installed": "Пак {key} уже установлен, загрузка пропущена.", "iris.runtime.pack_download.validation_failed": "Пакуй.{pack}Неудачная проверка; мир и создание студии будут отклонены. Причины:", "iris.runtime.pack_download.validation_reason": " - {reason}", "iris.runtime.pack_download.validated_with_warnings": { diff --git a/core/src/main/resources/languages/tr_TR.json b/core/src/main/resources/languages/tr_TR.json index 965d564e1..9fa106f1f 100644 --- a/core/src/main/resources/languages/tr_TR.json +++ b/core/src/main/resources/languages/tr_TR.json @@ -1295,6 +1295,7 @@ "iris.runtime.pack_download.dimension_key_conflict": "Paketlerdeki başka bir boyut zaten anahtarı kullanıyor {key}. İthalat başarısız oldu!", "iris.runtime.pack_download.pack_key_conflict": "Başka bir paket anahtarı kullanıyor {key}. İthalat başarısız oldu!", "iris.runtime.pack_download.acquired": "Başarılı bir şekilde satın alındı {name}.", + "iris.runtime.pack_download.already_installed": "{key} paketi zaten kurulu, indirme atlanıyor.", "iris.runtime.pack_download.validation_failed": "Pack \"{pack}“Başarısız doğrulama; dünya ve Stüdyo yaratımı reddedilecektir. Sebepler:", "iris.runtime.pack_download.validation_reason": " - {reason}", "iris.runtime.pack_download.validated_with_warnings": { diff --git a/core/src/main/resources/languages/vi_VI.json b/core/src/main/resources/languages/vi_VI.json index dd99c765b..69d5d42bd 100644 --- a/core/src/main/resources/languages/vi_VI.json +++ b/core/src/main/resources/languages/vi_VI.json @@ -1295,6 +1295,7 @@ "iris.runtime.pack_download.dimension_key_conflict": "Một chiều không gian khác trong thư mục gói là đã sử dụng phím {key}. Nhập thất bại!", "iris.runtime.pack_download.pack_key_conflict": "Name {key}. Nhập thất bại!", "iris.runtime.pack_download.acquired": "Được thành công {name}.", + "iris.runtime.pack_download.already_installed": "Gói {key} đã được cài đặt, bỏ qua tải xuống.", "iris.runtime.pack_download.validation_failed": "Gói '{pack}'Đã thất bại trong việc xác nhận; thế giới và phòng thu sẽ bị từ chối. Lý do:", "iris.runtime.pack_download.validation_reason": " - {reason}", "iris.runtime.pack_download.validated_with_warnings": { diff --git a/core/src/main/resources/languages/zh_CN.json b/core/src/main/resources/languages/zh_CN.json index 858cb57dc..04ef8d0b6 100644 --- a/core/src/main/resources/languages/zh_CN.json +++ b/core/src/main/resources/languages/zh_CN.json @@ -1295,6 +1295,7 @@ "iris.runtime.pack_download.dimension_key_conflict": "包文件夹中的另一个维度已经使用密钥 {key}. 导入失败 !", "iris.runtime.pack_download.pack_key_conflict": "另一个包是用钥匙 {key}. 导入失败 !", "iris.runtime.pack_download.acquired": "已成功获取 {name}.", + "iris.runtime.pack_download.already_installed": "包 {key} 已安装,跳过下载。", "iris.runtime.pack_download.validation_failed": "包{pack}' 验证失败; 世界和工作室的创建将被拒绝. 原因:", "iris.runtime.pack_download.validation_reason": " - {reason}", "iris.runtime.pack_download.validated_with_warnings": { diff --git a/core/src/main/resources/languages/zh_TW.json b/core/src/main/resources/languages/zh_TW.json index ca7a01ecc..8d1828488 100644 --- a/core/src/main/resources/languages/zh_TW.json +++ b/core/src/main/resources/languages/zh_TW.json @@ -1295,6 +1295,7 @@ "iris.runtime.pack_download.dimension_key_conflict": "包資料夾中的另一個維度已經使用金鑰 {key}. 匯入失敗 !", "iris.runtime.pack_download.pack_key_conflict": "另一個包是用鑰匙 {key}. 匯入失敗 !", "iris.runtime.pack_download.acquired": "已成功獲取 {name}.", + "iris.runtime.pack_download.already_installed": "套件 {key} 已安裝,跳過下載。", "iris.runtime.pack_download.validation_failed": "包{pack}' 驗證失敗; 世界和工作室的建立將被拒絕. 原因:", "iris.runtime.pack_download.validation_reason": " - {reason}", "iris.runtime.pack_download.validated_with_warnings": { diff --git a/core/src/test/java/art/arcane/iris/core/IrisWorldStorageTest.java b/core/src/test/java/art/arcane/iris/core/IrisWorldStorageTest.java index 86d50f788..8ae8eda8d 100644 --- a/core/src/test/java/art/arcane/iris/core/IrisWorldStorageTest.java +++ b/core/src/test/java/art/arcane/iris/core/IrisWorldStorageTest.java @@ -6,6 +6,9 @@ import org.junit.Test; import org.junit.rules.TemporaryFolder; import java.io.File; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.util.Properties; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertThrows; @@ -49,6 +52,37 @@ public class IrisWorldStorageTest { assertEquals("iris_world", IrisWorldStorage.logicalName(new NamespacedKey("iris", "iris_world"), "world")); } + @Test + public void readsLevelNameFromServerPropertiesFile() throws Exception { + File serverProperties = temporaryFolder.newFile("server.properties"); + Files.write(serverProperties.toPath(), "level-name=psycho_world\nmotd=A Minecraft Server\n".getBytes(StandardCharsets.UTF_8)); + + assertEquals("psycho_world", IrisWorldStorage.levelNameFromProperties(serverProperties)); + } + + @Test + public void defaultsLevelNameWhenServerPropertiesMissing() { + File missing = new File(temporaryFolder.getRoot(), "missing/server.properties"); + + assertEquals("world", IrisWorldStorage.levelNameFromProperties(missing)); + } + + @Test + public void defaultsLevelNameWhenPropertyAbsentOrBlank() { + assertEquals("world", IrisWorldStorage.levelNameFromProperties(new Properties())); + + Properties blank = new Properties(); + blank.setProperty("level-name", " "); + assertEquals("world", IrisWorldStorage.levelNameFromProperties(blank)); + } + + @Test + public void trimsLevelNameFromProperties() { + Properties padded = new Properties(); + padded.setProperty("level-name", " main_level "); + assertEquals("main_level", IrisWorldStorage.levelNameFromProperties(padded)); + } + @Test public void rejectsKeysThatEscapeNamespaceStorage() throws Exception { File levelRoot = temporaryFolder.newFolder("world"); diff --git a/core/src/test/java/art/arcane/iris/core/WorldCreatorCompatTest.java b/core/src/test/java/art/arcane/iris/core/WorldCreatorCompatTest.java new file mode 100644 index 000000000..a587c7594 --- /dev/null +++ b/core/src/test/java/art/arcane/iris/core/WorldCreatorCompatTest.java @@ -0,0 +1,40 @@ +package art.arcane.iris.core; + +import org.bukkit.NamespacedKey; +import org.junit.Test; + +import static org.junit.Assert.assertEquals; + +public class WorldCreatorCompatTest { + @Test + public void keyedPathPreservesWorldKey() { + NamespacedKey key = new NamespacedKey("iris", "compat_world"); + + assertEquals(key, WorldCreatorCompat.ofKey(key).key()); + } + + @Test + public void fallbackNameDerivesLogicalNameFromKey() { + assertEquals("compat_world", WorldCreatorCompat.fallbackName(new NamespacedKey("iris", "compat_world"), "world")); + assertEquals("world", WorldCreatorCompat.fallbackName(NamespacedKey.minecraft("overworld"), "world")); + assertEquals("world_nether", WorldCreatorCompat.fallbackName(NamespacedKey.minecraft("the_nether"), "world")); + assertEquals("world_the_end", WorldCreatorCompat.fallbackName(NamespacedKey.minecraft("the_end"), "world")); + } + + @Test + public void fallbackKeyRoundTripsCreatorName() { + assertEquals(new NamespacedKey("iris", "compat_world"), WorldCreatorCompat.fallbackKey("compat_world", "world")); + assertEquals(NamespacedKey.minecraft("overworld"), WorldCreatorCompat.fallbackKey("world", "world")); + assertEquals(NamespacedKey.minecraft("the_nether"), WorldCreatorCompat.fallbackKey("world_nether", "world")); + } + + @Test + public void fallbackMappingIsStableAcrossRoundTrips() { + NamespacedKey key = new NamespacedKey("iris", "iris_world"); + + String name = WorldCreatorCompat.fallbackName(key, "world"); + + assertEquals(key, WorldCreatorCompat.fallbackKey(name, "world")); + assertEquals(name, WorldCreatorCompat.fallbackName(key, "world")); + } +} diff --git a/core/src/test/java/art/arcane/iris/core/pack/PackDownloaderTest.java b/core/src/test/java/art/arcane/iris/core/pack/PackDownloaderTest.java index 79ee99280..94364bfa3 100644 --- a/core/src/test/java/art/arcane/iris/core/pack/PackDownloaderTest.java +++ b/core/src/test/java/art/arcane/iris/core/pack/PackDownloaderTest.java @@ -18,7 +18,15 @@ package art.arcane.iris.core.pack; +import org.junit.Rule; import org.junit.Test; +import org.junit.rules.TemporaryFolder; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.util.ArrayList; +import java.util.List; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; @@ -26,6 +34,8 @@ import static org.junit.Assert.assertThrows; import static org.junit.Assert.assertTrue; public class PackDownloaderTest { + @Rule + public TemporaryFolder temp = new TemporaryFolder(); @Test public void resolvesDefaultOverworldBetaRelease() { assertEquals( @@ -74,6 +84,55 @@ public class PackDownloaderTest { ); } + @Test + public void isPackPresentRequiresNonEmptyFolder() throws IOException { + File packsFolder = temp.newFolder("packs"); + + assertFalse(PackDownloader.isPackPresent(packsFolder, "overworld")); + assertFalse(PackDownloader.isPackPresent(packsFolder, null)); + assertFalse(PackDownloader.isPackPresent(packsFolder, "")); + assertFalse(PackDownloader.isPackPresent(null, "overworld")); + + File pack = new File(packsFolder, "overworld"); + assertTrue(pack.mkdirs()); + assertFalse(PackDownloader.isPackPresent(packsFolder, "overworld")); + + // A partial import (content but no dimension file) counts as absent so it can be replaced. + File biomes = new File(pack, "biomes"); + assertTrue(biomes.mkdirs()); + Files.writeString(new File(biomes, "plains.json").toPath(), "{}"); + assertFalse(PackDownloader.isPackPresent(packsFolder, "overworld")); + + File dimensions = new File(pack, "dimensions"); + assertTrue(dimensions.mkdirs()); + Files.writeString(new File(dimensions, "overworld.json").toPath(), "{}"); + assertTrue(PackDownloader.isPackPresent(packsFolder, "overworld")); + } + + @Test + public void downloadSkipsWhenExpectedPackAlreadyPresent() throws IOException { + File packsFolder = temp.newFolder("packs"); + File dimensions = new File(packsFolder, "overworld/dimensions"); + assertTrue(dimensions.mkdirs()); + Files.writeString(new File(dimensions, "overworld.json").toPath(), "{}"); + + List feedback = new ArrayList<>(); + // The URL is unreachable on purpose: reaching the network would fail the download and + // return null, so a non-null key proves the presence check ran before any fetch. + String key = PackDownloader.download( + packsFolder, + "IrisDimensions/overworld", + "http://127.0.0.1:9/unreachable.zip", + false, + true, + "overworld", + feedback::add + ); + + assertEquals("overworld", key); + assertFalse(feedback.isEmpty()); + } + @Test public void rejectsUnsafeRepositoryAndReference() { assertThrows(IllegalArgumentException.class, () -> PackDownloader.resolveGithubArchiveUrl("IrisDimensions/overworld?raw=1", "master")); diff --git a/core/src/test/java/art/arcane/iris/core/project/VanillaStructurePrefixSchemaTest.java b/core/src/test/java/art/arcane/iris/core/project/VanillaStructurePrefixSchemaTest.java new file mode 100644 index 000000000..3bdf7a242 --- /dev/null +++ b/core/src/test/java/art/arcane/iris/core/project/VanillaStructurePrefixSchemaTest.java @@ -0,0 +1,68 @@ +package art.arcane.iris.core.project; + +import art.arcane.iris.engine.object.IrisImportedStructureControl; +import art.arcane.iris.engine.object.IrisVanillaStructureAdjustment; +import art.arcane.iris.engine.object.annotations.ArrayType; +import art.arcane.iris.engine.object.annotations.Desc; +import art.arcane.iris.engine.object.annotations.RegistryListVanillaStructure; +import art.arcane.volmlib.util.collection.KList; +import art.arcane.volmlib.util.json.JSONArray; +import art.arcane.volmlib.util.json.JSONObject; +import org.junit.Test; + +import java.util.regex.Pattern; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +/** + * importedStructures.disabled and adjustments[].match accept family/namespace PREFIXES + * ("minecraft:village", "nova_structures:") per IrisImportedStructureControl.matchesKey, so the + * generated editor schema must not reject them with a strict registered-key enum. Prefix-capable + * fields emit an anyOf of the registry enum plus a key/prefix pattern; exact-key fields (e.g. + * nativeStructures[].structure) keep the strict enum. + */ +public class VanillaStructurePrefixSchemaTest { + @Test + public void prefixCapableFieldsDeclareThePrefixAnnotation() throws NoSuchFieldException { + assertTrue(IrisImportedStructureControl.class.getDeclaredField("disabled") + .getAnnotation(RegistryListVanillaStructure.class).prefixes()); + assertTrue(IrisVanillaStructureAdjustment.class.getDeclaredField("match") + .getAnnotation(RegistryListVanillaStructure.class).prefixes()); + } + + @Test + public void prefixListSchemaAcceptsFamilyAndNamespacePrefixes() { + JSONObject schema = new SchemaBuilder(PrefixModel.class, null).construct(); + JSONObject items = schema.getJSONObject("properties").getJSONObject("disabled") + .getJSONObject("items"); + String definitionKey = items.getString("$ref").substring("#/definitions/".length()); + JSONArray anyOf = schema.getJSONObject("definitions").getJSONObject(definitionKey) + .getJSONArray("anyOf"); + + String pattern = null; + for (int i = 0; i < anyOf.length(); i++) { + JSONObject branch = anyOf.getJSONObject(i); + if (branch.has("pattern")) { + pattern = branch.getString("pattern"); + } + } + assertTrue("anyOf must contain a pattern branch for prefixes", pattern != null); + + Pattern compiled = Pattern.compile(pattern); + assertTrue(compiled.matcher("minecraft:village").matches()); + assertTrue(compiled.matcher("minecraft:pillager_outpost").matches()); + assertTrue(compiled.matcher("nova_structures:").matches()); + assertTrue(compiled.matcher("towns_and_towers:exclusives/village_piglin").matches()); + assertFalse(compiled.matcher("village").matches()); + assertFalse(compiled.matcher("Nova Structures:tavern").matches()); + } + + @Desc("Schema model for prefix-capable vanilla structure lists.") + public static class PrefixModel { + @RegistryListVanillaStructure(prefixes = true) + @ArrayType(type = String.class, min = 1) + @Desc("Prefix-capable deny list.") + private KList disabled = new KList<>(); + } +} diff --git a/core/src/test/java/art/arcane/iris/core/splash/IrisSplashComposerTest.java b/core/src/test/java/art/arcane/iris/core/splash/IrisSplashComposerTest.java index ff4df1795..2d5dbdb54 100644 --- a/core/src/test/java/art/arcane/iris/core/splash/IrisSplashComposerTest.java +++ b/core/src/test/java/art/arcane/iris/core/splash/IrisSplashComposerTest.java @@ -14,4 +14,14 @@ public class IrisSplashComposerTest { assertEquals(" Version: 4.0.0-26.2", info[2]); assertFalse(String.join("\n", info).contains("RC.1.1.6")); } + + @Test + public void composeInfoShowsWebsiteAndKeepsSplashHeight() { + String[] info = IrisSplashComposer.composeInfo("4.0.0-26.2", "Paper 26.2", IrisSplashComposer.InfoStyle.PLAIN); + + assertEquals(11, info.length); + assertEquals(" By: Volmit Software (Arcane Arts)", info[3]); + assertEquals(" Web: VolmitSoftware.com", info[4]); + assertEquals(" Server: Paper 26.2", info[5]); + } } diff --git a/core/src/test/java/art/arcane/iris/core/structure/VillageImporterFailureLogTest.java b/core/src/test/java/art/arcane/iris/core/structure/VillageImporterFailureLogTest.java new file mode 100644 index 000000000..9f443dcff --- /dev/null +++ b/core/src/test/java/art/arcane/iris/core/structure/VillageImporterFailureLogTest.java @@ -0,0 +1,48 @@ +package art.arcane.iris.core.structure; + +import org.junit.Test; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +/** + * A bulk import walks every registered datapack structure, so one broken server-side assumption + * fails hundreds of times in a row from the same throw site. Printing a full stack trace per + * structure buried a Leaf 26.2 boot under ~4,700 identical lines. The first occurrence of a failure + * signature prints in full; repeats are counted by the per-structure "[fail] key: message" line + * instead of re-printing. + */ +public class VillageImporterFailureLogTest { + /** One throw site, mirroring a repeated in-loop failure (same class, message, and top frame). */ + private static Throwable raise(boolean illegalArgument, String message) { + return illegalArgument ? new IllegalArgumentException(message) : new IllegalStateException(message); + } + + @Test + public void repeatedIdenticalFailuresPrintOnce() { + VillageImporter.resetFailureLogState(); + + assertTrue(VillageImporter.shouldPrintFullTrace( + raise(true, "illegal data type conversion to int"))); + for (int i = 0; i < 200; i++) { + assertFalse(VillageImporter.shouldPrintFullTrace( + raise(true, "illegal data type conversion to int"))); + } + } + + @Test + public void distinctFailuresEachPrintOnce() { + VillageImporter.resetFailureLogState(); + + assertTrue(VillageImporter.shouldPrintFullTrace(raise(true, "a"))); + assertTrue(VillageImporter.shouldPrintFullTrace(raise(false, "a"))); + assertTrue(VillageImporter.shouldPrintFullTrace(raise(true, "b"))); + assertFalse(VillageImporter.shouldPrintFullTrace(raise(true, "b"))); + } + + @Test + public void nullFailureIsNotPrinted() { + VillageImporter.resetFailureLogState(); + assertFalse(VillageImporter.shouldPrintFullTrace(null)); + } +} diff --git a/core/src/test/java/art/arcane/iris/core/structure/VillageImporterMaxDistanceReadTest.java b/core/src/test/java/art/arcane/iris/core/structure/VillageImporterMaxDistanceReadTest.java new file mode 100644 index 000000000..62b6d72ea --- /dev/null +++ b/core/src/test/java/art/arcane/iris/core/structure/VillageImporterMaxDistanceReadTest.java @@ -0,0 +1,44 @@ +package art.arcane.iris.core.structure; + +import org.junit.Test; + +import static org.junit.Assert.assertEquals; + +/** + * JigsawStructure.maxDistanceFromCenter is a plain int on some server builds and a + * JigsawStructure$MaxDistance record ({int horizontal, int vertical}) on others (e.g. Leaf + * 26.2-33). The reflective member reader must handle both — the wrapper shape previously threw + * IllegalArgumentException from Field.getInt, failing the jigsaw import of every datapack + * structure ("Failed to read jigsaw structure graph"). + */ +public class VillageImporterMaxDistanceReadTest { + private static final class IntShape { + private final int maxDistanceFromCenter = 80; + } + + private record MaxDistance(int horizontal, int vertical) { + } + + private static final class WrapperShape { + private final MaxDistance maxDistanceFromCenter = new MaxDistance(96, 48); + } + + private static final class BoxedShape { + private final Integer maxDistanceFromCenter = 64; + } + + @Test + public void readsPlainIntField() throws Exception { + assertEquals(80, VillageImporter.readIntMember(new IntShape(), "maxDistanceFromCenter")); + } + + @Test + public void readsLargestIntComponentFromWrapperRecord() throws Exception { + assertEquals(96, VillageImporter.readIntMember(new WrapperShape(), "maxDistanceFromCenter")); + } + + @Test + public void readsBoxedNumberField() throws Exception { + assertEquals(64, VillageImporter.readIntMember(new BoxedShape(), "maxDistanceFromCenter")); + } +} diff --git a/core/src/test/java/art/arcane/iris/engine/framework/NativeStructureGenerationPolicyTest.java b/core/src/test/java/art/arcane/iris/engine/framework/NativeStructureGenerationPolicyTest.java index 21e23b0be..ed38f9ba2 100644 --- a/core/src/test/java/art/arcane/iris/engine/framework/NativeStructureGenerationPolicyTest.java +++ b/core/src/test/java/art/arcane/iris/engine/framework/NativeStructureGenerationPolicyTest.java @@ -1,9 +1,20 @@ package art.arcane.iris.engine.framework; +import art.arcane.iris.core.loader.IrisData; +import art.arcane.iris.engine.object.IrisDimension; +import art.arcane.iris.engine.object.IrisImportedStructureControl; +import art.arcane.iris.engine.object.IrisNativeStructure; +import art.arcane.iris.engine.object.IrisNativeStructureDecision; +import art.arcane.iris.engine.object.IrisRegion; +import art.arcane.iris.engine.object.IrisStructurePlacement; import art.arcane.iris.engine.object.NativeStructureGenerationStatus; +import art.arcane.volmlib.util.collection.KList; import org.junit.Test; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; public class NativeStructureGenerationPolicyTest { @Test @@ -17,4 +28,53 @@ public class NativeStructureGenerationPolicyTest { NativeStructureGenerationPolicy.generationStatusMessage( "minecraft:ancient_city", NativeStructureGenerationStatus.REPLACED_BY_IRIS)); } + + /** + * The "blanket-disable a namespace, re-place chosen structures via nativeStructures placements" + * pattern: generation honors the placement (the planner never consults the disable list), so the + * policy must report those keys as REPLACED_BY_IRIS — making /iris find|goto and + * /iris structure verify locate the placement instead of claiming the key is disabled. + */ + @Test + public void disabledKeyWithActivePlacementResolvesAsReplacedByIris() { + Engine engine = engineWithDisabledNamespaceAndRegionPlacement("nova_structures:tavern_oak"); + + assertEquals(NativeStructureGenerationStatus.REPLACED_BY_IRIS, + NativeStructureGenerationPolicy.resolve(engine, "nova_structures:tavern_oak", false).status()); + } + + @Test + public void disabledKeyWithoutPlacementStaysDisabled() { + Engine engine = engineWithDisabledNamespaceAndRegionPlacement("nova_structures:tavern_oak"); + + IrisNativeStructureDecision decision = + NativeStructureGenerationPolicy.resolve(engine, "nova_structures:witch_villa", false); + assertEquals(NativeStructureGenerationStatus.DISABLED_BY_PACK, decision.status()); + assertFalse(decision.generate()); + } + + private Engine engineWithDisabledNamespaceAndRegionPlacement(String placedKey) { + IrisData data = mock(IrisData.class); + Engine engine = mock(Engine.class); + IrisDimension dimension = mock(IrisDimension.class); + IrisImportedStructureControl control = new IrisImportedStructureControl(); + control.getDisabled().add("nova_structures:"); + + IrisStructurePlacement placement = new IrisStructurePlacement(); + placement.getNativeStructures().add(new IrisNativeStructure().setStructure(placedKey)); + IrisRegion region = mock(IrisRegion.class); + KList regionPlacements = new KList<>(); + regionPlacements.add(placement); + when(region.getStructures()).thenReturn(regionPlacements); + KList regions = new KList<>(); + regions.add(region); + + when(engine.getData()).thenReturn(data); + when(engine.getDimension()).thenReturn(dimension); + when(dimension.getImportedStructures()).thenReturn(control); + when(dimension.getStructures()).thenReturn(new KList<>()); + when(dimension.getAllRegions(engine)).thenReturn(regions); + when(dimension.getReachableBiomes(engine)).thenReturn(new KList<>()); + return engine; + } }