This commit is contained in:
Brian Neumann-Fopiano
2026-08-10 17:32:44 -04:00
parent 998a5c9f5f
commit dfae45663a
56 changed files with 2194 additions and 191 deletions
@@ -52,6 +52,7 @@ import art.arcane.iris.engine.object.IrisPosition;
import art.arcane.iris.engine.object.IrisStructure; import art.arcane.iris.engine.object.IrisStructure;
import art.arcane.iris.platform.bukkit.BukkitPlatform; import art.arcane.iris.platform.bukkit.BukkitPlatform;
import art.arcane.iris.util.common.director.DirectorExecutor; import art.arcane.iris.util.common.director.DirectorExecutor;
import art.arcane.iris.util.common.director.specialhandlers.IrisStructureHandler;
import art.arcane.iris.util.common.format.C; import art.arcane.iris.util.common.format.C;
import art.arcane.iris.util.common.plugin.VolmitSender; import art.arcane.iris.util.common.plugin.VolmitSender;
import art.arcane.iris.util.common.scheduling.J; import art.arcane.iris.util.common.scheduling.J;
@@ -118,9 +119,9 @@ public class CommandJigsaw implements DirectorExecutor {
customHandler = JigsawModeHandler.class) String mode, customHandler = JigsawModeHandler.class) String mode,
@Param(description = "iris or vanilla", defaultValue = "iris", @Param(description = "iris or vanilla", defaultValue = "iris",
customHandler = JigsawCompatibilityHandler.class) String compatibility, customHandler = JigsawCompatibilityHandler.class) String compatibility,
@Param(description = "Cell width", defaultValue = "16") int width, @Param(description = "Cell width", defaultValue = "15") int width,
@Param(description = "Cell height", defaultValue = "16") int height, @Param(description = "Cell height", defaultValue = "15") int height,
@Param(description = "Cell depth", defaultValue = "16") int depth, @Param(description = "Cell depth", defaultValue = "15") int depth,
@Param(description = "Studio world seed", defaultValue = "1337") long seed @Param(description = "Studio world seed", defaultValue = "1337") long seed
) { ) {
IrisData data = requireData(dimension); IrisData data = requireData(dimension);
@@ -157,6 +158,16 @@ public class CommandJigsaw implements DirectorExecutor {
} }
return; return;
} }
try {
JigsawStudioService.clearAutosaveHistory(
data.getDataFolder().toPath(),
structure);
} catch (IOException exception) {
Iris.reportError("Failed to clear stale Jigsaw Studio history for '" + structure + "'.", exception);
sendError("Jigsaw project was created, but stale autosave history could not be cleared: "
+ exception.getMessage());
return;
}
data.invalidateStructureResources(); data.invalidateStructureResources();
sender().sendMessage(C.GREEN + "Created Jigsaw project '" + structure + "' atomically."); sender().sendMessage(C.GREEN + "Created Jigsaw project '" + structure + "' atomically.");
open(dimension, structure, seed); open(dimension, structure, seed);
@@ -208,7 +219,8 @@ public class CommandJigsaw implements DirectorExecutor {
public void open( public void open(
@Param(description = "Pack dimension") IrisDimension dimension, @Param(description = "Pack dimension") IrisDimension dimension,
@Param(name = "key", aliases = {"structure", "name"}, @Param(name = "key", aliases = {"structure", "name"},
description = "Existing key loaded from structures/<key>.json") String structure, description = "Existing key loaded from structures/<key>.json",
customHandler = IrisStructureHandler.class) String structure,
@Param(description = "Studio world seed", defaultValue = "1337") long seed @Param(description = "Studio world seed", defaultValue = "1337") long seed
) { ) {
openProject(player(), sender(), dimension, structure, seed); openProject(player(), sender(), dimension, structure, seed);
@@ -202,7 +202,7 @@ public class CommandObject implements DirectorExecutor {
//Prevent blocks being set in or bellow bedrock //Prevent blocks being set in or bellow bedrock
if (y <= world.getMinHeight() || block.getType() == Material.BEDROCK) return; if (y <= world.getMinHeight() || block.getType() == Material.BEDROCK) return;
futureBlockChanges.put(block, block.getBlockData()); futureBlockChanges.putIfAbsent(block, block.getBlockData());
if (d instanceof IrisCustomData data) { if (d instanceof IrisCustomData data) {
block.setBlockData(data.getBase(), false); block.setBlockData(data.getBase(), false);
@@ -369,7 +369,24 @@ public class CommandStructure implements DirectorExecutor {
} }
p.getObject().place(p.getX(), p.getY(), p.getZ(), placer, config, rng, null, null, data); p.getObject().place(p.getX(), p.getY(), p.getZ(), placer, config, rng, null, null, data);
} }
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_STRUCTURE_PLACED_PIECES_AT_YOUR_LOCATION, MessageArgument.untrusted("structure", structure), MessageArgument.untrusted("value", pieces.size()))); int blockChanges = 0;
for (Map.Entry<Block, BlockData> entry : future.entrySet()) {
if (!entry.getKey().getBlockData().equals(entry.getValue())) {
blockChanges++;
}
}
if (blockChanges == 0) {
sender().sendMessage(IrisLanguage.text(
BukkitCommandMessagesExtended.COMMAND_STRUCTURE_PLACEMENT_CHANGED_NO_BLOCKS,
MessageArgument.untrusted("structure", structure),
MessageArgument.untrusted("value", pieces.size())));
return;
}
sender().sendMessage(IrisLanguage.text(
BukkitCommandMessagesExtended.COMMAND_STRUCTURE_PLACED_PIECES_AT_YOUR_LOCATION,
MessageArgument.untrusted("structure", structure),
MessageArgument.untrusted("value", pieces.size()),
MessageArgument.untrusted("value2", blockChanges)));
} }
} }
@@ -24,6 +24,7 @@ import art.arcane.iris.engine.object.IrisJigsawConnector;
import art.arcane.iris.engine.object.IrisJigsawPiece; import art.arcane.iris.engine.object.IrisJigsawPiece;
import art.arcane.iris.engine.object.IrisObject; import art.arcane.iris.engine.object.IrisObject;
import art.arcane.iris.engine.object.IrisPosition; import art.arcane.iris.engine.object.IrisPosition;
import art.arcane.iris.util.common.director.specialhandlers.IrisStructureHandler;
import art.arcane.volmlib.util.director.DirectorOrigin; import art.arcane.volmlib.util.director.DirectorOrigin;
import art.arcane.volmlib.util.director.annotations.Director; import art.arcane.volmlib.util.director.annotations.Director;
import art.arcane.volmlib.util.director.annotations.Param; import art.arcane.volmlib.util.director.annotations.Param;
@@ -219,9 +220,9 @@ public class CommandJigsawContractTest {
assertParameter(parameters[2], "planar", CommandJigsaw.JigsawModeHandler.class); assertParameter(parameters[2], "planar", CommandJigsaw.JigsawModeHandler.class);
assertParameter(parameters[3], "iris", CommandJigsaw.JigsawCompatibilityHandler.class); assertParameter(parameters[3], "iris", CommandJigsaw.JigsawCompatibilityHandler.class);
assertParameter(parameters[4], "16", null); assertParameter(parameters[4], "15", null);
assertParameter(parameters[5], "16", null); assertParameter(parameters[5], "15", null);
assertParameter(parameters[6], "16", null); assertParameter(parameters[6], "15", null);
assertParameter(parameters[7], "1337", null); assertParameter(parameters[7], "1337", null);
} }
@@ -249,6 +250,7 @@ public class CommandJigsawContractTest {
assertEquals("key", openKey.name()); assertEquals("key", openKey.name());
assertEquals(List.of("structure", "name"), List.of(openKey.aliases())); assertEquals(List.of("structure", "name"), List.of(openKey.aliases()));
assertEquals("Existing key loaded from structures/<key>.json", openKey.description()); assertEquals("Existing key loaded from structures/<key>.json", openKey.description());
assertEquals(IrisStructureHandler.class, openKey.customHandler());
assertEquals(List.of("edit", "reopen"), List.of(openCommand.aliases())); assertEquals(List.of("edit", "reopen"), List.of(openCommand.aliases()));
} }
@@ -267,6 +267,11 @@ public class IrisStructureLocateCommandContractTest {
assertTrue(method.contains("IrisData data = dimension.getLoader()")); assertTrue(method.contains("IrisData data = dimension.getLoader()"));
assertTrue(method.contains("PlatformChunkGenerator targetGenerator = IrisToolbelt.access(targetWorld)")); assertTrue(method.contains("PlatformChunkGenerator targetGenerator = IrisToolbelt.access(targetWorld)"));
assertTrue(method.contains("CommandObject.createPlacer(targetWorld, future, targetEngine)")); assertTrue(method.contains("CommandObject.createPlacer(targetWorld, future, targetEngine)"));
assertTrue(method.contains("if (blockChanges == 0)"));
assertTrue(method.contains("COMMAND_STRUCTURE_PLACEMENT_CHANGED_NO_BLOCKS"));
Path structureSource = Path.of(System.getProperty("iris.commandStructureSource"));
String objectSource = Files.readString(structureSource.resolveSibling("CommandObject.java"));
assertTrue(objectSource.contains("futureBlockChanges.putIfAbsent(block, block.getBlockData())"));
assertFalse(method.contains("data.getEngine()")); assertFalse(method.contains("data.getEngine()"));
} }
+24 -24
View File
@@ -1,12 +1,12 @@
[12:40:12] [Test worker/WARN]: Iris cannot resolve the biome id for 'minecraft:plains' before the Minecraft server is available; using biome id 0 [16:35:58] [Test worker/WARN]: Iris cannot resolve the biome id for 'minecraft:plains' before the Minecraft server is available; using biome id 0
[12:40:12] [Test worker/WARN]: Iris cannot enumerate the biome registry before the Minecraft server is available; returning no biomes [16:35:58] [Test worker/WARN]: Iris cannot enumerate the biome registry before the Minecraft server is available; returning no biomes
[12:40:12] [Test worker/INFO]: [STDERR]: [Iris/WARN] Can't find block data for minecraft:definitely_not_a_real_block [16:35:58] [Test worker/INFO]: [STDERR]: [Iris/WARN] Can't find block data for minecraft:definitely_not_a_real_block
[12:40:12] [Test worker/INFO]: [STDERR]: [Iris/WARN] Can't find block data for minecraft:minecraft:definitely_not_a_real_block [16:35:58] [Test worker/INFO]: [STDERR]: [Iris/WARN] Can't find block data for minecraft:minecraft:definitely_not_a_real_block
[12:40:12] [Test worker/INFO]: [STDERR]: [Iris/WARN] Block 'minecraft:oak_log' rejected state 'not_a_property=x'; using its default state [16:35:58] [Test worker/INFO]: [STDERR]: [Iris/WARN] Block 'minecraft:oak_log' rejected state 'not_a_property=x'; using its default state
[12:40:12] [Test worker/INFO]: Iris registered custom content provider 'iris_deferred_test' [16:35:58] [Test worker/INFO]: Iris registered custom content provider 'iris_deferred_test'
[12:40:12] [Test worker/WARN]: Iris persistent dimension registry entry 1 in /var/folders/l1/g6lr3c5x26z_mdmtfzt9gjbr0000gn/T/iris-dimension-registry-partial16265972129816775503/iris-dimensions.json is invalid (entry 1 in /var/folders/l1/g6lr3c5x26z_mdmtfzt9gjbr0000gn/T/iris-dimension-registry-partial16265972129816775503/iris-dimensions.json has no dimension); kept verbatim: {"id":"iris:broken","pack":"overworld"} [16:35:58] [Test worker/WARN]: Iris persistent dimension registry entry 1 in /var/folders/l1/g6lr3c5x26z_mdmtfzt9gjbr0000gn/T/iris-dimension-registry-partial11660810095261932969/iris-dimensions.json is invalid (entry 1 in /var/folders/l1/g6lr3c5x26z_mdmtfzt9gjbr0000gn/T/iris-dimension-registry-partial11660810095261932969/iris-dimensions.json has no dimension); kept verbatim: {"id":"iris:broken","pack":"overworld"}
[12:40:12] [Test worker/ERROR]: Iris persistent dimension registry at /var/folders/l1/g6lr3c5x26z_mdmtfzt9gjbr0000gn/T/iris-dimension-registry-corrupt-boot11106486503678824408/iris-dimensions.json is corrupt; quarantining it and continuing boot [16:35:58] [Test worker/ERROR]: Iris persistent dimension registry at /var/folders/l1/g6lr3c5x26z_mdmtfzt9gjbr0000gn/T/iris-dimension-registry-corrupt-boot5812784634372180772/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-boot11106486503678824408/iris-dimensions.json could not be read; refusing to discard persistent worlds java.lang.IllegalStateException: Iris persistent dimension registry at /var/folders/l1/g6lr3c5x26z_mdmtfzt9gjbr0000gn/T/iris-dimension-registry-corrupt-boot5812784634372180772/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.contents(ModdedDimensionRegistryStore.java:150)
at art.arcane.iris.modded.ModdedDimensionRegistryStore.load(ModdedDimensionRegistryStore.java:56) 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.ModdedDimensionRegistryStore.loadForStartup(ModdedDimensionRegistryStore.java:69)
@@ -63,9 +63,9 @@ Caused by: art.arcane.volmlib.util.json.JSONException: A JSONObject text must en
at art.arcane.volmlib.util.json.JSONObject.<init>(JSONObject.java:260) at art.arcane.volmlib.util.json.JSONObject.<init>(JSONObject.java:260)
at art.arcane.iris.modded.ModdedDimensionRegistryStore.contents(ModdedDimensionRegistryStore.java:117) at art.arcane.iris.modded.ModdedDimensionRegistryStore.contents(ModdedDimensionRegistryStore.java:117)
... 45 more ... 45 more
[12:40:12] [Test worker/ERROR]: Iris lost 1 persistent dimension(s) from the corrupt registry: iris:lost [16:35:58] [Test worker/ERROR]: Iris lost 1 persistent dimension(s) from the corrupt registry: iris:lost
[12:40:12] [Test worker/ERROR]: Iris moved the corrupt persistent dimension registry to /var/folders/l1/g6lr3c5x26z_mdmtfzt9gjbr0000gn/T/iris-dimension-registry-corrupt-boot11106486503678824408/iris-dimensions.json.broken-1786380012686 [16:35:58] [Test worker/ERROR]: Iris moved the corrupt persistent dimension registry to /var/folders/l1/g6lr3c5x26z_mdmtfzt9gjbr0000gn/T/iris-dimension-registry-corrupt-boot5812784634372180772/iris-dimensions.json.broken-1786394158359
[12:40:12] [Test worker/ERROR]: Iris service onDisable failed for art.arcane.iris.modded.ModdedServiceManagerTest$SecondService [16:35:58] [Test worker/ERROR]: Iris service onDisable failed for art.arcane.iris.modded.ModdedServiceManagerTest$SecondService
java.lang.RuntimeException: second disable failed java.lang.RuntimeException: second disable failed
at art.arcane.iris.modded.ModdedServiceManagerTest.disableAttemptsEveryServiceInReverseOrderAndNeverRethrows(ModdedServiceManagerTest.java:55) at art.arcane.iris.modded.ModdedServiceManagerTest.disableAttemptsEveryServiceInReverseOrderAndNeverRethrows(ModdedServiceManagerTest.java:55)
at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:104) at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:104)
@@ -110,7 +110,7 @@ java.lang.RuntimeException: second disable failed
at org.gradle.process.internal.worker.child.SystemApplicationClassLoaderWorker.call(SystemApplicationClassLoaderWorker.java:72) 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.run(GradleWorkerMain.java:69)
at worker.org.gradle.process.internal.worker.GradleWorkerMain.main(GradleWorkerMain.java:74) at worker.org.gradle.process.internal.worker.GradleWorkerMain.main(GradleWorkerMain.java:74)
[12:40:12] [Test worker/ERROR]: Iris service onDisable failed for art.arcane.iris.modded.ModdedServiceManagerTest$FirstService [16:35:58] [Test worker/ERROR]: Iris service onDisable failed for art.arcane.iris.modded.ModdedServiceManagerTest$FirstService
java.lang.RuntimeException: first disable failed java.lang.RuntimeException: first disable failed
at art.arcane.iris.modded.ModdedServiceManagerTest.disableAttemptsEveryServiceInReverseOrderAndNeverRethrows(ModdedServiceManagerTest.java:54) at art.arcane.iris.modded.ModdedServiceManagerTest.disableAttemptsEveryServiceInReverseOrderAndNeverRethrows(ModdedServiceManagerTest.java:54)
at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:104) at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:104)
@@ -155,7 +155,7 @@ java.lang.RuntimeException: first disable failed
at org.gradle.process.internal.worker.child.SystemApplicationClassLoaderWorker.call(SystemApplicationClassLoaderWorker.java:72) 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.run(GradleWorkerMain.java:69)
at worker.org.gradle.process.internal.worker.GradleWorkerMain.main(GradleWorkerMain.java:74) at worker.org.gradle.process.internal.worker.GradleWorkerMain.main(GradleWorkerMain.java:74)
[12:40:12] [Test worker/ERROR]: Iris disabled all services with 2 failure(s) [16:35:58] [Test worker/ERROR]: Iris disabled all services with 2 failure(s)
java.lang.RuntimeException: second disable failed java.lang.RuntimeException: second disable failed
at art.arcane.iris.modded.ModdedServiceManagerTest.disableAttemptsEveryServiceInReverseOrderAndNeverRethrows(ModdedServiceManagerTest.java:55) at art.arcane.iris.modded.ModdedServiceManagerTest.disableAttemptsEveryServiceInReverseOrderAndNeverRethrows(ModdedServiceManagerTest.java:55)
at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:104) at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:104)
@@ -203,7 +203,7 @@ java.lang.RuntimeException: second disable failed
Suppressed: java.lang.RuntimeException: first disable failed Suppressed: java.lang.RuntimeException: first disable failed
at art.arcane.iris.modded.ModdedServiceManagerTest.disableAttemptsEveryServiceInReverseOrderAndNeverRethrows(ModdedServiceManagerTest.java:54) at art.arcane.iris.modded.ModdedServiceManagerTest.disableAttemptsEveryServiceInReverseOrderAndNeverRethrows(ModdedServiceManagerTest.java:54)
... 42 more ... 42 more
[12:40:12] [Test worker/ERROR]: Iris service rollback failed for art.arcane.iris.modded.ModdedServiceManagerTest$SecondService [16:35:58] [Test worker/ERROR]: Iris service rollback failed for art.arcane.iris.modded.ModdedServiceManagerTest$SecondService
java.lang.RuntimeException: cleanup failed java.lang.RuntimeException: cleanup failed
at art.arcane.iris.modded.ModdedServiceManagerTest.rollsBackEnabledServicesAndPreservesCleanupFailures(ModdedServiceManagerTest.java:32) at art.arcane.iris.modded.ModdedServiceManagerTest.rollsBackEnabledServicesAndPreservesCleanupFailures(ModdedServiceManagerTest.java:32)
at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:104) at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:104)
@@ -248,7 +248,7 @@ java.lang.RuntimeException: cleanup failed
at org.gradle.process.internal.worker.child.SystemApplicationClassLoaderWorker.call(SystemApplicationClassLoaderWorker.java:72) 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.run(GradleWorkerMain.java:69)
at worker.org.gradle.process.internal.worker.GradleWorkerMain.main(GradleWorkerMain.java:74) at worker.org.gradle.process.internal.worker.GradleWorkerMain.main(GradleWorkerMain.java:74)
[12:40:12] [Test worker/ERROR]: Iris service onEnable failed for art.arcane.iris.modded.ModdedServiceManagerTest$SecondService [16:35:58] [Test worker/ERROR]: Iris service onEnable failed for art.arcane.iris.modded.ModdedServiceManagerTest$SecondService
java.lang.RuntimeException: enable failed java.lang.RuntimeException: enable failed
at art.arcane.iris.modded.ModdedServiceManagerTest.rollsBackEnabledServicesAndPreservesCleanupFailures(ModdedServiceManagerTest.java:31) at art.arcane.iris.modded.ModdedServiceManagerTest.rollsBackEnabledServicesAndPreservesCleanupFailures(ModdedServiceManagerTest.java:31)
at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:104) at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:104)
@@ -296,7 +296,7 @@ java.lang.RuntimeException: enable failed
Suppressed: java.lang.RuntimeException: cleanup failed Suppressed: java.lang.RuntimeException: cleanup failed
at art.arcane.iris.modded.ModdedServiceManagerTest.rollsBackEnabledServicesAndPreservesCleanupFailures(ModdedServiceManagerTest.java:32) at art.arcane.iris.modded.ModdedServiceManagerTest.rollsBackEnabledServicesAndPreservesCleanupFailures(ModdedServiceManagerTest.java:32)
... 42 more ... 42 more
[12:40:12] [Test worker/ERROR]: [worldcheck] server stop request failed [16:35:58] [Test worker/ERROR]: [worldcheck] server stop request failed
java.lang.IllegalStateException: 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.ModdedWorldCheckTest.lambda$stopRequestFailureForcesNonzeroResult$1(ModdedWorldCheckTest.java:238)
at art.arcane.iris.modded.ModdedWorldCheck.runAndRequestStop(ModdedWorldCheck.java:144) at art.arcane.iris.modded.ModdedWorldCheck.runAndRequestStop(ModdedWorldCheck.java:144)
@@ -343,7 +343,7 @@ java.lang.IllegalStateException: stop request failed
at org.gradle.process.internal.worker.child.SystemApplicationClassLoaderWorker.call(SystemApplicationClassLoaderWorker.java:72) 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.run(GradleWorkerMain.java:69)
at worker.org.gradle.process.internal.worker.GradleWorkerMain.main(GradleWorkerMain.java:74) at worker.org.gradle.process.internal.worker.GradleWorkerMain.main(GradleWorkerMain.java:74)
[12:40:12] [Test worker/ERROR]: [worldcheck] waiting for server shutdown failed [16:35:58] [Test worker/ERROR]: [worldcheck] waiting for server shutdown failed
java.lang.IllegalStateException: shutdown wait failed java.lang.IllegalStateException: shutdown wait failed
at art.arcane.iris.modded.ModdedWorldCheckTest.lambda$shutdownWaitFailureForcesNonzeroExit$0(ModdedWorldCheckTest.java:264) 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.ModdedWorldCheck.awaitStopAndExit(ModdedWorldCheck.java:159)
@@ -390,7 +390,7 @@ java.lang.IllegalStateException: shutdown wait failed
at org.gradle.process.internal.worker.child.SystemApplicationClassLoaderWorker.call(SystemApplicationClassLoaderWorker.java:72) 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.run(GradleWorkerMain.java:69)
at worker.org.gradle.process.internal.worker.GradleWorkerMain.main(GradleWorkerMain.java:74) at worker.org.gradle.process.internal.worker.GradleWorkerMain.main(GradleWorkerMain.java:74)
[12:40:12] [Test worker/ERROR]: [worldcheck] check failed [16:35:58] [Test worker/ERROR]: [worldcheck] check failed
java.lang.IllegalStateException: check failed java.lang.IllegalStateException: check failed
at art.arcane.iris.modded.ModdedWorldCheckTest.lambda$thrownCheckStillRequestsStop$0(ModdedWorldCheckTest.java:224) 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.ModdedWorldCheck.runAndRequestStop(ModdedWorldCheck.java:139)
@@ -437,8 +437,8 @@ java.lang.IllegalStateException: check failed
at org.gradle.process.internal.worker.child.SystemApplicationClassLoaderWorker.call(SystemApplicationClassLoaderWorker.java:72) 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.run(GradleWorkerMain.java:69)
at worker.org.gradle.process.internal.worker.GradleWorkerMain.main(GradleWorkerMain.java:74) at worker.org.gradle.process.internal.worker.GradleWorkerMain.main(GradleWorkerMain.java:74)
[12:40:12] [Test worker/INFO]: Iris registered custom content provider 'iris_discovery_success' [16:35:58] [Test worker/INFO]: Iris registered custom content provider 'iris_discovery_success'
[12:40:12] [Test worker/WARN]: Iris custom content provider discovery failed at provider 'iris_discovery_failure' (art.arcane.iris.modded.api.ModdedCustomContentRegistryTest$TestProvider) [16:35:58] [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 java.lang.RuntimeException: provider init failed
at art.arcane.iris.modded.api.ModdedCustomContentRegistryTest.failedDiscoveryPublishesNothingAndPreservesTheCause(ModdedCustomContentRegistryTest.java:39) 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/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:104)
@@ -483,6 +483,6 @@ java.lang.RuntimeException: provider init failed
at org.gradle.process.internal.worker.child.SystemApplicationClassLoaderWorker.call(SystemApplicationClassLoaderWorker.java:72) 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.run(GradleWorkerMain.java:69)
at worker.org.gradle.process.internal.worker.GradleWorkerMain.main(GradleWorkerMain.java:74) at worker.org.gradle.process.internal.worker.GradleWorkerMain.main(GradleWorkerMain.java:74)
[12:40:12] [Test worker/INFO]: Iris object undo service ready (bounded to 32 paste(s) per player) [16:35:58] [Test worker/INFO]: Iris object undo service ready (bounded to 32 paste(s) per player)
[12:40:12] [Test worker/INFO]: [STDOUT]: [Iris/INFO] Iris /iris command tree registered [16:35:58] [Test worker/INFO]: [STDOUT]: [Iris/INFO] Iris /iris command tree registered
[12:40:12] [Test worker/INFO]: [STDOUT]: [Iris/INFO] Iris /iris command tree registered [16:35:58] [Test worker/INFO]: [STDOUT]: [Iris/INFO] Iris /iris command tree registered
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+22 -22
View File
@@ -1,12 +1,12 @@
[10Aug2026 12:40:42.241] [Test worker/DEBUG] [io.netty.util.internal.logging.InternalLoggerFactory/]: Using SLF4J as the default logging framework [10Aug2026 16:36:16.711] [Test worker/DEBUG] [io.netty.util.internal.logging.InternalLoggerFactory/]: Using SLF4J as the default logging framework
[10Aug2026 12:40:42.242] [Test worker/DEBUG] [io.netty.util.ResourceLeakDetector/]: -Dio.netty.leakDetection.level: simple [10Aug2026 16:36:16.713] [Test worker/DEBUG] [io.netty.util.ResourceLeakDetector/]: -Dio.netty.leakDetection.level: simple
[10Aug2026 12:40:42.242] [Test worker/DEBUG] [io.netty.util.ResourceLeakDetector/]: -Dio.netty.leakDetection.targetRecords: 4 [10Aug2026 16:36:16.713] [Test worker/DEBUG] [io.netty.util.ResourceLeakDetector/]: -Dio.netty.leakDetection.targetRecords: 4
[10Aug2026 12:40:44.506] [Test worker/WARN] [Iris/]: Iris cannot resolve the biome id for 'minecraft:plains' before the Minecraft server is available; using biome id 0 [10Aug2026 16:36:18.446] [Test worker/WARN] [Iris/]: Iris cannot resolve the biome id for 'minecraft:plains' before the Minecraft server is available; using biome id 0
[10Aug2026 12:40:44.507] [Test worker/WARN] [Iris/]: Iris cannot enumerate the biome registry before the Minecraft server is available; returning no biomes [10Aug2026 16:36:18.447] [Test worker/WARN] [Iris/]: Iris cannot enumerate the biome registry before the Minecraft server is available; returning no biomes
[10Aug2026 12:40:44.546] [Test worker/INFO] [Iris/]: Iris registered custom content provider 'iris_deferred_test' [10Aug2026 16:36:18.481] [Test worker/INFO] [Iris/]: Iris registered custom content provider 'iris_deferred_test'
[10Aug2026 12:40:44.555] [Test worker/WARN] [Iris/]: Iris persistent dimension registry entry 1 in /var/folders/l1/g6lr3c5x26z_mdmtfzt9gjbr0000gn/T/iris-dimension-registry-partial8292600765329941987/iris-dimensions.json is invalid (entry 1 in /var/folders/l1/g6lr3c5x26z_mdmtfzt9gjbr0000gn/T/iris-dimension-registry-partial8292600765329941987/iris-dimensions.json has no dimension); kept verbatim: {"id":"iris:broken","pack":"overworld"} [10Aug2026 16:36:18.487] [Test worker/WARN] [Iris/]: Iris persistent dimension registry entry 1 in /var/folders/l1/g6lr3c5x26z_mdmtfzt9gjbr0000gn/T/iris-dimension-registry-partial10729038317795245166/iris-dimensions.json is invalid (entry 1 in /var/folders/l1/g6lr3c5x26z_mdmtfzt9gjbr0000gn/T/iris-dimension-registry-partial10729038317795245166/iris-dimensions.json has no dimension); kept verbatim: {"id":"iris:broken","pack":"overworld"}
[10Aug2026 12:40:44.572] [Test worker/ERROR] [Iris/]: Iris persistent dimension registry at /var/folders/l1/g6lr3c5x26z_mdmtfzt9gjbr0000gn/T/iris-dimension-registry-corrupt-boot9980172071037313072/iris-dimensions.json is corrupt; quarantining it and continuing boot [10Aug2026 16:36:18.500] [Test worker/ERROR] [Iris/]: Iris persistent dimension registry at /var/folders/l1/g6lr3c5x26z_mdmtfzt9gjbr0000gn/T/iris-dimension-registry-corrupt-boot15013382090322814704/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-boot9980172071037313072/iris-dimensions.json could not be read; refusing to discard persistent worlds java.lang.IllegalStateException: Iris persistent dimension registry at /var/folders/l1/g6lr3c5x26z_mdmtfzt9gjbr0000gn/T/iris-dimension-registry-corrupt-boot15013382090322814704/iris-dimensions.json could not be read; refusing to discard persistent worlds
at art.arcane.iris.modded.ModdedDimensionRegistryStore.contents(ModdedDimensionRegistryStore.java:150) ~[main/:?] at art.arcane.iris.modded.ModdedDimensionRegistryStore.contents(ModdedDimensionRegistryStore.java:150) ~[main/:?]
at art.arcane.iris.modded.ModdedDimensionRegistryStore.load(ModdedDimensionRegistryStore.java:56) ~[main/:?] at art.arcane.iris.modded.ModdedDimensionRegistryStore.load(ModdedDimensionRegistryStore.java:56) ~[main/:?]
at art.arcane.iris.modded.ModdedDimensionRegistryStore.loadForStartup(ModdedDimensionRegistryStore.java:69) ~[main/:?] at art.arcane.iris.modded.ModdedDimensionRegistryStore.loadForStartup(ModdedDimensionRegistryStore.java:69) ~[main/:?]
@@ -63,9 +63,9 @@ Caused by: art.arcane.volmlib.util.json.JSONException: A JSONObject text must en
at art.arcane.volmlib.util.json.JSONObject.<init>(JSONObject.java:260) ~[shared-local-SNAPSHOT.jar:?] at art.arcane.volmlib.util.json.JSONObject.<init>(JSONObject.java:260) ~[shared-local-SNAPSHOT.jar:?]
at art.arcane.iris.modded.ModdedDimensionRegistryStore.contents(ModdedDimensionRegistryStore.java:117) ~[main/:?] at art.arcane.iris.modded.ModdedDimensionRegistryStore.contents(ModdedDimensionRegistryStore.java:117) ~[main/:?]
... 45 more ... 45 more
[10Aug2026 12:40:44.580] [Test worker/ERROR] [Iris/]: Iris lost 1 persistent dimension(s) from the corrupt registry: iris:lost [10Aug2026 16:36:18.506] [Test worker/ERROR] [Iris/]: Iris lost 1 persistent dimension(s) from the corrupt registry: iris:lost
[10Aug2026 12:40:44.581] [Test worker/ERROR] [Iris/]: Iris moved the corrupt persistent dimension registry to /var/folders/l1/g6lr3c5x26z_mdmtfzt9gjbr0000gn/T/iris-dimension-registry-corrupt-boot9980172071037313072/iris-dimensions.json.broken-1786380044580 [10Aug2026 16:36:18.506] [Test worker/ERROR] [Iris/]: Iris moved the corrupt persistent dimension registry to /var/folders/l1/g6lr3c5x26z_mdmtfzt9gjbr0000gn/T/iris-dimension-registry-corrupt-boot15013382090322814704/iris-dimensions.json.broken-1786394178506
[10Aug2026 12:40:44.633] [Test worker/ERROR] [Iris/]: Iris service onDisable failed for art.arcane.iris.modded.ModdedServiceManagerTest$SecondService [10Aug2026 16:36:18.552] [Test worker/ERROR] [Iris/]: Iris service onDisable failed for art.arcane.iris.modded.ModdedServiceManagerTest$SecondService
java.lang.RuntimeException: second disable failed java.lang.RuntimeException: second disable failed
at art.arcane.iris.modded.ModdedServiceManagerTest.disableAttemptsEveryServiceInReverseOrderAndNeverRethrows(ModdedServiceManagerTest.java:55) ~[test/:?] at art.arcane.iris.modded.ModdedServiceManagerTest.disableAttemptsEveryServiceInReverseOrderAndNeverRethrows(ModdedServiceManagerTest.java:55) ~[test/:?]
at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:104) ~[?:?] at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:104) ~[?:?]
@@ -110,7 +110,7 @@ java.lang.RuntimeException: second disable failed
at org.gradle.process.internal.worker.child.SystemApplicationClassLoaderWorker.call(SystemApplicationClassLoaderWorker.java:72) ~[?:?] at org.gradle.process.internal.worker.child.SystemApplicationClassLoaderWorker.call(SystemApplicationClassLoaderWorker.java:72) ~[?:?]
at worker.org.gradle.process.internal.worker.GradleWorkerMain.run(GradleWorkerMain.java:69) [gradle-worker.jar:?] at worker.org.gradle.process.internal.worker.GradleWorkerMain.run(GradleWorkerMain.java:69) [gradle-worker.jar:?]
at worker.org.gradle.process.internal.worker.GradleWorkerMain.main(GradleWorkerMain.java:74) [gradle-worker.jar:?] at worker.org.gradle.process.internal.worker.GradleWorkerMain.main(GradleWorkerMain.java:74) [gradle-worker.jar:?]
[10Aug2026 12:40:44.635] [Test worker/ERROR] [Iris/]: Iris service onDisable failed for art.arcane.iris.modded.ModdedServiceManagerTest$FirstService [10Aug2026 16:36:18.555] [Test worker/ERROR] [Iris/]: Iris service onDisable failed for art.arcane.iris.modded.ModdedServiceManagerTest$FirstService
java.lang.RuntimeException: first disable failed java.lang.RuntimeException: first disable failed
at art.arcane.iris.modded.ModdedServiceManagerTest.disableAttemptsEveryServiceInReverseOrderAndNeverRethrows(ModdedServiceManagerTest.java:54) ~[test/:?] at art.arcane.iris.modded.ModdedServiceManagerTest.disableAttemptsEveryServiceInReverseOrderAndNeverRethrows(ModdedServiceManagerTest.java:54) ~[test/:?]
at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:104) ~[?:?] at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:104) ~[?:?]
@@ -155,7 +155,7 @@ java.lang.RuntimeException: first disable failed
at org.gradle.process.internal.worker.child.SystemApplicationClassLoaderWorker.call(SystemApplicationClassLoaderWorker.java:72) ~[?:?] at org.gradle.process.internal.worker.child.SystemApplicationClassLoaderWorker.call(SystemApplicationClassLoaderWorker.java:72) ~[?:?]
at worker.org.gradle.process.internal.worker.GradleWorkerMain.run(GradleWorkerMain.java:69) [gradle-worker.jar:?] at worker.org.gradle.process.internal.worker.GradleWorkerMain.run(GradleWorkerMain.java:69) [gradle-worker.jar:?]
at worker.org.gradle.process.internal.worker.GradleWorkerMain.main(GradleWorkerMain.java:74) [gradle-worker.jar:?] at worker.org.gradle.process.internal.worker.GradleWorkerMain.main(GradleWorkerMain.java:74) [gradle-worker.jar:?]
[10Aug2026 12:40:44.638] [Test worker/ERROR] [Iris/]: Iris disabled all services with 2 failure(s) [10Aug2026 16:36:18.557] [Test worker/ERROR] [Iris/]: Iris disabled all services with 2 failure(s)
java.lang.RuntimeException: second disable failed java.lang.RuntimeException: second disable failed
at art.arcane.iris.modded.ModdedServiceManagerTest.disableAttemptsEveryServiceInReverseOrderAndNeverRethrows(ModdedServiceManagerTest.java:55) ~[test/:?] at art.arcane.iris.modded.ModdedServiceManagerTest.disableAttemptsEveryServiceInReverseOrderAndNeverRethrows(ModdedServiceManagerTest.java:55) ~[test/:?]
at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:104) ~[?:?] at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:104) ~[?:?]
@@ -203,7 +203,7 @@ java.lang.RuntimeException: second disable failed
Suppressed: java.lang.RuntimeException: first disable failed Suppressed: java.lang.RuntimeException: first disable failed
at art.arcane.iris.modded.ModdedServiceManagerTest.disableAttemptsEveryServiceInReverseOrderAndNeverRethrows(ModdedServiceManagerTest.java:54) ~[test/:?] at art.arcane.iris.modded.ModdedServiceManagerTest.disableAttemptsEveryServiceInReverseOrderAndNeverRethrows(ModdedServiceManagerTest.java:54) ~[test/:?]
... 42 more ... 42 more
[10Aug2026 12:40:44.642] [Test worker/ERROR] [Iris/]: Iris service rollback failed for art.arcane.iris.modded.ModdedServiceManagerTest$SecondService [10Aug2026 16:36:18.560] [Test worker/ERROR] [Iris/]: Iris service rollback failed for art.arcane.iris.modded.ModdedServiceManagerTest$SecondService
java.lang.RuntimeException: cleanup failed java.lang.RuntimeException: cleanup failed
at art.arcane.iris.modded.ModdedServiceManagerTest.rollsBackEnabledServicesAndPreservesCleanupFailures(ModdedServiceManagerTest.java:32) ~[test/:?] at art.arcane.iris.modded.ModdedServiceManagerTest.rollsBackEnabledServicesAndPreservesCleanupFailures(ModdedServiceManagerTest.java:32) ~[test/:?]
at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:104) ~[?:?] at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:104) ~[?:?]
@@ -248,7 +248,7 @@ java.lang.RuntimeException: cleanup failed
at org.gradle.process.internal.worker.child.SystemApplicationClassLoaderWorker.call(SystemApplicationClassLoaderWorker.java:72) ~[?:?] at org.gradle.process.internal.worker.child.SystemApplicationClassLoaderWorker.call(SystemApplicationClassLoaderWorker.java:72) ~[?:?]
at worker.org.gradle.process.internal.worker.GradleWorkerMain.run(GradleWorkerMain.java:69) [gradle-worker.jar:?] at worker.org.gradle.process.internal.worker.GradleWorkerMain.run(GradleWorkerMain.java:69) [gradle-worker.jar:?]
at worker.org.gradle.process.internal.worker.GradleWorkerMain.main(GradleWorkerMain.java:74) [gradle-worker.jar:?] at worker.org.gradle.process.internal.worker.GradleWorkerMain.main(GradleWorkerMain.java:74) [gradle-worker.jar:?]
[10Aug2026 12:40:44.644] [Test worker/ERROR] [Iris/]: Iris service onEnable failed for art.arcane.iris.modded.ModdedServiceManagerTest$SecondService [10Aug2026 16:36:18.562] [Test worker/ERROR] [Iris/]: Iris service onEnable failed for art.arcane.iris.modded.ModdedServiceManagerTest$SecondService
java.lang.RuntimeException: enable failed java.lang.RuntimeException: enable failed
at art.arcane.iris.modded.ModdedServiceManagerTest.rollsBackEnabledServicesAndPreservesCleanupFailures(ModdedServiceManagerTest.java:31) ~[test/:?] at art.arcane.iris.modded.ModdedServiceManagerTest.rollsBackEnabledServicesAndPreservesCleanupFailures(ModdedServiceManagerTest.java:31) ~[test/:?]
at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:104) ~[?:?] at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:104) ~[?:?]
@@ -296,7 +296,7 @@ java.lang.RuntimeException: enable failed
Suppressed: java.lang.RuntimeException: cleanup failed Suppressed: java.lang.RuntimeException: cleanup failed
at art.arcane.iris.modded.ModdedServiceManagerTest.rollsBackEnabledServicesAndPreservesCleanupFailures(ModdedServiceManagerTest.java:32) ~[test/:?] at art.arcane.iris.modded.ModdedServiceManagerTest.rollsBackEnabledServicesAndPreservesCleanupFailures(ModdedServiceManagerTest.java:32) ~[test/:?]
... 42 more ... 42 more
[10Aug2026 12:40:44.683] [Test worker/ERROR] [Iris/]: [worldcheck] server stop request failed [10Aug2026 16:36:18.596] [Test worker/ERROR] [Iris/]: [worldcheck] server stop request failed
java.lang.IllegalStateException: stop request failed java.lang.IllegalStateException: stop request failed
at art.arcane.iris.modded.ModdedWorldCheckTest.lambda$stopRequestFailureForcesNonzeroResult$1(ModdedWorldCheckTest.java:238) ~[test/:?] at art.arcane.iris.modded.ModdedWorldCheckTest.lambda$stopRequestFailureForcesNonzeroResult$1(ModdedWorldCheckTest.java:238) ~[test/:?]
at art.arcane.iris.modded.ModdedWorldCheck.runAndRequestStop(ModdedWorldCheck.java:144) ~[main/:?] at art.arcane.iris.modded.ModdedWorldCheck.runAndRequestStop(ModdedWorldCheck.java:144) ~[main/:?]
@@ -343,7 +343,7 @@ java.lang.IllegalStateException: stop request failed
at org.gradle.process.internal.worker.child.SystemApplicationClassLoaderWorker.call(SystemApplicationClassLoaderWorker.java:72) ~[?:?] at org.gradle.process.internal.worker.child.SystemApplicationClassLoaderWorker.call(SystemApplicationClassLoaderWorker.java:72) ~[?:?]
at worker.org.gradle.process.internal.worker.GradleWorkerMain.run(GradleWorkerMain.java:69) [gradle-worker.jar:?] at worker.org.gradle.process.internal.worker.GradleWorkerMain.run(GradleWorkerMain.java:69) [gradle-worker.jar:?]
at worker.org.gradle.process.internal.worker.GradleWorkerMain.main(GradleWorkerMain.java:74) [gradle-worker.jar:?] at worker.org.gradle.process.internal.worker.GradleWorkerMain.main(GradleWorkerMain.java:74) [gradle-worker.jar:?]
[10Aug2026 12:40:44.685] [Test worker/ERROR] [Iris/]: [worldcheck] waiting for server shutdown failed [10Aug2026 16:36:18.598] [Test worker/ERROR] [Iris/]: [worldcheck] waiting for server shutdown failed
java.lang.IllegalStateException: shutdown wait failed java.lang.IllegalStateException: shutdown wait failed
at art.arcane.iris.modded.ModdedWorldCheckTest.lambda$shutdownWaitFailureForcesNonzeroExit$0(ModdedWorldCheckTest.java:264) ~[test/:?] at art.arcane.iris.modded.ModdedWorldCheckTest.lambda$shutdownWaitFailureForcesNonzeroExit$0(ModdedWorldCheckTest.java:264) ~[test/:?]
at art.arcane.iris.modded.ModdedWorldCheck.awaitStopAndExit(ModdedWorldCheck.java:159) ~[main/:?] at art.arcane.iris.modded.ModdedWorldCheck.awaitStopAndExit(ModdedWorldCheck.java:159) ~[main/:?]
@@ -390,7 +390,7 @@ java.lang.IllegalStateException: shutdown wait failed
at org.gradle.process.internal.worker.child.SystemApplicationClassLoaderWorker.call(SystemApplicationClassLoaderWorker.java:72) ~[?:?] at org.gradle.process.internal.worker.child.SystemApplicationClassLoaderWorker.call(SystemApplicationClassLoaderWorker.java:72) ~[?:?]
at worker.org.gradle.process.internal.worker.GradleWorkerMain.run(GradleWorkerMain.java:69) [gradle-worker.jar:?] at worker.org.gradle.process.internal.worker.GradleWorkerMain.run(GradleWorkerMain.java:69) [gradle-worker.jar:?]
at worker.org.gradle.process.internal.worker.GradleWorkerMain.main(GradleWorkerMain.java:74) [gradle-worker.jar:?] at worker.org.gradle.process.internal.worker.GradleWorkerMain.main(GradleWorkerMain.java:74) [gradle-worker.jar:?]
[10Aug2026 12:40:44.690] [Test worker/ERROR] [Iris/]: [worldcheck] check failed [10Aug2026 16:36:18.602] [Test worker/ERROR] [Iris/]: [worldcheck] check failed
java.lang.IllegalStateException: check failed java.lang.IllegalStateException: check failed
at art.arcane.iris.modded.ModdedWorldCheckTest.lambda$thrownCheckStillRequestsStop$0(ModdedWorldCheckTest.java:224) ~[test/:?] at art.arcane.iris.modded.ModdedWorldCheckTest.lambda$thrownCheckStillRequestsStop$0(ModdedWorldCheckTest.java:224) ~[test/:?]
at art.arcane.iris.modded.ModdedWorldCheck.runAndRequestStop(ModdedWorldCheck.java:139) ~[main/:?] at art.arcane.iris.modded.ModdedWorldCheck.runAndRequestStop(ModdedWorldCheck.java:139) ~[main/:?]
@@ -437,8 +437,8 @@ java.lang.IllegalStateException: check failed
at org.gradle.process.internal.worker.child.SystemApplicationClassLoaderWorker.call(SystemApplicationClassLoaderWorker.java:72) ~[?:?] at org.gradle.process.internal.worker.child.SystemApplicationClassLoaderWorker.call(SystemApplicationClassLoaderWorker.java:72) ~[?:?]
at worker.org.gradle.process.internal.worker.GradleWorkerMain.run(GradleWorkerMain.java:69) [gradle-worker.jar:?] at worker.org.gradle.process.internal.worker.GradleWorkerMain.run(GradleWorkerMain.java:69) [gradle-worker.jar:?]
at worker.org.gradle.process.internal.worker.GradleWorkerMain.main(GradleWorkerMain.java:74) [gradle-worker.jar:?] at worker.org.gradle.process.internal.worker.GradleWorkerMain.main(GradleWorkerMain.java:74) [gradle-worker.jar:?]
[10Aug2026 12:40:44.704] [Test worker/INFO] [Iris/]: Iris registered custom content provider 'iris_discovery_success' [10Aug2026 16:36:18.613] [Test worker/INFO] [Iris/]: Iris registered custom content provider 'iris_discovery_success'
[10Aug2026 12:40:44.705] [Test worker/WARN] [Iris/]: Iris custom content provider discovery failed at provider 'iris_discovery_failure' (art.arcane.iris.modded.api.ModdedCustomContentRegistryTest$TestProvider) [10Aug2026 16:36:18.613] [Test worker/WARN] [Iris/]: Iris custom content provider discovery failed at provider 'iris_discovery_failure' (art.arcane.iris.modded.api.ModdedCustomContentRegistryTest$TestProvider)
java.lang.RuntimeException: provider init failed java.lang.RuntimeException: provider init failed
at art.arcane.iris.modded.api.ModdedCustomContentRegistryTest.failedDiscoveryPublishesNothingAndPreservesTheCause(ModdedCustomContentRegistryTest.java:39) ~[test/:?] at art.arcane.iris.modded.api.ModdedCustomContentRegistryTest.failedDiscoveryPublishesNothingAndPreservesTheCause(ModdedCustomContentRegistryTest.java:39) ~[test/:?]
at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:104) ~[?:?] at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:104) ~[?:?]
@@ -483,4 +483,4 @@ java.lang.RuntimeException: provider init failed
at org.gradle.process.internal.worker.child.SystemApplicationClassLoaderWorker.call(SystemApplicationClassLoaderWorker.java:72) ~[?:?] at org.gradle.process.internal.worker.child.SystemApplicationClassLoaderWorker.call(SystemApplicationClassLoaderWorker.java:72) ~[?:?]
at worker.org.gradle.process.internal.worker.GradleWorkerMain.run(GradleWorkerMain.java:69) [gradle-worker.jar:?] at worker.org.gradle.process.internal.worker.GradleWorkerMain.run(GradleWorkerMain.java:69) [gradle-worker.jar:?]
at worker.org.gradle.process.internal.worker.GradleWorkerMain.main(GradleWorkerMain.java:74) [gradle-worker.jar:?] at worker.org.gradle.process.internal.worker.GradleWorkerMain.main(GradleWorkerMain.java:74) [gradle-worker.jar:?]
[10Aug2026 12:40:44.720] [Test worker/INFO] [Iris/]: Iris object undo service ready (bounded to 32 paste(s) per player) [10Aug2026 16:36:18.627] [Test worker/INFO] [Iris/]: Iris object undo service ready (bounded to 32 paste(s) per player)
+19 -19
View File
@@ -1,9 +1,9 @@
[10Aug2026 12:40:44.506] [Test worker/WARN] [Iris/]: Iris cannot resolve the biome id for 'minecraft:plains' before the Minecraft server is available; using biome id 0 [10Aug2026 16:36:18.446] [Test worker/WARN] [Iris/]: Iris cannot resolve the biome id for 'minecraft:plains' before the Minecraft server is available; using biome id 0
[10Aug2026 12:40:44.507] [Test worker/WARN] [Iris/]: Iris cannot enumerate the biome registry before the Minecraft server is available; returning no biomes [10Aug2026 16:36:18.447] [Test worker/WARN] [Iris/]: Iris cannot enumerate the biome registry before the Minecraft server is available; returning no biomes
[10Aug2026 12:40:44.546] [Test worker/INFO] [Iris/]: Iris registered custom content provider 'iris_deferred_test' [10Aug2026 16:36:18.481] [Test worker/INFO] [Iris/]: Iris registered custom content provider 'iris_deferred_test'
[10Aug2026 12:40:44.555] [Test worker/WARN] [Iris/]: Iris persistent dimension registry entry 1 in /var/folders/l1/g6lr3c5x26z_mdmtfzt9gjbr0000gn/T/iris-dimension-registry-partial8292600765329941987/iris-dimensions.json is invalid (entry 1 in /var/folders/l1/g6lr3c5x26z_mdmtfzt9gjbr0000gn/T/iris-dimension-registry-partial8292600765329941987/iris-dimensions.json has no dimension); kept verbatim: {"id":"iris:broken","pack":"overworld"} [10Aug2026 16:36:18.487] [Test worker/WARN] [Iris/]: Iris persistent dimension registry entry 1 in /var/folders/l1/g6lr3c5x26z_mdmtfzt9gjbr0000gn/T/iris-dimension-registry-partial10729038317795245166/iris-dimensions.json is invalid (entry 1 in /var/folders/l1/g6lr3c5x26z_mdmtfzt9gjbr0000gn/T/iris-dimension-registry-partial10729038317795245166/iris-dimensions.json has no dimension); kept verbatim: {"id":"iris:broken","pack":"overworld"}
[10Aug2026 12:40:44.572] [Test worker/ERROR] [Iris/]: Iris persistent dimension registry at /var/folders/l1/g6lr3c5x26z_mdmtfzt9gjbr0000gn/T/iris-dimension-registry-corrupt-boot9980172071037313072/iris-dimensions.json is corrupt; quarantining it and continuing boot [10Aug2026 16:36:18.500] [Test worker/ERROR] [Iris/]: Iris persistent dimension registry at /var/folders/l1/g6lr3c5x26z_mdmtfzt9gjbr0000gn/T/iris-dimension-registry-corrupt-boot15013382090322814704/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-boot9980172071037313072/iris-dimensions.json could not be read; refusing to discard persistent worlds java.lang.IllegalStateException: Iris persistent dimension registry at /var/folders/l1/g6lr3c5x26z_mdmtfzt9gjbr0000gn/T/iris-dimension-registry-corrupt-boot15013382090322814704/iris-dimensions.json could not be read; refusing to discard persistent worlds
at art.arcane.iris.modded.ModdedDimensionRegistryStore.contents(ModdedDimensionRegistryStore.java:150) ~[main/:?] at art.arcane.iris.modded.ModdedDimensionRegistryStore.contents(ModdedDimensionRegistryStore.java:150) ~[main/:?]
at art.arcane.iris.modded.ModdedDimensionRegistryStore.load(ModdedDimensionRegistryStore.java:56) ~[main/:?] at art.arcane.iris.modded.ModdedDimensionRegistryStore.load(ModdedDimensionRegistryStore.java:56) ~[main/:?]
at art.arcane.iris.modded.ModdedDimensionRegistryStore.loadForStartup(ModdedDimensionRegistryStore.java:69) ~[main/:?] at art.arcane.iris.modded.ModdedDimensionRegistryStore.loadForStartup(ModdedDimensionRegistryStore.java:69) ~[main/:?]
@@ -60,9 +60,9 @@ Caused by: art.arcane.volmlib.util.json.JSONException: A JSONObject text must en
at art.arcane.volmlib.util.json.JSONObject.<init>(JSONObject.java:260) ~[shared-local-SNAPSHOT.jar:?] at art.arcane.volmlib.util.json.JSONObject.<init>(JSONObject.java:260) ~[shared-local-SNAPSHOT.jar:?]
at art.arcane.iris.modded.ModdedDimensionRegistryStore.contents(ModdedDimensionRegistryStore.java:117) ~[main/:?] at art.arcane.iris.modded.ModdedDimensionRegistryStore.contents(ModdedDimensionRegistryStore.java:117) ~[main/:?]
... 45 more ... 45 more
[10Aug2026 12:40:44.580] [Test worker/ERROR] [Iris/]: Iris lost 1 persistent dimension(s) from the corrupt registry: iris:lost [10Aug2026 16:36:18.506] [Test worker/ERROR] [Iris/]: Iris lost 1 persistent dimension(s) from the corrupt registry: iris:lost
[10Aug2026 12:40:44.581] [Test worker/ERROR] [Iris/]: Iris moved the corrupt persistent dimension registry to /var/folders/l1/g6lr3c5x26z_mdmtfzt9gjbr0000gn/T/iris-dimension-registry-corrupt-boot9980172071037313072/iris-dimensions.json.broken-1786380044580 [10Aug2026 16:36:18.506] [Test worker/ERROR] [Iris/]: Iris moved the corrupt persistent dimension registry to /var/folders/l1/g6lr3c5x26z_mdmtfzt9gjbr0000gn/T/iris-dimension-registry-corrupt-boot15013382090322814704/iris-dimensions.json.broken-1786394178506
[10Aug2026 12:40:44.633] [Test worker/ERROR] [Iris/]: Iris service onDisable failed for art.arcane.iris.modded.ModdedServiceManagerTest$SecondService [10Aug2026 16:36:18.552] [Test worker/ERROR] [Iris/]: Iris service onDisable failed for art.arcane.iris.modded.ModdedServiceManagerTest$SecondService
java.lang.RuntimeException: second disable failed java.lang.RuntimeException: second disable failed
at art.arcane.iris.modded.ModdedServiceManagerTest.disableAttemptsEveryServiceInReverseOrderAndNeverRethrows(ModdedServiceManagerTest.java:55) ~[test/:?] at art.arcane.iris.modded.ModdedServiceManagerTest.disableAttemptsEveryServiceInReverseOrderAndNeverRethrows(ModdedServiceManagerTest.java:55) ~[test/:?]
at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:104) ~[?:?] at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:104) ~[?:?]
@@ -107,7 +107,7 @@ java.lang.RuntimeException: second disable failed
at org.gradle.process.internal.worker.child.SystemApplicationClassLoaderWorker.call(SystemApplicationClassLoaderWorker.java:72) ~[?:?] at org.gradle.process.internal.worker.child.SystemApplicationClassLoaderWorker.call(SystemApplicationClassLoaderWorker.java:72) ~[?:?]
at worker.org.gradle.process.internal.worker.GradleWorkerMain.run(GradleWorkerMain.java:69) [gradle-worker.jar:?] at worker.org.gradle.process.internal.worker.GradleWorkerMain.run(GradleWorkerMain.java:69) [gradle-worker.jar:?]
at worker.org.gradle.process.internal.worker.GradleWorkerMain.main(GradleWorkerMain.java:74) [gradle-worker.jar:?] at worker.org.gradle.process.internal.worker.GradleWorkerMain.main(GradleWorkerMain.java:74) [gradle-worker.jar:?]
[10Aug2026 12:40:44.635] [Test worker/ERROR] [Iris/]: Iris service onDisable failed for art.arcane.iris.modded.ModdedServiceManagerTest$FirstService [10Aug2026 16:36:18.555] [Test worker/ERROR] [Iris/]: Iris service onDisable failed for art.arcane.iris.modded.ModdedServiceManagerTest$FirstService
java.lang.RuntimeException: first disable failed java.lang.RuntimeException: first disable failed
at art.arcane.iris.modded.ModdedServiceManagerTest.disableAttemptsEveryServiceInReverseOrderAndNeverRethrows(ModdedServiceManagerTest.java:54) ~[test/:?] at art.arcane.iris.modded.ModdedServiceManagerTest.disableAttemptsEveryServiceInReverseOrderAndNeverRethrows(ModdedServiceManagerTest.java:54) ~[test/:?]
at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:104) ~[?:?] at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:104) ~[?:?]
@@ -152,7 +152,7 @@ java.lang.RuntimeException: first disable failed
at org.gradle.process.internal.worker.child.SystemApplicationClassLoaderWorker.call(SystemApplicationClassLoaderWorker.java:72) ~[?:?] at org.gradle.process.internal.worker.child.SystemApplicationClassLoaderWorker.call(SystemApplicationClassLoaderWorker.java:72) ~[?:?]
at worker.org.gradle.process.internal.worker.GradleWorkerMain.run(GradleWorkerMain.java:69) [gradle-worker.jar:?] at worker.org.gradle.process.internal.worker.GradleWorkerMain.run(GradleWorkerMain.java:69) [gradle-worker.jar:?]
at worker.org.gradle.process.internal.worker.GradleWorkerMain.main(GradleWorkerMain.java:74) [gradle-worker.jar:?] at worker.org.gradle.process.internal.worker.GradleWorkerMain.main(GradleWorkerMain.java:74) [gradle-worker.jar:?]
[10Aug2026 12:40:44.638] [Test worker/ERROR] [Iris/]: Iris disabled all services with 2 failure(s) [10Aug2026 16:36:18.557] [Test worker/ERROR] [Iris/]: Iris disabled all services with 2 failure(s)
java.lang.RuntimeException: second disable failed java.lang.RuntimeException: second disable failed
at art.arcane.iris.modded.ModdedServiceManagerTest.disableAttemptsEveryServiceInReverseOrderAndNeverRethrows(ModdedServiceManagerTest.java:55) ~[test/:?] at art.arcane.iris.modded.ModdedServiceManagerTest.disableAttemptsEveryServiceInReverseOrderAndNeverRethrows(ModdedServiceManagerTest.java:55) ~[test/:?]
at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:104) ~[?:?] at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:104) ~[?:?]
@@ -200,7 +200,7 @@ java.lang.RuntimeException: second disable failed
Suppressed: java.lang.RuntimeException: first disable failed Suppressed: java.lang.RuntimeException: first disable failed
at art.arcane.iris.modded.ModdedServiceManagerTest.disableAttemptsEveryServiceInReverseOrderAndNeverRethrows(ModdedServiceManagerTest.java:54) ~[test/:?] at art.arcane.iris.modded.ModdedServiceManagerTest.disableAttemptsEveryServiceInReverseOrderAndNeverRethrows(ModdedServiceManagerTest.java:54) ~[test/:?]
... 42 more ... 42 more
[10Aug2026 12:40:44.642] [Test worker/ERROR] [Iris/]: Iris service rollback failed for art.arcane.iris.modded.ModdedServiceManagerTest$SecondService [10Aug2026 16:36:18.560] [Test worker/ERROR] [Iris/]: Iris service rollback failed for art.arcane.iris.modded.ModdedServiceManagerTest$SecondService
java.lang.RuntimeException: cleanup failed java.lang.RuntimeException: cleanup failed
at art.arcane.iris.modded.ModdedServiceManagerTest.rollsBackEnabledServicesAndPreservesCleanupFailures(ModdedServiceManagerTest.java:32) ~[test/:?] at art.arcane.iris.modded.ModdedServiceManagerTest.rollsBackEnabledServicesAndPreservesCleanupFailures(ModdedServiceManagerTest.java:32) ~[test/:?]
at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:104) ~[?:?] at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:104) ~[?:?]
@@ -245,7 +245,7 @@ java.lang.RuntimeException: cleanup failed
at org.gradle.process.internal.worker.child.SystemApplicationClassLoaderWorker.call(SystemApplicationClassLoaderWorker.java:72) ~[?:?] at org.gradle.process.internal.worker.child.SystemApplicationClassLoaderWorker.call(SystemApplicationClassLoaderWorker.java:72) ~[?:?]
at worker.org.gradle.process.internal.worker.GradleWorkerMain.run(GradleWorkerMain.java:69) [gradle-worker.jar:?] at worker.org.gradle.process.internal.worker.GradleWorkerMain.run(GradleWorkerMain.java:69) [gradle-worker.jar:?]
at worker.org.gradle.process.internal.worker.GradleWorkerMain.main(GradleWorkerMain.java:74) [gradle-worker.jar:?] at worker.org.gradle.process.internal.worker.GradleWorkerMain.main(GradleWorkerMain.java:74) [gradle-worker.jar:?]
[10Aug2026 12:40:44.644] [Test worker/ERROR] [Iris/]: Iris service onEnable failed for art.arcane.iris.modded.ModdedServiceManagerTest$SecondService [10Aug2026 16:36:18.562] [Test worker/ERROR] [Iris/]: Iris service onEnable failed for art.arcane.iris.modded.ModdedServiceManagerTest$SecondService
java.lang.RuntimeException: enable failed java.lang.RuntimeException: enable failed
at art.arcane.iris.modded.ModdedServiceManagerTest.rollsBackEnabledServicesAndPreservesCleanupFailures(ModdedServiceManagerTest.java:31) ~[test/:?] at art.arcane.iris.modded.ModdedServiceManagerTest.rollsBackEnabledServicesAndPreservesCleanupFailures(ModdedServiceManagerTest.java:31) ~[test/:?]
at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:104) ~[?:?] at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:104) ~[?:?]
@@ -293,7 +293,7 @@ java.lang.RuntimeException: enable failed
Suppressed: java.lang.RuntimeException: cleanup failed Suppressed: java.lang.RuntimeException: cleanup failed
at art.arcane.iris.modded.ModdedServiceManagerTest.rollsBackEnabledServicesAndPreservesCleanupFailures(ModdedServiceManagerTest.java:32) ~[test/:?] at art.arcane.iris.modded.ModdedServiceManagerTest.rollsBackEnabledServicesAndPreservesCleanupFailures(ModdedServiceManagerTest.java:32) ~[test/:?]
... 42 more ... 42 more
[10Aug2026 12:40:44.683] [Test worker/ERROR] [Iris/]: [worldcheck] server stop request failed [10Aug2026 16:36:18.596] [Test worker/ERROR] [Iris/]: [worldcheck] server stop request failed
java.lang.IllegalStateException: stop request failed java.lang.IllegalStateException: stop request failed
at art.arcane.iris.modded.ModdedWorldCheckTest.lambda$stopRequestFailureForcesNonzeroResult$1(ModdedWorldCheckTest.java:238) ~[test/:?] at art.arcane.iris.modded.ModdedWorldCheckTest.lambda$stopRequestFailureForcesNonzeroResult$1(ModdedWorldCheckTest.java:238) ~[test/:?]
at art.arcane.iris.modded.ModdedWorldCheck.runAndRequestStop(ModdedWorldCheck.java:144) ~[main/:?] at art.arcane.iris.modded.ModdedWorldCheck.runAndRequestStop(ModdedWorldCheck.java:144) ~[main/:?]
@@ -340,7 +340,7 @@ java.lang.IllegalStateException: stop request failed
at org.gradle.process.internal.worker.child.SystemApplicationClassLoaderWorker.call(SystemApplicationClassLoaderWorker.java:72) ~[?:?] at org.gradle.process.internal.worker.child.SystemApplicationClassLoaderWorker.call(SystemApplicationClassLoaderWorker.java:72) ~[?:?]
at worker.org.gradle.process.internal.worker.GradleWorkerMain.run(GradleWorkerMain.java:69) [gradle-worker.jar:?] at worker.org.gradle.process.internal.worker.GradleWorkerMain.run(GradleWorkerMain.java:69) [gradle-worker.jar:?]
at worker.org.gradle.process.internal.worker.GradleWorkerMain.main(GradleWorkerMain.java:74) [gradle-worker.jar:?] at worker.org.gradle.process.internal.worker.GradleWorkerMain.main(GradleWorkerMain.java:74) [gradle-worker.jar:?]
[10Aug2026 12:40:44.685] [Test worker/ERROR] [Iris/]: [worldcheck] waiting for server shutdown failed [10Aug2026 16:36:18.598] [Test worker/ERROR] [Iris/]: [worldcheck] waiting for server shutdown failed
java.lang.IllegalStateException: shutdown wait failed java.lang.IllegalStateException: shutdown wait failed
at art.arcane.iris.modded.ModdedWorldCheckTest.lambda$shutdownWaitFailureForcesNonzeroExit$0(ModdedWorldCheckTest.java:264) ~[test/:?] at art.arcane.iris.modded.ModdedWorldCheckTest.lambda$shutdownWaitFailureForcesNonzeroExit$0(ModdedWorldCheckTest.java:264) ~[test/:?]
at art.arcane.iris.modded.ModdedWorldCheck.awaitStopAndExit(ModdedWorldCheck.java:159) ~[main/:?] at art.arcane.iris.modded.ModdedWorldCheck.awaitStopAndExit(ModdedWorldCheck.java:159) ~[main/:?]
@@ -387,7 +387,7 @@ java.lang.IllegalStateException: shutdown wait failed
at org.gradle.process.internal.worker.child.SystemApplicationClassLoaderWorker.call(SystemApplicationClassLoaderWorker.java:72) ~[?:?] at org.gradle.process.internal.worker.child.SystemApplicationClassLoaderWorker.call(SystemApplicationClassLoaderWorker.java:72) ~[?:?]
at worker.org.gradle.process.internal.worker.GradleWorkerMain.run(GradleWorkerMain.java:69) [gradle-worker.jar:?] at worker.org.gradle.process.internal.worker.GradleWorkerMain.run(GradleWorkerMain.java:69) [gradle-worker.jar:?]
at worker.org.gradle.process.internal.worker.GradleWorkerMain.main(GradleWorkerMain.java:74) [gradle-worker.jar:?] at worker.org.gradle.process.internal.worker.GradleWorkerMain.main(GradleWorkerMain.java:74) [gradle-worker.jar:?]
[10Aug2026 12:40:44.690] [Test worker/ERROR] [Iris/]: [worldcheck] check failed [10Aug2026 16:36:18.602] [Test worker/ERROR] [Iris/]: [worldcheck] check failed
java.lang.IllegalStateException: check failed java.lang.IllegalStateException: check failed
at art.arcane.iris.modded.ModdedWorldCheckTest.lambda$thrownCheckStillRequestsStop$0(ModdedWorldCheckTest.java:224) ~[test/:?] at art.arcane.iris.modded.ModdedWorldCheckTest.lambda$thrownCheckStillRequestsStop$0(ModdedWorldCheckTest.java:224) ~[test/:?]
at art.arcane.iris.modded.ModdedWorldCheck.runAndRequestStop(ModdedWorldCheck.java:139) ~[main/:?] at art.arcane.iris.modded.ModdedWorldCheck.runAndRequestStop(ModdedWorldCheck.java:139) ~[main/:?]
@@ -434,8 +434,8 @@ java.lang.IllegalStateException: check failed
at org.gradle.process.internal.worker.child.SystemApplicationClassLoaderWorker.call(SystemApplicationClassLoaderWorker.java:72) ~[?:?] at org.gradle.process.internal.worker.child.SystemApplicationClassLoaderWorker.call(SystemApplicationClassLoaderWorker.java:72) ~[?:?]
at worker.org.gradle.process.internal.worker.GradleWorkerMain.run(GradleWorkerMain.java:69) [gradle-worker.jar:?] at worker.org.gradle.process.internal.worker.GradleWorkerMain.run(GradleWorkerMain.java:69) [gradle-worker.jar:?]
at worker.org.gradle.process.internal.worker.GradleWorkerMain.main(GradleWorkerMain.java:74) [gradle-worker.jar:?] at worker.org.gradle.process.internal.worker.GradleWorkerMain.main(GradleWorkerMain.java:74) [gradle-worker.jar:?]
[10Aug2026 12:40:44.704] [Test worker/INFO] [Iris/]: Iris registered custom content provider 'iris_discovery_success' [10Aug2026 16:36:18.613] [Test worker/INFO] [Iris/]: Iris registered custom content provider 'iris_discovery_success'
[10Aug2026 12:40:44.705] [Test worker/WARN] [Iris/]: Iris custom content provider discovery failed at provider 'iris_discovery_failure' (art.arcane.iris.modded.api.ModdedCustomContentRegistryTest$TestProvider) [10Aug2026 16:36:18.613] [Test worker/WARN] [Iris/]: Iris custom content provider discovery failed at provider 'iris_discovery_failure' (art.arcane.iris.modded.api.ModdedCustomContentRegistryTest$TestProvider)
java.lang.RuntimeException: provider init failed java.lang.RuntimeException: provider init failed
at art.arcane.iris.modded.api.ModdedCustomContentRegistryTest.failedDiscoveryPublishesNothingAndPreservesTheCause(ModdedCustomContentRegistryTest.java:39) ~[test/:?] at art.arcane.iris.modded.api.ModdedCustomContentRegistryTest.failedDiscoveryPublishesNothingAndPreservesTheCause(ModdedCustomContentRegistryTest.java:39) ~[test/:?]
at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:104) ~[?:?] at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:104) ~[?:?]
@@ -480,4 +480,4 @@ java.lang.RuntimeException: provider init failed
at org.gradle.process.internal.worker.child.SystemApplicationClassLoaderWorker.call(SystemApplicationClassLoaderWorker.java:72) ~[?:?] at org.gradle.process.internal.worker.child.SystemApplicationClassLoaderWorker.call(SystemApplicationClassLoaderWorker.java:72) ~[?:?]
at worker.org.gradle.process.internal.worker.GradleWorkerMain.run(GradleWorkerMain.java:69) [gradle-worker.jar:?] at worker.org.gradle.process.internal.worker.GradleWorkerMain.run(GradleWorkerMain.java:69) [gradle-worker.jar:?]
at worker.org.gradle.process.internal.worker.GradleWorkerMain.main(GradleWorkerMain.java:74) [gradle-worker.jar:?] at worker.org.gradle.process.internal.worker.GradleWorkerMain.main(GradleWorkerMain.java:74) [gradle-worker.jar:?]
[10Aug2026 12:40:44.720] [Test worker/INFO] [Iris/]: Iris object undo service ready (bounded to 32 paste(s) per player) [10Aug2026 16:36:18.627] [Test worker/INFO] [Iris/]: Iris object undo service ready (bounded to 32 paste(s) per player)
@@ -621,7 +621,14 @@ public final class BukkitCommandMessagesExtended {
); );
public static final TextKey COMMAND_STRUCTURE_PLACED_PIECES_AT_YOUR_LOCATION = TextKey.of( public static final TextKey COMMAND_STRUCTURE_PLACED_PIECES_AT_YOUR_LOCATION = TextKey.of(
"iris.bukkit.commandstructure.placed_pieces_at_your_location", "iris.bukkit.commandstructure.placed_pieces_at_your_location",
C.GREEN + "Placed '" + "{structure}" + "' (" + "{value}" + " pieces) at your location." C.GREEN + "Placed '" + "{structure}" + "' (" + "{value}" + " pieces, "
+ "{value2}" + " block changes) at your location."
);
public static final TextKey COMMAND_STRUCTURE_PLACEMENT_CHANGED_NO_BLOCKS = TextKey.of(
"iris.bukkit.commandstructure.placement_changed_no_blocks",
C.RED + "Structure '" + "{structure}" + "' assembled " + "{value}"
+ " pieces but changed 0 blocks at your location. Check that the selected variants contain "
+ "non-air blocks and that the placement is above the world's minimum height."
); );
public static final TextKey COMMAND_STUDIO_OPENING_STUDIO_PACK_SEED = TextKey.of( public static final TextKey COMMAND_STUDIO_OPENING_STUDIO_PACK_SEED = TextKey.of(
"iris.bukkit.commandstudio.opening_studio_pack_seed", "iris.bukkit.commandstudio.opening_studio_pack_seed",
@@ -991,6 +998,7 @@ public final class BukkitCommandMessagesExtended {
COMMAND_STRUCTURE_NO_IRIS_STRUCTURE_THIS_PACK_2, COMMAND_STRUCTURE_NO_IRIS_STRUCTURE_THIS_PACK_2,
COMMAND_STRUCTURE_STRUCTURE_ASSEMBLED_0_PIECES, COMMAND_STRUCTURE_STRUCTURE_ASSEMBLED_0_PIECES,
COMMAND_STRUCTURE_PLACED_PIECES_AT_YOUR_LOCATION, COMMAND_STRUCTURE_PLACED_PIECES_AT_YOUR_LOCATION,
COMMAND_STRUCTURE_PLACEMENT_CHANGED_NO_BLOCKS,
COMMAND_STUDIO_OPENING_STUDIO_PACK_SEED, COMMAND_STUDIO_OPENING_STUDIO_PACK_SEED,
COMMAND_STUDIO_PROVIDE_DIMENSION_PACK_IRIS_STD_IMPORTVANILLA_PACK_DIMENSION, COMMAND_STUDIO_PROVIDE_DIMENSION_PACK_IRIS_STD_IMPORTVANILLA_PACK_DIMENSION,
COMMAND_STUDIO_COULD_NOT_RESOLVE_PACK_DIMENSION, COMMAND_STUDIO_COULD_NOT_RESOLVE_PACK_DIMENSION,
@@ -182,7 +182,27 @@ public final class InPlaceChunkRegenerator {
world.refreshChunk(chunkX, chunkZ); world.refreshChunk(chunkX, chunkZ);
} }
static void applyBlockDiffs(Chunk chunk, ChunkSnapshot snapshot, ChunkData generated, int minHeight, int maxHeight) { public static void applyBlockDiffs(
Chunk chunk,
ChunkData generated,
int minHeight,
int maxHeight
) {
applyBlockDiffs(
chunk,
chunk.getChunkSnapshot(false, false, false),
generated,
minHeight,
maxHeight);
}
public static void applyBlockDiffs(
Chunk chunk,
ChunkSnapshot snapshot,
ChunkData generated,
int minHeight,
int maxHeight
) {
for (int x = 0; x < 16; x++) { for (int x = 0; x < 16; x++) {
for (int z = 0; z < 16; z++) { for (int z = 0; z < 16; z++) {
for (int y = minHeight; y < maxHeight; y++) { for (int y = minHeight; y < maxHeight; y++) {
@@ -46,7 +46,7 @@ public final class JigsawStudioGraphMapper {
JigsawStudioVariantCatalog catalog = catalog(data, structure, mode); JigsawStudioVariantCatalog catalog = catalog(data, structure, mode);
IrisPosition configuredCell = structure.getCellSize(); IrisPosition configuredCell = structure.getCellSize();
JigsawStudioCellDimensions dimensions = configuredCell == null JigsawStudioCellDimensions dimensions = configuredCell == null
? new JigsawStudioCellDimensions(16, 16, 16) ? new JigsawStudioCellDimensions(15, 15, 15)
: new JigsawStudioCellDimensions( : new JigsawStudioCellDimensions(
Math.max(1, configuredCell.getX()), Math.max(1, configuredCell.getX()),
Math.max(1, configuredCell.getY()), Math.max(1, configuredCell.getY()),
@@ -12,7 +12,7 @@ import java.util.Optional;
public final class JigsawStudioLayout { public final class JigsawStudioLayout {
public static final int FLOOR_Y = 64; public static final int FLOOR_Y = 64;
public static final int PLANAR_COLUMNS = 3; public static final int PLANAR_COLUMNS = 3;
public static final int PLANAR_GAP = 2; public static final int PLANAR_GAP = 1;
public static final int MAX_VARIANTS = 512; public static final int MAX_VARIANTS = 512;
public static final String SPATIAL_WORKCELL_ID = "workcell/spatial"; public static final String SPATIAL_WORKCELL_ID = "workcell/spatial";
@@ -83,6 +83,16 @@ public final class JigsawStudioSession {
return state.snapshot(workcellId); return state.snapshot(workcellId);
} }
public synchronized boolean setConnectorsVisible(String workcellId, boolean visible) {
MutableWorkcellState state = requireWorkcellState(workcellId);
if (state.connectorsVisible == visible) {
return false;
}
state.connectorsVisible = visible;
revision++;
return true;
}
public synchronized boolean replaceLayout(JigsawStudioLayout replacement) { public synchronized boolean replaceLayout(JigsawStudioLayout replacement) {
JigsawStudioLayout nextLayout = Objects.requireNonNull(replacement, "Replacement Jigsaw Studio layout"); JigsawStudioLayout nextLayout = Objects.requireNonNull(replacement, "Replacement Jigsaw Studio layout");
if (layout.mode() != nextLayout.mode()) { if (layout.mode() != nextLayout.mode()) {
@@ -107,7 +117,8 @@ public final class JigsawStudioSession {
activeVariantKey, activeVariantKey,
nextLoadGeneration(), nextLoadGeneration(),
nextMutationGeneration(), nextMutationGeneration(),
false)); false,
previous != null && previous.connectorsVisible));
} }
layout = nextLayout; layout = nextLayout;
workcells.clear(); workcells.clear();
@@ -161,7 +172,8 @@ public final class JigsawStudioSession {
targetVariantKey, targetVariantKey,
nextLoadGeneration(), nextLoadGeneration(),
nextMutationGeneration(), nextMutationGeneration(),
false)); false,
previous != null && previous.connectorsVisible));
} }
layout = nextLayout; layout = nextLayout;
workcells.clear(); workcells.clear();
@@ -419,6 +431,7 @@ public final class JigsawStudioSession {
variantKey, variantKey,
nextLoadGeneration(), nextLoadGeneration(),
nextMutationGeneration(), nextMutationGeneration(),
false,
false)); false));
} }
} }
@@ -520,7 +533,8 @@ public final class JigsawStudioSession {
long mutationGeneration, long mutationGeneration,
boolean dirty, boolean dirty,
boolean saveInProgress, boolean saveInProgress,
boolean switchInProgress boolean switchInProgress,
boolean connectorsVisible
) { ) {
} }
@@ -635,21 +649,29 @@ public final class JigsawStudioSession {
private long saveGeneration; private long saveGeneration;
private boolean switchInProgress; private boolean switchInProgress;
private long switchGeneration; private long switchGeneration;
private boolean connectorsVisible;
private MutableWorkcellState( private MutableWorkcellState(
String activeVariantKey, String activeVariantKey,
long loadGeneration, long loadGeneration,
long mutationGeneration, long mutationGeneration,
boolean dirty boolean dirty,
boolean connectorsVisible
) { ) {
this.activeVariantKey = activeVariantKey; this.activeVariantKey = activeVariantKey;
this.loadGeneration = loadGeneration; this.loadGeneration = loadGeneration;
this.mutationGeneration = mutationGeneration; this.mutationGeneration = mutationGeneration;
this.dirty = dirty; this.dirty = dirty;
this.connectorsVisible = connectorsVisible;
} }
private MutableWorkcellState copy() { private MutableWorkcellState copy() {
return new MutableWorkcellState(activeVariantKey, loadGeneration, mutationGeneration, dirty); return new MutableWorkcellState(
activeVariantKey,
loadGeneration,
mutationGeneration,
dirty,
connectorsVisible);
} }
private WorkcellSnapshot snapshot(String workcellId) { private WorkcellSnapshot snapshot(String workcellId) {
@@ -660,7 +682,8 @@ public final class JigsawStudioSession {
mutationGeneration, mutationGeneration,
dirty, dirty,
saveInProgress, saveInProgress,
switchInProgress); switchInProgress,
connectorsVisible);
} }
} }
} }
@@ -0,0 +1,513 @@
package art.arcane.iris.core.service;
import art.arcane.iris.core.structure.authoring.StructureBackend;
import art.arcane.iris.core.structure.authoring.StructureCapability;
import art.arcane.iris.core.structure.authoring.StructureHash;
import art.arcane.iris.core.structure.authoring.StructureKey;
import art.arcane.iris.core.structure.authoring.StructureLoss;
import art.arcane.iris.core.structure.authoring.StructureOwnershipManifest;
import art.arcane.iris.core.structure.authoring.StructureResourceBundle;
import art.arcane.iris.core.structure.authoring.StructureSource;
import art.arcane.iris.core.structure.authoring.StructureTransactionWriter;
import art.arcane.iris.core.structure.authoring.StructureWriteOptions;
import art.arcane.iris.core.structure.authoring.StructureWriteResult;
import art.arcane.iris.engine.framework.structure.StructureResourceBundleGraphCompiler;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.charset.StandardCharsets;
import java.nio.file.AtomicMoveNotSupportedException;
import java.nio.file.Files;
import java.nio.file.LinkOption;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
import java.nio.file.StandardOpenOption;
import java.util.ArrayList;
import java.util.Base64;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.TreeMap;
import java.util.TreeSet;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.locks.ReentrantLock;
final class JigsawStudioHistoryStore {
static final int MAX_ITERATIONS = 5;
private static final int SCHEMA_VERSION = 1;
private static final long MAX_HISTORY_BYTES = 512L * 1024L * 1024L;
private static final Gson GSON = new GsonBuilder().setPrettyPrinting().create();
private static final ConcurrentMap<Path, ReentrantLock> LOCKS = new ConcurrentHashMap<>();
private final Path packRoot;
private final StructureKey structureKey;
private final StructureTransactionWriter writer;
private final Path historyPath;
private final ReentrantLock lock;
JigsawStudioHistoryStore(Path packRoot, String structureKey) {
this.packRoot = canonicalPackRoot(packRoot);
this.structureKey = new StructureKey(
"iris",
Objects.requireNonNull(structureKey, "Jigsaw Studio history structure key"));
writer = new StructureTransactionWriter(this.packRoot);
String identityHash = StructureHash.sha256(this.structureKey.value().getBytes(StandardCharsets.UTF_8));
historyPath = this.packRoot.resolve(".iris/jigsaw-history/key-" + identityHash + ".json").normalize();
if (!historyPath.startsWith(this.packRoot)) {
throw new IllegalArgumentException("Jigsaw Studio history path escapes its pack root");
}
lock = LOCKS.computeIfAbsent(historyPath, ignored -> new ReentrantLock());
}
Snapshot snapshotCurrent(String pieceKey) throws IOException {
lock.lock();
try {
return readCurrentSnapshot(pieceKey);
} finally {
lock.unlock();
}
}
int append(Snapshot snapshot) throws IOException {
Objects.requireNonNull(snapshot, "Jigsaw Studio history snapshot");
lock.lock();
try {
HistoryDocument document = readDocument();
if (!document.structure().equals(structureKey.value())) {
throw new IOException("Jigsaw Studio history belongs to " + document.structure());
}
ArrayList<HistoryIteration> iterations = new ArrayList<>(document.iterations());
HistoryIteration iteration = snapshot.iteration();
if (!iterations.isEmpty() && iterations.getLast().sameState(iteration)) {
return iterations.size();
}
iterations.add(iteration);
while (iterations.size() > MAX_ITERATIONS) {
iterations.removeFirst();
}
TreeMap<String, String> blobs = new TreeMap<>(document.blobs());
for (Map.Entry<String, byte[]> resource : snapshot.resources().entrySet()) {
String hash = StructureHash.sha256(resource.getValue());
blobs.putIfAbsent(hash, Base64.getEncoder().encodeToString(resource.getValue()));
}
retainReferencedBlobs(blobs, iterations);
writeDocument(new HistoryDocument(
SCHEMA_VERSION,
structureKey.value(),
List.copyOf(iterations),
Map.copyOf(blobs)));
return iterations.size();
} finally {
lock.unlock();
}
}
UndoResult undoLatest() throws IOException {
lock.lock();
try {
HistoryDocument document = readDocument();
if (document.iterations().isEmpty()) {
return UndoResult.unavailable();
}
HistoryIteration iteration = document.iterations().getLast();
StructureResourceBundle bundle = restoreBundle(iteration, document.blobs());
StructureResourceBundleGraphCompiler.requireViable(bundle);
Path manifestPath = writer.ownershipManifestPath(structureKey);
byte[] currentManifest = readRegularFile(manifestPath, "ownership manifest");
StructureWriteResult result = writer.write(
bundle,
StructureWriteOptions.overwriteExpected(StructureHash.sha256(currentManifest)));
if (!result.successful()) {
return new UndoResult(
false,
true,
document.iterations().size(),
iteration.pieceKey(),
result,
"");
}
ArrayList<HistoryIteration> remaining = new ArrayList<>(document.iterations());
remaining.removeLast();
TreeMap<String, String> blobs = new TreeMap<>(document.blobs());
retainReferencedBlobs(blobs, remaining);
String warning = "";
try {
if (remaining.isEmpty()) {
Files.deleteIfExists(historyPath);
forceDirectory(historyPath.getParent());
} else {
writeDocument(new HistoryDocument(
SCHEMA_VERSION,
structureKey.value(),
List.copyOf(remaining),
Map.copyOf(blobs)));
}
} catch (IOException historyFailure) {
warning = historyFailure.getMessage() == null
? historyFailure.getClass().getSimpleName()
: historyFailure.getMessage();
}
return new UndoResult(
true,
true,
remaining.size(),
iteration.pieceKey(),
result,
warning);
} finally {
lock.unlock();
}
}
int availableIterations() throws IOException {
lock.lock();
try {
return readDocument().iterations().size();
} finally {
lock.unlock();
}
}
void delete() throws IOException {
lock.lock();
try {
if (Files.deleteIfExists(historyPath)) {
forceDirectory(historyPath.getParent());
}
} finally {
lock.unlock();
}
}
Path historyPath() {
return historyPath;
}
private Snapshot readCurrentSnapshot(String pieceKey) throws IOException {
Path manifestPath = writer.ownershipManifestPath(structureKey);
byte[] manifestContent = readRegularFile(manifestPath, "ownership manifest");
StructureOwnershipManifest manifest;
try {
manifest = StructureOwnershipManifest.fromJson(manifestContent);
} catch (RuntimeException exception) {
throw new IOException("Jigsaw Studio history cannot parse the ownership manifest", exception);
}
if (!manifest.structure().equals(structureKey)) {
throw new IOException("Jigsaw Studio ownership manifest belongs to " + manifest.structure());
}
TreeMap<String, byte[]> resources = new TreeMap<>();
for (Map.Entry<String, String> resource : manifest.resourceHashes().entrySet()) {
Path resourcePath = resolveOwnedResource(resource.getKey());
byte[] content = readRegularFile(resourcePath, "owned resource " + resource.getKey());
String actualHash = StructureHash.sha256(content);
if (!resource.getValue().equals(actualHash)) {
throw new IOException("Jigsaw Studio owned resource changed before history capture: "
+ resource.getKey());
}
resources.put(resource.getKey(), content);
}
return new Snapshot(
new HistoryIteration(
System.currentTimeMillis(),
Objects.requireNonNull(pieceKey, "Jigsaw Studio history piece key"),
manifest.source(),
manifest.backend(),
manifest.capabilities(),
manifest.losses(),
manifest.resourceHashes()),
resources);
}
private StructureResourceBundle restoreBundle(
HistoryIteration iteration,
Map<String, String> blobs
) throws IOException {
StructureResourceBundle.Builder builder = StructureResourceBundle.builder(structureKey)
.source(iteration.source())
.backend(iteration.backend())
.capabilities(iteration.capabilities())
.losses(iteration.losses());
for (Map.Entry<String, String> resource : iteration.resourceHashes().entrySet()) {
String encoded = blobs.get(resource.getValue());
if (encoded == null) {
throw new IOException("Jigsaw Studio history is missing resource blob " + resource.getValue());
}
byte[] content;
try {
content = Base64.getDecoder().decode(encoded);
} catch (IllegalArgumentException exception) {
throw new IOException("Jigsaw Studio history contains invalid resource data", exception);
}
if (!resource.getValue().equals(StructureHash.sha256(content))) {
throw new IOException("Jigsaw Studio history resource hash does not match "
+ resource.getKey());
}
builder.resource(resource.getKey(), content);
}
return builder.build();
}
private HistoryDocument readDocument() throws IOException {
if (!Files.exists(historyPath, LinkOption.NOFOLLOW_LINKS)) {
return HistoryDocument.empty(structureKey.value());
}
byte[] content = readRegularFile(historyPath, "history file");
if (content.length > MAX_HISTORY_BYTES) {
throw new IOException("Jigsaw Studio history exceeds " + MAX_HISTORY_BYTES + " bytes");
}
HistoryDocument document;
try {
document = GSON.fromJson(new String(content, StandardCharsets.UTF_8), HistoryDocument.class);
} catch (RuntimeException exception) {
throw new IOException("Jigsaw Studio history is invalid", exception);
}
if (document == null || document.schemaVersion() != SCHEMA_VERSION) {
throw new IOException("Unsupported Jigsaw Studio history schema");
}
HistoryDocument validated;
try {
validated = document.validated();
} catch (RuntimeException exception) {
throw new IOException("Jigsaw Studio history is invalid", exception);
}
if (!validated.structure().equals(structureKey.value())) {
throw new IOException("Jigsaw Studio history belongs to " + validated.structure());
}
return validated;
}
private void writeDocument(HistoryDocument document) throws IOException {
HistoryDocument validated = document.validated();
byte[] content = (GSON.toJson(validated) + "\n").getBytes(StandardCharsets.UTF_8);
if (content.length > MAX_HISTORY_BYTES) {
throw new IOException("Jigsaw Studio history exceeds " + MAX_HISTORY_BYTES + " bytes");
}
Path historyRoot = historyPath.getParent();
Files.createDirectories(historyRoot);
rejectSymbolicPath(historyRoot);
Path temporary = historyRoot.resolve(historyPath.getFileName() + "."
+ UUID.randomUUID() + ".tmp").normalize();
try {
try (FileChannel channel = FileChannel.open(
temporary,
StandardOpenOption.CREATE_NEW,
StandardOpenOption.WRITE)) {
ByteBuffer buffer = ByteBuffer.wrap(content);
while (buffer.hasRemaining()) {
channel.write(buffer);
}
channel.force(true);
}
try {
Files.move(
temporary,
historyPath,
StandardCopyOption.ATOMIC_MOVE,
StandardCopyOption.REPLACE_EXISTING);
} catch (AtomicMoveNotSupportedException exception) {
Files.move(temporary, historyPath, StandardCopyOption.REPLACE_EXISTING);
}
forceDirectory(historyRoot);
} finally {
Files.deleteIfExists(temporary);
}
}
private Path resolveOwnedResource(String relativePath) throws IOException {
StructureResourceBundle.validateRelativePath(relativePath);
Path resource = packRoot.resolve(relativePath).normalize();
if (!resource.startsWith(packRoot)) {
throw new IOException("Jigsaw Studio history resource escapes its pack root: " + relativePath);
}
rejectSymbolicPath(resource.getParent());
return resource;
}
private static byte[] readRegularFile(Path path, String kind) throws IOException {
if (!Files.isRegularFile(path, LinkOption.NOFOLLOW_LINKS)) {
throw new IOException("Jigsaw Studio " + kind + " is missing or not a regular file: " + path);
}
return Files.readAllBytes(path);
}
private static void retainReferencedBlobs(
Map<String, String> blobs,
List<HistoryIteration> iterations
) {
Set<String> retained = new TreeSet<>();
for (HistoryIteration iteration : iterations) {
retained.addAll(iteration.resourceHashes().values());
}
blobs.keySet().retainAll(retained);
}
private void rejectSymbolicPath(Path path) throws IOException {
Path current = path;
while (current != null && current.startsWith(packRoot)) {
if (Files.isSymbolicLink(current)) {
throw new IOException("Jigsaw Studio history path contains a symbolic link: " + current);
}
if (current.equals(packRoot)) {
return;
}
current = current.getParent();
}
throw new IOException("Jigsaw Studio history path escapes its pack root: " + path);
}
private static Path canonicalPackRoot(Path root) {
Path normalized = Objects.requireNonNull(root, "Jigsaw Studio history pack root")
.toAbsolutePath().normalize();
try {
return normalized.toRealPath();
} catch (IOException exception) {
throw new IllegalArgumentException("Jigsaw Studio history pack root is unavailable: "
+ normalized, exception);
}
}
private static void forceDirectory(Path directory) throws IOException {
try (FileChannel channel = FileChannel.open(directory, StandardOpenOption.READ)) {
channel.force(true);
}
}
record Snapshot(HistoryIteration iteration, Map<String, byte[]> resources) {
Snapshot {
Objects.requireNonNull(iteration, "Jigsaw Studio history iteration");
Objects.requireNonNull(resources, "Jigsaw Studio history resources");
LinkedHashMap<String, byte[]> copies = new LinkedHashMap<>();
for (Map.Entry<String, byte[]> resource : resources.entrySet()) {
copies.put(resource.getKey(), resource.getValue().clone());
}
resources = Map.copyOf(copies);
}
boolean matches(StructureResourceBundle bundle) {
if (!iteration.source().equals(bundle.source())
|| iteration.backend() != bundle.backend()
|| !Set.copyOf(iteration.capabilities()).equals(bundle.capabilities())
|| !iteration.losses().equals(bundle.losses())
|| iteration.resourceHashes().size() != bundle.resources().size()) {
return false;
}
for (Map.Entry<String, StructureResourceBundle.Resource> resource
: bundle.resources().entrySet()) {
if (!resource.getValue().contentHash().equals(
iteration.resourceHashes().get(resource.getKey()))) {
return false;
}
}
return true;
}
}
record UndoResult(
boolean successful,
boolean available,
int remainingIterations,
String pieceKey,
StructureWriteResult writeResult,
String warning
) {
UndoResult {
pieceKey = pieceKey == null ? "" : pieceKey;
warning = warning == null ? "" : warning;
}
static UndoResult unavailable() {
return new UndoResult(false, false, 0, "", null, "");
}
}
private record HistoryDocument(
int schemaVersion,
String structure,
List<HistoryIteration> iterations,
Map<String, String> blobs
) {
private HistoryDocument {
structure = structure == null ? "" : structure;
iterations = iterations == null ? List.of() : List.copyOf(iterations);
blobs = blobs == null ? Map.of() : Map.copyOf(blobs);
}
private static HistoryDocument empty(String structure) {
return new HistoryDocument(SCHEMA_VERSION, structure, List.of(), Map.of());
}
private HistoryDocument validated() throws IOException {
if (schemaVersion != SCHEMA_VERSION || structure.isBlank()) {
throw new IOException("Jigsaw Studio history header is invalid");
}
if (iterations.size() > MAX_ITERATIONS) {
throw new IOException("Jigsaw Studio history contains too many iterations");
}
TreeMap<String, String> validatedBlobs = new TreeMap<>();
for (Map.Entry<String, String> blob : blobs.entrySet()) {
if (!StructureHash.isSha256(blob.getKey()) || blob.getValue() == null) {
throw new IOException("Jigsaw Studio history contains an invalid resource blob");
}
validatedBlobs.put(blob.getKey(), blob.getValue());
}
return new HistoryDocument(
SCHEMA_VERSION,
structure,
List.copyOf(iterations),
Map.copyOf(validatedBlobs));
}
}
private record HistoryIteration(
long recordedAtEpochMilli,
String pieceKey,
StructureSource source,
StructureBackend backend,
List<StructureCapability> capabilities,
List<StructureLoss> losses,
Map<String, String> resourceHashes
) {
private HistoryIteration {
pieceKey = Objects.requireNonNull(pieceKey, "Jigsaw Studio history piece key").trim();
if (pieceKey.isEmpty()) {
throw new IllegalArgumentException("Jigsaw Studio history piece key cannot be empty");
}
Objects.requireNonNull(source, "Jigsaw Studio history source");
Objects.requireNonNull(backend, "Jigsaw Studio history backend");
capabilities = List.copyOf(Objects.requireNonNull(
capabilities,
"Jigsaw Studio history capabilities"));
losses = List.copyOf(Objects.requireNonNull(losses, "Jigsaw Studio history losses"));
TreeMap<String, String> hashes = new TreeMap<>();
for (Map.Entry<String, String> resource : Objects.requireNonNull(
resourceHashes,
"Jigsaw Studio history resource hashes").entrySet()) {
String relativePath = StructureResourceBundle.validateRelativePath(resource.getKey());
if (!StructureHash.isSha256(resource.getValue())) {
throw new IllegalArgumentException("Invalid Jigsaw Studio history resource hash");
}
hashes.put(relativePath, resource.getValue());
}
if (hashes.isEmpty()) {
throw new IllegalArgumentException("Jigsaw Studio history iteration cannot be empty");
}
resourceHashes = Map.copyOf(hashes);
}
private boolean sameState(HistoryIteration other) {
return source.equals(other.source)
&& backend == other.backend
&& pieceKey.equals(other.pieceKey)
&& capabilities.equals(other.capabilities)
&& losses.equals(other.losses)
&& resourceHashes.equals(other.resourceHashes);
}
}
}
@@ -345,7 +345,9 @@ public final class JigsawStudioMenuController {
element.addLore(ChatColor.GRAY + "Loaded: " element.addLore(ChatColor.GRAY + "Loaded: "
+ (active == null ? "None" : safe(active.displayName()))); + (active == null ? "None" : safe(active.displayName())));
element.addLore(workcellStatus(workcell)); element.addLore(workcellStatus(workcell));
element.addLore(ChatColor.YELLOW + "Left-click to select"); element.addLore(ChatColor.GRAY + "Connector blocks: "
+ (workcell.connectorsVisible() ? "Visible" : "Hidden"));
element.addLore(ChatColor.YELLOW + "Left-click to select and teleport");
element.addLore(ChatColor.YELLOW + "Right-click for workcell settings"); element.addLore(ChatColor.YELLOW + "Right-click for workcell settings");
if (workcell.dirty() && !workcell.saving()) { if (workcell.dirty() && !workcell.saving()) {
element.addLore(ChatColor.GOLD + "Shift-left: Flush Autosave Now"); element.addLore(ChatColor.GOLD + "Shift-left: Flush Autosave Now");
@@ -564,6 +566,34 @@ public final class JigsawStudioMenuController {
window.setElement(0, 1, spatial); window.setElement(0, 1, spatial);
} }
UIElement connectors = element(
"settings-connectors",
workcell.connectorsVisible() ? Material.JIGSAW : Material.STRUCTURE_VOID,
workcell.connectorsVisible()
? ChatColor.GREEN + "Connector Blocks Visible"
: ChatColor.YELLOW + "Connector Blocks Hidden");
connectors.addLore(ChatColor.GRAY + "Hidden connectors retain their metadata and final block state.");
connectors.addLore(ChatColor.YELLOW + "Left-click to "
+ (workcell.connectorsVisible() ? "hide" : "show") + " connector blocks");
connectors.onLeftClick(clicked -> toggleConnectorBlocks(
window.getViewer(),
state.requestId(),
workcell.stableId(),
!workcell.connectorsVisible()));
window.setElement(2, 1, connectors);
UIElement resetConnectors = element(
"settings-reset-connectors",
Material.RECOVERY_COMPASS,
ChatColor.AQUA + "Reset Connector Blocks");
resetConnectors.addLore(ChatColor.GRAY + "Restore every connector in this workcell from disk.");
resetConnectors.addLore(ChatColor.GRAY + "Other edited blocks are left unchanged.");
resetConnectors.onLeftClick(clicked -> resetConnectorBlocks(
window.getViewer(),
state.requestId(),
workcell.stableId()));
window.setElement(4, 1, resetConnectors);
window.setElement(-2, 2, axisElement( window.setElement(-2, 2, axisElement(
window, window,
state, state,
@@ -588,6 +618,17 @@ public final class JigsawStudioMenuController {
window.getViewer(), state.requestId(), workcell.stableId(), 0)); window.getViewer(), state.requestId(), workcell.stableId(), 0));
window.setElement(-4, 5, footerBack); window.setElement(-4, 5, footerBack);
UIElement undo = element(
"settings-undo",
Material.CLOCK,
ChatColor.LIGHT_PURPLE + "Undo Last Autosave");
undo.addLore(ChatColor.GRAY + "Restore the previous owned graph iteration.");
undo.addLore(ChatColor.GRAY + "Up to five autosave iterations are retained on disk.");
undo.onLeftClick(clicked -> undoAutosave(
window.getViewer(),
state.requestId()));
window.setElement(0, 5, undo);
if (workcell.dirty() && !workcell.saving()) { if (workcell.dirty() && !workcell.saving()) {
UIElement saveNow = element( UIElement saveNow = element(
"save-now", "save-now",
@@ -1432,9 +1473,43 @@ public final class JigsawStudioMenuController {
if (matchingState(player, requestId, true).isEmpty()) { if (matchingState(player, requestId, true).isEmpty()) {
return; return;
} }
if (actions.selectWorkcell(player, workcellId)) { if (actions.teleportToWorkcell(player, workcellId)) {
clearConfirmations(player.getUniqueId()); clearConfirmations(player.getUniqueId());
refreshMain(player, requestId, workcellId, 0); closeAfterAction(player);
}
}
private void toggleConnectorBlocks(
Player player,
UUID requestId,
String workcellId,
boolean visible
) {
Optional<JigsawStudioMenuState> current = matchingState(player, requestId, true);
if (current.isEmpty() || current.get().workcell(workcellId) == null) {
return;
}
if (actions.setConnectorBlocksVisible(player, workcellId, visible)) {
closeAfterAction(player);
}
}
private void resetConnectorBlocks(Player player, UUID requestId, String workcellId) {
Optional<JigsawStudioMenuState> current = matchingState(player, requestId, true);
if (current.isEmpty() || current.get().workcell(workcellId) == null) {
return;
}
if (actions.resetConnectorBlocks(player, workcellId)) {
closeAfterAction(player);
}
}
private void undoAutosave(Player player, UUID requestId) {
if (matchingState(player, requestId, true).isEmpty()) {
return;
}
if (actions.undoAutosave(player)) {
closeAfterAction(player);
} }
} }
@@ -2843,6 +2918,14 @@ public final class JigsawStudioMenuController {
boolean selectWorkcell(Player player, String workcellId); boolean selectWorkcell(Player player, String workcellId);
boolean teleportToWorkcell(Player player, String workcellId);
boolean setConnectorBlocksVisible(Player player, String workcellId, boolean visible);
boolean resetConnectorBlocks(Player player, String workcellId);
boolean undoAutosave(Player player);
boolean switchVariant(Player player, String workcellId, String pieceKey, boolean discardDirty); boolean switchVariant(Player player, String workcellId, String pieceKey, boolean discardDirty);
boolean createVariant(Player player, String workcellId, boolean duplicateActive); boolean createVariant(Player player, String workcellId, boolean duplicateActive);
@@ -156,6 +156,7 @@ public record JigsawStudioMenuState(
boolean dirty, boolean dirty,
boolean saving, boolean saving,
boolean loading, boolean loading,
boolean connectorsVisible,
List<Variant> variants List<Variant> variants
) { ) {
public Workcell { public Workcell {
File diff suppressed because it is too large Load Diff
@@ -71,6 +71,7 @@ import java.util.LinkedHashSet;
import java.util.List; import java.util.List;
import java.util.Locale; import java.util.Locale;
import java.util.Objects; import java.util.Objects;
import java.util.UUID;
import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletableFuture;
import java.util.function.Consumer; import java.util.function.Consumer;
import java.util.function.Supplier; import java.util.function.Supplier;
@@ -579,7 +580,7 @@ public class StudioSVC implements IrisService {
String dimension, String dimension,
Consumer<World> onDone Consumer<World> onDone
) { ) {
return closeActiveProject().handle((closeResult, closeThrowable) -> { return closeActiveProjectForReplacement(sender).handle((closeResult, closeThrowable) -> {
if (closeThrowable != null) { if (closeThrowable != null) {
IrisLogging.reportError("Failed while closing an existing studio project before opening \"" + dimension + "\".", closeThrowable); IrisLogging.reportError("Failed while closing an existing studio project before opening \"" + dimension + "\".", closeThrowable);
J.s(() -> sender.sendMessage(IrisLanguage.text( J.s(() -> sender.sendMessage(IrisLanguage.text(
@@ -662,6 +663,29 @@ public class StudioSVC implements IrisService {
return studioTransitions.submit(this::closeActiveProject); return studioTransitions.submit(this::closeActiveProject);
} }
CompletableFuture<StudioOpenCoordinator.StudioCloseResult> closeActiveProjectForReplacement(
VolmitSender sender
) {
IrisProject project = activeProject;
if (project == null) {
return closeActiveProject();
}
JigsawStudioActivation.Request request = JigsawStudioActivation.getRequest(project.getName());
if (request == null || !sender.isPlayer()) {
return closeActiveProject();
}
UUID ownerId = sender.player().getUniqueId();
return JigsawStudioService.get()
.awaitCloseForReplacement(request.requestId(), ownerId)
.thenCompose(ignored -> {
if (activeProject != project) {
return CompletableFuture.failedFuture(new IllegalStateException(
"The active Studio project changed while replacement was waiting to close."));
}
return closeActiveProject();
});
}
private CompletableFuture<StudioOpenCoordinator.StudioCloseResult> closeActiveProject() { private CompletableFuture<StudioOpenCoordinator.StudioCloseResult> closeActiveProject() {
IrisProject project = activeProject; IrisProject project = activeProject;
if (project == null) { if (project == null) {
@@ -64,7 +64,7 @@ public class IrisStructure extends IrisRegistrant {
private IrisJigsawBranchFailurePolicy branchFailurePolicy = IrisJigsawBranchFailurePolicy.FAIL_ASSEMBLY; private IrisJigsawBranchFailurePolicy branchFailurePolicy = IrisJigsawBranchFailurePolicy.FAIL_ASSEMBLY;
@Desc("Default Studio cell dimensions. Legacy planar structures use this value for every workcell when planarWorkcells is empty.") @Desc("Default Studio cell dimensions. Legacy planar structures use this value for every workcell when planarWorkcells is empty.")
private IrisPosition cellSize = new IrisPosition(16, 16, 16); private IrisPosition cellSize = new IrisPosition(15, 15, 15);
@Desc("Optional author-facing name for the single spatial Jigsaw Studio workcell. Spatial is shown when this is blank.") @Desc("Optional author-facing name for the single spatial Jigsaw Studio workcell. Spatial is shown when this is blank.")
private String spatialWorkcellDisplayName = ""; private String spatialWorkcellDisplayName = "";
@@ -35,9 +35,11 @@ import art.arcane.volmlib.util.collection.KList;
import org.bukkit.World; import org.bukkit.World;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.HashSet;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Objects; import java.util.Objects;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap; import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicBoolean;
@@ -184,7 +186,7 @@ public final class JigsawStudioGenerator extends EnginedStudioGenerator {
} }
} }
void paintChunk(TerrainChunk terrainChunk, int chunkX, int chunkZ) { public void paintChunk(TerrainChunk terrainChunk, int chunkX, int chunkZ) {
Objects.requireNonNull(terrainChunk, "Jigsaw Studio terrain chunk"); Objects.requireNonNull(terrainChunk, "Jigsaw Studio terrain chunk");
int floorY = Math.max(terrainChunk.getMinHeight(), JigsawStudioLayout.FLOOR_Y); int floorY = Math.max(terrainChunk.getMinHeight(), JigsawStudioLayout.FLOOR_Y);
if (floorY >= terrainChunk.getMaxHeight()) { if (floorY >= terrainChunk.getMaxHeight()) {
@@ -217,7 +219,16 @@ public final class JigsawStudioGenerator extends EnginedStudioGenerator {
continue; continue;
} }
paintObject(terrainChunk, workcell, renderedBay, chunkWorldX, chunkWorldZ); paintObject(terrainChunk, workcell, renderedBay, chunkWorldX, chunkWorldZ);
paintConnectors(terrainChunk, workcell, renderedBay, chunkWorldX, chunkWorldZ); if (session.workcellSnapshot(workcell.stableId()).connectorsVisible()) {
paintConnectors(terrainChunk, workcell, renderedBay, chunkWorldX, chunkWorldZ);
} else {
paintHiddenConnectorFinalStates(
terrainChunk,
workcell,
renderedBay,
chunkWorldX,
chunkWorldZ);
}
} }
} }
@@ -632,6 +643,34 @@ public final class JigsawStudioGenerator extends EnginedStudioGenerator {
} }
} }
private void paintHiddenConnectorFinalStates(
TerrainChunk terrainChunk,
JigsawStudioBay workcell,
RenderedBay renderedBay,
int chunkWorldX,
int chunkWorldZ
) {
Set<RenderedPosition> occupied = new HashSet<>(renderedBay.blocks().size());
for (RenderedBlock block : renderedBay.blocks()) {
occupied.add(new RenderedPosition(block.x(), block.y(), block.z()));
}
JigsawStudioBounds bounds = workcell.bounds();
for (RenderedConnector connector : renderedBay.connectors()) {
if (occupied.contains(new RenderedPosition(connector.x(), connector.y(), connector.z()))) {
continue;
}
PlatformBlockState finalState = B.getStateOrNull(connector.connector().getFinalState(), false);
setWorldBlock(
terrainChunk,
bounds.originX() + connector.x(),
bounds.originY() + connector.y(),
bounds.originZ() + connector.z(),
finalState == null ? invalidMarker : finalState,
chunkWorldX,
chunkWorldZ);
}
}
private void paintInvalidBay( private void paintInvalidBay(
TerrainChunk terrainChunk, TerrainChunk terrainChunk,
JigsawStudioBay workcell, JigsawStudioBay workcell,
@@ -871,4 +910,7 @@ public final class JigsawStudioGenerator extends EnginedStudioGenerator {
record RotatedPosition(int x, int y, int z) { record RotatedPosition(int x, int y, int z) {
} }
private record RenderedPosition(int x, int y, int z) {
}
} }
@@ -0,0 +1,54 @@
package art.arcane.iris.util.common.director.specialhandlers;
import art.arcane.iris.core.loader.IrisData;
import art.arcane.iris.core.pack.PackDirectoryResolver;
import art.arcane.iris.spi.IrisPlatforms;
import art.arcane.iris.util.common.director.DirectorParameterHandler;
import art.arcane.volmlib.util.collection.KList;
import art.arcane.volmlib.util.director.exceptions.DirectorParsingException;
import java.io.File;
import java.util.LinkedHashSet;
import java.util.Set;
public final class IrisStructureHandler implements DirectorParameterHandler<String> {
@Override
public KList<String> getPossibilities() {
Set<String> keys = new LinkedHashSet<>();
IrisData activeData = data();
if (activeData != null) {
addStructureKeys(keys, activeData);
}
for (File pack : PackDirectoryResolver.listVisiblePackDirectories(
IrisPlatforms.get().dataFolder("packs"))) {
addStructureKeys(keys, IrisData.get(pack));
}
return new KList<>(keys);
}
@Override
public String toString(String value) {
return value == null ? "" : value;
}
@Override
public String parse(String input, boolean force) throws DirectorParsingException {
for (String option : getPossibilities(input)) {
if (option.equalsIgnoreCase(input)) {
return option;
}
}
throw new DirectorParsingException("Unable to find Iris structure \"" + input + "\"");
}
@Override
public boolean supports(Class<?> type) {
return type == String.class;
}
private static void addStructureKeys(Set<String> keys, IrisData data) {
for (String key : data.getStructureLoader().getPossibleKeys()) {
keys.add(key);
}
}
}
+2 -1
View File
@@ -260,7 +260,8 @@
"iris.bukkit.commandstructure.could_not_resolve_pack_dimension_5": "§cDer Pack für Dimension {value} konnte nicht aufgelöst werden", "iris.bukkit.commandstructure.could_not_resolve_pack_dimension_5": "§cDer Pack für Dimension {value} konnte nicht aufgelöst werden",
"iris.bukkit.commandstructure.no_iris_structure_this_pack_2": "§cKeine Iris-Struktur '{structure}' in diesem Pack", "iris.bukkit.commandstructure.no_iris_structure_this_pack_2": "§cKeine Iris-Struktur '{structure}' in diesem Pack",
"iris.bukkit.commandstructure.structure_assembled_0_pieces": "§cStruktur '{structure}' wurde aus 0 Teilen zusammengesetzt", "iris.bukkit.commandstructure.structure_assembled_0_pieces": "§cStruktur '{structure}' wurde aus 0 Teilen zusammengesetzt",
"iris.bukkit.commandstructure.placed_pieces_at_your_location": "§a'{structure}' ({value} Teile) an deiner Position platziert.", "iris.bukkit.commandstructure.placed_pieces_at_your_location": "§a'{structure}' ({value} Teile) an deiner Position platziert. ({value2} block changes)",
"iris.bukkit.commandstructure.placement_changed_no_blocks": "§cStructure '{structure}' assembled {value} pieces but changed 0 blocks at your location. Check that the selected variants contain non-air blocks and that the placement is above the world's minimum height.",
"iris.bukkit.commandstudio.opening_studio_pack_seed": "§aStudio für den Pack \"{value}\" wird geöffnet (Seed: {seed})", "iris.bukkit.commandstudio.opening_studio_pack_seed": "§aStudio für den Pack \"{value}\" wird geöffnet (Seed: {seed})",
"iris.bukkit.commandstudio.provide_dimension_pack_iris_std_importvanilla_pack_dimension": "§cGib ein Dimensions-Pack an: /iris std importvanilla pack=<dimension>", "iris.bukkit.commandstudio.provide_dimension_pack_iris_std_importvanilla_pack_dimension": "§cGib ein Dimensions-Pack an: /iris std importvanilla pack=<dimension>",
"iris.bukkit.commandstudio.could_not_resolve_pack_dimension": "§cDer Pack für Dimension {value} konnte nicht aufgelöst werden", "iris.bukkit.commandstudio.could_not_resolve_pack_dimension": "§cDer Pack für Dimension {value} konnte nicht aufgelöst werden",
+2 -1
View File
@@ -260,7 +260,8 @@
"iris.bukkit.commandstructure.could_not_resolve_pack_dimension_5": "§cNo se pudo resolver el pack de la dimensión {value}", "iris.bukkit.commandstructure.could_not_resolve_pack_dimension_5": "§cNo se pudo resolver el pack de la dimensión {value}",
"iris.bukkit.commandstructure.no_iris_structure_this_pack_2": "§cNo existe la estructura de Iris '{structure}' en este pack", "iris.bukkit.commandstructure.no_iris_structure_this_pack_2": "§cNo existe la estructura de Iris '{structure}' en este pack",
"iris.bukkit.commandstructure.structure_assembled_0_pieces": "§cLa estructura '{structure}' ensambló 0 piezas", "iris.bukkit.commandstructure.structure_assembled_0_pieces": "§cLa estructura '{structure}' ensambló 0 piezas",
"iris.bukkit.commandstructure.placed_pieces_at_your_location": "§aSe colocó '{structure}' ({value} piezas) en tu ubicación.", "iris.bukkit.commandstructure.placed_pieces_at_your_location": "§aSe colocó '{structure}' ({value} piezas) en tu ubicación. ({value2} block changes)",
"iris.bukkit.commandstructure.placement_changed_no_blocks": "§cStructure '{structure}' assembled {value} pieces but changed 0 blocks at your location. Check that the selected variants contain non-air blocks and that the placement is above the world's minimum height.",
"iris.bukkit.commandstudio.opening_studio_pack_seed": "§aAbriendo Studio para el pack \"{value}\" (semilla: {seed})", "iris.bukkit.commandstudio.opening_studio_pack_seed": "§aAbriendo Studio para el pack \"{value}\" (semilla: {seed})",
"iris.bukkit.commandstudio.provide_dimension_pack_iris_std_importvanilla_pack_dimension": "§cIndica un pack de dimensión: /iris std importvanilla pack=<dimension>", "iris.bukkit.commandstudio.provide_dimension_pack_iris_std_importvanilla_pack_dimension": "§cIndica un pack de dimensión: /iris std importvanilla pack=<dimension>",
"iris.bukkit.commandstudio.could_not_resolve_pack_dimension": "§cNo se pudo resolver el pack de la dimensión {value}", "iris.bukkit.commandstudio.could_not_resolve_pack_dimension": "§cNo se pudo resolver el pack de la dimensión {value}",
+2 -1
View File
@@ -260,7 +260,8 @@
"iris.bukkit.commandstructure.could_not_resolve_pack_dimension_5": "§cPakkausta ei voitu ratkaista mitan vuoksi {value}", "iris.bukkit.commandstructure.could_not_resolve_pack_dimension_5": "§cPakkausta ei voitu ratkaista mitan vuoksi {value}",
"iris.bukkit.commandstructure.no_iris_structure_this_pack_2": "§cEi iirisrakennetta '{structure}Tässä pakkauksessa", "iris.bukkit.commandstructure.no_iris_structure_this_pack_2": "§cEi iirisrakennetta '{structure}Tässä pakkauksessa",
"iris.bukkit.commandstructure.structure_assembled_0_pieces": "§cRakenne{structure}' koottu 0 kappaletta", "iris.bukkit.commandstructure.structure_assembled_0_pieces": "§cRakenne{structure}' koottu 0 kappaletta",
"iris.bukkit.commandstructure.placed_pieces_at_your_location": "§aPaikka{structure}' ({value} Palaset) sijaintisi.", "iris.bukkit.commandstructure.placed_pieces_at_your_location": "§aPaikka{structure}' ({value} Palaset) sijaintisi. ({value2} block changes)",
"iris.bukkit.commandstructure.placement_changed_no_blocks": "§cStructure '{structure}' assembled {value} pieces but changed 0 blocks at your location. Check that the selected variants contain non-air blocks and that the placement is above the world's minimum height.",
"iris.bukkit.commandstudio.opening_studio_pack_seed": "§aAvaa studio \"{value}\" pakkaus (siemen: {seed})", "iris.bukkit.commandstudio.opening_studio_pack_seed": "§aAvaa studio \"{value}\" pakkaus (siemen: {seed})",
"iris.bukkit.commandstudio.provide_dimension_pack_iris_std_importvanilla_pack_dimension": "§cAntakaa mittapaketti: /iris std importvanilla pack=<dimension>", "iris.bukkit.commandstudio.provide_dimension_pack_iris_std_importvanilla_pack_dimension": "§cAntakaa mittapaketti: /iris std importvanilla pack=<dimension>",
"iris.bukkit.commandstudio.could_not_resolve_pack_dimension": "§cPakkausta ei voitu ratkaista mitan vuoksi {value}", "iris.bukkit.commandstudio.could_not_resolve_pack_dimension": "§cPakkausta ei voitu ratkaista mitan vuoksi {value}",
+2 -1
View File
@@ -260,7 +260,8 @@
"iris.bukkit.commandstructure.could_not_resolve_pack_dimension_5": "§cImpossible de résoudre le pack de la dimension {value}", "iris.bukkit.commandstructure.could_not_resolve_pack_dimension_5": "§cImpossible de résoudre le pack de la dimension {value}",
"iris.bukkit.commandstructure.no_iris_structure_this_pack_2": "§cAucune structure Iris '{structure}' dans ce pack", "iris.bukkit.commandstructure.no_iris_structure_this_pack_2": "§cAucune structure Iris '{structure}' dans ce pack",
"iris.bukkit.commandstructure.structure_assembled_0_pieces": "§cLa structure '{structure}' a assemblé 0 pièce", "iris.bukkit.commandstructure.structure_assembled_0_pieces": "§cLa structure '{structure}' a assemblé 0 pièce",
"iris.bukkit.commandstructure.placed_pieces_at_your_location": "§aLa structure '{structure}' ({value} pièces) a été placée à votre position.", "iris.bukkit.commandstructure.placed_pieces_at_your_location": "§aLa structure '{structure}' ({value} pièces) a été placée à votre position. ({value2} block changes)",
"iris.bukkit.commandstructure.placement_changed_no_blocks": "§cStructure '{structure}' assembled {value} pieces but changed 0 blocks at your location. Check that the selected variants contain non-air blocks and that the placement is above the world's minimum height.",
"iris.bukkit.commandstudio.opening_studio_pack_seed": "§aOuverture de Studio pour le pack \"{value}\" (graine : {seed})", "iris.bukkit.commandstudio.opening_studio_pack_seed": "§aOuverture de Studio pour le pack \"{value}\" (graine : {seed})",
"iris.bukkit.commandstudio.provide_dimension_pack_iris_std_importvanilla_pack_dimension": "§cIndiquez un pack de dimension : /iris std importvanilla pack=<dimension>", "iris.bukkit.commandstudio.provide_dimension_pack_iris_std_importvanilla_pack_dimension": "§cIndiquez un pack de dimension : /iris std importvanilla pack=<dimension>",
"iris.bukkit.commandstudio.could_not_resolve_pack_dimension": "§cImpossible de résoudre le pack de la dimension {value}", "iris.bukkit.commandstudio.could_not_resolve_pack_dimension": "§cImpossible de résoudre le pack de la dimension {value}",
+2 -1
View File
@@ -260,7 +260,8 @@
"iris.bukkit.commandstructure.could_not_resolve_pack_dimension_5": "§cלא יכול לפתור את החבילה לממד {value}", "iris.bukkit.commandstructure.could_not_resolve_pack_dimension_5": "§cלא יכול לפתור את החבילה לממד {value}",
"iris.bukkit.commandstructure.no_iris_structure_this_pack_2": "§cאין מבנה איריס \"{structure}\"בחבילה הזאת", "iris.bukkit.commandstructure.no_iris_structure_this_pack_2": "§cאין מבנה איריס \"{structure}\"בחבילה הזאת",
"iris.bukkit.commandstructure.structure_assembled_0_pieces": "§cמבנה \"{structure}התאספו 0 חתיכות", "iris.bukkit.commandstructure.structure_assembled_0_pieces": "§cמבנה \"{structure}התאספו 0 חתיכות",
"iris.bukkit.commandstructure.placed_pieces_at_your_location": "§aמקום \"{structure}' ({value} חתיכות) במיקום שלך.", "iris.bukkit.commandstructure.placed_pieces_at_your_location": "§aמקום \"{structure}' ({value} חתיכות) במיקום שלך. ({value2} block changes)",
"iris.bukkit.commandstructure.placement_changed_no_blocks": "§cStructure '{structure}' assembled {value} pieces but changed 0 blocks at your location. Check that the selected variants contain non-air blocks and that the placement is above the world's minimum height.",
"iris.bukkit.commandstudio.opening_studio_pack_seed": "§aסטודיו פתיחה ל\"{value}\"חבילה\" (צילום: {seed})", "iris.bukkit.commandstudio.opening_studio_pack_seed": "§aסטודיו פתיחה ל\"{value}\"חבילה\" (צילום: {seed})",
"iris.bukkit.commandstudio.provide_dimension_pack_iris_std_importvanilla_pack_dimension": "§cלספק ערכת מימד: /iris std importvanilla pack=<dimension>", "iris.bukkit.commandstudio.provide_dimension_pack_iris_std_importvanilla_pack_dimension": "§cלספק ערכת מימד: /iris std importvanilla pack=<dimension>",
"iris.bukkit.commandstudio.could_not_resolve_pack_dimension": "§cלא יכול לפתור את החבילה לממד {value}", "iris.bukkit.commandstudio.could_not_resolve_pack_dimension": "§cלא יכול לפתור את החבילה לממד {value}",
+2 -1
View File
@@ -260,7 +260,8 @@
"iris.bukkit.commandstructure.could_not_resolve_pack_dimension_5": "§cImpossibile risolvere il pack della dimensione {value}", "iris.bukkit.commandstructure.could_not_resolve_pack_dimension_5": "§cImpossibile risolvere il pack della dimensione {value}",
"iris.bukkit.commandstructure.no_iris_structure_this_pack_2": "§cNessuna struttura Iris '{structure}' in questo pack", "iris.bukkit.commandstructure.no_iris_structure_this_pack_2": "§cNessuna struttura Iris '{structure}' in questo pack",
"iris.bukkit.commandstructure.structure_assembled_0_pieces": "§cLa struttura '{structure}' è stata assemblata con 0 pezzi", "iris.bukkit.commandstructure.structure_assembled_0_pieces": "§cLa struttura '{structure}' è stata assemblata con 0 pezzi",
"iris.bukkit.commandstructure.placed_pieces_at_your_location": "§a'{structure}' ({value} pezzi) posizionata nella tua posizione.", "iris.bukkit.commandstructure.placed_pieces_at_your_location": "§a'{structure}' ({value} pezzi) posizionata nella tua posizione. ({value2} block changes)",
"iris.bukkit.commandstructure.placement_changed_no_blocks": "§cStructure '{structure}' assembled {value} pieces but changed 0 blocks at your location. Check that the selected variants contain non-air blocks and that the placement is above the world's minimum height.",
"iris.bukkit.commandstudio.opening_studio_pack_seed": "§aApertura di Studio per il pack \"{value}\" (seed: {seed})", "iris.bukkit.commandstudio.opening_studio_pack_seed": "§aApertura di Studio per il pack \"{value}\" (seed: {seed})",
"iris.bukkit.commandstudio.provide_dimension_pack_iris_std_importvanilla_pack_dimension": "§cSpecifica un Pack di dimensione: /iris std importvanilla pack=<dimension>", "iris.bukkit.commandstudio.provide_dimension_pack_iris_std_importvanilla_pack_dimension": "§cSpecifica un Pack di dimensione: /iris std importvanilla pack=<dimension>",
"iris.bukkit.commandstudio.could_not_resolve_pack_dimension": "§cImpossibile risolvere il pack della dimensione {value}", "iris.bukkit.commandstudio.could_not_resolve_pack_dimension": "§cImpossibile risolvere il pack della dimensione {value}",
+2 -1
View File
@@ -260,7 +260,8 @@
"iris.bukkit.commandstructure.could_not_resolve_pack_dimension_5": "§cディメンション {value} のパックを解決できませんでした", "iris.bukkit.commandstructure.could_not_resolve_pack_dimension_5": "§cディメンション {value} のパックを解決できませんでした",
"iris.bukkit.commandstructure.no_iris_structure_this_pack_2": "§cこのパックには Iris 構造物 '{structure}' がありません", "iris.bukkit.commandstructure.no_iris_structure_this_pack_2": "§cこのパックには Iris 構造物 '{structure}' がありません",
"iris.bukkit.commandstructure.structure_assembled_0_pieces": "§c構造物 '{structure}' は 0 ピースで組み立てられました", "iris.bukkit.commandstructure.structure_assembled_0_pieces": "§c構造物 '{structure}' は 0 ピースで組み立てられました",
"iris.bukkit.commandstructure.placed_pieces_at_your_location": "§a現在位置に '{structure}'{value} ピース)を配置しました。", "iris.bukkit.commandstructure.placed_pieces_at_your_location": "§a現在位置に '{structure}'{value} ピース)を配置しました。 ({value2} block changes)",
"iris.bukkit.commandstructure.placement_changed_no_blocks": "§cStructure '{structure}' assembled {value} pieces but changed 0 blocks at your location. Check that the selected variants contain non-air blocks and that the placement is above the world's minimum height.",
"iris.bukkit.commandstudio.opening_studio_pack_seed": "§aパック \"{value}\" のスタジオを開いています(シード: {seed})", "iris.bukkit.commandstudio.opening_studio_pack_seed": "§aパック \"{value}\" のスタジオを開いています(シード: {seed})",
"iris.bukkit.commandstudio.provide_dimension_pack_iris_std_importvanilla_pack_dimension": "§cディメンションパックを指定してください: /iris std importvanilla pack=<dimension>", "iris.bukkit.commandstudio.provide_dimension_pack_iris_std_importvanilla_pack_dimension": "§cディメンションパックを指定してください: /iris std importvanilla pack=<dimension>",
"iris.bukkit.commandstudio.could_not_resolve_pack_dimension": "§cディメンション {value} のパックを解決できませんでした", "iris.bukkit.commandstudio.could_not_resolve_pack_dimension": "§cディメンション {value} のパックを解決できませんでした",
+2 -1
View File
@@ -260,7 +260,8 @@
"iris.bukkit.commandstructure.could_not_resolve_pack_dimension_5": "§c차원을 위한 팩을 해결할 수 없습니다 {value}", "iris.bukkit.commandstructure.could_not_resolve_pack_dimension_5": "§c차원을 위한 팩을 해결할 수 없습니다 {value}",
"iris.bukkit.commandstructure.no_iris_structure_this_pack_2": "§c아이리스 구조 없음 '{structure}이 팩에서", "iris.bukkit.commandstructure.no_iris_structure_this_pack_2": "§c아이리스 구조 없음 '{structure}이 팩에서",
"iris.bukkit.commandstructure.structure_assembled_0_pieces": "§c구조물 '{structure}' 조립 결과: 조각 0개", "iris.bukkit.commandstructure.structure_assembled_0_pieces": "§c구조물 '{structure}' 조립 결과: 조각 0개",
"iris.bukkit.commandstructure.placed_pieces_at_your_location": "§a장소 '{structure}' ({value} 당신의 위치에 조각).", "iris.bukkit.commandstructure.placed_pieces_at_your_location": "§a장소 '{structure}' ({value} 당신의 위치에 조각). ({value2} block changes)",
"iris.bukkit.commandstructure.placement_changed_no_blocks": "§cStructure '{structure}' assembled {value} pieces but changed 0 blocks at your location. Check that the selected variants contain non-air blocks and that the placement is above the world's minimum height.",
"iris.bukkit.commandstudio.opening_studio_pack_seed": "§a\"를 위한 오프닝 스튜디오{value}\"팩 (seed: {seed})", "iris.bukkit.commandstudio.opening_studio_pack_seed": "§a\"를 위한 오프닝 스튜디오{value}\"팩 (seed: {seed})",
"iris.bukkit.commandstudio.provide_dimension_pack_iris_std_importvanilla_pack_dimension": "§c차원 팩을 제공: /iris std importvanilla pack=<dimension>", "iris.bukkit.commandstudio.provide_dimension_pack_iris_std_importvanilla_pack_dimension": "§c차원 팩을 제공: /iris std importvanilla pack=<dimension>",
"iris.bukkit.commandstudio.could_not_resolve_pack_dimension": "§c차원을 위한 팩을 해결할 수 없습니다 {value}", "iris.bukkit.commandstudio.could_not_resolve_pack_dimension": "§c차원을 위한 팩을 해결할 수 없습니다 {value}",
+2 -1
View File
@@ -260,7 +260,8 @@
"iris.bukkit.commandstructure.could_not_resolve_pack_dimension_5": "§cNepavyko išspręsti pakuotės dimensijai {value}", "iris.bukkit.commandstructure.could_not_resolve_pack_dimension_5": "§cNepavyko išspręsti pakuotės dimensijai {value}",
"iris.bukkit.commandstructure.no_iris_structure_this_pack_2": "§cNėra rainelės struktūros \"{structure}\"šioje pakuotėje", "iris.bukkit.commandstructure.no_iris_structure_this_pack_2": "§cNėra rainelės struktūros \"{structure}\"šioje pakuotėje",
"iris.bukkit.commandstructure.structure_assembled_0_pieces": "§cStruktūra \"{structure}'surinkti 0 vienetai", "iris.bukkit.commandstructure.structure_assembled_0_pieces": "§cStruktūra \"{structure}'surinkti 0 vienetai",
"iris.bukkit.commandstructure.placed_pieces_at_your_location": "§aPateikta \"{structure}' ({value} vienetų).", "iris.bukkit.commandstructure.placed_pieces_at_your_location": "§aPateikta \"{structure}' ({value} vienetų). ({value2} block changes)",
"iris.bukkit.commandstructure.placement_changed_no_blocks": "§cStructure '{structure}' assembled {value} pieces but changed 0 blocks at your location. Check that the selected variants contain non-air blocks and that the placement is above the world's minimum height.",
"iris.bukkit.commandstudio.opening_studio_pack_seed": "§aAtidarymo studija \"{value}\"pakuotė (sėkla: {seed})", "iris.bukkit.commandstudio.opening_studio_pack_seed": "§aAtidarymo studija \"{value}\"pakuotė (sėkla: {seed})",
"iris.bukkit.commandstudio.provide_dimension_pack_iris_std_importvanilla_pack_dimension": "§cPateikite matmenų paketą: /iris std importvanilla pack=<dimension>", "iris.bukkit.commandstudio.provide_dimension_pack_iris_std_importvanilla_pack_dimension": "§cPateikite matmenų paketą: /iris std importvanilla pack=<dimension>",
"iris.bukkit.commandstudio.could_not_resolve_pack_dimension": "§cNepavyko išspręsti pakuotės dimensijai {value}", "iris.bukkit.commandstudio.could_not_resolve_pack_dimension": "§cNepavyko išspręsti pakuotės dimensijai {value}",
+2 -1
View File
@@ -260,7 +260,8 @@
"iris.bukkit.commandstructure.could_not_resolve_pack_dimension_5": "§cKon het pakket voor dimensie niet oplossen {value}", "iris.bukkit.commandstructure.could_not_resolve_pack_dimension_5": "§cKon het pakket voor dimensie niet oplossen {value}",
"iris.bukkit.commandstructure.no_iris_structure_this_pack_2": "§cGeen irisstructuur '{structure}' in deze verpakking", "iris.bukkit.commandstructure.no_iris_structure_this_pack_2": "§cGeen irisstructuur '{structure}' in deze verpakking",
"iris.bukkit.commandstructure.structure_assembled_0_pieces": "§cStructuur{structure}' gemonteerd 0 stuks", "iris.bukkit.commandstructure.structure_assembled_0_pieces": "§cStructuur{structure}' gemonteerd 0 stuks",
"iris.bukkit.commandstructure.placed_pieces_at_your_location": "§aGeplaatst '{structure}' ({value} stukken) op uw locatie.", "iris.bukkit.commandstructure.placed_pieces_at_your_location": "§aGeplaatst '{structure}' ({value} stukken) op uw locatie. ({value2} block changes)",
"iris.bukkit.commandstructure.placement_changed_no_blocks": "§cStructure '{structure}' assembled {value} pieces but changed 0 blocks at your location. Check that the selected variants contain non-air blocks and that the placement is above the world's minimum height.",
"iris.bukkit.commandstudio.opening_studio_pack_seed": "§aopenen studio voor de \"{value}\" verpakking (zaad: {seed})", "iris.bukkit.commandstudio.opening_studio_pack_seed": "§aopenen studio voor de \"{value}\" verpakking (zaad: {seed})",
"iris.bukkit.commandstudio.provide_dimension_pack_iris_std_importvanilla_pack_dimension": "§cGeef een maatpakket: /iris std importvanilla pack=<dimension>", "iris.bukkit.commandstudio.provide_dimension_pack_iris_std_importvanilla_pack_dimension": "§cGeef een maatpakket: /iris std importvanilla pack=<dimension>",
"iris.bukkit.commandstudio.could_not_resolve_pack_dimension": "§cKon het pakket voor dimensie niet oplossen {value}", "iris.bukkit.commandstudio.could_not_resolve_pack_dimension": "§cKon het pakket voor dimensie niet oplossen {value}",
+2 -1
View File
@@ -260,7 +260,8 @@
"iris.bukkit.commandstructure.could_not_resolve_pack_dimension_5": "§cNie można rozwiązać pakietu dla wymiaru {value}", "iris.bukkit.commandstructure.could_not_resolve_pack_dimension_5": "§cNie można rozwiązać pakietu dla wymiaru {value}",
"iris.bukkit.commandstructure.no_iris_structure_this_pack_2": "§cBrak struktury tęczówki \"{structure}'w tym opakowaniu", "iris.bukkit.commandstructure.no_iris_structure_this_pack_2": "§cBrak struktury tęczówki \"{structure}'w tym opakowaniu",
"iris.bukkit.commandstructure.structure_assembled_0_pieces": "§cStruktura \"{structure}\"zmontowane 0 kawałki", "iris.bukkit.commandstructure.structure_assembled_0_pieces": "§cStruktura \"{structure}\"zmontowane 0 kawałki",
"iris.bukkit.commandstructure.placed_pieces_at_your_location": "§aUmieszczone \"{structure}' ({value} sztuk) w miejscu.", "iris.bukkit.commandstructure.placed_pieces_at_your_location": "§aUmieszczone \"{structure}' ({value} sztuk) w miejscu. ({value2} block changes)",
"iris.bukkit.commandstructure.placement_changed_no_blocks": "§cStructure '{structure}' assembled {value} pieces but changed 0 blocks at your location. Check that the selected variants contain non-air blocks and that the placement is above the world's minimum height.",
"iris.bukkit.commandstudio.opening_studio_pack_seed": "§aStudio otwarcia dla \"{value}\"opakowanie (nasiona: {seed})", "iris.bukkit.commandstudio.opening_studio_pack_seed": "§aStudio otwarcia dla \"{value}\"opakowanie (nasiona: {seed})",
"iris.bukkit.commandstudio.provide_dimension_pack_iris_std_importvanilla_pack_dimension": "§cNależy podać zestaw wymiarów: /iris std importvanilla pack=<dimension>", "iris.bukkit.commandstudio.provide_dimension_pack_iris_std_importvanilla_pack_dimension": "§cNależy podać zestaw wymiarów: /iris std importvanilla pack=<dimension>",
"iris.bukkit.commandstudio.could_not_resolve_pack_dimension": "§cNie można rozwiązać pakietu dla wymiaru {value}", "iris.bukkit.commandstudio.could_not_resolve_pack_dimension": "§cNie można rozwiązać pakietu dla wymiaru {value}",
+2 -1
View File
@@ -260,7 +260,8 @@
"iris.bukkit.commandstructure.could_not_resolve_pack_dimension_5": "§cNão foi possível resolver o pacote para a dimensão {value}", "iris.bukkit.commandstructure.could_not_resolve_pack_dimension_5": "§cNão foi possível resolver o pacote para a dimensão {value}",
"iris.bukkit.commandstructure.no_iris_structure_this_pack_2": "§cSem estrutura íris '{structure}' nesta pack", "iris.bukkit.commandstructure.no_iris_structure_this_pack_2": "§cSem estrutura íris '{structure}' nesta pack",
"iris.bukkit.commandstructure.structure_assembled_0_pieces": "§cEstrutura{structure}' montados 0 peças", "iris.bukkit.commandstructure.structure_assembled_0_pieces": "§cEstrutura{structure}' montados 0 peças",
"iris.bukkit.commandstructure.placed_pieces_at_your_location": "§aColocado '{structure}' ({value} peças) na sua localização.", "iris.bukkit.commandstructure.placed_pieces_at_your_location": "§aColocado '{structure}' ({value} peças) na sua localização. ({value2} block changes)",
"iris.bukkit.commandstructure.placement_changed_no_blocks": "§cStructure '{structure}' assembled {value} pieces but changed 0 blocks at your location. Check that the selected variants contain non-air blocks and that the placement is above the world's minimum height.",
"iris.bukkit.commandstudio.opening_studio_pack_seed": "§aEstúdio de abertura para o \"{value}\" pack (sementes: {seed})", "iris.bukkit.commandstudio.opening_studio_pack_seed": "§aEstúdio de abertura para o \"{value}\" pack (sementes: {seed})",
"iris.bukkit.commandstudio.provide_dimension_pack_iris_std_importvanilla_pack_dimension": "§cFornecer um pacote de dimensões: /iris std importvanilla pack=<dimension>", "iris.bukkit.commandstudio.provide_dimension_pack_iris_std_importvanilla_pack_dimension": "§cFornecer um pacote de dimensões: /iris std importvanilla pack=<dimension>",
"iris.bukkit.commandstudio.could_not_resolve_pack_dimension": "§cNão foi possível resolver o pacote para a dimensão {value}", "iris.bukkit.commandstudio.could_not_resolve_pack_dimension": "§cNão foi possível resolver o pacote para a dimensão {value}",
+2 -1
View File
@@ -260,7 +260,8 @@
"iris.bukkit.commandstructure.could_not_resolve_pack_dimension_5": "§cНе удалось решить пакет для измерения {value}", "iris.bukkit.commandstructure.could_not_resolve_pack_dimension_5": "§cНе удалось решить пакет для измерения {value}",
"iris.bukkit.commandstructure.no_iris_structure_this_pack_2": "§cНет структуры радужной оболочки{structure}В этой пачке", "iris.bukkit.commandstructure.no_iris_structure_this_pack_2": "§cНет структуры радужной оболочки{structure}В этой пачке",
"iris.bukkit.commandstructure.structure_assembled_0_pieces": "§cСтруктура{structure}собранный 0 части", "iris.bukkit.commandstructure.structure_assembled_0_pieces": "§cСтруктура{structure}собранный 0 части",
"iris.bukkit.commandstructure.placed_pieces_at_your_location": "§aПомещение '{structure}' ({value} куски) в вашем месте.", "iris.bukkit.commandstructure.placed_pieces_at_your_location": "§aПомещение '{structure}' ({value} куски) в вашем месте. ({value2} block changes)",
"iris.bukkit.commandstructure.placement_changed_no_blocks": "§cStructure '{structure}' assembled {value} pieces but changed 0 blocks at your location. Check that the selected variants contain non-air blocks and that the placement is above the world's minimum height.",
"iris.bukkit.commandstudio.opening_studio_pack_seed": "§aОткрытие студии для\"{value}\"пак (семя):{seed})", "iris.bukkit.commandstudio.opening_studio_pack_seed": "§aОткрытие студии для\"{value}\"пак (семя):{seed})",
"iris.bukkit.commandstudio.provide_dimension_pack_iris_std_importvanilla_pack_dimension": "§cПредоставьте размерный пакет: /iris std importvanilla pack=<dimension>", "iris.bukkit.commandstudio.provide_dimension_pack_iris_std_importvanilla_pack_dimension": "§cПредоставьте размерный пакет: /iris std importvanilla pack=<dimension>",
"iris.bukkit.commandstudio.could_not_resolve_pack_dimension": "§cНе удалось решить пакет для измерения {value}", "iris.bukkit.commandstudio.could_not_resolve_pack_dimension": "§cНе удалось решить пакет для измерения {value}",
+2 -1
View File
@@ -260,7 +260,8 @@
"iris.bukkit.commandstructure.could_not_resolve_pack_dimension_5": "§cpaketi boyut için çözemez {value}", "iris.bukkit.commandstructure.could_not_resolve_pack_dimension_5": "§cpaketi boyut için çözemez {value}",
"iris.bukkit.commandstructure.no_iris_structure_this_pack_2": "§cyok iris structure \"{structure}“Bu pakette", "iris.bukkit.commandstructure.no_iris_structure_this_pack_2": "§cyok iris structure \"{structure}“Bu pakette",
"iris.bukkit.commandstructure.structure_assembled_0_pieces": "§cYapı \"{structure}\"Bir araya geldi\" 0 parçalar parça parçaları", "iris.bukkit.commandstructure.structure_assembled_0_pieces": "§cYapı \"{structure}\"Bir araya geldi\" 0 parçalar parça parçaları",
"iris.bukkit.commandstructure.placed_pieces_at_your_location": "§ayerleştirildi {structure}' ({value} parçalar) konumunuzda.", "iris.bukkit.commandstructure.placed_pieces_at_your_location": "§ayerleştirildi {structure}' ({value} parçalar) konumunuzda. ({value2} block changes)",
"iris.bukkit.commandstructure.placement_changed_no_blocks": "§cStructure '{structure}' assembled {value} pieces but changed 0 blocks at your location. Check that the selected variants contain non-air blocks and that the placement is above the world's minimum height.",
"iris.bukkit.commandstudio.opening_studio_pack_seed": "§aAçılış stüdyosu \"{value}\" paket (seed: {seed})", "iris.bukkit.commandstudio.opening_studio_pack_seed": "§aAçılış stüdyosu \"{value}\" paket (seed: {seed})",
"iris.bukkit.commandstudio.provide_dimension_pack_iris_std_importvanilla_pack_dimension": "§cBir boyut paketi sağlayın: /iris std importvanilla pack=<dimension>", "iris.bukkit.commandstudio.provide_dimension_pack_iris_std_importvanilla_pack_dimension": "§cBir boyut paketi sağlayın: /iris std importvanilla pack=<dimension>",
"iris.bukkit.commandstudio.could_not_resolve_pack_dimension": "§cpaketi boyut için çözemez {value}", "iris.bukkit.commandstudio.could_not_resolve_pack_dimension": "§cpaketi boyut için çözemez {value}",
+2 -1
View File
@@ -260,7 +260,8 @@
"iris.bukkit.commandstructure.could_not_resolve_pack_dimension_5": "§cKhông thể giải quyết gói cho kích thước {value}", "iris.bukkit.commandstructure.could_not_resolve_pack_dimension_5": "§cKhông thể giải quyết gói cho kích thước {value}",
"iris.bukkit.commandstructure.no_iris_structure_this_pack_2": "§cKhông có cấu trúc Iris{structure}'Trong gói này", "iris.bukkit.commandstructure.no_iris_structure_this_pack_2": "§cKhông có cấu trúc Iris{structure}'Trong gói này",
"iris.bukkit.commandstructure.structure_assembled_0_pieces": "§cCấu trúc{structure}Tập hợp 0 mảnh", "iris.bukkit.commandstructure.structure_assembled_0_pieces": "§cCấu trúc{structure}Tập hợp 0 mảnh",
"iris.bukkit.commandstructure.placed_pieces_at_your_location": "§aĐã đặt '{structure}' ({value} Các mảnh) tại vị trí.", "iris.bukkit.commandstructure.placed_pieces_at_your_location": "§aĐã đặt '{structure}' ({value} Các mảnh) tại vị trí. ({value2} block changes)",
"iris.bukkit.commandstructure.placement_changed_no_blocks": "§cStructure '{structure}' assembled {value} pieces but changed 0 blocks at your location. Check that the selected variants contain non-air blocks and that the placement is above the world's minimum height.",
"iris.bukkit.commandstudio.opening_studio_pack_seed": "§aMở studio cho \"{value}\" Gói (dòng dõi: {seed})", "iris.bukkit.commandstudio.opening_studio_pack_seed": "§aMở studio cho \"{value}\" Gói (dòng dõi: {seed})",
"iris.bukkit.commandstudio.provide_dimension_pack_iris_std_importvanilla_pack_dimension": "§cCung cấp một gói chiều: /iris std importvanilla pack=<dimension>", "iris.bukkit.commandstudio.provide_dimension_pack_iris_std_importvanilla_pack_dimension": "§cCung cấp một gói chiều: /iris std importvanilla pack=<dimension>",
"iris.bukkit.commandstudio.could_not_resolve_pack_dimension": "§cKhông thể giải quyết gói cho kích thước {value}", "iris.bukkit.commandstudio.could_not_resolve_pack_dimension": "§cKhông thể giải quyết gói cho kích thước {value}",
+2 -1
View File
@@ -260,7 +260,8 @@
"iris.bukkit.commandstructure.could_not_resolve_pack_dimension_5": "§c无法解析大小包 {value}", "iris.bukkit.commandstructure.could_not_resolve_pack_dimension_5": "§c无法解析大小包 {value}",
"iris.bukkit.commandstructure.no_iris_structure_this_pack_2": "§c无Iris结构 '{structure}\"在这个包里\"", "iris.bukkit.commandstructure.no_iris_structure_this_pack_2": "§c无Iris结构 '{structure}\"在这个包里\"",
"iris.bukkit.commandstructure.structure_assembled_0_pieces": "§c结构 '{structure}组装 0 块", "iris.bukkit.commandstructure.structure_assembled_0_pieces": "§c结构 '{structure}组装 0 块",
"iris.bukkit.commandstructure.placed_pieces_at_your_location": "§a已放置 '{structure}' ({value}在您的位置。", "iris.bukkit.commandstructure.placed_pieces_at_your_location": "§a已放置 '{structure}' ({value}在您的位置。 ({value2} block changes)",
"iris.bukkit.commandstructure.placement_changed_no_blocks": "§cStructure '{structure}' assembled {value} pieces but changed 0 blocks at your location. Check that the selected variants contain non-air blocks and that the placement is above the world's minimum height.",
"iris.bukkit.commandstudio.opening_studio_pack_seed": "§a工作室 \"{value}\" 包(种子: {seed})", "iris.bukkit.commandstudio.opening_studio_pack_seed": "§a工作室 \"{value}\" 包(种子: {seed})",
"iris.bukkit.commandstudio.provide_dimension_pack_iris_std_importvanilla_pack_dimension": "§c提供一个维度包: /iris std importvanilla pack=<dimension>", "iris.bukkit.commandstudio.provide_dimension_pack_iris_std_importvanilla_pack_dimension": "§c提供一个维度包: /iris std importvanilla pack=<dimension>",
"iris.bukkit.commandstudio.could_not_resolve_pack_dimension": "§c无法解析大小包 {value}", "iris.bukkit.commandstudio.could_not_resolve_pack_dimension": "§c无法解析大小包 {value}",
+2 -1
View File
@@ -260,7 +260,8 @@
"iris.bukkit.commandstructure.could_not_resolve_pack_dimension_5": "§c無法解析大小包 {value}", "iris.bukkit.commandstructure.could_not_resolve_pack_dimension_5": "§c無法解析大小包 {value}",
"iris.bukkit.commandstructure.no_iris_structure_this_pack_2": "§c無Iris結構 '{structure}\"在這個包裡\"", "iris.bukkit.commandstructure.no_iris_structure_this_pack_2": "§c無Iris結構 '{structure}\"在這個包裡\"",
"iris.bukkit.commandstructure.structure_assembled_0_pieces": "§c結構 '{structure}組裝 0 塊", "iris.bukkit.commandstructure.structure_assembled_0_pieces": "§c結構 '{structure}組裝 0 塊",
"iris.bukkit.commandstructure.placed_pieces_at_your_location": "§a已放置 '{structure}' ({value}在您的位置。", "iris.bukkit.commandstructure.placed_pieces_at_your_location": "§a已放置 '{structure}' ({value}在您的位置。 ({value2} block changes)",
"iris.bukkit.commandstructure.placement_changed_no_blocks": "§cStructure '{structure}' assembled {value} pieces but changed 0 blocks at your location. Check that the selected variants contain non-air blocks and that the placement is above the world's minimum height.",
"iris.bukkit.commandstudio.opening_studio_pack_seed": "§a工作室 \"{value}\" 包(種子: {seed})", "iris.bukkit.commandstudio.opening_studio_pack_seed": "§a工作室 \"{value}\" 包(種子: {seed})",
"iris.bukkit.commandstudio.provide_dimension_pack_iris_std_importvanilla_pack_dimension": "§c提供一個維度包: /iris std importvanilla pack=<dimension>", "iris.bukkit.commandstudio.provide_dimension_pack_iris_std_importvanilla_pack_dimension": "§c提供一個維度包: /iris std importvanilla pack=<dimension>",
"iris.bukkit.commandstudio.could_not_resolve_pack_dimension": "§c無法解析大小包 {value}", "iris.bukkit.commandstudio.could_not_resolve_pack_dimension": "§c無法解析大小包 {value}",
@@ -44,7 +44,7 @@ public class JigsawStudioLayoutTest {
} }
@Test @Test
public void planarWorkcellsUseDirectTwoBlockSpacing() { public void planarWorkcellsUseDirectOneBlockSpacing() {
JigsawStudioLayout layout = JigsawStudioLayout.create( JigsawStudioLayout layout = JigsawStudioLayout.create(
JigsawStudioMode.PLANAR_JIGSAW, JigsawStudioMode.PLANAR_JIGSAW,
new JigsawStudioCellDimensions(16, 8, 16), new JigsawStudioCellDimensions(16, 8, 16),
@@ -53,12 +53,12 @@ public class JigsawStudioLayoutTest {
JigsawStudioBay end = layout.get("workcell/end"); JigsawStudioBay end = layout.get("workcell/end");
JigsawStudioBay corner = layout.get("workcell/corner"); JigsawStudioBay corner = layout.get("workcell/corner");
assertEquals(18, end.bounds().originX() - blank.bounds().originX()); assertEquals(17, end.bounds().originX() - blank.bounds().originX());
assertEquals(18, corner.bounds().originZ() - blank.bounds().originZ()); assertEquals(17, corner.bounds().originZ() - blank.bounds().originZ());
assertEquals(2, end.bounds().originX() - blank.bounds().maxX() - 1); assertEquals(1, end.bounds().originX() - blank.bounds().maxX() - 1);
assertEquals(2, corner.bounds().originZ() - blank.bounds().maxZ() - 1); assertEquals(1, corner.bounds().originZ() - blank.bounds().maxZ() - 1);
assertEquals(3, layout.columns()); assertEquals(3, layout.columns());
assertEquals(2, layout.gap()); assertEquals(1, layout.gap());
} }
@Test @Test
@@ -87,9 +87,9 @@ public class JigsawStudioLayoutTest {
JigsawStudioBay corner = layout.get("workcell/corner"); JigsawStudioBay corner = layout.get("workcell/corner");
JigsawStudioBay tee = layout.get("workcell/tee"); JigsawStudioBay tee = layout.get("workcell/tee");
assertEquals(14, end.bounds().originX() - blank.bounds().originX()); assertEquals(13, end.bounds().originX() - blank.bounds().originX());
assertEquals(36, straight.bounds().originX() - blank.bounds().originX()); assertEquals(34, straight.bounds().originX() - blank.bounds().originX());
assertEquals(13, corner.bounds().originZ() - blank.bounds().originZ()); assertEquals(12, corner.bounds().originZ() - blank.bounds().originZ());
assertEquals(new JigsawStudioCellDimensions(7, 8, 15), tee.bounds().dimensions()); assertEquals(new JigsawStudioCellDimensions(7, 8, 15), tee.bounds().dimensions());
assertFalse(tee.enabled()); assertFalse(tee.enabled());
assertTrue(blank.enabled()); assertTrue(blank.enabled());
@@ -181,14 +181,19 @@ public class JigsawStudioSessionTest {
"workcell/end", east.pieceKey(), false).token().orElseThrow(); "workcell/end", east.pieceKey(), false).token().orElseThrow();
assertTrue(session.completeVariantSwitch(switchToken)); assertTrue(session.completeVariantSwitch(switchToken));
long selectedLoad = session.workcellSnapshot("workcell/end").loadGeneration(); long selectedLoad = session.workcellSnapshot("workcell/end").loadGeneration();
assertFalse(session.workcellSnapshot("workcell/end").connectorsVisible());
assertTrue(session.setConnectorsVisible("workcell/end", true));
assertFalse(session.setConnectorsVisible("workcell/end", true));
assertTrue(session.replaceLayout(planarLayout(north, east))); assertTrue(session.replaceLayout(planarLayout(north, east)));
assertSame(east, session.activeVariant("workcell/end").orElseThrow()); assertSame(east, session.activeVariant("workcell/end").orElseThrow());
assertEquals(selectedLoad, session.workcellSnapshot("workcell/end").loadGeneration()); assertEquals(selectedLoad, session.workcellSnapshot("workcell/end").loadGeneration());
assertTrue(session.workcellSnapshot("workcell/end").connectorsVisible());
assertTrue(session.replaceLayout(planarLayout(north))); assertTrue(session.replaceLayout(planarLayout(north)));
assertSame(north, session.activeVariant("workcell/end").orElseThrow()); assertSame(north, session.activeVariant("workcell/end").orElseThrow());
assertTrue(session.workcellSnapshot("workcell/end").loadGeneration() > selectedLoad); assertTrue(session.workcellSnapshot("workcell/end").loadGeneration() > selectedLoad);
assertTrue(session.workcellSnapshot("workcell/end").connectorsVisible());
} }
@Test @Test
@@ -0,0 +1,141 @@
package art.arcane.iris.core.service;
import art.arcane.iris.core.structure.authoring.StructureBackend;
import art.arcane.iris.core.structure.authoring.StructureCapability;
import art.arcane.iris.core.structure.authoring.StructureHash;
import art.arcane.iris.core.structure.authoring.StructureKey;
import art.arcane.iris.core.structure.authoring.StructureOwnershipManifest;
import art.arcane.iris.core.structure.authoring.StructureResourceBundle;
import art.arcane.iris.core.structure.authoring.StructureSource;
import art.arcane.iris.core.structure.authoring.StructureTransactionWriter;
import art.arcane.iris.core.structure.authoring.StructureWriteOptions;
import art.arcane.iris.core.structure.authoring.StructureWriteResult;
import art.arcane.iris.engine.object.IrisObject;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TemporaryFolder;
import java.io.ByteArrayOutputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
import java.util.Map;
import java.util.stream.Stream;
import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertTrue;
public class JigsawStudioHistoryStoreTest {
@Rule
public TemporaryFolder temporaryFolder = new TemporaryFolder();
@Test
public void retainsFiveDeduplicatedIterationsAndRestoresThemThroughTheWriter() throws Exception {
Path root = temporaryFolder.newFolder("history").toPath();
StructureTransactionWriter writer = new StructureTransactionWriter(root);
JigsawStudioHistoryStore history = new JigsawStudioHistoryStore(root, "qa/history");
assertTrue(writer.write(bundle(0), StructureWriteOptions.addOnly()).successful());
for (int version = 1; version <= 7; version++) {
JigsawStudioHistoryStore.Snapshot previous = history.snapshotCurrent("qa/history/piece");
assertEquals(Math.min(version, JigsawStudioHistoryStore.MAX_ITERATIONS), history.append(previous));
StructureWriteResult write = writer.write(
bundle(version),
StructureWriteOptions.overwriteExpected(manifestHash(writer)));
assertTrue(write.successful());
}
assertEquals(JigsawStudioHistoryStore.MAX_ITERATIONS, history.availableIterations());
assertTrue(Files.isRegularFile(history.historyPath()));
try (Stream<Path> historyFiles = Files.list(history.historyPath().getParent())) {
assertEquals(1L, historyFiles.filter(Files::isRegularFile).count());
}
for (int expectedVersion = 6; expectedVersion >= 2; expectedVersion--) {
JigsawStudioHistoryStore.UndoResult undo = history.undoLatest();
assertTrue(undo.available());
assertTrue(undo.successful());
assertEquals("qa/history/piece", undo.pieceKey());
assertEquals(expectedVersion - 2, undo.remainingIterations());
assertArrayEquals(
content(expectedVersion),
Files.readAllBytes(root.resolve("objects/qa/history/object.iob")));
assertOwnedResourcesMatchManifest(root, writer);
}
assertFalse(Files.exists(history.historyPath()));
assertFalse(history.undoLatest().available());
}
@Test
public void identicalSnapshotsDoNotConsumeAnotherIteration() throws Exception {
Path root = temporaryFolder.newFolder("dedup").toPath();
StructureTransactionWriter writer = new StructureTransactionWriter(root);
JigsawStudioHistoryStore history = new JigsawStudioHistoryStore(root, "qa/history");
assertTrue(writer.write(bundle(0), StructureWriteOptions.addOnly()).successful());
JigsawStudioHistoryStore.Snapshot snapshot = history.snapshotCurrent("qa/history/piece");
assertEquals(1, history.append(snapshot));
assertEquals(1, history.append(snapshot));
assertEquals(1, history.availableIterations());
}
@Test
public void refusesToSnapshotAnOwnedResourceThatChangedOutsideTheWriter() throws Exception {
Path root = temporaryFolder.newFolder("modified").toPath();
StructureTransactionWriter writer = new StructureTransactionWriter(root);
JigsawStudioHistoryStore history = new JigsawStudioHistoryStore(root, "qa/history");
assertTrue(writer.write(bundle(0), StructureWriteOptions.addOnly()).successful());
Files.writeString(root.resolve("objects/qa/history/object.iob"), "external-change");
assertThrows(Exception.class, () -> history.snapshotCurrent("qa/history/piece"));
assertFalse(Files.exists(history.historyPath()));
}
private static StructureResourceBundle bundle(int version) throws Exception {
StructureKey key = new StructureKey("iris", "qa/history");
return StructureResourceBundle.builder(key)
.source(StructureSource.of(StructureSource.Kind.IRIS, key))
.backend(StructureBackend.IRIS_ASSEMBLY)
.capability(StructureCapability.BLOCKS)
.textResource("structures/qa/history.json", "{\"startPool\":\"qa/history/start\"}")
.textResource("jigsaw-pools/qa/history/start.json",
"{\"pieces\":[{\"piece\":\"qa/history/piece\"}]}")
.textResource("jigsaw-pieces/qa/history/piece.json",
"{\"object\":\"qa/history/object\",\"connectors\":[]}")
.resource("objects/qa/history/object.iob", content(version))
.build();
}
private static byte[] content(int version) throws Exception {
IrisObject object = new IrisObject(version + 1, 1, 1);
try (ByteArrayOutputStream output = new ByteArrayOutputStream()) {
object.write(output);
return output.toByteArray();
}
}
private static String manifestHash(StructureTransactionWriter writer) throws Exception {
Path manifestPath = writer.ownershipManifestPath(new StructureKey("iris", "qa/history"));
return StructureHash.sha256(Files.readAllBytes(manifestPath));
}
private static void assertOwnedResourcesMatchManifest(
Path root,
StructureTransactionWriter writer
) throws Exception {
Path manifestPath = writer.ownershipManifestPath(new StructureKey("iris", "qa/history"));
StructureOwnershipManifest manifest = StructureOwnershipManifest.fromJson(
Files.readAllBytes(manifestPath));
for (Map.Entry<String, String> resource : manifest.resourceHashes().entrySet()) {
assertEquals(
resource.getValue(),
StructureHash.sha256(Files.readAllBytes(root.resolve(resource.getKey()))));
}
assertEquals(
List.of(StructureCapability.BLOCKS),
manifest.capabilities());
}
}
@@ -27,9 +27,11 @@ import java.util.Map;
import java.util.Optional; import java.util.Optional;
import java.util.UUID; import java.util.UUID;
import java.util.Set; import java.util.Set;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicLong; import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.atomic.AtomicReference;
import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertFalse;
@@ -113,6 +115,55 @@ public class JigsawStudioLifecycleTest {
assertEquals(JigsawStudioService.SaveStart.CLOSING, service.tryBeginSave(request.requestId())); assertEquals(JigsawStudioService.SaveStart.CLOSING, service.tryBeginSave(request.requestId()));
} }
@Test
public void ownerReplacementWaitsForAutosaveThenClaimsClose() {
assertTrue(JigsawStudioActivation.tryBeginOpen(OWNER));
JigsawStudioActivation.Request request = activateOwnedStudio();
JigsawStudioActivation.finishOpen(OWNER);
JigsawStudioSession session = JigsawStudioActivation.getSession(request.requestId());
JigsawStudioService service = new JigsawStudioService();
assertEquals(
JigsawStudioSession.DirtyStatus.MARKED,
session.markWorkcellDirty(JigsawStudioLayout.SPATIAL_WORKCELL_ID).status());
AtomicReference<Runnable> retry = new AtomicReference<>();
try (MockedStatic<J> scheduling = mockStatic(J.class)) {
scheduling.when(() -> J.s(any(Runnable.class), eq(5))).thenAnswer(invocation -> {
retry.set(invocation.getArgument(0));
return null;
});
CompletableFuture<Void> readiness = service.awaitCloseForReplacement(
request.requestId(), OWNER);
assertFalse(readiness.isDone());
assertTrue(retry.get() != null);
JigsawStudioSession.SaveStart save = session.beginSave(
JigsawStudioLayout.SPATIAL_WORKCELL_ID);
assertEquals(JigsawStudioSession.SaveStatus.STARTED, save.status());
assertTrue(session.markWorkcellSaved(save.identity().orElseThrow()));
retry.get().run();
readiness.join();
assertNull(service.closeProtectionFailure(request.requestId()));
}
}
@Test
public void nonOwnerReplacementFailsWithoutWaiting() {
assertTrue(JigsawStudioActivation.tryBeginOpen(OWNER));
JigsawStudioActivation.Request request = activateOwnedStudio();
JigsawStudioActivation.finishOpen(OWNER);
JigsawStudioService service = new JigsawStudioService();
try (MockedStatic<J> scheduling = mockStatic(J.class)) {
CompletableFuture<Void> readiness = service.awaitCloseForReplacement(
request.requestId(), OTHER_OWNER);
assertTrue(readiness.isCompletedExceptionally());
scheduling.verifyNoInteractions();
}
}
@Test @Test
public void lateJigsawGuiMutationKeepsCloseBehindTheFinalSnapshotAndAutosaveBarriers() public void lateJigsawGuiMutationKeepsCloseBehindTheFinalSnapshotAndAutosaveBarriers()
throws ReflectiveOperationException { throws ReflectiveOperationException {
@@ -75,6 +75,7 @@ public class JigsawStudioMenuControllerTest {
false, false,
false, false,
false, false,
false,
List.of(active)); List.of(active));
assertEquals(ChatColor.GREEN + "Autosaved", JigsawStudioMenuController.workcellStatus(fresh)); assertEquals(ChatColor.GREEN + "Autosaved", JigsawStudioMenuController.workcellStatus(fresh));
@@ -123,6 +124,7 @@ public class JigsawStudioMenuControllerTest {
true, true,
false, false,
false, false,
true,
List.of(active)); List.of(active));
JigsawStudioMenuState state = state(evaluation, corner); JigsawStudioMenuState state = state(evaluation, corner);
themes.add("late-theme"); themes.add("late-theme");
@@ -161,6 +163,7 @@ public class JigsawStudioMenuControllerTest {
false, false,
false, false,
false, false,
false,
List.of(active))); List.of(active)));
assertThrows(IllegalArgumentException.class, () -> new JigsawStudioMenuState( assertThrows(IllegalArgumentException.class, () -> new JigsawStudioMenuState(
WORLD_ID, WORLD_ID,
@@ -519,6 +522,7 @@ public class JigsawStudioMenuControllerTest {
true, true,
false, false,
false, false,
false,
variants); variants);
} }
@@ -5,6 +5,7 @@ import art.arcane.iris.core.loader.ResourceLoader;
import art.arcane.iris.core.runtime.jigsaw.JigsawStudioBounds; import art.arcane.iris.core.runtime.jigsaw.JigsawStudioBounds;
import art.arcane.iris.core.runtime.jigsaw.JigsawStudioActivation; import art.arcane.iris.core.runtime.jigsaw.JigsawStudioActivation;
import art.arcane.iris.core.runtime.jigsaw.JigsawStudioBay; import art.arcane.iris.core.runtime.jigsaw.JigsawStudioBay;
import art.arcane.iris.core.runtime.jigsaw.JigsawStudioBayKind;
import art.arcane.iris.core.runtime.jigsaw.JigsawStudioCellDimensions; import art.arcane.iris.core.runtime.jigsaw.JigsawStudioCellDimensions;
import art.arcane.iris.core.runtime.jigsaw.JigsawStudioCompatibilityTarget; import art.arcane.iris.core.runtime.jigsaw.JigsawStudioCompatibilityTarget;
import art.arcane.iris.core.runtime.jigsaw.JigsawStudioLayout; import art.arcane.iris.core.runtime.jigsaw.JigsawStudioLayout;
@@ -15,6 +16,8 @@ import art.arcane.iris.core.runtime.jigsaw.JigsawStudioSession;
import art.arcane.iris.core.runtime.jigsaw.JigsawStudioVariant; import art.arcane.iris.core.runtime.jigsaw.JigsawStudioVariant;
import art.arcane.iris.core.runtime.jigsaw.JigsawStudioVariantCatalog; import art.arcane.iris.core.runtime.jigsaw.JigsawStudioVariantCatalog;
import art.arcane.iris.core.runtime.jigsaw.JigsawPlanarTopology; import art.arcane.iris.core.runtime.jigsaw.JigsawPlanarTopology;
import art.arcane.iris.core.runtime.jigsaw.JigsawPlanarArchetype;
import art.arcane.iris.core.runtime.jigsaw.JigsawStudioWorkcellSpec;
import art.arcane.iris.core.structure.authoring.StructureWriteResult; import art.arcane.iris.core.structure.authoring.StructureWriteResult;
import art.arcane.iris.engine.object.IrisDirection; import art.arcane.iris.engine.object.IrisDirection;
import art.arcane.iris.engine.object.IrisJigsawConnector; import art.arcane.iris.engine.object.IrisJigsawConnector;
@@ -40,6 +43,7 @@ import art.arcane.volmlib.util.collection.KMap;
import org.bukkit.Chunk; import org.bukkit.Chunk;
import org.bukkit.Material; import org.bukkit.Material;
import org.bukkit.World; import org.bukkit.World;
import org.bukkit.block.Block;
import org.bukkit.block.BlockFace; import org.bukkit.block.BlockFace;
import org.bukkit.block.data.BlockData; import org.bukkit.block.data.BlockData;
import org.bukkit.block.data.Directional; import org.bukkit.block.data.Directional;
@@ -103,9 +107,79 @@ import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.mock; import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.mockStatic; import static org.mockito.Mockito.mockStatic;
import static org.mockito.Mockito.times; import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when; import static org.mockito.Mockito.when;
public class JigsawStudioServiceCaptureTest { public class JigsawStudioServiceCaptureTest {
@Test
public void hiddenConnectorResetRestoresOnlyItsSavedOrdinaryBlock() throws Exception {
World world = mock(World.class);
Block target = mock(Block.class);
BlockData blockData = mock(BlockData.class);
PlatformBlockState state = mock(PlatformBlockState.class);
JigsawStudioCellDimensions dimensions = new JigsawStudioCellDimensions(15, 15, 15);
JigsawStudioBay workcell = new JigsawStudioBay(
"spatial",
JigsawStudioBayKind.SPATIAL_WORKCELL,
Optional.empty(),
"",
new JigsawStudioBounds(10, 20, 30, dimensions));
IrisJigsawConnector connector = connectorAt(1, 2, 3)
.setFinalState("minecraft:stone");
JigsawStudioGenerator.RenderedConnector renderedConnector =
new JigsawStudioGenerator.RenderedConnector(
1,
2,
3,
connector,
"north_up");
JigsawStudioGenerator.RenderedBlock renderedBlock =
new JigsawStudioGenerator.RenderedBlock(1, 2, 3, state, null);
when(world.getBlockAt(11, 22, 33)).thenReturn(target);
when(state.isCustom()).thenReturn(false);
when(state.nativeHandle()).thenReturn(blockData);
JigsawStudioService.restoreConnectorChunk(
world,
workcell,
List.of(renderedConnector),
Map.of(new JigsawStudioService.LocalPosition(1, 2, 3), renderedBlock),
false);
verify(world).getBlockAt(11, 22, 33);
verify(target).setBlockData(blockData, false);
}
@Test
public void liveRelayoutDetectsMovedBoundsAndIncludesCageChunks() {
JigsawStudioCellDimensions originalDimensions = new JigsawStudioCellDimensions(16, 8, 16);
JigsawStudioLayout original = JigsawStudioLayout.create(
JigsawStudioMode.PLANAR_JIGSAW,
originalDimensions,
JigsawStudioVariantCatalog.empty());
List<JigsawStudioWorkcellSpec> expandedSpecs = new ArrayList<>();
for (JigsawPlanarArchetype archetype : JigsawPlanarArchetype.values()) {
expandedSpecs.add(new JigsawStudioWorkcellSpec(
archetype,
"",
archetype == JigsawPlanarArchetype.BLANK
? new JigsawStudioCellDimensions(33, 12, 17)
: originalDimensions,
true));
}
JigsawStudioLayout expanded = JigsawStudioLayout.createPlanar(
originalDimensions,
expandedSpecs,
JigsawStudioVariantCatalog.empty());
assertFalse(JigsawStudioService.layoutGeometryChanged(original, original));
assertTrue(JigsawStudioService.layoutGeometryChanged(original, expanded));
Set<Long> chunks = JigsawStudioService.relayoutChunks(original, expanded);
assertTrue(chunks.contains(0L));
assertTrue(chunks.contains(((long) 4 << 32)));
}
@Test @Test
public void mappedGraphOwnershipControlsNewVariantsEvenWhenTheCatalogIsEmpty() { public void mappedGraphOwnershipControlsNewVariantsEvenWhenTheCatalogIsEmpty() {
JigsawStudioCellDimensions dimensions = new JigsawStudioCellDimensions(16, 16, 16); JigsawStudioCellDimensions dimensions = new JigsawStudioCellDimensions(16, 16, 16);
@@ -720,6 +794,60 @@ public class JigsawStudioServiceCaptureTest {
} }
} }
@Test
public void hiddenConnectorCapturesExactBlockStateTilePayloadAndMetadata() throws Throwable {
JigsawStudioBounds bounds = new JigsawStudioBounds(
0,
64,
0,
new JigsawStudioCellDimensions(1, 1, 1));
IrisJigsawConnector connector = connector()
.setChannel("gate/owned")
.setSelectionPriority(-7)
.setPlacementPriority(11);
IrisJigsawPiece piece = new IrisJigsawPiece().setConnectors(new KList<>());
piece.getConnectors().add(connector);
BlockData chestData = directionalBlockData(Material.CHEST, BlockFace.EAST);
PlatformBlockState chestState = BukkitBlockState.of(chestData);
IrisObject sourceObject = new IrisObject(1, 1, 1);
sourceObject.setUnsigned(0, 0, 0, chestState);
KMap<String, Object> properties = new KMap<>();
properties.put("CustomName", "Hidden Connector Chest");
properties.put("Lock", "iris:hidden");
TileData tileData = new TileData("minecraft:chest", properties);
Block block = mock(Block.class);
when(block.getBlockData()).thenReturn(chestData);
World world = mock(World.class);
when(world.getBlockAt(0, 64, 0)).thenReturn(block);
JigsawStudioService.ChunkCaptureArea area = JigsawStudioService.chunkIntersections(bounds).getFirst();
JigsawStudioService.ChunkSnapshot snapshot;
try (MockedStatic<TileData> tiles = mockStatic(TileData.class)) {
tiles.when(() -> TileData.getTileState(block, false)).thenReturn(tileData);
snapshot = JigsawStudioService.captureChunkIntersection(
world,
bounds,
piece,
sourceObject,
area,
0,
false);
}
JigsawStudioService.Capture capture = JigsawStudioService.aggregateSnapshots(
bounds,
List.of(area),
List.of(snapshot));
IrisObject restored = readCapturedObject(capture.objectContent(), chestState);
IrisJigsawConnector captured = capture.connectors().getFirst();
assertEquals(chestData.getAsString(), captured.getFinalState());
assertEquals("gate/owned", captured.getChannel());
assertEquals(-7, captured.getSelectionPriority());
assertEquals(11, captured.getPlacementPriority());
assertEquals(tileData, restored.getStates().get(restored.getSigned(0, 0, 0)));
assertTrue(capture.hasBlockEntities());
}
@Test @Test
public void noOpTeeAndCrossCapturePreservesCreatorOrderForSeededAssembly() throws IOException { public void noOpTeeAndCrossCapturePreservesCreatorOrderForSeededAssembly() throws IOException {
JigsawStudioCellDimensions dimensions = new JigsawStudioCellDimensions(16, 16, 16); JigsawStudioCellDimensions dimensions = new JigsawStudioCellDimensions(16, 16, 16);
@@ -11,10 +11,12 @@ import art.arcane.iris.core.runtime.jigsaw.JigsawStudioMode;
import art.arcane.iris.core.runtime.jigsaw.JigsawStudioVariantCatalog; import art.arcane.iris.core.runtime.jigsaw.JigsawStudioVariantCatalog;
import art.arcane.iris.platform.bukkit.BukkitPlatform; import art.arcane.iris.platform.bukkit.BukkitPlatform;
import art.arcane.iris.util.common.plugin.VolmitPlugin; import art.arcane.iris.util.common.plugin.VolmitPlugin;
import art.arcane.iris.util.common.plugin.VolmitSender;
import org.junit.After; import org.junit.After;
import org.junit.Before; import org.junit.Before;
import org.junit.Test; import org.junit.Test;
import org.mockito.MockedStatic; import org.mockito.MockedStatic;
import org.bukkit.entity.Player;
import java.lang.reflect.Field; import java.lang.reflect.Field;
import java.util.UUID; import java.util.UUID;
@@ -92,6 +94,26 @@ public class StudioSVCJigsawProtectionTest {
verify(project).close(); verify(project).close();
} }
@Test
public void ownerCanReplaceJigsawStudioThroughOrdinaryStudioOpen() throws ReflectiveOperationException {
activateOwnedStudio();
IrisProject project = mock(IrisProject.class);
when(project.getName()).thenReturn("overworld");
StudioOpenCoordinator.StudioCloseResult closeResult = successfulClose();
when(project.close()).thenReturn(CompletableFuture.completedFuture(closeResult));
Player player = mock(Player.class);
when(player.getUniqueId()).thenReturn(OWNER);
VolmitSender sender = mock(VolmitSender.class);
when(sender.isPlayer()).thenReturn(true);
when(sender.player()).thenReturn(player);
StudioSVC studio = new StudioSVC();
setActiveProject(studio, project);
assertEquals(closeResult, studio.closeActiveProjectForReplacement(sender).join());
assertNull(studio.getActiveProject());
verify(project).close();
}
private static JigsawStudioActivation.Request activateOwnedStudio() { private static JigsawStudioActivation.Request activateOwnedStudio() {
assertTrue(JigsawStudioActivation.tryBeginOpen(OWNER)); assertTrue(JigsawStudioActivation.tryBeginOpen(OWNER));
JigsawStudioCellDimensions dimensions = new JigsawStudioCellDimensions(16, 16, 16); JigsawStudioCellDimensions dimensions = new JigsawStudioCellDimensions(16, 16, 16);
@@ -18,7 +18,7 @@ public class IrisJigsawModelMetadataTest {
assertEquals(IrisJigsawCompatibility.IRIS_EXTENDED, structure.resolvedCompatibility()); assertEquals(IrisJigsawCompatibility.IRIS_EXTENDED, structure.resolvedCompatibility());
assertEquals(IrisJigsawBranchFailurePolicy.FAIL_ASSEMBLY, assertEquals(IrisJigsawBranchFailurePolicy.FAIL_ASSEMBLY,
structure.resolvedBranchFailurePolicy()); structure.resolvedBranchFailurePolicy());
assertEquals(new IrisPosition(16, 16, 16), structure.getCellSize()); assertEquals(new IrisPosition(15, 15, 15), structure.getCellSize());
assertEquals("", connector.getChannel()); assertEquals("", connector.getChannel());
assertEquals("minecraft:air", connector.getFinalState()); assertEquals("minecraft:air", connector.getFinalState());
assertEquals(0, connector.getSelectionPriority()); assertEquals(0, connector.getSelectionPriority());
@@ -50,7 +50,7 @@ public class IrisJigsawModelMetadataTest {
assertEquals(IrisJigsawCompatibility.IRIS_EXTENDED, structure.resolvedCompatibility()); assertEquals(IrisJigsawCompatibility.IRIS_EXTENDED, structure.resolvedCompatibility());
assertEquals(IrisJigsawBranchFailurePolicy.FAIL_ASSEMBLY, assertEquals(IrisJigsawBranchFailurePolicy.FAIL_ASSEMBLY,
structure.resolvedBranchFailurePolicy()); structure.resolvedBranchFailurePolicy());
assertEquals(new IrisPosition(16, 16, 16), structure.getCellSize()); assertEquals(new IrisPosition(15, 15, 15), structure.getCellSize());
assertEquals("", connector.getChannel()); assertEquals("", connector.getChannel());
assertEquals("minecraft:air", connector.getFinalState()); assertEquals("minecraft:air", connector.getFinalState());
assertEquals(0, connector.getSelectionPriority()); assertEquals(0, connector.getSelectionPriority());
@@ -28,11 +28,13 @@ import art.arcane.iris.engine.object.JigsawJoint;
import art.arcane.iris.engine.object.TileData; import art.arcane.iris.engine.object.TileData;
import art.arcane.iris.spi.PlatformBiome; import art.arcane.iris.spi.PlatformBiome;
import art.arcane.iris.spi.PlatformBlockState; import art.arcane.iris.spi.PlatformBlockState;
import art.arcane.iris.util.common.data.B;
import art.arcane.volmlib.util.collection.KList; import art.arcane.volmlib.util.collection.KList;
import art.arcane.volmlib.util.collection.KMap; import art.arcane.volmlib.util.collection.KMap;
import org.bukkit.block.data.BlockData; import org.bukkit.block.data.BlockData;
import org.bukkit.generator.ChunkGenerator.ChunkData; import org.bukkit.generator.ChunkGenerator.ChunkData;
import org.junit.Test; import org.junit.Test;
import org.mockito.MockedStatic;
import java.util.HashMap; import java.util.HashMap;
import java.util.List; import java.util.List;
@@ -52,9 +54,70 @@ import static org.junit.Assert.assertNotSame;
import static org.junit.Assert.assertSame; import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertTrue; import static org.junit.Assert.assertTrue;
import static org.mockito.Mockito.mock; import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.mockStatic;
import static org.mockito.Mockito.when; import static org.mockito.Mockito.when;
public class JigsawStudioGeneratorTest { public class JigsawStudioGeneratorTest {
@Test
@SuppressWarnings("unchecked")
public void connectorBlocksAreHiddenByDefaultAndCanBeShownPerWorkcell() {
IrisData source = mock(IrisData.class);
ResourceLoader<IrisJigsawPiece> pieceLoader = mock(ResourceLoader.class);
ResourceLoader<IrisObject> objectLoader = mock(ResourceLoader.class);
when(source.getJigsawPieceLoader()).thenReturn(pieceLoader);
when(source.getObjectLoader()).thenReturn(objectLoader);
IrisJigsawConnector connector = new IrisJigsawConnector()
.setPosition(new IrisPosition(1, 1, 1))
.setDirection(IrisDirection.NORTH_NEGATIVE_Z)
.setTop(IrisDirection.UP_POSITIVE_Y)
.setPool("test/start")
.setName("door")
.setTargetName("door")
.setJoint(JigsawJoint.ALIGNED)
.setFinalState("minecraft:stone");
IrisJigsawPiece piece = new IrisJigsawPiece()
.setObject("test/room")
.setConnectors(new KList<>());
piece.getConnectors().add(connector);
PlatformBlockState stone = mock(PlatformBlockState.class);
IrisObject object = new IrisObject(3, 3, 3);
object.setUnsigned(1, 1, 1, stone);
when(pieceLoader.load("test/room", false)).thenReturn(piece);
when(objectLoader.load("test/room", false)).thenReturn(object);
JigsawStudioVariant variant = new JigsawStudioVariant(
"test/room",
"test/room",
"",
Optional.of(new JigsawStudioCellDimensions(3, 3, 3)),
JigsawStudioMode.SPATIAL_JIGSAW,
Optional.empty(),
true,
true,
List.of(),
new JigsawStudioPieceRules(0, 30, 0, 0, false),
List.of());
GeneratorFixture fixture = fixture(
source,
JigsawStudioMode.SPATIAL_JIGSAW,
new JigsawStudioCellDimensions(3, 3, 3),
new JigsawStudioVariantCatalog(List.of(variant)));
JigsawStudioBay workcell = fixture.layout().bays().getFirst();
int worldX = workcell.bounds().originX() + 1;
int worldY = workcell.bounds().originY() + 1;
int worldZ = workcell.bounds().originZ() + 1;
assertFalse(fixture.generator().getSession().workcellSnapshot(
workcell.stableId()).connectorsVisible());
assertSame(stone, stateAt(fixture.generator(), worldX, worldY, worldZ));
PlatformBlockState marker = mock(PlatformBlockState.class);
fixture.generator().getSession().setConnectorsVisible(workcell.stableId(), true);
try (MockedStatic<B> blocks = mockStatic(B.class)) {
blocks.when(() -> B.getState("minecraft:jigsaw[orientation=north_up]")).thenReturn(marker);
assertSame(marker, stateAt(fixture.generator(), worldX, worldY, worldZ));
}
}
@Test @Test
public void serviceRegistrationIsPublishedAfterTheRegistrationFinishes() throws Exception { public void serviceRegistrationIsPublishedAfterTheRegistrationFinishes() throws Exception {
GeneratorFixture fixture = fixture( GeneratorFixture fixture = fixture(
+7 -7
View File
@@ -11,7 +11,7 @@ Use these as entry points; follow the linked guide before running destructive or
| Create and enter a disposable world | `/iris create tutorial type=overworld seed=1337`, then `/iris tp tutorial` | `/iris create tutorial overworld 1337`, then `/iris tp irisworldgen:tutorial` | World/dimension appears in `/iris worlds` or `/iris world status`; ordinary chunks generate | `02 - Getting Started.md` | | Create and enter a disposable world | `/iris create tutorial type=overworld seed=1337`, then `/iris tp tutorial` | `/iris create tutorial overworld 1337`, then `/iris tp irisworldgen:tutorial` | World/dimension appears in `/iris worlds` or `/iris world status`; ordinary chunks generate | `02 - Getting Started.md` |
| Validate a pack before world creation | `/iris pack validate pack=overworld` | `/iris pack validate overworld` | No blocking validation errors | `25 - Pack Management.md` | | Validate a pack before world creation | `/iris pack validate pack=overworld` | `/iris pack validate overworld` | No blocking validation errors | `25 - Pack Management.md` |
| Open the live authoring pack | `/iris studio open overworld seed=1337` | `/iris studio open overworld 1337` | Transient Studio world opens and a valid save hotloads | `10 - Studio & VSCode Schemas.md` | | Open the live authoring pack | `/iris studio open overworld seed=1337` | `/iris studio open overworld 1337` | Transient Studio world opens and a valid save hotloads | `10 - Studio & VSCode Schemas.md` |
| Create an in-game jigsaw project | `/iris jigsaw create overworld village/demo` | Not available; author on Bukkit and copy the saved pack | Owned planar, Iris-native graph is created atomically with six 16×16×16 workcells, one variant per archetype, and seed `1337`; edits then autosave | `21 - Jigsaw Structures.md` | | Create an in-game jigsaw project | `/iris jigsaw create overworld village/demo` | Not available; author on Bukkit and copy the saved pack | Owned planar, Iris-native graph is created atomically with six 15×15×15 workcells, one variant per archetype, and seed `1337`; edits then autosave | `21 - Jigsaw Structures.md` |
| Inspect an Iris jigsaw graph | `/iris structure info overworld <structure>` | `/iris structure info <structure>` while in its Iris dimension | Resolved graph reports pieces and bounds | `21 - Jigsaw Structures.md` | | Inspect an Iris jigsaw graph | `/iris structure info overworld <structure>` | `/iris structure info <structure>` while in its Iris dimension | Resolved graph reports pieces and bounds | `21 - Jigsaw Structures.md` |
| Pregenerate a small test area | `/iris pregen start 352 world=<world> center=0,0 gui=false` | `/iris pregen start 352 <dimension> at 0 0` | `/iris pregen status` advances with no accumulating failures | `07 - Pregeneration.md` | | Pregenerate a small test area | `/iris pregen start 352 world=<world> center=0,0 gui=false` | `/iris pregen start 352 <dimension> at 0 0` | `/iris pregen status` advances with no accumulating failures | `07 - Pregeneration.md` |
| Remove a disposable Iris world | Evacuate players, unload, then `/iris remove <world>` | `/iris world delete <dimension>` | Target is absent from world status and its managed data is removed | `06 - Worlds & Lifecycle.md` | | Remove a disposable Iris world | Evacuate players, unload, then `/iris remove <world>` | `/iris world delete <dimension>` | Target is absent from world status and its managed data is removed | `06 - Worlds & Lifecycle.md` |
@@ -189,7 +189,7 @@ See `07 - Pregeneration.md`.
| Command | Aliases | Platforms | Params | Description | | Command | Aliases | Platforms | Params | Description |
|---------|---------|-----------|--------|-------------| |---------|---------|-----------|--------|-------------|
| `open` | `o` | Both | **Bukkit:** `<dimension> [seed=1337]`. **Modded:** `<pack> [seed]` | Open temporary studio dimension; Bukkit refuses to replace an active Jigsaw Studio outside its owner-authorized Jigsaw lifecycle | | `open` | `o` | Both | **Bukkit:** `<dimension> [seed=1337]`. **Modded:** `<pack> [seed]` | Open temporary studio dimension; the owning player may replace an active Jigsaw Studio, and Iris waits for its autosave and active operation barriers before closing it |
| `close` | `x` | Both | — | Close studio and discard world; Bukkit requires `/iris jigsaw close` for an active Jigsaw Studio | | `close` | `x` | Both | — | Close studio and discard world; Bukkit requires `/iris jigsaw close` for an active Jigsaw Studio |
| `tpstudio` | `stp` | Both | — | Teleport into open studio | | `tpstudio` | `stp` | Both | — | Teleport into open studio |
| `status` | | **Modded** (Bukkit uses other paths) | — | Show open studio and pack | | `status` | | **Modded** (Bukkit uses other paths) | — | Show open studio and pack |
@@ -218,11 +218,11 @@ See `10 - Studio & VSCode Schemas.md`.
| Command | Params | Description | | Command | Params | Description |
|---|---|---| |---|---|---|
| `create` | `<dimension> <key> [mode=planar] [compatibility=iris] [width=16] [height=16] [depth=16] [seed=1337]` | Add-only atomic graph creation followed by open; named `structure=` and `name=` alias `key=`; `mode` completes `planar`/`spatial`, compatibility completes `iris`/`vanilla`; planar X/Z `3..128`, spatial X/Z `1..128`, Y `1..192`, volume `<=2,097,152` | | `create` | `<dimension> <key> [mode=planar] [compatibility=iris] [width=15] [height=15] [depth=15] [seed=1337]` | Add-only atomic graph creation followed by open; named `structure=` and `name=` alias `key=`; `mode` completes `planar`/`spatial`, compatibility completes `iris`/`vanilla`; planar X/Z `3..128`, spatial X/Z `1..128`, Y `1..192`, volume `<=2,097,152` |
| `convert` | `<dimension> <source> [target=auto] [seed=1337]` | Add-only conversion of one live registered vanilla/datapack jigsaw into an owned Iris graph, then open it; aliases `import`, `import-vanilla` | | `convert` | `<dimension> <source> [target=auto] [seed=1337]` | Add-only conversion of one live registered vanilla/datapack jigsaw into an owned Iris graph, then open it; aliases `import`, `import-vanilla` |
| `adopt inspect` | `<dimension> <source> [target=auto] [strategy=auto]` | Asynchronously inspect an existing Iris closure and issue a hash-pinned `IN_PLACE`, `CLONE_REQUIRED`, or `BLOCKED` plan; strategy completes `auto`, `in-place`, `clone` | | `adopt inspect` | `<dimension> <source> [target=auto] [strategy=auto]` | Asynchronously inspect an existing Iris closure and issue a hash-pinned `IN_PLACE`, `CLONE_REQUIRED`, or `BLOCKED` plan; strategy completes `auto`, `in-place`, `clone` |
| `adopt apply` | `<planId>` | Revalidate and atomically apply that player's unexpired plan, then open the target at seed `1337`; active/opening Jigsaw Studio is rejected | | `adopt apply` | `<planId>` | Revalidate and atomically apply that player's unexpired plan, then open the target at seed `1337`; active/opening Jigsaw Studio is rejected |
| `open` | `<dimension> <key> [seed=1337]` | Open an existing graph in compact workcells; aliases `edit`, `reopen`; owner, autosave, and operation barriers protect replacement | | `open` | `<dimension> <key> [seed=1337]` | Open an existing graph in compact workcells; aliases `edit`, `reopen`; existing Iris structure keys tab-complete; owner, autosave, and operation barriers protect replacement |
| `close` | `[discard=false]` | Close Studio; refuse active autosave/load/graph work or a pending dirty capture unless deliberately discarded | | `close` | `[discard=false]` | Close Studio; refuse active autosave/load/graph work or a pending dirty capture unless deliberately discarded |
| `status` | — | Show project/workcell state and the current automatic seed-`1337` evaluation, theme, piece count, and diagnostic | | `status` | — | Show project/workcell state and the current automatic seed-`1337` evaluation, theme, piece count, and diagnostic |
| `menu` | — | Open the six-row controls also opened by the generated chest or three sneaks within 1.5 seconds | | `menu` | — | Open the six-row controls also opened by the generated chest or three sneaks within 1.5 seconds |
@@ -231,7 +231,7 @@ See `10 - Studio & VSCode Schemas.md`.
| `particles` | `<visible>` | Toggle player-local bounds and connector particles | | `particles` | `<visible>` | Toggle player-local bounds and connector particles |
| `save` | `[bay=selected]` | Flush the selected dirty workcell's automatic capture now; normal block and container updates already autosave | | `save` | `[bay=selected]` | Flush the selected dirty workcell's automatic capture now; normal block and container updates already autosave |
| `connector channel` | `<channel\|none>` | Look at a saved marker in the active owned workcell within 8 blocks and set/clear its Iris-only channel at the inverse-mapped source position | | `connector channel` | `<channel\|none>` | Look at a saved marker in the active owned workcell within 8 blocks and set/clear its Iris-only channel at the inverse-mapped source position |
| `bounds` | `<width> <height> <depth>` | Set the selected workcell capacity without resizing any variant object; every existing variant must fit, and the compact Studio layout requires close/reopen; aliases `cell`, `resize` | | `bounds` | `<width> <height> <depth>` | Set the selected workcell capacity without resizing any variant object; every existing variant must fit, and the compact Studio layout regenerates in place; aliases `cell`, `resize` |
| `workcell capacity` | `<width> <height> <depth>` | Explicit nested form of `bounds`; planar capacities are per canonical archetype and spatial capacity is the single project envelope | | `workcell capacity` | `<width> <height> <depth>` | Explicit nested form of `bounds`; planar capacities are per canonical archetype and spatial capacity is the single project envelope |
| `workcell label` | `<displayName>` | Set the selected planar or spatial workcell's author label; quote spaces; solver identity remains canonical | | `workcell label` | `<displayName>` | Set the selected planar or spatial workcell's author label; quote spaces; solver identity remains canonical |
| `workcell label-reset` | — | Reset the selected workcell to its canonical solver label; alias `reset-label` | | `workcell label-reset` | — | Reset the selected workcell to its canonical solver label; alias `reset-label` |
@@ -254,7 +254,7 @@ See `10 - Studio & VSCode Schemas.md`.
| `export` | `[namespace=iris] [output=jigsaw-export] [format=zip] [replace=false]` | Start a background strict Minecraft 26.2 vanilla datapack export as one direct artifact under the Studio packs `exports/` folder | | `export` | `[namespace=iris] [output=jigsaw-export] [format=zip] [replace=false]` | Start a background strict Minecraft 26.2 vanilla datapack export as one direct artifact under the Studio packs `exports/` folder |
| `delete` | `[confirm=false]` | With `confirm=true`, scan reverse references, close Studio, and hash-pinned-delete the complete owned project; alias `remove` | | `delete` | `[confirm=false]` | With `confirm=true`, scan reverse references, close Studio, and hash-pinned-delete the complete owned project; alias `remove` |
There are no Jigsaw Studio undo, adoption rollback, or mod-loader authoring commands. Planar Studio always has six independently capacitated/enabled canonical workcells, spatial Studio one, and every variant retains its own exact dimensions and optional display label. Workcell and variant rename tools are renamed in an anvil, right-clicked to apply, and sneak-right-clicked to reset. A catalog may contain at most 512 variants. The seed-`1337` assembly is evaluated automatically and rendered as a permanent protected block preview; `preview assemble` is the separate temporary arbitrary-seed particle diagnostic. See `21 - Jigsaw Structures.md` for GUI/toolbox controls, themes/chance/rules/caps, markers, ownership, placement, export, and recovery. There is no Jigsaw Studio undo command, adoption rollback command, or mod-loader authoring command. **Undo Last Autosave** in Workcell Settings restores the newest of five previous saved graph iterations retained in one `.iris/jigsaw-history/key-<sha256>.json` file. **Reset Connector Blocks** restores the selected workcell's saved connector blocks without replacing its other edited blocks. Planar Studio always has six independently capacitated/enabled canonical workcells, spatial Studio one, and every variant retains its own exact dimensions and optional display label. Workcell and variant rename tools are renamed in an anvil, right-clicked to apply, and sneak-right-clicked to reset. A catalog may contain at most 512 variants. The seed-`1337` assembly is evaluated automatically and rendered as a permanent protected block preview; `preview assemble` is the separate temporary arbitrary-seed particle diagnostic. See `21 - Jigsaw Structures.md` for GUI/toolbox controls, themes/chance/rules/caps, markers, ownership, placement, export, and recovery.
Bukkit has one global Studio project/world and the Jigsaw session belongs to one owning player. Only that owner can control, load, or mutate it; entering a workcell makes that physical cell the owner's next menu selection. Non-owner edits are cancelled and non-owner commands use a strict informational/communication allowlist. Block and inventory changes in loaded owned workcells autosave after a 40-tick quiet period. Duplicate-one and duplicate-family actions queue once behind pending autosave, expedite it, and continue automatically against the same request and source variants. The chest and live preview are protected; schema-1 or otherwise stale toolbox sticks are rejected. A later mutation after capture starts remains dirty for another capture. Plugins that bypass covered events must call `JigsawStudioService.markDirty(...)` or `markAllDirty(...)`. Bukkit has one global Studio project/world and the Jigsaw session belongs to one owning player. Only that owner can control, load, or mutate it; entering a workcell makes that physical cell the owner's next menu selection. Non-owner edits are cancelled and non-owner commands use a strict informational/communication allowlist. Block and inventory changes in loaded owned workcells autosave after a 40-tick quiet period. Duplicate-one and duplicate-family actions queue once behind pending autosave, expedite it, and continue automatically against the same request and source variants. The chest and live preview are protected; schema-1 or otherwise stale toolbox sticks are rejected. A later mutation after capture starts remains dirty for another capture. Plugins that bypass covered events must call `JigsawStudioService.markDirty(...)` or `markAllDirty(...)`.
@@ -279,7 +279,7 @@ See `25 - Pack Management.md`.
|---------|---------|-----------|--------|-------------| |---------|---------|-----------|--------|-------------|
| `list` | `ls` | Both | **Bukkit:** `<dimension>`. **Modded:** current engine pack | Write `structure-index.json` | | `list` | `ls` | Both | **Bukkit:** `<dimension>`. **Modded:** current engine pack | Write `structure-index.json` |
| `info` | | Both | **Bukkit:** `<dimension> <structure>`. **Modded:** `<key>` | Resolve jigsaw graph bounds | | `info` | | Both | **Bukkit:** `<dimension> <structure>`. **Modded:** `<key>` | Resolve jigsaw graph bounds |
| `place` | `p` | Both | **Bukkit:** `<dimension> <structure>` (player). **Modded:** `<key>` | Assemble and place structure at player | | `place` | `p` | Both | **Bukkit:** `<dimension> <structure>` (player). **Modded:** `<key>` | Assemble and place at the player; Bukkit reports the exact changed-block count and rejects air-only or already-identical no-op results |
| `import` | `import-all`, `reimport`, `imp`, `all` | **Bukkit**; modded message | `<dimension>` | Import all vanilla/datapack structures as editable Iris resources (overwrites) | | `import` | `import-all`, `reimport`, `imp`, `all` | **Bukkit**; modded message | `<dimension>` | Import all vanilla/datapack structures as editable Iris resources (overwrites) |
| `capture` | `cap` | **Bukkit**; modded message | `<dimension>` | Capture code-only structures via scratch world | | `capture` | `cap` | **Bukkit**; modded message | `<dimension>` | Capture code-only structures via scratch world |
| `verify` | `locateall` | Both | **Bukkit:** `<dimension> [radius=48]`. **Modded:** `[key]` | Native/Iris structure reachability report | | `verify` | `locateall` | Both | **Bukkit:** `<dimension> [radius=48]`. **Modded:** `[key]` | Native/Iris structure reachability report |
+6 -6
View File
@@ -90,7 +90,7 @@ Permissions and the full `/iris` tree: see `04 - Commands & Permissions.md`.
`/iris jigsaw` opens one selected structure graph through the transient Studio lifecycle, but chooses `JigsawStudioGenerator` for that activation without persisting a special dimension mode. The owner enters in creative. Planar Studio has six rotation-independent workcells in a compact three-column by two-row layout: Blank, End Cap, Hallway, L Junction, T Junction, and Cross Junction. Spatial Studio has one workcell. There is no orientation, permutation, piece, or derived-rotation gallery. `/iris jigsaw` opens one selected structure graph through the transient Studio lifecycle, but chooses `JigsawStudioGenerator` for that activation without persisting a special dimension mode. The owner enters in creative. Planar Studio has six rotation-independent workcells in a compact three-column by two-row layout: Blank, End Cap, Hallway, L Junction, T Junction, and Cross Junction. Spatial Studio has one workcell. There is no orientation, permutation, piece, or derived-rotation gallery.
Each planar floor is light-gray wool with a red canonical topology glyph and sea-lantern caps at its face-center connector positions. Every workcell has an independent width, height, depth, enabled state, and optional author label. Those dimensions are capacity only: changing one never rewrites a variant object, and the complete change is rejected if any existing variant would no longer fit. Each owned variant has its own exact width, height, depth, and optional label, so one End Cap can be a `16×3×3` longhouse while another End Cap in the same workcell remains `3×3×3`. Per-variant growth or lossless shrink preserves in-bounds canonical content and moves canonical connector payloads and sockets to the new face centers; cropped stored content, connector collisions, or shared/read-only objects reject the transaction. Capacity changes require close/reopen to regenerate the compact layout, while resizing the loaded variant reloads that cell in place. A disabled planar workcell remains editable but is excluded from assembly and vanilla export; a full-volume red stained-glass display marks it and is recreated when its origin chunk reloads. Existing planar variants are rotated into the archetype's canonical display orientation and inverse-rotated during capture, while their piece resources, dimensions, labels, and pool entries remain distinct. Each planar floor is light-gray wool with a red canonical topology glyph and sea-lantern caps at its face-center connector positions. Every workcell has an independent width, height, depth, enabled state, and optional author label. Those dimensions are capacity only: changing one never rewrites a variant object, and the complete change is rejected if any existing variant would no longer fit. Each owned variant has its own exact width, height, depth, and optional label, so one End Cap can be a `16×3×3` longhouse while another End Cap in the same workcell remains `3×3×3`. Per-variant growth or lossless shrink preserves in-bounds canonical content and moves canonical connector payloads and sockets to the new face centers; cropped stored content, connector collisions, or shared/read-only objects reject the transaction. Capacity changes regenerate and rehydrate the compact layout in place, while resizing the loaded variant reloads that cell in place. A disabled planar workcell remains editable but is excluded from assembly and vanilla export; a full-volume red stained-glass display marks it and is recreated when its origin chunk reloads. Existing planar variants are rotated into the archetype's canonical display orientation and inverse-rotated during capture, while their piece resources, dimensions, labels, and pool entries remain distinct.
Create a default planar, Iris-native graph with: Create a default planar, Iris-native graph with:
@@ -98,25 +98,25 @@ Create a default planar, Iris-native graph with:
/iris jigsaw create <dimension> <key> /iris jigsaw create <dimension> <key>
``` ```
`key` is the structure's internal resource path: `village/demo` writes `structures/village/demo.json` and becomes the key used by Iris placements and later editing. Named arguments `structure=` and `name=` are aliases for `key=`; they do not select a separate vanilla structure or template. Omitted options default to `mode=planar`, `compatibility=iris`, `width=16`, `height=16`, `depth=16`, and `seed=1337`; `mode=` completes `planar` or `spatial`, while `compatibility=` completes `iris` or `vanilla`. `key` is the structure's internal resource path: `village/demo` writes `structures/village/demo.json` and becomes the key used by Iris placements and later editing. Named arguments `structure=` and `name=` are aliases for `key=`; they do not select a separate vanilla structure or template. Omitted options default to `mode=planar`, `compatibility=iris`, `width=15`, `height=15`, `depth=15`, and `seed=1337`; `mode=` completes `planar` or `spatial`, while `compatibility=` completes `iris` or `vanilla`. Existing Iris keys tab-complete for `open`, `edit`, and `reopen`.
New Iris-compatible planar projects contain one owned piece for every archetype, assign every piece to the weighted `variant-1` structure theme, and mark the End piece terminal. New vanilla-compatible projects contain the same six owned pieces but omit Iris theme and terminal-rule metadata. Open an owned graph with `/iris jigsaw open <dimension> <key>` or the equivalent `edit`/`reopen` alias. Existing unowned Iris graphs use `adopt inspect` then `adopt apply`; managed datapack imports must be cloned. Registered vanilla or datapack jigsaws use `convert`, which creates a separate owned Iris graph. New Iris-compatible planar projects contain one owned piece for every archetype, assign every piece to the weighted `variant-1` structure theme, and mark the End piece terminal. New vanilla-compatible projects contain the same six owned pieces but omit Iris theme and terminal-rule metadata. Open an owned graph with `/iris jigsaw open <dimension> <key>` or the equivalent `edit`/`reopen` alias. Existing unowned Iris graphs use `adopt inspect` then `adopt apply`; managed datapack imports must be cloned. Registered vanilla or datapack jigsaws use `convert`, which creates a separate owned Iris graph.
The owner can open the six-row control GUI by right-clicking its protected chest, running `/iris jigsaw menu`, or starting three sneaks within 1.5 seconds. Walking into a workcell also makes that physical cell the owner's next menu selection. The GUI selects workcells, loads and creates variants, independently resizes variants, changes workcell capacity or enabled state, adjusts exact pool-entry weights and chances, edits theme membership and piece rules, toggles mandatory caps, navigates to the live preview, and deletes inactive variants or the complete project. Destructive actions require a second confirmation within 10 seconds. **New Blank Variant** clones the active owned piece's complete metadata and every exact pool membership but creates an empty object with the same dimensions; **Duplicate This Cell's Variant** preserves the same metadata and memberships while copying only that source object's bytes. **Duplicate All Enabled Cells as Family** atomically clones the loaded owned variant in every enabled workcell and rebinds the complete family together. All duplication uses service-generated keys and requires active owned variants with owned pool memberships. A duplication clicked during dirty or in-flight autosave is queued once, expedites autosave, and continues automatically only while the request, session, and source variants still match. Iris never chooses a first or lexicographically sorted pool as a fallback; use `/iris jigsaw piece create <poolKey> <pieceKey>` for an empty or unassigned workcell. The owner can open the six-row control GUI by right-clicking its protected chest, running `/iris jigsaw menu`, or starting three sneaks within 1.5 seconds. Walking into a workcell also makes that physical cell the owner's next menu selection, while left-clicking a workcell selects it and teleports the owner to its horizontal center. The GUI selects workcells, loads and creates variants, independently resizes variants, changes workcell capacity or enabled state, toggles per-workcell connector blocks, restores broken connector blocks from the saved variant, rewinds the latest autosave, adjusts exact pool-entry weights and chances, edits theme membership and piece rules, toggles mandatory caps, navigates to the live preview, and deletes inactive variants or the complete project. Destructive actions require a second confirmation within 10 seconds. **New Blank Variant** clones the active owned piece's complete metadata and every exact pool membership but creates an empty object with the same dimensions; **Duplicate This Cell's Variant** preserves the same metadata and memberships while copying only that source object's bytes. **Duplicate All Enabled Cells as Family** atomically clones the loaded owned variant in every enabled workcell and rebinds the complete family together. All duplication uses service-generated keys and requires active owned variants with owned pool memberships. A duplication clicked during dirty or in-flight autosave is queued once, expedites autosave, and continues automatically only while the request, session, and source variants still match. Iris never chooses a first or lexicographically sorted pool as a fallback; use `/iris jigsaw piece create <poolKey> <pieceKey>` for an empty or unassigned workcell.
The **Toolbox** page gives the player named stick items bound to the current Studio request and the selected workcell, variant, pool entry, or action. Variant/workcell rename sticks are renamed in an anvil, right-clicked to apply the 64-code-point label, and sneak-right-clicked to reset; control characters and section-sign formatting are rejected. Right-clicking another valid tool performs its action or opens the exact GUI context needed for capacity, per-variant size, themes, or rules. Bound tools use schema `2`; schema-`1` tools and sticks from a replaced or closed Studio are rejected. The active variant uses a jigsaw-block icon, valid evaluation uses an emerald, and lime dye is reserved for the explicitly labeled theme-membership toggle. Destructive stick tools also require a second right-click within 10 seconds. The **Toolbox** page gives the player named stick items bound to the current Studio request and the selected workcell, variant, pool entry, or action. Variant/workcell rename sticks are renamed in an anvil, right-clicked to apply the 64-code-point label, and sneak-right-clicked to reset; control characters and section-sign formatting are rejected. Right-clicking another valid tool performs its action or opens the exact GUI context needed for capacity, per-variant size, themes, or rules. Bound tools use schema `2`; schema-`1` tools and sticks from a replaced or closed Studio are rejected. The active variant uses a jigsaw-block icon, valid evaluation uses an emerald, and lime dye is reserved for the explicitly labeled theme-membership toggle. Destructive stick tools also require a second right-click within 10 seconds.
Building, marker, container, and machine changes inside a loaded owned workcell autosave after a 40-tick quiet period. Fresh untouched workcells report **Autosaved**. Later edits replace the pending capture identity, and a busy autosave retries until the current save/load/graph barrier permits it. Opening Mojang's jigsaw-block UI starts a persistent owning-region NBT watch; changed tile data marks the workcell dirty, and commands, tools, teleport/world changes, quit, graph operations, **Flush Autosave Now**, close, and enabled-world unload request a final tile snapshot before proceeding. Tracked events include block placement, breakage, buckets, growth, fluids, pistons, redstone, explosions, block-state interactions, recognized mutating commands, inventory click/drag/close plus internal move/pickup, and furnace, brewing-stand, dispenser, and crafter activity. **Flush Autosave Now** and `/iris jigsaw save` only request an immediate flush; if a barrier or scheduler prevents capture from starting, the same pending autosave remains queued for retry. They are not a required authoring step. Paper drains pending work synchronously during disable. A forced Folia plugin disable occurs after Folia rejects new region tasks, so close Studio or wait for `status` to report no pending autosave before a reload or server stop; that late disable hook cannot guarantee a new final cross-region capture. An external integration that bypasses Bukkit events must call `JigsawStudioService.markDirty(...)` or `markAllDirty(...)`. Building, marker, container, and machine changes inside a loaded owned workcell autosave after a 40-tick quiet period. Before each changed graph commit, Iris retains the prior complete ownership-manifest closure; identical resource blobs are deduplicated and the newest five iterations remain in one atomic `.iris/jigsaw-history/key-<sha256>.json` sidecar. **Undo Last Autosave** restores the newest entry through the same ownership writer, then reloads the affected active variant; repeated clicks rewind until the five-entry stack is empty. Project creation clears stale same-key history and project deletion removes it. Fresh untouched workcells report **Autosaved**. Later edits replace the pending capture identity, and a busy autosave retries until the current save/load/graph barrier permits it. When the owning player runs `/iris studio open` while Jigsaw Studio is active, Iris expedites and waits for these barriers, claims the close, and continues opening the ordinary Studio; console and non-owner replacement remain blocked. Opening Mojang's jigsaw-block UI starts a persistent owning-region NBT watch; changed tile data marks the workcell dirty, and commands, tools, teleport/world changes, quit, graph operations, **Flush Autosave Now**, close, and enabled-world unload request a final tile snapshot before proceeding. Tracked events include block placement, breakage, buckets, growth, fluids, pistons, redstone, explosions, block-state interactions, recognized mutating commands, inventory click/drag/close plus internal move/pickup, and furnace, brewing-stand, dispenser, and crafter activity. **Flush Autosave Now** and `/iris jigsaw save` only request an immediate flush; if a barrier or scheduler prevents capture from starting, the same pending autosave remains queued for retry. They are not a required authoring step. Paper drains pending work synchronously during disable. A forced Folia plugin disable occurs after Folia rejects new region tasks, so close Studio or wait for `status` to report no pending autosave before a reload or server stop; that late disable hook cannot guarantee a new final cross-region capture. An external integration that bypasses Bukkit events must call `JigsawStudioService.markDirty(...)` or `markAllDirty(...)`.
Each committed graph is compiled and assembled automatically with seed `1337`. The GUI reports `PENDING`, `VALID`, `WARNING`, `INVALID`, or `STALE`, the selected theme, piece count, and current detail. Iris keeps the assembled blocks on the negative-X side for the active Studio session, replaces them after later commits, and protects the complete preview bounds from edits, fluids, pistons, fire, growth, explosions, entities, and redstone. The live renderer accepts at most 250,000 explicit blocks; a larger result becomes `INVALID` with the render-limit diagnostic. **Go to Preview** or `/iris jigsaw preview goto` teleports above it. This live block preview is separate from `/iris jigsaw preview assemble`, which remains a temporary player-local particle diagnostic for an arbitrary seed. Each committed graph is compiled and assembled automatically with seed `1337`. The GUI reports `PENDING`, `VALID`, `WARNING`, `INVALID`, or `STALE`, the selected theme, piece count, and current detail. Iris keeps the assembled blocks on the negative-X side for the active Studio session, replaces them after later commits, and protects the complete preview bounds from edits, fluids, pistons, fire, growth, explosions, entities, and redstone. The live renderer accepts at most 250,000 explicit blocks; a larger result becomes `INVALID` with the render-limit diagnostic. **Go to Preview** or `/iris jigsaw preview goto` teleports above it. This live block preview is separate from `/iris jigsaw preview assemble`, which remains a temporary player-local particle diagnostic for an arbitrary seed.
Structure themes select one weighted family before assembly. **Duplicate All Enabled Cells as Family** allocates the next `variant-<n>` theme by default, clones the currently loaded owned variant from every enabled workcell with its exact object size and label, duplicates their pool memberships, assigns the new pieces to that family, and atomically loads the new family across those workcells. Individual loaded owned variants can join one or more declared themes; an empty theme list makes a piece available to every selected theme. Pool membership `chance` is an independent `0..1` eligibility gate applied before its positive relative weight. Piece rules constrain minimum/maximum depth, minimum/maximum placements, and terminal status. With mandatory caps enabled, an unresolved open connector must use its direct fallback to place a compatible terminal piece; failure rejects that assembly. Themes, chance gates, piece rules, and mandatory caps are Iris-only and block `VANILLA_PORTABLE` compilation or export when used. Structure themes select one weighted family before assembly. **Duplicate All Enabled Cells as Family** allocates the next `variant-<n>` theme by default, clones the currently loaded owned variant from every enabled workcell with its exact object size and label, duplicates their pool memberships, assigns the new pieces to that family, and atomically loads the new family across those workcells. Individual loaded owned variants can join one or more declared themes; an empty theme list makes a piece available to every selected theme. Pool membership `chance` is an independent `0..1` eligibility gate applied before its positive relative weight. Piece rules constrain minimum/maximum depth, minimum/maximum placements, and terminal status. With mandatory caps enabled, an unresolved open connector must use its direct fallback to place a compatible terminal piece; failure rejects that assembly. Themes, chance gates, piece rules, and mandatory caps are Iris-only and block `VANILLA_PORTABLE` compilation or export when used.
Mojang's jigsaw UI owns pool, name, target, joint, final state, and both signed priorities; `/iris jigsaw connector channel <channel|none>` changes the saved Iris-only channel for the exact targeted connector. Aqua particles mark the occupied workcell, dark gray marks nearby valid bounds, red marks invalid bounds or incomplete connector identity, lime marks a valid connector without a channel, and a channel receives a deterministic color. The Iris scoreboard switches automatically to Jigsaw context and shows the structure, author workcell label, canonical solver role when the label differs, loaded variant label, state, and `Triple-sneak for controls`; `/iris studio scoreboard` retains its session-only toggle behavior. Connector blocks are hidden per workcell by default and can be shown from **Workcell Settings**. **Reset Connector Blocks** rewrites every saved connector coordinate in the selected workcell from the active on-disk variant while leaving every other edited block unchanged; it restores jigsaw orientation and NBT while visible, or the exact final block and tile NBT while hidden. If an autosave already committed a deleted connector, use **Undo Last Autosave** first. Hidden capture retains each connector's pool, identity, orientation, priorities, channel, and authored order while the ordinary block and tile NBT at that coordinate become its exact final state; visible mode exposes Mojang's jigsaw UI for pool, name, target, joint, final state, and both signed priorities. `/iris jigsaw connector channel <channel|none>` changes the saved Iris-only channel for the exact targeted visible connector. Aqua particles mark the occupied workcell, dark gray marks nearby valid bounds, red marks invalid bounds or incomplete connector identity, lime marks a valid connector without a channel, and a channel receives a deterministic color. The Iris scoreboard switches automatically to Jigsaw context and shows the structure, author workcell label, canonical solver role when the label differs, loaded variant label, state, and `Triple-sneak for controls`; `/iris studio scoreboard` retains its session-only toggle behavior.
Bukkit has one global Studio project/world and one owning Jigsaw session. Only that owner can control or mutate it. Non-owner edits are cancelled, non-owner commands use a strict informational/communication allowlist, and the control chest plus live preview are protected. Autosave, variant switching, graph changes, opening, closing, and deletion share operation barriers. Close waits for clean state unless `discard=true`; discard is only for deliberately losing pending work. Bukkit has one global Studio project/world and one owning Jigsaw session. Only that owner can control or mutate it. Non-owner edits are cancelled, non-owner commands use a strict informational/communication allowlist, and the control chest plus live preview are protected. Autosave, variant switching, graph changes, opening, closing, and deletion share operation barriers. Close waits for clean state unless `discard=true`; discard is only for deliberately losing pending work.
Dimensions are capped at 128 blocks on X/Z, 192 on Y, and 2,097,152 blocks in total; planar variant and capacity width/depth must each be at least 3. A workcell capacity change persists only structure metadata, verifies every variant fits, leaves all object bytes unchanged, and requires close/reopen before the physical cage moves. `variant resize` and the **Variant Size** screen change only the selected owned object; lossless growth and shrink preserve in-bounds canonical content, reject cropped explicit air/blocks/tiles/connectors, and relocate planar canonical sockets. The loaded variant reloads in place; inactive variants remain untouched. **Resize to Capacity** or `/iris jigsaw piece expand` is a convenience for setting one selected object exactly to its current capacity. On Folia, each intersecting object chunk is read on its owning region and one graph write begins only after the full snapshot validates. Dimensions are capped at 128 blocks on X/Z, 192 on Y, and 2,097,152 blocks in total; planar variant and capacity width/depth must each be at least 3. A workcell capacity change persists only structure metadata, verifies every variant fits, leaves all object bytes unchanged, and atomically regenerates the affected live cages, objects, connector view, and block-entity hydration before editing resumes. `variant resize` and the **Variant Size** screen change only the selected owned object; lossless growth and shrink preserve in-bounds canonical content, reject cropped explicit air/blocks/tiles/connectors, and relocate planar canonical sockets. The loaded variant reloads in place; inactive variants remain untouched. **Resize to Capacity** or `/iris jigsaw piece expand` is a convenience for setting one selected object exactly to its current capacity. On Folia, each intersecting object chunk is read on its owning region and one graph write begins only after the full snapshot validates.
Deleting a variant is limited to an owned, inactive variant when another variant remains in that workcell. Project deletion first verifies ownership hashes and scans the pack for external JSON or ownership-manifest references; any reverse reference blocks deletion. A clear result closes Studio and removes the complete owned resource set through a hash-pinned transaction. If the post-close delete fails, the project files remain on disk for recovery. Deleting a variant is limited to an owned, inactive variant when another variant remains in that workcell. Project deletion first verifies ownership hashes and scans the pack for external JSON or ownership-manifest references; any reverse reference blocks deletion. A clear result closes Studio and removes the complete owned resource set through a hash-pinned transaction. If the post-close delete fails, the project files remain on disk for recovery.
+15 -15
View File
@@ -14,7 +14,7 @@ Prerequisites: a Bukkit-family Iris server, a writable pack under the Iris `pack
/iris jigsaw create overworld village/demo /iris jigsaw create overworld village/demo
``` ```
`village/demo` is the new structure key: it writes `structures/village/demo.json`, identifies the graph in Iris placements, and is the key used to reopen it later. `structure=` and `name=` are named aliases for `key=`, not references to a separate vanilla structure. With no optional arguments, creation defaults to planar mode, Iris-native compatibility, 16×16×16 initial workcells, and Studio seed `1337`. `mode=` tab-completes `planar` or `spatial`; `compatibility=` tab-completes `iris` or `vanilla`. Planar width and depth must each be at least `3`, X/Z cannot exceed `128`, Y must stay within `1..192`, and one workcell cannot exceed `2,097,152` blocks. Width and depth may differ. Creation is add-only: Iris refuses any occupied or conflicting target. `village/demo` is the new structure key: it writes `structures/village/demo.json`, identifies the graph in Iris placements, and is the key used to reopen it later. `structure=` and `name=` are named aliases for `key=`, not references to a separate vanilla structure. With no optional arguments, creation defaults to planar mode, Iris-native compatibility, 15×15×15 initial workcells, and Studio seed `1337`. `mode=` tab-completes `planar` or `spatial`; `compatibility=` completes `iris` or `vanilla`; existing Iris structure keys complete for `open`, `edit`, and `reopen`. Planar width and depth must each be at least `3`, X/Z cannot exceed `128`, Y must stay within `1..192`, and one workcell cannot exceed `2,097,152` blocks. Width and depth may differ. Creation is add-only: Iris refuses any occupied or conflicting target.
A planar project begins with one owned variant for each archetype, three owned pools, one `variant-1` theme set, and one ownership manifest: A planar project begins with one owned variant for each archetype, three owned pools, one `variant-1` theme set, and one ownership manifest:
@@ -39,15 +39,15 @@ Prerequisites: a Bukkit-family Iris server, a writable pack under the Iris `pack
/iris jigsaw particles true /iris jigsaw particles true
``` ```
Planar Studio has exactly six rotation-independent workcells in a three-column by two-row grid: Blank, End Cap, and Hallway on the first row; L Junction, T Junction, and Cross Junction on the second. Their stable IDs remain `blank`, `end`, `straight`, `corner`, `tee`, and `cross`. Neighboring capacities retain two clear blocks even when they differ. Each floor is light-gray wool, the canonical connector path is red wool, and every canonical face-center socket is capped with a sea lantern. There is no orientation, permutation, authored-piece, or derived-rotation gallery. Planar Studio has exactly six rotation-independent workcells in a three-column by two-row grid: Blank, End Cap, and Hallway on the first row; L Junction, T Junction, and Cross Junction on the second. Their stable IDs remain `blank`, `end`, `straight`, `corner`, `tee`, and `cross`. Neighboring capacity columns and rows retain at least one clear block even when their sizes differ. Each floor is light-gray wool, the canonical connector path is red wool, and every canonical face-center socket is capped with a sea lantern. There is no orientation, permutation, authored-piece, or derived-rotation gallery.
Aqua particles outline the workcell containing the player, dark gray outlines nearby valid workcells, and red identifies an invalid workcell. A focused connector is a 1.75-block direction line: lime for complete metadata with no Iris channel, red for incomplete identity metadata, or a deterministic channel color. `/iris jigsaw particles <true|false>` is player-local. The existing Iris scoreboard switches automatically to Jigsaw context and reports the structure, workcell, variant, and Loading, Saving, Disabled, Read-only, Invalid, Unsaved, or Saved state. `/iris studio scoreboard` toggles that sidebar for the current login. Aqua particles outline the workcell containing the player, dark gray outlines nearby valid workcells, and red identifies an invalid workcell. A focused connector is a 1.75-block direction line: lime for complete metadata with no Iris channel, red for incomplete identity metadata, or a deterministic channel color. `/iris jigsaw particles <true|false>` is player-local. The existing Iris scoreboard switches automatically to Jigsaw context and reports the structure, workcell, variant, and Loading, Saving, Disabled, Read-only, Invalid, Unsaved, or Saved state. `/iris studio scoreboard` toggles that sidebar for the current login.
3. Right-click the generated control chest with the main hand, run `/iris jigsaw menu`, or start three sneaks within 1.5 seconds. The six-row GUI shows the six workcells and pages only the variants belonging to the selected rotational archetype. Entering a workcell selects it for the owner's next menu open. Left-click **Hallway**, then click **New Blank Variant**. Iris clones the active owned piece's complete metadata and every exact pool-entry membership into a service-named owned piece, creates an empty object with the source object's dimensions, closes the GUI while the graph transaction runs, and loads the new variant into Hallway. Reopen the menu after the completion message. 3. Right-click the generated control chest with the main hand, run `/iris jigsaw menu`, or start three sneaks within 1.5 seconds. The six-row GUI shows the six workcells and pages only the variants belonging to the selected rotational archetype. Entering a workcell selects it for the owner's next menu open; left-clicking a workcell selects it, closes the menu, and teleports the owner to its horizontal center. Left-click **Hallway**, then click **New Blank Variant**. Iris clones the active owned piece's complete metadata and every exact pool-entry membership into a service-named owned piece, creates an empty object with the source object's dimensions, closes the GUI while the graph transaction runs, and loads the new variant into Hallway. Reopen the menu after the completion message.
New keys are deterministic, such as `village/demo/variants/straight/variant-1`. **Rename This Variant** and **Rename This Workcell** use an anvil text input; labels are author-facing only and do not change piece keys, stable workcell IDs, or solver archetypes. **Duplicate This Cell's Variant** clones the same complete piece metadata and every exact membership while copying the source object's bytes and its author-facing label. An End Cap clone therefore keeps both its pieces- and caps-pool entries, and a Cross Junction clone keeps both its start- and pieces-pool entries, including each entry's weight, chance, and other fields. Neither action guesses a first or lexicographically sorted owned pool. Both require an active owned variant with at least one owned membership; for an empty or unassigned workcell, use `/iris jigsaw piece create <poolKey> <pieceKey>` to select the pool explicitly. A non-owned variant cannot be duplicated or mutated. New keys are deterministic, such as `village/demo/variants/straight/variant-1`. **Rename This Variant** and **Rename This Workcell** use an anvil text input; labels are author-facing only and do not change piece keys, stable workcell IDs, or solver archetypes. **Duplicate This Cell's Variant** clones the same complete piece metadata and every exact membership while copying the source object's bytes and its author-facing label. An End Cap clone therefore keeps both its pieces- and caps-pool entries, and a Cross Junction clone keeps both its start- and pieces-pool entries, including each entry's weight, chance, and other fields. Neither action guesses a first or lexicographically sorted owned pool. Both require an active owned variant with at least one owned membership; for an empty or unassigned workcell, use `/iris jigsaw piece create <poolKey> <pieceKey>` to select the pool explicitly. A non-owned variant cannot be duplicated or mutated.
4. Enter Hallway and build only inside its aqua particle bounds. The workcell displays the active object's real blocks and overlays every saved connector as a real `minecraft:jigsaw` block. An existing planar piece authored in another direction is rotated automatically into canonical orientation; block states, connectors, positions, and final states rotate with it. Capture applies the inverse rotation so the source resources stay coherent. 4. Enter Hallway and build only inside its aqua particle bounds. The workcell displays the active object's real blocks with connector blocks hidden by default. **Workcell Settings** toggles real `minecraft:jigsaw` overlays when marker editing is needed. If a shown connector is broken, **Reset Connector Blocks** restores all saved connector coordinates without touching other edited blocks. An existing planar piece authored in another direction is rotated automatically into canonical orientation; block states, connectors, positions, and final states rotate with it. Capture applies the inverse rotation so the source resources stay coherent.
5. Configure each `minecraft:jigsaw` marker through Mojang's block UI. For a newly generated Hallway variant, the north and south markers already use: 5. Configure each `minecraft:jigsaw` marker through Mojang's block UI. For a newly generated Hallway variant, the north and south markers already use:
@@ -70,9 +70,9 @@ Prerequisites: a Bukkit-family Iris server, a writable pack under the Iris `pack
/iris jigsaw save /iris jigsaw save
``` ```
Autosave preserves the authored connector order for every marker that remains at the same source-local position, including markers whose metadata or orientation changed there. Removed markers disappear; new or moved markers append in deterministic X/Y/Z order, and duplicate source or captured positions reject the save instead of changing seeded assembly through world scan order. With connector blocks visible, autosave preserves the authored connector order for every marker that remains at the same source-local position, including markers whose metadata or orientation changed there. Removed markers disappear; new or moved markers append in deterministic X/Y/Z order. With connector blocks hidden, connector identity and order remain fixed while the exact ordinary block state and tile NBT placed at that coordinate are captured into the object and become that connector's final state. Duplicate source or captured positions reject the save.
`status` reports whether an autosave is pending. `/iris jigsaw save` and the GUI's **Flush Autosave Now** action request an immediate flush; if the final marker snapshot, another operation, or scheduler availability prevents capture from starting, the same pending ticket is retained and retried. Persistent validation or atomic-writer failures leave that mutation dirty, emit one console report with request, structure, workcell, and piece context, and retry after 2, 4, 8, 16, then at most every 30 seconds. A later edit resets that failure state; a manual flush attempts immediately without discarding it. Pending tickets resolve their workcell by stable ID after every committed graph reload, so one workcell save cannot strand sibling autosaves on replaced layout objects. Neither manual action is required in the normal loop. Fresh untouched workcells report **Autosaved**, not pending. Capture reads the active owned variant across its exact displayed dimensions, converts jigsaw blocks into connector metadata, writes each connector's `final_state` into the object cell, replaces only the piece JSON connector array so omitted defaults and extension fields remain intact, compiles the complete owned graph, then commits the JSON, `.iob`, and manifest together. The Jigsaw service directly invalidates, reloads, evaluates, and rematerializes these graph resources without running ordinary Studio's full-engine pack hotloader. If an object crosses chunks, Iris snapshots every intersection on that chunk's owning region and begins the write only after the complete capture validates. A failed or incomplete capture writes nothing. `status` reports whether an autosave is pending. `/iris jigsaw save` and the GUI's **Flush Autosave Now** action request an immediate flush; if the final marker snapshot, another operation, or scheduler availability prevents capture from starting, the same pending ticket is retained and retried. Each changed autosave retains the previous complete owned closure in one per-project history file; content blobs are deduplicated and only the newest five iterations remain. **Undo Last Autosave** restores and removes the newest retained iteration through the atomic writer, so it can be clicked repeatedly to rewind up to five saves. Persistent validation or atomic-writer failures leave that mutation dirty, emit one console report with request, structure, workcell, and piece context, and retry after 2, 4, 8, 16, then at most every 30 seconds. A later edit resets that failure state; a manual flush attempts immediately without discarding it. Pending tickets resolve their workcell by stable ID after every committed graph reload, so one workcell save cannot strand sibling autosaves on replaced layout objects. Neither manual action is required in the normal loop. Fresh untouched workcells report **Autosaved**, not pending. Capture reads the active owned variant across its exact displayed dimensions, converts jigsaw blocks into connector metadata, writes each connector's `final_state` into the object cell, replaces only the piece JSON connector array so omitted defaults and extension fields remain intact, compiles the complete owned graph, then commits the JSON, `.iob`, and manifest together. The Jigsaw service directly invalidates, reloads, evaluates, and rematerializes these graph resources without running ordinary Studio's full-engine pack hotloader. If an object crosses chunks, Iris snapshots every intersection on that chunk's owning region and begins the write only after the complete capture validates. A failed or incomplete capture writes nothing.
7. Inspect the session-persistent preview. Every committed mutation triggers a background compile and seed-`1337` assembly. The menu reports `PENDING`, `VALID`, `WARNING`, `INVALID`, or `STALE`, plus selected theme, piece count, and the current diagnostic. Iris renders the assembled blocks on the negative-X side of the workcells, keeps them until replacement or Studio close, and updates that read-only area after each later commit. Click **Go to Preview** or run `/iris jigsaw preview goto` to teleport above it. The preview bounds are protected from players, fluids, pistons, explosions, growth, fire, entities, and redstone. The renderer accepts at most 250,000 explicit blocks; a larger assembly becomes `INVALID` with the render-limit diagnostic and is not rendered. 7. Inspect the session-persistent preview. Every committed mutation triggers a background compile and seed-`1337` assembly. The menu reports `PENDING`, `VALID`, `WARNING`, `INVALID`, or `STALE`, plus selected theme, piece count, and the current diagnostic. Iris renders the assembled blocks on the negative-X side of the workcells, keeps them until replacement or Studio close, and updates that read-only area after each later commit. Click **Go to Preview** or run `/iris jigsaw preview goto` to teleport above it. The preview bounds are protected from players, fluids, pistons, explosions, growth, fire, entities, and redstone. The renderer accepts at most 250,000 explicit blocks; a larger assembly becomes `INVALID` with the render-limit diagnostic and is not rendered.
@@ -93,7 +93,7 @@ Prerequisites: a Bukkit-family Iris server, a writable pack under the Iris `pack
Piece themes, non-default chance, piece rules, and mandatory caps are Iris-only metadata. A graph using them is not `VANILLA_PORTABLE`. Piece themes, non-default chance, piece rules, and mandatory caps are Iris-only metadata. A graph using them is not `VANILLA_PORTABLE`.
9. Resize or disable workcells as needed. Open **Workcell Settings** and adjust capacity width, height, or depth by 1 or 8. Every planar workcell persists its own capacity; changing it never rewrites a variant object. A capacity cannot shrink below any variant already assigned to that cell. Open **Variant Size** to give the selected owned variant its own exact width, height, and depth within that capacity. Growth adds air; a safe shrink preserves in-bounds blocks and moves canonical connector payloads and sockets to the new face centers, while cropping or collision rejects the transaction without writes. **Resize This Variant to Capacity** is the one-click exact-size shortcut. A loaded resized variant reloads in place after commit; sibling variants keep their independent dimensions and bytes. 9. Resize or disable workcells as needed. Open **Workcell Settings** and adjust capacity width, height, or depth by 1 or 8. Iris regenerates moved cages and their active variants in place, keeps one clear block between capacity rows and columns, rehydrates tile data, and moves the owner with the selected workcell; reopening is only the recovery path if that live regeneration fails. Every planar workcell persists its own capacity; changing it never rewrites a variant object. A capacity cannot shrink below any variant already assigned to that cell. Open **Variant Size** to give the selected owned variant its own exact width, height, and depth within that capacity. Growth adds air; a safe shrink preserves in-bounds blocks and moves canonical connector payloads and sockets to the new face centers, while cropping or collision rejects the transaction without writes. **Resize This Variant to Capacity** is the one-click exact-size shortcut. A loaded resized variant reloads in place after commit; sibling variants keep their independent dimensions and bytes.
Disabling a planar workcell removes all pieces of that archetype from assembly and vanilla export but preserves its size and variants for later editing. A red stained-glass block display fills the disabled bounds; Iris removes its tracked display when the origin chunk unloads and recreates it after that chunk loads again. Re-enable the workcell from the same settings page to restore participation. Disabling a planar workcell removes all pieces of that archetype from assembly and vanilla export but preserves its size and variants for later editing. A red stained-glass block display fills the disabled bounds; Iris removes its tracked display when the origin chunk unloads and recreates it after that chunk loads again. Re-enable the workcell from the same settings page to restore participation.
@@ -183,7 +183,7 @@ Create additional owned pools before targeting them from new spatial markers or
## Studio workcells and canonical planar display ## Studio workcells and canonical planar display
The surrounding platform uses a four-block checker pattern, and smooth-quartz cages plus particles identify each workcell's editable volume. The first workcell origin is `(16, 65, 16)`; every workcell's bounds begin at Y 65, one block above its floor, and that origin is the displayed object's lowest unsigned corner. Planar projects use six cells in this exact three-by-two order. Each column uses the widest workcell in that column, each row uses the deepest workcell in that row, and adjacent bounds retain two clear blocks: The surrounding platform uses a four-block checker pattern, and smooth-quartz cages plus particles identify each workcell's editable volume. The first workcell origin is `(16, 65, 16)`; every workcell's bounds begin at Y 65, one block above its floor, and that origin is the displayed object's lowest unsigned corner. Planar projects use six cells in this exact three-by-two order. Each column uses the widest workcell in that column, each row uses the deepest workcell in that row, and adjacent column and row envelopes retain one clear block. A smaller workcell can have additional open space beside it because its row and column remain aligned to the largest workcell in that envelope:
| Row | Workcell | Stable ID | Canonical open sides | | Row | Workcell | Stable ID | Canonical open sides |
|---|---|---|---| |---|---|---|---|
@@ -256,7 +256,7 @@ The create/open `<key>` is the root structure's internal lowercase resource path
| Command | Behavior | | Command | Behavior |
|---|---| |---|---|
| `create <dimension> <key> [mode=planar] [compatibility=iris] [width=16] [height=16] [depth=16] [seed=1337]` | Add-only atomic creation of a complete owned graph followed by an open request; mode completes `planar`/`spatial`, compatibility completes `iris`/`vanilla`, planar X/Z are `3..128`, spatial X/Z are `1..128`, Y is `1..192`, and one workcell volume is at most `2,097,152` | | `create <dimension> <key> [mode=planar] [compatibility=iris] [width=15] [height=15] [depth=15] [seed=1337]` | Add-only atomic creation of a complete owned graph followed by an open request; mode completes `planar`/`spatial`, compatibility completes `iris`/`vanilla`, existing keys complete for `open`/`edit`/`reopen`, planar X/Z are `3..128`, spatial X/Z are `1..128`, Y is `1..192`, and one workcell volume is at most `2,097,152` |
| `convert <dimension> <registered-key> [target=auto] [seed=1337]` | Add-only conversion of one live registered vanilla/datapack jigsaw into an owned Iris graph, followed by Studio open; aliases `import`, `import-vanilla` | | `convert <dimension> <registered-key> [target=auto] [seed=1337]` | Add-only conversion of one live registered vanilla/datapack jigsaw into an owned Iris graph, followed by Studio open; aliases `import`, `import-vanilla` |
| `adopt inspect <dimension> <source> [target=auto] [strategy=auto]` | Asynchronously inspect a complete existing Iris closure and issue a 15-minute, hash-pinned `IN_PLACE`, `CLONE_REQUIRED`, or `BLOCKED` plan; strategy completes `auto`, `in-place`, or `clone` | | `adopt inspect <dimension> <source> [target=auto] [strategy=auto]` | Asynchronously inspect a complete existing Iris closure and issue a 15-minute, hash-pinned `IN_PLACE`, `CLONE_REQUIRED`, or `BLOCKED` plan; strategy completes `auto`, `in-place`, or `clone` |
| `adopt apply <planId>` | Revalidate and atomically apply a plan owned by that player, then open the target with seed `1337`; no Studio may be active or opening | | `adopt apply <planId>` | Revalidate and atomically apply a plan owned by that player, then open the target with seed `1337`; no Studio may be active or opening |
@@ -269,7 +269,7 @@ The create/open `<key>` is the root structure's internal lowercase resource path
| `particles <visible>` | Toggle player-local bounds and connector particles | | `particles <visible>` | Toggle player-local bounds and connector particles |
| `save [bay=selected]` | Flush automatic capture now for one dirty ready workcell; ordinary block and container changes already schedule this operation | | `save [bay=selected]` | Flush automatic capture now for one dirty ready workcell; ordinary block and container changes already schedule this operation |
| `connector channel <channel\|none>` | Look at a saved marker in the active owned workcell within 8 blocks and set/clear its Iris-only channel at the inverse-mapped source position; reopen to refresh the workcell and particles | | `connector channel <channel\|none>` | Look at a saved marker in the active owned workcell within 8 blocks and set/clear its Iris-only channel at the inverse-mapped source position; reopen to refresh the workcell and particles |
| `bounds <width> <height> <depth>` | Set the selected workcell capacity without rewriting any variant object; all variants must fit, live geometry stays unchanged, and editing pauses until close/reopen; aliases `cell`, `resize` | | `bounds <width> <height> <depth>` | Set the selected workcell capacity without rewriting any variant object; all variants must fit, and the live aligned layout regenerates and rehydrates without close/reopen; aliases `cell`, `resize` |
| `workcell capacity <width> <height> <depth>` | Explicit nested form of `bounds`; planar capacity belongs to one canonical archetype and spatial capacity is the single project envelope | | `workcell capacity <width> <height> <depth>` | Explicit nested form of `bounds`; planar capacity belongs to one canonical archetype and spatial capacity is the single project envelope |
| `workcell label <displayName>` | Set the selected planar or spatial workcell's author-facing label; quote spaces; canonical solver identity remains unchanged | | `workcell label <displayName>` | Set the selected planar or spatial workcell's author-facing label; quote spaces; canonical solver identity remains unchanged |
| `workcell label-reset` | Reset the selected workcell to its canonical solver label; alias `reset-label` | | `workcell label-reset` | Reset the selected workcell to its canonical solver label; alias `reset-label` |
@@ -306,7 +306,7 @@ There is no world-edit undo command for Jigsaw Studio. A successful graph transa
### Capacity and per-variant object size ### Capacity and per-variant object size
`bounds` and `workcell capacity` target the selected workcell. Planar workcells persist independent width, height, depth, enabled state, and display label; spatial mode persists one shared `cellSize` plus `spatialWorkcellDisplayName`. Planar capacity width/depth are `3..128`, spatial width/depth are `1..128`, height is `1..192`, and volume is at most `2,097,152`. Capacity is an upper bound for every variant in that workcell. A successful capacity change updates only structure JSON, verifies the complete graph, leaves every object byte unchanged, retains the old live cage, and blocks editing until close/reopen regenerates Studio. `bounds` and `workcell capacity` target the selected workcell. Planar workcells persist independent width, height, depth, enabled state, and display label; spatial mode persists one shared `cellSize` plus `spatialWorkcellDisplayName`. Planar capacity width/depth are `3..128`, spatial width/depth are `1..128`, height is `1..192`, and volume is at most `2,097,152`. Capacity is an upper bound for every variant in that workcell. A successful capacity change updates only structure JSON, verifies the complete graph, leaves every object byte unchanged, regenerates and rehydrates the affected live layout, and teleports the owner to the selected cell's new horizontal center when that cell moves. A failed live regeneration restores the prior layout and requires reopen only as an explicit recovery boundary.
`variant resize` and the **Variant Size** screen target one owned variant. The exact requested width, height, and depth must fit its workcell capacity. Growth and shrink preserve blocks and tiles at their in-bounds canonical coordinates, account for rectangular source rotations, and relocate each planar canonical connector plus its stored block payload to the new face center. Shrink is lossless only: any stored block, including explicit air, or tile outside the target; a connector destination collision; connector tile data that cannot move safely; a read-only object; or an object shared by another piece rejects the transaction before any authored file changes. New growth volume is air. A loaded variant reloads in place after commit; siblings keep their dimensions and bytes. Marker block-entity data is applied on its owning region before Iris verifies either the candidate or its rollback, so live resize cannot reject a valid marker merely because its NBT merge was deferred to the next tick. `variant resize` and the **Variant Size** screen target one owned variant. The exact requested width, height, and depth must fit its workcell capacity. Growth and shrink preserve blocks and tiles at their in-bounds canonical coordinates, account for rectangular source rotations, and relocate each planar canonical connector plus its stored block payload to the new face center. Shrink is lossless only: any stored block, including explicit air, or tile outside the target; a connector destination collision; connector tile data that cannot move safely; a read-only object; or an object shared by another piece rejects the transaction before any authored file changes. New growth volume is air. A loaded variant reloads in place after commit; siblings keep their dimensions and bytes. Marker block-entity data is applied on its owning region before Iris verifies either the candidate or its rollback, so live resize cannot reject a valid marker merely because its NBT merge was deferred to the next tick.
@@ -552,7 +552,7 @@ Test the exported artifact on an unmodded Minecraft 26.2 server or client: stop
| An external plugin edit is not captured | The plugin bypassed Bukkit's covered mutation events | Have the integration call `JigsawStudioService.markDirty(...)` for affected coordinates or `markAllDirty(...)`; autosave then follows normally | | An external plugin edit is not captured | The plugin bypassed Bukkit's covered mutation events | Have the integration call `JigsawStudioService.markDirty(...)` for affected coordinates or `markAllDirty(...)`; autosave then follows normally |
| Autosave has no active/editable variant | The workcell is empty or its loaded variant is read-only | Load an owned variant, or adopt/clone the graph first | | Autosave has no active/editable variant | The workcell is empty or its loaded variant is read-only | Load an owned variant, or adopt/clone the graph first |
| Autosave reports Loading, Invalid, or not hydrated | Variant materialization or real jigsaw block-entity hydration is incomplete/failed | Wait for completion, reopen or reload the variant, and do not build until the scoreboard reports a stable state | | Autosave reports Loading, Invalid, or not hydrated | Variant materialization or real jigsaw block-entity hydration is incomplete/failed | Wait for completion, reopen or reload the variant, and do not build until the scoreboard reports a stable state |
| Capacity succeeds but the cage is still old | Capacity changes deliberately retain the current generated layout and never rewrite variant objects | Close and reopen Studio before editing; resize individual variants separately when their geometry should change | | Capacity succeeds but live regeneration reports a failure | The metadata committed, but one owning-region repaint or hydration step failed | Close and reopen Studio before editing; the persisted capacity remains authoritative |
| Autosave says a chunk is not loaded | Part of the capture volume is unloaded | Visit/load the whole workcell; the autosave retry remains pending, or use **Flush Autosave Now** after loading it | | Autosave says a chunk is not loaded | Part of the capture volume is unloaded | Visit/load the whole workcell; the autosave retry remains pending, or use **Flush Autosave Now** after loading it |
| Multi-chunk autosave aborts | One owning-region schedule/snapshot failed, a chunk unloaded, Studio changed, marker/tile capture failed, or aggregation was incomplete/invalid | Keep the complete capture volume loaded and fix the reported cause; no graph file is written from a partial capture | | Multi-chunk autosave aborts | One owning-region schedule/snapshot failed, a chunk unloaded, Studio changed, marker/tile capture failed, or aggregation was incomplete/invalid | Keep the complete capture volume loaded and fix the reported cause; no graph file is written from a partial capture |
| Marker capture fails | Marker NBT is incomplete, final state is invalid, or active NMS cannot serialize the tile | Fix the named marker field or use the matching supported Bukkit/NMS build | | Marker capture fails | Marker NBT is incomplete, final state is invalid, or active NMS cannot serialize the tile | Fix the named marker field or use the matching supported Bukkit/NMS build |
@@ -573,12 +573,12 @@ Test the exported artifact on an unmodded Minecraft 26.2 server or client: stop
Run this in a purpose-named disposable pack/world and record each gate separately. Run this in a purpose-named disposable pack/world and record each gate separately.
1. **Creation:** create a planar `IRIS_EXTENDED` project without optional mode, compatibility, dimensions, or seed. Confirm planar/Iris/16×16×16/1337 defaults, one structure, three pools, six pieces, six objects, one ownership manifest, and no partial files after a duplicate-create rejection. 1. **Creation:** create a planar `IRIS_EXTENDED` project without optional mode, compatibility, dimensions, or seed. Confirm planar/Iris/15×15×15/1337 defaults, one structure, three pools, six pieces, six objects, one ownership manifest, tab completion of its key for `open`/`edit`/`reopen`, and no partial files after a duplicate-create rejection.
2. **Default catalog:** confirm all six workcells have one loaded owned variant, `variant-1` is the selected theme family, End is terminal, and mandatory caps are initially off. 2. **Default catalog:** confirm all six workcells have one loaded owned variant, `variant-1` is the selected theme family, End is terminal, and mandatory caps are initially off.
3. **Workcell layout:** verify Blank/End Cap/Hallway then L Junction/T Junction/Cross Junction, two clear blocks between capacities, light-gray floors, red canonical glyphs, sea-lantern endpoints, and no orientation/permutation gallery. 3. **Workcell layout:** verify Blank/End Cap/Hallway then L Junction/T Junction/Cross Junction, one clear block between capacity rows and columns, light-gray floors, red canonical glyphs, sea-lantern endpoints, and no orientation/permutation gallery.
4. **Controls and context:** confirm every untouched workcell starts **Autosaved**. Walk outside and into End Cap; verify the Iris scoreboard context and `Triple-sneak for controls`, then open the menu and confirm End Cap is selected. Rename its workcell and active variant sticks in an anvil, apply them, verify the scoreboard shows the author names plus canonical role, then reset both labels. 4. **Controls and context:** confirm every untouched workcell starts **Autosaved**. Walk outside and into End Cap; verify the Iris scoreboard context and `Triple-sneak for controls`, then open the menu and confirm End Cap is selected. Rename its workcell and active variant sticks in an anvil, apply them, verify the scoreboard shows the author names plus canonical role, then reset both labels.
5. **Autosave:** change a solid block, a marker field, and container contents. Immediately click **Duplicate This Cell's Variant**; confirm autosave is expedited and the duplicate runs once automatically without a wait/retry instruction. Repeat with edits in multiple enabled cells and **Duplicate All Enabled Cells as Family**. Wait for the final clean state, reopen Studio, and verify all authored changes plus both clone operations round-trip. 5. **Autosave:** change a solid block, a marker field, and container contents. Immediately click **Duplicate This Cell's Variant**; confirm autosave is expedited and the duplicate runs once automatically without a wait/retry instruction. Repeat with edits in multiple enabled cells and **Duplicate All Enabled Cells as Family**. Wait for the final clean state, reopen Studio, and verify all authored changes plus both clone operations round-trip.
6. **Capacity and independent sizes:** make Hallway capacity `16×3×3` and another workcell capacity `16×8×16`; confirm no existing object byte changes and close/reopen moves only the cages. In the larger workcell, resize one variant to `16×3×16` and another to `3×3×3`; confirm exact independent dimensions, live reload of the loaded variant, and unchanged siblings. Confirm cropped authored content, connector collision, and shared/read-only objects each reject the single-variant resize without writes. 6. **Capacity and independent sizes:** make Hallway capacity `16×3×3` and another workcell capacity `16×8×16`; confirm no existing object byte changes and the live relayout moves only the cages without close/reopen. In the larger workcell, resize one variant to `16×3×16` and another to `3×3×3`; confirm exact independent dimensions, live reload of the loaded variant, and unchanged siblings. Confirm cropped authored content, connector collision, and shared/read-only objects each reject the single-variant resize without writes.
7. **Disable:** disable Tee, confirm a full red stained-glass display fills that workcell, and confirm seed-`1337` evaluation excludes Tee pieces. Re-enable it and confirm participation returns; test export filtering separately on the portable fixture. 7. **Disable:** disable Tee, confirm a full red stained-glass display fills that workcell, and confirm seed-`1337` evaluation excludes Tee pieces. Re-enable it and confirm participation returns; test export filtering separately on the portable fixture.
8. **Dynamic preview:** confirm evaluation moves through pending/stale to valid or an understood warning, reports theme/piece count, and renders the same protected block assembly on the negative-X side after reopen. Reach it through both **Go to Preview** and `/iris jigsaw preview goto`; verify edits, fluids, pistons, explosions, growth, fire, entities, and redstone cannot alter it. 8. **Dynamic preview:** confirm evaluation moves through pending/stale to valid or an understood warning, reports theme/piece count, and renders the same protected block assembly on the negative-X side after reopen. Reach it through both **Go to Preview** and `/iris jigsaw preview goto`; verify edits, fluids, pistons, explosions, growth, fire, entities, and redstone cannot alter it.
9. **Variants and rules:** create a blank variant and duplicate one active variant; adjust one exact weight and chance; create `variant-2` through the all-enabled family action and confirm one exact-size clone per enabled workcell, duplicated memberships, and atomic active-family rebind. Change theme membership, depth/count rules, terminal status, and mandatory caps. Confirm only selected resources change and invalid rules fail atomically. 9. **Variants and rules:** create a blank variant and duplicate one active variant; adjust one exact weight and chance; create `variant-2` through the all-enabled family action and confirm one exact-size clone per enabled workcell, duplicated memberships, and atomic active-family rebind. Change theme membership, depth/count rules, terminal status, and mandatory caps. Confirm only selected resources change and invalid rules fail atomically.
+4 -4
View File
@@ -169,7 +169,7 @@ Use a disposable pack/structure key and the owning builder account. Bukkit has o
/iris jigsaw status /iris jigsaw status
``` ```
Gate: the add-only transaction owns one structure, three pools, six pieces, six objects, and one manifest before Studio opens. The player enters creative above Blank. `status` reports `PLANAR_JIGSAW`, `IRIS_EXTENDED`, six workcells, 16×16×16 for the selected workcell, six variants, no pending autosave, and the seed-`1337` evaluation. The GUI and owned resources show one loaded variant per archetype, theme `variant-1`, terminal End, and mandatory caps off. Gate: the add-only transaction owns one structure, three pools, six pieces, six objects, and one manifest before Studio opens. The player enters creative above Blank. `status` reports `PLANAR_JIGSAW`, `IRIS_EXTENDED`, six workcells, 15×15×15 for the selected workcell, six variants, no pending autosave, and the seed-`1337` evaluation. The key tab-completes for `open`, `edit`, and `reopen`; the GUI and owned resources show one loaded variant per archetype, theme `variant-1`, terminal End, and mandatory caps off.
2. Inspect the exact Blank, End Cap, Hallway, L Junction, T Junction, Cross Junction layout. Floors are light-gray wool, topology paths are red wool, and canonical endpoints are sea lanterns. There are no orientation, permutation, piece, or derived-rotation cells. Toggle player-local particles: 2. Inspect the exact Blank, End Cap, Hallway, L Junction, T Junction, Cross Junction layout. Floors are light-gray wool, topology paths are red wool, and canonical endpoints are sea lanterns. There are no orientation, permutation, piece, or derived-rotation cells. Toggle player-local particles:
@@ -185,7 +185,7 @@ Use a disposable pack/structure key and the owning builder account. Bukkit has o
3. Open the same six-row controls three ways: right-click the protected chest, run `/iris jigsaw menu`, and start three sneaks within 1.5 seconds. Select Hallway and click **New Blank Variant**. Wait for its atomic graph result and load, then reopen the controls. Rename the loaded variant and Hallway workcell through their anvil inputs; confirm labels round-trip while the piece key, `straight` stable ID, and solver role stay unchanged. Load End Cap and use **Duplicate This Cell's Variant**, then load Cross Junction and duplicate it as well. 3. Open the same six-row controls three ways: right-click the protected chest, run `/iris jigsaw menu`, and start three sneaks within 1.5 seconds. Select Hallway and click **New Blank Variant**. Wait for its atomic graph result and load, then reopen the controls. Rename the loaded variant and Hallway workcell through their anvil inputs; confirm labels round-trip while the piece key, `straight` stable ID, and solver role stay unchanged. Load End Cap and use **Duplicate This Cell's Variant**, then load Cross Junction and duplicate it as well.
Gate: the new key follows `smoke/jigsaw/variants/straight/variant-<n>` and loads into Hallway. It has the source piece's complete metadata and exact pool entries but an empty same-sized object. At the default 16×16×16, its two real markers occupy `(8,8,0)` and `(8,8,15)`, face north/south with top `UP_POSITIVE_Y`, show pool `iris:smoke/jigsaw/pieces`, use name/target `iris:planar`, `ALIGNED`, `minecraft:structure_void`, and signed priorities `0`. Mojang's UI is usable after hydration. Each duplicate copies the active object's bytes, display label, and complete piece metadata. The End Cap duplicate has exact matching entries in both `smoke/jigsaw/pieces` and `smoke/jigsaw/caps`; the Cross Junction duplicate has exact matching entries in both `smoke/jigsaw/start` and `smoke/jigsaw/pieces`. An empty or unassigned workcell refuses both GUI actions and directs the operator to `/iris jigsaw piece create <poolKey> <pieceKey>` instead of choosing a fallback pool. Gate: the new key follows `smoke/jigsaw/variants/straight/variant-<n>` and loads into Hallway. It has the source piece's complete metadata and exact pool entries but an empty same-sized object. At the default 15×15×15, its two real markers occupy `(7,7,0)` and `(7,7,14)`, face north/south with top `UP_POSITIVE_Y`, show pool `iris:smoke/jigsaw/pieces`, use name/target `iris:planar`, `ALIGNED`, `minecraft:structure_void`, and signed priorities `0`. Mojang's UI is usable after hydration. Break one marker and click **Reset Connector Blocks** before autosave; both saved markers must return while another edited block remains unchanged. Each duplicate copies the active object's bytes, display label, and complete piece metadata. The End Cap duplicate has exact matching entries in both `smoke/jigsaw/pieces` and `smoke/jigsaw/caps`; the Cross Junction duplicate has exact matching entries in both `smoke/jigsaw/start` and `smoke/jigsaw/pieces`. An empty or unassigned workcell refuses both GUI actions and directs the operator to `/iris jigsaw piece create <poolKey> <pieceKey>` instead of choosing a fallback pool.
4. Change one permanent block, one marker field, and one chest inventory inside Hallway. Keep the permanent block and chest within the later 16×3×3 target, such as Y/Z offsets `1,1`. After changing a marker field in Mojang's UI, immediately run `/iris jigsaw status`; change it again and immediately run `/iris jigsaw close`. Also trigger internal inventory transfer or hopper pickup and at least one furnace, brewing-stand, dispenser, or crafter update inside the workcell. Do not flush autosave. Wait at least 40 ticks after the final update, then inspect status: 4. Change one permanent block, one marker field, and one chest inventory inside Hallway. Keep the permanent block and chest within the later 16×3×3 target, such as Y/Z offsets `1,1`. After changing a marker field in Mojang's UI, immediately run `/iris jigsaw status`; change it again and immediately run `/iris jigsaw close`. Also trigger internal inventory transfer or hopper pickup and at least one furnace, brewing-stand, dispenser, or crafter update inside the workcell. Do not flush autosave. Wait at least 40 ticks after the final update, then inspect status:
@@ -193,7 +193,7 @@ Use a disposable pack/structure key and the owning builder account. Bukkit has o
/iris jigsaw status /iris jigsaw status
``` ```
Gates: the command and close attempt request a final owning-region marker snapshot; close waits behind marker finalization and autosave instead of losing the last UI change. State moves through dirty/saving to clean automatically; the inventory and machine changes also mark it dirty; one complete multi-resource commit occurs; and no partial resource appears. Make another edit while capture is pending, immediately click **Duplicate This Cell's Variant**, and confirm Iris expedites autosave then performs that one duplicate exactly once without a wait/retry instruction. Repeat with dirty edits in multiple enabled cells and **Duplicate All Enabled Cells as Family**. Invoke **Flush Autosave Now** while capture cannot start and confirm the same ticket remains pending, retries, and eventually becomes clean. Close/reopen, load the variant, and confirm block, marker NBT, inventory, explicit-air final state when used, and `structure_void` absence round-trip. **Flush Autosave Now** and `/iris jigsaw save` are not required. Gates: the command and close attempt request a final owning-region marker snapshot; close waits behind marker finalization and autosave instead of losing the last UI change. State moves through dirty/saving to clean automatically; the inventory and machine changes also mark it dirty; one complete multi-resource commit occurs; and no partial resource appears. Make six distinct saved block edits, then click **Undo Last Autosave** five times. Confirm each prior block state and manifest hash returns in reverse order, the sixth-oldest state is no longer available, one `.iris/jigsaw-history/key-<sha256>.json` file held the stack, and no transaction debris remains. Make another edit while capture is pending, immediately click **Duplicate This Cell's Variant**, and confirm Iris expedites autosave then performs that one duplicate exactly once without a wait/retry instruction. Repeat with dirty edits in multiple enabled cells and **Duplicate All Enabled Cells as Family**. Invoke **Flush Autosave Now** while capture cannot start and confirm the same ticket remains pending, retries, and eventually becomes clean. Close/reopen, load the variant, and confirm block, marker NBT, inventory, explicit-air final state when used, and `structure_void` absence round-trip. **Flush Autosave Now** and `/iris jigsaw save` are not required.
On Paper, repeat one dirty edit immediately before plugin disable and confirm the synchronous final drain persists it. On Folia, verify an enabled-world unload or unregister remains deferred and retries until autosave finishes. Record the forced-disable boundary separately: once Folia has disabled the plugin it rejects new region tasks, so a new final cross-region capture cannot be guaranteed. Close Studio or wait for `status` to report no pending autosave before reload or server shutdown. On Paper, repeat one dirty edit immediately before plugin disable and confirm the synchronous final drain persists it. On Folia, verify an enabled-world unload or unregister remains deferred and retries until autosave finishes. Record the forced-disable boundary separately: once Folia has disabled the plugin it rejects new region tasks, so a new final cross-region capture cannot be guaranteed. Close Studio or wait for `status` to report no pending autosave before reload or server shutdown.
@@ -239,7 +239,7 @@ Use a disposable pack/structure key and the owning builder account. Bukkit has o
/iris jigsaw piece expand /iris jigsaw piece expand
``` ```
Gate: spatial capacity and its author-facing workcell label persist while the live layout stays unchanged and require reopen. `piece expand` resizes only the active object to 48×24×32; another smaller variant keeps its dimensions. Change blocks in separated chunks of the expanded workcell without manually flushing autosave. With all intersections loaded, automatic capture schedules each intersection on its owning region and commits once after complete validation. Repeat with one intersection unloaded and confirm no owned file changes. Automated coordinator tests are not live Folia proof. Gate: spatial capacity and its author-facing workcell label persist while the live layout regenerates and rehydrates without reopen. `piece expand` resizes only the active object to 48×24×32; another smaller variant keeps its dimensions. Change blocks in separated chunks of the expanded workcell without manually flushing autosave. With all intersections loaded, automatic capture schedules each intersection on its owning region and commits once after complete validation. Repeat with one intersection unloaded and confirm no owned file changes. Automated coordinator tests are not live Folia proof.
15. Reopen a retained Iris project, attach it to a dimension/region/biome placement with a unique `placementId`, validate the pack, and generate new chunks. Gate natural occurrence separately from Studio preview. For cave work, first generate the mantle, then verify no-anchor chunks skip and actual anchors align as described in `15 - Caves & Carving.md`. 15. Reopen a retained Iris project, attach it to a dimension/region/biome placement with a unique `placementId`, validate the pack, and generate new chunks. Gate natural occurrence separately from Studio preview. For cave work, first generate the mantle, then verify no-anchor chunks skip and actual anchors align as described in `15 - Caves & Carving.md`.