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
2026-10-15 10:11:03 -04:00
13 changed files with 1118 additions and 223 deletions
@@ -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;
@@ -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,
@@ -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));
}
}
}
@@ -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();
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) {
@@ -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<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(
String packFingerprint,
GenerationEpoch.DimensionContract dimensionContract,
@@ -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<ModdedDimensionRestore<ModdedWorldEngines.PreparedPack, ModdedDimensionManager.RestoringLevel, Engine>> RESTORE =
new AtomicReference<>();
private static final List<String> DIMENSION_FAILURES = new CopyOnWriteArrayList<>();
private ModdedStartup() {
}
public static void reset() {
ModdedDimensionRestore<ModdedWorldEngines.PreparedPack, ModdedDimensionManager.RestoringLevel, Engine> 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<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:
* 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<ModdedDimensionRegistryStore.PersistentDimension> dimensions =
ModdedDimensionRegistryStore.loadForStartup(server);
if (dimensions.isEmpty()) {
prepareDimensionRestore(server);
ModdedDimensionRestore<ModdedWorldEngines.PreparedPack, ModdedDimensionManager.RestoringLevel, Engine> restore =
RESTORE.getAndSet(null);
if (restore == null) {
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 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<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.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<NativeWorld, Engine> 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<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.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<PackValidator> 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<PackValidator> 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<PackValidationCache> 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<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) {
return new PackValidationResult(root.getName(), List.of(), List.of(), System.currentTimeMillis());
}
@@ -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<ModdedDimensionRegistryStore> store = mockStatic(ModdedDimensionRegistryStore.class);
MockedStatic<ModdedDimensionManager> manager = mockStatic(ModdedDimensionManager.class);
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);
try (MockedStatic<ModdedIrisLog> 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<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()) {
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;
@@ -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<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
public void identicalInputsShareRegardlessOfWorldLocation() {
GenerationKernelRegistry.RuntimeKernel kernel = kernel("implementation");