Fix Iris replacement

This commit is contained in:
Brian Neumann-Fopiano
2026-08-15 15:30:42 -04:00
parent 4568b3fff1
commit 8297e49ed2
52 changed files with 4748 additions and 198 deletions
@@ -30,6 +30,7 @@ import art.arcane.iris.engine.object.BlockDataMergeSupport;
import art.arcane.iris.engine.object.IrisObjectRotation;
import art.arcane.iris.engine.object.TileData;
import art.arcane.iris.modded.api.ModdedCustomContentRegistry;
import art.arcane.iris.modded.command.IrisModdedCommands;
import art.arcane.iris.modded.command.ModdedGuiHost;
import art.arcane.iris.modded.command.ModdedObjectUndo;
import art.arcane.iris.modded.command.ModdedPregenBossBar;
@@ -109,6 +110,7 @@ public final class ModdedEngineBootstrap {
if (scheduler != null) {
scheduler.reset();
}
IrisModdedCommands.openDownloadAdmission();
ModdedStartup.prepareForStartup();
IrisModdedChunkGenerator.startGenPool();
bindWorldGenerators(server);
@@ -169,6 +171,7 @@ public final class ModdedEngineBootstrap {
public static void stop() {
MinecraftServer stoppingServer = currentServer;
Throwable failure = null;
failure = runStopStage(failure, "pack downloads", IrisModdedCommands::shutdownDownloads);
failure = runStopStage(failure, "world check", () -> ModdedWorldCheck.serverStopped(stoppingServer));
failure = runStopStage(failure, "protocol", ModdedProtocolHandler::stop);
failure = runStopStage(failure, "pregenerator", ModdedPregenJob::shutdown);
@@ -349,7 +349,7 @@ final class ModdedNativeStructureStage {
try {
int runtimeMinY = world.getMinY();
WorldgenTerrainHeightmaps.primeStructurePlacement(
world, heightmapStarts,
world, chunkPos, heightmapStarts,
worldgenSurfaceHeight(current, runtimeMinY),
worldgenFloorHeight(current, runtimeMinY));
} catch (Throwable error) {
@@ -24,14 +24,19 @@ import net.minecraft.server.MinecraftServer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.PriorityBlockingQueue;
import java.util.concurrent.RejectedExecutionException;
import java.util.concurrent.RejectedExecutionHandler;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicLong;
@@ -76,6 +81,9 @@ public final class ModdedScheduler implements PlatformScheduler {
private static RejectedExecutionHandler dropRejectedTask() {
return (Runnable task, ThreadPoolExecutor executor) -> {
if (task instanceof RejectionAwareTask rejectionAwareTask) {
rejectionAwareTask.reject();
}
if (executor.isShutdown()) {
LOGGER.debug("Iris async task dropped: scheduler is shut down");
return;
@@ -130,6 +138,24 @@ public final class ModdedScheduler implements PlatformScheduler {
executor.execute(() -> runGuarded(task));
}
public boolean asyncIfRunning(Runnable task, Runnable rejection) {
if (task == null) {
return false;
}
ThreadPoolExecutor executor = asyncExecutor;
RejectionAwareTask submittedTask = new RejectionAwareTask(
() -> runGuarded(task),
Objects.requireNonNull(rejection, "rejection")
);
warnOnBacklog(executor);
try {
executor.execute(submittedTask);
} catch (RejectedExecutionException exception) {
submittedTask.reject();
}
return !submittedTask.isRejected();
}
@Override
public void laterGlobal(Runnable task, int ticks) {
if (task == null) {
@@ -151,7 +177,9 @@ public final class ModdedScheduler implements PlatformScheduler {
if (asyncExecutor.isShutdown()) {
asyncExecutor = createAsyncExecutor();
} else {
asyncExecutor.getQueue().clear();
List<Runnable> abandonedTasks = new ArrayList<>();
asyncExecutor.getQueue().drainTo(abandonedTasks);
rejectAbandonedTasks(abandonedTasks);
}
mainQueue.clear();
delayedQueue.clear();
@@ -162,7 +190,8 @@ public final class ModdedScheduler implements PlatformScheduler {
}
public void shutdown() {
asyncExecutor.shutdownNow();
List<Runnable> abandonedTasks = asyncExecutor.shutdownNow();
rejectAbandonedTasks(abandonedTasks);
mainQueue.clear();
delayedQueue.clear();
mainThread = null;
@@ -170,6 +199,14 @@ public final class ModdedScheduler implements PlatformScheduler {
ModdedServerLevels.forget();
}
private static void rejectAbandonedTasks(List<Runnable> abandonedTasks) {
for (Runnable abandonedTask : abandonedTasks) {
if (abandonedTask instanceof RejectionAwareTask rejectionAwareTask) {
rejectionAwareTask.reject();
}
}
}
private void warnOnBacklog(ThreadPoolExecutor executor) {
int queued = executor.getQueue().size();
if (queued < ASYNC_BACKLOG_WARN) {
@@ -247,4 +284,33 @@ public final class ModdedScheduler implements PlatformScheduler {
return thread;
}
}
private static final class RejectionAwareTask implements Runnable {
private final Runnable task;
private final Runnable rejection;
private final AtomicBoolean rejected;
private RejectionAwareTask(Runnable task, Runnable rejection) {
this.task = task;
this.rejection = rejection;
rejected = new AtomicBoolean();
}
@Override
public void run() {
if (!rejected.get()) {
task.run();
}
}
private void reject() {
if (rejected.compareAndSet(false, true)) {
rejection.run();
}
}
private boolean isRejected() {
return rejected.get();
}
}
}
@@ -19,7 +19,13 @@
package art.arcane.iris.modded.command;
import art.arcane.iris.core.IrisSettings;
import art.arcane.iris.core.localization.IrisLanguage;
import art.arcane.iris.core.localization.IrisMessages;
import art.arcane.iris.core.localization.ModdedCommandMessages;
import art.arcane.iris.core.localization.PackDownloadMessages;
import art.arcane.iris.core.localization.RuntimeUiMessages;
import art.arcane.iris.core.lifecycle.LifecycleOperationCoordinator;
import art.arcane.iris.core.pack.PackDownloadExecution;
import art.arcane.iris.core.pack.PackDownloader;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.modded.IrisModdedChunkGenerator;
@@ -56,15 +62,18 @@ import java.util.Locale;
import java.util.Map;
import java.util.TreeMap;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.TimeUnit;
import art.arcane.iris.core.localization.IrisLanguage;
import art.arcane.iris.core.localization.ModdedCommandMessages;
import art.arcane.iris.core.localization.RuntimeUiMessages;
import art.arcane.volmlib.util.localization.MessageArgument;
public final class IrisModdedCommands {
private static final Logger LOGGER = LoggerFactory.getLogger("Iris");
private static final long DOWNLOAD_SHUTDOWN_POLL_SECONDS = 15L;
private static final Object DOWNLOAD_MONITOR = new Object();
static final SuggestionProvider<CommandSourceStack> PACK_NAMES = ModdedCommandSuggestions.PACK_NAMES;
private static PackDownloadExecution activeDownload;
private static boolean downloadAdmissionOpen;
private IrisModdedCommands() {
}
@@ -76,6 +85,43 @@ public final class IrisModdedCommands {
IrisLogging.info("Iris /iris command tree registered");
}
public static void openDownloadAdmission() {
synchronized (DOWNLOAD_MONITOR) {
downloadAdmissionOpen = true;
}
}
public static void shutdownDownloads() {
PackDownloadExecution execution;
synchronized (DOWNLOAD_MONITOR) {
downloadAdmissionOpen = false;
execution = activeDownload;
}
if (execution == null) {
return;
}
execution.cancel();
boolean interrupted = false;
boolean warned = false;
while (!execution.isComplete()) {
try {
if (!execution.await(DOWNLOAD_SHUTDOWN_POLL_SECONDS, TimeUnit.SECONDS) && !warned) {
warned = true;
LOGGER.warn(execution.isPublishing()
? "Waiting for atomic pack publication to finish before Iris shutdown."
: "Waiting for the active pack download to cancel before Iris shutdown.");
}
} catch (InterruptedException exception) {
interrupted = true;
execution.cancel();
}
}
if (interrupted) {
Thread.currentThread().interrupt();
}
}
static int tp(CommandSourceStack source, ServerLevel level, ServerPlayer target) {
ServerPlayer player = target != null ? target : source.getPlayer();
if (player == null) {
@@ -264,9 +310,8 @@ public final class IrisModdedCommands {
fail(source, "Use /iris download pack=overworld, /iris download pack=underworld, or /iris download link=<zip-url>.");
return 0;
}
String target = request.pack() == null ? request.url() : request.pack();
String target = downloadDisplayTarget(request);
String downloadSource = request.pack() == null ? "direct ZIP URL" : "built-in beta release";
ok(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_DOWNLOADING_IRISDIMENSIONS, MessageArgument.untrusted("pack", target), MessageArgument.untrusted("downloadSource", downloadSource)));
ModdedScheduler scheduler = ModdedEngineBootstrap.schedulerOrNull();
if (scheduler == null) {
fail(source, IrisLanguage.plain(
@@ -275,36 +320,143 @@ public final class IrisModdedCommands {
MessageArgument.untrusted("downloadSource", downloadSource)));
return 0;
}
scheduler.async(() -> {
boolean installed = false;
File packs = ModdedPackCommands.packsRoot();
PackDownloadExecution execution;
synchronized (DOWNLOAD_MONITOR) {
if (!downloadAdmissionOpen) {
fail(source, IrisLanguage.plain(
ModdedCommandMessages.IRIS_MODDED_COMMANDS_PACK_DOWNLOAD_FAILED_SEE_CONSOLE,
MessageArgument.untrusted("pack", target),
MessageArgument.untrusted("downloadSource", downloadSource)));
return 0;
}
LifecycleOperationCoordinator.Lease lease;
try {
PackDownloader.PackInstallResult result = request.pack() == null
? PackDownloader.downloadUrl(
packs,
request.url(),
false,
(String message) -> scheduler.global(() -> ok(source, message))
)
: PackDownloader.downloadBuiltIn(
packs,
request.pack(),
false,
(String message) -> scheduler.global(() -> ok(source, message))
);
installed = result != null;
} catch (IOException | RuntimeException error) {
LOGGER.error("Iris pack download failed for {}", target, error);
lease = LifecycleOperationCoordinator.get().acquire(
LifecycleOperationCoordinator.Domain.PACK_MUTATION,
LifecycleOperationCoordinator.OperationKind.PACK_DOWNLOAD,
target
);
} catch (LifecycleOperationCoordinator.BusyException error) {
fail(source, downloadBusyMessage(error.currentOperation()));
return 0;
}
if (installed) {
scheduler.global(() -> ok(source, "Pack installed on disk. Restart the server before using it."));
} else {
scheduler.global(() -> fail(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_PACK_DOWNLOAD_FAILED_SEE_CONSOLE, MessageArgument.untrusted("pack", target), MessageArgument.untrusted("downloadSource", downloadSource))));
execution = new PackDownloadExecution(
lease,
cancellation -> executeDownload(source, request, target, downloadSource, scheduler, cancellation)
);
PackDownloadExecution trackedExecution = execution;
execution.onCompletion(() -> clearActiveDownload(trackedExecution));
activeDownload = execution;
boolean accepted;
try {
accepted = scheduler.asyncIfRunning(execution, execution::cancel);
} catch (Throwable error) {
execution.cancel();
LOGGER.error("Iris pack download dispatch failed for {}", target, error);
fail(source, IrisLanguage.plain(
ModdedCommandMessages.IRIS_MODDED_COMMANDS_PACK_DOWNLOAD_FAILED_SEE_CONSOLE,
MessageArgument.untrusted("pack", target),
MessageArgument.untrusted("downloadSource", downloadSource)));
return 0;
}
});
if (!accepted) {
execution.cancel();
LOGGER.error("Iris pack download dispatch rejected for {} because the scheduler is shut down", target);
fail(source, IrisLanguage.plain(
ModdedCommandMessages.IRIS_MODDED_COMMANDS_PACK_DOWNLOAD_FAILED_SEE_CONSOLE,
MessageArgument.untrusted("pack", target),
MessageArgument.untrusted("downloadSource", downloadSource)));
return 0;
}
}
ok(source, IrisLanguage.plain(
ModdedCommandMessages.IRIS_MODDED_COMMANDS_DOWNLOADING_IRISDIMENSIONS,
MessageArgument.untrusted("pack", target),
MessageArgument.untrusted("downloadSource", downloadSource)));
return 1;
}
private static void executeDownload(
CommandSourceStack source,
DownloadRequest request,
String target,
String downloadSource,
ModdedScheduler scheduler,
PackDownloader.DownloadCancellation cancellation
) throws PackDownloader.PackDownloadCancelledException {
File packs = ModdedPackCommands.packsRoot();
try {
PackDownloader.PackInstallResult result = request.pack() == null
? PackDownloader.downloadUrl(
packs,
request.url(),
false,
(String message) -> scheduler.global(() -> ok(source, message)),
cancellation
)
: PackDownloader.downloadBuiltIn(
packs,
request.pack(),
false,
(String message) -> scheduler.global(() -> ok(source, message)),
cancellation
);
String completionMessage = downloadCompletionMessage(result);
if (result != null) {
if (completionMessage != null) {
scheduler.global(() -> ok(source, completionMessage));
}
return;
}
} catch (PackDownloader.PackDownloadCancelledException error) {
throw error;
} catch (PackDownloader.PackDownloadBusyException error) {
scheduler.global(() -> fail(source, error.getMessage()));
return;
} catch (IOException | RuntimeException error) {
LOGGER.error("Iris pack download failed for {}", target, error);
}
scheduler.global(() -> fail(source, IrisLanguage.plain(
ModdedCommandMessages.IRIS_MODDED_COMMANDS_PACK_DOWNLOAD_FAILED_SEE_CONSOLE,
MessageArgument.untrusted("pack", target),
MessageArgument.untrusted("downloadSource", downloadSource))));
}
static String downloadBusyMessage(LifecycleOperationCoordinator.ActiveOperation operation) {
if (operation.domain() == LifecycleOperationCoordinator.Domain.PACK_MUTATION
&& operation.kind() == LifecycleOperationCoordinator.OperationKind.PACK_DOWNLOAD) {
return IrisLanguage.plain(PackDownloadMessages.IN_PROGRESS);
}
return "Iris pack changes are busy with " + operation.kind().name().toLowerCase(Locale.ROOT)
+ " for '" + operation.target() + "'. Try again when it completes.";
}
static String downloadCompletionMessage(PackDownloader.PackInstallResult result) {
if (result == null || !result.changed()) {
return null;
}
return result.restartRequired()
? "Pack installed on disk. Restart the server before using it."
: "Pack installed on disk.";
}
static String downloadDisplayTarget(DownloadRequest request) {
return request.pack() == null
? IrisLanguage.plain(PackDownloadMessages.PROGRESS_SOURCE_REMOTE)
: request.pack();
}
private static void clearActiveDownload(PackDownloadExecution execution) {
synchronized (DOWNLOAD_MONITOR) {
if (activeDownload == execution) {
activeDownload = null;
}
}
}
static DownloadRequest parseDownloadRequest(String rawRequest) {
if (rawRequest == null || rawRequest.isBlank()) {
return null;
@@ -86,6 +86,60 @@ public class ModdedGenerationLeaseContractTest {
assertTrue(source.contains("active.awaitTermination(INTERRUPT_DRAIN_TIMEOUT_SECONDS, TimeUnit.SECONDS)"));
}
@Test
public void packDownloadAdmissionPrecedesAsyncDispatch() throws IOException {
String source = source("art/arcane/iris/modded/command/IrisModdedCommands.java");
String download = method(source, "static int download(CommandSourceStack source, String rawRequest)");
int admission = download.indexOf("LifecycleOperationCoordinator.get().acquire(");
int executionTracking = download.indexOf("new PackDownloadExecution(");
int dispatch = download.indexOf("scheduler.asyncIfRunning(execution, execution::cancel)");
assertTrue(admission >= 0);
assertTrue(executionTracking > admission);
assertTrue(dispatch > admission);
assertTrue(download.contains("execution.cancel();"));
String execution = method(source, "private static void executeDownload(");
assertTrue(execution.contains("PackDownloader.DownloadCancellation cancellation"));
assertTrue(execution.contains("catch (PackDownloader.PackDownloadCancelledException error)"));
assertFalse(execution.contains("lease.close();"));
}
@Test
public void packDownloadsDrainBeforeModdedSchedulerShutdown() throws IOException {
String commands = source("art/arcane/iris/modded/command/IrisModdedCommands.java");
String shutdownDownloads = method(commands, "public static void shutdownDownloads()");
assertTrue(shutdownDownloads.contains("downloadAdmissionOpen = false;"));
assertTrue(shutdownDownloads.contains("execution.cancel();"));
assertTrue(shutdownDownloads.contains("execution.await("));
String bootstrap = source("art/arcane/iris/modded/ModdedEngineBootstrap.java");
String stop = method(bootstrap, "public static void stop()");
int downloads = stop.indexOf("IrisModdedCommands::shutdownDownloads");
int scheduler = stop.indexOf("scheduler::shutdown");
assertTrue(downloads >= 0);
assertTrue(scheduler > downloads);
}
@Test
public void moddedSchedulerRejectsAndCancelsAbandonedDownloadSubmissions() throws IOException {
String scheduler = source("art/arcane/iris/modded/ModdedScheduler.java");
String dispatch = method(scheduler, "public boolean asyncIfRunning(Runnable task, Runnable rejection)");
assertTrue(dispatch.contains("RejectionAwareTask"));
assertTrue(dispatch.contains("Objects.requireNonNull(rejection"));
String shutdown = method(scheduler, "public void shutdown()");
assertTrue(shutdown.contains("asyncExecutor.shutdownNow()"));
assertTrue(shutdown.contains("rejectAbandonedTasks(abandonedTasks);"));
String reset = method(scheduler, "public void reset()");
assertTrue(reset.contains("asyncExecutor.getQueue().drainTo(abandonedTasks);"));
assertTrue(reset.contains("rejectAbandonedTasks(abandonedTasks);"));
String rejection = method(scheduler, "private static void rejectAbandonedTasks(");
assertTrue(rejection.contains("rejectionAwareTask.reject();"));
}
@Test
public void blockingPregenShutdownDefersItsFinalSaveToTheServerThread() throws IOException {
String jobSource = source("art/arcane/iris/modded/command/ModdedPregenJob.java");
@@ -0,0 +1,110 @@
package art.arcane.iris.modded;
import art.arcane.iris.core.lifecycle.LifecycleOperationCoordinator;
import art.arcane.iris.core.pack.PackDownloadExecution;
import org.junit.Test;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
public class ModdedSchedulerDownloadCancellationTest {
@Test
public void rejectedDownloadSubmissionReleasesItsLease() throws Exception {
ModdedScheduler scheduler = new ModdedScheduler();
scheduler.shutdown();
TestLease lease = new TestLease();
AtomicBoolean ran = new AtomicBoolean();
PackDownloadExecution execution = new PackDownloadExecution(
lease,
cancellation -> ran.set(true)
);
boolean accepted = scheduler.asyncIfRunning(execution, execution::cancel);
assertFalse(accepted);
assertTrue(execution.await(1L, TimeUnit.SECONDS));
assertFalse(ran.get());
assertEquals(1, lease.closeCount());
}
@Test
public void schedulerShutdownCancelsQueuedDownloadAndReleasesItsLease() throws Exception {
ModdedScheduler scheduler = new ModdedScheduler();
int workerCount = Math.max(4, Runtime.getRuntime().availableProcessors());
CountDownLatch workersStarted = new CountDownLatch(workerCount);
CountDownLatch releaseWorkers = new CountDownLatch(1);
for (int index = 0; index < workerCount; index++) {
scheduler.async(() -> {
workersStarted.countDown();
try {
releaseWorkers.await();
} catch (InterruptedException exception) {
Thread.currentThread().interrupt();
}
});
}
assertTrue(workersStarted.await(10L, TimeUnit.SECONDS));
TestLease lease = new TestLease();
AtomicBoolean ran = new AtomicBoolean();
PackDownloadExecution execution = new PackDownloadExecution(
lease,
cancellation -> ran.set(true)
);
assertTrue(scheduler.asyncIfRunning(execution, execution::cancel));
try {
scheduler.shutdown();
assertTrue(execution.await(5L, TimeUnit.SECONDS));
assertFalse(ran.get());
assertEquals(1, lease.closeCount());
} finally {
releaseWorkers.countDown();
scheduler.shutdown();
}
}
private static final class TestLease implements LifecycleOperationCoordinator.Lease {
private final LifecycleOperationCoordinator.ActiveOperation operation;
private final AtomicBoolean closed;
private final AtomicInteger closeCount;
private TestLease() {
operation = new LifecycleOperationCoordinator.ActiveOperation(
1L,
LifecycleOperationCoordinator.Domain.PACK_MUTATION,
LifecycleOperationCoordinator.OperationKind.PACK_DOWNLOAD,
"test"
);
closed = new AtomicBoolean();
closeCount = new AtomicInteger();
}
@Override
public LifecycleOperationCoordinator.ActiveOperation operation() {
return operation;
}
@Override
public boolean isClosed() {
return closed.get();
}
@Override
public void close() {
closeCount.incrementAndGet();
closed.set(true);
}
private int closeCount() {
return closeCount.get();
}
}
}
@@ -1,5 +1,9 @@
package art.arcane.iris.modded.command;
import art.arcane.iris.core.localization.IrisLanguage;
import art.arcane.iris.core.localization.PackDownloadMessages;
import art.arcane.iris.core.lifecycle.LifecycleOperationCoordinator;
import art.arcane.iris.core.pack.PackDownloader;
import com.mojang.brigadier.CommandDispatcher;
import com.mojang.brigadier.tree.CommandNode;
import net.minecraft.SharedConstants;
@@ -9,10 +13,11 @@ import org.junit.Test;
import java.util.List;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
public class IrisModdedCommandParityTest {
@@ -73,6 +78,9 @@ public class IrisModdedCommandParityTest {
assertEquals("underworld", underworld.pack());
assertNotNull(link);
assertEquals("https://packs.example.test/custom.zip?token=a=b", link.url());
String displayTarget = IrisModdedCommands.downloadDisplayTarget(link);
assertEquals(IrisLanguage.plain(PackDownloadMessages.PROGRESS_SOURCE_REMOTE), displayTarget);
assertFalse(displayTarget.contains("token"));
assertNull(IrisModdedCommands.parseDownloadRequest("overworld"));
assertNull(IrisModdedCommands.parseDownloadRequest("pack=custom"));
assertNull(IrisModdedCommands.parseDownloadRequest("link=https://packs.example.test/custom.tar.gz"));
@@ -80,6 +88,51 @@ public class IrisModdedCommandParityTest {
assertNull(IrisModdedCommands.parseDownloadRequest("pack=underworld overwrite=true"));
}
@Test
public void downloadCompletionMessageOnlyReportsActualPackChanges() {
assertEquals(
"Pack installed on disk. Restart the server before using it.",
IrisModdedCommands.downloadCompletionMessage(
new PackDownloader.PackInstallResult("overworld", true, true)
)
);
assertEquals(
"Pack installed on disk.",
IrisModdedCommands.downloadCompletionMessage(
new PackDownloader.PackInstallResult("overworld", true, false)
)
);
assertNull(IrisModdedCommands.downloadCompletionMessage(
new PackDownloader.PackInstallResult("overworld", false, false)
));
assertNull(IrisModdedCommands.downloadCompletionMessage(null));
}
@Test
public void downloadBusyMessageDistinguishesPackDownloadsFromOtherLifecycleWork() {
LifecycleOperationCoordinator.ActiveOperation packDownload = new LifecycleOperationCoordinator.ActiveOperation(
1L,
LifecycleOperationCoordinator.Domain.PACK_MUTATION,
LifecycleOperationCoordinator.OperationKind.PACK_DOWNLOAD,
"overworld"
);
LifecycleOperationCoordinator.ActiveOperation worldCreation = new LifecycleOperationCoordinator.ActiveOperation(
2L,
LifecycleOperationCoordinator.Domain.WORLD_MUTATION,
LifecycleOperationCoordinator.OperationKind.WORLD_CREATE,
"iris_world"
);
assertEquals(
IrisLanguage.plain(PackDownloadMessages.IN_PROGRESS),
IrisModdedCommands.downloadBusyMessage(packDownload)
);
assertEquals(
"Iris pack changes are busy with world_create for 'iris_world'. Try again when it completes.",
IrisModdedCommands.downloadBusyMessage(worldCreation)
);
}
@Test
public void helpDocumentsParityCommandsAndPlatformStubs() {
assertTrue(ModdedCommandHelp.documents("what", "here"));