(removed ai file from when i generated docs)
This commit is contained in:
Brian Neumann-Fopiano
2026-08-10 15:47:57 -04:00
parent ebfe278b3b
commit 998a5c9f5f
256 changed files with 49776 additions and 1221 deletions
@@ -37,14 +37,11 @@ import java.nio.channels.FileChannel;
import java.nio.charset.StandardCharsets;
import java.nio.file.AtomicMoveNotSupportedException;
import java.nio.file.DirectoryStream;
import java.nio.file.FileVisitResult;
import java.nio.file.Files;
import java.nio.file.LinkOption;
import java.nio.file.Path;
import java.nio.file.SimpleFileVisitor;
import java.nio.file.StandardCopyOption;
import java.nio.file.StandardOpenOption;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.ArrayList;
import java.util.Collection;
import java.util.LinkedHashMap;
@@ -341,40 +338,31 @@ public final class PendingWorldDeleteQueue implements WorldDeletionQueue {
LinkedHashMap<String, String> remaining
) {
try {
QueueEntry entry = QueueEntry.parse(worldName, levelRoot.getName());
List<DeleteTarget> targets = entry.targets(levelRoot);
if (targets.stream().anyMatch(PendingWorldDeleteQueue::isLoaded)) {
EntryDeletionResult result = attemptEntry(
levelRoot,
worldName,
PendingWorldDeleteQueue::isLoaded
);
if (result.loaded()) {
Iris.warn("Skipping queued deletion for \"" + worldName + "\" because it is currently loaded.");
remaining.put(worldName.toLowerCase(Locale.ROOT), worldName);
return;
}
boolean foundAny = false;
boolean deletedAll = true;
for (DeleteTarget target : targets) {
Path worldFolder = target.path();
if (!Files.exists(worldFolder, LinkOption.NOFOLLOW_LINKS)) {
continue;
}
if (Files.isSymbolicLink(worldFolder) || !Files.isDirectory(worldFolder, LinkOption.NOFOLLOW_LINKS)) {
throw new IOException("Queued world target is not a safe directory: " + worldFolder);
}
foundAny = true;
try {
deleteTree(worldFolder);
Iris.info("Deleted queued world folder \"" + worldFolder.getFileName() + "\".");
} catch (IOException failure) {
deletedAll = false;
Iris.reportError("Failed to delete queued world folder \"" + worldFolder + "\".", failure);
}
for (Path deleted : result.deleted()) {
Iris.info("Deleted queued world folder \"" + deleted.getFileName() + "\".");
}
if (!foundAny) {
for (DeletionFailure deletionFailure : result.failures()) {
Iris.reportError(
"Failed to delete queued world folder \"" + deletionFailure.path() + "\".",
deletionFailure.failure()
);
}
if (!result.foundAny()) {
Iris.info("Queued world deletion skipped for \"" + worldName + "\" (folder missing).");
return;
}
if (!deletedAll) {
if (result.retainQueueEntry()) {
remaining.put(worldName.toLowerCase(Locale.ROOT), worldName);
}
} catch (Throwable failure) {
@@ -383,12 +371,51 @@ public final class PendingWorldDeleteQueue implements WorldDeletionQueue {
}
}
private static boolean isLoaded(DeleteTarget target) {
if (target.key() != null && WorldIdentity.resolve(target.key()).isPresent()) {
static EntryDeletionResult attemptEntry(
File levelRoot,
String worldName,
LoadedTargetCheck loadedTargetCheck
) throws IOException {
QueueEntry entry = QueueEntry.parse(worldName, levelRoot.getName());
List<DeleteTarget> targets = entry.targets(levelRoot);
if (targets.stream().anyMatch(target -> loadedTargetCheck.isLoaded(target.key(), target.path()))) {
return new EntryDeletionResult(true, false, List.of(), List.of());
}
boolean foundAny = false;
ArrayList<Path> deleted = new ArrayList<>(targets.size());
ArrayList<DeletionFailure> failures = new ArrayList<>(targets.size());
for (DeleteTarget target : targets) {
Path worldFolder = target.path();
if (!Files.exists(worldFolder, LinkOption.NOFOLLOW_LINKS)) {
continue;
}
if (Files.isSymbolicLink(worldFolder) || !Files.isDirectory(worldFolder, LinkOption.NOFOLLOW_LINKS)) {
throw new IOException("Queued world target is not a safe directory: " + worldFolder);
}
foundAny = true;
try {
SnapshotDirectoryTreeDeleter.delete(worldFolder);
deleted.add(worldFolder);
} catch (IOException failure) {
failures.add(new DeletionFailure(worldFolder, failure));
}
}
return new EntryDeletionResult(
false,
foundAny,
List.copyOf(deleted),
List.copyOf(failures)
);
}
private static boolean isLoaded(@Nullable NamespacedKey key, Path path) {
if (key != null && WorldIdentity.resolve(key).isPresent()) {
return true;
}
Path targetPath = target.path().toAbsolutePath().normalize();
Path targetPath = path.toAbsolutePath().normalize();
for (World world : Bukkit.getWorlds()) {
if (world.getWorldFolder().toPath().toAbsolutePath().normalize().equals(targetPath)) {
return true;
@@ -397,25 +424,6 @@ public final class PendingWorldDeleteQueue implements WorldDeletionQueue {
return false;
}
private static void deleteTree(Path target) throws IOException {
Files.walkFileTree(target, new SimpleFileVisitor<>() {
@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attributes) throws IOException {
Files.delete(file);
return FileVisitResult.CONTINUE;
}
@Override
public FileVisitResult postVisitDirectory(Path directory, IOException failure) throws IOException {
if (failure != null) {
throw failure;
}
Files.delete(directory);
return FileVisitResult.CONTINUE;
}
});
}
private enum QueueEntryType {
EXACT,
LOGICAL,
@@ -479,4 +487,23 @@ public final class PendingWorldDeleteQueue implements WorldDeletionQueue {
private record DeleteTarget(@Nullable NamespacedKey key, Path path) {
}
@FunctionalInterface
interface LoadedTargetCheck {
boolean isLoaded(@Nullable NamespacedKey key, Path path);
}
record EntryDeletionResult(
boolean loaded,
boolean foundAny,
List<Path> deleted,
List<DeletionFailure> failures
) {
boolean retainQueueEntry() {
return loaded || !failures.isEmpty();
}
}
record DeletionFailure(Path path, IOException failure) {
}
}
@@ -98,6 +98,7 @@ public class CommandIris implements DirectorExecutor {
private CommandPregen pregen;
private CommandObject object;
private CommandStructure structure;
private CommandJigsaw jigsaw;
private CommandWhat what;
private CommandEdit edit;
private CommandDeveloper developer;
@@ -182,8 +182,7 @@ public class CommandObject implements DirectorExecutor {
private static final Set<Material> skipBlocks = Set.of(Materials.GRASS, Material.SNOW, Material.VINE, Material.TORCH, Material.DEAD_BUSH,
Material.POPPY, Material.DANDELION);
public static IObjectPlacer createPlacer(World world, Map<Block, BlockData> futureBlockChanges) {
public static IObjectPlacer createPlacer(World world, Map<Block, BlockData> futureBlockChanges, Engine targetEngine) {
return new IObjectPlacer() {
@Override
public int getHighest(int x, int z, IrisData data) {
@@ -263,7 +262,7 @@ public class CommandObject implements DirectorExecutor {
@Override
public Engine getEngine() {
return null;
return targetEngine;
}
};
}
@@ -565,7 +564,7 @@ public class CommandObject implements DirectorExecutor {
// Block writes must run on the thread owning the target chunk; the undo log stays global.
final IrisObject placed = o;
if (!J.runAt(block, () -> {
placed.place(block.getBlockX(), block.getBlockY() + (int) placed.getCenter().getY(), block.getBlockZ(), createPlacer(block.getWorld(), futureChanges), placement, new RNG(), null);
placed.place(block.getBlockX(), block.getBlockY() + (int) placed.getCenter().getY(), block.getBlockZ(), createPlacer(block.getWorld(), futureChanges, null), placement, new RNG(), null);
J.runGlobal(() -> Iris.service(ObjectSVC.class).addChanges(futureChanges));
if (!edit) {
@@ -30,6 +30,7 @@ import art.arcane.iris.engine.framework.NativeStructureGenerationPolicy;
import art.arcane.iris.engine.framework.PlacedStructurePiece;
import art.arcane.iris.engine.framework.StructureAssembler;
import art.arcane.iris.engine.framework.StructureReachability;
import art.arcane.iris.engine.framework.structure.StructureAssemblyResult;
import art.arcane.iris.engine.object.IObjectPlacer;
import art.arcane.iris.engine.object.IrisDimension;
import art.arcane.iris.engine.object.IrisNativeStructureDecision;
@@ -61,6 +62,7 @@ import org.bukkit.generator.structure.Structure;
import java.io.File;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
@@ -99,14 +101,15 @@ public class CommandStructure implements DirectorExecutor {
BulkStructureImporter.Report jigsaws = BulkStructureImporter.importAllVanilla(data, StructureImporter.Mode.OVERWRITE, true, sender());
BulkStructureImporter.Report templates = BulkStructureImporter.importAllTemplates(data, StructureImporter.Mode.OVERWRITE, sender());
BulkStructureImporter.Report groups = BulkStructureImporter.importTemplateGroups(data, StructureImporter.Mode.OVERWRITE, sender());
StructureCaptureImporter.Report captured = StructureCaptureImporter.importAllStructures(data, StructureImporter.Mode.OVERWRITE, sender());
StructureCaptureImporter.Report captured = StructureCaptureImporter.importStructures(
data, StructureImporter.Mode.OVERWRITE, sender(), jigsaws.captureCandidates());
int imported = jigsaws.imported() + templates.imported() + groups.imported() + captured.imported();
int failed = jigsaws.failed() + templates.failed() + groups.failed() + captured.failed();
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_STRUCTURE_IMPORT_COMPLETE_STRUCTURES_OBJECTS_WRITTEN_FAILED, MessageArgument.untrusted("imported", imported), MessageArgument.untrusted("failed", failed)));
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_STRUCTURE_REFERENCE_THEM_FROM_BIOME_REGION_DIMENSION_STRUCTURES_LIST_RUN_IRIS, MessageArgument.untrusted("value", dimension.getLoadKey())));
}
@Director(name = "capture", description = "Capture code-generated structures that have no NBT template (swamp huts, igloos, etc.) into editable Iris objects by generating each one in a throwaway scratch world and reading back its blocks. Skips structures that already import as a structure, structures wider/taller than the capture cap (strongholds, mansions, monuments stay vanilla), and anything that will not generate in a flat overworld. Each captured structure becomes a single-piece Iris structure you can place from a biome/region/dimension 'structures' list. Runs automatically as the last pass of /iris structure import.", descriptionKey = "iris.director.commandstructure.director.capture_code_generated_structures_that_have_no_nbt_template_swamp_huts_igloos", aliases = {"cap"}, origin = DirectorOrigin.BOTH)
@Director(name = "capture", description = "Capture live registered structures into editable Iris objects by generating each one in a throwaway scratch world and reading back its blocks. This standalone command overwrites its owned outputs; structures wider/taller than the capture cap and anything that will not generate in a flat overworld are skipped. Each captured structure becomes a single-piece Iris structure you can place from a biome/region/dimension 'structures' list. /iris structure import runs a restricted final capture pass only for non-jigsaw structures that have no loadable NBT template.", descriptionKey = "iris.director.commandstructure.director.capture_code_generated_structures_that_have_no_nbt_template_swamp_huts_igloos", aliases = {"cap"}, origin = DirectorOrigin.BOTH)
public void capture(
@Param(description = "The dimension whose pack to capture into", descriptionKey = "iris.director.commandstructure.param.dimension_whose_pack_capture_into", aliases = "dim")
IrisDimension dimension
@@ -305,8 +308,9 @@ public class CommandStructure implements DirectorExecutor {
}
StructureAssembler assembler = StructureAssembler.forData(
data, s, new IrisPosition(0, 64, 0));
KList<PlacedStructurePiece> pieces = assembler.assemble(new RNG(1234));
if (pieces == null || pieces.isEmpty()) {
StructureAssemblyResult assembly = assembler.assemble(new RNG(1234));
List<PlacedStructurePiece> pieces = assembly.pieces();
if (!assembly.hasOutput()) {
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_STRUCTURE_STRUCTURE_ASSEMBLED_0_PIECES_CHECK_STARTPOOL, MessageArgument.untrusted("structure", structure), MessageArgument.untrusted("value", s.getStartPool())));
return;
}
@@ -344,13 +348,17 @@ public class CommandStructure implements DirectorExecutor {
StructureAssembler assembler = StructureAssembler.forData(
data, s, new IrisPosition(loc.getBlockX(), loc.getBlockY(), loc.getBlockZ()));
RNG rng = new RNG((long) loc.getBlockX() * 341873128712L + loc.getBlockZ());
KList<PlacedStructurePiece> pieces = assembler.assemble(rng);
if (pieces == null || pieces.isEmpty()) {
StructureAssemblyResult assembly = assembler.assemble(rng);
List<PlacedStructurePiece> pieces = assembly.pieces();
if (!assembly.hasOutput()) {
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_STRUCTURE_STRUCTURE_ASSEMBLED_0_PIECES, MessageArgument.untrusted("structure", structure)));
return;
}
Map<Block, BlockData> future = new HashMap<>();
IObjectPlacer placer = CommandObject.createPlacer(player().getWorld(), future);
World targetWorld = player().getWorld();
PlatformChunkGenerator targetGenerator = IrisToolbelt.access(targetWorld);
Engine targetEngine = targetGenerator == null ? null : targetGenerator.getEngine();
IObjectPlacer placer = CommandObject.createPlacer(targetWorld, future, targetEngine);
for (PlacedStructurePiece p : pieces) {
IrisObjectPlacement config = new IrisObjectPlacement();
config.setMode(ObjectPlaceMode.STRUCTURE_PIECE);
@@ -67,6 +67,7 @@ public class CommandSVC implements IrisService, CommandExecutor, TabCompleter, D
private static final String ROOT_PERMISSION = "iris.all";
private final transient AtomicCache<DirectorRuntimeEngine> directorCache = new AtomicCache<>();
private final transient ThreadLocal<CommandSender> dispatchSenders = new ThreadLocal<>();
@Override
public void onEnable() {
@@ -120,11 +121,18 @@ public class CommandSVC implements IrisService, CommandExecutor, TabCompleter, D
}
private void dispatchDirector(DirectorExecutionMode mode, Runnable runnable) {
if (mode == DirectorExecutionMode.SYNC) {
J.s(runnable);
} else {
if (mode != DirectorExecutionMode.SYNC) {
runnable.run();
return;
}
CommandSender sender = dispatchSenders.get();
if (sender instanceof Player player) {
if (!J.runEntity(player, runnable)) {
throw new IllegalStateException("Failed to schedule player command on its entity thread");
}
return;
}
J.s(runnable);
}
@Override
@@ -241,11 +249,14 @@ public class CommandSVC implements IrisService, CommandExecutor, TabCompleter, D
}
private DirectorExecutionResult runDirector(CommandSender sender, String label, String[] args) {
dispatchSenders.set(sender);
try {
return getDirector().execute(new DirectorInvocation(new BukkitDirectorSender(sender), label, Arrays.asList(args)));
} catch (Throwable e) {
Iris.warn("Director command execution failed: " + e.getClass().getSimpleName() + " " + e.getMessage());
return DirectorExecutionResult.notHandled();
} finally {
dispatchSenders.remove();
}
}
@@ -267,7 +278,8 @@ public class CommandSVC implements IrisService, CommandExecutor, TabCompleter, D
}
if (sender instanceof Player player) {
J.s(() -> playSounds(player, Sound.BLOCK_AMETHYST_CLUSTER_BREAK, 0.77f, 0.25f, Sound.BLOCK_BEACON_DEACTIVATE, 0.2f, 0.45f));
J.runEntity(player, () -> playSounds(player, Sound.BLOCK_AMETHYST_CLUSTER_BREAK, 0.77f, 0.25f,
Sound.BLOCK_BEACON_DEACTIVATE, 0.2f, 0.45f));
}
}
@@ -277,7 +289,8 @@ public class CommandSVC implements IrisService, CommandExecutor, TabCompleter, D
}
if (sender instanceof Player player) {
J.s(() -> playSounds(player, Sound.BLOCK_AMETHYST_CLUSTER_BREAK, 0.77f, 1.65f, Sound.BLOCK_RESPAWN_ANCHOR_CHARGE, 0.125f, 2.99f));
J.runEntity(player, () -> playSounds(player, Sound.BLOCK_AMETHYST_CLUSTER_BREAK, 0.77f, 1.65f,
Sound.BLOCK_RESPAWN_ANCHOR_CHARGE, 0.125f, 2.99f));
}
}
@@ -0,0 +1,91 @@
package art.arcane.iris.core.service;
import art.arcane.iris.core.datapack.DatapackIngestService;
import art.arcane.iris.core.datapack.DatapackStructureScopeIndex;
import art.arcane.iris.core.nms.DatapackStructureScopeResult;
import art.arcane.iris.core.nms.INMS;
import art.arcane.iris.core.tools.IrisToolbelt;
import art.arcane.iris.engine.platform.BukkitChunkGenerator;
import art.arcane.iris.engine.platform.PlatformChunkGenerator;
import art.arcane.iris.spi.IrisLogging;
import art.arcane.iris.util.common.plugin.IrisService;
import org.bukkit.Bukkit;
import org.bukkit.World;
import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority;
import org.bukkit.event.world.WorldInitEvent;
import org.bukkit.event.world.WorldUnloadEvent;
import java.io.IOException;
import java.util.Set;
public final class DatapackStructureScopeSVC implements IrisService {
private DatapackStructureScopeIndex scopeIndex = DatapackStructureScopeIndex.create(null);
@Override
public void onEnable() {
try {
scopeIndex = DatapackStructureScopeIndex.create(
DatapackIngestService.installedStructureScopeResources());
} catch (IOException e) {
throw new IllegalStateException(
"Iris could not establish ownership for installed datapack structure sets", e);
}
if (scopeIndex.isEmpty()) {
return;
}
for (World world : Bukkit.getWorlds()) {
applyScope(world);
}
}
@Override
public void onDisable() {
for (World world : Bukkit.getWorlds()) {
INMS.get().abandonStudioStructureBootstrap(world);
}
scopeIndex = DatapackStructureScopeIndex.create(null);
}
@EventHandler(priority = EventPriority.HIGHEST)
public void onWorldInit(WorldInitEvent event) {
boolean studioEntryBootstrap = event.getWorld().getGenerator()
instanceof BukkitChunkGenerator generator
&& generator.isStudioEntryBootstrapActive();
if (shouldApplyScope(scopeIndex.isEmpty(), studioEntryBootstrap)) {
applyScope(event.getWorld());
}
}
@EventHandler(priority = EventPriority.LOWEST)
public void onWorldUnload(WorldUnloadEvent event) {
INMS.get().abandonStudioStructureBootstrap(event.getWorld());
}
static boolean shouldApplyScope(boolean scopeIndexEmpty, boolean studioEntryBootstrap) {
return !scopeIndexEmpty || studioEntryBootstrap;
}
private void applyScope(World world) {
Set<String> declaredSources = declaredSources(world);
try {
DatapackStructureScopeResult result = INMS.get().scopeDatapackStructures(
world, scopeIndex, declaredSources);
IrisLogging.info("Scoped Iris-managed datapack structure sets for world '"
+ world.getName() + "': " + result.retainedManagedSets() + " retained, "
+ result.excludedManagedSets() + " excluded.");
} catch (Throwable error) {
throw new IllegalStateException("Could not scope Iris-managed datapack structure sets for world '"
+ world.getName() + "'", error);
}
}
private Set<String> declaredSources(World world) {
PlatformChunkGenerator generator = IrisToolbelt.access(world);
if (generator == null) {
return Set.of();
}
return scopeIndex.declaredSources(
generator.getTarget().getDimension().getDatapackImports());
}
}
@@ -2,6 +2,7 @@ package art.arcane.iris.core.service;
import art.arcane.iris.core.IrisSettings;
import art.arcane.iris.core.gui.PregeneratorJob;
import art.arcane.iris.core.lifecycle.WorldUnloadBoundaryRegistry;
import art.arcane.iris.core.loader.ResourceLoader;
import art.arcane.iris.core.pregenerator.MantleHeapPressure;
import art.arcane.iris.core.tools.IrisToolbelt;
@@ -36,6 +37,7 @@ import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionStage;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.RejectedExecutionException;
@@ -157,7 +159,10 @@ public final class IrisEngineSVC implements IrisService {
@EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
public void onWorldUnload(WorldUnloadEvent event) {
remove(event.getWorld());
World world = event.getWorld();
CompletionStage<Boolean> unloadBoundary = WorldUnloadBoundaryRegistry.claim(
WorldIdentity.serialize(world));
remove(world, unloadBoundary);
}
@EventHandler
@@ -230,7 +235,7 @@ public final class IrisEngineSVC implements IrisService {
}
}
private void remove(World world) {
private void remove(World world, CompletionStage<Boolean> unloadBoundary) {
if (world == null) {
return;
}
@@ -247,10 +252,53 @@ public final class IrisEngineSVC implements IrisService {
}
if (closing != null) {
phases.closing(world);
startClose(registered, closing);
deferCloseUntilWorldUnload(world, registered, closing, unloadBoundary);
}
}
private void deferCloseUntilWorldUnload(
World world,
Registered registered,
ClosingGenerator closing,
CompletionStage<Boolean> unloadBoundary
) {
if (unloadBoundary == null) {
J.sfut(() -> startClose(registered, closing), 1)
.whenComplete((ignored, failure) -> {
if (failure != null) {
Throwable cause = failure.getCause() == null ? failure : failure.getCause();
reportFailure("Failed to defer generator close for " + registered.name(), cause);
completeClose(closing, cause);
}
});
return;
}
unloadBoundary.whenComplete((unloaded, failure) -> {
if (failure == null && Boolean.TRUE.equals(unloaded)) {
startClose(registered, closing);
return;
}
abandonClose(world, closing);
});
}
private void abandonClose(World world, ClosingGenerator closing) {
synchronized (registrationLock) {
closingGenerators.remove(closing);
}
closing.completion().complete(null);
J.sfut(() -> {
if (isCurrentWorld(world)) {
add(world);
}
}, 1).whenComplete((ignored, failure) -> {
if (failure != null) {
Throwable cause = failure.getCause() == null ? failure : failure.getCause();
reportFailure("Failed to restore generator maintenance for " + world.getName(), cause);
}
});
}
private ClosingGenerator reserveClose(Registered registered) {
ClosingGenerator closing = new ClosingGenerator(
registered.registrationIdentity(),
@@ -173,4 +173,37 @@ public class PendingWorldDeleteQueueTest {
levelRoot.toPath().resolve("dimensions/iris/alpha_the_end").toAbsolutePath()
), family);
}
@Test
public void failedSafeDeletionSignalsQueueRetentionAndSucceedsOnRetry() throws IOException {
File levelRoot = temporaryFolder.newFolder("retry-world");
Path quarantine = levelRoot.toPath().resolve("dimensions/iris").resolve(QUARANTINE_NAME);
Files.createDirectories(quarantine);
Path external = temporaryFolder.newFolder("retry-external").toPath();
Path link = Files.createSymbolicLink(quarantine.resolve("unsafe-link"), external);
PendingWorldDeleteQueue.EntryDeletionResult first = PendingWorldDeleteQueue.attemptEntry(
levelRoot,
QUARANTINE_NAME,
(key, path) -> false
);
assertTrue(first.retainQueueEntry());
assertEquals(1, first.failures().size());
assertTrue(Files.isSymbolicLink(link));
assertTrue(Files.exists(external));
Files.delete(link);
Files.writeString(quarantine.resolve("safe.dat"), "safe");
PendingWorldDeleteQueue.EntryDeletionResult retry = PendingWorldDeleteQueue.attemptEntry(
levelRoot,
QUARANTINE_NAME,
(key, path) -> false
);
assertFalse(retry.retainQueueEntry());
assertTrue(retry.failures().isEmpty());
assertFalse(Files.exists(quarantine));
}
}
@@ -0,0 +1,589 @@
package art.arcane.iris.core.commands;
import art.arcane.iris.core.runtime.StudioOpenCoordinator;
import art.arcane.iris.core.runtime.jigsaw.JigsawPlanarArchetype;
import art.arcane.iris.core.runtime.jigsaw.JigsawStudioCellDimensions;
import art.arcane.iris.core.runtime.jigsaw.JigsawStudioLayout;
import art.arcane.iris.core.runtime.jigsaw.JigsawStudioMode;
import art.arcane.iris.core.runtime.jigsaw.JigsawStudioVariantCatalog;
import art.arcane.iris.core.runtime.jigsaw.JigsawStudioWorkcellSpec;
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.StructureSource;
import art.arcane.iris.core.structure.authoring.StructureTransactionWriter;
import art.arcane.iris.core.structure.conversion.IrisStructureAdoptionInputKind;
import art.arcane.iris.core.structure.export.VanillaJigsawExportFormat;
import art.arcane.iris.core.tools.IrisCreator;
import art.arcane.iris.core.service.JigsawStudioService;
import art.arcane.iris.engine.object.IrisDimension;
import art.arcane.iris.engine.object.IrisDirection;
import art.arcane.iris.engine.object.IrisJigsawConnector;
import art.arcane.iris.engine.object.IrisJigsawPiece;
import art.arcane.iris.engine.object.IrisObject;
import art.arcane.iris.engine.object.IrisPosition;
import art.arcane.volmlib.util.director.DirectorOrigin;
import art.arcane.volmlib.util.director.annotations.Director;
import art.arcane.volmlib.util.director.annotations.Param;
import art.arcane.volmlib.util.director.exceptions.DirectorParsingException;
import org.bukkit.NamespacedKey;
import org.bukkit.entity.Player;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TemporaryFolder;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Parameter;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.atomic.AtomicInteger;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertTrue;
public class CommandJigsawContractTest {
@Rule
public TemporaryFolder temporaryFolder = new TemporaryFolder();
@Test
public void commandIrisRegistersJigsawTree() throws Exception {
Field field = CommandIris.class.getDeclaredField("jigsaw");
assertEquals(CommandJigsaw.class, field.getType());
assertNotNull(CommandJigsaw.class.getDeclaredField("piece"));
assertNotNull(CommandJigsaw.class.getDeclaredField("pool"));
assertNotNull(CommandJigsaw.class.getDeclaredField("connector"));
assertNotNull(CommandJigsaw.class.getDeclaredField("variant"));
assertNotNull(CommandJigsaw.class.getDeclaredField("workcell"));
assertNotNull(CommandJigsaw.class.getDeclaredField("rules"));
assertNotNull(CommandJigsaw.class.getDeclaredField("preview"));
assertNotNull(CommandJigsaw.class.getDeclaredField("adopt"));
}
@Test
public void exposesStudioLifecycleAndAuthoringCommands() throws Exception {
assertCommand("create", IrisDimension.class, String.class, String.class, String.class,
int.class, int.class, int.class, long.class);
assertCommand("convert", IrisDimension.class, String.class, String.class, long.class);
assertCommand("open", IrisDimension.class, String.class, long.class);
assertCommand("close", boolean.class);
assertCommand("delete", boolean.class);
assertCommand("status");
assertCommand("menu");
assertCommand("select");
assertCommand("bounds", int.class, int.class, int.class);
assertCommand("save", String.class);
assertCommand("gotoBay", String.class);
assertCommand("particles", boolean.class);
assertCommand("export", String.class, String.class, String.class, boolean.class);
}
@Test
public void jigsawStudioUsesItsDedicatedOpenLifecycle() {
assertEquals(
StudioOpenCoordinator.StudioOpenKind.JIGSAW,
CommandJigsaw.STUDIO_OPEN_KIND);
assertFalse(CommandJigsaw.STUDIO_OPEN_KIND.openWorkspace());
assertFalse(CommandJigsaw.STUDIO_OPEN_KIND.teleportThroughStandardEntry());
assertEquals(
IrisCreator.DatapackPreparation.REUSE_LOADED_RUNTIME_IF_READY,
CommandJigsaw.STUDIO_OPEN_KIND.datapackPreparation());
}
@Test
public void committedActivationStartsInitialEvaluationBeforePlayerBinding() throws Exception {
String source = Files.readString(Path.of(
"src/main/java/art/arcane/iris/core/commands/CommandJigsaw.java"));
int commit = source.indexOf("JigsawStudioActivation.commit(staged)");
int evaluation = source.indexOf(
"studioService.activationCommitted(world, request.requestId())",
commit);
int binding = source.indexOf("PLAYER_PACKS.put", evaluation);
assertTrue(commit >= 0);
assertTrue(evaluation > commit);
assertTrue(binding > evaluation);
}
@Test
public void convertIsAddOnlyWorkflowWithAliasesAndDefaults() throws Exception {
Method convert = CommandJigsaw.class.getDeclaredMethod(
"convert", IrisDimension.class, String.class, String.class, long.class);
Director command = convert.getAnnotation(Director.class);
Parameter[] parameters = convert.getParameters();
assertEquals(List.of("import", "import-vanilla"), List.of(command.aliases()));
assertParameter(parameters[2], "auto", null);
assertParameter(parameters[3], "1337", null);
NamespacedKey source = CommandJigsaw.parseRegisteredStructureKey("minecraft:village_plains");
assertEquals("minecraft_village_plains", CommandJigsaw.resolveConversionTarget(source, "auto"));
assertEquals("villages/plains", CommandJigsaw.resolveConversionTarget(source, "iris:villages/plains"));
assertThrows(IllegalArgumentException.class,
() -> CommandJigsaw.resolveConversionTarget(source, "custom:village"));
}
@Test
public void adoptionCommandsExposeTwoStepPlanContract() throws Exception {
Method inspect = CommandJigsaw.CommandJigsawAdopt.class.getDeclaredMethod(
"inspect", IrisDimension.class, String.class, String.class, String.class);
Method apply = CommandJigsaw.CommandJigsawAdopt.class.getDeclaredMethod("apply", String.class);
Parameter[] inspectParameters = inspect.getParameters();
assertNotNull(inspect.getAnnotation(Director.class));
assertNotNull(apply.getAnnotation(Director.class));
assertParameter(inspectParameters[2], "auto", null);
assertParameter(inspectParameters[3], "auto", CommandJigsaw.JigsawAdoptionStrategyHandler.class);
assertEquals(CommandJigsaw.JigsawAdoptionPlanHandler.class,
apply.getParameters()[0].getAnnotation(Param.class).customHandler());
CommandJigsaw.JigsawAdoptionStrategyHandler strategyHandler =
new CommandJigsaw.JigsawAdoptionStrategyHandler();
assertEquals(List.of("auto", "in-place", "clone"), strategyHandler.getPossibilities());
assertEquals("in-place", strategyHandler.parse("claim", false));
assertEquals("clone", strategyHandler.parse("copy", false));
assertThrows(DirectorParsingException.class, () -> strategyHandler.parse("overwrite", false));
}
@Test
public void adoptionInputKindUsesOnlyOwnershipProvenance() throws Exception {
Path root = temporaryFolder.newFolder("adoption-provenance").toPath();
assertEquals(IrisStructureAdoptionInputKind.UNOWNED_IRIS,
CommandJigsaw.adoptionInputKind(root, "unowned"));
writeManifest(root, "datapack-created", StructureSource.Kind.DATAPACK,
StructureOwnershipManifest.Provenance.created());
assertEquals(IrisStructureAdoptionInputKind.UNOWNED_IRIS,
CommandJigsaw.adoptionInputKind(root, "datapack-created"));
writeManifest(root, "managed-provenance", StructureSource.Kind.IRIS, managedProvenance());
assertEquals(IrisStructureAdoptionInputKind.MANAGED_DATAPACK,
CommandJigsaw.adoptionInputKind(root, "managed-provenance"));
}
@Test
public void legacyGraphWritersShareTheServiceMutationContract() throws Exception {
Method mutation = CommandJigsaw.class.getDeclaredMethod(
"runGraphMutation",
Player.class,
CommandJigsaw.ActiveContext.class,
JigsawStudioService.CommandGraphMutation.class);
assertEquals(boolean.class, mutation.getReturnType());
assertNotNull(JigsawStudioService.CommandGraphMutation.class.getDeclaredMethod("run"));
assertNotNull(JigsawStudioService.CommandGraphMutationResult.class.getDeclaredConstructor(
JigsawStudioLayout.class,
String.class,
String.class,
String.class));
}
@Test
public void exposesAutosaveDynamicEvaluationAndSelectedWorkcellResize() throws Exception {
Method save = CommandJigsaw.class.getDeclaredMethod("save", String.class);
Method status = CommandJigsaw.class.getDeclaredMethod("status");
Method bounds = CommandJigsaw.class.getDeclaredMethod(
"bounds", int.class, int.class, int.class);
assertEquals("Flush the automatic save for a workcell now",
save.getAnnotation(Director.class).description());
assertEquals("Show active Jigsaw Studio and dynamic evaluation state",
status.getAnnotation(Director.class).description());
assertEquals("Set the selected Studio workcell capacity",
bounds.getAnnotation(Director.class).description());
assertThrows(NoSuchMethodException.class,
() -> CommandJigsaw.class.getDeclaredMethod("validate"));
}
@Test
public void createDefaultsToPlanarIrisWithCompleteCellAndSeedDefaults() throws Exception {
Method create = CommandJigsaw.class.getDeclaredMethod(
"create",
IrisDimension.class,
String.class,
String.class,
String.class,
int.class,
int.class,
int.class,
long.class);
Parameter[] parameters = create.getParameters();
assertParameter(parameters[2], "planar", CommandJigsaw.JigsawModeHandler.class);
assertParameter(parameters[3], "iris", CommandJigsaw.JigsawCompatibilityHandler.class);
assertParameter(parameters[4], "16", null);
assertParameter(parameters[5], "16", null);
assertParameter(parameters[6], "16", null);
assertParameter(parameters[7], "1337", null);
}
@Test
public void structureKeysExplainTheirResourceAndOpenSupportsEditingAliases() throws Exception {
Method create = CommandJigsaw.class.getDeclaredMethod(
"create",
IrisDimension.class,
String.class,
String.class,
String.class,
int.class,
int.class,
int.class,
long.class);
Method open = CommandJigsaw.class.getDeclaredMethod(
"open", IrisDimension.class, String.class, long.class);
Param createKey = create.getParameters()[1].getAnnotation(Param.class);
Param openKey = open.getParameters()[1].getAnnotation(Param.class);
Director openCommand = open.getAnnotation(Director.class);
assertEquals("key", createKey.name());
assertEquals(List.of("structure", "name"), List.of(createKey.aliases()));
assertEquals("New key written as structures/<key>.json", createKey.description());
assertEquals("key", openKey.name());
assertEquals(List.of("structure", "name"), List.of(openKey.aliases()));
assertEquals("Existing key loaded from structures/<key>.json", openKey.description());
assertEquals(List.of("edit", "reopen"), List.of(openCommand.aliases()));
}
@Test
public void createChoiceHandlersExposeCanonicalCompletionsAndRetainAliases() throws Exception {
CommandJigsaw.JigsawModeHandler modeHandler = new CommandJigsaw.JigsawModeHandler();
CommandJigsaw.JigsawCompatibilityHandler compatibilityHandler =
new CommandJigsaw.JigsawCompatibilityHandler();
assertEquals(List.of("planar", "spatial"), modeHandler.getPossibilities());
assertEquals("planar", modeHandler.parse("2d", false));
assertEquals("spatial", modeHandler.parse("3d", false));
assertEquals(List.of("iris", "vanilla"), compatibilityHandler.getPossibilities());
assertEquals("iris", compatibilityHandler.parse("extended", false));
assertEquals("vanilla", compatibilityHandler.parse("portable", false));
assertThrows(DirectorParsingException.class, () -> modeHandler.parse("volume", false));
assertThrows(DirectorParsingException.class, () -> compatibilityHandler.parse("mixed", false));
}
@Test
public void exposesNestedPieceVariantAndPreviewCommands() throws Exception {
assertNotNull(CommandJigsaw.CommandJigsawPool.class
.getDeclaredMethod("create", String.class, String.class)
.getAnnotation(Director.class));
assertNotNull(CommandJigsaw.CommandJigsawConnector.class
.getDeclaredMethod("channel", String.class)
.getAnnotation(Director.class));
assertNotNull(CommandJigsaw.CommandJigsawPiece.class
.getDeclaredMethod("create", String.class, String.class, int.class)
.getAnnotation(Director.class));
assertNotNull(CommandJigsaw.CommandJigsawPiece.class
.getDeclaredMethod("add", String.class, String.class, int.class)
.getAnnotation(Director.class));
assertNotNull(CommandJigsaw.CommandJigsawPiece.class
.getDeclaredMethod("remove", String.class)
.getAnnotation(Director.class));
assertNotNull(CommandJigsaw.CommandJigsawPiece.class
.getDeclaredMethod("rotatable", boolean.class)
.getAnnotation(Director.class));
assertNotNull(CommandJigsaw.CommandJigsawPiece.class
.getDeclaredMethod("expand")
.getAnnotation(Director.class));
assertEquals("Resize the selected piece object exactly to workcell capacity",
CommandJigsaw.CommandJigsawPiece.class
.getDeclaredMethod("expand")
.getAnnotation(Director.class)
.description());
assertNotNull(CommandJigsaw.CommandJigsawVariant.class
.getDeclaredMethod("weight", String.class, int.class)
.getAnnotation(Director.class));
assertNotNull(CommandJigsaw.CommandJigsawVariant.class
.getDeclaredMethod("resize", int.class, int.class, int.class)
.getAnnotation(Director.class));
assertNotNull(CommandJigsaw.CommandJigsawVariant.class
.getDeclaredMethod("label", String.class)
.getAnnotation(Director.class));
assertNotNull(CommandJigsaw.CommandJigsawVariant.class
.getDeclaredMethod("labelReset")
.getAnnotation(Director.class));
assertNotNull(CommandJigsaw.CommandJigsawVariant.class
.getDeclaredMethod("duplicate")
.getAnnotation(Director.class));
Method duplicateFamily = CommandJigsaw.CommandJigsawVariant.class
.getDeclaredMethod("duplicateFamily", String.class);
assertNotNull(duplicateFamily.getAnnotation(Director.class));
assertParameter(duplicateFamily.getParameters()[0], "next", null);
assertNotNull(CommandJigsaw.CommandJigsawWorkcell.class
.getDeclaredMethod("capacity", int.class, int.class, int.class)
.getAnnotation(Director.class));
assertNotNull(CommandJigsaw.CommandJigsawWorkcell.class
.getDeclaredMethod("label", String.class)
.getAnnotation(Director.class));
assertNotNull(CommandJigsaw.CommandJigsawWorkcell.class
.getDeclaredMethod("labelReset")
.getAnnotation(Director.class));
assertNotNull(CommandJigsaw.CommandJigsawRules.class
.getDeclaredMethod("limits", int.class, int.class)
.getAnnotation(Director.class));
assertNotNull(CommandJigsaw.CommandJigsawRules.class
.getDeclaredMethod("fallback", String.class, String.class)
.getAnnotation(Director.class));
assertNotNull(CommandJigsaw.CommandJigsawPreview.class
.getDeclaredMethod("assemble", long.class)
.getAnnotation(Director.class));
assertNotNull(CommandJigsaw.CommandJigsawPreview.class
.getDeclaredMethod("gotoPreview")
.getAnnotation(Director.class));
}
@Test
public void pieceCreateUsesResolvedWorkcellCapacityInsteadOfLayoutDefault() throws Exception {
String source = Files.readString(Path.of(
"src/main/java/art/arcane/iris/core/commands/CommandJigsaw.java"));
int pieceCommands = source.indexOf("public static class CommandJigsawPiece");
int create = source.indexOf("public void create(", pieceCommands);
int add = source.indexOf("public void add(", create);
String createSource = source.substring(create, add);
assertTrue(createSource.contains("targetWorkcell = contextual;"));
assertTrue(createSource.contains("JigsawStudioCellDimensions dimensions = targetWorkcell.capacity();"));
assertFalse(createSource.contains("layout().cellDimensions()"));
}
@Test
public void pieceAddUsesCanonicalAxesForRotatedRectangularPlanarWorkcells() {
JigsawStudioLayout layout = nonuniformPlanarLayout();
IrisJigsawPiece eastEnd = new IrisJigsawPiece();
eastEnd.getConnectors().add(new IrisJigsawConnector().setDirection(IrisDirection.EAST_POSITIVE_X));
IrisObject exactObject = new IrisObject(7, 5, 13);
CommandJigsaw.PieceWorkcellResolution exact = CommandJigsaw.resolvePieceWorkcell(
layout,
eastEnd,
exactObject);
CommandJigsaw.PieceWorkcellResolution oversized = CommandJigsaw.resolvePieceWorkcell(
layout,
eastEnd,
new IrisObject(8, 5, 13));
assertEquals(JigsawPlanarArchetype.END.stableId(), exact.workcell().stableId());
assertEquals(new IrisPosition(13, 5, 7), exact.requiredDimensions());
assertEquals(new JigsawStudioCellDimensions(13, 5, 7), exact.workcell().capacity());
assertTrue(exact.fits());
assertTrue(exactObject.getD() > exact.workcell().capacity().depth());
assertFalse(oversized.fits());
}
@Test
public void pieceAddKeepsRawObjectAxesForSpatialWorkcells() {
JigsawStudioCellDimensions capacity = new JigsawStudioCellDimensions(7, 5, 13);
JigsawStudioLayout layout = JigsawStudioLayout.create(
JigsawStudioMode.SPATIAL_JIGSAW,
capacity,
JigsawStudioVariantCatalog.empty());
IrisJigsawPiece spatialPiece = new IrisJigsawPiece();
spatialPiece.getConnectors().add(
new IrisJigsawConnector().setDirection(IrisDirection.EAST_POSITIVE_X));
CommandJigsaw.PieceWorkcellResolution resolution = CommandJigsaw.resolvePieceWorkcell(
layout,
spatialPiece,
new IrisObject(7, 5, 13));
assertEquals(JigsawStudioLayout.SPATIAL_WORKCELL_ID, resolution.workcell().stableId());
assertEquals(new IrisPosition(7, 5, 13), resolution.requiredDimensions());
assertEquals(capacity, resolution.workcell().capacity());
assertTrue(resolution.fits());
}
@Test
public void everyExecutableCommandRejectsNonPlayerOrigins() {
Class<?>[] commandTypes = {
CommandJigsaw.class,
CommandJigsaw.CommandJigsawConnector.class,
CommandJigsaw.CommandJigsawPool.class,
CommandJigsaw.CommandJigsawPiece.class,
CommandJigsaw.CommandJigsawVariant.class,
CommandJigsaw.CommandJigsawWorkcell.class,
CommandJigsaw.CommandJigsawRules.class,
CommandJigsaw.CommandJigsawPreview.class,
CommandJigsaw.CommandJigsawAdopt.class
};
for (Class<?> commandType : commandTypes) {
for (Method method : commandType.getDeclaredMethods()) {
Director director = method.getAnnotation(Director.class);
if (director != null) {
assertEquals(method.toString(), DirectorOrigin.PLAYER, director.origin());
}
}
}
}
@Test
public void exportOutputIsOneSafeChildArtifact() {
Path root = Path.of("build", "jigsaw-exports").toAbsolutePath().normalize();
assertEquals(root.resolve("village.zip"), CommandJigsaw.resolveExportDestination(
root, "village", VanillaJigsawExportFormat.ZIP));
assertEquals(root.resolve("village"), CommandJigsaw.resolveExportDestination(
root, "village", VanillaJigsawExportFormat.DIRECTORY));
assertThrows(IllegalArgumentException.class, () -> CommandJigsaw.resolveExportDestination(
root, "", VanillaJigsawExportFormat.DIRECTORY));
assertThrows(IllegalArgumentException.class, () -> CommandJigsaw.resolveExportDestination(
root, ".", VanillaJigsawExportFormat.DIRECTORY));
assertThrows(IllegalArgumentException.class, () -> CommandJigsaw.resolveExportDestination(
root, "../all-exports", VanillaJigsawExportFormat.DIRECTORY));
assertThrows(IllegalArgumentException.class, () -> CommandJigsaw.resolveExportDestination(
root, "nested/export", VanillaJigsawExportFormat.DIRECTORY));
}
@Test
public void exportLeaseRejectsDuplicatePlayerAndDestination() {
UUID firstPlayer = UUID.randomUUID();
UUID secondPlayer = UUID.randomUUID();
Path firstDestination = Path.of("build", "jigsaw-exports", "first.zip");
Path secondDestination = Path.of("build", "jigsaw-exports", "second.zip");
assertEquals(true, CommandJigsaw.beginExport(firstPlayer, firstDestination));
try {
assertEquals(false, CommandJigsaw.beginExport(firstPlayer, secondDestination));
assertEquals(false, CommandJigsaw.beginExport(secondPlayer, firstDestination));
} finally {
CommandJigsaw.finishExport(firstPlayer, firstDestination);
}
assertEquals(true, CommandJigsaw.beginExport(secondPlayer, firstDestination));
CommandJigsaw.finishExport(secondPlayer, firstDestination);
}
@Test
public void exportStartFailuresHavePreciseOperatorMessages() {
assertEquals("The active Jigsaw Studio is no longer available.",
CommandJigsaw.exportStartError(JigsawStudioService.ExportStart.NOT_ACTIVE));
assertEquals("Only the Jigsaw Studio owner can export this project.",
CommandJigsaw.exportStartError(JigsawStudioService.ExportStart.NOT_OWNER));
assertEquals("Wait for the pending autosave or discard the edits before exporting the on-disk graph.",
CommandJigsaw.exportStartError(JigsawStudioService.ExportStart.DIRTY));
assertEquals("The active Jigsaw Studio is closing and cannot be exported.",
CommandJigsaw.exportStartError(JigsawStudioService.ExportStart.CLOSING));
assertEquals("Wait for the current Jigsaw Studio save to finish before exporting.",
CommandJigsaw.exportStartError(JigsawStudioService.ExportStart.SAVE_IN_PROGRESS));
assertEquals("Wait for the current Jigsaw Studio operation to finish before exporting.",
CommandJigsaw.exportStartError(JigsawStudioService.ExportStart.OPERATION_IN_PROGRESS));
assertEquals("A Jigsaw Studio export is already in progress.",
CommandJigsaw.exportStartError(JigsawStudioService.ExportStart.IN_PROGRESS));
assertThrows(IllegalArgumentException.class,
() -> CommandJigsaw.exportStartError(JigsawStudioService.ExportStart.STARTED));
}
@Test
public void exportLeaseReleaseActionRunsExactlyOnce() {
AtomicInteger releases = new AtomicInteger();
CommandJigsaw.ExportLease lease = new CommandJigsaw.ExportLease(releases::incrementAndGet);
lease.release();
lease.release();
assertEquals(1, releases.get());
}
@Test
public void exportSourceAcquiresStudioLeaseBeforeStaticLeaseAndReleasesBothPaths() throws Exception {
String source = Files.readString(Path.of(
"src/main/java/art/arcane/iris/core/commands/CommandJigsaw.java"));
int serviceLease = source.indexOf("studioService.tryBeginExport(requestId, playerId)");
int staticLease = source.indexOf("beginExport(playerId, destination)", serviceLease);
int dispatch = source.indexOf("J.a(() -> runExport(operation))", staticLease);
int schedulingRelease = source.indexOf("operation.lease().release()", dispatch);
int exporter = source.indexOf("new VanillaJigsawDatapackExporter().export(operation.request())", dispatch);
int completionRelease = source.indexOf("operation.lease().release()", exporter);
assertTrue(serviceLease >= 0);
assertTrue(staticLease > serviceLease);
assertTrue(dispatch > staticLease);
assertTrue(schedulingRelease > dispatch);
assertTrue(exporter > schedulingRelease);
assertTrue(completionRelease > exporter);
}
private static void assertCommand(String name, Class<?>... parameterTypes) throws Exception {
Method method = CommandJigsaw.class.getDeclaredMethod(name, parameterTypes);
assertNotNull(method.getAnnotation(Director.class));
}
private static JigsawStudioLayout nonuniformPlanarLayout() {
List<JigsawStudioWorkcellSpec> workcells = List.of(
workcell(JigsawPlanarArchetype.BLANK, 3, 1, 3),
workcell(JigsawPlanarArchetype.END, 13, 5, 7),
workcell(JigsawPlanarArchetype.STRAIGHT, 5, 2, 11),
workcell(JigsawPlanarArchetype.CORNER, 9, 3, 6),
workcell(JigsawPlanarArchetype.TEE, 12, 4, 8),
workcell(JigsawPlanarArchetype.CROSS, 10, 6, 10));
return JigsawStudioLayout.createPlanar(
new JigsawStudioCellDimensions(3, 1, 3),
workcells,
JigsawStudioVariantCatalog.empty());
}
private static JigsawStudioWorkcellSpec workcell(
JigsawPlanarArchetype archetype,
int width,
int height,
int depth
) {
return new JigsawStudioWorkcellSpec(
archetype,
"",
new JigsawStudioCellDimensions(width, height, depth),
true);
}
private static void assertParameter(Parameter parameter, String defaultValue, Class<?> customHandler) {
Param annotation = parameter.getAnnotation(Param.class);
assertNotNull(annotation);
assertEquals(defaultValue, annotation.defaultValue());
if (customHandler != null) {
assertEquals(customHandler, annotation.customHandler());
}
}
private static void writeManifest(
Path root,
String structure,
StructureSource.Kind sourceKind,
StructureOwnershipManifest.Provenance provenance
) throws Exception {
StructureKey structureKey = new StructureKey("iris", structure);
StructureOwnershipManifest manifest = new StructureOwnershipManifest(
StructureOwnershipManifest.CURRENT_SCHEMA_VERSION,
structureKey,
StructureSource.of(sourceKind, new StructureKey("iris", "source/" + structure)),
StructureBackend.IRIS_ASSEMBLY,
List.of(StructureCapability.BLOCKS, StructureCapability.CONNECTORS),
List.of(),
Map.of("structures/" + structure + ".json", StructureHash.sha256(
structure.getBytes(StandardCharsets.UTF_8))),
provenance);
Path manifestPath = new StructureTransactionWriter(root).ownershipManifestPath(structureKey);
Files.createDirectories(manifestPath.getParent());
Files.write(manifestPath, manifest.toJson());
}
private static StructureOwnershipManifest.Provenance managedProvenance() {
String path = "structures/source.json";
String hash = StructureHash.sha256("source".getBytes(StandardCharsets.UTF_8));
return new StructureOwnershipManifest.Provenance(
StructureOwnershipManifest.Origin.MANAGED_DATAPACK,
UUID.randomUUID().toString(),
StructureHash.sha256("plan".getBytes(StandardCharsets.UTF_8)),
StructureHash.sha256("closure".getBytes(StandardCharsets.UTF_8)),
1L,
Map.of(path, hash),
Map.of(path, path),
StructureOwnershipManifest.RollbackDisposition.NONE);
}
}
@@ -238,6 +238,38 @@ public class IrisStructureLocateCommandContractTest {
assertFalse(source.contains("at[2] + 8"));
}
@Test
public void bulkStructureImportRestrictsCaptureWithoutChangingStandaloneCapture() throws IOException {
String source = Files.readString(Path.of(System.getProperty("iris.commandStructureSource")));
int importStart = source.indexOf("public void importAll(");
int importEnd = source.indexOf("@Director(name = \"capture\"", importStart);
int captureStart = source.indexOf("public void capture(", importEnd);
int captureEnd = source.indexOf("@Director(description = \"Verify", captureStart);
assertTrue(importStart >= 0);
assertTrue(importEnd > importStart);
assertTrue(captureStart > importEnd);
assertTrue(captureEnd > captureStart);
String importMethod = source.substring(importStart, importEnd);
String captureMethod = source.substring(captureStart, captureEnd);
assertTrue(importMethod.contains("StructureCaptureImporter.importStructures("));
assertTrue(importMethod.contains("jigsaws.captureCandidates()"));
assertFalse(importMethod.contains("StructureCaptureImporter.importAllStructures("));
assertTrue(captureMethod.contains("StructureCaptureImporter.importAllStructures("));
}
@Test
public void structurePlaceSeparatesPackContentFromTargetWorldEngine() throws IOException {
String source = Files.readString(Path.of(System.getProperty("iris.commandStructureSource")));
int methodStart = source.indexOf("public void place(");
assertTrue(methodStart >= 0);
String method = source.substring(methodStart);
assertTrue(method.contains("IrisData data = dimension.getLoader()"));
assertTrue(method.contains("PlatformChunkGenerator targetGenerator = IrisToolbelt.access(targetWorld)"));
assertTrue(method.contains("CommandObject.createPlacer(targetWorld, future, targetEngine)"));
assertFalse(method.contains("data.getEngine()"));
}
@Test
public void structureVerifyPartitionsPolicyBeforeNativeReachability() throws IOException {
String source = Files.readString(Path.of(System.getProperty("iris.commandStructureSource")));
@@ -20,6 +20,12 @@ public class BukkitEngineLifecycleContractTest {
assertTrue(closeAsync.contains("operation.whenComplete("));
assertFalse(closeAsync.contains("!existing.isDone()"));
String exclusiveFuture = method(source, "public CompletableFuture<Void> withExclusiveControlFuture(Runnable r)");
assertTrue(exclusiveFuture.contains("J.a(() -> completeExclusiveControlFuture(loadLock, r, future))"));
String exclusiveCompletion = method(source, "static void completeExclusiveControlFuture(");
assertBefore(exclusiveCompletion, "activeGate.releaseExclusive();", "outward.complete(null);");
assertBefore(exclusiveCompletion, "activeGate.releaseExclusive();", "outward.completeExceptionally(failure);");
String baseHeight = method(source, "public int getBaseHeight(");
assertTrue(baseHeight.contains("currentEngine.acquireGenerationLease(\"bukkit_base_height\")"));
assertTrue(baseHeight.contains("IrisContext.open(currentEngine, lease.sessionId(), null)"));
@@ -0,0 +1,41 @@
package art.arcane.iris.core.service;
import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority;
import org.bukkit.event.world.WorldInitEvent;
import org.bukkit.event.world.WorldUnloadEvent;
import org.junit.Test;
import java.lang.reflect.Method;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
public class DatapackStructureScopeSVCTest {
@Test
public void structureScopeRunsAfterIrisGeneratorInjectionAndBeforeSpawnChunks() throws NoSuchMethodException {
Method handler = DatapackStructureScopeSVC.class.getMethod("onWorldInit", WorldInitEvent.class);
EventHandler annotation = handler.getAnnotation(EventHandler.class);
assertNotNull(annotation);
assertEquals(EventPriority.HIGHEST, annotation.priority());
}
@Test
public void unloadAbandonsRetainedStudioStateBeforeEngineTeardown() throws NoSuchMethodException {
Method handler = DatapackStructureScopeSVC.class.getMethod("onWorldUnload", WorldUnloadEvent.class);
EventHandler annotation = handler.getAnnotation(EventHandler.class);
assertNotNull(annotation);
assertEquals(EventPriority.LOWEST, annotation.priority());
}
@Test
public void emptyScopeStillAppliesToJigsawStudioBootstrap() {
assertTrue(DatapackStructureScopeSVC.shouldApplyScope(true, true));
assertFalse(DatapackStructureScopeSVC.shouldApplyScope(true, false));
assertTrue(DatapackStructureScopeSVC.shouldApplyScope(false, false));
}
}
@@ -75,9 +75,11 @@ public class IrisApiWiringContractTest {
assertBefore(add, "catch (RejectedExecutionException exception)", "phases.ready(world)");
assertBefore(add, "registered = true;", "phases.ready(world)");
String remove = method(source, "private void remove(World world)");
String remove = method(source,
"private void remove(World world, CompletionStage<Boolean> unloadBoundary)");
assertBefore(remove, "registered = worlds.remove(world)", "phases.closing(world)");
assertBefore(remove, "phases.closing(world)", "startClose(registered, closing)");
assertBefore(remove, "phases.closing(world)",
"deferCloseUntilWorldUnload(world, registered, closing, unloadBoundary)");
}
@Test
@@ -49,13 +49,14 @@ public class IrisEngineLifecycleContractTest {
public void registrationRetryWaitsAsynchronouslyForClose() throws IOException {
String source = Files.readString(Path.of(System.getProperty("iris.engineSvcSource")));
String add = method(source, "private void add(World world)");
String remove = method(source, "private void remove(World world)");
String remove = method(source, "private void remove(World world, CompletionStage<Boolean> unloadBoundary)");
String completeClose = method(source, "private void completeClose(");
String retry = method(source, "private void retryRegistrationAfterClose(");
String invokeClose = method(source, "private CompletableFuture<Void> invokeGeneratorClose()");
assertBefore(add, "findClosingGenerator(registrationIdentity)", "new Registered(");
assertBefore(remove, "reserveClose(registered)", "startClose(registered, closing)");
assertBefore(remove, "registered.close()", "reserveClose(registered)");
assertBefore(remove, "reserveClose(registered)", "deferCloseUntilWorldUnload(");
assertBefore(completeClose, "if (failure == null)", "closingGenerators.remove(closing)");
assertBefore(completeClose, "closingGenerators.remove(closing)", "closing.completion().complete(null)");
assertBefore(completeClose, "} else {", "closing.completion().completeExceptionally(failure)");
@@ -68,6 +69,24 @@ public class IrisEngineLifecycleContractTest {
assertTrue(invokeClose.contains("future.whenComplete("));
}
@Test
public void worldUnloadDefersGeneratorCloseUntilTheRawBoundaryCompletesTrue() throws IOException {
String source = Files.readString(Path.of(System.getProperty("iris.engineSvcSource")));
String handler = method(source, "public void onWorldUnload(WorldUnloadEvent event)");
String remove = method(source, "private void remove(World world, CompletionStage<Boolean> unloadBoundary)");
String defer = method(source, "private void deferCloseUntilWorldUnload(");
assertBefore(handler, "WorldUnloadBoundaryRegistry.claim(", "remove(world, unloadBoundary)");
assertBefore(remove, "registered.close()", "deferCloseUntilWorldUnload(");
assertFalse(remove.contains("startClose(registered, closing)"));
assertTrue(defer.contains("J.sfut(() -> startClose(registered, closing), 1)"));
assertTrue(defer.contains("unloadBoundary.whenComplete("));
String managedBoundary = defer.substring(defer.indexOf("unloadBoundary.whenComplete("));
assertBefore(managedBoundary, "failure == null && Boolean.TRUE.equals(unloaded)",
"startClose(registered, closing)");
assertBefore(managedBoundary, "startClose(registered, closing)", "abandonClose(world, closing)");
}
private static void assertMonitorUnloadHandler(Method method) {
EventHandler eventHandler = method.getAnnotation(EventHandler.class);
assertNotNull(eventHandler);