Merged off-thread dimension restore

- Saved dimensions prepare in parallel during world load
- Server start only creates levels and publishes engines
This commit is contained in:
Brian Neumann-Fopiano committed 2026-10-15 10:11:03 -04:00
commit a0cf3d79bf
13 files changed
+1118 -223

No files matched your search

@@ -743,14 +743,8 @@ public final class IrisModdedChunkGenerator implements NativeGeneratorOwner, Nat
PackValidationRegistry.requireLoadable(packRoot); PackValidationRegistry.requireLoadable(packRoot);
packDirectory = packRoot.toFile(); packDirectory = packRoot.toFile();
} }
IrisData data = IrisData.get(packDirectory); ModdedDimensionMetadata.ConfiguredPack resolved =
IrisDimension dimension = data.getDimensionLoader().load(activeDimensionKey); ModdedDimensionMetadata.ConfiguredPack.load(packDirectory, 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));
configuredPack = resolved; configuredPack = resolved;
heightMetadata = resolved.metadata(); heightMetadata = resolved.metadata();
return resolved; return resolved;
@@ -107,104 +107,109 @@ public final class ModdedDimensionManager {
) { ) {
return create( return create(
server, server,
dimensionId, new DimensionRequest(dimensionId, pack, packDimensionKey, seed),
pack, () -> transientStudioPack(pack, packDimensionKey)
packDimensionKey,
seed,
ModdedGenerationMode.TRANSIENT_STUDIO
); );
} }
static Handle restorePersistent( static RestoringLevel beginRestore(
NativeModdedServer server, NativeModdedServer server,
String dimensionId, ModdedDimensionRegistryStore.PersistentDimension dimension,
String pack, ModdedGenerationHistoryStorage.ActivePack active
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
) { ) {
NativeDimensionRuntime serverAccess = requireAccess(); NativeDimensionRuntime serverAccess = requireAccess();
DimensionRequest requested = new DimensionRequest(dimension.id(), dimension.pack(), dimension.dimension(), dimension.seed());
synchronized (LOCK) { synchronized (LOCK) {
String key = dimensionId; Handle present = presentHandle(server, serverAccess, requested);
RuntimePack runtimePack = switch (generationMode) { if (present != null) {
case TRANSIENT_STUDIO -> transientStudioPack(pack, packDimensionKey); return new RestoringLevel(present, true);
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;
} }
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 { try {
Handle handle = inject( RuntimePack runtimePack = persistentRuntimePack(active, ModdedGenerationMode.PERSISTENT_RESTORE);
server, return new RestoringLevel(createLevel(server, serverAccess, requested, runtimePack), false);
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;
} catch (Throwable e) { } catch (Throwable e) {
ModdedIrisLog.error("Iris failed to inject runtime dimension '{}' (pack={} dim={} seed={})", dimensionId, pack, packDimensionKey, seed, e); throw injectionFailure(requested, e);
throw new IllegalStateException("Iris runtime dimension injection failed for " + dimensionId, 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) { public static Handle createPersistent(NativeModdedServer server, String dimensionId, String pack, String packDimensionKey, long seed) {
synchronized (LOCK) { synchronized (LOCK) {
ModdedDimensionRegistryStore.PersistentDimension previous = ModdedDimensionRegistryStore.PersistentDimension previous =
@@ -222,11 +227,17 @@ public final class ModdedDimensionManager {
try { try {
return create( return create(
server, server,
dimensionId, new DimensionRequest(dimensionId, pack, packDimensionKey, seed),
pack, () -> persistentRuntimePack(
packDimensionKey, ModdedGenerationHistoryStorage.createOrStage(
seed, server,
ModdedGenerationMode.PERSISTENT_CREATE dimensionId,
pack,
packDimensionKey,
seed
),
ModdedGenerationMode.PERSISTENT_CREATE
)
); );
} catch (Throwable e) { } catch (Throwable e) {
try { try {
@@ -392,16 +403,16 @@ public final class ModdedDimensionManager {
} }
} }
private static Handle inject( private static Handle createLevel(
NativeModdedServer server, NativeModdedServer server,
NativeDimensionRuntime serverAccess, NativeDimensionRuntime serverAccess,
String dimensionId, DimensionRequest requested,
String key,
String pack,
String packDimensionKey,
long seed,
RuntimePack runtimePack RuntimePack runtimePack
) { ) {
String dimensionId = requested.dimensionId();
String pack = requested.pack();
String packDimensionKey = requested.packDimensionKey();
long seed = requested.seed();
String typeRef = runtimePack.dimensionTypeKey(); String typeRef = runtimePack.dimensionTypeKey();
ModdedForcedDatapack.requireRegisteredDimensionType(typeRef, ModdedForcedDatapack.requireRegisteredDimensionType(typeRef,
serverAccess.hasDimensionType(server, typeRef) ? Optional.of(Boolean.TRUE) : Optional.empty(), serverAccess.hasDimensionType(server, typeRef) ? Optional.of(Boolean.TRUE) : Optional.empty(),
@@ -414,35 +425,54 @@ public final class ModdedDimensionManager {
runtimePack.generationMode()); runtimePack.generationMode());
return generator; return generator;
})); }));
NativeWorld level = created.world(); try {
IrisModdedChunkGenerator generator = created.generator(); 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; boolean loadEventStarted = false;
try { try {
serverAccess.initialize(server, level); if (prepared != null) {
ModdedWorldEngines.installPrepared(level, prepared);
}
generator.bindLevel(level); generator.bindLevel(level);
Handle handle = new Handle(dimensionId, pack, packDimensionKey, seed, level, generator);
NativeWorld previous = serverAccess.publish(server, level); NativeWorld previous = serverAccess.publish(server, level);
if (previous != null) { 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"); + "': the level was registered concurrently");
} }
serverAccess.addWorldBorderListener(server, level); serverAccess.addWorldBorderListener(server, level);
loadEventStarted = true; loadEventStarted = true;
ModdedEngineBootstrap.loader().fireDynamicLevelLoad(level); ModdedEngineBootstrap.loader().fireDynamicLevelLoad(level);
return handle; return created;
} catch (Throwable error) { } catch (Throwable error) {
rollbackInjection(server, serverAccess, key, level, generator, loadEventStarted, error); rollbackInjection(server, serverAccess, created.dimensionId(), level, generator, loadEventStarted, error);
if (error instanceof RuntimeException runtimeException) { throw rethrow(created.dimensionId(), error);
throw runtimeException;
}
if (error instanceof Error fatalError) {
throw fatalError;
}
throw new IllegalStateException("Iris runtime dimension injection failed for " + 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, private static void rollbackInjection(NativeModdedServer server, NativeDimensionRuntime serverAccess,
String key, NativeWorld level, String key, NativeWorld level,
IrisModdedChunkGenerator generator, boolean loadEventStarted, 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) { 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( public record UpdateResult(
boolean pending, boolean pending,
boolean restartRequired, boolean restartRequired,
@@ -28,6 +28,7 @@ import art.arcane.iris.generation.terrain.IrisDimension;
import art.arcane.iris.generation.terrain.IrisRegion; import art.arcane.iris.generation.terrain.IrisRegion;
import art.arcane.volmlib.nativelib.minecraft26_2.terrain.NativeResourceKeys; import art.arcane.volmlib.nativelib.minecraft26_2.terrain.NativeResourceKeys;
import java.io.File;
import java.util.LinkedHashSet; import java.util.LinkedHashSet;
import java.util.Locale; import java.util.Locale;
import java.util.Set; 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) { 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));
}
} }
} }
@@ -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 <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
) {
}
}
@@ -145,6 +145,7 @@ public final class ModdedEngineBootstrap {
} }
IrisModdedCommands.openDownloadAdmission(); IrisModdedCommands.openDownloadAdmission();
ModdedStartup.prepareForStartup(); ModdedStartup.prepareForStartup();
ModdedStartup.prepareDimensionRestore(server);
IrisModdedChunkGenerator.startGenPool(); IrisModdedChunkGenerator.startGenPool();
bindWorldGenerators(server); bindWorldGenerators(server);
services().enableAll(); services().enableAll();
@@ -164,6 +165,7 @@ public final class ModdedEngineBootstrap {
finishPreviousServer("server start"); finishPreviousServer("server start");
captureInitialSpawn(server); captureInitialSpawn(server);
ModdedStartup.prepareForStartup(); ModdedStartup.prepareForStartup();
ModdedStartup.prepareDimensionRestore(server);
} }
public static void serverStarted(NativeModdedServer server) { public static void serverStarted(NativeModdedServer server) {
@@ -9,6 +9,7 @@ import art.arcane.iris.pack.BrokenPackException;
import art.arcane.iris.pack.PackFingerprints; import art.arcane.iris.pack.PackFingerprints;
import art.arcane.iris.pack.PackValidationRegistry; import art.arcane.iris.pack.PackValidationRegistry;
import art.arcane.iris.pack.PackValidationResult; 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.GenerationEpoch;
import art.arcane.iris.world.history.GenerationEpochContractFactory; import art.arcane.iris.world.history.GenerationEpochContractFactory;
import art.arcane.iris.world.history.GenerationHistory; import art.arcane.iris.world.history.GenerationHistory;
@@ -53,7 +54,7 @@ final class ModdedGenerationHistoryStorage {
ModdedGenerationHistoryStorage::requireRegistryDefinitions, ModdedGenerationHistoryStorage::requireRegistryDefinitions,
IrisSettings.get().getGenerator().getGenerationTransitionWidthBlocks() IrisSettings.get().getGenerator().getGenerationTransitionWidthBlocks()
); );
return resolveActive(history); return resolveActive(history, source);
} catch (IOException failure) { } catch (IOException failure) {
throw historyFailure(levelKey, failure); throw historyFailure(levelKey, failure);
} }
@@ -68,25 +69,57 @@ final class ModdedGenerationHistoryStorage {
) { ) {
Path dimensionRoot = dimensionRoot(server, levelKey); Path dimensionRoot = dimensionRoot(server, levelKey);
try { try {
GenerationHistory history = restoreOrAdopt( GenerationHistory history = openForRestore(dimensionRoot, selectedPack, selectedDimensionKey, worldSeed);
dimensionRoot, return resolveActive(history, installedPackSource(selectedPack));
worldSeed,
() -> {
Path source = selectedPackSource(selectedPack, selectedDimensionKey);
return new AdoptionSource(source, captureCandidate(
source,
selectedDimensionKey,
adoptionAliasPolicy(dimensionRoot)
));
},
ModdedGenerationHistoryStorage::requireRegistryDefinitions
);
return resolveActive(history);
} catch (IOException failure) { } catch (IOException failure) {
throw historyFailure(levelKey, 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( static ActivePack openOrAdopt(
NativeWorld level, NativeWorld level,
String selectedPack, 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(); GenerationEpoch epoch = history.activeEpoch();
Path packRoot = normalize(history.activePackRoot()); Path packRoot = normalize(history.activePackRoot());
validatePack(packRoot); validatePack(packRoot, validatedSource);
validateDimension(packRoot, epoch); validateDimension(packRoot, epoch);
return new ActivePack( return new ActivePack(
packRoot, packRoot,
@@ -197,9 +230,12 @@ final class ModdedGenerationHistoryStorage {
); );
} }
static PackValidationResult validatePack(Path packRoot) { static PackValidationResult validatePack(Path packRoot, Path validatedSource) {
PackValidationResult validation = PackValidationRegistry.getMatching( String contentFingerprint = PackFingerprints.computePackTreeFingerprint(packRoot.toFile());
packRoot, PackFingerprints.computePackTreeFingerprint(packRoot.toFile())); PackValidationResult validation = PackValidationRegistry.getMatching(packRoot, contentFingerprint);
if (validation == null && validatedSource != null) {
validation = PackValidationRegistry.publishMatchingCopy(validatedSource, packRoot, contentFingerprint);
}
if (validation == null) { if (validation == null) {
validation = ModdedStartup.validatePack(packRoot.toFile()); validation = ModdedStartup.validatePack(packRoot.toFile());
} }
@@ -286,6 +322,11 @@ final class ModdedGenerationHistoryStorage {
return normalize(ModdedWorldEngines.resolvePack(pack, dimensionKey).toPath()); 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( private static GenerationRegistryContract requireRegistryDefinitions(
Collection<GenerationRegistryContract> contracts Collection<GenerationRegistryContract> 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( record Candidate(
String packFingerprint, String packFingerprint,
GenerationEpoch.DimensionContract dimensionContract, GenerationEpoch.DimensionContract dimensionContract,
@@ -27,6 +27,7 @@ import art.arcane.iris.pack.PackValidationRegistry;
import art.arcane.iris.pack.PackValidationCache; import art.arcane.iris.pack.PackValidationCache;
import art.arcane.iris.pack.PackValidationResult; import art.arcane.iris.pack.PackValidationResult;
import art.arcane.iris.pack.PackValidator; import art.arcane.iris.pack.PackValidator;
import art.arcane.iris.generation.runtime.Engine;
import art.arcane.iris.modded.command.ModdedPackCommands; import art.arcane.iris.modded.command.ModdedPackCommands;
import art.arcane.iris.spi.IrisPlatforms; import art.arcane.iris.spi.IrisPlatforms;
import art.arcane.iris.world.safeguard.GenerationRefusalNotice; import art.arcane.iris.world.safeguard.GenerationRefusalNotice;
@@ -39,19 +40,29 @@ import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;
public final class ModdedStartup { public final class ModdedStartup {
private static final int COMPAT_BOOT_KEY_CAP = 3; private static final int COMPAT_BOOT_KEY_CAP = 3;
private static final AtomicBoolean PREPARED = new AtomicBoolean(false); private static final AtomicBoolean PREPARED = new AtomicBoolean(false);
private static final AtomicBoolean STARTED = 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 =
new AtomicReference<>();
private static final List<String> DIMENSION_FAILURES = new CopyOnWriteArrayList<>(); private static final List<String> DIMENSION_FAILURES = new CopyOnWriteArrayList<>();
private ModdedStartup() { private ModdedStartup() {
} }
public static void reset() { public static void reset() {
ModdedDimensionRestore<ModdedWorldEngines.PreparedPack, ModdedDimensionManager.RestoringLevel, Engine> restore =
RESTORE.getAndSet(null);
if (restore != null) {
restore.close();
}
PREPARED.set(false); PREPARED.set(false);
STARTED.set(false); STARTED.set(false);
RESTORE_BEGUN.set(false);
DIMENSION_FAILURES.clear(); DIMENSION_FAILURES.clear();
} }
@@ -62,6 +73,24 @@ public final class ModdedStartup {
validateAllPacks(); validateAllPacks();
} }
public static void prepareDimensionRestore(NativeModdedServer server) {
if (server == null || !RESTORE_BEGUN.compareAndSet(false, true)) {
return;
}
List<ModdedDimensionRegistryStore.PersistentDimension> dimensions =
ModdedDimensionRegistryStore.loadForStartup(server);
if (dimensions.isEmpty()) {
return;
}
long startedAt = System.currentTimeMillis();
ModdedDimensionRestore<ModdedWorldEngines.PreparedPack, ModdedDimensionManager.RestoringLevel, Engine> 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: * 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 * 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) { static void reinjectPersistentDimensions(NativeModdedServer server) {
List<ModdedDimensionRegistryStore.PersistentDimension> dimensions = prepareDimensionRestore(server);
ModdedDimensionRegistryStore.loadForStartup(server); ModdedDimensionRestore<ModdedWorldEngines.PreparedPack, ModdedDimensionManager.RestoringLevel, Engine> restore =
if (dimensions.isEmpty()) { RESTORE.getAndSet(null);
if (restore == null) {
return; return;
} }
finishDimensionRestore(restore);
}
static void finishDimensionRestore(ModdedDimensionRestore<?, ?, ?> restore) {
long startedAt = System.currentTimeMillis();
List<ModdedDimensionRestore.Result> results;
try (restore) {
results = restore.finish();
}
long waited = System.currentTimeMillis() - startedAt;
int injected = 0; int injected = 0;
int index = 0; int index = 0;
long startedAt = System.currentTimeMillis(); OutOfMemoryError outOfMemory = null;
for (ModdedDimensionRegistryStore.PersistentDimension dimension : dimensions) { for (ModdedDimensionRestore.Result result : results) {
index++; index++;
long dimensionStartedAt = System.currentTimeMillis(); ModdedDimensionRegistryStore.PersistentDimension dimension = result.dimension();
try { Throwable failure = result.failure();
ModdedDimensionManager.restorePersistent( if (failure == null) {
server,
dimension.id(),
dimension.pack(),
dimension.dimension(),
dimension.seed()
);
injected++; injected++;
ModdedIrisLog.info("Iris re-injected {}/{} '{}' (pack={} dim={}) in {}ms", ModdedIrisLog.info("Iris re-injected {}/{} '{}' (pack={} dim={}): prepared {}ms, engine {}ms",
index, dimensions.size(), dimension.id(), dimension.pack(), dimension.dimension(), index, results.size(), dimension.id(), dimension.pack(), dimension.dimension(),
System.currentTimeMillis() - dimensionStartedAt); result.preparedMillis(), result.builtMillis());
} catch (Throwable e) { continue;
reportDimensionFailure(dimension, e); }
if (e instanceof OutOfMemoryError outOfMemory) { reportDimensionFailure(dimension, failure);
throw outOfMemory; if (failure instanceof OutOfMemoryError fatal && outOfMemory == null) {
} outOfMemory = fatal;
} }
} }
ModdedIrisLog.info("Iris re-injected {}/{} persistent dimension(s) at startup in {}ms", ModdedIrisLog.info("Iris re-injected {}/{} persistent dimension(s) at startup; server thread waited {}ms",
injected, dimensions.size(), System.currentTimeMillis() - startedAt); injected, results.size(), waited);
if (outOfMemory != null) {
throw outOfMemory;
}
} }
private static void reportDimensionFailure(ModdedDimensionRegistryStore.PersistentDimension dimension, Throwable failure) { 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<ModdedWorldEngines.PreparedPack, ModdedDimensionManager.RestoringLevel, Engine> {
@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();
}
}
} }
@@ -31,6 +31,7 @@ import art.arcane.iris.generation.runtime.IrisEngine;
import art.arcane.iris.generation.runtime.Engine; import art.arcane.iris.generation.runtime.Engine;
import art.arcane.iris.generation.runtime.EngineTarget; import art.arcane.iris.generation.runtime.EngineTarget;
import art.arcane.iris.world.history.GenerationActivation; 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.GenerationEpoch;
import art.arcane.iris.world.history.GenerationEpochContractFactory; import art.arcane.iris.world.history.GenerationEpochContractFactory;
import art.arcane.iris.world.history.GenerationHistory; 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) { static void closeUnregistered(Engine engine) {
close(engine); close(engine);
ENGINES.entrySet().removeIf((Map.Entry<NativeWorld, Engine> entry) -> entry.getValue() == engine); ENGINES.entrySet().removeIf((Map.Entry<NativeWorld, Engine> entry) -> entry.getValue() == engine);
@@ -142,9 +180,13 @@ public final class ModdedWorldEngines {
ModdedGenerationMode generationMode ModdedGenerationMode generationMode
) { ) {
Engine engine = create(level, pack, dimensionKey, seedOverride, generationMode); Engine engine = create(level, pack, dimensionKey, seedOverride, generationMode);
applyWorldBoundary(level, engine);
return engine;
}
private static void applyWorldBoundary(NativeWorld level, Engine engine) {
try { try {
engine.getPlatformHooks().applyWorldBoundary(engine); engine.getPlatformHooks().applyWorldBoundary(engine);
return engine;
} catch (Throwable failure) { } catch (Throwable failure) {
try { try {
close(engine); close(engine);
@@ -197,57 +239,63 @@ public final class ModdedWorldEngines {
GenerationHistory history = opened.history(); GenerationHistory history = opened.history();
try (GenerationAdmission.RuntimeLease startupAdmission = history.retainRuntime()) { try (GenerationAdmission.RuntimeLease startupAdmission = history.retainRuntime()) {
history.prepareCurrentGenerator(IrisSettings.get().getGenerator().getGenerationTransitionWidthBlocks()); history.prepareCurrentGenerator(IrisSettings.get().getGenerator().getGenerationTransitionWidthBlocks());
opened = ModdedGenerationHistoryStorage.resolveActive(history); ModdedGenerationHistoryStorage.ActivePack active = ModdedGenerationHistoryStorage.resolveActive(history, null);
long openedActivationId = history.activeActivation().activationId(); return attachHistoryEngine(level, seed, active, loadPack(active));
IrisEngine candidate = buildEngine(level, seed, opened); }
boolean ready = false; }
try {
GenerationHistoryRuntimeRouter router = candidate.attachGenerationHistory( 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, history,
IrisBoundarySignatureSampler.INSTANCE, IrisBoundarySignatureSampler.INSTANCE
IrisSettings.get().getGenerator().getGenerationTransitionWidthBlocks()
); );
router.preloadActiveRuntimes(); router.preloadActiveRuntimes();
if (history.activeActivation().activationId() != openedActivationId) { }
close(candidate); requireReady(candidate, level, ModdedGenerationMode.PERSISTENT_RESTORE);
ModdedGenerationHistoryStorage.ActivePack promoted = ready = true;
ModdedGenerationHistoryStorage.resolveActive(history); ModdedIrisLog.info("Iris engine up for {}: pack={} dim={} seed={} height={}..{} activation={}",
candidate = buildEngine(level, seed, promoted); level.name(),
router = GenerationHistoryRuntimeRouter.attach( candidate.getData().getDataFolder().getAbsolutePath(),
candidate, candidate.getDimension().getLoadKey(),
history, seed,
IrisBoundarySignatureSampler.INSTANCE candidate.getDimension().getMinHeight(),
); candidate.getDimension().getMaxHeight(),
router.preloadActiveRuntimes(); history.activeActivation().activationId());
} return candidate;
requireReady(candidate, level, ModdedGenerationMode.PERSISTENT_RESTORE); } finally {
ready = true; if (!ready) {
ModdedIrisLog.info("Iris engine up for {}: pack={} dim={} seed={} height={}..{} activation={}", close(candidate);
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( private static LoadedPack loadPack(ModdedGenerationHistoryStorage.ActivePack active) {
NativeWorld level,
long seed,
ModdedGenerationHistoryStorage.ActivePack active
) throws IOException {
File packDir = active.packRoot().toFile(); File packDir = active.packRoot().toFile();
PackValidationRegistry.requireLoadable(active.packRoot()); PackValidationRegistry.requireLoadable(active.packRoot());
IrisData data = IrisData.openRuntime(packDir); IrisData data = IrisData.openRuntime(packDir);
boolean runtimeOwnsData = false; boolean loaded = false;
try { try {
IrisDimension dimension = data.getDimensionLoader().load(active.dimensionKey()); IrisDimension dimension = data.getDimensionLoader().load(active.dimensionKey());
if (dimension == null) { if (dimension == null) {
@@ -256,7 +304,26 @@ public final class ModdedWorldEngines {
throw new IllegalStateException("Iris dimension '" + active.dimensionKey() throw new IllegalStateException("Iris dimension '" + active.dimensionKey()
+ "' missing from immutable pack " + packDir.getAbsolutePath()); + "' 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); validateDimensionContract(active.dimensionContract(), dimension, level);
File worldFolder = NativeModdedServer.forWorld(level).dimensionFolder(level.name()).toFile(); File worldFolder = NativeModdedServer.forWorld(level).dimensionFolder(level.name()).toFile();
IrisWorld world = buildWorld(level, seed, dimension, worldFolder); 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() { public static void shutdown() {
Throwable failure = null; Throwable failure = null;
for (Map.Entry<NativeWorld, Engine> entry : new ArrayList<>(ENGINES.entrySet())) { for (Map.Entry<NativeWorld, Engine> entry : new ArrayList<>(ENGINES.entrySet())) {
@@ -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<ModdedDimensionRegistryStore.PersistentDimension> dimensions = dimensions(4);
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)) {
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<ModdedDimensionRegistryStore.PersistentDimension> dimensions = dimensions(3);
RecordingSteps steps = new RecordingSteps(dimensions.size());
steps.buildBarrier = new CountDownLatch(dimensions.size());
try (ModdedDimensionRestore<String, String, String> 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<ModdedDimensionRegistryStore.PersistentDimension> dimensions = dimensions(3);
RecordingSteps steps = new RecordingSteps(dimensions.size());
IllegalStateException failure = new IllegalStateException("generation history is unusable");
steps.prepareFailure = failure;
steps.failingIndex = 1;
List<ModdedDimensionRestore.Result> results;
try (ModdedDimensionRestore<String, String, String> 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<ModdedDimensionRegistryStore.PersistentDimension> dimensions = dimensions(2);
RecordingSteps steps = new RecordingSteps(dimensions.size());
IllegalStateException failure = new IllegalStateException("engine refused");
steps.buildFailure = failure;
steps.failingIndex = 0;
List<ModdedDimensionRestore.Result> results;
try (ModdedDimensionRestore<String, String, String> 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<ModdedDimensionRegistryStore.PersistentDimension> dimensions = dimensions(2);
RecordingSteps steps = new RecordingSteps(dimensions.size());
IllegalStateException failure = new IllegalStateException("dimension type missing");
steps.beginFailure = failure;
steps.failingIndex = 1;
List<ModdedDimensionRestore.Result> results;
try (ModdedDimensionRestore<String, String, String> 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<ModdedDimensionRegistryStore.PersistentDimension> 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<String, String, String> 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<ModdedDimensionRegistryStore.PersistentDimension> dimensions = dimensions(3);
RecordingSteps steps = new RecordingSteps(dimensions.size());
ModdedDimensionRestore<String, String, String> 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<ModdedDimensionRegistryStore.PersistentDimension> dimensions(int count) {
List<ModdedDimensionRegistryStore.PersistentDimension> 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<String, String, String> {
private final Set<Thread> prepareThreads = ConcurrentHashMap.newKeySet();
private final Set<Thread> buildThreads = ConcurrentHashMap.newKeySet();
private final Set<Thread> serverThreads = ConcurrentHashMap.newKeySet();
private final List<String> begun = Collections.synchronizedList(new ArrayList<>());
private final List<String> completed = Collections.synchronizedList(new ArrayList<>());
private final List<String> abandoned = Collections.synchronizedList(new ArrayList<>());
private final List<String> 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();
}
}
}
@@ -24,6 +24,7 @@ import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertSame; import static org.junit.Assert.assertSame;
import static org.junit.Assert.fail; import static org.junit.Assert.fail;
import static org.mockito.Mockito.mockStatic; import static org.mockito.Mockito.mockStatic;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.times; import static org.mockito.Mockito.times;
public class ModdedPackValidationPublicationTest { public class ModdedPackValidationPublicationTest {
@@ -41,8 +42,8 @@ public class ModdedPackValidationPublicationTest {
PackValidationResult result = loadable(root); PackValidationResult result = loadable(root);
try (MockedStatic<PackValidator> validator = mockStatic(PackValidator.class)) { try (MockedStatic<PackValidator> validator = mockStatic(PackValidator.class)) {
validator.when(() -> PackValidator.validate(root)).thenReturn(result); validator.when(() -> PackValidator.validate(root)).thenReturn(result);
assertSame(result, ModdedGenerationHistoryStorage.validatePack(root.toPath())); assertSame(result, ModdedGenerationHistoryStorage.validatePack(root.toPath(), null));
assertSame(result, ModdedGenerationHistoryStorage.validatePack(root.toPath())); assertSame(result, ModdedGenerationHistoryStorage.validatePack(root.toPath(), null));
validator.verify(() -> PackValidator.validate(root), times(1)); 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); PackValidationResult broken = new PackValidationResult(root.getName(), List.of("invalid"), List.of(), 1L);
try (MockedStatic<PackValidator> validator = mockStatic(PackValidator.class)) { try (MockedStatic<PackValidator> validator = mockStatic(PackValidator.class)) {
validator.when(() -> PackValidator.validate(root)).thenReturn(loadable(root), broken); validator.when(() -> PackValidator.validate(root)).thenReturn(loadable(root), broken);
ModdedGenerationHistoryStorage.validatePack(root.toPath()); ModdedGenerationHistoryStorage.validatePack(root.toPath(), null);
Files.writeString(content.toPath(), "wrong"); Files.writeString(content.toPath(), "wrong");
Files.setLastModifiedTime(content.toPath(), timestamp); Files.setLastModifiedTime(content.toPath(), timestamp);
assertThrows(BrokenPackException.class, assertThrows(BrokenPackException.class,
() -> ModdedGenerationHistoryStorage.validatePack(root.toPath())); () -> ModdedGenerationHistoryStorage.validatePack(root.toPath(), null));
assertThrows(BrokenPackException.class, assertThrows(BrokenPackException.class,
() -> ModdedGenerationHistoryStorage.validatePack(root.toPath())); () -> ModdedGenerationHistoryStorage.validatePack(root.toPath(), null));
validator.verify(() -> PackValidator.validate(root), times(2)); validator.verify(() -> PackValidator.validate(root), times(2));
} }
} }
@@ -77,9 +78,9 @@ public class ModdedPackValidationPublicationTest {
MockedStatic<PackValidationCache> cache = mockStatic(PackValidationCache.class)) { MockedStatic<PackValidationCache> cache = mockStatic(PackValidationCache.class)) {
cache.when(PackValidationCache::contextFingerprint).thenAnswer(ignored -> context.get()); cache.when(PackValidationCache::contextFingerprint).thenAnswer(ignored -> context.get());
validator.when(() -> PackValidator.validate(root)).thenReturn(first, second); validator.when(() -> PackValidator.validate(root)).thenReturn(first, second);
assertSame(first, ModdedGenerationHistoryStorage.validatePack(root.toPath())); assertSame(first, ModdedGenerationHistoryStorage.validatePack(root.toPath(), null));
context.set("second"); context.set("second");
assertSame(second, ModdedGenerationHistoryStorage.validatePack(root.toPath())); assertSame(second, ModdedGenerationHistoryStorage.validatePack(root.toPath(), null));
validator.verify(() -> PackValidator.validate(root), times(2)); 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<PackValidator> 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<PackValidator> 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) { private static PackValidationResult loadable(File root) {
return new PackValidationResult(root.getName(), List.of(), List.of(), System.currentTimeMillis()); return new PackValidationResult(root.getName(), List.of(), List.of(), System.currentTimeMillis());
} }
@@ -1,7 +1,6 @@
package art.arcane.iris.modded; package art.arcane.iris.modded;
import art.arcane.iris.pack.PackValidationRegistry; 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 art.arcane.volmlib.nativelib.minecraft26_2.modded.NativeProtocolPlayer;
import org.junit.After; import org.junit.After;
import org.junit.Test; import org.junit.Test;
@@ -32,19 +31,12 @@ public class ModdedStartupReinjectFailureTest {
@Test @Test
public void aDimensionThatFailsToReinjectIsBanneredAndReportedToOperatorsOnJoin() { public void aDimensionThatFailsToReinjectIsBanneredAndReportedToOperatorsOnJoin() {
NativeModdedServer server = mock(NativeModdedServer.class);
ModdedDimensionRegistryStore.PersistentDimension moon = ModdedDimensionRegistryStore.PersistentDimension moon =
new ModdedDimensionRegistryStore.PersistentDimension("iris:moon", "overworld", "overworld", 1337L); new ModdedDimensionRegistryStore.PersistentDimension("iris:moon", "overworld", "overworld", 1337L);
IllegalStateException failure = new IllegalStateException("Iris generation history is unusable for 'iris:moon'"); IllegalStateException failure = new IllegalStateException("Iris generation history is unusable for 'iris:moon'");
try (MockedStatic<ModdedDimensionRegistryStore> store = mockStatic(ModdedDimensionRegistryStore.class); try (MockedStatic<ModdedIrisLog> log = mockStatic(ModdedIrisLog.class)) {
MockedStatic<ModdedDimensionManager> manager = mockStatic(ModdedDimensionManager.class); ModdedStartup.finishDimensionRestore(ModdedDimensionRestore.start(List.of(moon), new FailingSteps(failure)));
MockedStatic<ModdedIrisLog> 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);
log.verify(() -> ModdedIrisLog.error(argThat((String line) -> line != null && line.contains("iris:moon") log.verify(() -> ModdedIrisLog.error(argThat((String line) -> line != null && line.contains("iris:moon")
&& line.startsWith("Iris refused"))), times(1)); && line.startsWith("Iris refused"))), times(1));
@@ -73,4 +65,35 @@ public class ModdedStartupReinjectFailureTest {
ModdedStartup.warnStartupFailuresTo(laterOperator); ModdedStartup.warnStartupFailuresTo(laterOperator);
verify(laterOperator, never()).sendMessage(any()); verify(laterOperator, never()).sendMessage(any());
} }
private record FailingSteps(RuntimeException failure) implements ModdedDimensionRestore.Steps<String, String, String> {
@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) {
}
}
} }
@@ -30,9 +30,12 @@ record PreparedHydrologyCacheIdentity(String fingerprint) {
if (context.isBlank()) { if (context.isBlank()) {
return null; return null;
} }
String content = GenerationPackFingerprint.compute( boolean immutable = target.getData().hasGenerationRegistryContract();
target.getData().getDataFolder().toPath(), GenerationPackFingerprint.CURRENT_VERSION); String content = immutable
String dependencies = target.getData().hasGenerationRegistryContract() ? "closed" ? target.getData().generationPackFingerprint()
: GenerationPackFingerprint.compute(
target.getData().getDataFolder().toPath(), GenerationPackFingerprint.CURRENT_VERSION);
String dependencies = immutable ? "closed"
: dependencyFingerprint(IrisPlatforms.get().packsFolderNoCreate()); : dependencyFingerprint(IrisPlatforms.get().packsFolderNoCreate());
if (dependencies.isBlank()) { if (dependencies.isBlank()) {
return null; return null;
@@ -1,6 +1,7 @@
package art.arcane.iris.generation.runtime; package art.arcane.iris.generation.runtime;
import art.arcane.iris.world.history.GenerationKernelRegistry; import art.arcane.iris.world.history.GenerationKernelRegistry;
import art.arcane.iris.world.history.GenerationPackFingerprint;
import art.arcane.iris.world.history.TransitionGenerationPlan; import art.arcane.iris.world.history.TransitionGenerationPlan;
import org.junit.Test; import org.junit.Test;
import org.junit.Rule; 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.assertEquals;
import static org.junit.Assert.assertNotEquals; import static org.junit.Assert.assertNotEquals;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.mock; import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when; import static org.mockito.Mockito.when;
@@ -121,6 +123,8 @@ public class PreparedHydrologyCacheIdentityTest {
IrisData data = mock(IrisData.class); IrisData data = mock(IrisData.class);
when(data.getDataFolder()).thenReturn(path.toFile()); when(data.getDataFolder()).thenReturn(path.toFile());
when(data.hasGenerationRegistryContract()).thenReturn(true); when(data.hasGenerationRegistryContract()).thenReturn(true);
when(data.generationPackFingerprint()).thenAnswer(invocation ->
GenerationPackFingerprint.compute(path, GenerationPackFingerprint.CURRENT_VERSION));
IrisWorld world = mock(IrisWorld.class); IrisWorld world = mock(IrisWorld.class);
when(world.minHeight()).thenReturn(-64); when(world.minHeight()).thenReturn(-64);
when(world.maxHeight()).thenReturn(320); when(world.maxHeight()).thenReturn(320);
@@ -133,6 +137,23 @@ public class PreparedHydrologyCacheIdentityTest {
return target; 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<PackValidationCache> 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 @Test
public void identicalInputsShareRegardlessOfWorldLocation() { public void identicalInputsShareRegardlessOfWorldLocation() {
GenerationKernelRegistry.RuntimeKernel kernel = kernel("implementation"); GenerationKernelRegistry.RuntimeKernel kernel = kernel("implementation");