> preparations, Steps steps, ExecutorService workers) {
+ this.preparations = preparations;
+ this.steps = steps;
+ this.workers = workers;
+ }
+
+ static
ModdedDimensionInjection
start(
+ List dimensions,
+ Steps steps
+ ) {
+ Steps
requiredSteps = Objects.requireNonNull(steps, "steps");
+ ExecutorService workers = Executors.newFixedThreadPool(workerCount(dimensions.size()),
+ Thread.ofPlatform().name("Iris Dimension Restore ", 1L).daemon(true).factory());
+ List> preparations = new ArrayList<>(dimensions.size());
+ for (ModdedDimensionRegistryStore.PersistentDimension dimension : dimensions) {
+ preparations.add(new Preparation<>(dimension, CompletableFuture.supplyAsync(
+ () -> timed(() -> requiredSteps.prepare(dimension)), workers)));
+ }
+ return new ModdedDimensionInjection<>(List.copyOf(preparations), requiredSteps, workers);
+ }
+
+ static Async
async(
+ ModdedDimensionRegistryStore.PersistentDimension dimension,
+ Steps
steps,
+ Executor serverThread
+ ) {
+ return new Async<>(dimension, steps, serverThread);
+ }
+
+ void awaitPreparations() {
+ List>> futures = new ArrayList<>(preparations.size());
+ for (Preparation preparation : preparations) {
+ futures.add(preparation.future());
+ }
+ CompletableFuture.allOf(futures.toArray(CompletableFuture[]::new))
+ .handle((Void ignored, Throwable failure) -> null)
+ .join();
+ }
+
+ List finish() {
+ if (!finished.compareAndSet(false, true)) {
+ throw new IllegalStateException("Iris dimension injection already finished");
+ }
+ try {
+ List> building = new ArrayList<>(preparations.size());
+ Result[] results = new Result[preparations.size()];
+ for (int index = 0; index < preparations.size(); index++) {
+ Preparation preparation = preparations.get(index);
+ Timed
prepared;
+ try {
+ prepared = join(preparation.future());
+ } catch (Throwable failure) {
+ results[index] = new Result(preparation.dimension(), failure, 0L, 0L);
+ continue;
+ }
+ L level;
+ try {
+ level = steps.begin(preparation.dimension(), prepared.value());
+ } catch (Throwable failure) {
+ discard(steps, prepared.value(), failure);
+ results[index] = new Result(preparation.dimension(), failure, prepared.millis(), 0L);
+ continue;
+ }
+ CompletableFuture> engine = CompletableFuture.supplyAsync(
+ () -> timed(() -> steps.build(level, prepared.value())), workers);
+ building.add(new Building<>(index, preparation.dimension(), prepared.millis(), level, engine));
+ }
+ for (Building pending : building) {
+ results[pending.index()] = complete(pending);
+ }
+ return List.of(results);
+ } finally {
+ workers.shutdown();
+ }
+ }
+
+ @Override
+ public void close() {
+ if (finished.compareAndSet(false, true)) {
+ for (Preparation preparation : preparations) {
+ preparation.future().whenComplete((Timed
prepared, Throwable failure) -> {
+ if (prepared != null) {
+ discard(steps, prepared.value(), null);
+ }
+ });
+ }
+ }
+ workers.shutdown();
+ }
+
+ private Result complete(Building pending) {
+ Timed engine;
+ try {
+ engine = join(pending.engine());
+ } catch (Throwable failure) {
+ abandon(steps, pending.level(), null, failure);
+ return new Result(pending.dimension(), failure, pending.preparedMillis(), 0L);
+ }
+ try {
+ steps.complete(pending.level(), engine.value());
+ return new Result(pending.dimension(), null, pending.preparedMillis(), engine.millis());
+ } catch (Throwable failure) {
+ return new Result(pending.dimension(), failure, pending.preparedMillis(), engine.millis());
+ }
+ }
+
+ private static void abandon(Steps
steps, L level, E engine, Throwable failure) {
+ try {
+ steps.abandon(level, engine, failure);
+ } catch (Throwable cleanupFailure) {
+ failure.addSuppressed(cleanupFailure);
+ }
+ }
+
+ private static
void discard(Steps
steps, P prepared, Throwable failure) {
+ try {
+ steps.discard(prepared);
+ } catch (Throwable cleanupFailure) {
+ if (failure != null) {
+ failure.addSuppressed(cleanupFailure);
+ return;
+ }
+ ModdedIrisLog.error("Iris could not release an unused dimension preparation", cleanupFailure);
+ }
+ }
+
+ private static int workerCount(int dimensions) {
+ int processors = Runtime.getRuntime().availableProcessors();
+ return Math.max(1, Math.min(dimensions, Math.max(MINIMUM_WORKERS, processors / 2)));
+ }
+
+ private static Timed timed(Step step) {
+ long startedAt = System.nanoTime();
+ try {
+ T value = step.run();
+ return new Timed<>(value, (System.nanoTime() - startedAt) / 1_000_000L);
+ } catch (RuntimeException | Error failure) {
+ throw failure;
+ } catch (Exception failure) {
+ throw new CompletionException(failure);
+ }
+ }
+
+ private static Throwable unwrap(Throwable failure) {
+ Throwable current = failure;
+ while (current instanceof CompletionException && current.getCause() != null) {
+ current = current.getCause();
+ }
+ return current;
+ }
+
+ private static T join(CompletableFuture future) throws Throwable {
+ try {
+ return future.get();
+ } catch (ExecutionException failure) {
+ Throwable cause = failure.getCause();
+ if (cause instanceof CompletionException wrapped && wrapped.getCause() != null) {
+ throw wrapped.getCause();
+ }
+ throw cause == null ? failure : cause;
+ } catch (InterruptedException interrupted) {
+ Thread.currentThread().interrupt();
+ throw new IllegalStateException("Interrupted while injecting Iris dimensions", interrupted);
+ }
+ }
+
+ interface Steps {
+ P prepare(ModdedDimensionRegistryStore.PersistentDimension dimension) throws Exception;
+
+ L begin(ModdedDimensionRegistryStore.PersistentDimension dimension, P prepared) throws Exception;
+
+ E build(L level, P prepared) throws Exception;
+
+ void complete(L level, E engine) throws Exception;
+
+ void abandon(L level, E engine, Throwable failure);
+
+ void discard(P prepared);
+ }
+
+ record Result(
+ ModdedDimensionRegistryStore.PersistentDimension dimension,
+ Throwable failure,
+ long preparedMillis,
+ long builtMillis
+ ) {
+ }
+
+ static final class Async
{
+ private final ModdedDimensionRegistryStore.PersistentDimension dimension;
+ private final Steps
steps;
+ private final Executor serverThread;
+ private final ExecutorService worker;
+ private final CompletableFuture result = new CompletableFuture<>();
+ private State state = State.UNSTARTED;
+ private Timed prepared;
+ private L level;
+ private CompletableFuture> engine;
+
+ private Async(
+ ModdedDimensionRegistryStore.PersistentDimension dimension,
+ Steps steps,
+ Executor serverThread
+ ) {
+ this.dimension = Objects.requireNonNull(dimension, "dimension");
+ this.steps = Objects.requireNonNull(steps, "steps");
+ this.serverThread = Objects.requireNonNull(serverThread, "serverThread");
+ this.worker = Executors.newSingleThreadExecutor(
+ Thread.ofPlatform().name("Iris Dimension Create " + dimension.id()).daemon(true).factory());
+ }
+
+ CompletableFuture result() {
+ return result;
+ }
+
+ synchronized void start() {
+ if (state != State.UNSTARTED) {
+ throw new IllegalStateException("Iris dimension injection for '" + dimension.id() + "' already started");
+ }
+ state = State.PREPARING;
+ CompletableFuture.supplyAsync(() -> timed(() -> steps.prepare(dimension)), worker)
+ .whenComplete(this::prepared);
+ }
+
+ void cancel() {
+ Timed unused = null;
+ CompletableFuture> building = null;
+ L created = null;
+ synchronized (this) {
+ if (state == State.DONE || state == State.CANCELLED) {
+ return;
+ }
+ if (state == State.PREPARED) {
+ unused = prepared;
+ } else if (state == State.BUILDING) {
+ building = engine;
+ created = level;
+ }
+ state = State.CANCELLED;
+ }
+ CancellationException cancellation = new CancellationException("Iris dimension injection for '"
+ + dimension.id() + "' was cancelled because the server is stopping");
+ long preparedMillis = 0L;
+ if (unused != null) {
+ discard(steps, unused.value(), cancellation);
+ preparedMillis = unused.millis();
+ }
+ if (building != null) {
+ abandon(steps, created, awaitCancelledEngine(building, cancellation), cancellation);
+ }
+ done(new Result(dimension, cancellation, preparedMillis, 0L));
+ }
+
+ private void prepared(Timed value, Throwable failure) {
+ synchronized (this) {
+ if (state == State.CANCELLED) {
+ if (value != null) {
+ discard(steps, value.value(), null);
+ }
+ worker.shutdown();
+ return;
+ }
+ if (failure != null) {
+ state = State.DONE;
+ } else {
+ prepared = value;
+ state = State.PREPARED;
+ }
+ }
+ if (failure != null) {
+ done(new Result(dimension, unwrap(failure), 0L, 0L));
+ return;
+ }
+ serverThread.execute(this::begin);
+ }
+
+ private void begin() {
+ Timed
ready;
+ synchronized (this) {
+ if (state != State.PREPARED) {
+ return;
+ }
+ ready = prepared;
+ state = State.BEGINNING;
+ }
+ L created;
+ try {
+ created = steps.begin(dimension, ready.value());
+ } catch (Throwable failure) {
+ discard(steps, ready.value(), failure);
+ synchronized (this) {
+ state = State.DONE;
+ }
+ done(new Result(dimension, failure, ready.millis(), 0L));
+ return;
+ }
+ CompletableFuture> building;
+ synchronized (this) {
+ level = created;
+ state = State.BUILDING;
+ building = CompletableFuture.supplyAsync(() -> timed(() -> steps.build(created, ready.value())), worker);
+ engine = building;
+ }
+ building.whenComplete((Timed built, Throwable failure) -> serverThread.execute(this::complete));
+ }
+
+ private void complete() {
+ CompletableFuture> building;
+ L created;
+ long preparedMillis;
+ synchronized (this) {
+ if (state != State.BUILDING) {
+ return;
+ }
+ state = State.DONE;
+ building = engine;
+ created = level;
+ preparedMillis = prepared.millis();
+ }
+ Timed built;
+ try {
+ built = join(building);
+ } catch (Throwable failure) {
+ abandon(steps, created, null, failure);
+ done(new Result(dimension, failure, preparedMillis, 0L));
+ return;
+ }
+ try {
+ steps.complete(created, built.value());
+ done(new Result(dimension, null, preparedMillis, built.millis()));
+ } catch (Throwable failure) {
+ done(new Result(dimension, failure, preparedMillis, built.millis()));
+ }
+ }
+
+ private E awaitCancelledEngine(CompletableFuture> building, CancellationException cancellation) {
+ try {
+ return building.get(CANCEL_BUILD_WAIT_SECONDS, TimeUnit.SECONDS).value();
+ } catch (ExecutionException failure) {
+ return null;
+ } catch (TimeoutException failure) {
+ ModdedIrisLog.error("Iris dimension engine for '{}' did not finish within {}s of shutdown; abandoning its level",
+ dimension.id(), CANCEL_BUILD_WAIT_SECONDS);
+ cancellation.addSuppressed(failure);
+ return null;
+ } catch (InterruptedException interrupted) {
+ Thread.currentThread().interrupt();
+ cancellation.addSuppressed(interrupted);
+ return null;
+ }
+ }
+
+ private void done(Result outcome) {
+ worker.shutdown();
+ result.complete(outcome);
+ }
+
+ private enum State {
+ UNSTARTED,
+ PREPARING,
+ PREPARED,
+ BEGINNING,
+ BUILDING,
+ DONE,
+ CANCELLED
+ }
+ }
+
+ @FunctionalInterface
+ private interface Step {
+ T run() throws Exception;
+ }
+
+ private record Timed(T value, long millis) {
+ }
+
+ private record Preparation(
+ ModdedDimensionRegistryStore.PersistentDimension dimension,
+ CompletableFuture> future
+ ) {
+ }
+
+ private record Building(
+ int index,
+ ModdedDimensionRegistryStore.PersistentDimension dimension,
+ long preparedMillis,
+ L level,
+ CompletableFuture> engine
+ ) {
+ }
+}
diff --git a/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedDimensionManager.java b/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedDimensionManager.java
index d4303bea0..8e9ec2266 100644
--- a/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedDimensionManager.java
+++ b/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedDimensionManager.java
@@ -112,52 +112,108 @@ public final class ModdedDimensionManager {
);
}
- static RestoringLevel beginRestore(
+ static PendingInjection beginInjection(
NativeModdedServer server,
ModdedDimensionRegistryStore.PersistentDimension dimension,
- ModdedGenerationHistoryStorage.ActivePack active
+ ModdedGenerationHistoryStorage.ActivePack active,
+ ModdedGenerationMode generationMode
) {
NativeDimensionRuntime serverAccess = requireAccess();
DimensionRequest requested = new DimensionRequest(dimension.id(), dimension.pack(), dimension.dimension(), dimension.seed());
synchronized (LOCK) {
Handle present = presentHandle(server, serverAccess, requested);
if (present != null) {
- return new RestoringLevel(present, true);
+ return new PendingInjection(present, true);
}
try {
- RuntimePack runtimePack = persistentRuntimePack(active, ModdedGenerationMode.PERSISTENT_RESTORE);
- return new RestoringLevel(createLevel(server, serverAccess, requested, runtimePack), false);
+ RuntimePack runtimePack = persistentRuntimePack(active, generationMode);
+ return new PendingInjection(createLevel(server, serverAccess, requested, runtimePack), false);
} catch (Throwable e) {
throw injectionFailure(requested, e);
}
}
}
- static Handle completeRestore(NativeModdedServer server, RestoringLevel restoring, Engine engine) {
- if (restoring.present()) {
- return restoring.handle();
+ static Handle completeInjection(NativeModdedServer server, PendingInjection pending, Engine engine) {
+ if (pending.present()) {
+ return pending.handle();
}
NativeDimensionRuntime serverAccess = requireAccess();
synchronized (LOCK) {
try {
- return register(publishLevel(server, serverAccess, restoring.handle(), engine));
+ return register(publishLevel(server, serverAccess, pending.handle(), engine));
} catch (Throwable e) {
- throw injectionFailure(DimensionRequest.of(restoring.handle()), e);
+ throw injectionFailure(DimensionRequest.of(pending.handle()), e);
}
}
}
- static void abandonRestore(NativeModdedServer server, RestoringLevel restoring, Throwable failure) {
- if (restoring.present()) {
+ static void abandonInjection(NativeModdedServer server, PendingInjection pending, Engine engine, Throwable failure) {
+ if (engine != null) {
+ try {
+ ModdedWorldEngines.closeUnregistered(engine);
+ } catch (Throwable cleanupFailure) {
+ failure.addSuppressed(cleanupFailure);
+ ModdedIrisLog.error("Iris failed to close the abandoned engine for '{}'",
+ pending.handle().dimensionId(), cleanupFailure);
+ }
+ }
+ if (pending.present()) {
return;
}
NativeDimensionRuntime serverAccess = requireAccess();
- Handle handle = restoring.handle();
+ Handle handle = pending.handle();
synchronized (LOCK) {
rollbackInjection(server, serverAccess, handle.dimensionId(), handle.level(), handle.generator(), false, failure);
}
}
+ static PendingInjection beginCreation(
+ NativeModdedServer server,
+ ModdedDimensionRegistryStore.PersistentDimension dimension,
+ ModdedGenerationHistoryStorage.ActivePack active
+ ) {
+ synchronized (LOCK) {
+ if (ModdedDimensionRegistryStore.get(server, dimension.id()) != null) {
+ throw new IllegalStateException(alreadyExists(dimension.id()));
+ }
+ ModdedDimensionRegistryStore.put(server, dimension);
+ try {
+ return beginInjection(server, dimension, active, ModdedGenerationMode.PERSISTENT_CREATE);
+ } catch (RuntimeException | Error failure) {
+ forgetCreation(server, dimension.id(), failure);
+ throw failure;
+ }
+ }
+ }
+
+ static Handle completeCreation(NativeModdedServer server, PendingInjection pending, Engine engine) {
+ try {
+ return completeInjection(server, pending, engine);
+ } catch (RuntimeException | Error failure) {
+ forgetCreation(server, pending.handle().dimensionId(), failure);
+ throw failure;
+ }
+ }
+
+ static void abandonCreation(NativeModdedServer server, PendingInjection pending, Engine engine, Throwable failure) {
+ abandonInjection(server, pending, engine, failure);
+ forgetCreation(server, pending.handle().dimensionId(), failure);
+ }
+
+ static String alreadyExists(String dimensionId) {
+ return "Iris dimension '" + dimensionId
+ + "' already exists. Use the Iris world update command to stage a pack update.";
+ }
+
+ private static void forgetCreation(NativeModdedServer server, String dimensionId, Throwable failure) {
+ try {
+ ModdedDimensionRegistryStore.remove(server, dimensionId);
+ } catch (Throwable rollbackFailure) {
+ failure.addSuppressed(rollbackFailure);
+ }
+ }
+
private static Handle create(NativeModdedServer server, DimensionRequest requested, RuntimePackSource source) {
NativeDimensionRuntime serverAccess = requireAccess();
synchronized (LOCK) {
@@ -210,58 +266,13 @@ public final class ModdedDimensionManager {
return new IllegalStateException("Iris runtime dimension injection failed for " + requested.dimensionId(), failure);
}
- public static Handle createPersistent(NativeModdedServer server, String dimensionId, String pack, String packDimensionKey, long seed) {
- synchronized (LOCK) {
- ModdedDimensionRegistryStore.PersistentDimension previous =
- ModdedDimensionRegistryStore.get(server, dimensionId);
- if (previous != null) {
- throw new IllegalStateException("Iris dimension '" + dimensionId
- + "' already exists. Use the Iris world update command to stage a pack update.");
- }
- ModdedDimensionRegistryStore.put(server, new ModdedDimensionRegistryStore.PersistentDimension(
- dimensionId,
- pack,
- packDimensionKey,
- seed
- ));
- try {
- return create(
- server,
- new DimensionRequest(dimensionId, pack, packDimensionKey, seed),
- () -> persistentRuntimePack(
- ModdedGenerationHistoryStorage.createOrStage(
- server,
- dimensionId,
- pack,
- packDimensionKey,
- seed
- ),
- ModdedGenerationMode.PERSISTENT_CREATE
- )
- );
- } catch (Throwable e) {
- try {
- ModdedDimensionRegistryStore.remove(server, dimensionId);
- } catch (Throwable rollbackFailure) {
- e.addSuppressed(rollbackFailure);
- }
- if (e instanceof RuntimeException runtimeException) {
- throw runtimeException;
- }
- if (e instanceof Error fatalError) {
- throw fatalError;
- }
- throw new IllegalStateException("Iris runtime dimension injection failed for " + dimensionId, e);
- }
- }
- }
-
public static UpdateResult stagePersistentUpdate(
NativeModdedServer server,
String dimensionId,
String pack,
String packDimensionKey
) {
+ ModdedDimensionCreations.requireNotCreating(dimensionId);
synchronized (LOCK) {
ModdedDimensionRegistryStore.PersistentDimension previous =
ModdedDimensionRegistryStore.get(server, dimensionId);
@@ -324,6 +335,7 @@ public final class ModdedDimensionManager {
}
public static boolean removePersistent(NativeModdedServer server, String dimensionId, boolean wipeStorage) {
+ ModdedDimensionCreations.requireNotCreating(dimensionId);
ModdedDimensionRegistryStore.PersistentDimension previous =
ModdedDimensionRegistryStore.get(server, dimensionId);
ModdedDimensionRegistryStore.remove(server, dimensionId);
@@ -592,8 +604,8 @@ public final class ModdedDimensionManager {
public record Handle(String dimensionId, String pack, String packDimensionKey, long seed, NativeWorld level, IrisModdedChunkGenerator generator) {
}
- record RestoringLevel(Handle handle, boolean present) {
- RestoringLevel {
+ record PendingInjection(Handle handle, boolean present) {
+ PendingInjection {
handle = Objects.requireNonNull(handle, "handle");
}
}
diff --git a/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedDimensionRestore.java b/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedDimensionRestore.java
deleted file mode 100644
index ec1d9c7e2..000000000
--- a/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedDimensionRestore.java
+++ /dev/null
@@ -1,233 +0,0 @@
-/*
- * Iris is a World Generator for Minecraft Servers
- * Copyright (c) 2026 Arcane Arts (Volmit Software)
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see .
- */
-
-package art.arcane.iris.modded;
-
-import java.util.ArrayList;
-import java.util.List;
-import java.util.Objects;
-import java.util.concurrent.CompletableFuture;
-import java.util.concurrent.CompletionException;
-import java.util.concurrent.ExecutionException;
-import java.util.concurrent.ExecutorService;
-import java.util.concurrent.Executors;
-import java.util.concurrent.atomic.AtomicBoolean;
-
-final class ModdedDimensionRestore implements AutoCloseable {
- private static final int MINIMUM_WORKERS = 2;
-
- private final List> preparations;
- private final Steps steps;
- private final ExecutorService workers;
- private final AtomicBoolean finished = new AtomicBoolean();
-
- private ModdedDimensionRestore(List> preparations, Steps steps, ExecutorService workers) {
- this.preparations = preparations;
- this.steps = steps;
- this.workers = workers;
- }
-
- static
ModdedDimensionRestore
start(
- List dimensions,
- Steps steps
- ) {
- Steps
requiredSteps = Objects.requireNonNull(steps, "steps");
- ExecutorService workers = Executors.newFixedThreadPool(workerCount(dimensions.size()),
- Thread.ofPlatform().name("Iris Dimension Restore ", 1L).daemon(true).factory());
- List> preparations = new ArrayList<>(dimensions.size());
- for (ModdedDimensionRegistryStore.PersistentDimension dimension : dimensions) {
- preparations.add(new Preparation<>(dimension, CompletableFuture.supplyAsync(
- () -> timed(() -> requiredSteps.prepare(dimension)), workers)));
- }
- return new ModdedDimensionRestore<>(List.copyOf(preparations), requiredSteps, workers);
- }
-
- void awaitPreparations() {
- List>> futures = new ArrayList<>(preparations.size());
- for (Preparation preparation : preparations) {
- futures.add(preparation.future());
- }
- CompletableFuture.allOf(futures.toArray(CompletableFuture[]::new))
- .handle((Void ignored, Throwable failure) -> null)
- .join();
- }
-
- List finish() {
- if (!finished.compareAndSet(false, true)) {
- throw new IllegalStateException("Iris dimension restore already finished");
- }
- try {
- List> building = new ArrayList<>(preparations.size());
- Result[] results = new Result[preparations.size()];
- for (int index = 0; index < preparations.size(); index++) {
- Preparation preparation = preparations.get(index);
- Timed
prepared;
- try {
- prepared = join(preparation.future());
- } catch (Throwable failure) {
- results[index] = new Result(preparation.dimension(), failure, 0L, 0L);
- continue;
- }
- L level;
- try {
- level = steps.begin(preparation.dimension(), prepared.value());
- } catch (Throwable failure) {
- discard(prepared.value(), failure);
- results[index] = new Result(preparation.dimension(), failure, prepared.millis(), 0L);
- continue;
- }
- CompletableFuture> engine = CompletableFuture.supplyAsync(
- () -> timed(() -> steps.build(level, prepared.value())), workers);
- building.add(new Building<>(index, preparation.dimension(), prepared.millis(), level, engine));
- }
- for (Building pending : building) {
- results[pending.index()] = complete(pending);
- }
- return List.of(results);
- } finally {
- workers.shutdown();
- }
- }
-
- @Override
- public void close() {
- if (finished.compareAndSet(false, true)) {
- for (Preparation preparation : preparations) {
- preparation.future().whenComplete((Timed
prepared, Throwable failure) -> {
- if (prepared != null) {
- discard(prepared.value(), null);
- }
- });
- }
- }
- workers.shutdown();
- }
-
- private Result complete(Building pending) {
- Timed engine;
- try {
- engine = join(pending.engine());
- } catch (Throwable failure) {
- abandon(pending.level(), failure);
- return new Result(pending.dimension(), failure, pending.preparedMillis(), 0L);
- }
- try {
- steps.complete(pending.level(), engine.value());
- return new Result(pending.dimension(), null, pending.preparedMillis(), engine.millis());
- } catch (Throwable failure) {
- return new Result(pending.dimension(), failure, pending.preparedMillis(), engine.millis());
- }
- }
-
- private void abandon(L level, Throwable failure) {
- try {
- steps.abandon(level, failure);
- } catch (Throwable cleanupFailure) {
- failure.addSuppressed(cleanupFailure);
- }
- }
-
- private void discard(P prepared, Throwable failure) {
- try {
- steps.discard(prepared);
- } catch (Throwable cleanupFailure) {
- if (failure != null) {
- failure.addSuppressed(cleanupFailure);
- return;
- }
- ModdedIrisLog.error("Iris could not release an unused dimension restore preparation", cleanupFailure);
- }
- }
-
- private static int workerCount(int dimensions) {
- int processors = Runtime.getRuntime().availableProcessors();
- return Math.max(1, Math.min(dimensions, Math.max(MINIMUM_WORKERS, processors / 2)));
- }
-
- private static Timed timed(Step step) {
- long startedAt = System.nanoTime();
- try {
- T value = step.run();
- return new Timed<>(value, (System.nanoTime() - startedAt) / 1_000_000L);
- } catch (RuntimeException | Error failure) {
- throw failure;
- } catch (Exception failure) {
- throw new CompletionException(failure);
- }
- }
-
- private static T join(CompletableFuture future) throws Throwable {
- try {
- return future.get();
- } catch (ExecutionException failure) {
- Throwable cause = failure.getCause();
- if (cause instanceof CompletionException wrapped && wrapped.getCause() != null) {
- throw wrapped.getCause();
- }
- throw cause == null ? failure : cause;
- } catch (InterruptedException interrupted) {
- Thread.currentThread().interrupt();
- throw new IllegalStateException("Interrupted while restoring Iris dimensions", interrupted);
- }
- }
-
- interface Steps {
- P prepare(ModdedDimensionRegistryStore.PersistentDimension dimension) throws Exception;
-
- L begin(ModdedDimensionRegistryStore.PersistentDimension dimension, P prepared) throws Exception;
-
- E build(L level, P prepared) throws Exception;
-
- void complete(L level, E engine) throws Exception;
-
- void abandon(L level, Throwable failure);
-
- void discard(P prepared);
- }
-
- record Result(
- ModdedDimensionRegistryStore.PersistentDimension dimension,
- Throwable failure,
- long preparedMillis,
- long builtMillis
- ) {
- }
-
- @FunctionalInterface
- private interface Step {
- T run() throws Exception;
- }
-
- private record Timed(T value, long millis) {
- }
-
- private record Preparation(
- ModdedDimensionRegistryStore.PersistentDimension dimension,
- CompletableFuture> future
- ) {
- }
-
- private record Building(
- int index,
- ModdedDimensionRegistryStore.PersistentDimension dimension,
- long preparedMillis,
- L level,
- CompletableFuture> engine
- ) {
- }
-}
diff --git a/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedEngineBootstrap.java b/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedEngineBootstrap.java
index a559615af..30d00bac0 100644
--- a/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedEngineBootstrap.java
+++ b/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedEngineBootstrap.java
@@ -212,6 +212,7 @@ public final class ModdedEngineBootstrap {
stoppedPending = true;
Throwable failure = null;
failure = runStopStage(failure, "pack downloads", IrisModdedCommands::shutdownDownloads);
+ failure = runStopStage(failure, "dimension creations", ModdedDimensionCreations::cancelAll);
failure = runStopStage(failure, "world check", () -> ModdedWorldCheck.serverStopped(stoppingServer));
failure = runStopStage(failure, "protocol", ModdedProtocolHandler::stop);
failure = runStopStage(failure, "pregenerator", ModdedPregenJob::shutdown);
diff --git a/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedGenerationHistoryStorage.java b/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedGenerationHistoryStorage.java
index 4ce134029..3dfa86329 100644
--- a/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedGenerationHistoryStorage.java
+++ b/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedGenerationHistoryStorage.java
@@ -86,19 +86,47 @@ final class ModdedGenerationHistoryStorage {
Path dimensionRoot = dimensionRoot(server, levelKey);
try {
GenerationHistory history = openForRestore(dimensionRoot, selectedPack, selectedDimensionKey, worldSeed);
- GenerationAdmission.RuntimeLease lease = history.retainRuntime();
- try {
- history.prepareCurrentGenerator(IrisSettings.get().getGenerator().getGenerationTransitionWidthBlocks());
- return new PreparedHistory(resolveActive(history, installedPackSource(selectedPack)), lease);
- } catch (IOException | RuntimeException | Error failure) {
- lease.close();
- throw failure;
- }
+ return prepare(history, installedPackSource(selectedPack));
} catch (IOException failure) {
throw historyFailure(levelKey, failure);
}
}
+ static PreparedHistory prepareCreate(
+ NativeModdedServer server,
+ String levelKey,
+ String selectedPack,
+ String selectedDimensionKey,
+ long worldSeed
+ ) {
+ Path dimensionRoot = dimensionRoot(server, levelKey);
+ try {
+ Path source = selectedPackSource(selectedPack, selectedDimensionKey);
+ GenerationHistory history = createOrStage(
+ dimensionRoot,
+ source,
+ worldSeed,
+ captureCandidate(source, selectedDimensionKey),
+ ModdedGenerationHistoryStorage::requireRegistryDefinitions,
+ IrisSettings.get().getGenerator().getGenerationTransitionWidthBlocks()
+ );
+ return prepare(history, source);
+ } catch (IOException failure) {
+ throw historyFailure(levelKey, failure);
+ }
+ }
+
+ private static PreparedHistory prepare(GenerationHistory history, Path validatedSource) throws IOException {
+ GenerationAdmission.RuntimeLease lease = history.retainRuntime();
+ try {
+ history.prepareCurrentGenerator(IrisSettings.get().getGenerator().getGenerationTransitionWidthBlocks());
+ return new PreparedHistory(resolveActive(history, validatedSource), lease);
+ } catch (IOException | RuntimeException | Error failure) {
+ lease.close();
+ throw failure;
+ }
+ }
+
private static GenerationHistory openForRestore(
Path dimensionRoot,
String selectedPack,
diff --git a/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedStartup.java b/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedStartup.java
index a8d0bf13b..f5bf2c458 100644
--- a/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedStartup.java
+++ b/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedStartup.java
@@ -47,7 +47,7 @@ public final class ModdedStartup {
private static final AtomicBoolean PREPARED = new AtomicBoolean(false);
private static final AtomicBoolean STARTED = new AtomicBoolean(false);
private static final AtomicBoolean RESTORE_BEGUN = new AtomicBoolean(false);
- private static final AtomicReference> RESTORE =
+ private static final AtomicReference> RESTORE =
new AtomicReference<>();
private static final List DIMENSION_FAILURES = new CopyOnWriteArrayList<>();
@@ -55,7 +55,7 @@ public final class ModdedStartup {
}
public static void reset() {
- ModdedDimensionRestore restore =
+ ModdedDimensionInjection restore =
RESTORE.getAndSet(null);
if (restore != null) {
restore.close();
@@ -83,8 +83,8 @@ public final class ModdedStartup {
return;
}
long startedAt = System.currentTimeMillis();
- ModdedDimensionRestore restore =
- ModdedDimensionRestore.start(dimensions, new PersistentRestoreSteps(server));
+ ModdedDimensionInjection restore =
+ ModdedDimensionInjection.start(dimensions, new PersistentRestoreSteps(server));
RESTORE.set(restore);
restore.awaitPreparations();
ModdedIrisLog.info("Iris prepared {} persistent dimension(s) during world load in {}ms",
@@ -249,7 +249,7 @@ public final class ModdedStartup {
static void reinjectPersistentDimensions(NativeModdedServer server) {
prepareDimensionRestore(server);
- ModdedDimensionRestore restore =
+ ModdedDimensionInjection restore =
RESTORE.getAndSet(null);
if (restore == null) {
return;
@@ -257,9 +257,9 @@ public final class ModdedStartup {
finishDimensionRestore(restore);
}
- static void finishDimensionRestore(ModdedDimensionRestore, ?, ?> restore) {
+ static void finishDimensionRestore(ModdedDimensionInjection, ?, ?> restore) {
long startedAt = System.currentTimeMillis();
- List results;
+ List results;
try (restore) {
results = restore.finish();
}
@@ -267,7 +267,7 @@ public final class ModdedStartup {
int injected = 0;
int index = 0;
OutOfMemoryError outOfMemory = null;
- for (ModdedDimensionRestore.Result result : results) {
+ for (ModdedDimensionInjection.Result result : results) {
index++;
ModdedDimensionRegistryStore.PersistentDimension dimension = result.dimension();
Throwable failure = result.failure();
@@ -342,22 +342,23 @@ public final class ModdedStartup {
}
private record PersistentRestoreSteps(NativeModdedServer server)
- implements ModdedDimensionRestore.Steps {
+ implements ModdedDimensionInjection.Steps {
@Override
public ModdedWorldEngines.PreparedPack prepare(ModdedDimensionRegistryStore.PersistentDimension dimension) {
return ModdedWorldEngines.preparePersistent(server, dimension);
}
@Override
- public ModdedDimensionManager.RestoringLevel begin(
+ public ModdedDimensionManager.PendingInjection begin(
ModdedDimensionRegistryStore.PersistentDimension dimension,
ModdedWorldEngines.PreparedPack prepared
) {
- return ModdedDimensionManager.beginRestore(server, dimension, prepared.active());
+ return ModdedDimensionManager.beginInjection(
+ server, dimension, prepared.active(), ModdedGenerationMode.PERSISTENT_RESTORE);
}
@Override
- public Engine build(ModdedDimensionManager.RestoringLevel level, ModdedWorldEngines.PreparedPack prepared) {
+ public Engine build(ModdedDimensionManager.PendingInjection level, ModdedWorldEngines.PreparedPack prepared) {
if (level.present()) {
prepared.close();
return null;
@@ -366,13 +367,13 @@ public final class ModdedStartup {
}
@Override
- public void complete(ModdedDimensionManager.RestoringLevel level, Engine engine) {
- ModdedDimensionManager.completeRestore(server, level, engine);
+ public void complete(ModdedDimensionManager.PendingInjection level, Engine engine) {
+ ModdedDimensionManager.completeInjection(server, level, engine);
}
@Override
- public void abandon(ModdedDimensionManager.RestoringLevel level, Throwable failure) {
- ModdedDimensionManager.abandonRestore(server, level, failure);
+ public void abandon(ModdedDimensionManager.PendingInjection level, Engine engine, Throwable failure) {
+ ModdedDimensionManager.abandonInjection(server, level, engine, failure);
}
@Override
diff --git a/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedWorldEngines.java b/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedWorldEngines.java
index 4b2a7e375..fb739ddbf 100644
--- a/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedWorldEngines.java
+++ b/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedWorldEngines.java
@@ -134,13 +134,30 @@ public final class ModdedWorldEngines {
ModdedDimensionRegistryStore.PersistentDimension dimension
) {
ModdedEngineBootstrap.bind();
- ModdedGenerationHistoryStorage.PreparedHistory prepared = ModdedGenerationHistoryStorage.prepareRestore(
+ return prepared(ModdedGenerationHistoryStorage.prepareRestore(
server,
dimension.id(),
dimension.pack(),
dimension.dimension(),
dimension.seed()
- );
+ ));
+ }
+
+ static PreparedPack prepareCreated(
+ NativeModdedServer server,
+ ModdedDimensionRegistryStore.PersistentDimension dimension
+ ) {
+ ModdedEngineBootstrap.bind();
+ return prepared(ModdedGenerationHistoryStorage.prepareCreate(
+ server,
+ dimension.id(),
+ dimension.pack(),
+ dimension.dimension(),
+ dimension.seed()
+ ));
+ }
+
+ private static PreparedPack prepared(ModdedGenerationHistoryStorage.PreparedHistory prepared) {
try {
ModdedDimensionMetadata.warmConfiguredPack(prepared.active().packRoot().toFile(), prepared.active().dimensionKey());
return new PreparedPack(prepared.active(), prepared.lease(), loadPack(prepared.active()));
diff --git a/adapters/modded-common/src/main/java/art/arcane/iris/modded/command/ModdedWorldCommands.java b/adapters/modded-common/src/main/java/art/arcane/iris/modded/command/ModdedWorldCommands.java
index e1d9f758f..9f13e588c 100644
--- a/adapters/modded-common/src/main/java/art/arcane/iris/modded/command/ModdedWorldCommands.java
+++ b/adapters/modded-common/src/main/java/art/arcane/iris/modded/command/ModdedWorldCommands.java
@@ -26,6 +26,8 @@ import art.arcane.iris.generation.terrain.IrisDimension;
import art.arcane.iris.modded.IrisModdedChunkGenerator;
import art.arcane.volmlib.nativelib.minecraft26_2.modded.NativeWorldGenerators;
import art.arcane.iris.modded.MainWorldService;
+import art.arcane.iris.modded.MissingPackDimensionException;
+import art.arcane.iris.modded.ModdedDimensionCreations;
import art.arcane.iris.modded.ModdedDimensionManager;
import art.arcane.iris.modded.ModdedEngineBootstrap;
import art.arcane.iris.modded.ModdedModConfig;
@@ -47,12 +49,16 @@ import java.io.File;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
+import java.util.UUID;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.CompletionException;
import java.util.concurrent.ThreadLocalRandom;
import java.util.function.Predicate;
import art.arcane.iris.localization.IrisLanguage;
import art.arcane.iris.modded.localization.ModdedCommandMessages;
import art.arcane.iris.localization.RuntimeUiMessages;
+import art.arcane.volmlib.util.localization.LanguageAudience;
import art.arcane.volmlib.util.localization.MessageArgument;
public final class ModdedWorldCommands {
private static final Predicate GATE = NativeCommandRegistration.GAMEMASTERS;
@@ -193,24 +199,75 @@ public final class ModdedWorldCommands {
}
private static int enableInstalled(NativeCommandSource source, NativeModdedServer server, String dimensionId, String pack, String packDimension, long seed) {
- if (blockIfPackBroken(source, dimensionId, pack)) {
- return 0;
+ return create(source, server, new CreationRequest(dimensionId, pack, packDimension, seed), ModdedWorldCommands::reportWorldCreation);
+ }
+
+ private static int create(NativeCommandSource source, NativeModdedServer server, CreationRequest request, CreationReport report) {
+ CompletableFuture creation = ModdedDimensionCreations.create(
+ server, request.dimensionId(), request.pack(), request.packDimension(), request.seed());
+ if (creation.isDone()) {
+ Throwable failure = creation.isCompletedExceptionally() ? creation.exceptionNow() : null;
+ report.report(source, request, failure);
+ return failure == null ? 1 : 0;
}
- if (!loadPackDimension(source, pack, packDimension)) {
- return 0;
- }
- try {
- ModdedDimensionManager.createPersistent(server, dimensionId, pack, packDimension, seed);
- } catch (Throwable e) {
- ModdedIrisLog.error("Iris world injection failed for {} (pack={} dim={})", dimensionId, pack, packDimension, e);
- IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.MODDED_WORLD_COMMANDS_FAILED_INJECT_IRIS_WORLD, MessageArgument.untrusted("dimensionId", dimensionId), MessageArgument.untrusted("value", e.getClass().getSimpleName()), MessageArgument.trusted("errorMessage", IrisLanguage.errorDetail(e))));
- return 0;
- }
- IrisModdedCommands.ok(source, IrisLanguage.plain(ModdedCommandMessages.MODDED_WORLD_COMMANDS_CREATED_IRIS_WORLD_FROM_PACK_DIMENSION_SEED, MessageArgument.untrusted("dimensionId", dimensionId), MessageArgument.untrusted("pack", pack), MessageArgument.untrusted("packDimension", packDimension), MessageArgument.untrusted("seed", seed)));
- IrisModdedCommands.ok(source, IrisLanguage.plain(ModdedCommandMessages.MODDED_WORLD_COMMANDS_IT_IS_LIVE_NOW_RE_INJECTED_ON_EVERY_STARTUP_TELEPORT));
+ IrisModdedCommands.ok(source, IrisLanguage.plain(ModdedCommandMessages.MODDED_WORLD_COMMANDS_CREATING_IRIS_WORLD_IN_BACKGROUND, MessageArgument.untrusted("dimensionId", request.dimensionId()), MessageArgument.untrusted("pack", request.pack()), MessageArgument.untrusted("packDimension", request.packDimension()), MessageArgument.untrusted("seed", request.seed())));
+ UUID audience = source.playerId();
+ creation.whenComplete((Void ignored, Throwable failure) -> server.execute(
+ () -> LanguageAudience.run(audience, () -> report.report(source, request, failure))));
return 1;
}
+ private static void reportWorldCreation(NativeCommandSource source, CreationRequest request, Throwable failure) {
+ Throwable cause = creationCause(failure);
+ if (cause == null) {
+ IrisModdedCommands.ok(source, IrisLanguage.plain(ModdedCommandMessages.MODDED_WORLD_COMMANDS_CREATED_IRIS_WORLD_FROM_PACK_DIMENSION_SEED, MessageArgument.untrusted("dimensionId", request.dimensionId()), MessageArgument.untrusted("pack", request.pack()), MessageArgument.untrusted("packDimension", request.packDimension()), MessageArgument.untrusted("seed", request.seed())));
+ IrisModdedCommands.ok(source, IrisLanguage.plain(ModdedCommandMessages.MODDED_WORLD_COMMANDS_IT_IS_LIVE_NOW_RE_INJECTED_ON_EVERY_STARTUP_TELEPORT));
+ return;
+ }
+ if (reportPackRefusal(source, request, cause)) {
+ return;
+ }
+ ModdedIrisLog.error("Iris world injection failed for {} (pack={} dim={})", request.dimensionId(), request.pack(), request.packDimension(), cause);
+ IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.MODDED_WORLD_COMMANDS_FAILED_INJECT_IRIS_WORLD, MessageArgument.untrusted("dimensionId", request.dimensionId()), MessageArgument.untrusted("value", cause.getClass().getSimpleName()), MessageArgument.trusted("errorMessage", IrisLanguage.errorDetail(cause))));
+ }
+
+ private static void reportPrimaryCreation(NativeCommandSource source, CreationRequest request, Throwable failure) {
+ Throwable cause = creationCause(failure);
+ if (cause == null) {
+ ModdedModConfig.setPrimaryWorld(request.dimensionId());
+ ModdedPrimaryWorldRouter.clear();
+ IrisModdedCommands.ok(source, IrisLanguage.plain(ModdedCommandMessages.MODDED_WORLD_COMMANDS_IRIS_PRIMARY_WORLD_SET_PACK_DIMENSION_SEED, MessageArgument.untrusted("dimensionId", request.dimensionId()), MessageArgument.untrusted("pack", request.pack()), MessageArgument.untrusted("packDimension", request.packDimension()), MessageArgument.untrusted("seed", request.seed())));
+ IrisModdedCommands.ok(source, IrisLanguage.plain(ModdedCommandMessages.MODDED_WORLD_COMMANDS_VANILLA_OVERWORLD_GENERATOR_CANNOT_BE_HOT_SWAPPED_SO_THIS_DOES));
+ IrisModdedCommands.ok(source, IrisLanguage.plain(ModdedCommandMessages.MODDED_WORLD_COMMANDS_INSTEAD_IS_NOW_CONFIGURED_PRIMARY_WORLD_PLAYERS_VANILLA_OVERWORLD_ARE, MessageArgument.untrusted("dimensionId", request.dimensionId())));
+ return;
+ }
+ if (reportPackRefusal(source, request, cause)) {
+ return;
+ }
+ ModdedIrisLog.error("Iris primary world injection failed for {} (pack={} dim={})", request.dimensionId(), request.pack(), request.packDimension(), cause);
+ IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.MODDED_WORLD_COMMANDS_FAILED_INJECT_IRIS_PRIMARY_WORLD, MessageArgument.untrusted("value", cause.getClass().getSimpleName()), MessageArgument.trusted("errorMessage", IrisLanguage.errorDetail(cause))));
+ }
+
+ private static boolean reportPackRefusal(NativeCommandSource source, CreationRequest request, Throwable cause) {
+ if (cause instanceof BrokenPackException broken) {
+ reportBrokenPack(source, request.dimensionId(), request.pack(), broken);
+ return true;
+ }
+ if (cause instanceof MissingPackDimensionException missing) {
+ IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.MODDED_WORLD_COMMANDS_PACK_DOES_NOT_CONTAIN_DIMENSIONS_JSON, MessageArgument.untrusted("pack", missing.pack()), MessageArgument.untrusted("packDimension", missing.dimensionKey())));
+ return true;
+ }
+ return false;
+ }
+
+ private static Throwable creationCause(Throwable failure) {
+ Throwable cause = failure;
+ while (cause instanceof CompletionException && cause.getCause() != null) {
+ cause = cause.getCause();
+ }
+ return cause;
+ }
+
private static int update(NativeCommandSource source, String targetDimension, String packRaw) {
String dimensionId;
try {
@@ -265,25 +322,12 @@ public final class ModdedWorldCommands {
return 0;
}
String dimensionId = DEFAULT_NAMESPACE + ":primary";
- if (blockIfPackBroken(source, dimensionId, pack)) {
+ File packFolder = new File(ModdedPackCommands.packsRoot(), pack);
+ if (!packFolder.isDirectory()) {
+ IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.MODDED_WORLD_COMMANDS_PACK_WAS_NOT_FOUND_UNDER, MessageArgument.untrusted("pack", pack), MessageArgument.untrusted("value", ModdedPackCommands.packsRoot().getAbsolutePath())));
return 0;
}
- if (!loadPackDimension(source, pack, packDimension)) {
- return 0;
- }
- try {
- ModdedDimensionManager.createPersistent(server, dimensionId, pack, packDimension, seed);
- } catch (Throwable e) {
- ModdedIrisLog.error("Iris primary world injection failed for {} (pack={} dim={})", dimensionId, pack, packDimension, e);
- IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.MODDED_WORLD_COMMANDS_FAILED_INJECT_IRIS_PRIMARY_WORLD, MessageArgument.untrusted("value", e.getClass().getSimpleName()), MessageArgument.trusted("errorMessage", IrisLanguage.errorDetail(e))));
- return 0;
- }
- ModdedModConfig.setPrimaryWorld(dimensionId);
- ModdedPrimaryWorldRouter.clear();
- IrisModdedCommands.ok(source, IrisLanguage.plain(ModdedCommandMessages.MODDED_WORLD_COMMANDS_IRIS_PRIMARY_WORLD_SET_PACK_DIMENSION_SEED, MessageArgument.untrusted("dimensionId", dimensionId), MessageArgument.untrusted("pack", pack), MessageArgument.untrusted("packDimension", packDimension), MessageArgument.untrusted("seed", seed)));
- IrisModdedCommands.ok(source, IrisLanguage.plain(ModdedCommandMessages.MODDED_WORLD_COMMANDS_VANILLA_OVERWORLD_GENERATOR_CANNOT_BE_HOT_SWAPPED_SO_THIS_DOES));
- IrisModdedCommands.ok(source, IrisLanguage.plain(ModdedCommandMessages.MODDED_WORLD_COMMANDS_INSTEAD_IS_NOW_CONFIGURED_PRIMARY_WORLD_PLAYERS_VANILLA_OVERWORLD_ARE, MessageArgument.untrusted("dimensionId", dimensionId)));
- return 1;
+ return create(source, server, new CreationRequest(dimensionId, pack, packDimension, seed), ModdedWorldCommands::reportPrimaryCreation);
}
private static int clearMainWorld(NativeCommandSource source) {
@@ -368,15 +412,19 @@ public final class ModdedWorldCommands {
ModdedStartup.requirePackForWorldCreation(pack);
return false;
} catch (BrokenPackException e) {
- IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.MODDED_WORLD_COMMANDS_REFUSING_CREATE_WORLD_USING_PACK_BECAUSE_REQUIRED_VALIDATION_FAILED, MessageArgument.untrusted("dimensionId", dimensionId), MessageArgument.untrusted("pack", pack)));
- for (String reason : e.getReasons()) {
- IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.MODDED_WORLD_COMMANDS_MESSAGE, MessageArgument.untrusted("reason", reason)));
- }
- IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.MODDED_WORLD_COMMANDS_FIX_PACK_RUN_IRIS_PACK_VALIDATE_REVALIDATE, MessageArgument.untrusted("pack", pack)));
+ reportBrokenPack(source, dimensionId, pack, e);
return true;
}
}
+ private static void reportBrokenPack(NativeCommandSource source, String dimensionId, String pack, BrokenPackException broken) {
+ IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.MODDED_WORLD_COMMANDS_REFUSING_CREATE_WORLD_USING_PACK_BECAUSE_REQUIRED_VALIDATION_FAILED, MessageArgument.untrusted("dimensionId", dimensionId), MessageArgument.untrusted("pack", pack)));
+ for (String reason : broken.getReasons()) {
+ IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.MODDED_WORLD_COMMANDS_MESSAGE, MessageArgument.untrusted("reason", reason)));
+ }
+ IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.MODDED_WORLD_COMMANDS_FIX_PACK_RUN_IRIS_PACK_VALIDATE_REVALIDATE, MessageArgument.untrusted("pack", pack)));
+ }
+
private static boolean loadPackDimension(NativeCommandSource source, String pack, String packDimension) {
File packFolder = new File(ModdedPackCommands.packsRoot(), pack);
if (!packFolder.isDirectory()) {
@@ -532,4 +580,12 @@ public final class ModdedWorldCommands {
}
return namespace + ":" + path;
}
+
+ @FunctionalInterface
+ private interface CreationReport {
+ void report(NativeCommandSource source, CreationRequest request, Throwable failure);
+ }
+
+ private record CreationRequest(String dimensionId, String pack, String packDimension, long seed) {
+ }
}
diff --git a/adapters/modded-common/src/main/java/art/arcane/iris/modded/localization/ModdedCommandMessages.java b/adapters/modded-common/src/main/java/art/arcane/iris/modded/localization/ModdedCommandMessages.java
index f6b6779cb..c798ffbf3 100644
--- a/adapters/modded-common/src/main/java/art/arcane/iris/modded/localization/ModdedCommandMessages.java
+++ b/adapters/modded-common/src/main/java/art/arcane/iris/modded/localization/ModdedCommandMessages.java
@@ -1124,6 +1124,10 @@ public final class ModdedCommandMessages {
"iris.modded.moddedworldcommands.failed_inject_iris_world",
"Failed to inject Iris world '" + "{dimensionId}" + "': " + "{value}" + "{errorMessage}"
);
+ public static final TextKey MODDED_WORLD_COMMANDS_CREATING_IRIS_WORLD_IN_BACKGROUND = TextKey.of(
+ "iris.modded.moddedworldcommands.creating_iris_world_in_background",
+ "Creating Iris world " + "{dimensionId}" + " from pack '" + "{pack}" + "' dimension '" + "{packDimension}" + "' (seed " + "{seed}" + ") in the background. You will be told when it is live."
+ );
public static final TextKey MODDED_WORLD_COMMANDS_CREATED_IRIS_WORLD_FROM_PACK_DIMENSION_SEED = TextKey.of(
"iris.modded.moddedworldcommands.created_iris_world_from_pack_dimension_seed",
"Created Iris world " + "{dimensionId}" + " from pack '" + "{pack}" + "' dimension '" + "{packDimension}" + "' (seed " + "{seed}" + ")."
@@ -1541,6 +1545,7 @@ public final class ModdedCommandMessages {
MODDED_STUDIO_COMMANDS_REGION_SAMPLING_FAILED,
MODDED_WORLD_COMMANDS_REQUIRED_PACK_IS_NOT_INSTALLED_INSTALL_THEN_RESTART,
MODDED_WORLD_COMMANDS_FAILED_INJECT_IRIS_WORLD,
+ MODDED_WORLD_COMMANDS_CREATING_IRIS_WORLD_IN_BACKGROUND,
MODDED_WORLD_COMMANDS_CREATED_IRIS_WORLD_FROM_PACK_DIMENSION_SEED,
MODDED_WORLD_COMMANDS_IT_IS_LIVE_NOW_RE_INJECTED_ON_EVERY_STARTUP_TELEPORT,
MODDED_WORLD_COMMANDS_FAILED_INJECT_IRIS_PRIMARY_WORLD,
diff --git a/adapters/modded-common/src/test/java/art/arcane/iris/modded/ModdedDimensionRestoreTest.java b/adapters/modded-common/src/test/java/art/arcane/iris/modded/ModdedDimensionInjectionTest.java
similarity index 54%
rename from adapters/modded-common/src/test/java/art/arcane/iris/modded/ModdedDimensionRestoreTest.java
rename to adapters/modded-common/src/test/java/art/arcane/iris/modded/ModdedDimensionInjectionTest.java
index 55755744f..c568b9833 100644
--- a/adapters/modded-common/src/test/java/art/arcane/iris/modded/ModdedDimensionRestoreTest.java
+++ b/adapters/modded-common/src/test/java/art/arcane/iris/modded/ModdedDimensionInjectionTest.java
@@ -7,7 +7,11 @@ import java.util.Collections;
import java.util.List;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.CancellationException;
import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
@@ -17,7 +21,7 @@ import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertTrue;
-public class ModdedDimensionRestoreTest {
+public class ModdedDimensionInjectionTest {
private static final long STEP_TIMEOUT_SECONDS = 10L;
@Test
@@ -26,13 +30,13 @@ public class ModdedDimensionRestoreTest {
RecordingSteps steps = new RecordingSteps(dimensions.size());
steps.prepareBarrier = new CountDownLatch(dimensions.size());
- List results;
- try (ModdedDimensionRestore restore = ModdedDimensionRestore.start(dimensions, steps)) {
+ List results;
+ try (ModdedDimensionInjection restore = ModdedDimensionInjection.start(dimensions, steps)) {
results = restore.finish();
}
assertEquals(dimensions.size(), results.size());
- for (ModdedDimensionRestore.Result result : results) {
+ for (ModdedDimensionInjection.Result result : results) {
assertNull(result.failure());
}
assertFalse(steps.prepareThreads.contains(Thread.currentThread()));
@@ -44,7 +48,7 @@ public class ModdedDimensionRestoreTest {
RecordingSteps steps = new RecordingSteps(dimensions.size());
steps.buildBarrier = new CountDownLatch(dimensions.size());
- try (ModdedDimensionRestore restore = ModdedDimensionRestore.start(dimensions, steps)) {
+ try (ModdedDimensionInjection restore = ModdedDimensionInjection.start(dimensions, steps)) {
restore.finish();
}
@@ -61,8 +65,8 @@ public class ModdedDimensionRestoreTest {
steps.prepareFailure = failure;
steps.failingIndex = 1;
- List results;
- try (ModdedDimensionRestore restore = ModdedDimensionRestore.start(dimensions, steps)) {
+ List results;
+ try (ModdedDimensionInjection restore = ModdedDimensionInjection.start(dimensions, steps)) {
results = restore.finish();
}
@@ -82,14 +86,14 @@ public class ModdedDimensionRestoreTest {
steps.buildFailure = failure;
steps.failingIndex = 0;
- List results;
- try (ModdedDimensionRestore restore = ModdedDimensionRestore.start(dimensions, steps)) {
+ List results;
+ try (ModdedDimensionInjection restore = ModdedDimensionInjection.start(dimensions, steps)) {
results = restore.finish();
}
assertSame(failure, results.get(0).failure());
assertNull(results.get(1).failure());
- assertEquals(List.of("level-0"), steps.abandoned);
+ assertEquals(List.of("level-0:null"), steps.abandoned);
assertEquals(List.of("level-1:engine-1"), steps.completed);
}
@@ -101,8 +105,8 @@ public class ModdedDimensionRestoreTest {
steps.beginFailure = failure;
steps.failingIndex = 1;
- List results;
- try (ModdedDimensionRestore restore = ModdedDimensionRestore.start(dimensions, steps)) {
+ List results;
+ try (ModdedDimensionInjection restore = ModdedDimensionInjection.start(dimensions, steps)) {
results = restore.finish();
}
@@ -126,7 +130,7 @@ public class ModdedDimensionRestoreTest {
release.countDown();
});
- try (ModdedDimensionRestore restore = ModdedDimensionRestore.start(dimensions, steps)) {
+ try (ModdedDimensionInjection restore = ModdedDimensionInjection.start(dimensions, steps)) {
restore.awaitPreparations();
assertEquals(dimensions.size(), steps.prepared.get());
@@ -143,7 +147,7 @@ public class ModdedDimensionRestoreTest {
List dimensions = dimensions(3);
RecordingSteps steps = new RecordingSteps(dimensions.size());
- ModdedDimensionRestore restore = ModdedDimensionRestore.start(dimensions, steps);
+ ModdedDimensionInjection restore = ModdedDimensionInjection.start(dimensions, steps);
restore.close();
assertTrue(steps.discardedAll.await(STEP_TIMEOUT_SECONDS, TimeUnit.SECONDS));
@@ -151,6 +155,143 @@ public class ModdedDimensionRestoreTest {
assertEquals(Set.of("prepared-0", "prepared-1", "prepared-2"), Set.copyOf(steps.discarded));
}
+ @Test
+ public void asyncInjectionReturnsAtOnceAndMutatesLevelsOnlyOnTheServerExecutor() throws Exception {
+ ModdedDimensionRegistryStore.PersistentDimension dimension = dimensions(1).getFirst();
+ RecordingSteps steps = new RecordingSteps(1);
+ CountDownLatch release = new CountDownLatch(1);
+ steps.prepareGate = release;
+ ExecutorService server = Executors.newSingleThreadExecutor(Thread.ofPlatform().name("test-server").factory());
+ try {
+ Thread serverThread = server.submit(Thread::currentThread).get();
+ ModdedDimensionInjection.Async async =
+ ModdedDimensionInjection.async(dimension, steps, server);
+ async.start();
+
+ assertFalse(async.result().isDone());
+ release.countDown();
+ ModdedDimensionInjection.Result result = async.result().get(STEP_TIMEOUT_SECONDS, TimeUnit.SECONDS);
+
+ assertNull(result.failure());
+ assertSame(dimension, result.dimension());
+ assertEquals(List.of("level-0:engine-0"), steps.completed);
+ assertEquals(Set.of(serverThread), steps.serverThreads);
+ assertFalse(steps.prepareThreads.contains(serverThread));
+ assertFalse(steps.prepareThreads.contains(Thread.currentThread()));
+ assertFalse(steps.buildThreads.contains(serverThread));
+ } finally {
+ server.shutdownNow();
+ }
+ }
+
+ @Test
+ public void asyncPreparationFailureCompletesWithoutCreatingALevel() throws Exception {
+ ModdedDimensionRegistryStore.PersistentDimension dimension = dimensions(1).getFirst();
+ RecordingSteps steps = new RecordingSteps(1);
+ IllegalStateException failure = new IllegalStateException("pack refused");
+ steps.prepareFailure = failure;
+ steps.failingIndex = 0;
+ ExecutorService server = Executors.newSingleThreadExecutor();
+ try {
+ ModdedDimensionInjection.Async async =
+ ModdedDimensionInjection.async(dimension, steps, server);
+ async.start();
+
+ ModdedDimensionInjection.Result result = async.result().get(STEP_TIMEOUT_SECONDS, TimeUnit.SECONDS);
+
+ assertSame(failure, result.failure());
+ assertTrue(steps.begun.isEmpty());
+ assertTrue(steps.completed.isEmpty());
+ } finally {
+ server.shutdownNow();
+ }
+ }
+
+ @Test
+ public void asyncEngineFailureAbandonsTheCreatedLevel() throws Exception {
+ ModdedDimensionRegistryStore.PersistentDimension dimension = dimensions(1).getFirst();
+ RecordingSteps steps = new RecordingSteps(1);
+ IllegalStateException failure = new IllegalStateException("engine refused");
+ steps.buildFailure = failure;
+ steps.failingIndex = 0;
+ ExecutorService server = Executors.newSingleThreadExecutor();
+ try {
+ ModdedDimensionInjection.Async async =
+ ModdedDimensionInjection.async(dimension, steps, server);
+ async.start();
+
+ ModdedDimensionInjection.Result result = async.result().get(STEP_TIMEOUT_SECONDS, TimeUnit.SECONDS);
+
+ assertSame(failure, result.failure());
+ assertEquals(List.of("level-0:null"), steps.abandoned);
+ assertTrue(steps.completed.isEmpty());
+ } finally {
+ server.shutdownNow();
+ }
+ }
+
+ @Test
+ public void cancellingWhilePreparingDiscardsThePreparationWithoutCreatingALevel() throws Exception {
+ ModdedDimensionRegistryStore.PersistentDimension dimension = dimensions(1).getFirst();
+ RecordingSteps steps = new RecordingSteps(1);
+ CountDownLatch release = new CountDownLatch(1);
+ steps.prepareGate = release;
+ ExecutorService server = Executors.newSingleThreadExecutor();
+ try {
+ ModdedDimensionInjection.Async async =
+ ModdedDimensionInjection.async(dimension, steps, server);
+ async.start();
+
+ server.submit(async::cancel).get(STEP_TIMEOUT_SECONDS, TimeUnit.SECONDS);
+ release.countDown();
+ ModdedDimensionInjection.Result result = async.result().get(STEP_TIMEOUT_SECONDS, TimeUnit.SECONDS);
+
+ assertTrue(result.failure() instanceof CancellationException);
+ assertTrue(steps.discardedAll.await(STEP_TIMEOUT_SECONDS, TimeUnit.SECONDS));
+ assertEquals(List.of("prepared-0"), steps.discarded);
+ server.submit(() -> null).get(STEP_TIMEOUT_SECONDS, TimeUnit.SECONDS);
+ assertTrue(steps.begun.isEmpty());
+ } finally {
+ server.shutdownNow();
+ }
+ }
+
+ @Test
+ public void cancellingWhileBuildingWaitsForTheEngineAndAbandonsTheLevelWithIt() throws Exception {
+ ModdedDimensionRegistryStore.PersistentDimension dimension = dimensions(1).getFirst();
+ RecordingSteps steps = new RecordingSteps(1);
+ CountDownLatch building = new CountDownLatch(1);
+ CountDownLatch release = new CountDownLatch(1);
+ steps.buildStarted = building;
+ steps.buildGate = release;
+ ExecutorService server = Executors.newSingleThreadExecutor();
+ try {
+ ModdedDimensionInjection.Async async =
+ ModdedDimensionInjection.async(dimension, steps, server);
+ async.start();
+ assertTrue(building.await(STEP_TIMEOUT_SECONDS, TimeUnit.SECONDS));
+
+ Future> cancelled = server.submit(async::cancel);
+ Thread.ofPlatform().daemon(true).start(() -> {
+ try {
+ Thread.sleep(200L);
+ } catch (InterruptedException interrupted) {
+ Thread.currentThread().interrupt();
+ }
+ release.countDown();
+ });
+ cancelled.get(STEP_TIMEOUT_SECONDS, TimeUnit.SECONDS);
+ ModdedDimensionInjection.Result result = async.result().get(STEP_TIMEOUT_SECONDS, TimeUnit.SECONDS);
+ server.submit(() -> null).get(STEP_TIMEOUT_SECONDS, TimeUnit.SECONDS);
+
+ assertTrue(result.failure() instanceof CancellationException);
+ assertEquals(List.of("level-0:engine-0"), steps.abandoned);
+ assertTrue(steps.completed.isEmpty());
+ } finally {
+ server.shutdownNow();
+ }
+ }
+
private static List dimensions(int count) {
List dimensions = new ArrayList<>(count);
for (int index = 0; index < count; index++) {
@@ -189,15 +330,15 @@ public class ModdedDimensionRestoreTest {
}
try {
if (!gate.await(STEP_TIMEOUT_SECONDS, TimeUnit.SECONDS)) {
- throw new IllegalStateException("Preparation gate never opened");
+ throw new IllegalStateException("Restore step gate never opened");
}
} catch (InterruptedException interrupted) {
Thread.currentThread().interrupt();
- throw new IllegalStateException("Interrupted while waiting for the preparation gate", interrupted);
+ throw new IllegalStateException("Interrupted while waiting for a restore step gate", interrupted);
}
}
- private static final class RecordingSteps implements ModdedDimensionRestore.Steps {
+ private static final class RecordingSteps implements ModdedDimensionInjection.Steps {
private final Set prepareThreads = ConcurrentHashMap.newKeySet();
private final Set buildThreads = ConcurrentHashMap.newKeySet();
private final Set serverThreads = ConcurrentHashMap.newKeySet();
@@ -210,6 +351,8 @@ public class ModdedDimensionRestoreTest {
private CountDownLatch prepareGate;
private CountDownLatch prepareBarrier;
private CountDownLatch buildBarrier;
+ private CountDownLatch buildStarted;
+ private CountDownLatch buildGate;
private RuntimeException prepareFailure;
private RuntimeException beginFailure;
private RuntimeException buildFailure;
@@ -248,6 +391,10 @@ public class ModdedDimensionRestoreTest {
public String build(String level, String prepared) {
buildThreads.add(Thread.currentThread());
await(buildBarrier);
+ if (buildStarted != null) {
+ buildStarted.countDown();
+ }
+ awaitGate(buildGate);
int index = index(level);
if (buildFailure != null && index == failingIndex) {
throw buildFailure;
@@ -262,9 +409,9 @@ public class ModdedDimensionRestoreTest {
}
@Override
- public void abandon(String level, Throwable failure) {
+ public void abandon(String level, String engine, Throwable failure) {
serverThreads.add(Thread.currentThread());
- abandoned.add(level);
+ abandoned.add(level + ":" + engine);
}
@Override
diff --git a/adapters/modded-common/src/test/java/art/arcane/iris/modded/ModdedStartupReinjectFailureTest.java b/adapters/modded-common/src/test/java/art/arcane/iris/modded/ModdedStartupReinjectFailureTest.java
index 6810a25c3..ab5004cf9 100644
--- a/adapters/modded-common/src/test/java/art/arcane/iris/modded/ModdedStartupReinjectFailureTest.java
+++ b/adapters/modded-common/src/test/java/art/arcane/iris/modded/ModdedStartupReinjectFailureTest.java
@@ -36,7 +36,7 @@ public class ModdedStartupReinjectFailureTest {
IllegalStateException failure = new IllegalStateException("Iris generation history is unusable for 'iris:moon'");
try (MockedStatic log = mockStatic(ModdedIrisLog.class)) {
- ModdedStartup.finishDimensionRestore(ModdedDimensionRestore.start(List.of(moon), new FailingSteps(failure)));
+ ModdedStartup.finishDimensionRestore(ModdedDimensionInjection.start(List.of(moon), new FailingSteps(failure)));
log.verify(() -> ModdedIrisLog.error(argThat((String line) -> line != null && line.contains("iris:moon")
&& line.startsWith("Iris refused"))), times(1));
@@ -66,7 +66,7 @@ public class ModdedStartupReinjectFailureTest {
verify(laterOperator, never()).sendMessage(any());
}
- private record FailingSteps(RuntimeException failure) implements ModdedDimensionRestore.Steps {
+ private record FailingSteps(RuntimeException failure) implements ModdedDimensionInjection.Steps {
@Override
public String prepare(ModdedDimensionRegistryStore.PersistentDimension dimension) {
throw failure;
@@ -88,7 +88,7 @@ public class ModdedStartupReinjectFailureTest {
}
@Override
- public void abandon(String level, Throwable failure) {
+ public void abandon(String level, String engine, Throwable failure) {
throw new AssertionError("A failed preparation has no level to abandon");
}
diff --git a/adapters/modded-common/src/test/java/art/arcane/iris/modded/command/ModdedWorldCreateCommandTest.java b/adapters/modded-common/src/test/java/art/arcane/iris/modded/command/ModdedWorldCreateCommandTest.java
new file mode 100644
index 000000000..69af628b2
--- /dev/null
+++ b/adapters/modded-common/src/test/java/art/arcane/iris/modded/command/ModdedWorldCreateCommandTest.java
@@ -0,0 +1,154 @@
+package art.arcane.iris.modded.command;
+
+import art.arcane.iris.modded.MissingPackDimensionException;
+import art.arcane.iris.modded.ModdedDimensionCreations;
+import art.arcane.iris.pack.BrokenPackException;
+import art.arcane.volmlib.nativelib.minecraft26_2.modded.NativeCommandSource;
+import art.arcane.volmlib.nativelib.minecraft26_2.modded.NativeCommandText;
+import art.arcane.volmlib.nativelib.minecraft26_2.modded.NativeModdedServer;
+import net.minecraft.SharedConstants;
+import net.minecraft.server.Bootstrap;
+import org.junit.BeforeClass;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+import org.mockito.MockedStatic;
+import org.mockito.invocation.InvocationOnMock;
+
+import java.io.File;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.CompletableFuture;
+import java.util.function.Supplier;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyBoolean;
+import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.mockStatic;
+import static org.mockito.Mockito.when;
+
+public class ModdedWorldCreateCommandTest {
+ @Rule
+ public TemporaryFolder temporaryFolder = new TemporaryFolder();
+
+ @BeforeClass
+ public static void bootstrap() {
+ SharedConstants.tryDetectVersion();
+ Bootstrap.bootStrap();
+ }
+
+ @Test
+ public void createAcknowledgesAtOnceAndReportsTheLiveWorldFromALaterServerTask() throws Exception {
+ CompletableFuture creation = new CompletableFuture<>();
+ Harness harness = harness();
+
+ try (MockedStatic packs = mockStatic(ModdedPackCommands.class);
+ MockedStatic creations = mockStatic(ModdedDimensionCreations.class)) {
+ packs.when(ModdedPackCommands::packsRoot).thenReturn(harness.packs());
+ creations.when(() -> ModdedDimensionCreations.create(harness.server(), "irisworldgen:moon", "overworld", "overworld", 7L))
+ .thenReturn(creation);
+
+ assertEquals(1, ModdedWorldCommands.createWorld(harness.source(), "moon", "overworld", 7L));
+ assertTrue(String.join("\n", harness.success()), harness.success().stream()
+ .anyMatch((String line) -> line.contains("irisworldgen:moon") && !line.startsWith("Created")));
+ assertFalse(harness.success().stream().anyMatch((String line) -> line.startsWith("Created Iris world")));
+
+ creation.complete(null);
+ harness.runServerTasks();
+ }
+
+ assertTrue(String.join("\n", harness.success()), harness.success().stream()
+ .anyMatch((String line) -> line.startsWith("Created Iris world irisworldgen:moon from pack 'overworld'")));
+ assertTrue(harness.failure().isEmpty());
+ }
+
+ @Test
+ public void creationFailureIsReportedAsAFailedInjection() throws Exception {
+ Harness harness = harness();
+ List failures = run(harness, new IllegalStateException("Iris generation history is unusable for 'irisworldgen:moon'."));
+
+ assertTrue(String.join("\n", failures), failures.stream().anyMatch((String line) ->
+ line.startsWith("Failed to inject Iris world 'irisworldgen:moon'") && line.contains("generation history is unusable")));
+ }
+
+ @Test
+ public void brokenPackIsReportedWithItsValidationReasons() throws Exception {
+ Harness harness = harness();
+ List failures = run(harness, new BrokenPackException("overworld", List.of("biomes/plains.json is invalid")));
+
+ assertTrue(String.join("\n", failures), failures.stream().anyMatch((String line) -> line.contains("irisworldgen:moon")
+ && line.contains("overworld")));
+ assertTrue(String.join("\n", failures), failures.stream().anyMatch((String line) -> line.contains("biomes/plains.json is invalid")));
+ }
+
+ @Test
+ public void missingPackDimensionIsReportedAsAMissingDimensionFile() throws Exception {
+ Harness harness = harness();
+ List failures = run(harness, new MissingPackDimensionException("overworld", "overworld"));
+
+ assertTrue(String.join("\n", failures), failures.stream().anyMatch((String line) ->
+ line.contains("dimensions/overworld.json")));
+ }
+
+ private List run(Harness harness, RuntimeException failure) throws Exception {
+ CompletableFuture creation = new CompletableFuture<>();
+ try (MockedStatic packs = mockStatic(ModdedPackCommands.class);
+ MockedStatic creations = mockStatic(ModdedDimensionCreations.class)) {
+ packs.when(ModdedPackCommands::packsRoot).thenReturn(harness.packs());
+ creations.when(() -> ModdedDimensionCreations.create(harness.server(), "irisworldgen:moon", "overworld", "overworld", 7L))
+ .thenReturn(creation);
+
+ assertEquals(1, ModdedWorldCommands.createWorld(harness.source(), "moon", "overworld", 7L));
+ assertTrue(harness.failure().isEmpty());
+ creation.completeExceptionally(failure);
+ harness.runServerTasks();
+ }
+ return harness.failure();
+ }
+
+ private Harness harness() throws Exception {
+ File packs = temporaryFolder.newFolder("packs");
+ assertTrue(new File(packs, "overworld").mkdirs());
+ NativeModdedServer server = mock(NativeModdedServer.class);
+ List serverTasks = new ArrayList<>();
+ doAnswer((InvocationOnMock invocation) -> {
+ serverTasks.add(invocation.getArgument(0));
+ return null;
+ }).when(server).execute(any());
+ List success = new ArrayList<>();
+ List failure = new ArrayList<>();
+ NativeCommandSource source = mock(NativeCommandSource.class);
+ when(source.server()).thenReturn(server);
+ when(source.isGameMaster()).thenReturn(true);
+ doAnswer((InvocationOnMock invocation) -> {
+ Supplier text = invocation.getArgument(0);
+ success.add(text.get().getString());
+ return null;
+ }).when(source).sendSuccess(any(), anyBoolean());
+ doAnswer((InvocationOnMock invocation) -> {
+ NativeCommandText text = invocation.getArgument(0);
+ failure.add(text.getString());
+ return null;
+ }).when(source).sendFailure(any());
+ return new Harness(packs, server, source, serverTasks, success, failure);
+ }
+
+ private record Harness(
+ File packs,
+ NativeModdedServer server,
+ NativeCommandSource source,
+ List serverTasks,
+ List success,
+ List failure
+ ) {
+ void runServerTasks() {
+ while (!serverTasks.isEmpty()) {
+ serverTasks.removeFirst().run();
+ }
+ }
+ }
+}
diff --git a/core/src/main/resources/languages/de_DE.toml b/core/src/main/resources/languages/de_DE.toml
index f59feba8e..32fce3ed2 100644
--- a/core/src/main/resources/languages/de_DE.toml
+++ b/core/src/main/resources/languages/de_DE.toml
@@ -1077,6 +1077,7 @@ opening_image_map_studio_on_server_display = "Opening the Image Map Studio for {
[iris.modded.moddedworldcommands]
required_pack_is_not_installed_install_then_restart = "Das erforderliche Pack '{pack}' ist nicht installiert. Installiere es mit /iris download und starte den Server anschließend neu."
failed_inject_iris_world = "Iris-Welt '{dimensionId}' konnte nicht eingebunden werden: {value}{errorMessage}"
+creating_iris_world_in_background = "Iris-Welt {dimensionId} wird aus Pack '{pack}', Dimension '{packDimension}' (Seed {seed}) im Hintergrund erstellt. Du wirst benachrichtigt, sobald sie bereit ist."
created_iris_world_from_pack_dimension_seed = "Iris-Welt {dimensionId} aus Pack '{pack}', Dimension '{packDimension}', erstellt (Seed {seed})."
it_is_live_now_re_injected_on_every_startup_teleport = "Sie ist jetzt aktiv und wird bei jedem Start erneut eingebunden. Teleportiere dich über /iris world status oder ein Portal hinein; ein Neustart ist nicht erforderlich."
failed_inject_iris_primary_world = "Primäre Iris-Welt konnte nicht eingebunden werden: {value}{errorMessage}"
diff --git a/core/src/main/resources/languages/es_ES.toml b/core/src/main/resources/languages/es_ES.toml
index 49d86cb38..57c9e6398 100644
--- a/core/src/main/resources/languages/es_ES.toml
+++ b/core/src/main/resources/languages/es_ES.toml
@@ -1077,6 +1077,7 @@ opening_image_map_studio_on_server_display = "Opening the Image Map Studio for {
[iris.modded.moddedworldcommands]
required_pack_is_not_installed_install_then_restart = "El pack requerido '{pack}' no está instalado. Instálalo con /iris download y después reinicia el servidor."
failed_inject_iris_world = "No se pudo inyectar el mundo de Iris '{dimensionId}': {value}{errorMessage}"
+creating_iris_world_in_background = "Creando el mundo de Iris {dimensionId} desde el pack '{pack}', dimensión '{packDimension}' (semilla {seed}) en segundo plano. Se te avisará cuando esté listo."
created_iris_world_from_pack_dimension_seed = "Se creó el mundo de Iris {dimensionId} desde el pack '{pack}', dimensión '{packDimension}' (semilla {seed})."
it_is_live_now_re_injected_on_every_startup_teleport = "Ya está activo y se vuelve a inyectar en cada inicio. Teletranspórtate con /iris world status o mediante un portal; no es necesario reiniciar."
failed_inject_iris_primary_world = "No se pudo inyectar el mundo principal de Iris: {value}{errorMessage}"
diff --git a/core/src/main/resources/languages/fi_FI.toml b/core/src/main/resources/languages/fi_FI.toml
index 7331c9545..b046ca2ee 100644
--- a/core/src/main/resources/languages/fi_FI.toml
+++ b/core/src/main/resources/languages/fi_FI.toml
@@ -1082,6 +1082,7 @@ opening_image_map_studio_on_server_display = "Opening the Image Map Studio for {
[iris.modded.moddedworldcommands]
required_pack_is_not_installed_install_then_restart = "Vaadittua pakettia '{pack}' ei ole asennettu. Asenna se komennolla /iris download ja käynnistä palvelin sitten uudelleen."
failed_inject_iris_world = "Injektio epäonnistui Iris maailma{dimensionId}': {value}{errorMessage}"
+creating_iris_world_in_background = "Luodaan Iris-maailmaa {dimensionId} pakkauksesta '{pack}', ulottuvuus '{packDimension}' (siemen {seed}), taustalla. Saat ilmoituksen, kun se on valmis."
created_iris_world_from_pack_dimension_seed = "Luotu Iris maailma {dimensionId} pakkauksesta \"{pack}' ulottuvuudella'{packDimension}\" (siemen {seed})."
it_is_live_now_re_injected_on_every_startup_teleport = "Se on nyt suorassa lähetyksessä. Teleportata /iris world status tai portaali; uudelleenkäynnistystä ei tarvita."
failed_inject_iris_primary_world = "Injektio epäonnistui Iris ensisijainen maailma: {value}{errorMessage}"
diff --git a/core/src/main/resources/languages/fr_FR.toml b/core/src/main/resources/languages/fr_FR.toml
index 3a9d49a4c..ac02f4ed2 100644
--- a/core/src/main/resources/languages/fr_FR.toml
+++ b/core/src/main/resources/languages/fr_FR.toml
@@ -1077,6 +1077,7 @@ opening_image_map_studio_on_server_display = "Opening the Image Map Studio for {
[iris.modded.moddedworldcommands]
required_pack_is_not_installed_install_then_restart = "Le pack requis '{pack}' n’est pas installé. Installez-le avec /iris download, puis redémarrez le serveur."
failed_inject_iris_world = "Impossible d'injecter le monde Iris '{dimensionId}' : {value}{errorMessage}"
+creating_iris_world_in_background = "Création du monde Iris {dimensionId} depuis le pack '{pack}', dimension '{packDimension}' (graine {seed}) en arrière-plan. Vous serez prévenu dès qu'il sera prêt."
created_iris_world_from_pack_dimension_seed = "Monde Iris {dimensionId} créé depuis le pack '{pack}', dimension '{packDimension}' (graine {seed})."
it_is_live_now_re_injected_on_every_startup_teleport = "Il est maintenant actif et réinjecté à chaque démarrage. Téléportez-vous avec /iris world status ou un portail ; aucun redémarrage nécessaire."
failed_inject_iris_primary_world = "Impossible d'injecter le monde principal Iris : {value}{errorMessage}"
diff --git a/core/src/main/resources/languages/he_IL.toml b/core/src/main/resources/languages/he_IL.toml
index fa8a790f2..a2d4fc6ed 100644
--- a/core/src/main/resources/languages/he_IL.toml
+++ b/core/src/main/resources/languages/he_IL.toml
@@ -1082,6 +1082,7 @@ opening_image_map_studio_on_server_display = "Opening the Image Map Studio for {
[iris.modded.moddedworldcommands]
required_pack_is_not_installed_install_then_restart = "החבילה הנדרשת '{pack}' אינה מותקנת. התקן אותה באמצעות /iris download ולאחר מכן הפעל מחדש את השרת."
failed_inject_iris_world = "נכשל להזרק Iris עולם »{dimensionId}': {value}{errorMessage}"
+creating_iris_world_in_background = "יוצר את עולם Iris {dimensionId} מהחבילה '{pack}', מימד '{packDimension}' (זרע {seed}) ברקע. תקבל הודעה כשהוא יהיה זמין."
created_iris_world_from_pack_dimension_seed = "נוצר Iris עולם העולם {dimensionId} מתוך \"חבילה\"{pack}מימד \"{packDimension}(זה {seed})."
it_is_live_now_re_injected_on_every_startup_teleport = "הוא חי עכשיו ומופץ מחדש על כל סטארט-אפ. טלפורט עם /iris world status או פורטל, אין צורך מחדש."
failed_inject_iris_primary_world = "נכשל להזרק Iris עולם ראשון: {value}{errorMessage}"
diff --git a/core/src/main/resources/languages/it_IT.toml b/core/src/main/resources/languages/it_IT.toml
index 8b8a2ea36..a16875d36 100644
--- a/core/src/main/resources/languages/it_IT.toml
+++ b/core/src/main/resources/languages/it_IT.toml
@@ -1077,6 +1077,7 @@ opening_image_map_studio_on_server_display = "Opening the Image Map Studio for {
[iris.modded.moddedworldcommands]
required_pack_is_not_installed_install_then_restart = "Il pack richiesto '{pack}' non è installato. Installalo con /iris download, quindi riavvia il server."
failed_inject_iris_world = "Iniezione del mondo Iris '{dimensionId}' non riuscita: {value}{errorMessage}"
+creating_iris_world_in_background = "Creazione del mondo Iris {dimensionId} dal pack '{pack}', dimensione '{packDimension}' (seed {seed}) in background. Riceverai un avviso quando sarà pronto."
created_iris_world_from_pack_dimension_seed = "Creato il mondo Iris {dimensionId} dal Pack '{pack}', dimensione '{packDimension}' (seed {seed})."
it_is_live_now_re_injected_on_every_startup_teleport = "Ora è attivo e viene iniettato a ogni avvio. Teletrasportati con /iris world status o tramite un portale; non è necessario riavviare."
failed_inject_iris_primary_world = "Iniezione del mondo Iris principale non riuscita: {value}{errorMessage}"
diff --git a/core/src/main/resources/languages/ja-JP.toml b/core/src/main/resources/languages/ja-JP.toml
index cc14b6a4d..efc1d6bae 100644
--- a/core/src/main/resources/languages/ja-JP.toml
+++ b/core/src/main/resources/languages/ja-JP.toml
@@ -1080,6 +1080,7 @@ opening_image_map_studio_on_server_display = "Opening the Image Map Studio for {
[iris.modded.moddedworldcommands]
required_pack_is_not_installed_install_then_restart = "必要なパック '{pack}' がインストールされていません。/iris download でインストールしてからサーバーを再起動してください。"
failed_inject_iris_world = "Iris ワールド '{dimensionId}' を注入できませんでした: {value}{errorMessage}"
+creating_iris_world_in_background = "パック '{pack}' のディメンション '{packDimension}'(シード {seed})から Iris ワールド {dimensionId} をバックグラウンドで作成しています。準備ができたらお知らせします。"
created_iris_world_from_pack_dimension_seed = "Iris ワールド {dimensionId} を、パック '{pack}' のディメンション '{packDimension}'(シード {seed})から作成しました。"
it_is_live_now_re_injected_on_every_startup_teleport = "現在使用可能で、起動するたびに再注入されます。/iris world status またはポータルからテレポートできます。再起動は不要です。"
failed_inject_iris_primary_world = "Iris プライマリーワールドを注入できませんでした: {value}{errorMessage}"
diff --git a/core/src/main/resources/languages/ko_KR.toml b/core/src/main/resources/languages/ko_KR.toml
index a67aeddd8..6596e77df 100644
--- a/core/src/main/resources/languages/ko_KR.toml
+++ b/core/src/main/resources/languages/ko_KR.toml
@@ -1080,6 +1080,7 @@ opening_image_map_studio_on_server_display = "Opening the Image Map Studio for {
[iris.modded.moddedworldcommands]
required_pack_is_not_installed_install_then_restart = "필수 팩 '{pack}'이(가) 설치되어 있지 않습니다. /iris download로 설치한 다음 서버를 다시 시작하세요."
failed_inject_iris_world = "Iris 월드 '{dimensionId}' 주입 실패: {value}{errorMessage}"
+creating_iris_world_in_background = "팩 '{pack}'의 차원 '{packDimension}'(시드 {seed})에서 Iris 월드 {dimensionId}을(를) 백그라운드에서 만드는 중입니다. 준비되면 알려 드립니다."
created_iris_world_from_pack_dimension_seed = "팩 '{pack}'의 차원 '{packDimension}'(시드 {seed})에서 Iris 월드 {dimensionId}을(를) 만들었습니다."
it_is_live_now_re_injected_on_every_startup_teleport = "그것은 지금 살고 모든 시작에 거부. Teleport와 함께 /iris world status 또는 포털; 재시작 필요 없음."
failed_inject_iris_primary_world = "Iris 기본 월드 주입 실패: {value}{errorMessage}"
diff --git a/core/src/main/resources/languages/lt_LT.toml b/core/src/main/resources/languages/lt_LT.toml
index 2d1ce337a..fc58ff650 100644
--- a/core/src/main/resources/languages/lt_LT.toml
+++ b/core/src/main/resources/languages/lt_LT.toml
@@ -1082,6 +1082,7 @@ opening_image_map_studio_on_server_display = "Opening the Image Map Studio for {
[iris.modded.moddedworldcommands]
required_pack_is_not_installed_install_then_restart = "Reikalingas paketas '{pack}' neįdiegtas. Įdiekite jį naudodami /iris download, tada paleiskite serverį iš naujo."
failed_inject_iris_world = "Nepavyko sušvirkšti Iris pasaulis \"{dimensionId}': {value}{errorMessage}"
+creating_iris_world_in_background = "Iris pasaulis {dimensionId} kuriamas fone iš paketo '{pack}', dimensija '{packDimension}' (sėkla {seed}). Pranešime, kai jis bus paruoštas."
created_iris_world_from_pack_dimension_seed = "Sukurta Iris pasaulis {dimensionId} iš pakuotės \"{pack}\"matmuo\"{packDimension}\"(sėklos {seed})."
it_is_live_now_re_injected_on_every_startup_teleport = "Jis gyvena dabar ir pakartotinai įšvirkščiamas į kiekvieną paleidimą. Teleportas su /iris world status arba portalas; nereikia paleisti iš naujo."
failed_inject_iris_primary_world = "Nepavyko sušvirkšti Iris pirminis pasaulis: {value}{errorMessage}"
diff --git a/core/src/main/resources/languages/nl_NL.toml b/core/src/main/resources/languages/nl_NL.toml
index 22e589492..d73f0012f 100644
--- a/core/src/main/resources/languages/nl_NL.toml
+++ b/core/src/main/resources/languages/nl_NL.toml
@@ -1077,6 +1077,7 @@ opening_image_map_studio_on_server_display = "Opening the Image Map Studio for {
[iris.modded.moddedworldcommands]
required_pack_is_not_installed_install_then_restart = "Het vereiste pack '{pack}' is niet geïnstalleerd. Installeer het met /iris download en start de server daarna opnieuw."
failed_inject_iris_world = "Spuiten mislukt Iris wereld{dimensionId}': {value}{errorMessage}"
+creating_iris_world_in_background = "Iris-wereld {dimensionId} wordt op de achtergrond aangemaakt uit pack '{pack}', dimensie '{packDimension}' (seed {seed}). Je krijgt een melding zodra hij klaar is."
created_iris_world_from_pack_dimension_seed = "Aangemaakt Iris wereld {dimensionId} uit verpakking{pack}' dimensie'{packDimension}\" (zaad {seed})."
it_is_live_now_re_injected_on_every_startup_teleport = "Het is nu live en opnieuw bij elke opstart. Teleporteren met /iris world status of een portaal; geen herstart vereist."
failed_inject_iris_primary_world = "Spuiten mislukt Iris primaire wereld: {value}{errorMessage}"
diff --git a/core/src/main/resources/languages/pl_PL.toml b/core/src/main/resources/languages/pl_PL.toml
index 99a973149..efc686ba8 100644
--- a/core/src/main/resources/languages/pl_PL.toml
+++ b/core/src/main/resources/languages/pl_PL.toml
@@ -1082,6 +1082,7 @@ opening_image_map_studio_on_server_display = "Opening the Image Map Studio for {
[iris.modded.moddedworldcommands]
required_pack_is_not_installed_install_then_restart = "Wymagany pakiet '{pack}' nie jest zainstalowany. Zainstaluj go poleceniem /iris download, a następnie uruchom serwer ponownie."
failed_inject_iris_world = "Nie można wykonać wstrzyknięcia Iris Świat \"{dimensionId}': {value}{errorMessage}"
+creating_iris_world_in_background = "Tworzenie świata Iris {dimensionId} z paczki '{pack}', wymiar '{packDimension}' (ziarno {seed}) w tle. Otrzymasz powiadomienie, gdy będzie gotowy."
created_iris_world_from_pack_dimension_seed = "Stworzony Iris świat {dimensionId} od opakowania \"{pack}\"wymiar\"{packDimension}\"(nasiona {seed})."
it_is_live_now_re_injected_on_every_startup_teleport = "To jest teraz na żywo i ponownie wstrzyknięte na każdym startup. Teleport /iris world status lub portalu; nie jest wymagane ponowne uruchomienie."
failed_inject_iris_primary_world = "Nie można wykonać wstrzyknięcia Iris świat pierwotny: {value}{errorMessage}"
diff --git a/core/src/main/resources/languages/pt_PT.toml b/core/src/main/resources/languages/pt_PT.toml
index 28f9263cc..b3d7d3271 100644
--- a/core/src/main/resources/languages/pt_PT.toml
+++ b/core/src/main/resources/languages/pt_PT.toml
@@ -1077,6 +1077,7 @@ opening_image_map_studio_on_server_display = "Opening the Image Map Studio for {
[iris.modded.moddedworldcommands]
required_pack_is_not_installed_install_then_restart = "O pacote necessário '{pack}' não está instalado. Instale-o com /iris download e depois reinicie o servidor."
failed_inject_iris_world = "Não foi possível injectar Iris mundo'{dimensionId}': {value}{errorMessage}"
+creating_iris_world_in_background = "A criar o mundo Iris {dimensionId} a partir do pack '{pack}', dimensão '{packDimension}' (semente {seed}) em segundo plano. Serás avisado quando estiver pronto."
created_iris_world_from_pack_dimension_seed = "Criado Iris mundo {dimensionId} da pack{pack}« dimensão »{packDimension}« (sementes {seed})."
it_is_live_now_re_injected_on_every_startup_teleport = "É ao vivo agora e reinjectado em cada startup. Teletransporte com /iris world status ou um portal; não é necessário reiniciar."
failed_inject_iris_primary_world = "Não foi possível injectar Iris mundo primário: {value}{errorMessage}"
diff --git a/core/src/main/resources/languages/ru_RU.toml b/core/src/main/resources/languages/ru_RU.toml
index 2d1be8afc..6a9367c2c 100644
--- a/core/src/main/resources/languages/ru_RU.toml
+++ b/core/src/main/resources/languages/ru_RU.toml
@@ -1082,6 +1082,7 @@ opening_image_map_studio_on_server_display = "Opening the Image Map Studio for {
[iris.modded.moddedworldcommands]
required_pack_is_not_installed_install_then_restart = "Необходимый пакет '{pack}' не установлен. Установите его командой /iris download, затем перезапустите сервер."
failed_inject_iris_world = "Не удалось ввести Iris мир{dimensionId}': {value}{errorMessage}"
+creating_iris_world_in_background = "Мир Iris {dimensionId} создаётся в фоне из пакета '{pack}', измерение '{packDimension}' (сид {seed}). Вы получите сообщение, когда он будет готов."
created_iris_world_from_pack_dimension_seed = "созданный Iris мир {dimensionId} из пакета{pack}Измерение{packDimension}' (seed {seed})."
it_is_live_now_re_injected_on_every_startup_teleport = "Теперь он жив и повторно вводится в каждый стартап. Телепортируйте с /iris world status Портал; перезагрузка не требуется."
failed_inject_iris_primary_world = "Не удалось ввести Iris Первичный мир: {value}{errorMessage}"
diff --git a/core/src/main/resources/languages/tr_TR.toml b/core/src/main/resources/languages/tr_TR.toml
index d95fae2e3..80f3631ef 100644
--- a/core/src/main/resources/languages/tr_TR.toml
+++ b/core/src/main/resources/languages/tr_TR.toml
@@ -1082,6 +1082,7 @@ opening_image_map_studio_on_server_display = "Opening the Image Map Studio for {
[iris.modded.moddedworldcommands]
required_pack_is_not_installed_install_then_restart = "Gerekli '{pack}' paketi kurulu değil. /iris download ile kurun, ardından sunucuyu yeniden başlatın."
failed_inject_iris_world = "enjekte etmek için başarısız oldu Iris Dünya{dimensionId}': {value}{errorMessage}"
+creating_iris_world_in_background = "Iris dünyası {dimensionId}, '{pack}' paketinin '{packDimension}' boyutundan (tohum {seed}) arka planda oluşturuluyor. Hazır olduğunda bilgilendirileceksin."
created_iris_world_from_pack_dimension_seed = "Oluşturuluş Iris dünya dünyası {dimensionId} paketten ‘{pack}‘ Boyut’{packDimension}\"(s. {seed})."
it_is_live_now_re_injected_on_every_startup_teleport = "Şimdi yaşıyor ve her başlangıçta yeniden canlanıyor. Teleport ile birlikte /iris world status veya bir portal; hiçbir yeniden başlama gerekli değildir."
failed_inject_iris_primary_world = "enjekte etmek için başarısız oldu Iris birincil dünya: {value}{errorMessage}"
diff --git a/core/src/main/resources/languages/vi_VI.toml b/core/src/main/resources/languages/vi_VI.toml
index 86a373f1b..e272f6b4b 100644
--- a/core/src/main/resources/languages/vi_VI.toml
+++ b/core/src/main/resources/languages/vi_VI.toml
@@ -1080,6 +1080,7 @@ opening_image_map_studio_on_server_display = "Opening the Image Map Studio for {
[iris.modded.moddedworldcommands]
required_pack_is_not_installed_install_then_restart = "Gói bắt buộc '{pack}' chưa được cài đặt. Hãy cài bằng /iris download, rồi khởi động lại máy chủ."
failed_inject_iris_world = "Lỗi khi tiêm Iris Thế giới{dimensionId}': {value}{errorMessage}"
+creating_iris_world_in_background = "Đang tạo thế giới Iris {dimensionId} từ gói '{pack}', chiều '{packDimension}' (hạt giống {seed}) trong nền. Bạn sẽ được thông báo khi nó sẵn sàng."
created_iris_world_from_pack_dimension_seed = "Tạo Iris thế giới {dimensionId} từ gói '{pack}Không gian{packDimension}' (dòng dõi {seed})."
it_is_live_now_re_injected_on_every_startup_teleport = "Nó được truyền hình trực tiếp và phát lại vào mỗi lần khởi động. Name /iris world status hoặc cổng; không cần khởi động lại."
failed_inject_iris_primary_world = "Lỗi khi tiêm Iris Thế giới chính: {value}{errorMessage}"
diff --git a/core/src/main/resources/languages/zh_CN.toml b/core/src/main/resources/languages/zh_CN.toml
index 99070694e..eab713f9e 100644
--- a/core/src/main/resources/languages/zh_CN.toml
+++ b/core/src/main/resources/languages/zh_CN.toml
@@ -1080,6 +1080,7 @@ opening_image_map_studio_on_server_display = "Opening the Image Map Studio for {
[iris.modded.moddedworldcommands]
required_pack_is_not_installed_install_then_restart = "所需维度包“{pack}”尚未安装。请使用 /iris download 安装,然后重启服务器。"
failed_inject_iris_world = "注入失败 Iris 世界{dimensionId}': {value}{errorMessage}"
+creating_iris_world_in_background = "正在后台从包 '{pack}' 的维度 '{packDimension}'(种子 {seed})创建 Iris 世界 {dimensionId}。完成后会通知你。"
created_iris_world_from_pack_dimension_seed = "创建 Iris 世界 {dimensionId} 从包\"{pack}\" 维度 \"{packDimension}'(种子 {seed})."
it_is_live_now_re_injected_on_every_startup_teleport = "它现在正在直播,并在每次启动时重新注入。 传送到 /iris world status 或门户网站;不需要重新启动。"
failed_inject_iris_primary_world = "注入失败 Iris 初级世界: {value}{errorMessage}"
diff --git a/core/src/main/resources/languages/zh_TW.toml b/core/src/main/resources/languages/zh_TW.toml
index edb721831..b8261511c 100644
--- a/core/src/main/resources/languages/zh_TW.toml
+++ b/core/src/main/resources/languages/zh_TW.toml
@@ -1080,6 +1080,7 @@ opening_image_map_studio_on_server_display = "Opening the Image Map Studio for {
[iris.modded.moddedworldcommands]
required_pack_is_not_installed_install_then_restart = "所需維度包「{pack}」尚未安裝。請使用 /iris download 安裝,然後重新啟動伺服器。"
failed_inject_iris_world = "注入失敗 Iris 世界{dimensionId}': {value}{errorMessage}"
+creating_iris_world_in_background = "正在背景從包 '{pack}' 的維度 '{packDimension}'(種子 {seed})建立 Iris 世界 {dimensionId}。完成後會通知你。"
created_iris_world_from_pack_dimension_seed = "建立 Iris 世界 {dimensionId} 從包\"{pack}\" 維度 \"{packDimension}'(種子 {seed})."
it_is_live_now_re_injected_on_every_startup_teleport = "它現在正在直播,並在每次啟動時重新注入。 傳送到 /iris world status 或入口網站;不需要重新啟動。"
failed_inject_iris_primary_world = "注入失敗 Iris 初級世界: {value}{errorMessage}"