This commit is contained in:
Brian Neumann-Fopiano
2026-08-04 11:13:16 -06:00
parent 10b2363226
commit aaccbacf32
66 changed files with 930 additions and 557 deletions
+3 -3
View File
@@ -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
@@ -53,15 +53,19 @@ final class VanillaStructureBiomes {
if (structure == null) {
throw new IllegalArgumentException("Registered structure does not exist: " + structureKey);
}
boolean hasFilterEntries = false;
for (Holder<Biome> holder : structure.biomes()) {
hasFilterEntries = true;
Optional<ResourceKey<Biome>> 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;
}
@@ -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"));
}
}
@@ -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/<key> 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);
@@ -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);
@@ -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);
@@ -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<Structure> structureRegistry = RegistryAccess.registryAccess().getRegistry(RegistryKey.STRUCTURE);
Registry<Structure> 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)) {
@@ -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<String> structureKeys = new KList<>();
Registry<Structure> structureRegistry = RegistryAccess.registryAccess().getRegistry(RegistryKey.STRUCTURE);
for (Structure structure : structureRegistry) {
NamespacedKey key = structureRegistry.getKey(structure);
if (key != null) {
structureKeys.add(key.toString());
Registry<Structure> 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();
@@ -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,
@@ -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<String> tabCompleteRoot(CommandSender sender, String alias, String[] args) {
public List<String> tabCompleteRoot(CommandSender sender, String alias, String[] args) {
List<String> 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()
);
@@ -25,6 +25,7 @@ final class FellingRun {
volatile int effectStride = 1;
volatile ItemStack expectedTool;
volatile List<TreeMember> work = List.of();
volatile String abortReason;
FellingRun(
TreeClaim claim,
@@ -67,6 +67,9 @@ final class TreeFellingRunner {
)
);
List<TreeMarkerTraversal.Position> 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<TreeMember> 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);
}
}
@@ -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:
@@ -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:
@@ -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
@@ -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<String> tabCompleteRoot(CommandSender sender, String alias, String[] args) {
public List<String> tabCompleteRoot(CommandSender sender, String alias, String[] args) {
return List.of("first", "second");
}
}
@@ -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));
}
}
-488
View File
@@ -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.<init>(JSONObject.java:145)
at art.arcane.volmlib.util.json.JSONTokener.nextValue(JSONTokener.java:345)
at art.arcane.volmlib.util.json.JSONArray.<init>(JSONArray.java:111)
at art.arcane.volmlib.util.json.JSONTokener.nextValue(JSONTokener.java:348)
at art.arcane.volmlib.util.json.JSONObject.<init>(JSONObject.java:159)
at art.arcane.volmlib.util.json.JSONObject.<init>(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
@@ -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
@@ -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<Biome> holder : structure.biomes()) {
hasFilterEntries = true;
Optional<ResourceKey<Biome>> 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 '"
+4
View File
@@ -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)
}
}
}
@@ -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 &lt;dimension&gt; 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;
@@ -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) {
@@ -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);
@@ -363,6 +363,7 @@ public class ServerConfigurator {
File[] packs = IrisPlatforms.get().dataFolder("packs").listFiles(File::isDirectory);
Stream<File> 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;
@@ -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);
}
}
@@ -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 <dimension> 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 <dimension>, 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();
@@ -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)
@@ -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,
@@ -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<String, Object> 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<String> 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<String> 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<String> 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<String> 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<String> 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/<key> 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);
@@ -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<String, JSONObject> 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<JSONArray> 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<JSONArray> 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<Vanilla Structure>";
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<Vanilla Structure Set>";
putRegistryEnumItems(prop, "enum-vanilla-structure-set", this::vanillaStructureSets);
@@ -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;
@@ -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),
"",
"",
"",
""
};
}
@@ -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);
@@ -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<String> PRINTED_FAILURE_SIGNATURES = ConcurrentHashMap.newKeySet();
public record Result(boolean success, String message, int pools, int pieces, List<StructureLoss> 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) {
@@ -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)");
@@ -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);
@@ -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)) {
@@ -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<String> disabled = new KList<>();
@@ -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<String> match = new KList<>();
@@ -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;
}
@@ -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": {
@@ -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": {
@@ -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": {
@@ -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": {
@@ -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": {
@@ -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": {
@@ -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": {
@@ -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": {
@@ -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": {
@@ -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": {
@@ -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": {
@@ -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": {
@@ -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": {
@@ -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": {
@@ -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": {
@@ -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": {
@@ -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": {
@@ -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");
@@ -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"));
}
}
@@ -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<String> 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"));
@@ -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<String> disabled = new KList<>();
}
}
@@ -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]);
}
}
@@ -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));
}
}
@@ -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"));
}
}
@@ -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<IrisStructurePlacement> regionPlacements = new KList<>();
regionPlacements.add(placement);
when(region.getStructures()).thenReturn(regionPlacements);
KList<IrisRegion> 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;
}
}