mirror of
https://github.com/VolmitSoftware/Iris.git
synced 2026-10-25 13:30:08 +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:
commit
a0cf3d79bf
13 files changed
+1118
-223
No files matched your search
+2
-8
@@ -743,14 +743,8 @@ public final class IrisModdedChunkGenerator implements NativeGeneratorOwner, Nat
|
|||||||
PackValidationRegistry.requireLoadable(packRoot);
|
PackValidationRegistry.requireLoadable(packRoot);
|
||||||
packDirectory = packRoot.toFile();
|
packDirectory = packRoot.toFile();
|
||||||
}
|
}
|
||||||
IrisData data = IrisData.get(packDirectory);
|
ModdedDimensionMetadata.ConfiguredPack resolved =
|
||||||
IrisDimension dimension = data.getDimensionLoader().load(activeDimensionKey);
|
ModdedDimensionMetadata.ConfiguredPack.load(packDirectory, activeDimensionKey);
|
||||||
if (dimension == null) {
|
|
||||||
throw new IllegalStateException("Iris dimension '" + activeDimensionKey
|
|
||||||
+ "' missing from pack " + packDirectory.getAbsolutePath());
|
|
||||||
}
|
|
||||||
ModdedDimensionMetadata.ConfiguredPack resolved = new ModdedDimensionMetadata.ConfiguredPack(
|
|
||||||
data, dimension, ModdedDimensionMetadata.dimensionMetadata(dimension));
|
|
||||||
configuredPack = resolved;
|
configuredPack = resolved;
|
||||||
heightMetadata = resolved.metadata();
|
heightMetadata = resolved.metadata();
|
||||||
return resolved;
|
return resolved;
|
||||||
|
|||||||
+160
-109
@@ -107,104 +107,109 @@ public final class ModdedDimensionManager {
|
|||||||
) {
|
) {
|
||||||
return create(
|
return create(
|
||||||
server,
|
server,
|
||||||
dimensionId,
|
new DimensionRequest(dimensionId, pack, packDimensionKey, seed),
|
||||||
pack,
|
() -> transientStudioPack(pack, packDimensionKey)
|
||||||
packDimensionKey,
|
|
||||||
seed,
|
|
||||||
ModdedGenerationMode.TRANSIENT_STUDIO
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
static Handle restorePersistent(
|
static RestoringLevel beginRestore(
|
||||||
NativeModdedServer server,
|
NativeModdedServer server,
|
||||||
String dimensionId,
|
ModdedDimensionRegistryStore.PersistentDimension dimension,
|
||||||
String pack,
|
ModdedGenerationHistoryStorage.ActivePack active
|
||||||
String packDimensionKey,
|
|
||||||
long seed
|
|
||||||
) {
|
|
||||||
return create(
|
|
||||||
server,
|
|
||||||
dimensionId,
|
|
||||||
pack,
|
|
||||||
packDimensionKey,
|
|
||||||
seed,
|
|
||||||
ModdedGenerationMode.PERSISTENT_RESTORE
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static Handle create(
|
|
||||||
NativeModdedServer server,
|
|
||||||
String dimensionId,
|
|
||||||
String pack,
|
|
||||||
String packDimensionKey,
|
|
||||||
long seed,
|
|
||||||
ModdedGenerationMode generationMode
|
|
||||||
) {
|
) {
|
||||||
NativeDimensionRuntime serverAccess = requireAccess();
|
NativeDimensionRuntime serverAccess = requireAccess();
|
||||||
|
DimensionRequest requested = new DimensionRequest(dimension.id(), dimension.pack(), dimension.dimension(), dimension.seed());
|
||||||
synchronized (LOCK) {
|
synchronized (LOCK) {
|
||||||
String key = dimensionId;
|
Handle present = presentHandle(server, serverAccess, requested);
|
||||||
RuntimePack runtimePack = switch (generationMode) {
|
if (present != null) {
|
||||||
case TRANSIENT_STUDIO -> transientStudioPack(pack, packDimensionKey);
|
return new RestoringLevel(present, true);
|
||||||
case PERSISTENT_CREATE -> persistentRuntimePack(
|
|
||||||
ModdedGenerationHistoryStorage.createOrStage(
|
|
||||||
server,
|
|
||||||
key,
|
|
||||||
pack,
|
|
||||||
packDimensionKey,
|
|
||||||
seed
|
|
||||||
),
|
|
||||||
generationMode
|
|
||||||
);
|
|
||||||
case PERSISTENT_RESTORE -> persistentRuntimePack(
|
|
||||||
ModdedGenerationHistoryStorage.restoreOrAdopt(
|
|
||||||
server,
|
|
||||||
key,
|
|
||||||
pack,
|
|
||||||
packDimensionKey,
|
|
||||||
seed
|
|
||||||
),
|
|
||||||
generationMode
|
|
||||||
);
|
|
||||||
};
|
|
||||||
Handle existing = HANDLES.get(dimensionId);
|
|
||||||
if (existing != null && serverAccess.hasLevel(server, key)) {
|
|
||||||
Handle refreshed = new Handle(dimensionId, pack, packDimensionKey, seed, existing.level(), existing.generator());
|
|
||||||
HANDLES.put(dimensionId, refreshed);
|
|
||||||
return refreshed;
|
|
||||||
}
|
}
|
||||||
if (serverAccess.hasLevel(server, key)) {
|
|
||||||
NativeWorld present = level(server, dimensionId);
|
|
||||||
IrisModdedChunkGenerator generator = NativeWorldGenerators.find(present, IrisModdedChunkGenerator.class);
|
|
||||||
if (generator == null) {
|
|
||||||
throw new IllegalStateException("Iris cannot inject dimension '" + dimensionId + "': a non-Iris level with that id is already loaded");
|
|
||||||
}
|
|
||||||
ModdedIrisLog.warn("Iris dimension '{}' is already present in the running server; reusing it", dimensionId);
|
|
||||||
Handle handle = new Handle(dimensionId, pack, packDimensionKey, seed, present, generator);
|
|
||||||
HANDLES.put(dimensionId, handle);
|
|
||||||
return handle;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
Handle handle = inject(
|
RuntimePack runtimePack = persistentRuntimePack(active, ModdedGenerationMode.PERSISTENT_RESTORE);
|
||||||
server,
|
return new RestoringLevel(createLevel(server, serverAccess, requested, runtimePack), false);
|
||||||
serverAccess,
|
|
||||||
dimensionId,
|
|
||||||
key,
|
|
||||||
pack,
|
|
||||||
packDimensionKey,
|
|
||||||
seed,
|
|
||||||
runtimePack
|
|
||||||
);
|
|
||||||
HANDLES.put(dimensionId, handle);
|
|
||||||
ModdedIrisLog.info("Iris injected runtime dimension '{}' (pack={} dim={} seed={})", dimensionId, pack, packDimensionKey, seed);
|
|
||||||
return handle;
|
|
||||||
} catch (Throwable e) {
|
} catch (Throwable e) {
|
||||||
ModdedIrisLog.error("Iris failed to inject runtime dimension '{}' (pack={} dim={} seed={})", dimensionId, pack, packDimensionKey, seed, e);
|
throw injectionFailure(requested, e);
|
||||||
throw new IllegalStateException("Iris runtime dimension injection failed for " + dimensionId, e);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static Handle completeRestore(NativeModdedServer server, RestoringLevel restoring, Engine engine) {
|
||||||
|
if (restoring.present()) {
|
||||||
|
return restoring.handle();
|
||||||
|
}
|
||||||
|
NativeDimensionRuntime serverAccess = requireAccess();
|
||||||
|
synchronized (LOCK) {
|
||||||
|
try {
|
||||||
|
return register(publishLevel(server, serverAccess, restoring.handle(), engine));
|
||||||
|
} catch (Throwable e) {
|
||||||
|
throw injectionFailure(DimensionRequest.of(restoring.handle()), e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void abandonRestore(NativeModdedServer server, RestoringLevel restoring, Throwable failure) {
|
||||||
|
if (restoring.present()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
NativeDimensionRuntime serverAccess = requireAccess();
|
||||||
|
Handle handle = restoring.handle();
|
||||||
|
synchronized (LOCK) {
|
||||||
|
rollbackInjection(server, serverAccess, handle.dimensionId(), handle.level(), handle.generator(), false, failure);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Handle create(NativeModdedServer server, DimensionRequest requested, RuntimePackSource source) {
|
||||||
|
NativeDimensionRuntime serverAccess = requireAccess();
|
||||||
|
synchronized (LOCK) {
|
||||||
|
RuntimePack runtimePack = source.open();
|
||||||
|
Handle present = presentHandle(server, serverAccess, requested);
|
||||||
|
if (present != null) {
|
||||||
|
return present;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return register(publishLevel(server, serverAccess,
|
||||||
|
createLevel(server, serverAccess, requested, runtimePack), null));
|
||||||
|
} catch (Throwable e) {
|
||||||
|
throw injectionFailure(requested, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Handle presentHandle(NativeModdedServer server, NativeDimensionRuntime serverAccess, DimensionRequest requested) {
|
||||||
|
String dimensionId = requested.dimensionId();
|
||||||
|
Handle existing = HANDLES.get(dimensionId);
|
||||||
|
if (existing != null && serverAccess.hasLevel(server, dimensionId)) {
|
||||||
|
Handle refreshed = requested.bind(existing.level(), existing.generator());
|
||||||
|
HANDLES.put(dimensionId, refreshed);
|
||||||
|
return refreshed;
|
||||||
|
}
|
||||||
|
if (!serverAccess.hasLevel(server, dimensionId)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
NativeWorld present = level(server, dimensionId);
|
||||||
|
IrisModdedChunkGenerator generator = NativeWorldGenerators.find(present, IrisModdedChunkGenerator.class);
|
||||||
|
if (generator == null) {
|
||||||
|
throw new IllegalStateException("Iris cannot inject dimension '" + dimensionId + "': a non-Iris level with that id is already loaded");
|
||||||
|
}
|
||||||
|
ModdedIrisLog.warn("Iris dimension '{}' is already present in the running server; reusing it", dimensionId);
|
||||||
|
Handle handle = requested.bind(present, generator);
|
||||||
|
HANDLES.put(dimensionId, handle);
|
||||||
|
return handle;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Handle register(Handle handle) {
|
||||||
|
HANDLES.put(handle.dimensionId(), handle);
|
||||||
|
ModdedIrisLog.info("Iris injected runtime dimension '{}' (pack={} dim={} seed={})",
|
||||||
|
handle.dimensionId(), handle.pack(), handle.packDimensionKey(), handle.seed());
|
||||||
|
return handle;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static IllegalStateException injectionFailure(DimensionRequest requested, Throwable failure) {
|
||||||
|
ModdedIrisLog.error("Iris failed to inject runtime dimension '{}' (pack={} dim={} seed={})",
|
||||||
|
requested.dimensionId(), requested.pack(), requested.packDimensionKey(), requested.seed(), failure);
|
||||||
|
return new IllegalStateException("Iris runtime dimension injection failed for " + requested.dimensionId(), failure);
|
||||||
|
}
|
||||||
|
|
||||||
public static Handle createPersistent(NativeModdedServer server, String dimensionId, String pack, String packDimensionKey, long seed) {
|
public static Handle createPersistent(NativeModdedServer server, String dimensionId, String pack, String packDimensionKey, long seed) {
|
||||||
synchronized (LOCK) {
|
synchronized (LOCK) {
|
||||||
ModdedDimensionRegistryStore.PersistentDimension previous =
|
ModdedDimensionRegistryStore.PersistentDimension previous =
|
||||||
@@ -222,11 +227,17 @@ public final class ModdedDimensionManager {
|
|||||||
try {
|
try {
|
||||||
return create(
|
return create(
|
||||||
server,
|
server,
|
||||||
dimensionId,
|
new DimensionRequest(dimensionId, pack, packDimensionKey, seed),
|
||||||
pack,
|
() -> persistentRuntimePack(
|
||||||
packDimensionKey,
|
ModdedGenerationHistoryStorage.createOrStage(
|
||||||
seed,
|
server,
|
||||||
ModdedGenerationMode.PERSISTENT_CREATE
|
dimensionId,
|
||||||
|
pack,
|
||||||
|
packDimensionKey,
|
||||||
|
seed
|
||||||
|
),
|
||||||
|
ModdedGenerationMode.PERSISTENT_CREATE
|
||||||
|
)
|
||||||
);
|
);
|
||||||
} catch (Throwable e) {
|
} catch (Throwable e) {
|
||||||
try {
|
try {
|
||||||
@@ -392,16 +403,16 @@ public final class ModdedDimensionManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static Handle inject(
|
private static Handle createLevel(
|
||||||
NativeModdedServer server,
|
NativeModdedServer server,
|
||||||
NativeDimensionRuntime serverAccess,
|
NativeDimensionRuntime serverAccess,
|
||||||
String dimensionId,
|
DimensionRequest requested,
|
||||||
String key,
|
|
||||||
String pack,
|
|
||||||
String packDimensionKey,
|
|
||||||
long seed,
|
|
||||||
RuntimePack runtimePack
|
RuntimePack runtimePack
|
||||||
) {
|
) {
|
||||||
|
String dimensionId = requested.dimensionId();
|
||||||
|
String pack = requested.pack();
|
||||||
|
String packDimensionKey = requested.packDimensionKey();
|
||||||
|
long seed = requested.seed();
|
||||||
String typeRef = runtimePack.dimensionTypeKey();
|
String typeRef = runtimePack.dimensionTypeKey();
|
||||||
ModdedForcedDatapack.requireRegisteredDimensionType(typeRef,
|
ModdedForcedDatapack.requireRegisteredDimensionType(typeRef,
|
||||||
serverAccess.hasDimensionType(server, typeRef) ? Optional.of(Boolean.TRUE) : Optional.empty(),
|
serverAccess.hasDimensionType(server, typeRef) ? Optional.of(Boolean.TRUE) : Optional.empty(),
|
||||||
@@ -414,35 +425,54 @@ public final class ModdedDimensionManager {
|
|||||||
runtimePack.generationMode());
|
runtimePack.generationMode());
|
||||||
return generator;
|
return generator;
|
||||||
}));
|
}));
|
||||||
NativeWorld level = created.world();
|
try {
|
||||||
IrisModdedChunkGenerator generator = created.generator();
|
serverAccess.initialize(server, created.world());
|
||||||
|
return requested.bind(created.world(), created.generator());
|
||||||
|
} catch (Throwable error) {
|
||||||
|
rollbackInjection(server, serverAccess, dimensionId, created.world(), created.generator(), false, error);
|
||||||
|
throw rethrow(dimensionId, error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Handle publishLevel(
|
||||||
|
NativeModdedServer server,
|
||||||
|
NativeDimensionRuntime serverAccess,
|
||||||
|
Handle created,
|
||||||
|
Engine prepared
|
||||||
|
) {
|
||||||
|
NativeWorld level = created.level();
|
||||||
|
IrisModdedChunkGenerator generator = created.generator();
|
||||||
boolean loadEventStarted = false;
|
boolean loadEventStarted = false;
|
||||||
try {
|
try {
|
||||||
serverAccess.initialize(server, level);
|
if (prepared != null) {
|
||||||
|
ModdedWorldEngines.installPrepared(level, prepared);
|
||||||
|
}
|
||||||
generator.bindLevel(level);
|
generator.bindLevel(level);
|
||||||
Handle handle = new Handle(dimensionId, pack, packDimensionKey, seed, level, generator);
|
|
||||||
NativeWorld previous = serverAccess.publish(server, level);
|
NativeWorld previous = serverAccess.publish(server, level);
|
||||||
if (previous != null) {
|
if (previous != null) {
|
||||||
throw new IllegalStateException("Iris cannot inject dimension '" + dimensionId
|
throw new IllegalStateException("Iris cannot inject dimension '" + created.dimensionId()
|
||||||
+ "': the level was registered concurrently");
|
+ "': the level was registered concurrently");
|
||||||
}
|
}
|
||||||
serverAccess.addWorldBorderListener(server, level);
|
serverAccess.addWorldBorderListener(server, level);
|
||||||
loadEventStarted = true;
|
loadEventStarted = true;
|
||||||
ModdedEngineBootstrap.loader().fireDynamicLevelLoad(level);
|
ModdedEngineBootstrap.loader().fireDynamicLevelLoad(level);
|
||||||
return handle;
|
return created;
|
||||||
} catch (Throwable error) {
|
} catch (Throwable error) {
|
||||||
rollbackInjection(server, serverAccess, key, level, generator, loadEventStarted, error);
|
rollbackInjection(server, serverAccess, created.dimensionId(), level, generator, loadEventStarted, error);
|
||||||
if (error instanceof RuntimeException runtimeException) {
|
throw rethrow(created.dimensionId(), error);
|
||||||
throw runtimeException;
|
|
||||||
}
|
|
||||||
if (error instanceof Error fatalError) {
|
|
||||||
throw fatalError;
|
|
||||||
}
|
|
||||||
throw new IllegalStateException("Iris runtime dimension injection failed for " + dimensionId, error);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static RuntimeException rethrow(String dimensionId, Throwable error) {
|
||||||
|
if (error instanceof RuntimeException runtimeException) {
|
||||||
|
return runtimeException;
|
||||||
|
}
|
||||||
|
if (error instanceof Error fatalError) {
|
||||||
|
throw fatalError;
|
||||||
|
}
|
||||||
|
return new IllegalStateException("Iris runtime dimension injection failed for " + dimensionId, error);
|
||||||
|
}
|
||||||
|
|
||||||
private static void rollbackInjection(NativeModdedServer server, NativeDimensionRuntime serverAccess,
|
private static void rollbackInjection(NativeModdedServer server, NativeDimensionRuntime serverAccess,
|
||||||
String key, NativeWorld level,
|
String key, NativeWorld level,
|
||||||
IrisModdedChunkGenerator generator, boolean loadEventStarted,
|
IrisModdedChunkGenerator generator, boolean loadEventStarted,
|
||||||
@@ -544,9 +574,30 @@ public final class ModdedDimensionManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@FunctionalInterface
|
||||||
|
private interface RuntimePackSource {
|
||||||
|
RuntimePack open();
|
||||||
|
}
|
||||||
|
|
||||||
|
private record DimensionRequest(String dimensionId, String pack, String packDimensionKey, long seed) {
|
||||||
|
private static DimensionRequest of(Handle handle) {
|
||||||
|
return new DimensionRequest(handle.dimensionId(), handle.pack(), handle.packDimensionKey(), handle.seed());
|
||||||
|
}
|
||||||
|
|
||||||
|
private Handle bind(NativeWorld level, IrisModdedChunkGenerator generator) {
|
||||||
|
return new Handle(dimensionId, pack, packDimensionKey, seed, level, generator);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public record Handle(String dimensionId, String pack, String packDimensionKey, long seed, NativeWorld level, IrisModdedChunkGenerator generator) {
|
public record Handle(String dimensionId, String pack, String packDimensionKey, long seed, NativeWorld level, IrisModdedChunkGenerator generator) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
record RestoringLevel(Handle handle, boolean present) {
|
||||||
|
RestoringLevel {
|
||||||
|
handle = Objects.requireNonNull(handle, "handle");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public record UpdateResult(
|
public record UpdateResult(
|
||||||
boolean pending,
|
boolean pending,
|
||||||
boolean restartRequired,
|
boolean restartRequired,
|
||||||
|
|||||||
+15
@@ -28,6 +28,7 @@ import art.arcane.iris.generation.terrain.IrisDimension;
|
|||||||
import art.arcane.iris.generation.terrain.IrisRegion;
|
import art.arcane.iris.generation.terrain.IrisRegion;
|
||||||
import art.arcane.volmlib.nativelib.minecraft26_2.terrain.NativeResourceKeys;
|
import art.arcane.volmlib.nativelib.minecraft26_2.terrain.NativeResourceKeys;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
import java.util.LinkedHashSet;
|
import java.util.LinkedHashSet;
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
@@ -126,6 +127,20 @@ final class ModdedDimensionMetadata {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static void warmConfiguredPack(File packDirectory, String dimensionKey) {
|
||||||
|
ConfiguredPack configured = ConfiguredPack.load(packDirectory, dimensionKey);
|
||||||
|
collectConfiguredBiomeKeys(configured.dimension(), configured.data());
|
||||||
|
}
|
||||||
|
|
||||||
record ConfiguredPack(IrisData data, IrisDimension dimension, DimensionMetadata metadata) {
|
record ConfiguredPack(IrisData data, IrisDimension dimension, DimensionMetadata metadata) {
|
||||||
|
static ConfiguredPack load(File packDirectory, String dimensionKey) {
|
||||||
|
IrisData data = IrisData.get(packDirectory);
|
||||||
|
IrisDimension dimension = data.getDimensionLoader().load(dimensionKey);
|
||||||
|
if (dimension == null) {
|
||||||
|
throw new IllegalStateException("Iris dimension '" + dimensionKey
|
||||||
|
+ "' missing from pack " + packDirectory.getAbsolutePath());
|
||||||
|
}
|
||||||
|
return new ConfiguredPack(data, dimension, dimensionMetadata(dimension));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+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();
|
IrisModdedCommands.openDownloadAdmission();
|
||||||
ModdedStartup.prepareForStartup();
|
ModdedStartup.prepareForStartup();
|
||||||
|
ModdedStartup.prepareDimensionRestore(server);
|
||||||
IrisModdedChunkGenerator.startGenPool();
|
IrisModdedChunkGenerator.startGenPool();
|
||||||
bindWorldGenerators(server);
|
bindWorldGenerators(server);
|
||||||
services().enableAll();
|
services().enableAll();
|
||||||
@@ -164,6 +165,7 @@ public final class ModdedEngineBootstrap {
|
|||||||
finishPreviousServer("server start");
|
finishPreviousServer("server start");
|
||||||
captureInitialSpawn(server);
|
captureInitialSpawn(server);
|
||||||
ModdedStartup.prepareForStartup();
|
ModdedStartup.prepareForStartup();
|
||||||
|
ModdedStartup.prepareDimensionRestore(server);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void serverStarted(NativeModdedServer server) {
|
public static void serverStarted(NativeModdedServer server) {
|
||||||
|
|||||||
+68
-20
@@ -9,6 +9,7 @@ import art.arcane.iris.pack.BrokenPackException;
|
|||||||
import art.arcane.iris.pack.PackFingerprints;
|
import art.arcane.iris.pack.PackFingerprints;
|
||||||
import art.arcane.iris.pack.PackValidationRegistry;
|
import art.arcane.iris.pack.PackValidationRegistry;
|
||||||
import art.arcane.iris.pack.PackValidationResult;
|
import art.arcane.iris.pack.PackValidationResult;
|
||||||
|
import art.arcane.iris.world.history.GenerationAdmission;
|
||||||
import art.arcane.iris.world.history.GenerationEpoch;
|
import art.arcane.iris.world.history.GenerationEpoch;
|
||||||
import art.arcane.iris.world.history.GenerationEpochContractFactory;
|
import art.arcane.iris.world.history.GenerationEpochContractFactory;
|
||||||
import art.arcane.iris.world.history.GenerationHistory;
|
import art.arcane.iris.world.history.GenerationHistory;
|
||||||
@@ -53,7 +54,7 @@ final class ModdedGenerationHistoryStorage {
|
|||||||
ModdedGenerationHistoryStorage::requireRegistryDefinitions,
|
ModdedGenerationHistoryStorage::requireRegistryDefinitions,
|
||||||
IrisSettings.get().getGenerator().getGenerationTransitionWidthBlocks()
|
IrisSettings.get().getGenerator().getGenerationTransitionWidthBlocks()
|
||||||
);
|
);
|
||||||
return resolveActive(history);
|
return resolveActive(history, source);
|
||||||
} catch (IOException failure) {
|
} catch (IOException failure) {
|
||||||
throw historyFailure(levelKey, failure);
|
throw historyFailure(levelKey, failure);
|
||||||
}
|
}
|
||||||
@@ -68,25 +69,57 @@ final class ModdedGenerationHistoryStorage {
|
|||||||
) {
|
) {
|
||||||
Path dimensionRoot = dimensionRoot(server, levelKey);
|
Path dimensionRoot = dimensionRoot(server, levelKey);
|
||||||
try {
|
try {
|
||||||
GenerationHistory history = restoreOrAdopt(
|
GenerationHistory history = openForRestore(dimensionRoot, selectedPack, selectedDimensionKey, worldSeed);
|
||||||
dimensionRoot,
|
return resolveActive(history, installedPackSource(selectedPack));
|
||||||
worldSeed,
|
|
||||||
() -> {
|
|
||||||
Path source = selectedPackSource(selectedPack, selectedDimensionKey);
|
|
||||||
return new AdoptionSource(source, captureCandidate(
|
|
||||||
source,
|
|
||||||
selectedDimensionKey,
|
|
||||||
adoptionAliasPolicy(dimensionRoot)
|
|
||||||
));
|
|
||||||
},
|
|
||||||
ModdedGenerationHistoryStorage::requireRegistryDefinitions
|
|
||||||
);
|
|
||||||
return resolveActive(history);
|
|
||||||
} catch (IOException failure) {
|
} catch (IOException failure) {
|
||||||
throw historyFailure(levelKey, failure);
|
throw historyFailure(levelKey, failure);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static PreparedHistory prepareRestore(
|
||||||
|
NativeModdedServer server,
|
||||||
|
String levelKey,
|
||||||
|
String selectedPack,
|
||||||
|
String selectedDimensionKey,
|
||||||
|
long worldSeed
|
||||||
|
) {
|
||||||
|
Path dimensionRoot = dimensionRoot(server, levelKey);
|
||||||
|
try {
|
||||||
|
GenerationHistory history = openForRestore(dimensionRoot, selectedPack, selectedDimensionKey, worldSeed);
|
||||||
|
GenerationAdmission.RuntimeLease lease = history.retainRuntime();
|
||||||
|
try {
|
||||||
|
history.prepareCurrentGenerator(IrisSettings.get().getGenerator().getGenerationTransitionWidthBlocks());
|
||||||
|
return new PreparedHistory(resolveActive(history, installedPackSource(selectedPack)), lease);
|
||||||
|
} catch (IOException | RuntimeException | Error failure) {
|
||||||
|
lease.close();
|
||||||
|
throw failure;
|
||||||
|
}
|
||||||
|
} catch (IOException failure) {
|
||||||
|
throw historyFailure(levelKey, failure);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static GenerationHistory openForRestore(
|
||||||
|
Path dimensionRoot,
|
||||||
|
String selectedPack,
|
||||||
|
String selectedDimensionKey,
|
||||||
|
long worldSeed
|
||||||
|
) throws IOException {
|
||||||
|
return restoreOrAdopt(
|
||||||
|
dimensionRoot,
|
||||||
|
worldSeed,
|
||||||
|
() -> {
|
||||||
|
Path source = selectedPackSource(selectedPack, selectedDimensionKey);
|
||||||
|
return new AdoptionSource(source, captureCandidate(
|
||||||
|
source,
|
||||||
|
selectedDimensionKey,
|
||||||
|
adoptionAliasPolicy(dimensionRoot)
|
||||||
|
));
|
||||||
|
},
|
||||||
|
ModdedGenerationHistoryStorage::requireRegistryDefinitions
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
static ActivePack openOrAdopt(
|
static ActivePack openOrAdopt(
|
||||||
NativeWorld level,
|
NativeWorld level,
|
||||||
String selectedPack,
|
String selectedPack,
|
||||||
@@ -184,10 +217,10 @@ final class ModdedGenerationHistoryStorage {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static ActivePack resolveActive(GenerationHistory history) throws IOException {
|
static ActivePack resolveActive(GenerationHistory history, Path validatedSource) throws IOException {
|
||||||
GenerationEpoch epoch = history.activeEpoch();
|
GenerationEpoch epoch = history.activeEpoch();
|
||||||
Path packRoot = normalize(history.activePackRoot());
|
Path packRoot = normalize(history.activePackRoot());
|
||||||
validatePack(packRoot);
|
validatePack(packRoot, validatedSource);
|
||||||
validateDimension(packRoot, epoch);
|
validateDimension(packRoot, epoch);
|
||||||
return new ActivePack(
|
return new ActivePack(
|
||||||
packRoot,
|
packRoot,
|
||||||
@@ -197,9 +230,12 @@ final class ModdedGenerationHistoryStorage {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
static PackValidationResult validatePack(Path packRoot) {
|
static PackValidationResult validatePack(Path packRoot, Path validatedSource) {
|
||||||
PackValidationResult validation = PackValidationRegistry.getMatching(
|
String contentFingerprint = PackFingerprints.computePackTreeFingerprint(packRoot.toFile());
|
||||||
packRoot, PackFingerprints.computePackTreeFingerprint(packRoot.toFile()));
|
PackValidationResult validation = PackValidationRegistry.getMatching(packRoot, contentFingerprint);
|
||||||
|
if (validation == null && validatedSource != null) {
|
||||||
|
validation = PackValidationRegistry.publishMatchingCopy(validatedSource, packRoot, contentFingerprint);
|
||||||
|
}
|
||||||
if (validation == null) {
|
if (validation == null) {
|
||||||
validation = ModdedStartup.validatePack(packRoot.toFile());
|
validation = ModdedStartup.validatePack(packRoot.toFile());
|
||||||
}
|
}
|
||||||
@@ -286,6 +322,11 @@ final class ModdedGenerationHistoryStorage {
|
|||||||
return normalize(ModdedWorldEngines.resolvePack(pack, dimensionKey).toPath());
|
return normalize(ModdedWorldEngines.resolvePack(pack, dimensionKey).toPath());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static Path installedPackSource(String pack) {
|
||||||
|
File folder = ModdedWorldEngines.packFolder(pack);
|
||||||
|
return folder.isDirectory() ? normalize(folder.toPath()) : null;
|
||||||
|
}
|
||||||
|
|
||||||
private static GenerationRegistryContract requireRegistryDefinitions(
|
private static GenerationRegistryContract requireRegistryDefinitions(
|
||||||
Collection<GenerationRegistryContract> contracts
|
Collection<GenerationRegistryContract> contracts
|
||||||
)
|
)
|
||||||
@@ -346,6 +387,13 @@ final class ModdedGenerationHistoryStorage {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
record PreparedHistory(ActivePack active, GenerationAdmission.RuntimeLease lease) {
|
||||||
|
PreparedHistory {
|
||||||
|
active = Objects.requireNonNull(active, "active");
|
||||||
|
lease = Objects.requireNonNull(lease, "lease");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
record Candidate(
|
record Candidate(
|
||||||
String packFingerprint,
|
String packFingerprint,
|
||||||
GenerationEpoch.DimensionContract dimensionContract,
|
GenerationEpoch.DimensionContract dimensionContract,
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ import art.arcane.iris.pack.PackValidationRegistry;
|
|||||||
import art.arcane.iris.pack.PackValidationCache;
|
import art.arcane.iris.pack.PackValidationCache;
|
||||||
import art.arcane.iris.pack.PackValidationResult;
|
import art.arcane.iris.pack.PackValidationResult;
|
||||||
import art.arcane.iris.pack.PackValidator;
|
import art.arcane.iris.pack.PackValidator;
|
||||||
|
import art.arcane.iris.generation.runtime.Engine;
|
||||||
import art.arcane.iris.modded.command.ModdedPackCommands;
|
import art.arcane.iris.modded.command.ModdedPackCommands;
|
||||||
import art.arcane.iris.spi.IrisPlatforms;
|
import art.arcane.iris.spi.IrisPlatforms;
|
||||||
import art.arcane.iris.world.safeguard.GenerationRefusalNotice;
|
import art.arcane.iris.world.safeguard.GenerationRefusalNotice;
|
||||||
@@ -39,19 +40,29 @@ import java.util.List;
|
|||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.concurrent.CopyOnWriteArrayList;
|
import java.util.concurrent.CopyOnWriteArrayList;
|
||||||
import java.util.concurrent.atomic.AtomicBoolean;
|
import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
|
import java.util.concurrent.atomic.AtomicReference;
|
||||||
|
|
||||||
public final class ModdedStartup {
|
public final class ModdedStartup {
|
||||||
private static final int COMPAT_BOOT_KEY_CAP = 3;
|
private static final int COMPAT_BOOT_KEY_CAP = 3;
|
||||||
private static final AtomicBoolean PREPARED = new AtomicBoolean(false);
|
private static final AtomicBoolean PREPARED = new AtomicBoolean(false);
|
||||||
private static final AtomicBoolean STARTED = new AtomicBoolean(false);
|
private static final AtomicBoolean STARTED = new AtomicBoolean(false);
|
||||||
|
private static final AtomicBoolean RESTORE_BEGUN = new AtomicBoolean(false);
|
||||||
|
private static final AtomicReference<ModdedDimensionRestore<ModdedWorldEngines.PreparedPack, ModdedDimensionManager.RestoringLevel, Engine>> RESTORE =
|
||||||
|
new AtomicReference<>();
|
||||||
private static final List<String> DIMENSION_FAILURES = new CopyOnWriteArrayList<>();
|
private static final List<String> DIMENSION_FAILURES = new CopyOnWriteArrayList<>();
|
||||||
|
|
||||||
private ModdedStartup() {
|
private ModdedStartup() {
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void reset() {
|
public static void reset() {
|
||||||
|
ModdedDimensionRestore<ModdedWorldEngines.PreparedPack, ModdedDimensionManager.RestoringLevel, Engine> restore =
|
||||||
|
RESTORE.getAndSet(null);
|
||||||
|
if (restore != null) {
|
||||||
|
restore.close();
|
||||||
|
}
|
||||||
PREPARED.set(false);
|
PREPARED.set(false);
|
||||||
STARTED.set(false);
|
STARTED.set(false);
|
||||||
|
RESTORE_BEGUN.set(false);
|
||||||
DIMENSION_FAILURES.clear();
|
DIMENSION_FAILURES.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -62,6 +73,24 @@ public final class ModdedStartup {
|
|||||||
validateAllPacks();
|
validateAllPacks();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static void prepareDimensionRestore(NativeModdedServer server) {
|
||||||
|
if (server == null || !RESTORE_BEGUN.compareAndSet(false, true)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
List<ModdedDimensionRegistryStore.PersistentDimension> dimensions =
|
||||||
|
ModdedDimensionRegistryStore.loadForStartup(server);
|
||||||
|
if (dimensions.isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
long startedAt = System.currentTimeMillis();
|
||||||
|
ModdedDimensionRestore<ModdedWorldEngines.PreparedPack, ModdedDimensionManager.RestoringLevel, Engine> restore =
|
||||||
|
ModdedDimensionRestore.start(dimensions, new PersistentRestoreSteps(server));
|
||||||
|
RESTORE.set(restore);
|
||||||
|
restore.awaitPreparations();
|
||||||
|
ModdedIrisLog.info("Iris prepared {} persistent dimension(s) during world load in {}ms",
|
||||||
|
dimensions.size(), System.currentTimeMillis() - startedAt);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Boot trigger for the forced datapack. Runs on its own daemon thread rather than the Iris scheduler:
|
* Boot trigger for the forced datapack. Runs on its own daemon thread rather than the Iris scheduler:
|
||||||
* ModdedEngineBootstrap.start clears the async queue at SERVER_STARTING, which would silently drop this
|
* ModdedEngineBootstrap.start clears the async queue at SERVER_STARTING, which would silently drop this
|
||||||
@@ -219,38 +248,46 @@ public final class ModdedStartup {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static void reinjectPersistentDimensions(NativeModdedServer server) {
|
static void reinjectPersistentDimensions(NativeModdedServer server) {
|
||||||
List<ModdedDimensionRegistryStore.PersistentDimension> dimensions =
|
prepareDimensionRestore(server);
|
||||||
ModdedDimensionRegistryStore.loadForStartup(server);
|
ModdedDimensionRestore<ModdedWorldEngines.PreparedPack, ModdedDimensionManager.RestoringLevel, Engine> restore =
|
||||||
if (dimensions.isEmpty()) {
|
RESTORE.getAndSet(null);
|
||||||
|
if (restore == null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
finishDimensionRestore(restore);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void finishDimensionRestore(ModdedDimensionRestore<?, ?, ?> restore) {
|
||||||
|
long startedAt = System.currentTimeMillis();
|
||||||
|
List<ModdedDimensionRestore.Result> results;
|
||||||
|
try (restore) {
|
||||||
|
results = restore.finish();
|
||||||
|
}
|
||||||
|
long waited = System.currentTimeMillis() - startedAt;
|
||||||
int injected = 0;
|
int injected = 0;
|
||||||
int index = 0;
|
int index = 0;
|
||||||
long startedAt = System.currentTimeMillis();
|
OutOfMemoryError outOfMemory = null;
|
||||||
for (ModdedDimensionRegistryStore.PersistentDimension dimension : dimensions) {
|
for (ModdedDimensionRestore.Result result : results) {
|
||||||
index++;
|
index++;
|
||||||
long dimensionStartedAt = System.currentTimeMillis();
|
ModdedDimensionRegistryStore.PersistentDimension dimension = result.dimension();
|
||||||
try {
|
Throwable failure = result.failure();
|
||||||
ModdedDimensionManager.restorePersistent(
|
if (failure == null) {
|
||||||
server,
|
|
||||||
dimension.id(),
|
|
||||||
dimension.pack(),
|
|
||||||
dimension.dimension(),
|
|
||||||
dimension.seed()
|
|
||||||
);
|
|
||||||
injected++;
|
injected++;
|
||||||
ModdedIrisLog.info("Iris re-injected {}/{} '{}' (pack={} dim={}) in {}ms",
|
ModdedIrisLog.info("Iris re-injected {}/{} '{}' (pack={} dim={}): prepared {}ms, engine {}ms",
|
||||||
index, dimensions.size(), dimension.id(), dimension.pack(), dimension.dimension(),
|
index, results.size(), dimension.id(), dimension.pack(), dimension.dimension(),
|
||||||
System.currentTimeMillis() - dimensionStartedAt);
|
result.preparedMillis(), result.builtMillis());
|
||||||
} catch (Throwable e) {
|
continue;
|
||||||
reportDimensionFailure(dimension, e);
|
}
|
||||||
if (e instanceof OutOfMemoryError outOfMemory) {
|
reportDimensionFailure(dimension, failure);
|
||||||
throw outOfMemory;
|
if (failure instanceof OutOfMemoryError fatal && outOfMemory == null) {
|
||||||
}
|
outOfMemory = fatal;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
ModdedIrisLog.info("Iris re-injected {}/{} persistent dimension(s) at startup in {}ms",
|
ModdedIrisLog.info("Iris re-injected {}/{} persistent dimension(s) at startup; server thread waited {}ms",
|
||||||
injected, dimensions.size(), System.currentTimeMillis() - startedAt);
|
injected, results.size(), waited);
|
||||||
|
if (outOfMemory != null) {
|
||||||
|
throw outOfMemory;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void reportDimensionFailure(ModdedDimensionRegistryStore.PersistentDimension dimension, Throwable failure) {
|
private static void reportDimensionFailure(ModdedDimensionRegistryStore.PersistentDimension dimension, Throwable failure) {
|
||||||
@@ -304,4 +341,43 @@ public final class ModdedStartup {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private record PersistentRestoreSteps(NativeModdedServer server)
|
||||||
|
implements ModdedDimensionRestore.Steps<ModdedWorldEngines.PreparedPack, ModdedDimensionManager.RestoringLevel, Engine> {
|
||||||
|
@Override
|
||||||
|
public ModdedWorldEngines.PreparedPack prepare(ModdedDimensionRegistryStore.PersistentDimension dimension) {
|
||||||
|
return ModdedWorldEngines.preparePersistent(server, dimension);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ModdedDimensionManager.RestoringLevel begin(
|
||||||
|
ModdedDimensionRegistryStore.PersistentDimension dimension,
|
||||||
|
ModdedWorldEngines.PreparedPack prepared
|
||||||
|
) {
|
||||||
|
return ModdedDimensionManager.beginRestore(server, dimension, prepared.active());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Engine build(ModdedDimensionManager.RestoringLevel level, ModdedWorldEngines.PreparedPack prepared) {
|
||||||
|
if (level.present()) {
|
||||||
|
prepared.close();
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return ModdedWorldEngines.createPrepared(level.handle().level(), level.handle().seed(), prepared);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void complete(ModdedDimensionManager.RestoringLevel level, Engine engine) {
|
||||||
|
ModdedDimensionManager.completeRestore(server, level, engine);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void abandon(ModdedDimensionManager.RestoringLevel level, Throwable failure) {
|
||||||
|
ModdedDimensionManager.abandonRestore(server, level, failure);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void discard(ModdedWorldEngines.PreparedPack prepared) {
|
||||||
|
prepared.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
+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.Engine;
|
||||||
import art.arcane.iris.generation.runtime.EngineTarget;
|
import art.arcane.iris.generation.runtime.EngineTarget;
|
||||||
import art.arcane.iris.world.history.GenerationActivation;
|
import art.arcane.iris.world.history.GenerationActivation;
|
||||||
|
import art.arcane.iris.world.history.GenerationAdmission;
|
||||||
import art.arcane.iris.world.history.GenerationEpoch;
|
import art.arcane.iris.world.history.GenerationEpoch;
|
||||||
import art.arcane.iris.world.history.GenerationEpochContractFactory;
|
import art.arcane.iris.world.history.GenerationEpochContractFactory;
|
||||||
import art.arcane.iris.world.history.GenerationHistory;
|
import art.arcane.iris.world.history.GenerationHistory;
|
||||||
@@ -128,6 +129,43 @@ public final class ModdedWorldEngines {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static PreparedPack preparePersistent(
|
||||||
|
NativeModdedServer server,
|
||||||
|
ModdedDimensionRegistryStore.PersistentDimension dimension
|
||||||
|
) {
|
||||||
|
ModdedEngineBootstrap.bind();
|
||||||
|
ModdedGenerationHistoryStorage.PreparedHistory prepared = ModdedGenerationHistoryStorage.prepareRestore(
|
||||||
|
server,
|
||||||
|
dimension.id(),
|
||||||
|
dimension.pack(),
|
||||||
|
dimension.dimension(),
|
||||||
|
dimension.seed()
|
||||||
|
);
|
||||||
|
try {
|
||||||
|
ModdedDimensionMetadata.warmConfiguredPack(prepared.active().packRoot().toFile(), prepared.active().dimensionKey());
|
||||||
|
return new PreparedPack(prepared.active(), prepared.lease(), loadPack(prepared.active()));
|
||||||
|
} catch (RuntimeException | Error failure) {
|
||||||
|
prepared.lease().close();
|
||||||
|
throw failure;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static Engine createPrepared(NativeWorld level, long seedOverride, PreparedPack prepared) {
|
||||||
|
ModdedEngineBootstrap.bind();
|
||||||
|
long seed = seedOverride == Long.MIN_VALUE ? level.seed() : seedOverride;
|
||||||
|
try (prepared) {
|
||||||
|
return attachHistoryEngine(level, seed, prepared.active(), prepared.take());
|
||||||
|
} catch (IOException failure) {
|
||||||
|
throw new IllegalStateException("Iris generation history could not initialize runtime routing for '"
|
||||||
|
+ level.name() + "'.", failure);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void installPrepared(NativeWorld level, Engine engine) {
|
||||||
|
applyWorldBoundary(level, engine);
|
||||||
|
installReplacement(level, engine);
|
||||||
|
}
|
||||||
|
|
||||||
static void closeUnregistered(Engine engine) {
|
static void closeUnregistered(Engine engine) {
|
||||||
close(engine);
|
close(engine);
|
||||||
ENGINES.entrySet().removeIf((Map.Entry<NativeWorld, Engine> entry) -> entry.getValue() == engine);
|
ENGINES.entrySet().removeIf((Map.Entry<NativeWorld, Engine> entry) -> entry.getValue() == engine);
|
||||||
@@ -142,9 +180,13 @@ public final class ModdedWorldEngines {
|
|||||||
ModdedGenerationMode generationMode
|
ModdedGenerationMode generationMode
|
||||||
) {
|
) {
|
||||||
Engine engine = create(level, pack, dimensionKey, seedOverride, generationMode);
|
Engine engine = create(level, pack, dimensionKey, seedOverride, generationMode);
|
||||||
|
applyWorldBoundary(level, engine);
|
||||||
|
return engine;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void applyWorldBoundary(NativeWorld level, Engine engine) {
|
||||||
try {
|
try {
|
||||||
engine.getPlatformHooks().applyWorldBoundary(engine);
|
engine.getPlatformHooks().applyWorldBoundary(engine);
|
||||||
return engine;
|
|
||||||
} catch (Throwable failure) {
|
} catch (Throwable failure) {
|
||||||
try {
|
try {
|
||||||
close(engine);
|
close(engine);
|
||||||
@@ -197,57 +239,63 @@ public final class ModdedWorldEngines {
|
|||||||
GenerationHistory history = opened.history();
|
GenerationHistory history = opened.history();
|
||||||
try (GenerationAdmission.RuntimeLease startupAdmission = history.retainRuntime()) {
|
try (GenerationAdmission.RuntimeLease startupAdmission = history.retainRuntime()) {
|
||||||
history.prepareCurrentGenerator(IrisSettings.get().getGenerator().getGenerationTransitionWidthBlocks());
|
history.prepareCurrentGenerator(IrisSettings.get().getGenerator().getGenerationTransitionWidthBlocks());
|
||||||
opened = ModdedGenerationHistoryStorage.resolveActive(history);
|
ModdedGenerationHistoryStorage.ActivePack active = ModdedGenerationHistoryStorage.resolveActive(history, null);
|
||||||
long openedActivationId = history.activeActivation().activationId();
|
return attachHistoryEngine(level, seed, active, loadPack(active));
|
||||||
IrisEngine candidate = buildEngine(level, seed, opened);
|
}
|
||||||
boolean ready = false;
|
}
|
||||||
try {
|
|
||||||
GenerationHistoryRuntimeRouter router = candidate.attachGenerationHistory(
|
private static IrisEngine attachHistoryEngine(
|
||||||
|
NativeWorld level,
|
||||||
|
long seed,
|
||||||
|
ModdedGenerationHistoryStorage.ActivePack active,
|
||||||
|
LoadedPack loaded
|
||||||
|
) throws IOException {
|
||||||
|
GenerationHistory history = active.history();
|
||||||
|
long openedActivationId = history.activeActivation().activationId();
|
||||||
|
IrisEngine candidate = buildEngine(level, seed, active, loaded);
|
||||||
|
boolean ready = false;
|
||||||
|
try {
|
||||||
|
GenerationHistoryRuntimeRouter router = candidate.attachGenerationHistory(
|
||||||
|
history,
|
||||||
|
IrisBoundarySignatureSampler.INSTANCE,
|
||||||
|
IrisSettings.get().getGenerator().getGenerationTransitionWidthBlocks()
|
||||||
|
);
|
||||||
|
router.preloadActiveRuntimes();
|
||||||
|
if (history.activeActivation().activationId() != openedActivationId) {
|
||||||
|
close(candidate);
|
||||||
|
ModdedGenerationHistoryStorage.ActivePack promoted =
|
||||||
|
ModdedGenerationHistoryStorage.resolveActive(history, null);
|
||||||
|
candidate = buildEngine(level, seed, promoted, loadPack(promoted));
|
||||||
|
router = GenerationHistoryRuntimeRouter.attach(
|
||||||
|
candidate,
|
||||||
history,
|
history,
|
||||||
IrisBoundarySignatureSampler.INSTANCE,
|
IrisBoundarySignatureSampler.INSTANCE
|
||||||
IrisSettings.get().getGenerator().getGenerationTransitionWidthBlocks()
|
|
||||||
);
|
);
|
||||||
router.preloadActiveRuntimes();
|
router.preloadActiveRuntimes();
|
||||||
if (history.activeActivation().activationId() != openedActivationId) {
|
}
|
||||||
close(candidate);
|
requireReady(candidate, level, ModdedGenerationMode.PERSISTENT_RESTORE);
|
||||||
ModdedGenerationHistoryStorage.ActivePack promoted =
|
ready = true;
|
||||||
ModdedGenerationHistoryStorage.resolveActive(history);
|
ModdedIrisLog.info("Iris engine up for {}: pack={} dim={} seed={} height={}..{} activation={}",
|
||||||
candidate = buildEngine(level, seed, promoted);
|
level.name(),
|
||||||
router = GenerationHistoryRuntimeRouter.attach(
|
candidate.getData().getDataFolder().getAbsolutePath(),
|
||||||
candidate,
|
candidate.getDimension().getLoadKey(),
|
||||||
history,
|
seed,
|
||||||
IrisBoundarySignatureSampler.INSTANCE
|
candidate.getDimension().getMinHeight(),
|
||||||
);
|
candidate.getDimension().getMaxHeight(),
|
||||||
router.preloadActiveRuntimes();
|
history.activeActivation().activationId());
|
||||||
}
|
return candidate;
|
||||||
requireReady(candidate, level, ModdedGenerationMode.PERSISTENT_RESTORE);
|
} finally {
|
||||||
ready = true;
|
if (!ready) {
|
||||||
ModdedIrisLog.info("Iris engine up for {}: pack={} dim={} seed={} height={}..{} activation={}",
|
close(candidate);
|
||||||
level.name(),
|
|
||||||
candidate.getData().getDataFolder().getAbsolutePath(),
|
|
||||||
candidate.getDimension().getLoadKey(),
|
|
||||||
seed,
|
|
||||||
candidate.getDimension().getMinHeight(),
|
|
||||||
candidate.getDimension().getMaxHeight(),
|
|
||||||
history.activeActivation().activationId());
|
|
||||||
return candidate;
|
|
||||||
} finally {
|
|
||||||
if (!ready) {
|
|
||||||
close(candidate);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static IrisEngine buildEngine(
|
private static LoadedPack loadPack(ModdedGenerationHistoryStorage.ActivePack active) {
|
||||||
NativeWorld level,
|
|
||||||
long seed,
|
|
||||||
ModdedGenerationHistoryStorage.ActivePack active
|
|
||||||
) throws IOException {
|
|
||||||
File packDir = active.packRoot().toFile();
|
File packDir = active.packRoot().toFile();
|
||||||
PackValidationRegistry.requireLoadable(active.packRoot());
|
PackValidationRegistry.requireLoadable(active.packRoot());
|
||||||
IrisData data = IrisData.openRuntime(packDir);
|
IrisData data = IrisData.openRuntime(packDir);
|
||||||
boolean runtimeOwnsData = false;
|
boolean loaded = false;
|
||||||
try {
|
try {
|
||||||
IrisDimension dimension = data.getDimensionLoader().load(active.dimensionKey());
|
IrisDimension dimension = data.getDimensionLoader().load(active.dimensionKey());
|
||||||
if (dimension == null) {
|
if (dimension == null) {
|
||||||
@@ -256,7 +304,26 @@ public final class ModdedWorldEngines {
|
|||||||
throw new IllegalStateException("Iris dimension '" + active.dimensionKey()
|
throw new IllegalStateException("Iris dimension '" + active.dimensionKey()
|
||||||
+ "' missing from immutable pack " + packDir.getAbsolutePath());
|
+ "' missing from immutable pack " + packDir.getAbsolutePath());
|
||||||
}
|
}
|
||||||
|
data.generationPackFingerprint();
|
||||||
|
loaded = true;
|
||||||
|
return new LoadedPack(data, dimension);
|
||||||
|
} finally {
|
||||||
|
if (!loaded) {
|
||||||
|
data.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static IrisEngine buildEngine(
|
||||||
|
NativeWorld level,
|
||||||
|
long seed,
|
||||||
|
ModdedGenerationHistoryStorage.ActivePack active,
|
||||||
|
LoadedPack loaded
|
||||||
|
) throws IOException {
|
||||||
|
IrisData data = loaded.data();
|
||||||
|
IrisDimension dimension = loaded.dimension();
|
||||||
|
boolean runtimeOwnsData = false;
|
||||||
|
try {
|
||||||
validateDimensionContract(active.dimensionContract(), dimension, level);
|
validateDimensionContract(active.dimensionContract(), dimension, level);
|
||||||
File worldFolder = NativeModdedServer.forWorld(level).dimensionFolder(level.name()).toFile();
|
File worldFolder = NativeModdedServer.forWorld(level).dimensionFolder(level.name()).toFile();
|
||||||
IrisWorld world = buildWorld(level, seed, dimension, worldFolder);
|
IrisWorld world = buildWorld(level, seed, dimension, worldFolder);
|
||||||
@@ -428,6 +495,55 @@ public final class ModdedWorldEngines {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private record LoadedPack(IrisData data, IrisDimension dimension) {
|
||||||
|
private LoadedPack {
|
||||||
|
data = Objects.requireNonNull(data, "data");
|
||||||
|
dimension = Objects.requireNonNull(dimension, "dimension");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static final class PreparedPack implements AutoCloseable {
|
||||||
|
private final ModdedGenerationHistoryStorage.ActivePack active;
|
||||||
|
private final GenerationAdmission.RuntimeLease lease;
|
||||||
|
private LoadedPack loaded;
|
||||||
|
|
||||||
|
private PreparedPack(
|
||||||
|
ModdedGenerationHistoryStorage.ActivePack active,
|
||||||
|
GenerationAdmission.RuntimeLease lease,
|
||||||
|
LoadedPack loaded
|
||||||
|
) {
|
||||||
|
this.active = Objects.requireNonNull(active, "active");
|
||||||
|
this.lease = Objects.requireNonNull(lease, "lease");
|
||||||
|
this.loaded = Objects.requireNonNull(loaded, "loaded");
|
||||||
|
}
|
||||||
|
|
||||||
|
ModdedGenerationHistoryStorage.ActivePack active() {
|
||||||
|
return active;
|
||||||
|
}
|
||||||
|
|
||||||
|
private synchronized LoadedPack take() {
|
||||||
|
LoadedPack taken = loaded;
|
||||||
|
if (taken == null) {
|
||||||
|
throw new IllegalStateException("Iris prepared pack for " + active.packRoot() + " was already used");
|
||||||
|
}
|
||||||
|
loaded = null;
|
||||||
|
return taken;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public synchronized void close() {
|
||||||
|
LoadedPack unused = loaded;
|
||||||
|
loaded = null;
|
||||||
|
try {
|
||||||
|
if (unused != null) {
|
||||||
|
unused.data().close();
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
lease.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public static void shutdown() {
|
public static void shutdown() {
|
||||||
Throwable failure = null;
|
Throwable failure = null;
|
||||||
for (Map.Entry<NativeWorld, Engine> entry : new ArrayList<>(ENGINES.entrySet())) {
|
for (Map.Entry<NativeWorld, Engine> entry : new ArrayList<>(ENGINES.entrySet())) {
|
||||||
|
|||||||
+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.assertSame;
|
||||||
import static org.junit.Assert.fail;
|
import static org.junit.Assert.fail;
|
||||||
import static org.mockito.Mockito.mockStatic;
|
import static org.mockito.Mockito.mockStatic;
|
||||||
|
import static org.mockito.Mockito.never;
|
||||||
import static org.mockito.Mockito.times;
|
import static org.mockito.Mockito.times;
|
||||||
|
|
||||||
public class ModdedPackValidationPublicationTest {
|
public class ModdedPackValidationPublicationTest {
|
||||||
@@ -41,8 +42,8 @@ public class ModdedPackValidationPublicationTest {
|
|||||||
PackValidationResult result = loadable(root);
|
PackValidationResult result = loadable(root);
|
||||||
try (MockedStatic<PackValidator> validator = mockStatic(PackValidator.class)) {
|
try (MockedStatic<PackValidator> validator = mockStatic(PackValidator.class)) {
|
||||||
validator.when(() -> PackValidator.validate(root)).thenReturn(result);
|
validator.when(() -> PackValidator.validate(root)).thenReturn(result);
|
||||||
assertSame(result, ModdedGenerationHistoryStorage.validatePack(root.toPath()));
|
assertSame(result, ModdedGenerationHistoryStorage.validatePack(root.toPath(), null));
|
||||||
assertSame(result, ModdedGenerationHistoryStorage.validatePack(root.toPath()));
|
assertSame(result, ModdedGenerationHistoryStorage.validatePack(root.toPath(), null));
|
||||||
validator.verify(() -> PackValidator.validate(root), times(1));
|
validator.verify(() -> PackValidator.validate(root), times(1));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -56,13 +57,13 @@ public class ModdedPackValidationPublicationTest {
|
|||||||
PackValidationResult broken = new PackValidationResult(root.getName(), List.of("invalid"), List.of(), 1L);
|
PackValidationResult broken = new PackValidationResult(root.getName(), List.of("invalid"), List.of(), 1L);
|
||||||
try (MockedStatic<PackValidator> validator = mockStatic(PackValidator.class)) {
|
try (MockedStatic<PackValidator> validator = mockStatic(PackValidator.class)) {
|
||||||
validator.when(() -> PackValidator.validate(root)).thenReturn(loadable(root), broken);
|
validator.when(() -> PackValidator.validate(root)).thenReturn(loadable(root), broken);
|
||||||
ModdedGenerationHistoryStorage.validatePack(root.toPath());
|
ModdedGenerationHistoryStorage.validatePack(root.toPath(), null);
|
||||||
Files.writeString(content.toPath(), "wrong");
|
Files.writeString(content.toPath(), "wrong");
|
||||||
Files.setLastModifiedTime(content.toPath(), timestamp);
|
Files.setLastModifiedTime(content.toPath(), timestamp);
|
||||||
assertThrows(BrokenPackException.class,
|
assertThrows(BrokenPackException.class,
|
||||||
() -> ModdedGenerationHistoryStorage.validatePack(root.toPath()));
|
() -> ModdedGenerationHistoryStorage.validatePack(root.toPath(), null));
|
||||||
assertThrows(BrokenPackException.class,
|
assertThrows(BrokenPackException.class,
|
||||||
() -> ModdedGenerationHistoryStorage.validatePack(root.toPath()));
|
() -> ModdedGenerationHistoryStorage.validatePack(root.toPath(), null));
|
||||||
validator.verify(() -> PackValidator.validate(root), times(2));
|
validator.verify(() -> PackValidator.validate(root), times(2));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -77,9 +78,9 @@ public class ModdedPackValidationPublicationTest {
|
|||||||
MockedStatic<PackValidationCache> cache = mockStatic(PackValidationCache.class)) {
|
MockedStatic<PackValidationCache> cache = mockStatic(PackValidationCache.class)) {
|
||||||
cache.when(PackValidationCache::contextFingerprint).thenAnswer(ignored -> context.get());
|
cache.when(PackValidationCache::contextFingerprint).thenAnswer(ignored -> context.get());
|
||||||
validator.when(() -> PackValidator.validate(root)).thenReturn(first, second);
|
validator.when(() -> PackValidator.validate(root)).thenReturn(first, second);
|
||||||
assertSame(first, ModdedGenerationHistoryStorage.validatePack(root.toPath()));
|
assertSame(first, ModdedGenerationHistoryStorage.validatePack(root.toPath(), null));
|
||||||
context.set("second");
|
context.set("second");
|
||||||
assertSame(second, ModdedGenerationHistoryStorage.validatePack(root.toPath()));
|
assertSame(second, ModdedGenerationHistoryStorage.validatePack(root.toPath(), null));
|
||||||
validator.verify(() -> PackValidator.validate(root), times(2));
|
validator.verify(() -> PackValidator.validate(root), times(2));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -189,6 +190,42 @@ public class ModdedPackValidationPublicationTest {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void snapshotReusesTheValidationOfAnIdenticalValidatedSource() throws Exception {
|
||||||
|
File source = temporaryFolder.newFolder("overworld");
|
||||||
|
Files.writeString(new File(source, "dimension.json").toPath(), "terrain");
|
||||||
|
File snapshot = temporaryFolder.newFolder("epoch", "pack");
|
||||||
|
Files.writeString(new File(snapshot, "dimension.json").toPath(), "terrain");
|
||||||
|
PackValidationResult sourceResult = loadable(source);
|
||||||
|
try (MockedStatic<PackValidator> validator = mockStatic(PackValidator.class)) {
|
||||||
|
validator.when(() -> PackValidator.validate(source)).thenReturn(sourceResult);
|
||||||
|
ModdedStartup.validatePack(source);
|
||||||
|
|
||||||
|
assertSame(sourceResult, ModdedGenerationHistoryStorage.validatePack(snapshot.toPath(), source.toPath()));
|
||||||
|
|
||||||
|
validator.verify(() -> PackValidator.validate(snapshot), never());
|
||||||
|
assertSame(sourceResult, PackValidationRegistry.requireLoadable(snapshot.toPath()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void snapshotThatDiffersFromItsSourceIsValidatedOnItsOwn() throws Exception {
|
||||||
|
File source = temporaryFolder.newFolder("overworld");
|
||||||
|
Files.writeString(new File(source, "dimension.json").toPath(), "terrain");
|
||||||
|
File snapshot = temporaryFolder.newFolder("epoch", "pack");
|
||||||
|
Files.writeString(new File(snapshot, "dimension.json").toPath(), "older terrain");
|
||||||
|
PackValidationResult snapshotResult = loadable(snapshot);
|
||||||
|
try (MockedStatic<PackValidator> validator = mockStatic(PackValidator.class)) {
|
||||||
|
validator.when(() -> PackValidator.validate(source)).thenReturn(loadable(source));
|
||||||
|
validator.when(() -> PackValidator.validate(snapshot)).thenReturn(snapshotResult);
|
||||||
|
ModdedStartup.validatePack(source);
|
||||||
|
|
||||||
|
assertSame(snapshotResult, ModdedGenerationHistoryStorage.validatePack(snapshot.toPath(), source.toPath()));
|
||||||
|
|
||||||
|
validator.verify(() -> PackValidator.validate(snapshot), times(1));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private static PackValidationResult loadable(File root) {
|
private static PackValidationResult loadable(File root) {
|
||||||
return new PackValidationResult(root.getName(), List.of(), List.of(), System.currentTimeMillis());
|
return new PackValidationResult(root.getName(), List.of(), List.of(), System.currentTimeMillis());
|
||||||
}
|
}
|
||||||
|
|||||||
+33
-10
@@ -1,7 +1,6 @@
|
|||||||
package art.arcane.iris.modded;
|
package art.arcane.iris.modded;
|
||||||
|
|
||||||
import art.arcane.iris.pack.PackValidationRegistry;
|
import art.arcane.iris.pack.PackValidationRegistry;
|
||||||
import art.arcane.volmlib.nativelib.minecraft26_2.modded.NativeModdedServer;
|
|
||||||
import art.arcane.volmlib.nativelib.minecraft26_2.modded.NativeProtocolPlayer;
|
import art.arcane.volmlib.nativelib.minecraft26_2.modded.NativeProtocolPlayer;
|
||||||
import org.junit.After;
|
import org.junit.After;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
@@ -32,19 +31,12 @@ public class ModdedStartupReinjectFailureTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void aDimensionThatFailsToReinjectIsBanneredAndReportedToOperatorsOnJoin() {
|
public void aDimensionThatFailsToReinjectIsBanneredAndReportedToOperatorsOnJoin() {
|
||||||
NativeModdedServer server = mock(NativeModdedServer.class);
|
|
||||||
ModdedDimensionRegistryStore.PersistentDimension moon =
|
ModdedDimensionRegistryStore.PersistentDimension moon =
|
||||||
new ModdedDimensionRegistryStore.PersistentDimension("iris:moon", "overworld", "overworld", 1337L);
|
new ModdedDimensionRegistryStore.PersistentDimension("iris:moon", "overworld", "overworld", 1337L);
|
||||||
IllegalStateException failure = new IllegalStateException("Iris generation history is unusable for 'iris:moon'");
|
IllegalStateException failure = new IllegalStateException("Iris generation history is unusable for 'iris:moon'");
|
||||||
|
|
||||||
try (MockedStatic<ModdedDimensionRegistryStore> store = mockStatic(ModdedDimensionRegistryStore.class);
|
try (MockedStatic<ModdedIrisLog> log = mockStatic(ModdedIrisLog.class)) {
|
||||||
MockedStatic<ModdedDimensionManager> manager = mockStatic(ModdedDimensionManager.class);
|
ModdedStartup.finishDimensionRestore(ModdedDimensionRestore.start(List.of(moon), new FailingSteps(failure)));
|
||||||
MockedStatic<ModdedIrisLog> log = mockStatic(ModdedIrisLog.class)) {
|
|
||||||
store.when(() -> ModdedDimensionRegistryStore.loadForStartup(server)).thenReturn(List.of(moon));
|
|
||||||
manager.when(() -> ModdedDimensionManager.restorePersistent(server, "iris:moon", "overworld", "overworld", 1337L))
|
|
||||||
.thenThrow(failure);
|
|
||||||
|
|
||||||
ModdedStartup.reinjectPersistentDimensions(server);
|
|
||||||
|
|
||||||
log.verify(() -> ModdedIrisLog.error(argThat((String line) -> line != null && line.contains("iris:moon")
|
log.verify(() -> ModdedIrisLog.error(argThat((String line) -> line != null && line.contains("iris:moon")
|
||||||
&& line.startsWith("Iris refused"))), times(1));
|
&& line.startsWith("Iris refused"))), times(1));
|
||||||
@@ -73,4 +65,35 @@ public class ModdedStartupReinjectFailureTest {
|
|||||||
ModdedStartup.warnStartupFailuresTo(laterOperator);
|
ModdedStartup.warnStartupFailuresTo(laterOperator);
|
||||||
verify(laterOperator, never()).sendMessage(any());
|
verify(laterOperator, never()).sendMessage(any());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private record FailingSteps(RuntimeException failure) implements ModdedDimensionRestore.Steps<String, String, String> {
|
||||||
|
@Override
|
||||||
|
public String prepare(ModdedDimensionRegistryStore.PersistentDimension dimension) {
|
||||||
|
throw failure;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String begin(ModdedDimensionRegistryStore.PersistentDimension dimension, String prepared) {
|
||||||
|
throw new AssertionError("A failed preparation must not create a level");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String build(String level, String prepared) {
|
||||||
|
throw new AssertionError("A failed preparation must not build an engine");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void complete(String level, String engine) {
|
||||||
|
throw new AssertionError("A failed preparation must not publish a level");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void abandon(String level, Throwable failure) {
|
||||||
|
throw new AssertionError("A failed preparation has no level to abandon");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void discard(String prepared) {
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
+6
-3
@@ -30,9 +30,12 @@ record PreparedHydrologyCacheIdentity(String fingerprint) {
|
|||||||
if (context.isBlank()) {
|
if (context.isBlank()) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
String content = GenerationPackFingerprint.compute(
|
boolean immutable = target.getData().hasGenerationRegistryContract();
|
||||||
target.getData().getDataFolder().toPath(), GenerationPackFingerprint.CURRENT_VERSION);
|
String content = immutable
|
||||||
String dependencies = target.getData().hasGenerationRegistryContract() ? "closed"
|
? target.getData().generationPackFingerprint()
|
||||||
|
: GenerationPackFingerprint.compute(
|
||||||
|
target.getData().getDataFolder().toPath(), GenerationPackFingerprint.CURRENT_VERSION);
|
||||||
|
String dependencies = immutable ? "closed"
|
||||||
: dependencyFingerprint(IrisPlatforms.get().packsFolderNoCreate());
|
: dependencyFingerprint(IrisPlatforms.get().packsFolderNoCreate());
|
||||||
if (dependencies.isBlank()) {
|
if (dependencies.isBlank()) {
|
||||||
return null;
|
return null;
|
||||||
|
|||||||
+21
@@ -1,6 +1,7 @@
|
|||||||
package art.arcane.iris.generation.runtime;
|
package art.arcane.iris.generation.runtime;
|
||||||
|
|
||||||
import art.arcane.iris.world.history.GenerationKernelRegistry;
|
import art.arcane.iris.world.history.GenerationKernelRegistry;
|
||||||
|
import art.arcane.iris.world.history.GenerationPackFingerprint;
|
||||||
import art.arcane.iris.world.history.TransitionGenerationPlan;
|
import art.arcane.iris.world.history.TransitionGenerationPlan;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
import org.junit.Rule;
|
import org.junit.Rule;
|
||||||
@@ -21,6 +22,7 @@ import static org.mockito.Mockito.mockStatic;
|
|||||||
|
|
||||||
import static org.junit.Assert.assertEquals;
|
import static org.junit.Assert.assertEquals;
|
||||||
import static org.junit.Assert.assertNotEquals;
|
import static org.junit.Assert.assertNotEquals;
|
||||||
|
import static org.mockito.Mockito.doReturn;
|
||||||
import static org.mockito.Mockito.mock;
|
import static org.mockito.Mockito.mock;
|
||||||
import static org.mockito.Mockito.when;
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
@@ -121,6 +123,8 @@ public class PreparedHydrologyCacheIdentityTest {
|
|||||||
IrisData data = mock(IrisData.class);
|
IrisData data = mock(IrisData.class);
|
||||||
when(data.getDataFolder()).thenReturn(path.toFile());
|
when(data.getDataFolder()).thenReturn(path.toFile());
|
||||||
when(data.hasGenerationRegistryContract()).thenReturn(true);
|
when(data.hasGenerationRegistryContract()).thenReturn(true);
|
||||||
|
when(data.generationPackFingerprint()).thenAnswer(invocation ->
|
||||||
|
GenerationPackFingerprint.compute(path, GenerationPackFingerprint.CURRENT_VERSION));
|
||||||
IrisWorld world = mock(IrisWorld.class);
|
IrisWorld world = mock(IrisWorld.class);
|
||||||
when(world.minHeight()).thenReturn(-64);
|
when(world.minHeight()).thenReturn(-64);
|
||||||
when(world.maxHeight()).thenReturn(320);
|
when(world.maxHeight()).thenReturn(320);
|
||||||
@@ -133,6 +137,23 @@ public class PreparedHydrologyCacheIdentityTest {
|
|||||||
return target;
|
return target;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void immutableGenerationPackUsesTheDataFingerprintWithoutRereadingThePack() throws Exception {
|
||||||
|
Path missing = folder.getRoot().toPath().resolve("immutable-epoch-pack");
|
||||||
|
EngineTarget target = target(missing);
|
||||||
|
IrisData data = target.getData();
|
||||||
|
doReturn("verified-epoch").when(data).generationPackFingerprint();
|
||||||
|
GenerationKernelRegistry.RuntimeKernel kernel = kernel("implementation");
|
||||||
|
try (MockedStatic<PackValidationCache> validation = mockStatic(PackValidationCache.class)) {
|
||||||
|
validation.when(PackValidationCache::contextFingerprint).thenReturn("platform");
|
||||||
|
|
||||||
|
PreparedHydrologyCacheIdentity captured = PreparedHydrologyCacheIdentity.capture(target, kernel, null, false);
|
||||||
|
|
||||||
|
assertEquals(PreparedHydrologyCacheIdentity.fromInputs(
|
||||||
|
"verified-epoch", "closed", "platform", kernel, null, -64, 320, 0), captured);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void identicalInputsShareRegardlessOfWorldLocation() {
|
public void identicalInputsShareRegardlessOfWorldLocation() {
|
||||||
GenerationKernelRegistry.RuntimeKernel kernel = kernel("implementation");
|
GenerationKernelRegistry.RuntimeKernel kernel = kernel("implementation");
|
||||||
|
|||||||
Reference in new issue
Block a user