mirror of
https://github.com/VolmitSoftware/Iris.git
synced 2026-10-18 06:24:47 +00:00
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:
+2
-8
@@ -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;
|
||||
|
||||
+160
-109
@@ -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,
|
||||
|
||||
+15
@@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+233
@@ -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) {
|
||||
|
||||
+68
-20
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+158
-42
@@ -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())) {
|
||||
|
||||
+276
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
+44
-7
@@ -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());
|
||||
}
|
||||
|
||||
+33
-10
@@ -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) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+6
-3
@@ -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;
|
||||
|
||||
+21
@@ -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");
|
||||
|
||||
Reference in New Issue
Block a user