mirror of
https://github.com/VolmitSoftware/Iris.git
synced 2026-10-19 06:54:45 +00:00
Created modded worlds off-thread
- /iris create, /iris world create, /iris world enable and /iris world replace-overworld acknowledge at once, build the world in the background, and report success or the existing failure messages to the sender when it finishes - Pack validation, the pack dimension check, generation history creation, pack data loading and the engine build run off the server thread; the server thread only registers the world, creates the level and publishes it - A second create of a world that is still being created, and delete, disable or update of that world, are refused until it is live - Server shutdown cancels in-flight creations, waits for an engine that is still building, and rolls back its level and registry entry - Startup restore and world creation share one dimension injection orchestrator, and abandoning an injection also closes an engine that was already built - Added the creating-in-background message to every locale file
This commit is contained in:
+38
@@ -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;
|
||||
}
|
||||
}
|
||||
+148
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
+434
@@ -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
|
||||
) {
|
||||
}
|
||||
}
|
||||
+73
-61
@@ -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");
|
||||
}
|
||||
}
|
||||
|
||||
-233
@@ -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
|
||||
) {
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
+36
-8
@@ -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
|
||||
|
||||
+19
-2
@@ -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()));
|
||||
|
||||
+92
-36
@@ -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) {
|
||||
}
|
||||
}
|
||||
|
||||
+5
@@ -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,
|
||||
|
||||
+166
-19
@@ -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
|
||||
+3
-3
@@ -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");
|
||||
}
|
||||
|
||||
|
||||
+154
@@ -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}"
|
||||
|
||||
Reference in New Issue
Block a user