diff --git a/adapters/modded-common/src/main/java/art/arcane/iris/modded/IrisModdedChunkGenerator.java b/adapters/modded-common/src/main/java/art/arcane/iris/modded/IrisModdedChunkGenerator.java
index 165ee583a..045832dd2 100644
--- a/adapters/modded-common/src/main/java/art/arcane/iris/modded/IrisModdedChunkGenerator.java
+++ b/adapters/modded-common/src/main/java/art/arcane/iris/modded/IrisModdedChunkGenerator.java
@@ -743,14 +743,8 @@ public final class IrisModdedChunkGenerator implements NativeGeneratorOwner, Nat
PackValidationRegistry.requireLoadable(packRoot);
packDirectory = packRoot.toFile();
}
- IrisData data = IrisData.get(packDirectory);
- IrisDimension dimension = data.getDimensionLoader().load(activeDimensionKey);
- if (dimension == null) {
- throw new IllegalStateException("Iris dimension '" + activeDimensionKey
- + "' missing from pack " + packDirectory.getAbsolutePath());
- }
- ModdedDimensionMetadata.ConfiguredPack resolved = new ModdedDimensionMetadata.ConfiguredPack(
- data, dimension, ModdedDimensionMetadata.dimensionMetadata(dimension));
+ ModdedDimensionMetadata.ConfiguredPack resolved =
+ ModdedDimensionMetadata.ConfiguredPack.load(packDirectory, activeDimensionKey);
configuredPack = resolved;
heightMetadata = resolved.metadata();
return resolved;
diff --git a/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedDimensionManager.java b/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedDimensionManager.java
index b2d0a475f..d4303bea0 100644
--- a/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedDimensionManager.java
+++ b/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedDimensionManager.java
@@ -107,104 +107,109 @@ public final class ModdedDimensionManager {
) {
return create(
server,
- dimensionId,
- pack,
- packDimensionKey,
- seed,
- ModdedGenerationMode.TRANSIENT_STUDIO
+ new DimensionRequest(dimensionId, pack, packDimensionKey, seed),
+ () -> transientStudioPack(pack, packDimensionKey)
);
}
- static Handle restorePersistent(
+ static RestoringLevel beginRestore(
NativeModdedServer server,
- String dimensionId,
- String pack,
- String packDimensionKey,
- long seed
- ) {
- return create(
- server,
- dimensionId,
- pack,
- packDimensionKey,
- seed,
- ModdedGenerationMode.PERSISTENT_RESTORE
- );
- }
-
- private static Handle create(
- NativeModdedServer server,
- String dimensionId,
- String pack,
- String packDimensionKey,
- long seed,
- ModdedGenerationMode generationMode
+ ModdedDimensionRegistryStore.PersistentDimension dimension,
+ ModdedGenerationHistoryStorage.ActivePack active
) {
NativeDimensionRuntime serverAccess = requireAccess();
+ DimensionRequest requested = new DimensionRequest(dimension.id(), dimension.pack(), dimension.dimension(), dimension.seed());
synchronized (LOCK) {
- String key = dimensionId;
- RuntimePack runtimePack = switch (generationMode) {
- case TRANSIENT_STUDIO -> transientStudioPack(pack, packDimensionKey);
- case PERSISTENT_CREATE -> persistentRuntimePack(
- ModdedGenerationHistoryStorage.createOrStage(
- server,
- key,
- pack,
- packDimensionKey,
- seed
- ),
- generationMode
- );
- case PERSISTENT_RESTORE -> persistentRuntimePack(
- ModdedGenerationHistoryStorage.restoreOrAdopt(
- server,
- key,
- pack,
- packDimensionKey,
- seed
- ),
- generationMode
- );
- };
- Handle existing = HANDLES.get(dimensionId);
- if (existing != null && serverAccess.hasLevel(server, key)) {
- Handle refreshed = new Handle(dimensionId, pack, packDimensionKey, seed, existing.level(), existing.generator());
- HANDLES.put(dimensionId, refreshed);
- return refreshed;
+ Handle present = presentHandle(server, serverAccess, requested);
+ if (present != null) {
+ return new RestoringLevel(present, true);
}
- if (serverAccess.hasLevel(server, key)) {
- NativeWorld present = level(server, dimensionId);
- IrisModdedChunkGenerator generator = NativeWorldGenerators.find(present, IrisModdedChunkGenerator.class);
- if (generator == null) {
- throw new IllegalStateException("Iris cannot inject dimension '" + dimensionId + "': a non-Iris level with that id is already loaded");
- }
- ModdedIrisLog.warn("Iris dimension '{}' is already present in the running server; reusing it", dimensionId);
- Handle handle = new Handle(dimensionId, pack, packDimensionKey, seed, present, generator);
- HANDLES.put(dimensionId, handle);
- return handle;
- }
-
try {
- Handle handle = inject(
- server,
- serverAccess,
- dimensionId,
- key,
- pack,
- packDimensionKey,
- seed,
- runtimePack
- );
- HANDLES.put(dimensionId, handle);
- ModdedIrisLog.info("Iris injected runtime dimension '{}' (pack={} dim={} seed={})", dimensionId, pack, packDimensionKey, seed);
- return handle;
+ RuntimePack runtimePack = persistentRuntimePack(active, ModdedGenerationMode.PERSISTENT_RESTORE);
+ return new RestoringLevel(createLevel(server, serverAccess, requested, runtimePack), false);
} catch (Throwable e) {
- ModdedIrisLog.error("Iris failed to inject runtime dimension '{}' (pack={} dim={} seed={})", dimensionId, pack, packDimensionKey, seed, e);
- throw new IllegalStateException("Iris runtime dimension injection failed for " + dimensionId, e);
+ throw injectionFailure(requested, e);
}
}
}
+ static Handle completeRestore(NativeModdedServer server, RestoringLevel restoring, Engine engine) {
+ if (restoring.present()) {
+ return restoring.handle();
+ }
+ NativeDimensionRuntime serverAccess = requireAccess();
+ synchronized (LOCK) {
+ try {
+ return register(publishLevel(server, serverAccess, restoring.handle(), engine));
+ } catch (Throwable e) {
+ throw injectionFailure(DimensionRequest.of(restoring.handle()), e);
+ }
+ }
+ }
+
+ static void abandonRestore(NativeModdedServer server, RestoringLevel restoring, Throwable failure) {
+ if (restoring.present()) {
+ return;
+ }
+ NativeDimensionRuntime serverAccess = requireAccess();
+ Handle handle = restoring.handle();
+ synchronized (LOCK) {
+ rollbackInjection(server, serverAccess, handle.dimensionId(), handle.level(), handle.generator(), false, failure);
+ }
+ }
+
+ private static Handle create(NativeModdedServer server, DimensionRequest requested, RuntimePackSource source) {
+ NativeDimensionRuntime serverAccess = requireAccess();
+ synchronized (LOCK) {
+ RuntimePack runtimePack = source.open();
+ Handle present = presentHandle(server, serverAccess, requested);
+ if (present != null) {
+ return present;
+ }
+ try {
+ return register(publishLevel(server, serverAccess,
+ createLevel(server, serverAccess, requested, runtimePack), null));
+ } catch (Throwable e) {
+ throw injectionFailure(requested, e);
+ }
+ }
+ }
+
+ private static Handle presentHandle(NativeModdedServer server, NativeDimensionRuntime serverAccess, DimensionRequest requested) {
+ String dimensionId = requested.dimensionId();
+ Handle existing = HANDLES.get(dimensionId);
+ if (existing != null && serverAccess.hasLevel(server, dimensionId)) {
+ Handle refreshed = requested.bind(existing.level(), existing.generator());
+ HANDLES.put(dimensionId, refreshed);
+ return refreshed;
+ }
+ if (!serverAccess.hasLevel(server, dimensionId)) {
+ return null;
+ }
+ NativeWorld present = level(server, dimensionId);
+ IrisModdedChunkGenerator generator = NativeWorldGenerators.find(present, IrisModdedChunkGenerator.class);
+ if (generator == null) {
+ throw new IllegalStateException("Iris cannot inject dimension '" + dimensionId + "': a non-Iris level with that id is already loaded");
+ }
+ ModdedIrisLog.warn("Iris dimension '{}' is already present in the running server; reusing it", dimensionId);
+ Handle handle = requested.bind(present, generator);
+ HANDLES.put(dimensionId, handle);
+ return handle;
+ }
+
+ private static Handle register(Handle handle) {
+ HANDLES.put(handle.dimensionId(), handle);
+ ModdedIrisLog.info("Iris injected runtime dimension '{}' (pack={} dim={} seed={})",
+ handle.dimensionId(), handle.pack(), handle.packDimensionKey(), handle.seed());
+ return handle;
+ }
+
+ private static IllegalStateException injectionFailure(DimensionRequest requested, Throwable failure) {
+ ModdedIrisLog.error("Iris failed to inject runtime dimension '{}' (pack={} dim={} seed={})",
+ requested.dimensionId(), requested.pack(), requested.packDimensionKey(), requested.seed(), failure);
+ 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 =
@@ -222,11 +227,17 @@ public final class ModdedDimensionManager {
try {
return create(
server,
- dimensionId,
- pack,
- packDimensionKey,
- seed,
- ModdedGenerationMode.PERSISTENT_CREATE
+ new DimensionRequest(dimensionId, pack, packDimensionKey, seed),
+ () -> persistentRuntimePack(
+ ModdedGenerationHistoryStorage.createOrStage(
+ server,
+ dimensionId,
+ pack,
+ packDimensionKey,
+ seed
+ ),
+ ModdedGenerationMode.PERSISTENT_CREATE
+ )
);
} catch (Throwable e) {
try {
@@ -392,16 +403,16 @@ public final class ModdedDimensionManager {
}
}
- private static Handle inject(
+ private static Handle createLevel(
NativeModdedServer server,
NativeDimensionRuntime serverAccess,
- String dimensionId,
- String key,
- String pack,
- String packDimensionKey,
- long seed,
+ DimensionRequest requested,
RuntimePack runtimePack
) {
+ String dimensionId = requested.dimensionId();
+ String pack = requested.pack();
+ String packDimensionKey = requested.packDimensionKey();
+ long seed = requested.seed();
String typeRef = runtimePack.dimensionTypeKey();
ModdedForcedDatapack.requireRegisteredDimensionType(typeRef,
serverAccess.hasDimensionType(server, typeRef) ? Optional.of(Boolean.TRUE) : Optional.empty(),
@@ -414,35 +425,54 @@ public final class ModdedDimensionManager {
runtimePack.generationMode());
return generator;
}));
- NativeWorld level = created.world();
- IrisModdedChunkGenerator generator = created.generator();
+ try {
+ serverAccess.initialize(server, created.world());
+ return requested.bind(created.world(), created.generator());
+ } catch (Throwable error) {
+ rollbackInjection(server, serverAccess, dimensionId, created.world(), created.generator(), false, error);
+ throw rethrow(dimensionId, error);
+ }
+ }
+ private static Handle publishLevel(
+ NativeModdedServer server,
+ NativeDimensionRuntime serverAccess,
+ Handle created,
+ Engine prepared
+ ) {
+ NativeWorld level = created.level();
+ IrisModdedChunkGenerator generator = created.generator();
boolean loadEventStarted = false;
try {
- serverAccess.initialize(server, level);
+ if (prepared != null) {
+ ModdedWorldEngines.installPrepared(level, prepared);
+ }
generator.bindLevel(level);
- Handle handle = new Handle(dimensionId, pack, packDimensionKey, seed, level, generator);
NativeWorld previous = serverAccess.publish(server, level);
if (previous != null) {
- throw new IllegalStateException("Iris cannot inject dimension '" + dimensionId
+ throw new IllegalStateException("Iris cannot inject dimension '" + created.dimensionId()
+ "': the level was registered concurrently");
}
serverAccess.addWorldBorderListener(server, level);
loadEventStarted = true;
ModdedEngineBootstrap.loader().fireDynamicLevelLoad(level);
- return handle;
+ return created;
} catch (Throwable error) {
- rollbackInjection(server, serverAccess, key, level, generator, loadEventStarted, error);
- if (error instanceof RuntimeException runtimeException) {
- throw runtimeException;
- }
- if (error instanceof Error fatalError) {
- throw fatalError;
- }
- throw new IllegalStateException("Iris runtime dimension injection failed for " + dimensionId, error);
+ rollbackInjection(server, serverAccess, created.dimensionId(), level, generator, loadEventStarted, error);
+ throw rethrow(created.dimensionId(), error);
}
}
+ private static RuntimeException rethrow(String dimensionId, Throwable error) {
+ if (error instanceof RuntimeException runtimeException) {
+ return runtimeException;
+ }
+ if (error instanceof Error fatalError) {
+ throw fatalError;
+ }
+ return new IllegalStateException("Iris runtime dimension injection failed for " + dimensionId, error);
+ }
+
private static void rollbackInjection(NativeModdedServer server, NativeDimensionRuntime serverAccess,
String key, NativeWorld level,
IrisModdedChunkGenerator generator, boolean loadEventStarted,
@@ -544,9 +574,30 @@ public final class ModdedDimensionManager {
}
}
+ @FunctionalInterface
+ private interface RuntimePackSource {
+ RuntimePack open();
+ }
+
+ private record DimensionRequest(String dimensionId, String pack, String packDimensionKey, long seed) {
+ private static DimensionRequest of(Handle handle) {
+ return new DimensionRequest(handle.dimensionId(), handle.pack(), handle.packDimensionKey(), handle.seed());
+ }
+
+ private Handle bind(NativeWorld level, IrisModdedChunkGenerator generator) {
+ return new Handle(dimensionId, pack, packDimensionKey, seed, level, generator);
+ }
+ }
+
public record Handle(String dimensionId, String pack, String packDimensionKey, long seed, NativeWorld level, IrisModdedChunkGenerator generator) {
}
+ record RestoringLevel(Handle handle, boolean present) {
+ RestoringLevel {
+ handle = Objects.requireNonNull(handle, "handle");
+ }
+ }
+
public record UpdateResult(
boolean pending,
boolean restartRequired,
diff --git a/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedDimensionMetadata.java b/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedDimensionMetadata.java
index 3fd14abab..009b2451d 100644
--- a/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedDimensionMetadata.java
+++ b/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedDimensionMetadata.java
@@ -28,6 +28,7 @@ import art.arcane.iris.generation.terrain.IrisDimension;
import art.arcane.iris.generation.terrain.IrisRegion;
import art.arcane.volmlib.nativelib.minecraft26_2.terrain.NativeResourceKeys;
+import java.io.File;
import java.util.LinkedHashSet;
import java.util.Locale;
import java.util.Set;
@@ -126,6 +127,20 @@ final class ModdedDimensionMetadata {
}
}
+ static void warmConfiguredPack(File packDirectory, String dimensionKey) {
+ ConfiguredPack configured = ConfiguredPack.load(packDirectory, dimensionKey);
+ collectConfiguredBiomeKeys(configured.dimension(), configured.data());
+ }
+
record ConfiguredPack(IrisData data, IrisDimension dimension, DimensionMetadata metadata) {
+ static ConfiguredPack load(File packDirectory, String dimensionKey) {
+ IrisData data = IrisData.get(packDirectory);
+ IrisDimension dimension = data.getDimensionLoader().load(dimensionKey);
+ if (dimension == null) {
+ throw new IllegalStateException("Iris dimension '" + dimensionKey
+ + "' missing from pack " + packDirectory.getAbsolutePath());
+ }
+ return new ConfiguredPack(data, dimension, dimensionMetadata(dimension));
+ }
}
}
diff --git a/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedDimensionRestore.java b/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedDimensionRestore.java
new file mode 100644
index 000000000..ec1d9c7e2
--- /dev/null
+++ b/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedDimensionRestore.java
@@ -0,0 +1,233 @@
+/*
+ * Iris is a World Generator for Minecraft Servers
+ * Copyright (c) 2026 Arcane Arts (Volmit Software)
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+package art.arcane.iris.modded;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Objects;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.CompletionException;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+final class ModdedDimensionRestore
implements AutoCloseable {
+ private static final int MINIMUM_WORKERS = 2;
+
+ private final List> preparations;
+ private final Steps steps;
+ private final ExecutorService workers;
+ private final AtomicBoolean finished = new AtomicBoolean();
+
+ private ModdedDimensionRestore(List> preparations, Steps steps, ExecutorService workers) {
+ this.preparations = preparations;
+ this.steps = steps;
+ this.workers = workers;
+ }
+
+ static
ModdedDimensionRestore
start(
+ List dimensions,
+ Steps steps
+ ) {
+ Steps
requiredSteps = Objects.requireNonNull(steps, "steps");
+ ExecutorService workers = Executors.newFixedThreadPool(workerCount(dimensions.size()),
+ Thread.ofPlatform().name("Iris Dimension Restore ", 1L).daemon(true).factory());
+ List> preparations = new ArrayList<>(dimensions.size());
+ for (ModdedDimensionRegistryStore.PersistentDimension dimension : dimensions) {
+ preparations.add(new Preparation<>(dimension, CompletableFuture.supplyAsync(
+ () -> timed(() -> requiredSteps.prepare(dimension)), workers)));
+ }
+ return new ModdedDimensionRestore<>(List.copyOf(preparations), requiredSteps, workers);
+ }
+
+ void awaitPreparations() {
+ List>> futures = new ArrayList<>(preparations.size());
+ for (Preparation preparation : preparations) {
+ futures.add(preparation.future());
+ }
+ CompletableFuture.allOf(futures.toArray(CompletableFuture[]::new))
+ .handle((Void ignored, Throwable failure) -> null)
+ .join();
+ }
+
+ List finish() {
+ if (!finished.compareAndSet(false, true)) {
+ throw new IllegalStateException("Iris dimension restore already finished");
+ }
+ try {
+ List> building = new ArrayList<>(preparations.size());
+ Result[] results = new Result[preparations.size()];
+ for (int index = 0; index < preparations.size(); index++) {
+ Preparation preparation = preparations.get(index);
+ Timed
prepared;
+ try {
+ prepared = join(preparation.future());
+ } catch (Throwable failure) {
+ results[index] = new Result(preparation.dimension(), failure, 0L, 0L);
+ continue;
+ }
+ L level;
+ try {
+ level = steps.begin(preparation.dimension(), prepared.value());
+ } catch (Throwable failure) {
+ discard(prepared.value(), failure);
+ results[index] = new Result(preparation.dimension(), failure, prepared.millis(), 0L);
+ continue;
+ }
+ CompletableFuture> engine = CompletableFuture.supplyAsync(
+ () -> timed(() -> steps.build(level, prepared.value())), workers);
+ building.add(new Building<>(index, preparation.dimension(), prepared.millis(), level, engine));
+ }
+ for (Building pending : building) {
+ results[pending.index()] = complete(pending);
+ }
+ return List.of(results);
+ } finally {
+ workers.shutdown();
+ }
+ }
+
+ @Override
+ public void close() {
+ if (finished.compareAndSet(false, true)) {
+ for (Preparation preparation : preparations) {
+ preparation.future().whenComplete((Timed
prepared, Throwable failure) -> {
+ if (prepared != null) {
+ discard(prepared.value(), null);
+ }
+ });
+ }
+ }
+ workers.shutdown();
+ }
+
+ private Result complete(Building pending) {
+ Timed engine;
+ try {
+ engine = join(pending.engine());
+ } catch (Throwable failure) {
+ abandon(pending.level(), failure);
+ return new Result(pending.dimension(), failure, pending.preparedMillis(), 0L);
+ }
+ try {
+ steps.complete(pending.level(), engine.value());
+ return new Result(pending.dimension(), null, pending.preparedMillis(), engine.millis());
+ } catch (Throwable failure) {
+ return new Result(pending.dimension(), failure, pending.preparedMillis(), engine.millis());
+ }
+ }
+
+ private void abandon(L level, Throwable failure) {
+ try {
+ steps.abandon(level, failure);
+ } catch (Throwable cleanupFailure) {
+ failure.addSuppressed(cleanupFailure);
+ }
+ }
+
+ private void discard(P prepared, Throwable failure) {
+ try {
+ steps.discard(prepared);
+ } catch (Throwable cleanupFailure) {
+ if (failure != null) {
+ failure.addSuppressed(cleanupFailure);
+ return;
+ }
+ ModdedIrisLog.error("Iris could not release an unused dimension restore preparation", cleanupFailure);
+ }
+ }
+
+ private static int workerCount(int dimensions) {
+ int processors = Runtime.getRuntime().availableProcessors();
+ return Math.max(1, Math.min(dimensions, Math.max(MINIMUM_WORKERS, processors / 2)));
+ }
+
+ private static Timed timed(Step step) {
+ long startedAt = System.nanoTime();
+ try {
+ T value = step.run();
+ return new Timed<>(value, (System.nanoTime() - startedAt) / 1_000_000L);
+ } catch (RuntimeException | Error failure) {
+ throw failure;
+ } catch (Exception failure) {
+ throw new CompletionException(failure);
+ }
+ }
+
+ private static T join(CompletableFuture future) throws Throwable {
+ try {
+ return future.get();
+ } catch (ExecutionException failure) {
+ Throwable cause = failure.getCause();
+ if (cause instanceof CompletionException wrapped && wrapped.getCause() != null) {
+ throw wrapped.getCause();
+ }
+ throw cause == null ? failure : cause;
+ } catch (InterruptedException interrupted) {
+ Thread.currentThread().interrupt();
+ throw new IllegalStateException("Interrupted while restoring Iris dimensions", interrupted);
+ }
+ }
+
+ interface Steps {
+ P prepare(ModdedDimensionRegistryStore.PersistentDimension dimension) throws Exception;
+
+ L begin(ModdedDimensionRegistryStore.PersistentDimension dimension, P prepared) throws Exception;
+
+ E build(L level, P prepared) throws Exception;
+
+ void complete(L level, E engine) throws Exception;
+
+ void abandon(L level, Throwable failure);
+
+ void discard(P prepared);
+ }
+
+ record Result(
+ ModdedDimensionRegistryStore.PersistentDimension dimension,
+ Throwable failure,
+ long preparedMillis,
+ long builtMillis
+ ) {
+ }
+
+ @FunctionalInterface
+ private interface Step {
+ T run() throws Exception;
+ }
+
+ private record Timed(T value, long millis) {
+ }
+
+ private record Preparation(
+ ModdedDimensionRegistryStore.PersistentDimension dimension,
+ CompletableFuture> future
+ ) {
+ }
+
+ private record Building(
+ int index,
+ ModdedDimensionRegistryStore.PersistentDimension dimension,
+ long preparedMillis,
+ L level,
+ CompletableFuture> engine
+ ) {
+ }
+}
diff --git a/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedEngineBootstrap.java b/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedEngineBootstrap.java
index 5818308ff..44dfb3681 100644
--- a/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedEngineBootstrap.java
+++ b/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedEngineBootstrap.java
@@ -145,6 +145,7 @@ public final class ModdedEngineBootstrap {
}
IrisModdedCommands.openDownloadAdmission();
ModdedStartup.prepareForStartup();
+ ModdedStartup.prepareDimensionRestore(server);
IrisModdedChunkGenerator.startGenPool();
bindWorldGenerators(server);
services().enableAll();
@@ -164,6 +165,7 @@ public final class ModdedEngineBootstrap {
finishPreviousServer("server start");
captureInitialSpawn(server);
ModdedStartup.prepareForStartup();
+ ModdedStartup.prepareDimensionRestore(server);
}
public static void serverStarted(NativeModdedServer server) {
diff --git a/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedGenerationHistoryStorage.java b/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedGenerationHistoryStorage.java
index c4cd571e5..4ce134029 100644
--- a/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedGenerationHistoryStorage.java
+++ b/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedGenerationHistoryStorage.java
@@ -9,6 +9,7 @@ import art.arcane.iris.pack.BrokenPackException;
import art.arcane.iris.pack.PackFingerprints;
import art.arcane.iris.pack.PackValidationRegistry;
import art.arcane.iris.pack.PackValidationResult;
+import art.arcane.iris.world.history.GenerationAdmission;
import art.arcane.iris.world.history.GenerationEpoch;
import art.arcane.iris.world.history.GenerationEpochContractFactory;
import art.arcane.iris.world.history.GenerationHistory;
@@ -53,7 +54,7 @@ final class ModdedGenerationHistoryStorage {
ModdedGenerationHistoryStorage::requireRegistryDefinitions,
IrisSettings.get().getGenerator().getGenerationTransitionWidthBlocks()
);
- return resolveActive(history);
+ return resolveActive(history, source);
} catch (IOException failure) {
throw historyFailure(levelKey, failure);
}
@@ -68,25 +69,57 @@ final class ModdedGenerationHistoryStorage {
) {
Path dimensionRoot = dimensionRoot(server, levelKey);
try {
- GenerationHistory history = restoreOrAdopt(
- dimensionRoot,
- worldSeed,
- () -> {
- Path source = selectedPackSource(selectedPack, selectedDimensionKey);
- return new AdoptionSource(source, captureCandidate(
- source,
- selectedDimensionKey,
- adoptionAliasPolicy(dimensionRoot)
- ));
- },
- ModdedGenerationHistoryStorage::requireRegistryDefinitions
- );
- return resolveActive(history);
+ GenerationHistory history = openForRestore(dimensionRoot, selectedPack, selectedDimensionKey, worldSeed);
+ return resolveActive(history, installedPackSource(selectedPack));
} catch (IOException failure) {
throw historyFailure(levelKey, failure);
}
}
+ static PreparedHistory prepareRestore(
+ NativeModdedServer server,
+ String levelKey,
+ String selectedPack,
+ String selectedDimensionKey,
+ long worldSeed
+ ) {
+ 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;
+ }
+ } catch (IOException failure) {
+ throw historyFailure(levelKey, failure);
+ }
+ }
+
+ private static GenerationHistory openForRestore(
+ Path dimensionRoot,
+ String selectedPack,
+ String selectedDimensionKey,
+ long worldSeed
+ ) throws IOException {
+ return restoreOrAdopt(
+ dimensionRoot,
+ worldSeed,
+ () -> {
+ Path source = selectedPackSource(selectedPack, selectedDimensionKey);
+ return new AdoptionSource(source, captureCandidate(
+ source,
+ selectedDimensionKey,
+ adoptionAliasPolicy(dimensionRoot)
+ ));
+ },
+ ModdedGenerationHistoryStorage::requireRegistryDefinitions
+ );
+ }
+
static ActivePack openOrAdopt(
NativeWorld level,
String selectedPack,
@@ -184,10 +217,10 @@ final class ModdedGenerationHistoryStorage {
}
}
- static ActivePack resolveActive(GenerationHistory history) throws IOException {
+ static ActivePack resolveActive(GenerationHistory history, Path validatedSource) throws IOException {
GenerationEpoch epoch = history.activeEpoch();
Path packRoot = normalize(history.activePackRoot());
- validatePack(packRoot);
+ validatePack(packRoot, validatedSource);
validateDimension(packRoot, epoch);
return new ActivePack(
packRoot,
@@ -197,9 +230,12 @@ final class ModdedGenerationHistoryStorage {
);
}
- static PackValidationResult validatePack(Path packRoot) {
- PackValidationResult validation = PackValidationRegistry.getMatching(
- packRoot, PackFingerprints.computePackTreeFingerprint(packRoot.toFile()));
+ static PackValidationResult validatePack(Path packRoot, Path validatedSource) {
+ String contentFingerprint = PackFingerprints.computePackTreeFingerprint(packRoot.toFile());
+ PackValidationResult validation = PackValidationRegistry.getMatching(packRoot, contentFingerprint);
+ if (validation == null && validatedSource != null) {
+ validation = PackValidationRegistry.publishMatchingCopy(validatedSource, packRoot, contentFingerprint);
+ }
if (validation == null) {
validation = ModdedStartup.validatePack(packRoot.toFile());
}
@@ -286,6 +322,11 @@ final class ModdedGenerationHistoryStorage {
return normalize(ModdedWorldEngines.resolvePack(pack, dimensionKey).toPath());
}
+ private static Path installedPackSource(String pack) {
+ File folder = ModdedWorldEngines.packFolder(pack);
+ return folder.isDirectory() ? normalize(folder.toPath()) : null;
+ }
+
private static GenerationRegistryContract requireRegistryDefinitions(
Collection contracts
)
@@ -346,6 +387,13 @@ final class ModdedGenerationHistoryStorage {
}
}
+ record PreparedHistory(ActivePack active, GenerationAdmission.RuntimeLease lease) {
+ PreparedHistory {
+ active = Objects.requireNonNull(active, "active");
+ lease = Objects.requireNonNull(lease, "lease");
+ }
+ }
+
record Candidate(
String packFingerprint,
GenerationEpoch.DimensionContract dimensionContract,
diff --git a/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedStartup.java b/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedStartup.java
index 0cd0ef925..a8d0bf13b 100644
--- a/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedStartup.java
+++ b/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedStartup.java
@@ -27,6 +27,7 @@ import art.arcane.iris.pack.PackValidationRegistry;
import art.arcane.iris.pack.PackValidationCache;
import art.arcane.iris.pack.PackValidationResult;
import art.arcane.iris.pack.PackValidator;
+import art.arcane.iris.generation.runtime.Engine;
import art.arcane.iris.modded.command.ModdedPackCommands;
import art.arcane.iris.spi.IrisPlatforms;
import art.arcane.iris.world.safeguard.GenerationRefusalNotice;
@@ -39,19 +40,29 @@ import java.util.List;
import java.util.Map;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicReference;
public final class ModdedStartup {
private static final int COMPAT_BOOT_KEY_CAP = 3;
private static final AtomicBoolean PREPARED = new AtomicBoolean(false);
private static final AtomicBoolean STARTED = new AtomicBoolean(false);
+ private static final AtomicBoolean RESTORE_BEGUN = new AtomicBoolean(false);
+ private static final AtomicReference> RESTORE =
+ new AtomicReference<>();
private static final List DIMENSION_FAILURES = new CopyOnWriteArrayList<>();
private ModdedStartup() {
}
public static void reset() {
+ ModdedDimensionRestore restore =
+ RESTORE.getAndSet(null);
+ if (restore != null) {
+ restore.close();
+ }
PREPARED.set(false);
STARTED.set(false);
+ RESTORE_BEGUN.set(false);
DIMENSION_FAILURES.clear();
}
@@ -62,6 +73,24 @@ public final class ModdedStartup {
validateAllPacks();
}
+ public static void prepareDimensionRestore(NativeModdedServer server) {
+ if (server == null || !RESTORE_BEGUN.compareAndSet(false, true)) {
+ return;
+ }
+ List dimensions =
+ ModdedDimensionRegistryStore.loadForStartup(server);
+ if (dimensions.isEmpty()) {
+ return;
+ }
+ long startedAt = System.currentTimeMillis();
+ ModdedDimensionRestore restore =
+ ModdedDimensionRestore.start(dimensions, new PersistentRestoreSteps(server));
+ RESTORE.set(restore);
+ restore.awaitPreparations();
+ ModdedIrisLog.info("Iris prepared {} persistent dimension(s) during world load in {}ms",
+ dimensions.size(), System.currentTimeMillis() - startedAt);
+ }
+
/**
* Boot trigger for the forced datapack. Runs on its own daemon thread rather than the Iris scheduler:
* ModdedEngineBootstrap.start clears the async queue at SERVER_STARTING, which would silently drop this
@@ -219,38 +248,46 @@ public final class ModdedStartup {
}
static void reinjectPersistentDimensions(NativeModdedServer server) {
- List dimensions =
- ModdedDimensionRegistryStore.loadForStartup(server);
- if (dimensions.isEmpty()) {
+ prepareDimensionRestore(server);
+ ModdedDimensionRestore restore =
+ RESTORE.getAndSet(null);
+ if (restore == null) {
return;
}
+ finishDimensionRestore(restore);
+ }
+
+ static void finishDimensionRestore(ModdedDimensionRestore, ?, ?> restore) {
+ long startedAt = System.currentTimeMillis();
+ List results;
+ try (restore) {
+ results = restore.finish();
+ }
+ long waited = System.currentTimeMillis() - startedAt;
int injected = 0;
int index = 0;
- long startedAt = System.currentTimeMillis();
- for (ModdedDimensionRegistryStore.PersistentDimension dimension : dimensions) {
+ OutOfMemoryError outOfMemory = null;
+ for (ModdedDimensionRestore.Result result : results) {
index++;
- long dimensionStartedAt = System.currentTimeMillis();
- try {
- ModdedDimensionManager.restorePersistent(
- server,
- dimension.id(),
- dimension.pack(),
- dimension.dimension(),
- dimension.seed()
- );
+ ModdedDimensionRegistryStore.PersistentDimension dimension = result.dimension();
+ Throwable failure = result.failure();
+ if (failure == null) {
injected++;
- ModdedIrisLog.info("Iris re-injected {}/{} '{}' (pack={} dim={}) in {}ms",
- index, dimensions.size(), dimension.id(), dimension.pack(), dimension.dimension(),
- System.currentTimeMillis() - dimensionStartedAt);
- } catch (Throwable e) {
- reportDimensionFailure(dimension, e);
- if (e instanceof OutOfMemoryError outOfMemory) {
- throw outOfMemory;
- }
+ ModdedIrisLog.info("Iris re-injected {}/{} '{}' (pack={} dim={}): prepared {}ms, engine {}ms",
+ index, results.size(), dimension.id(), dimension.pack(), dimension.dimension(),
+ result.preparedMillis(), result.builtMillis());
+ continue;
+ }
+ reportDimensionFailure(dimension, failure);
+ if (failure instanceof OutOfMemoryError fatal && outOfMemory == null) {
+ outOfMemory = fatal;
}
}
- ModdedIrisLog.info("Iris re-injected {}/{} persistent dimension(s) at startup in {}ms",
- injected, dimensions.size(), System.currentTimeMillis() - startedAt);
+ ModdedIrisLog.info("Iris re-injected {}/{} persistent dimension(s) at startup; server thread waited {}ms",
+ injected, results.size(), waited);
+ if (outOfMemory != null) {
+ throw outOfMemory;
+ }
}
private static void reportDimensionFailure(ModdedDimensionRegistryStore.PersistentDimension dimension, Throwable failure) {
@@ -304,4 +341,43 @@ public final class ModdedStartup {
}
}
+ private record PersistentRestoreSteps(NativeModdedServer server)
+ implements ModdedDimensionRestore.Steps {
+ @Override
+ public ModdedWorldEngines.PreparedPack prepare(ModdedDimensionRegistryStore.PersistentDimension dimension) {
+ return ModdedWorldEngines.preparePersistent(server, dimension);
+ }
+
+ @Override
+ public ModdedDimensionManager.RestoringLevel begin(
+ ModdedDimensionRegistryStore.PersistentDimension dimension,
+ ModdedWorldEngines.PreparedPack prepared
+ ) {
+ return ModdedDimensionManager.beginRestore(server, dimension, prepared.active());
+ }
+
+ @Override
+ public Engine build(ModdedDimensionManager.RestoringLevel 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.RestoringLevel level, Engine engine) {
+ ModdedDimensionManager.completeRestore(server, level, engine);
+ }
+
+ @Override
+ public void abandon(ModdedDimensionManager.RestoringLevel level, Throwable failure) {
+ ModdedDimensionManager.abandonRestore(server, level, failure);
+ }
+
+ @Override
+ public void discard(ModdedWorldEngines.PreparedPack prepared) {
+ prepared.close();
+ }
+ }
}
diff --git a/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedWorldEngines.java b/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedWorldEngines.java
index 9bf710cf1..4b2a7e375 100644
--- a/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedWorldEngines.java
+++ b/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedWorldEngines.java
@@ -31,6 +31,7 @@ import art.arcane.iris.generation.runtime.IrisEngine;
import art.arcane.iris.generation.runtime.Engine;
import art.arcane.iris.generation.runtime.EngineTarget;
import art.arcane.iris.world.history.GenerationActivation;
+import art.arcane.iris.world.history.GenerationAdmission;
import art.arcane.iris.world.history.GenerationEpoch;
import art.arcane.iris.world.history.GenerationEpochContractFactory;
import art.arcane.iris.world.history.GenerationHistory;
@@ -128,6 +129,43 @@ public final class ModdedWorldEngines {
});
}
+ static PreparedPack preparePersistent(
+ NativeModdedServer server,
+ ModdedDimensionRegistryStore.PersistentDimension dimension
+ ) {
+ ModdedEngineBootstrap.bind();
+ ModdedGenerationHistoryStorage.PreparedHistory prepared = ModdedGenerationHistoryStorage.prepareRestore(
+ server,
+ dimension.id(),
+ dimension.pack(),
+ dimension.dimension(),
+ dimension.seed()
+ );
+ try {
+ ModdedDimensionMetadata.warmConfiguredPack(prepared.active().packRoot().toFile(), prepared.active().dimensionKey());
+ return new PreparedPack(prepared.active(), prepared.lease(), loadPack(prepared.active()));
+ } catch (RuntimeException | Error failure) {
+ prepared.lease().close();
+ throw failure;
+ }
+ }
+
+ static Engine createPrepared(NativeWorld level, long seedOverride, PreparedPack prepared) {
+ ModdedEngineBootstrap.bind();
+ long seed = seedOverride == Long.MIN_VALUE ? level.seed() : seedOverride;
+ try (prepared) {
+ return attachHistoryEngine(level, seed, prepared.active(), prepared.take());
+ } catch (IOException failure) {
+ throw new IllegalStateException("Iris generation history could not initialize runtime routing for '"
+ + level.name() + "'.", failure);
+ }
+ }
+
+ static void installPrepared(NativeWorld level, Engine engine) {
+ applyWorldBoundary(level, engine);
+ installReplacement(level, engine);
+ }
+
static void closeUnregistered(Engine engine) {
close(engine);
ENGINES.entrySet().removeIf((Map.Entry entry) -> entry.getValue() == engine);
@@ -142,9 +180,13 @@ public final class ModdedWorldEngines {
ModdedGenerationMode generationMode
) {
Engine engine = create(level, pack, dimensionKey, seedOverride, generationMode);
+ applyWorldBoundary(level, engine);
+ return engine;
+ }
+
+ private static void applyWorldBoundary(NativeWorld level, Engine engine) {
try {
engine.getPlatformHooks().applyWorldBoundary(engine);
- return engine;
} catch (Throwable failure) {
try {
close(engine);
@@ -197,57 +239,63 @@ public final class ModdedWorldEngines {
GenerationHistory history = opened.history();
try (GenerationAdmission.RuntimeLease startupAdmission = history.retainRuntime()) {
history.prepareCurrentGenerator(IrisSettings.get().getGenerator().getGenerationTransitionWidthBlocks());
- opened = ModdedGenerationHistoryStorage.resolveActive(history);
- long openedActivationId = history.activeActivation().activationId();
- IrisEngine candidate = buildEngine(level, seed, opened);
- boolean ready = false;
- try {
- GenerationHistoryRuntimeRouter router = candidate.attachGenerationHistory(
+ ModdedGenerationHistoryStorage.ActivePack active = ModdedGenerationHistoryStorage.resolveActive(history, null);
+ return attachHistoryEngine(level, seed, active, loadPack(active));
+ }
+ }
+
+ private static IrisEngine attachHistoryEngine(
+ NativeWorld level,
+ long seed,
+ ModdedGenerationHistoryStorage.ActivePack active,
+ LoadedPack loaded
+ ) throws IOException {
+ GenerationHistory history = active.history();
+ long openedActivationId = history.activeActivation().activationId();
+ IrisEngine candidate = buildEngine(level, seed, active, loaded);
+ boolean ready = false;
+ try {
+ GenerationHistoryRuntimeRouter router = candidate.attachGenerationHistory(
+ history,
+ IrisBoundarySignatureSampler.INSTANCE,
+ IrisSettings.get().getGenerator().getGenerationTransitionWidthBlocks()
+ );
+ router.preloadActiveRuntimes();
+ if (history.activeActivation().activationId() != openedActivationId) {
+ close(candidate);
+ ModdedGenerationHistoryStorage.ActivePack promoted =
+ ModdedGenerationHistoryStorage.resolveActive(history, null);
+ candidate = buildEngine(level, seed, promoted, loadPack(promoted));
+ router = GenerationHistoryRuntimeRouter.attach(
+ candidate,
history,
- IrisBoundarySignatureSampler.INSTANCE,
- IrisSettings.get().getGenerator().getGenerationTransitionWidthBlocks()
+ IrisBoundarySignatureSampler.INSTANCE
);
router.preloadActiveRuntimes();
- if (history.activeActivation().activationId() != openedActivationId) {
- close(candidate);
- ModdedGenerationHistoryStorage.ActivePack promoted =
- ModdedGenerationHistoryStorage.resolveActive(history);
- candidate = buildEngine(level, seed, promoted);
- router = GenerationHistoryRuntimeRouter.attach(
- candidate,
- history,
- IrisBoundarySignatureSampler.INSTANCE
- );
- router.preloadActiveRuntimes();
- }
- requireReady(candidate, level, ModdedGenerationMode.PERSISTENT_RESTORE);
- ready = true;
- ModdedIrisLog.info("Iris engine up for {}: pack={} dim={} seed={} height={}..{} activation={}",
- level.name(),
- candidate.getData().getDataFolder().getAbsolutePath(),
- candidate.getDimension().getLoadKey(),
- seed,
- candidate.getDimension().getMinHeight(),
- candidate.getDimension().getMaxHeight(),
- history.activeActivation().activationId());
- return candidate;
- } finally {
- if (!ready) {
- close(candidate);
- }
+ }
+ requireReady(candidate, level, ModdedGenerationMode.PERSISTENT_RESTORE);
+ ready = true;
+ ModdedIrisLog.info("Iris engine up for {}: pack={} dim={} seed={} height={}..{} activation={}",
+ level.name(),
+ candidate.getData().getDataFolder().getAbsolutePath(),
+ candidate.getDimension().getLoadKey(),
+ seed,
+ candidate.getDimension().getMinHeight(),
+ candidate.getDimension().getMaxHeight(),
+ history.activeActivation().activationId());
+ return candidate;
+ } finally {
+ if (!ready) {
+ close(candidate);
}
}
}
- private static IrisEngine buildEngine(
- NativeWorld level,
- long seed,
- ModdedGenerationHistoryStorage.ActivePack active
- ) throws IOException {
+ private static LoadedPack loadPack(ModdedGenerationHistoryStorage.ActivePack active) {
File packDir = active.packRoot().toFile();
PackValidationRegistry.requireLoadable(active.packRoot());
IrisData data = IrisData.openRuntime(packDir);
- boolean runtimeOwnsData = false;
+ boolean loaded = false;
try {
IrisDimension dimension = data.getDimensionLoader().load(active.dimensionKey());
if (dimension == null) {
@@ -256,7 +304,26 @@ public final class ModdedWorldEngines {
throw new IllegalStateException("Iris dimension '" + active.dimensionKey()
+ "' missing from immutable pack " + packDir.getAbsolutePath());
}
+ data.generationPackFingerprint();
+ loaded = true;
+ return new LoadedPack(data, dimension);
+ } finally {
+ if (!loaded) {
+ data.close();
+ }
+ }
+ }
+ private static IrisEngine buildEngine(
+ NativeWorld level,
+ long seed,
+ ModdedGenerationHistoryStorage.ActivePack active,
+ LoadedPack loaded
+ ) throws IOException {
+ IrisData data = loaded.data();
+ IrisDimension dimension = loaded.dimension();
+ boolean runtimeOwnsData = false;
+ try {
validateDimensionContract(active.dimensionContract(), dimension, level);
File worldFolder = NativeModdedServer.forWorld(level).dimensionFolder(level.name()).toFile();
IrisWorld world = buildWorld(level, seed, dimension, worldFolder);
@@ -428,6 +495,55 @@ public final class ModdedWorldEngines {
}
}
+ private record LoadedPack(IrisData data, IrisDimension dimension) {
+ private LoadedPack {
+ data = Objects.requireNonNull(data, "data");
+ dimension = Objects.requireNonNull(dimension, "dimension");
+ }
+ }
+
+ static final class PreparedPack implements AutoCloseable {
+ private final ModdedGenerationHistoryStorage.ActivePack active;
+ private final GenerationAdmission.RuntimeLease lease;
+ private LoadedPack loaded;
+
+ private PreparedPack(
+ ModdedGenerationHistoryStorage.ActivePack active,
+ GenerationAdmission.RuntimeLease lease,
+ LoadedPack loaded
+ ) {
+ this.active = Objects.requireNonNull(active, "active");
+ this.lease = Objects.requireNonNull(lease, "lease");
+ this.loaded = Objects.requireNonNull(loaded, "loaded");
+ }
+
+ ModdedGenerationHistoryStorage.ActivePack active() {
+ return active;
+ }
+
+ private synchronized LoadedPack take() {
+ LoadedPack taken = loaded;
+ if (taken == null) {
+ throw new IllegalStateException("Iris prepared pack for " + active.packRoot() + " was already used");
+ }
+ loaded = null;
+ return taken;
+ }
+
+ @Override
+ public synchronized void close() {
+ LoadedPack unused = loaded;
+ loaded = null;
+ try {
+ if (unused != null) {
+ unused.data().close();
+ }
+ } finally {
+ lease.close();
+ }
+ }
+ }
+
public static void shutdown() {
Throwable failure = null;
for (Map.Entry entry : new ArrayList<>(ENGINES.entrySet())) {
diff --git a/adapters/modded-common/src/test/java/art/arcane/iris/modded/ModdedDimensionRestoreTest.java b/adapters/modded-common/src/test/java/art/arcane/iris/modded/ModdedDimensionRestoreTest.java
new file mode 100644
index 000000000..55755744f
--- /dev/null
+++ b/adapters/modded-common/src/test/java/art/arcane/iris/modded/ModdedDimensionRestoreTest.java
@@ -0,0 +1,276 @@
+package art.arcane.iris.modded;
+
+import org.junit.Test;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertSame;
+import static org.junit.Assert.assertTrue;
+
+public class ModdedDimensionRestoreTest {
+ private static final long STEP_TIMEOUT_SECONDS = 10L;
+
+ @Test
+ public void everyDimensionPreparesConcurrentlyOffTheCallingThread() {
+ List dimensions = dimensions(4);
+ RecordingSteps steps = new RecordingSteps(dimensions.size());
+ steps.prepareBarrier = new CountDownLatch(dimensions.size());
+
+ List results;
+ try (ModdedDimensionRestore restore = ModdedDimensionRestore.start(dimensions, steps)) {
+ results = restore.finish();
+ }
+
+ assertEquals(dimensions.size(), results.size());
+ for (ModdedDimensionRestore.Result result : results) {
+ assertNull(result.failure());
+ }
+ assertFalse(steps.prepareThreads.contains(Thread.currentThread()));
+ }
+
+ @Test
+ public void engineBuildsRunConcurrentlyWhileLevelMutationStaysOnTheCallingThread() {
+ List dimensions = dimensions(3);
+ RecordingSteps steps = new RecordingSteps(dimensions.size());
+ steps.buildBarrier = new CountDownLatch(dimensions.size());
+
+ try (ModdedDimensionRestore restore = ModdedDimensionRestore.start(dimensions, steps)) {
+ restore.finish();
+ }
+
+ assertFalse(steps.buildThreads.contains(Thread.currentThread()));
+ assertEquals(Set.of(Thread.currentThread()), steps.serverThreads);
+ assertEquals(List.of("level-0:engine-0", "level-1:engine-1", "level-2:engine-2"), steps.completed);
+ }
+
+ @Test
+ public void aFailedPreparationIsReportedWhileTheOtherDimensionsStillRestore() {
+ List dimensions = dimensions(3);
+ RecordingSteps steps = new RecordingSteps(dimensions.size());
+ IllegalStateException failure = new IllegalStateException("generation history is unusable");
+ steps.prepareFailure = failure;
+ steps.failingIndex = 1;
+
+ List results;
+ try (ModdedDimensionRestore restore = ModdedDimensionRestore.start(dimensions, steps)) {
+ results = restore.finish();
+ }
+
+ assertNull(results.get(0).failure());
+ assertSame(failure, results.get(1).failure());
+ assertNull(results.get(2).failure());
+ assertSame(dimensions.get(1), results.get(1).dimension());
+ assertEquals(List.of("level-0:engine-0", "level-2:engine-2"), steps.completed);
+ assertTrue(steps.begun.stream().noneMatch((String level) -> level.equals("level-1")));
+ }
+
+ @Test
+ public void aFailedEngineBuildAbandonsItsLevelWithoutCompletingIt() {
+ List dimensions = dimensions(2);
+ RecordingSteps steps = new RecordingSteps(dimensions.size());
+ IllegalStateException failure = new IllegalStateException("engine refused");
+ steps.buildFailure = failure;
+ steps.failingIndex = 0;
+
+ List results;
+ try (ModdedDimensionRestore restore = ModdedDimensionRestore.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-1:engine-1"), steps.completed);
+ }
+
+ @Test
+ public void aLevelThatCannotBeCreatedDiscardsItsPreparation() {
+ List dimensions = dimensions(2);
+ RecordingSteps steps = new RecordingSteps(dimensions.size());
+ IllegalStateException failure = new IllegalStateException("dimension type missing");
+ steps.beginFailure = failure;
+ steps.failingIndex = 1;
+
+ List results;
+ try (ModdedDimensionRestore restore = ModdedDimensionRestore.start(dimensions, steps)) {
+ results = restore.finish();
+ }
+
+ assertSame(failure, results.get(1).failure());
+ assertEquals(List.of("prepared-1"), steps.discarded);
+ assertEquals(List.of("level-0:engine-0"), steps.completed);
+ }
+
+ @Test
+ public void awaitingPreparationsReturnsOnlyOnceEveryDimensionIsPreparedAndCreatesNoLevel() {
+ List dimensions = dimensions(3);
+ RecordingSteps steps = new RecordingSteps(dimensions.size());
+ CountDownLatch release = new CountDownLatch(1);
+ steps.prepareGate = release;
+ Thread.ofPlatform().daemon(true).start(() -> {
+ try {
+ Thread.sleep(200L);
+ } catch (InterruptedException interrupted) {
+ Thread.currentThread().interrupt();
+ }
+ release.countDown();
+ });
+
+ try (ModdedDimensionRestore restore = ModdedDimensionRestore.start(dimensions, steps)) {
+ restore.awaitPreparations();
+
+ assertEquals(dimensions.size(), steps.prepared.get());
+ assertTrue(steps.begun.isEmpty());
+ restore.finish();
+ }
+
+ assertEquals(List.of("level-0:engine-0", "level-1:engine-1", "level-2:engine-2"), steps.completed);
+ assertEquals(0L, release.getCount());
+ }
+
+ @Test
+ public void closingAnUnfinishedRestoreDiscardsEveryPreparation() throws InterruptedException {
+ List dimensions = dimensions(3);
+ RecordingSteps steps = new RecordingSteps(dimensions.size());
+
+ ModdedDimensionRestore restore = ModdedDimensionRestore.start(dimensions, steps);
+ restore.close();
+
+ assertTrue(steps.discardedAll.await(STEP_TIMEOUT_SECONDS, TimeUnit.SECONDS));
+ assertTrue(steps.begun.isEmpty());
+ assertEquals(Set.of("prepared-0", "prepared-1", "prepared-2"), Set.copyOf(steps.discarded));
+ }
+
+ private static List dimensions(int count) {
+ List dimensions = new ArrayList<>(count);
+ for (int index = 0; index < count; index++) {
+ dimensions.add(new ModdedDimensionRegistryStore.PersistentDimension(
+ "iris:world" + index, "overworld", "overworld", 1000L + index));
+ }
+ return dimensions;
+ }
+
+ private static int index(String value) {
+ return Integer.parseInt(value.substring(value.lastIndexOf('-') + 1));
+ }
+
+ private static int index(ModdedDimensionRegistryStore.PersistentDimension dimension) {
+ return Integer.parseInt(dimension.id().substring("iris:world".length()));
+ }
+
+ private static void await(CountDownLatch barrier) {
+ if (barrier == null) {
+ return;
+ }
+ barrier.countDown();
+ try {
+ if (!barrier.await(STEP_TIMEOUT_SECONDS, TimeUnit.SECONDS)) {
+ throw new IllegalStateException("Restore steps did not run concurrently");
+ }
+ } catch (InterruptedException interrupted) {
+ Thread.currentThread().interrupt();
+ throw new IllegalStateException("Interrupted while waiting for concurrent restore steps", interrupted);
+ }
+ }
+
+ private static void awaitGate(CountDownLatch gate) {
+ if (gate == null) {
+ return;
+ }
+ try {
+ if (!gate.await(STEP_TIMEOUT_SECONDS, TimeUnit.SECONDS)) {
+ throw new IllegalStateException("Preparation gate never opened");
+ }
+ } catch (InterruptedException interrupted) {
+ Thread.currentThread().interrupt();
+ throw new IllegalStateException("Interrupted while waiting for the preparation gate", interrupted);
+ }
+ }
+
+ private static final class RecordingSteps implements ModdedDimensionRestore.Steps {
+ private final Set prepareThreads = ConcurrentHashMap.newKeySet();
+ private final Set buildThreads = ConcurrentHashMap.newKeySet();
+ private final Set serverThreads = ConcurrentHashMap.newKeySet();
+ private final List begun = Collections.synchronizedList(new ArrayList<>());
+ private final List completed = Collections.synchronizedList(new ArrayList<>());
+ private final List abandoned = Collections.synchronizedList(new ArrayList<>());
+ private final List discarded = Collections.synchronizedList(new ArrayList<>());
+ private final CountDownLatch discardedAll;
+ private final AtomicInteger prepared = new AtomicInteger();
+ private CountDownLatch prepareGate;
+ private CountDownLatch prepareBarrier;
+ private CountDownLatch buildBarrier;
+ private RuntimeException prepareFailure;
+ private RuntimeException beginFailure;
+ private RuntimeException buildFailure;
+ private int failingIndex = -1;
+
+ private RecordingSteps(int dimensions) {
+ this.discardedAll = new CountDownLatch(dimensions);
+ }
+
+ @Override
+ public String prepare(ModdedDimensionRegistryStore.PersistentDimension dimension) {
+ prepareThreads.add(Thread.currentThread());
+ await(prepareBarrier);
+ awaitGate(prepareGate);
+ int index = index(dimension);
+ if (prepareFailure != null && index == failingIndex) {
+ throw prepareFailure;
+ }
+ prepared.incrementAndGet();
+ return "prepared-" + index;
+ }
+
+ @Override
+ public String begin(ModdedDimensionRegistryStore.PersistentDimension dimension, String prepared) {
+ serverThreads.add(Thread.currentThread());
+ int index = index(dimension);
+ if (beginFailure != null && index == failingIndex) {
+ throw beginFailure;
+ }
+ String level = "level-" + index;
+ begun.add(level);
+ return level;
+ }
+
+ @Override
+ public String build(String level, String prepared) {
+ buildThreads.add(Thread.currentThread());
+ await(buildBarrier);
+ int index = index(level);
+ if (buildFailure != null && index == failingIndex) {
+ throw buildFailure;
+ }
+ return "engine-" + index;
+ }
+
+ @Override
+ public void complete(String level, String engine) {
+ serverThreads.add(Thread.currentThread());
+ completed.add(level + ":" + engine);
+ }
+
+ @Override
+ public void abandon(String level, Throwable failure) {
+ serverThreads.add(Thread.currentThread());
+ abandoned.add(level);
+ }
+
+ @Override
+ public void discard(String prepared) {
+ discarded.add(prepared);
+ discardedAll.countDown();
+ }
+ }
+}
diff --git a/adapters/modded-common/src/test/java/art/arcane/iris/modded/ModdedPackValidationPublicationTest.java b/adapters/modded-common/src/test/java/art/arcane/iris/modded/ModdedPackValidationPublicationTest.java
index 5d1b120ca..03d146a34 100644
--- a/adapters/modded-common/src/test/java/art/arcane/iris/modded/ModdedPackValidationPublicationTest.java
+++ b/adapters/modded-common/src/test/java/art/arcane/iris/modded/ModdedPackValidationPublicationTest.java
@@ -24,6 +24,7 @@ import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.fail;
import static org.mockito.Mockito.mockStatic;
+import static org.mockito.Mockito.never;
import static org.mockito.Mockito.times;
public class ModdedPackValidationPublicationTest {
@@ -41,8 +42,8 @@ public class ModdedPackValidationPublicationTest {
PackValidationResult result = loadable(root);
try (MockedStatic validator = mockStatic(PackValidator.class)) {
validator.when(() -> PackValidator.validate(root)).thenReturn(result);
- assertSame(result, ModdedGenerationHistoryStorage.validatePack(root.toPath()));
- assertSame(result, ModdedGenerationHistoryStorage.validatePack(root.toPath()));
+ assertSame(result, ModdedGenerationHistoryStorage.validatePack(root.toPath(), null));
+ assertSame(result, ModdedGenerationHistoryStorage.validatePack(root.toPath(), null));
validator.verify(() -> PackValidator.validate(root), times(1));
}
}
@@ -56,13 +57,13 @@ public class ModdedPackValidationPublicationTest {
PackValidationResult broken = new PackValidationResult(root.getName(), List.of("invalid"), List.of(), 1L);
try (MockedStatic validator = mockStatic(PackValidator.class)) {
validator.when(() -> PackValidator.validate(root)).thenReturn(loadable(root), broken);
- ModdedGenerationHistoryStorage.validatePack(root.toPath());
+ ModdedGenerationHistoryStorage.validatePack(root.toPath(), null);
Files.writeString(content.toPath(), "wrong");
Files.setLastModifiedTime(content.toPath(), timestamp);
assertThrows(BrokenPackException.class,
- () -> ModdedGenerationHistoryStorage.validatePack(root.toPath()));
+ () -> ModdedGenerationHistoryStorage.validatePack(root.toPath(), null));
assertThrows(BrokenPackException.class,
- () -> ModdedGenerationHistoryStorage.validatePack(root.toPath()));
+ () -> ModdedGenerationHistoryStorage.validatePack(root.toPath(), null));
validator.verify(() -> PackValidator.validate(root), times(2));
}
}
@@ -77,9 +78,9 @@ public class ModdedPackValidationPublicationTest {
MockedStatic cache = mockStatic(PackValidationCache.class)) {
cache.when(PackValidationCache::contextFingerprint).thenAnswer(ignored -> context.get());
validator.when(() -> PackValidator.validate(root)).thenReturn(first, second);
- assertSame(first, ModdedGenerationHistoryStorage.validatePack(root.toPath()));
+ assertSame(first, ModdedGenerationHistoryStorage.validatePack(root.toPath(), null));
context.set("second");
- assertSame(second, ModdedGenerationHistoryStorage.validatePack(root.toPath()));
+ assertSame(second, ModdedGenerationHistoryStorage.validatePack(root.toPath(), null));
validator.verify(() -> PackValidator.validate(root), times(2));
}
}
@@ -189,6 +190,42 @@ public class ModdedPackValidationPublicationTest {
}
}
+ @Test
+ public void snapshotReusesTheValidationOfAnIdenticalValidatedSource() throws Exception {
+ File source = temporaryFolder.newFolder("overworld");
+ Files.writeString(new File(source, "dimension.json").toPath(), "terrain");
+ File snapshot = temporaryFolder.newFolder("epoch", "pack");
+ Files.writeString(new File(snapshot, "dimension.json").toPath(), "terrain");
+ PackValidationResult sourceResult = loadable(source);
+ try (MockedStatic validator = mockStatic(PackValidator.class)) {
+ validator.when(() -> PackValidator.validate(source)).thenReturn(sourceResult);
+ ModdedStartup.validatePack(source);
+
+ assertSame(sourceResult, ModdedGenerationHistoryStorage.validatePack(snapshot.toPath(), source.toPath()));
+
+ validator.verify(() -> PackValidator.validate(snapshot), never());
+ assertSame(sourceResult, PackValidationRegistry.requireLoadable(snapshot.toPath()));
+ }
+ }
+
+ @Test
+ public void snapshotThatDiffersFromItsSourceIsValidatedOnItsOwn() throws Exception {
+ File source = temporaryFolder.newFolder("overworld");
+ Files.writeString(new File(source, "dimension.json").toPath(), "terrain");
+ File snapshot = temporaryFolder.newFolder("epoch", "pack");
+ Files.writeString(new File(snapshot, "dimension.json").toPath(), "older terrain");
+ PackValidationResult snapshotResult = loadable(snapshot);
+ try (MockedStatic validator = mockStatic(PackValidator.class)) {
+ validator.when(() -> PackValidator.validate(source)).thenReturn(loadable(source));
+ validator.when(() -> PackValidator.validate(snapshot)).thenReturn(snapshotResult);
+ ModdedStartup.validatePack(source);
+
+ assertSame(snapshotResult, ModdedGenerationHistoryStorage.validatePack(snapshot.toPath(), source.toPath()));
+
+ validator.verify(() -> PackValidator.validate(snapshot), times(1));
+ }
+ }
+
private static PackValidationResult loadable(File root) {
return new PackValidationResult(root.getName(), List.of(), List.of(), System.currentTimeMillis());
}
diff --git a/adapters/modded-common/src/test/java/art/arcane/iris/modded/ModdedStartupReinjectFailureTest.java b/adapters/modded-common/src/test/java/art/arcane/iris/modded/ModdedStartupReinjectFailureTest.java
index d81288a87..6810a25c3 100644
--- a/adapters/modded-common/src/test/java/art/arcane/iris/modded/ModdedStartupReinjectFailureTest.java
+++ b/adapters/modded-common/src/test/java/art/arcane/iris/modded/ModdedStartupReinjectFailureTest.java
@@ -1,7 +1,6 @@
package art.arcane.iris.modded;
import art.arcane.iris.pack.PackValidationRegistry;
-import art.arcane.volmlib.nativelib.minecraft26_2.modded.NativeModdedServer;
import art.arcane.volmlib.nativelib.minecraft26_2.modded.NativeProtocolPlayer;
import org.junit.After;
import org.junit.Test;
@@ -32,19 +31,12 @@ public class ModdedStartupReinjectFailureTest {
@Test
public void aDimensionThatFailsToReinjectIsBanneredAndReportedToOperatorsOnJoin() {
- NativeModdedServer server = mock(NativeModdedServer.class);
ModdedDimensionRegistryStore.PersistentDimension moon =
new ModdedDimensionRegistryStore.PersistentDimension("iris:moon", "overworld", "overworld", 1337L);
IllegalStateException failure = new IllegalStateException("Iris generation history is unusable for 'iris:moon'");
- try (MockedStatic store = mockStatic(ModdedDimensionRegistryStore.class);
- MockedStatic manager = mockStatic(ModdedDimensionManager.class);
- MockedStatic log = mockStatic(ModdedIrisLog.class)) {
- store.when(() -> ModdedDimensionRegistryStore.loadForStartup(server)).thenReturn(List.of(moon));
- manager.when(() -> ModdedDimensionManager.restorePersistent(server, "iris:moon", "overworld", "overworld", 1337L))
- .thenThrow(failure);
-
- ModdedStartup.reinjectPersistentDimensions(server);
+ try (MockedStatic log = mockStatic(ModdedIrisLog.class)) {
+ ModdedStartup.finishDimensionRestore(ModdedDimensionRestore.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));
@@ -73,4 +65,35 @@ public class ModdedStartupReinjectFailureTest {
ModdedStartup.warnStartupFailuresTo(laterOperator);
verify(laterOperator, never()).sendMessage(any());
}
+
+ private record FailingSteps(RuntimeException failure) implements ModdedDimensionRestore.Steps {
+ @Override
+ public String prepare(ModdedDimensionRegistryStore.PersistentDimension dimension) {
+ throw failure;
+ }
+
+ @Override
+ public String begin(ModdedDimensionRegistryStore.PersistentDimension dimension, String prepared) {
+ throw new AssertionError("A failed preparation must not create a level");
+ }
+
+ @Override
+ public String build(String level, String prepared) {
+ throw new AssertionError("A failed preparation must not build an engine");
+ }
+
+ @Override
+ public void complete(String level, String engine) {
+ throw new AssertionError("A failed preparation must not publish a level");
+ }
+
+ @Override
+ public void abandon(String level, Throwable failure) {
+ throw new AssertionError("A failed preparation has no level to abandon");
+ }
+
+ @Override
+ public void discard(String prepared) {
+ }
+ }
}
diff --git a/core/src/main/java/art/arcane/iris/generation/runtime/PreparedHydrologyCacheIdentity.java b/core/src/main/java/art/arcane/iris/generation/runtime/PreparedHydrologyCacheIdentity.java
index eafcaa673..000b2d1c4 100644
--- a/core/src/main/java/art/arcane/iris/generation/runtime/PreparedHydrologyCacheIdentity.java
+++ b/core/src/main/java/art/arcane/iris/generation/runtime/PreparedHydrologyCacheIdentity.java
@@ -30,9 +30,12 @@ record PreparedHydrologyCacheIdentity(String fingerprint) {
if (context.isBlank()) {
return null;
}
- String content = GenerationPackFingerprint.compute(
- target.getData().getDataFolder().toPath(), GenerationPackFingerprint.CURRENT_VERSION);
- String dependencies = target.getData().hasGenerationRegistryContract() ? "closed"
+ boolean immutable = target.getData().hasGenerationRegistryContract();
+ String content = immutable
+ ? target.getData().generationPackFingerprint()
+ : GenerationPackFingerprint.compute(
+ target.getData().getDataFolder().toPath(), GenerationPackFingerprint.CURRENT_VERSION);
+ String dependencies = immutable ? "closed"
: dependencyFingerprint(IrisPlatforms.get().packsFolderNoCreate());
if (dependencies.isBlank()) {
return null;
diff --git a/core/src/test/java/art/arcane/iris/generation/runtime/PreparedHydrologyCacheIdentityTest.java b/core/src/test/java/art/arcane/iris/generation/runtime/PreparedHydrologyCacheIdentityTest.java
index 89155f02e..4ba84853f 100644
--- a/core/src/test/java/art/arcane/iris/generation/runtime/PreparedHydrologyCacheIdentityTest.java
+++ b/core/src/test/java/art/arcane/iris/generation/runtime/PreparedHydrologyCacheIdentityTest.java
@@ -1,6 +1,7 @@
package art.arcane.iris.generation.runtime;
import art.arcane.iris.world.history.GenerationKernelRegistry;
+import art.arcane.iris.world.history.GenerationPackFingerprint;
import art.arcane.iris.world.history.TransitionGenerationPlan;
import org.junit.Test;
import org.junit.Rule;
@@ -21,6 +22,7 @@ import static org.mockito.Mockito.mockStatic;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotEquals;
+import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
@@ -121,6 +123,8 @@ public class PreparedHydrologyCacheIdentityTest {
IrisData data = mock(IrisData.class);
when(data.getDataFolder()).thenReturn(path.toFile());
when(data.hasGenerationRegistryContract()).thenReturn(true);
+ when(data.generationPackFingerprint()).thenAnswer(invocation ->
+ GenerationPackFingerprint.compute(path, GenerationPackFingerprint.CURRENT_VERSION));
IrisWorld world = mock(IrisWorld.class);
when(world.minHeight()).thenReturn(-64);
when(world.maxHeight()).thenReturn(320);
@@ -133,6 +137,23 @@ public class PreparedHydrologyCacheIdentityTest {
return target;
}
+ @Test
+ public void immutableGenerationPackUsesTheDataFingerprintWithoutRereadingThePack() throws Exception {
+ Path missing = folder.getRoot().toPath().resolve("immutable-epoch-pack");
+ EngineTarget target = target(missing);
+ IrisData data = target.getData();
+ doReturn("verified-epoch").when(data).generationPackFingerprint();
+ GenerationKernelRegistry.RuntimeKernel kernel = kernel("implementation");
+ try (MockedStatic validation = mockStatic(PackValidationCache.class)) {
+ validation.when(PackValidationCache::contextFingerprint).thenReturn("platform");
+
+ PreparedHydrologyCacheIdentity captured = PreparedHydrologyCacheIdentity.capture(target, kernel, null, false);
+
+ assertEquals(PreparedHydrologyCacheIdentity.fromInputs(
+ "verified-epoch", "closed", "platform", kernel, null, -64, 320, 0), captured);
+ }
+ }
+
@Test
public void identicalInputsShareRegardlessOfWorldLocation() {
GenerationKernelRegistry.RuntimeKernel kernel = kernel("implementation");