Merged generation history

- Immutable generation epochs now coexist with dimension stacks
- Historical terrain, biomes, structures, and locate behavior remain stable across updates
- Versioned generation kernels preserve the combined runtime contract
This commit is contained in:
Brian Neumann-Fopiano committed 2026-09-04 15:06:55 -04:00
commit 56b5f4dd0b
263 files changed
+39624 -1849

No files matched your search

@@ -55,6 +55,7 @@ import art.arcane.volmlib.util.director.help.DirectorMiniMenu;
import art.arcane.volmlib.util.localization.BukkitLanguageSwitcher;
import art.arcane.iris.core.link.MultiverseCoreLink;
import art.arcane.iris.core.nms.INMS;
import art.arcane.iris.core.nms.ServerShutdownBoundary;
import art.arcane.iris.core.gui.BukkitGuiHost;
import art.arcane.iris.core.gui.PregeneratorJob;
import art.arcane.iris.core.service.BoardSVC;
@@ -139,6 +140,8 @@ import org.jetbrains.annotations.Nullable;
import java.io.File;
import java.io.FileOutputStream;
import java.io.FileDescriptor;
import java.io.PrintStream;
import java.io.PrintWriter;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
@@ -160,6 +163,7 @@ import java.util.regex.Pattern;
@SuppressWarnings("CanBeFinal")
public class Iris extends VolmitPlugin implements Listener, ReloadAware {
private static final long SERVER_SHUTDOWN_BOUNDARY_TIMEOUT_SECONDS = 300L;
private static final PrintStream SHUTDOWN_ERRORS = new PrintStream(new FileOutputStream(FileDescriptor.err), true);
private static final long SERVER_STOP_PREGEN_TIMEOUT_MILLIS = 30000L;
private static final Queue<Runnable> syncJobs = new ShurikenQueue<>();
@@ -190,6 +194,8 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
private final AtomicBoolean sharedRuntimeClosed = new AtomicBoolean(false);
private final AtomicBoolean startupBoundaryRestart = new AtomicBoolean(false);
private final AtomicBoolean terminalCleanupCompleted = new AtomicBoolean(false);
private final AtomicBoolean runtimeTeardownFailed = new AtomicBoolean(false);
private volatile boolean generatorDrainCompleted;
private volatile PlaceholderRegistration papiRegistration;
private volatile IrisPapiListener papiListener;
private volatile IrisPapiState papiState;
@@ -206,6 +212,7 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
private BukkitDebugDump debugDump;
private volatile SettingsHotloadWatch settingsHotloadWatch;
private volatile Thread serverLifecycleThread;
private volatile ServerShutdownBoundary serverShutdownBoundary;
public static VolmitSender getSender() {
if (sender == null) {
@@ -427,6 +434,10 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
if (e == null) {
return;
}
if (instance != null && instance.serverStopTeardownDeferred.get()) {
e.printStackTrace(SHUTDOWN_ERRORS);
return;
}
boolean debug = false;
if (instance != null) {
@@ -466,6 +477,11 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
public static void reportError(String context, Throwable e) {
Throwable error = e == null ? new IllegalStateException("Unknown Iris failure") : e;
String message = context == null || context.isBlank() ? "Unhandled Iris failure." : context;
if (instance != null && instance.serverStopTeardownDeferred.get()) {
SHUTDOWN_ERRORS.println("[Iris] " + message);
error.printStackTrace(SHUTDOWN_ERRORS);
return;
}
try {
if (instance != null) {
@@ -603,6 +619,8 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
sharedRuntimeClosed.set(false);
startupBoundaryRestart.set(false);
terminalCleanupCompleted.set(false);
runtimeTeardownFailed.set(false);
generatorDrainCompleted = false;
deferredShutdownGenerators.clear();
MultiBurst.burst.reopen();
MultiBurst.ioBurst.reopen();
@@ -742,6 +760,7 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
public void addShutdownHook() {
removeShutdownHook();
serverLifecycleThread = Thread.currentThread();
serverShutdownBoundary = INMS.isBound() ? INMS.get().createServerShutdownBoundary() : null;
shutdownHook = new Thread(this::runShutdownHook, "Iris-ShutdownHook");
try {
Runtime.getRuntime().addShutdownHook(shutdownHook);
@@ -963,6 +982,10 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
drainWorldGenerators(reason, timeoutSeconds);
}
if (serverStopTeardownDeferred.get() && !generatorDrainCompleted) {
return;
}
if (services != null && servicesDisabled.compareAndSet(false, true)) {
// Only services whose onEnable actually completed; disabling a service that
// never initialized runs teardown against uninitialized state.
@@ -970,6 +993,7 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
try {
service.onDisable();
} catch (Throwable e) {
runtimeTeardownFailed.set(true);
Iris.reportError("Failed to disable " + service.getClass().getSimpleName() + ".", e);
}
}
@@ -979,8 +1003,18 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
return;
}
J.attempt(MultiBurst.burst::close);
J.attempt(MultiBurst.ioBurst::close);
try {
MultiBurst.burst.close();
} catch (Throwable failure) {
runtimeTeardownFailed.set(true);
Iris.reportError("Failed to close Iris generation workers.", failure);
}
try {
MultiBurst.ioBurst.close();
} catch (Throwable failure) {
runtimeTeardownFailed.set(true);
Iris.reportError("Failed to close Iris I/O workers.", failure);
}
clearQueues();
IrisServices.clear();
}
@@ -988,6 +1022,7 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
private void quiesceRuntimeForServerShutdown(String reason) {
serverStopTeardownDeferred.set(true);
INMS.get().deferPluginClassLoaderClose();
JigsawStudioService jigsawStudioService = IrisServices.getOrNull(JigsawStudioService.class);
if (jigsawStudioService != null) {
try {
@@ -1060,29 +1095,35 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
return;
}
if (!awaitServerShutdownBoundary()) {
Iris.warn("Iris skipped JVM-hook runtime teardown because Paper did not reach its post-world-close boundary.");
SHUTDOWN_ERRORS.println("[Iris] Paper did not reach its post-world-close boundary; retaining the runtime and plugin class loader until JVM exit.");
return;
}
finishDeferredRuntimeTeardown("shutdown-hook", 30L);
}
private boolean awaitServerShutdownBoundary() {
if (!INMS.isBound()) {
ServerShutdownBoundary boundary = serverShutdownBoundary;
if (boundary == null) {
return true;
}
try {
return INMS.get().awaitServerShutdownBoundary(
return boundary.await(
SERVER_SHUTDOWN_BOUNDARY_TIMEOUT_SECONDS,
TimeUnit.SECONDS
);
} catch (Throwable e) {
Iris.reportError("Failed to await Paper's post-world-close shutdown boundary.", e);
SHUTDOWN_ERRORS.println("[Iris] Failed to await Paper's post-world-close shutdown boundary.");
e.printStackTrace(SHUTDOWN_ERRORS);
return false;
}
}
private void finishDeferredRuntimeTeardown(String reason, long timeoutSeconds) {
teardownRuntime(reason, timeoutSeconds);
if (serverStopTeardownDeferred.get() && !generatorDrainCompleted) {
SHUTDOWN_ERRORS.println("[Iris] Runtime cleanup is incomplete; retaining services, pools and the plugin class loader until JVM exit.");
return;
}
finishTerminalCleanup();
}
@@ -1094,12 +1135,36 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
try {
runPostShutdown();
} catch (Throwable e) {
runtimeTeardownFailed.set(true);
Iris.reportError("Failed to run Iris post-shutdown cleanup.", e);
} finally {
if (serverStopTeardownDeferred.get() && hasActiveShutdownResources()) {
runtimeTeardownFailed.set(true);
SHUTDOWN_ERRORS.println("[Iris] Owned background work remains active after cleanup; retaining the plugin class loader until JVM exit.");
}
IrisPlatforms.unbind();
if (serverStopTeardownDeferred.get() && runtimeTeardownFailed.get()) {
SHUTDOWN_ERRORS.println("[Iris] Runtime cleanup failed; retaining the plugin class loader until JVM exit.");
} else {
try {
INMS.get().releasePluginClassLoaderClose();
} catch (Throwable failure) {
SHUTDOWN_ERRORS.println("[Iris] Failed to release the plugin class loader after runtime cleanup.");
failure.printStackTrace(SHUTDOWN_ERRORS);
}
}
}
}
private boolean hasActiveShutdownResources() {
if (!MultiBurst.burst.isTerminated() || !MultiBurst.ioBurst.isTerminated()) {
return true;
}
return services != null
&& services.get(PreservationSVC.class) instanceof PreservationSVC preservation
&& preservation.hasActiveResources();
}
private void drainWorldGenerators(String reason, long timeoutSeconds) {
List<World> irisWorlds = new ArrayList<>();
List<PlatformChunkGenerator> generators = new ArrayList<>();
@@ -1115,6 +1180,7 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
}
}
if (generators.isEmpty()) {
generatorDrainCompleted = true;
Iris.debug("No Iris worlds to freeze.");
return;
}
@@ -1139,12 +1205,15 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
try {
CompletableFuture.allOf(closes.toArray(new CompletableFuture<?>[0]))
.get(timeoutSeconds, TimeUnit.SECONDS);
Iris.debug("All Iris chunk generators parked. Safe to unload.");
generatorDrainCompleted = closes.size() == generators.size();
if (generatorDrainCompleted) {
Iris.debug("All Iris chunk generators parked. Safe to unload.");
}
} catch (TimeoutException e) {
Iris.warn("Iris generator drain timed out after " + timeoutSeconds + "s; unload proceeding anyway.");
Iris.reportError("Iris generator drain timed out after " + timeoutSeconds + "s.", e);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
Iris.warn("Iris generator drain interrupted; unload proceeding.");
Iris.reportError("Iris generator drain was interrupted.", e);
} catch (ExecutionException e) {
Iris.reportError(e.getCause() == null ? e : e.getCause());
}
@@ -765,13 +765,13 @@ public final class BukkitWorldReconciler {
@Override
public void requireDimensionLoadable(NamespacedKey worldKey, String dimension) {
File snapshotRoot = snapshotRoot(worldKey);
IrisWorldGeneratorResolver.requireSnapshotLoadable(snapshotRoot);
String configuredWorldName = configuredWorldName(worldKey);
IrisDimension irisDimension = IrisWorldGeneratorResolver.loadDimension(configuredWorldName, dimension);
if (irisDimension == null) {
throw new IllegalStateException("Could not resolve the Iris dimension \"" + dimension + "\".");
}
File snapshotRoot = snapshotRoot(worldKey);
IrisWorldGeneratorResolver.requireSnapshotLoadable(snapshotRoot);
}
private File snapshotRoot(NamespacedKey worldKey) {
@@ -788,7 +788,7 @@ public final class BukkitWorldReconciler {
throw new IllegalStateException("Iris world storage does not match the current platform layout for "
+ worldKey + ".");
}
return IrisWorldStorage.requireFrozenPackRoot(dimensionRoot);
return IrisWorldStorage.requireActiveGenerationPackRoot(dimensionRoot);
}
}
}
@@ -20,6 +20,7 @@ package art.arcane.iris.core;
import art.arcane.iris.Iris;
import art.arcane.iris.core.lifecycle.WorldLifecycleStaging;
import art.arcane.iris.core.lifecycle.WorldReplacementSeed;
import art.arcane.iris.core.loader.IrisData;
import art.arcane.iris.core.pack.BrokenPackException;
import art.arcane.iris.core.pack.PackDownloader;
@@ -32,6 +33,13 @@ import art.arcane.iris.spi.IrisLogging;
import art.arcane.iris.spi.IrisPlatforms;
import art.arcane.iris.engine.object.IrisDimension;
import art.arcane.iris.engine.object.IrisWorld;
import art.arcane.iris.engine.history.GenerationEpoch;
import art.arcane.iris.engine.history.GenerationEpochContractFactory;
import art.arcane.iris.engine.history.GenerationHistory;
import art.arcane.iris.engine.history.GenerationHistoryPaths;
import art.arcane.iris.engine.history.GenerationPackFingerprint;
import art.arcane.iris.engine.history.GenerationRegistryContract;
import art.arcane.iris.engine.history.GenerationRegistryContractFactory;
import art.arcane.iris.engine.platform.BukkitChunkGenerator;
import art.arcane.iris.util.common.plugin.VolmitPlugin;
import art.arcane.volmlib.util.bukkit.WorldIdentity;
@@ -255,15 +263,21 @@ public final class IrisWorldGeneratorResolver {
File levelRoot = IrisWorldStorage.levelRoot();
NamespacedKey worldKey = configuredWorldKey(worldName, levelRoot.getName(), levelRoot);
String configuredWorldName = IrisWorldStorage.configuredWorldName(worldKey, levelRoot.getName());
File pack = IrisWorldStorage.frozenDimensionRoot(
File dimensionRoot = IrisWorldStorage.frozenDimensionRoot(
Bukkit.getWorldContainer(),
levelRoot,
configuredWorldName,
worldKey
)
.map(IrisWorldStorage::requireFrozenPackRoot)
.orElse(null);
IrisDimension dimension = pack == null ? null : IrisData.get(pack).getDimensionLoader().load(id);
IrisDimension dimension = null;
if (dimensionRoot != null) {
long worldSeed = requireStoredWorldSeed(configuredWorldName, dimensionRoot);
GenerationHistory history = requireGenerationHistory(dimensionRoot, id, worldSeed);
File pack = requireActivePack(history);
requireSnapshotLoadable(pack);
dimension = requireHistoricalDimension(history, pack, id);
}
if (dimension == null) dimension = IrisData.loadAnyDimension(id, null);
if (dimension == null) {
File packsRoot = IrisPlatforms.get().packsFolderNoCreate();
@@ -445,7 +459,7 @@ public final class IrisWorldGeneratorResolver {
return;
}
if (dimensionRoot != null
&& (IRIS_DIMENSION_NAMESPACE.equals(worldKey.getNamespace()) || hasFrozenPack(dimensionRoot))) {
&& (IRIS_DIMENSION_NAMESPACE.equals(worldKey.getNamespace()) || hasGenerationStorage(dimensionRoot))) {
return;
}
NamespacedKey messageKey = messageWorldKey(worldName, levelRoot.getName());
@@ -455,9 +469,10 @@ public final class IrisWorldGeneratorResolver {
+ " type=<pack>; Iris registers them with Multiverse itself.");
}
private static boolean hasFrozenPack(File dimensionRoot) {
Path packRoot = dimensionRoot.toPath().toAbsolutePath().normalize().resolve("iris").resolve("pack");
return Files.isDirectory(packRoot, LinkOption.NOFOLLOW_LINKS);
private static boolean hasGenerationStorage(File dimensionRoot) {
GenerationHistoryPaths paths = GenerationHistoryPaths.forDimension(dimensionRoot.toPath());
return Files.isDirectory(paths.generationRoot(), LinkOption.NOFOLLOW_LINKS)
|| Files.isDirectory(paths.legacyPackRoot(), LinkOption.NOFOLLOW_LINKS);
}
private ChunkGenerator resolveFrozenWorldGenerator(String worldName, String id) {
@@ -475,7 +490,9 @@ public final class IrisWorldGeneratorResolver {
throw new IllegalStateException("Frozen Iris world storage does not match the current platform layout for "
+ worldKey + ".");
}
File snapshotRoot = IrisWorldStorage.requireFrozenPackRoot(dimensionRoot);
long worldSeed = requireStoredWorldSeed(worldName, dimensionRoot);
GenerationHistory history = requireGenerationHistory(dimensionRoot, id, worldSeed);
File snapshotRoot = requireActivePack(history);
try {
requireSnapshotLoadable(snapshotRoot);
} catch (BrokenPackException exception) {
@@ -487,18 +504,14 @@ public final class IrisWorldGeneratorResolver {
throw exception;
}
IrisDimension dimension = IrisData.get(snapshotRoot).getDimensionLoader().load(id, false);
if (dimension == null) {
throw new IllegalStateException("Frozen Iris pack snapshot at " + snapshotRoot
+ " does not contain dimension " + id + ".");
}
IrisDimension dimension = requireHistoricalDimension(history, snapshotRoot, id);
Iris.debug("Assuming IrisDimension: " + dimension.getName());
IrisWorld world = IrisWorld.builder()
.platformIdentity(worldKey.toString())
.name(worldName)
.seed(1337)
.seed(history.activeEpoch().worldSeed())
.worldFolder(dimensionRoot)
.minHeight(dimension.getMinHeight())
.maxHeight(dimension.getMaxHeight())
@@ -506,7 +519,133 @@ public final class IrisWorldGeneratorResolver {
Iris.debug("Generator Config: " + world);
return new BukkitChunkGenerator(world, false, snapshotRoot, dimension.getLoadKey());
return new BukkitChunkGenerator(world, false, snapshotRoot, dimension.getLoadKey(), history);
}
private static GenerationHistory requireGenerationHistory(
File dimensionRoot,
String dimensionKey,
long worldSeed
) {
Path root = dimensionRoot.toPath().toAbsolutePath().normalize();
try {
Optional<GenerationHistory> current = GenerationHistory.openIfPresent(root, worldSeed);
GenerationHistory history = current.isPresent()
? current.get()
: adoptLegacyGenerationHistory(root, dimensionKey, worldSeed);
List<GenerationRegistryContract> retainedContracts = history.manifest()
.epochs()
.stream()
.map(GenerationEpoch::registryContract)
.toList();
history.requireRegistryDefinitions(
GenerationRegistryContractFactory.captureRequiredDefinitions(retainedContracts)
);
return history;
} catch (IOException failure) {
throw new IllegalStateException("Iris generation history is unusable at " + root + ".", failure);
}
}
private static GenerationHistory adoptLegacyGenerationHistory(
Path dimensionRoot,
String dimensionKey,
long worldSeed
) throws IOException {
Path legacyPack = GenerationHistoryPaths.forDimension(dimensionRoot).legacyPackRoot();
requireSnapshotLoadable(legacyPack.toFile());
String fingerprint = GenerationPackFingerprint.compute(
legacyPack,
GenerationPackFingerprint.CURRENT_VERSION
);
IrisData data = IrisData.openDatapackCompiler(legacyPack.toFile());
try {
IrisDimension dimension = data.getDimensionLoader().load(dimensionKey, false);
if (dimension == null) {
throw new IOException("Legacy Iris pack does not contain dimension " + dimensionKey + ".");
}
GenerationEpoch.DimensionContract dimensionContract = captureDimensionContract(data, dimension);
GenerationRegistryContract registryContract = GenerationRegistryContractFactory.create(
data,
dimension,
fingerprint,
GenerationRegistryContractFactory.CustomBiomeAliasPolicy.RETAIN_LEGACY_ALIASES
);
return GenerationHistory.adoptLegacyPack(
dimensionRoot,
fingerprint,
worldSeed,
dimensionContract,
registryContract
);
} finally {
data.close();
}
}
private static IrisDimension requireHistoricalDimension(
GenerationHistory history,
File packRoot,
String dimensionKey
) {
GenerationEpoch epoch = history.activeEpoch();
if (!epoch.dimensionContract().dimensionKey().equals(dimensionKey)) {
throw new IllegalStateException("Configured dimension " + dimensionKey
+ " does not match active generation history dimension "
+ epoch.dimensionContract().dimensionKey() + ".");
}
IrisData data = IrisData.get(packRoot);
IrisDimension dimension = data.getDimensionLoader().load(dimensionKey, false);
if (dimension == null) {
throw new IllegalStateException("Immutable Iris generation pack at " + packRoot
+ " does not contain dimension " + dimensionKey + ".");
}
GenerationEpoch.DimensionContract actualContract = captureDimensionContract(data, dimension);
if (!epoch.dimensionContract().equals(actualContract)) {
throw new IllegalStateException("Immutable Iris generation pack dimension contract changed at "
+ packRoot + ".");
}
return dimension;
}
private static GenerationEpoch.DimensionContract captureDimensionContract(
IrisData data,
IrisDimension dimension
) {
String dimensionTypeKey = IrisPlatforms.get()
.registries()
.generationRegistry()
.dimensionTypeResourceKey(
data.getDataFolder().getName(),
dimension.getLoadKey(),
dimension.getDimensionTypeKey()
);
return GenerationEpochContractFactory.create(
dimension,
dimension.getLoadKey(),
dimensionTypeKey
);
}
private static File requireActivePack(GenerationHistory history) {
try {
return history.activePackRoot().toFile();
} catch (IOException failure) {
throw new IllegalStateException("Active Iris generation pack is unusable.", failure);
}
}
private static long requireStoredWorldSeed(String worldName, File dimensionRoot) {
Long configuredSeed = IrisWorlds.readBukkitWorldSeed(worldName);
if (configuredSeed != null) {
return configuredSeed;
}
try {
return WorldReplacementSeed.readAuthoritativeSeed(dimensionRoot.toPath());
} catch (IOException failure) {
throw new IllegalStateException("Iris world " + worldName
+ " has no authoritative generation seed.", failure);
}
}
private record FreshValidation(
@@ -164,24 +164,28 @@ public final class PendingWorldReplacementManager implements Listener {
boolean configurationApplied = false;
try {
Files.createDirectory(paths.stage());
IrisDimension installed = Iris.service(StudioSVC.class).installIntoWorld(
requiredSender,
requiredDimension,
paths.stage().toFile()
);
if (installed == null) {
throw new IOException("Iris could not stage the dimension pack.");
}
requireCompatibleEnvironment(target.slotKind(), installed.getEnvironment());
long effectiveSeed = WorldReplacementSeed.stageAuthoritativeSeed(
paths.target(),
paths.stage(),
seedSelection
);
IrisDimension installed = Iris.service(StudioSVC.class).installIntoWorld(
requiredSender,
requiredDimension,
paths.stage().toFile(),
effectiveSeed
);
if (installed == null) {
throw new IOException("Iris could not stage the dimension pack.");
}
requireCompatibleEnvironment(target.slotKind(), installed.getEnvironment());
if (target.slotKind() == SlotKind.VANILLA_OVERWORLD) {
WorldReplacementEntryGuard.stage(target.levelRoot(), paths.stage(), transactionId);
}
File stagedPack = paths.stage().resolve("iris/pack").toFile();
File stagedPack = IrisWorldStorage.requireActiveGenerationPackRoot(
paths.stage().toFile(),
effectiveSeed
);
IrisWorldGeneratorResolver.requireSnapshotLoadable(stagedPack);
String packFingerprint = WorldReplacementFilesystem.fingerprintPack(stagedPack.toPath());
transaction = new Transaction(
@@ -806,7 +810,10 @@ public final class PendingWorldReplacementManager implements Listener {
validatePublishedWorldRuntime(runtimeState, transaction, target.slotKind());
ReplacementPaths paths = WorldReplacementFilesystem.paths(target, transaction.id());
String fingerprint = WorldReplacementFilesystem.fingerprintPack(
paths.target().resolve("iris/pack"));
IrisWorldStorage.requireActiveGenerationPackRoot(
paths.target().toFile(),
transaction.seed()
).toPath());
if (!transaction.packFingerprint().equals(fingerprint)) {
throw new IOException("The replacement pack changed before runtime verification.");
}
@@ -176,7 +176,7 @@ public class CommandDeveloper implements DirectorExecutor {
return z ^ (z >>> 31);
}
@Director(description = "Update the pack of a world (UNSAFE!)", descriptionKey = "iris.director.commanddeveloper.director.update_pack_world_unsafe", name = "update-world", aliases = "^world")
@Director(description = "Stage a world generation update", descriptionKey = "iris.director.commanddeveloper.director.stage_world_generation_update", name = "update-world", aliases = "^world")
public void updateWorld(
@Param(description = "The world to update", descriptionKey = "iris.director.commanddeveloper.param.world_update", contextual = true, contextualOverride = true)
World world,
@@ -202,7 +202,7 @@ public class CommandDeveloper implements DirectorExecutor {
LifecycleOperationCoordinator.OperationKind.PACK_PUBLISH,
pack.getLoadKey()
)) {
Iris.service(StudioSVC.class).replaceIntoWorld(sender(), pack, folder);
Iris.service(StudioSVC.class).replaceIntoWorld(sender(), pack, folder, world.getSeed());
} catch (LifecycleOperationCoordinator.BusyException e) {
sender().sendMessage(C.YELLOW + e.getMessage());
}
@@ -28,8 +28,8 @@ import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.framework.IrisStructureLocator;
import art.arcane.iris.engine.framework.NativeStructureGenerationPolicy;
import art.arcane.iris.engine.framework.StructureReachability;
import art.arcane.iris.engine.history.GenerationSemanticQueries;
import art.arcane.iris.engine.hydrology.HydrologyFeatureQuery;
import art.arcane.iris.engine.hydrology.HydrologyFeatureRef;
import art.arcane.iris.engine.hydrology.runtime.IrisHydrologyRuntime;
import art.arcane.iris.engine.object.IrisBiome;
import art.arcane.iris.engine.object.IrisNativeStructureDecision;
@@ -41,8 +41,10 @@ import art.arcane.iris.spi.IrisPlatforms;
import art.arcane.iris.spi.PlatformStructureHooks;
import art.arcane.iris.util.common.director.DirectorExecutor;
import art.arcane.iris.util.common.director.specialhandlers.HydrologyTypeHandler;
import art.arcane.iris.util.common.director.specialhandlers.ObjectHandler;
import art.arcane.iris.engine.history.GenerationFindCatalog;
import art.arcane.iris.util.common.director.specialhandlers.LocatableObjectHandler;
import art.arcane.iris.util.common.director.specialhandlers.ReachableBiomeHandler;
import art.arcane.iris.util.common.director.specialhandlers.ReachableRegionHandler;
import art.arcane.iris.util.common.director.specialhandlers.StructureHandler;
import art.arcane.iris.util.common.format.C;
import art.arcane.iris.util.common.plugin.VolmitSender;
@@ -90,7 +92,7 @@ public class CommandFind implements DirectorExecutor {
@Director(description = "Find a region", descriptionKey = "iris.director.commandfind.director.find_region")
public void region(
@Param(description = "The region to look for", descriptionKey = "iris.director.commandfind.param.region_look")
@Param(description = "The region to look for", descriptionKey = "iris.director.commandfind.param.region_look", customHandler = ReachableRegionHandler.class)
IrisRegion region,
@Param(description = "Should you be teleported", descriptionKey = "iris.director.commandfind.param.should_you_be_teleported_2", defaultValue = "true")
boolean teleport
@@ -138,10 +140,6 @@ public class CommandFind implements DirectorExecutor {
return;
}
IrisHydrologyRuntime runtime = activeEngine.getComplex().getHydrologyRuntime();
if (runtime == null) {
commandSender.sendMessage(C.YELLOW + "Hydrology is not active in this world.");
return;
}
HydrologyFeatureQuery query;
try {
query = HydrologyFeatureQuery.parse(type);
@@ -150,15 +148,19 @@ public class CommandFind implements DirectorExecutor {
return;
}
Location origin = target.getLocation();
int requestedDistance = Math.min(8192, runtime.settings().routing().tileSize() * 15);
int maximumDistance = runtime.maximumFeatureSearchDistance(
origin.getBlockX(), origin.getBlockZ(), requestedDistance);
int requestedDistance = runtime == null
? 8192
: Math.min(8192, runtime.settings().routing().tileSize() * 15);
int maximumDistance = runtime == null
? requestedDistance
: runtime.maximumFeatureSearchDistance(
origin.getBlockX(), origin.getBlockZ(), requestedDistance);
commandSender.sendMessage(C.GRAY + "Searching accepted hydrology plans for " + type + "...");
J.a(() -> {
try {
HydrologyFeatureRef feature = runtime.nearestFeature(
query.types(),
query.profileKey(),
GenerationSemanticQueries.RiverResult feature = GenerationSemanticQueries.nearestRiver(
activeEngine,
query,
origin.getBlockX(),
origin.getBlockZ(),
maximumDistance,
@@ -171,15 +173,14 @@ public class CommandFind implements DirectorExecutor {
+ maximumDistance + " blocks.");
return;
}
int worldY = feature.y() + activeEngine.getDimension().getMinHeight();
String label = type + " hydrology feature";
if (!teleport) {
sendStructureMessage(target, commandSender,
C.GREEN + "Found " + label + " at " + feature.x() + ", " + worldY + ", " + feature.z() + ".");
C.GREEN + "Found " + label + " at " + feature.x() + ", " + feature.y() + ", " + feature.z() + ".");
return;
}
Location destination = new Location(
target.getWorld(), feature.x(), worldY, feature.z());
target.getWorld(), feature.x(), feature.y(), feature.z());
prepareStructureTeleport(
target, target.getWorld(), commandSender, label, destination, true);
} catch (Throwable error) {
@@ -233,6 +234,12 @@ public class CommandFind implements DirectorExecutor {
registered, decision, nativePlacement, locatableNativePlacement,
locatableEditablePlacement, nativeGenerationEnabled, reachable);
if (route != StructureLookupRoute.NATIVE && route != StructureLookupRoute.IRIS
&& GenerationFindCatalog.hasRetainedStructurePlacement(e, structureKey)) {
locateIrisStructure(e, structureKey, commandSender);
return;
}
if (route == StructureLookupRoute.UNKNOWN) {
commandSender.sendMessage(IrisLanguage.text(BukkitCommandMessages.COMMAND_FIND_UNKNOWN_STRUCTURE, MessageArgument.untrusted("structureKey", structureKey)));
return;
@@ -672,7 +679,7 @@ public class CommandFind implements DirectorExecutor {
J.a(() -> {
try {
IrisStructureLocator.LocateResult result =
IrisStructureLocator.locate(engine, structure, blockX, blockZ, 1024);
GenerationSemanticQueries.nearestStructure(engine, structure, blockX, blockZ, 1024);
if (result.status() == IrisStructureLocator.LocateStatus.SEARCH_LIMIT_REACHED) {
sendStructureMessage(target, commandSender,
C.YELLOW + "Unable to locate " + structure
@@ -697,7 +704,7 @@ public class CommandFind implements DirectorExecutor {
@Director(description = "Find an object", descriptionKey = "iris.director.commandfind.director.find_object")
public void object(
@Param(description = "The object to look for", descriptionKey = "iris.director.commandfind.param.object_look", customHandler = ObjectHandler.class)
@Param(description = "The object to look for", descriptionKey = "iris.director.commandfind.param.object_look", customHandler = LocatableObjectHandler.class)
String object,
@Param(description = "Should you be teleported", descriptionKey = "iris.director.commandfind.param.should_you_be_teleported_4", defaultValue = "true")
boolean teleport
@@ -721,7 +728,7 @@ public class CommandFind implements DirectorExecutor {
}
}
if (e.hasObjectPlacement(object)) {
if (GenerationFindCatalog.hasObjectPlacement(e, object)) {
EngineBukkitOps.gotoObject(e, object, player(), teleport);
return;
}
@@ -279,12 +279,14 @@ public class CommandSVC implements IrisService, CommandExecutor, TabCompleter, D
private DirectorExecutionResult runDirector(CommandSender sender, String label, String[] args) {
dispatchSenders.set(sender);
DirectorContext.touch(new VolmitSender(sender));
try (LanguageAudience.Scope audience = LanguageAudience.open(sender instanceof Player player ? player.getUniqueId() : null)) {
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 {
DirectorContext.remove();
dispatchSenders.remove();
}
}
@@ -9,6 +9,40 @@ import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
public class IrisShutdownOrderingTest {
@Test
public void serverStopHoldsThePluginLoaderUntilTerminalCleanupCompletes() throws Exception {
String source = Files.readString(Path.of(System.getProperty("iris.startupSource")));
String quiesce = section(source, "private void quiesceRuntimeForServerShutdown", "private void startPostStopFinisher");
String cleanup = section(source, "private void finishTerminalCleanup()", "private void drainWorldGenerators");
String teardown = section(source, "private void teardownRuntime", "private void quiesceRuntimeForServerShutdown");
String deferred = section(source, "private void finishDeferredRuntimeTeardown", "private void finishTerminalCleanup");
assertOrdered(quiesce, "serverStopTeardownDeferred.set(true)", "deferPluginClassLoaderClose()",
"jigsawStudioService.quiesceForServerShutdown()");
assertOrdered(cleanup, "runPostShutdown()", "hasActiveShutdownResources()", "IrisPlatforms.unbind()",
"runtimeTeardownFailed.get()", "releasePluginClassLoaderClose()");
assertTrue(cleanup.contains("!MultiBurst.burst.isTerminated() || !MultiBurst.ioBurst.isTerminated()"));
assertTrue(cleanup.contains("preservation.hasActiveResources()"));
assertOrdered(teardown, "drainWorldGenerators(reason, timeoutSeconds)", "!generatorDrainCompleted",
"return;", "service.onDisable()");
assertOrdered(deferred, "teardownRuntime(reason, timeoutSeconds)", "!generatorDrainCompleted",
"SHUTDOWN_ERRORS.println", "return;", "finishTerminalCleanup()");
}
@Test
public void deferredShutdownErrorsBypassClosedLoggingServices() throws Exception {
String source = Files.readString(Path.of(System.getProperty("iris.startupSource")));
String throwableReport = section(source, "public static void reportError(Throwable e)",
"public static void reportError(String context, Throwable e)");
String contextualReport = section(source, "public static void reportError(String context, Throwable e)",
"public static void dump()");
assertOrdered(throwableReport, "serverStopTeardownDeferred.get()",
"e.printStackTrace(SHUTDOWN_ERRORS)", "boolean debug = false");
assertOrdered(contextualReport, "serverStopTeardownDeferred.get()",
"SHUTDOWN_ERRORS.println(", "error.printStackTrace(SHUTDOWN_ERRORS)", "Iris.error(message)");
}
@Test
public void drainWorldGenerators_closesGeneratorsWithoutEagerMantleSave() throws Exception {
String source = Files.readString(Path.of(System.getProperty("iris.startupSource")));
@@ -18,6 +52,22 @@ public class IrisShutdownOrderingTest {
assertFalse("Shutdown must not close Mantle plates before generation drains", drain.contains("saveAllNow()"));
}
@Test
public void shutdownBoundaryIsPreparedBeforeHookRegistrationAndDoesNotUseClosedBindings() throws Exception {
String source = Files.readString(Path.of(System.getProperty("iris.startupSource")));
String registration = section(source, "public void addShutdownHook()", "public void removeShutdownHook()");
String boundary = section(source, "private boolean awaitServerShutdownBoundary()",
"private void finishDeferredRuntimeTeardown");
assertOrdered(registration, "createServerShutdownBoundary()", "new Thread(this::runShutdownHook",
"Runtime.getRuntime().addShutdownHook");
assertTrue(boundary.contains("boundary.await("));
assertFalse(boundary.contains("INMS.get()"));
assertFalse(boundary.contains("Iris.reportError"));
assertTrue(source.contains("new FileOutputStream(FileDescriptor.err)"));
assertOrdered(boundary, "SHUTDOWN_ERRORS.println(", "e.printStackTrace(SHUTDOWN_ERRORS)", "return false;");
}
@Test
public void serverStop_defersRuntimeTeardownUntilPaperShutdownBoundary() throws Exception {
String source = Files.readString(Path.of(System.getProperty("iris.startupSource")));
@@ -43,6 +93,7 @@ public class IrisShutdownOrderingTest {
"finishDeferredRuntimeTeardown(\"startup-boundary-restart-hook\", 30L)",
"return;",
"awaitServerShutdownBoundary()",
"SHUTDOWN_ERRORS.println",
"finishDeferredRuntimeTeardown(\"shutdown-hook\", 30L)");
assertOrdered(finisher,
"finisher.setDaemon(false)",
@@ -85,8 +136,8 @@ public class IrisShutdownOrderingTest {
assertOrdered(teardown,
"drainWorldGenerators(reason, timeoutSeconds)",
"service.onDisable()",
"MultiBurst.burst::close",
"MultiBurst.ioBurst::close");
"MultiBurst.burst.close()",
"MultiBurst.ioBurst.close()");
assertTrue("Engine service must retain its generator close behind deferred service teardown",
engineDisable.contains("startClose("));
assertTrue("Studio service must retain its generator close behind deferred service teardown",
@@ -195,7 +195,7 @@ public class IrisWorldGeneratorResolverTest {
}
@Test
public void configuredWorldResolutionUsesOnlyFrozenWorldLocalSnapshot() throws Exception {
public void configuredWorldResolutionUsesOnlyActiveImmutableGenerationPack() throws Exception {
String source = Files.readString(Path.of(
"src/main/java/art/arcane/iris/core/IrisWorldGeneratorResolver.java")).replace("\r\n", "\n");
int resolverStart = source.indexOf("private ChunkGenerator resolveFrozenWorldGenerator(");
@@ -208,26 +208,32 @@ public class IrisWorldGeneratorResolverTest {
"Frozen Iris world storage does not match the current platform layout",
currentPlatformRoot
);
int snapshotRoot = resolver.indexOf("IrisWorldStorage.requireFrozenPackRoot(dimensionRoot)");
int history = resolver.indexOf("requireGenerationHistory(dimensionRoot, id, worldSeed)");
int snapshotRoot = resolver.indexOf("requireActivePack(history)", history);
int validation = resolver.indexOf("requireSnapshotLoadable(snapshotRoot)");
int exactLoad = resolver.indexOf(
"IrisData.get(snapshotRoot).getDimensionLoader().load(id, false)"
);
int exactLoad = resolver.indexOf("requireHistoricalDimension(history, snapshotRoot, id)");
int canonicalIdentity = resolver.indexOf(".platformIdentity(worldKey.toString())");
int historicalSeed = resolver.indexOf(".seed(history.activeEpoch().worldSeed())");
int resolvedStorage = resolver.indexOf(".worldFolder(dimensionRoot)");
assertTrue(dimensionRoot >= 0);
assertTrue(currentPlatformRoot > dimensionRoot);
assertTrue(layoutRefusal > currentPlatformRoot);
assertTrue(snapshotRoot > layoutRefusal);
assertTrue(history > layoutRefusal);
assertTrue(snapshotRoot > history);
assertTrue(validation > snapshotRoot);
assertTrue(exactLoad > validation);
assertTrue(canonicalIdentity > exactLoad);
assertTrue(resolvedStorage > canonicalIdentity);
assertTrue(historicalSeed > canonicalIdentity);
assertTrue(resolvedStorage > historicalSeed);
assertFalse(resolver.contains("loadDimension("));
assertFalse(resolver.contains("loadAnyDimension("));
assertFalse(resolver.contains("replaceIntoWorld("));
assertFalse(resolver.contains("installIntoWorld("));
assertFalse(resolver.contains(".seed(1337)"));
assertTrue(resolver.contains("GenerationHistory.openIfPresent(root, worldSeed)"));
assertTrue(resolver.contains("GenerationHistory.adoptLegacyPack("));
assertTrue(resolver.contains("GenerationRegistryContractFactory.captureRequiredDefinitions("));
}
@Test
@@ -0,0 +1,26 @@
package art.arcane.iris.core.commands;
import org.junit.Test;
import java.nio.file.Files;
import java.nio.file.Path;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
public final class CommandDeveloperGenerationHistoryTest {
@Test
public void updateWorldStagesThePackWithTheLiveWorldSeed() throws Exception {
String source = Files.readString(Path.of(
"src/main/java/art/arcane/iris/core/commands/CommandDeveloper.java"
)).replace("\r\n", "\n");
int updateStart = source.indexOf("public void updateWorld(");
int updateEnd = source.indexOf("@Director(description = \"Test\"", updateStart);
String update = source.substring(updateStart, updateEnd);
assertTrue(update.contains(
"replaceIntoWorld(sender(), pack, folder, world.getSeed())"
));
assertFalse(update.contains("iris/pack"));
}
}
@@ -0,0 +1,26 @@
package art.arcane.iris.core.commands;
import org.junit.Test;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
public final class CommandFindRiverContractTest {
@Test
public void riverLookupMergesRecordedAndEligibleActiveFeatures() throws IOException {
String source = Files.readString(Path.of(System.getProperty("iris.commandFindSource")));
int methodStart = source.indexOf("public void river(");
int methodEnd = source.indexOf("public void structure(", methodStart);
String method = source.substring(methodStart, methodEnd);
assertTrue(method.contains("GenerationSemanticQueries.nearestRiver("));
assertTrue(method.contains("runtime == null"));
assertTrue(method.contains("feature.y()"));
assertFalse(method.contains("runtime.nearestFeature("));
assertFalse(method.contains("feature.y() + activeEngine.getDimension().getMinHeight()"));
}
}
@@ -0,0 +1,27 @@
package art.arcane.iris.core.commands;
import org.junit.Test;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
public final class CommandFindStructureHistoryContractTest {
@Test
public void admitsRetainedStructuresBeforeActiveOnlyRejectionsAndUsesSemanticSearch() throws IOException {
String source = Files.readString(Path.of(System.getProperty("iris.commandFindSource")));
int structureStart = source.indexOf("public void structure(");
int retainedAdmission = source.indexOf("GenerationFindCatalog.hasRetainedStructurePlacement(e, structureKey)", structureStart);
int unknownRejection = source.indexOf("if (route == StructureLookupRoute.UNKNOWN)", structureStart);
assertTrue(retainedAdmission > structureStart && retainedAdmission < unknownRejection);
int locateStart = source.indexOf("private void locateIrisStructure(");
int locateEnd = source.indexOf("public void object(", locateStart);
String locate = source.substring(locateStart, locateEnd);
assertTrue(locate.contains("GenerationSemanticQueries.nearestStructure("));
assertFalse(locate.contains("IrisStructureLocator.locate("));
}
}
@@ -30,7 +30,7 @@ public class BukkitEngineLifecycleContractTest {
String baseHeight = method(source, "public int getBaseHeight(");
assertTrue(baseHeight.contains("currentEngine.acquireGenerationLease(\"bukkit_base_height\")"));
assertTrue(baseHeight.contains("IrisContext.open(currentEngine, lease.sessionId(), null)"));
assertTrue(baseHeight.contains("catch (GenerationSessionException e)"));
assertTrue(baseHeight.contains("catch (GenerationSessionException | IOException e)"));
String generation = method(source, "public void generateNoise(");
assertTrue(generation.contains("throw new IllegalStateException"));
@@ -0,0 +1,155 @@
package art.arcane.iris.core.service;
import art.arcane.iris.Iris;
import art.arcane.iris.core.commands.CommandIris;
import art.arcane.iris.core.tools.IrisToolbelt;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.history.GenerationFindCatalog;
import art.arcane.iris.engine.object.IrisBiome;
import art.arcane.iris.engine.object.IrisRegion;
import art.arcane.iris.engine.platform.EngineBukkitOps;
import art.arcane.iris.engine.platform.PlatformChunkGenerator;
import art.arcane.iris.util.common.director.DirectorContext;
import art.arcane.volmlib.util.collection.KList;
import art.arcane.volmlib.util.director.DirectorEngineOptions;
import art.arcane.volmlib.util.director.compat.DirectorEngineFactory;
import art.arcane.volmlib.util.director.runtime.DirectorExecutionDispatcher;
import art.arcane.volmlib.util.director.runtime.DirectorExecutionResult;
import art.arcane.volmlib.util.director.runtime.DirectorRuntimeEngine;
import art.arcane.volmlib.util.plugin.ComponentMessenger;
import org.bukkit.World;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player;
import org.junit.Test;
import org.mockito.MockedStatic;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.ArgumentMatchers.same;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.mockStatic;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.when;
public class CommandSVCFindParsingTest {
@Test
public void registeredBiomeHandlerResolvesActiveThenRemovedNamesDuringParsing() throws Exception {
Fixture fixture = new Fixture();
TestCommandService service = new TestCommandService(DirectorExecutionDispatcher.IMMEDIATE);
IrisBiome plateau = biome("plateau");
IrisBiome meadow = biome("meadow");
try (MockedStatic<IrisToolbelt> toolbelt = mockStatic(IrisToolbelt.class);
MockedStatic<GenerationFindCatalog> catalog = mockStatic(GenerationFindCatalog.class);
MockedStatic<EngineBukkitOps> operations = mockStatic(EngineBukkitOps.class)) {
toolbelt.when(() -> IrisToolbelt.access(fixture.world)).thenReturn(fixture.generator);
catalog.when(() -> GenerationFindCatalog.biomes(fixture.engine))
.thenReturn(new KList<>(plateau, meadow));
assertTrue(execute(service, fixture.player, "biome", "biome=plateau").isSuccess());
assertNull(DirectorContext.get());
assertTrue(execute(service, fixture.player, "biome", "biome=meadow").isSuccess());
assertNull(DirectorContext.get());
operations.verify(() -> EngineBukkitOps.gotoBiome(same(fixture.engine), same(plateau), same(fixture.player), eq(true)));
operations.verify(() -> EngineBukkitOps.gotoBiome(same(fixture.engine), same(meadow), same(fixture.player), eq(true)));
catalog.verify(() -> GenerationFindCatalog.biomes(fixture.engine), times(2));
catalog.verifyNoMoreInteractions();
}
}
@Test
public void registeredHistoricalHandlersParseBeforeDeferredInvocationAndClearFailedContext() throws Exception {
Fixture fixture = new Fixture();
List<Runnable> queued = new ArrayList<>();
TestCommandService service = new TestCommandService((mode, action) -> queued.add(action));
IrisBiome meadow = biome("meadow");
IrisRegion oldRegion = new IrisRegion();
oldRegion.setLoadKey("old-region");
try (MockedStatic<IrisToolbelt> toolbelt = mockStatic(IrisToolbelt.class);
MockedStatic<GenerationFindCatalog> catalog = mockStatic(GenerationFindCatalog.class);
MockedStatic<EngineBukkitOps> operations = mockStatic(EngineBukkitOps.class);
MockedStatic<ComponentMessenger> messages = mockStatic(ComponentMessenger.class)) {
toolbelt.when(() -> IrisToolbelt.access(fixture.world)).thenReturn(fixture.generator);
catalog.when(() -> GenerationFindCatalog.biomes(fixture.engine)).thenReturn(new KList<>(meadow));
catalog.when(() -> GenerationFindCatalog.regions(fixture.engine)).thenReturn(new KList<>(oldRegion));
catalog.when(() -> GenerationFindCatalog.objectKeys(fixture.engine)).thenReturn(new KList<>("old-tree"));
operations.when(() -> EngineBukkitOps.gotoBiome(fixture.engine, meadow, fixture.player, true))
.thenAnswer(invocation -> {
assertSame(fixture.player, DirectorContext.get().player());
return null;
});
assertTrue(execute(service, fixture.player, "biome", "biome=meadow").isSuccess());
assertTrue(execute(service, fixture.player, "region", "region=old-region").isSuccess());
assertTrue(execute(service, fixture.player, "object", "object=old-tree").isSuccess());
assertEquals(3, queued.size());
assertNull(DirectorContext.get());
queued.getFirst().run();
assertNull(DirectorContext.get());
operations.verify(() -> EngineBukkitOps.gotoBiome(fixture.engine, meadow, fixture.player, true));
DirectorExecutionResult rejected = execute(service, fixture.player, "biome", "biome=authoring-only");
assertFalse(rejected.isSuccess());
assertTrue(rejected.getMessage().contains("Cannot convert"));
assertEquals(3, queued.size());
assertNull(DirectorContext.get());
catalog.verify(() -> GenerationFindCatalog.regions(fixture.engine));
catalog.verify(() -> GenerationFindCatalog.objectKeys(fixture.engine));
}
}
private static DirectorExecutionResult execute(CommandSVC service, Player player, String command, String argument)
throws Exception {
Method run = CommandSVC.class.getDeclaredMethod("runDirector", CommandSender.class, String.class, String[].class);
run.setAccessible(true);
return (DirectorExecutionResult) run.invoke(service, player, "iris",
new String[]{"find", command, argument, "teleport=true"});
}
private static IrisBiome biome(String key) {
IrisBiome biome = new IrisBiome();
biome.setLoadKey(key);
return biome;
}
private static final class Fixture {
private final Engine engine = mock(Engine.class);
private final Player player = mock(Player.class);
private final World world = mock(World.class);
private final PlatformChunkGenerator generator = mock(PlatformChunkGenerator.class);
private Fixture() {
when(player.getWorld()).thenReturn(world);
when(player.getUniqueId()).thenReturn(UUID.fromString("00000000-0000-0000-0000-000000000001"));
when(generator.getEngine()).thenReturn(engine);
}
}
private static final class TestCommandService extends CommandSVC {
private final DirectorRuntimeEngine director;
private TestCommandService(DirectorExecutionDispatcher dispatcher) {
try (MockedStatic<Iris> iris = mockStatic(Iris.class)) {
director = DirectorEngineFactory.create(new CommandIris(), DirectorEngineOptions.builder()
.dispatcher(dispatcher)
.invocationHook(this)
.build());
}
}
@Override
public DirectorRuntimeEngine getDirector() {
return director;
}
}
}