mirror of
https://github.com/VolmitSoftware/Iris.git
synced 2026-08-27 04:37:47 +00:00
dwa
(removed ai file from when i generated docs)
This commit is contained in:
+77
-50
@@ -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;
|
||||
|
||||
+2187
File diff suppressed because it is too large
Load Diff
+3
-4
@@ -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) {
|
||||
|
||||
+15
-7
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+91
@@ -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());
|
||||
}
|
||||
}
|
||||
+51
-3
@@ -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(),
|
||||
|
||||
+33
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
+589
@@ -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);
|
||||
}
|
||||
}
|
||||
+32
@@ -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")));
|
||||
|
||||
+6
@@ -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)"));
|
||||
|
||||
+41
@@ -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));
|
||||
}
|
||||
}
|
||||
+4
-2
@@ -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
|
||||
|
||||
+21
-2
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user