Merged off-thread world creation

- Modded create, enable and replace-overworld prepare worlds in the background
- Worlds still being created refuse duplicate create, delete, disable and update
This commit is contained in:
Brian Neumann-Fopiano
2026-10-15 19:01:06 -04:00
31 changed files with 1203 additions and 378 deletions
@@ -0,0 +1,38 @@
/*
* 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 <https://www.gnu.org/licenses/>.
*/
package art.arcane.iris.modded;
public final class MissingPackDimensionException extends IllegalStateException {
private final String pack;
private final String dimensionKey;
public MissingPackDimensionException(String pack, String dimensionKey) {
super("Pack '" + pack + "' does not contain dimensions/" + dimensionKey + ".json");
this.pack = pack;
this.dimensionKey = dimensionKey;
}
public String pack() {
return pack;
}
public String dimensionKey() {
return dimensionKey;
}
}
@@ -0,0 +1,148 @@
/*
* 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 <https://www.gnu.org/licenses/>.
*/
package art.arcane.iris.modded;
import art.arcane.iris.generation.runtime.Engine;
import art.arcane.iris.modded.command.ModdedPackCommands;
import art.arcane.iris.pack.loading.IrisData;
import art.arcane.volmlib.nativelib.minecraft26_2.modded.NativeModdedServer;
import java.io.File;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentHashMap;
public final class ModdedDimensionCreations {
private static final Map<String, ModdedDimensionInjection.Async<?, ?, ?>> ACTIVE = new ConcurrentHashMap<>();
private ModdedDimensionCreations() {
}
public static CompletableFuture<Void> create(
NativeModdedServer server,
String dimensionId,
String pack,
String packDimensionKey,
long seed
) {
if (ModdedDimensionRegistryStore.get(server, dimensionId) != null) {
return CompletableFuture.failedFuture(new IllegalStateException(
ModdedDimensionManager.alreadyExists(dimensionId)));
}
ModdedDimensionRegistryStore.PersistentDimension dimension =
new ModdedDimensionRegistryStore.PersistentDimension(dimensionId, pack, packDimensionKey, seed);
ModdedDimensionInjection.Async<ModdedWorldEngines.PreparedPack, ModdedDimensionManager.PendingInjection, Engine> creation =
ModdedDimensionInjection.async(dimension, new CreationSteps(server), server::execute);
if (ACTIVE.putIfAbsent(dimensionId, creation) != null) {
return CompletableFuture.failedFuture(new IllegalStateException(beingCreated(dimensionId)));
}
CompletableFuture<Void> created = creation.result().handle(
(ModdedDimensionInjection.Result result, Throwable failure) -> settle(creation, dimension, result));
creation.start();
return created;
}
public static boolean isCreating(String dimensionId) {
return ACTIVE.containsKey(dimensionId);
}
public static void requireNotCreating(String dimensionId) {
if (isCreating(dimensionId)) {
throw new IllegalStateException(beingCreated(dimensionId));
}
}
public static void cancelAll() {
List<ModdedDimensionInjection.Async<?, ?, ?>> active = new ArrayList<>(ACTIVE.values());
for (ModdedDimensionInjection.Async<?, ?, ?> creation : active) {
creation.cancel();
}
}
private static Void settle(
ModdedDimensionInjection.Async<?, ?, ?> creation,
ModdedDimensionRegistryStore.PersistentDimension dimension,
ModdedDimensionInjection.Result result
) {
ACTIVE.remove(dimension.id(), creation);
Throwable cause = result == null
? new IllegalStateException("Iris dimension creation for '" + dimension.id() + "' ended without a result")
: result.failure();
if (cause instanceof Error fatal) {
throw fatal;
}
if (cause != null) {
throw cause instanceof RuntimeException runtimeException ? runtimeException : new IllegalStateException(cause);
}
ModdedIrisLog.info("Iris created runtime dimension '{}' (pack={} dim={}): prepared {}ms, engine {}ms",
dimension.id(), dimension.pack(), dimension.dimension(), result.preparedMillis(), result.builtMillis());
return null;
}
private static String beingCreated(String dimensionId) {
return "Iris dimension '" + dimensionId + "' is still being created. Try again when it is live.";
}
private record CreationSteps(NativeModdedServer server)
implements ModdedDimensionInjection.Steps<ModdedWorldEngines.PreparedPack, ModdedDimensionManager.PendingInjection, Engine> {
@Override
public ModdedWorldEngines.PreparedPack prepare(ModdedDimensionRegistryStore.PersistentDimension dimension) {
ModdedStartup.requirePackForWorldCreation(dimension.pack());
IrisData data = IrisData.get(new File(ModdedPackCommands.packsRoot(), dimension.pack()));
if (data.getDimensionLoader().load(dimension.dimension()) == null) {
throw new MissingPackDimensionException(dimension.pack(), dimension.dimension());
}
return ModdedWorldEngines.prepareCreated(server, dimension);
}
@Override
public ModdedDimensionManager.PendingInjection begin(
ModdedDimensionRegistryStore.PersistentDimension dimension,
ModdedWorldEngines.PreparedPack prepared
) {
return ModdedDimensionManager.beginCreation(server, dimension, prepared.active());
}
@Override
public Engine build(ModdedDimensionManager.PendingInjection level, ModdedWorldEngines.PreparedPack prepared) {
if (level.present()) {
prepared.close();
return null;
}
return ModdedWorldEngines.createPrepared(level.handle().level(), level.handle().seed(), prepared);
}
@Override
public void complete(ModdedDimensionManager.PendingInjection level, Engine engine) {
ModdedDimensionManager.completeCreation(server, level, engine);
}
@Override
public void abandon(ModdedDimensionManager.PendingInjection level, Engine engine, Throwable failure) {
ModdedDimensionManager.abandonCreation(server, level, engine, failure);
}
@Override
public void discard(ModdedWorldEngines.PreparedPack prepared) {
prepared.close();
}
}
}
@@ -0,0 +1,434 @@
/*
* 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 <https://www.gnu.org/licenses/>.
*/
package art.arcane.iris.modded;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.CancellationException;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionException;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Executor;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.concurrent.atomic.AtomicBoolean;
final class ModdedDimensionInjection<P, L, E> implements AutoCloseable {
private static final int MINIMUM_WORKERS = 2;
private static final long CANCEL_BUILD_WAIT_SECONDS = 120L;
private final List<Preparation<P>> preparations;
private final Steps<P, L, E> steps;
private final ExecutorService workers;
private final AtomicBoolean finished = new AtomicBoolean();
private ModdedDimensionInjection(List<Preparation<P>> preparations, Steps<P, L, E> steps, ExecutorService workers) {
this.preparations = preparations;
this.steps = steps;
this.workers = workers;
}
static <P, L, E> ModdedDimensionInjection<P, L, E> start(
List<ModdedDimensionRegistryStore.PersistentDimension> dimensions,
Steps<P, L, E> steps
) {
Steps<P, L, E> requiredSteps = Objects.requireNonNull(steps, "steps");
ExecutorService workers = Executors.newFixedThreadPool(workerCount(dimensions.size()),
Thread.ofPlatform().name("Iris Dimension Restore ", 1L).daemon(true).factory());
List<Preparation<P>> 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 <P, L, E> Async<P, L, E> async(
ModdedDimensionRegistryStore.PersistentDimension dimension,
Steps<P, L, E> steps,
Executor serverThread
) {
return new Async<>(dimension, steps, serverThread);
}
void awaitPreparations() {
List<CompletableFuture<Timed<P>>> futures = new ArrayList<>(preparations.size());
for (Preparation<P> preparation : preparations) {
futures.add(preparation.future());
}
CompletableFuture.allOf(futures.toArray(CompletableFuture[]::new))
.handle((Void ignored, Throwable failure) -> null)
.join();
}
List<Result> finish() {
if (!finished.compareAndSet(false, true)) {
throw new IllegalStateException("Iris dimension injection already finished");
}
try {
List<Building<L, E>> building = new ArrayList<>(preparations.size());
Result[] results = new Result[preparations.size()];
for (int index = 0; index < preparations.size(); index++) {
Preparation<P> preparation = preparations.get(index);
Timed<P> 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<Timed<E>> engine = CompletableFuture.supplyAsync(
() -> timed(() -> steps.build(level, prepared.value())), workers);
building.add(new Building<>(index, preparation.dimension(), prepared.millis(), level, engine));
}
for (Building<L, E> 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<P> preparation : preparations) {
preparation.future().whenComplete((Timed<P> prepared, Throwable failure) -> {
if (prepared != null) {
discard(steps, prepared.value(), null);
}
});
}
}
workers.shutdown();
}
private Result complete(Building<L, E> pending) {
Timed<E> 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 <P, L, E> void abandon(Steps<P, L, E> steps, L level, E engine, Throwable failure) {
try {
steps.abandon(level, engine, failure);
} catch (Throwable cleanupFailure) {
failure.addSuppressed(cleanupFailure);
}
}
private static <P, L, E> void discard(Steps<P, L, E> 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 <T> Timed<T> timed(Step<T> 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> T join(CompletableFuture<T> 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, L, E> {
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<P, L, E> {
private final ModdedDimensionRegistryStore.PersistentDimension dimension;
private final Steps<P, L, E> steps;
private final Executor serverThread;
private final ExecutorService worker;
private final CompletableFuture<Result> result = new CompletableFuture<>();
private State state = State.UNSTARTED;
private Timed<P> prepared;
private L level;
private CompletableFuture<Timed<E>> engine;
private Async(
ModdedDimensionRegistryStore.PersistentDimension dimension,
Steps<P, L, E> 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> 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<P> unused = null;
CompletableFuture<Timed<E>> 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<P> 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<P> 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<Timed<E>> building;
synchronized (this) {
level = created;
state = State.BUILDING;
building = CompletableFuture.supplyAsync(() -> timed(() -> steps.build(created, ready.value())), worker);
engine = building;
}
building.whenComplete((Timed<E> built, Throwable failure) -> serverThread.execute(this::complete));
}
private void complete() {
CompletableFuture<Timed<E>> building;
L created;
long preparedMillis;
synchronized (this) {
if (state != State.BUILDING) {
return;
}
state = State.DONE;
building = engine;
created = level;
preparedMillis = prepared.millis();
}
Timed<E> 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<Timed<E>> 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> {
T run() throws Exception;
}
private record Timed<T>(T value, long millis) {
}
private record Preparation<P>(
ModdedDimensionRegistryStore.PersistentDimension dimension,
CompletableFuture<Timed<P>> future
) {
}
private record Building<L, E>(
int index,
ModdedDimensionRegistryStore.PersistentDimension dimension,
long preparedMillis,
L level,
CompletableFuture<Timed<E>> engine
) {
}
}
@@ -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");
}
}
@@ -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 <https://www.gnu.org/licenses/>.
*/
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<P, L, E> implements AutoCloseable {
private static final int MINIMUM_WORKERS = 2;
private final List<Preparation<P>> preparations;
private final Steps<P, L, E> steps;
private final ExecutorService workers;
private final AtomicBoolean finished = new AtomicBoolean();
private ModdedDimensionRestore(List<Preparation<P>> preparations, Steps<P, L, E> steps, ExecutorService workers) {
this.preparations = preparations;
this.steps = steps;
this.workers = workers;
}
static <P, L, E> ModdedDimensionRestore<P, L, E> start(
List<ModdedDimensionRegistryStore.PersistentDimension> dimensions,
Steps<P, L, E> steps
) {
Steps<P, L, E> requiredSteps = Objects.requireNonNull(steps, "steps");
ExecutorService workers = Executors.newFixedThreadPool(workerCount(dimensions.size()),
Thread.ofPlatform().name("Iris Dimension Restore ", 1L).daemon(true).factory());
List<Preparation<P>> 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<CompletableFuture<Timed<P>>> futures = new ArrayList<>(preparations.size());
for (Preparation<P> preparation : preparations) {
futures.add(preparation.future());
}
CompletableFuture.allOf(futures.toArray(CompletableFuture[]::new))
.handle((Void ignored, Throwable failure) -> null)
.join();
}
List<Result> finish() {
if (!finished.compareAndSet(false, true)) {
throw new IllegalStateException("Iris dimension restore already finished");
}
try {
List<Building<L, E>> building = new ArrayList<>(preparations.size());
Result[] results = new Result[preparations.size()];
for (int index = 0; index < preparations.size(); index++) {
Preparation<P> preparation = preparations.get(index);
Timed<P> 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<Timed<E>> engine = CompletableFuture.supplyAsync(
() -> timed(() -> steps.build(level, prepared.value())), workers);
building.add(new Building<>(index, preparation.dimension(), prepared.millis(), level, engine));
}
for (Building<L, E> 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<P> preparation : preparations) {
preparation.future().whenComplete((Timed<P> prepared, Throwable failure) -> {
if (prepared != null) {
discard(prepared.value(), null);
}
});
}
}
workers.shutdown();
}
private Result complete(Building<L, E> pending) {
Timed<E> 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 <T> Timed<T> timed(Step<T> 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> T join(CompletableFuture<T> 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, L, E> {
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> {
T run() throws Exception;
}
private record Timed<T>(T value, long millis) {
}
private record Preparation<P>(
ModdedDimensionRegistryStore.PersistentDimension dimension,
CompletableFuture<Timed<P>> future
) {
}
private record Building<L, E>(
int index,
ModdedDimensionRegistryStore.PersistentDimension dimension,
long preparedMillis,
L level,
CompletableFuture<Timed<E>> engine
) {
}
}
@@ -213,6 +213,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);
@@ -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,
@@ -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<ModdedDimensionRestore<ModdedWorldEngines.PreparedPack, ModdedDimensionManager.RestoringLevel, Engine>> RESTORE =
private static final AtomicReference<ModdedDimensionInjection<ModdedWorldEngines.PreparedPack, ModdedDimensionManager.PendingInjection, Engine>> RESTORE =
new AtomicReference<>();
private static final List<String> DIMENSION_FAILURES = new CopyOnWriteArrayList<>();
@@ -55,7 +55,7 @@ public final class ModdedStartup {
}
public static void reset() {
ModdedDimensionRestore<ModdedWorldEngines.PreparedPack, ModdedDimensionManager.RestoringLevel, Engine> restore =
ModdedDimensionInjection<ModdedWorldEngines.PreparedPack, ModdedDimensionManager.PendingInjection, Engine> restore =
RESTORE.getAndSet(null);
if (restore != null) {
restore.close();
@@ -83,8 +83,8 @@ public final class ModdedStartup {
return;
}
long startedAt = System.currentTimeMillis();
ModdedDimensionRestore<ModdedWorldEngines.PreparedPack, ModdedDimensionManager.RestoringLevel, Engine> restore =
ModdedDimensionRestore.start(dimensions, new PersistentRestoreSteps(server));
ModdedDimensionInjection<ModdedWorldEngines.PreparedPack, ModdedDimensionManager.PendingInjection, Engine> 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<ModdedWorldEngines.PreparedPack, ModdedDimensionManager.RestoringLevel, Engine> restore =
ModdedDimensionInjection<ModdedWorldEngines.PreparedPack, ModdedDimensionManager.PendingInjection, Engine> 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<ModdedDimensionRestore.Result> results;
List<ModdedDimensionInjection.Result> 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<ModdedWorldEngines.PreparedPack, ModdedDimensionManager.RestoringLevel, Engine> {
implements ModdedDimensionInjection.Steps<ModdedWorldEngines.PreparedPack, ModdedDimensionManager.PendingInjection, Engine> {
@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
@@ -150,13 +150,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()));
@@ -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<NativeCommandSource> 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<Void> 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) {
}
}
@@ -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}" + ")."
@@ -1553,6 +1557,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,
@@ -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<ModdedDimensionRestore.Result> results;
try (ModdedDimensionRestore<String, String, String> restore = ModdedDimensionRestore.start(dimensions, steps)) {
List<ModdedDimensionInjection.Result> results;
try (ModdedDimensionInjection<String, String, String> 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<String, String, String> restore = ModdedDimensionRestore.start(dimensions, steps)) {
try (ModdedDimensionInjection<String, String, String> restore = ModdedDimensionInjection.start(dimensions, steps)) {
restore.finish();
}
@@ -61,8 +65,8 @@ public class ModdedDimensionRestoreTest {
steps.prepareFailure = failure;
steps.failingIndex = 1;
List<ModdedDimensionRestore.Result> results;
try (ModdedDimensionRestore<String, String, String> restore = ModdedDimensionRestore.start(dimensions, steps)) {
List<ModdedDimensionInjection.Result> results;
try (ModdedDimensionInjection<String, String, String> restore = ModdedDimensionInjection.start(dimensions, steps)) {
results = restore.finish();
}
@@ -82,14 +86,14 @@ public class ModdedDimensionRestoreTest {
steps.buildFailure = failure;
steps.failingIndex = 0;
List<ModdedDimensionRestore.Result> results;
try (ModdedDimensionRestore<String, String, String> restore = ModdedDimensionRestore.start(dimensions, steps)) {
List<ModdedDimensionInjection.Result> results;
try (ModdedDimensionInjection<String, String, String> 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<ModdedDimensionRestore.Result> results;
try (ModdedDimensionRestore<String, String, String> restore = ModdedDimensionRestore.start(dimensions, steps)) {
List<ModdedDimensionInjection.Result> results;
try (ModdedDimensionInjection<String, String, String> restore = ModdedDimensionInjection.start(dimensions, steps)) {
results = restore.finish();
}
@@ -126,7 +130,7 @@ public class ModdedDimensionRestoreTest {
release.countDown();
});
try (ModdedDimensionRestore<String, String, String> restore = ModdedDimensionRestore.start(dimensions, steps)) {
try (ModdedDimensionInjection<String, String, String> restore = ModdedDimensionInjection.start(dimensions, steps)) {
restore.awaitPreparations();
assertEquals(dimensions.size(), steps.prepared.get());
@@ -143,7 +147,7 @@ public class ModdedDimensionRestoreTest {
List<ModdedDimensionRegistryStore.PersistentDimension> dimensions = dimensions(3);
RecordingSteps steps = new RecordingSteps(dimensions.size());
ModdedDimensionRestore<String, String, String> restore = ModdedDimensionRestore.start(dimensions, steps);
ModdedDimensionInjection<String, String, String> 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<String, String, String> 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<String, String, String> 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<String, String, String> 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<String, String, String> 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<String, String, String> 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<ModdedDimensionRegistryStore.PersistentDimension> dimensions(int count) {
List<ModdedDimensionRegistryStore.PersistentDimension> 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<String, String, String> {
private static final class RecordingSteps implements ModdedDimensionInjection.Steps<String, String, String> {
private final Set<Thread> prepareThreads = ConcurrentHashMap.newKeySet();
private final Set<Thread> buildThreads = ConcurrentHashMap.newKeySet();
private final Set<Thread> 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
@@ -36,7 +36,7 @@ public class ModdedStartupReinjectFailureTest {
IllegalStateException failure = new IllegalStateException("Iris generation history is unusable for 'iris:moon'");
try (MockedStatic<ModdedIrisLog> 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<String, String, String> {
private record FailingSteps(RuntimeException failure) implements ModdedDimensionInjection.Steps<String, String, String> {
@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");
}
@@ -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<Void> creation = new CompletableFuture<>();
Harness harness = harness();
try (MockedStatic<ModdedPackCommands> packs = mockStatic(ModdedPackCommands.class);
MockedStatic<ModdedDimensionCreations> 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<String> 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<String> 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<String> failures = run(harness, new MissingPackDimensionException("overworld", "overworld"));
assertTrue(String.join("\n", failures), failures.stream().anyMatch((String line) ->
line.contains("dimensions/overworld.json")));
}
private List<String> run(Harness harness, RuntimeException failure) throws Exception {
CompletableFuture<Void> creation = new CompletableFuture<>();
try (MockedStatic<ModdedPackCommands> packs = mockStatic(ModdedPackCommands.class);
MockedStatic<ModdedDimensionCreations> 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<Runnable> serverTasks = new ArrayList<>();
doAnswer((InvocationOnMock invocation) -> {
serverTasks.add(invocation.getArgument(0));
return null;
}).when(server).execute(any());
List<String> success = new ArrayList<>();
List<String> failure = new ArrayList<>();
NativeCommandSource source = mock(NativeCommandSource.class);
when(source.server()).thenReturn(server);
when(source.isGameMaster()).thenReturn(true);
doAnswer((InvocationOnMock invocation) -> {
Supplier<NativeCommandText> 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<Runnable> serverTasks,
List<String> success,
List<String> failure
) {
void runServerTasks() {
while (!serverTasks.isEmpty()) {
serverTasks.removeFirst().run();
}
}
}
}
@@ -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}"
@@ -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}"
@@ -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}"
@@ -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}"
@@ -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}"
@@ -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}"
@@ -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}"
@@ -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}"
@@ -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}"
@@ -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}"
@@ -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}"
@@ -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}"
@@ -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}"
@@ -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}"
@@ -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}"
@@ -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}"
@@ -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}"