mirror of
https://github.com/VolmitSoftware/Iris.git
synced 2026-10-26 05:50:39 +00:00
Improved saved world startup
- Startup holds generation admission through recovery and runtime attachment - Prepared startup state removes the second recovery scan before generator readiness - Recovery batches allocated chunks by region with reusable snapshot buffers - Region decoding uses up to four workers within processor and heap limits - Oversized regions and external chunks use bounded direct reads - Status parsing validates chunk coordinates without constructing the full NBT tree - Region snapshots reject changed files, allocation headers and truncated payloads - Native zlib decompression uses jlibdeflate with a Java inflater fallback - Chunk readers reuse region handles and decompression buffers during recovery and boundary capture - Semantic shards reuse validated summaries with invalidation on mutation - Semantic key validation avoids regular expression allocation - Saved biome indexes use buffered sequential reads with reusable CRC state - Pack inspection reads validated manifest and pack state without opening terrain indexes - Datapack compilation reuses pack inspection within each operation - Kernel staging avoids duplicate pack hashing when no activation is pending - Pack copying and directory synchronization use up to four available processors - Boundary publication batches directory synchronization around durable catalog publication - Configured world reconciliation reuses ready runtimes after identity and saved-state checks - Explicit world loads retain the full validation path - Regression tests cover startup admission, recovery, malformed data, resource limits, publication and reconciliation
This commit is contained in:
1 parent
30c533feaf
commit
98ddb6644d
35 files changed
+3301
-197
No files matched your search
+85
-1
@@ -27,6 +27,11 @@ import art.arcane.iris.world.lifecycle.BukkitWorldConfiguration;
|
||||
import art.arcane.iris.world.lifecycle.LifecycleOperationCoordinator;
|
||||
import art.arcane.iris.platform.bukkit.nms.INMS;
|
||||
import art.arcane.iris.generation.terrain.IrisDimension;
|
||||
import art.arcane.iris.generation.runtime.IrisEngine;
|
||||
import art.arcane.iris.platform.generation.BukkitChunkGenerator;
|
||||
import art.arcane.iris.world.history.GenerationEpoch;
|
||||
import art.arcane.iris.world.history.GenerationHistory;
|
||||
import art.arcane.iris.world.history.GenerationHistoryRuntimeRouter;
|
||||
import art.arcane.iris.platform.bukkit.BukkitEnvironment;
|
||||
import art.arcane.iris.localization.C;
|
||||
import art.arcane.iris.platform.bootstrap.ServerProperties;
|
||||
@@ -182,7 +187,7 @@ public final class BukkitWorldReconciler {
|
||||
});
|
||||
}
|
||||
|
||||
private CompletableFuture<LoadResult> loadConfiguredWorld(
|
||||
CompletableFuture<LoadResult> loadConfiguredWorld(
|
||||
File configurationFile,
|
||||
String configuredWorldName,
|
||||
NamespacedKey worldKey,
|
||||
@@ -196,6 +201,28 @@ public final class BukkitWorldReconciler {
|
||||
return CompletableFuture.completedFuture(LoadResult.busy(worldKey, failure));
|
||||
}
|
||||
|
||||
try {
|
||||
IrisStartupValidation.requireWorldCreationReady();
|
||||
Optional<World> loaded = backend.loadedWorld(worldKey);
|
||||
if (loaded.isPresent() && backend.validateLoadedRuntime(
|
||||
loaded.get(), worldKey, dimension, seed) == LoadedRuntimeStatus.READY) {
|
||||
ReconciliationResult result = verifyLoadedWorld(worldKey, loaded.get(), true);
|
||||
BukkitWorldConfiguration.Registration registration = BukkitWorldConfiguration.Registration.UNCHANGED;
|
||||
if (result.succeeded()) {
|
||||
try {
|
||||
registration = BukkitWorldConfiguration.register(configurationFile, configuredWorldName, dimension, seed);
|
||||
} catch (Throwable failure) {
|
||||
lease.close();
|
||||
return CompletableFuture.completedFuture(LoadResult.configurationFailure(worldKey, failure));
|
||||
}
|
||||
}
|
||||
lease.close();
|
||||
return CompletableFuture.completedFuture(new LoadResult(result, registration, false, true, null));
|
||||
}
|
||||
} catch (Throwable failure) {
|
||||
lease.close();
|
||||
return CompletableFuture.completedFuture(LoadResult.dimensionFailure(worldKey, failure));
|
||||
}
|
||||
return loadWithLease(configurationFile, configuredWorldName, worldKey, dimension, seed, lease);
|
||||
}
|
||||
|
||||
@@ -434,11 +461,19 @@ public final class BukkitWorldReconciler {
|
||||
|
||||
boolean isIrisWorld(World world);
|
||||
|
||||
LoadedRuntimeStatus validateLoadedRuntime(World world, NamespacedKey worldKey, String dimension, Long seed)
|
||||
throws IOException;
|
||||
|
||||
DimensionResolution resolveDimension(NamespacedKey worldKey);
|
||||
|
||||
void requireDimensionLoadable(NamespacedKey worldKey, String dimension);
|
||||
}
|
||||
|
||||
enum LoadedRuntimeStatus {
|
||||
READY,
|
||||
UNAVAILABLE
|
||||
}
|
||||
|
||||
public enum ReconciliationStatus {
|
||||
LOADED,
|
||||
ALREADY_LOADED,
|
||||
@@ -717,6 +752,55 @@ public final class BukkitWorldReconciler {
|
||||
return IrisToolbelt.isIrisWorld(world);
|
||||
}
|
||||
|
||||
@Override
|
||||
public LoadedRuntimeStatus validateLoadedRuntime(
|
||||
World world, NamespacedKey worldKey, String dimension, Long seed) throws IOException {
|
||||
if (!(world.getGenerator() instanceof BukkitChunkGenerator generator)
|
||||
|| generator.isStudio()
|
||||
|| generator.isClosing()
|
||||
|| generator.getInitializationFailure() != null
|
||||
|| !generator.getStartupReady().isDone()
|
||||
|| generator.getStartupReady().isCompletedExceptionally()
|
||||
|| !(generator.getEngine() instanceof IrisEngine engine)
|
||||
|| engine.isClosed() || engine.isClosing()) {
|
||||
return LoadedRuntimeStatus.UNAVAILABLE;
|
||||
}
|
||||
GenerationHistory history = generator.getGenerationHistory();
|
||||
GenerationHistoryRuntimeRouter router = engine.getGenerationHistoryRuntimeRouter().orElse(null);
|
||||
if (history == null || router == null || router.history() != history) {
|
||||
return LoadedRuntimeStatus.UNAVAILABLE;
|
||||
}
|
||||
GenerationEpoch active = history.activeEpoch();
|
||||
if (!worldKey.equals(WorldIdentity.key(world))
|
||||
|| !worldKey.toString().equals(engine.getWorld().identity())
|
||||
|| !dimension.equals(generator.getDimensionKey())
|
||||
|| !dimension.equals(active.dimensionContract().dimensionKey())
|
||||
|| !dimension.equals(engine.getDimension().getLoadKey())
|
||||
|| seed != null && seed.longValue() != active.worldSeed()) {
|
||||
throw new IOException("Loaded Iris runtime does not match configured world " + worldKey + ".");
|
||||
}
|
||||
Path expectedRoot = WorldCreatorCompat.persistentDimensionRoot(worldKey).toPath().toAbsolutePath().normalize();
|
||||
File levelRoot = IrisWorldStorage.levelRoot();
|
||||
Path storedRoot = IrisWorldStorage.requireFrozenDimensionRoot(
|
||||
Bukkit.getWorldContainer(), levelRoot, configuredWorldName(worldKey), worldKey)
|
||||
.toPath().toAbsolutePath().normalize();
|
||||
if (!expectedRoot.equals(storedRoot) || !storedRoot.equals(history.paths().dimensionRoot())
|
||||
|| !storedRoot.equals(engine.getWorld().worldFolder().toPath().toAbsolutePath().normalize())
|
||||
|| engine.getData().isClosed()) {
|
||||
throw new IOException("Loaded Iris runtime storage does not match world " + worldKey + ".");
|
||||
}
|
||||
GenerationHistory.PackInspection inspection = GenerationHistory.inspectPacks(storedRoot);
|
||||
if (!active.equals(inspection.manifest().activeEpoch())
|
||||
|| !history.activeActivation().equals(inspection.manifest().activeActivation())
|
||||
|| !history.pendingActivation().equals(inspection.manifest().pendingActivation())
|
||||
|| !inspection.activePackRoot().equals(engine.getData().getDataFolder().toPath().toAbsolutePath().normalize())
|
||||
|| !Files.isDirectory(storedRoot.resolve("iris/generation/semantics"), LinkOption.NOFOLLOW_LINKS)
|
||||
|| !Files.isRegularFile(storedRoot.resolve("iris/generation/semantics/index.isix"), LinkOption.NOFOLLOW_LINKS)) {
|
||||
throw new IOException("Loaded Iris runtime history does not match saved world " + worldKey + ".");
|
||||
}
|
||||
return LoadedRuntimeStatus.READY;
|
||||
}
|
||||
|
||||
@Override
|
||||
public DimensionResolution resolveDimension(NamespacedKey worldKey) {
|
||||
File dimensionsDirectory = new File(snapshotRoot(worldKey), "dimensions");
|
||||
|
||||
+9
-12
@@ -383,8 +383,9 @@ public final class IrisWorldGeneratorResolver {
|
||||
IrisDimension dimension = null;
|
||||
if (dimensionRoot != null) {
|
||||
long worldSeed = requireStoredWorldSeed(configuredWorldName, dimensionRoot);
|
||||
GenerationHistory history = requireGenerationHistory(dimensionRoot, id, worldSeed);
|
||||
File pack = requireActivePack(history);
|
||||
LoadedHistory loaded = requireGenerationHistory(dimensionRoot, id, worldSeed);
|
||||
GenerationHistory history = loaded.history();
|
||||
File pack = loaded.activePack();
|
||||
requireSnapshotLoadable(pack);
|
||||
dimension = requireHistoricalDimension(history, pack, id);
|
||||
}
|
||||
@@ -615,8 +616,9 @@ public final class IrisWorldGeneratorResolver {
|
||||
+ worldKey + ".");
|
||||
}
|
||||
long worldSeed = requireStoredWorldSeed(worldName, dimensionRoot);
|
||||
GenerationHistory history = requireGenerationHistory(dimensionRoot, id, worldSeed);
|
||||
File snapshotRoot = requireActivePack(history);
|
||||
LoadedHistory loaded = requireGenerationHistory(dimensionRoot, id, worldSeed);
|
||||
GenerationHistory history = loaded.history();
|
||||
File snapshotRoot = loaded.activePack();
|
||||
PackValidationResult validation = validateSnapshot(snapshotRoot);
|
||||
if (hasPendingExternalContent(validation, IrisServices.getOrNull(ExternalDataSVC.class))) {
|
||||
return deferFrozenWorldGenerator(worldName, worldKey, dimensionRoot, snapshotRoot, id, history);
|
||||
@@ -705,7 +707,7 @@ public final class IrisWorldGeneratorResolver {
|
||||
}
|
||||
}
|
||||
|
||||
private static GenerationHistory requireGenerationHistory(
|
||||
private static LoadedHistory requireGenerationHistory(
|
||||
File dimensionRoot,
|
||||
String dimensionKey,
|
||||
long worldSeed
|
||||
@@ -724,7 +726,7 @@ public final class IrisWorldGeneratorResolver {
|
||||
history.requireRegistryDefinitions(
|
||||
GenerationRegistryContractFactory.captureRequiredDefinitions(retainedContracts)
|
||||
);
|
||||
return history;
|
||||
return new LoadedHistory(history, history.paths().packRoot(history.activeEpoch().epochId()).toFile());
|
||||
} catch (IOException failure) {
|
||||
throw new IllegalStateException("Iris generation history is unusable at " + root + ".", failure);
|
||||
}
|
||||
@@ -825,12 +827,7 @@ public final class IrisWorldGeneratorResolver {
|
||||
);
|
||||
}
|
||||
|
||||
private static File requireActivePack(GenerationHistory history) {
|
||||
try {
|
||||
return history.activePackRoot().toFile();
|
||||
} catch (IOException failure) {
|
||||
throw new IllegalStateException("Active Iris generation pack is unusable.", failure);
|
||||
}
|
||||
private record LoadedHistory(GenerationHistory history, File activePack) {
|
||||
}
|
||||
|
||||
private static long requireStoredWorldSeed(String worldName, File dimensionRoot) {
|
||||
|
||||
+148
@@ -0,0 +1,148 @@
|
||||
package art.arcane.iris.world;
|
||||
|
||||
import art.arcane.iris.Iris;
|
||||
import art.arcane.iris.generation.runtime.IrisEngine;
|
||||
import art.arcane.iris.generation.terrain.IrisDimension;
|
||||
import art.arcane.iris.pack.loading.IrisData;
|
||||
import art.arcane.iris.platform.generation.BukkitChunkGenerator;
|
||||
import art.arcane.iris.world.history.GenerationActivation;
|
||||
import art.arcane.iris.world.history.GenerationEpoch;
|
||||
import art.arcane.iris.world.history.GenerationHistory;
|
||||
import art.arcane.iris.world.history.GenerationHistoryPaths;
|
||||
import art.arcane.iris.world.history.GenerationHistoryRuntimeRouter;
|
||||
import art.arcane.iris.world.history.GenerationManifest;
|
||||
import java.io.IOException;
|
||||
import java.lang.reflect.Constructor;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.NamespacedKey;
|
||||
import org.bukkit.World;
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.junit.rules.TemporaryFolder;
|
||||
import org.mockito.MockedStatic;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertThrows;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.mockStatic;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
public class BukkitWorldReconcilerRuntimeTest {
|
||||
@Rule
|
||||
public TemporaryFolder temporaryFolder = new TemporaryFolder();
|
||||
|
||||
@Test
|
||||
public void incompleteOrDetachedRuntimeRequiresFullValidation() throws Exception {
|
||||
RuntimeFixture fixture = fixture();
|
||||
when(fixture.generator().getStartupReady()).thenReturn(new CompletableFuture<>());
|
||||
assertEquals(BukkitWorldReconciler.LoadedRuntimeStatus.UNAVAILABLE,
|
||||
fixture.backend().validateLoadedRuntime(fixture.world(), fixture.key(), "overworld", 1337L));
|
||||
when(fixture.generator().getStartupReady()).thenReturn(CompletableFuture.completedFuture(null));
|
||||
when(fixture.engine().getGenerationHistoryRuntimeRouter()).thenReturn(Optional.empty());
|
||||
assertEquals(BukkitWorldReconciler.LoadedRuntimeStatus.UNAVAILABLE,
|
||||
fixture.backend().validateLoadedRuntime(fixture.world(), fixture.key(), "overworld", 1337L));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void configuredDimensionAndSeedMustMatchLiveRuntime() throws Exception {
|
||||
RuntimeFixture fixture = fixture();
|
||||
assertThrows(IOException.class, () -> fixture.backend().validateLoadedRuntime(
|
||||
fixture.world(), fixture.key(), "other", 1337L));
|
||||
assertThrows(IOException.class, () -> fixture.backend().validateLoadedRuntime(
|
||||
fixture.world(), fixture.key(), "overworld", 7L));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void validatedLiveRuntimeStillRequiresMatchingSavedStorage() throws Exception {
|
||||
RuntimeFixture fixture = fixture();
|
||||
Path levelRoot = fixture.root().getParent().getParent().getParent();
|
||||
Path container = levelRoot.getParent();
|
||||
GenerationManifest manifest = mock(GenerationManifest.class);
|
||||
GenerationEpoch activeEpoch = fixture.history().activeEpoch();
|
||||
GenerationActivation activeActivation = fixture.history().activeActivation();
|
||||
when(manifest.activeEpoch()).thenReturn(activeEpoch);
|
||||
when(manifest.activeActivation()).thenReturn(activeActivation);
|
||||
when(manifest.pendingActivation()).thenReturn(Optional.empty());
|
||||
GenerationHistory.PackInspection inspection = new GenerationHistory.PackInspection(
|
||||
fixture.history().paths(), manifest, fixture.pack(), Optional.empty());
|
||||
try (MockedStatic<Bukkit> bukkit = mockStatic(Bukkit.class);
|
||||
MockedStatic<WorldCreatorCompat> creator = mockStatic(WorldCreatorCompat.class);
|
||||
MockedStatic<IrisWorldStorage> storage = mockStatic(IrisWorldStorage.class);
|
||||
MockedStatic<GenerationHistory> history = mockStatic(GenerationHistory.class)) {
|
||||
bukkit.when(Bukkit::getWorldContainer).thenReturn(container.toFile());
|
||||
creator.when(() -> WorldCreatorCompat.persistentDimensionRoot(fixture.key())).thenReturn(fixture.root().toFile());
|
||||
storage.when(IrisWorldStorage::levelRoot).thenReturn(levelRoot.toFile());
|
||||
storage.when(() -> IrisWorldStorage.configuredWorldName(fixture.key(), "world")).thenReturn("world_iris_probe");
|
||||
storage.when(() -> IrisWorldStorage.requireFrozenDimensionRoot(
|
||||
container.toFile(), levelRoot.toFile(), "world_iris_probe", fixture.key())).thenReturn(fixture.root().toFile());
|
||||
history.when(() -> GenerationHistory.inspectPacks(fixture.root())).thenReturn(inspection);
|
||||
|
||||
assertEquals(BukkitWorldReconciler.LoadedRuntimeStatus.READY, fixture.backend().validateLoadedRuntime(
|
||||
fixture.world(), fixture.key(), "overworld", 1337L));
|
||||
|
||||
GenerationActivation differentActivation = mock(GenerationActivation.class);
|
||||
when(manifest.activeActivation()).thenReturn(differentActivation);
|
||||
assertThrows(IOException.class, () -> fixture.backend().validateLoadedRuntime(
|
||||
fixture.world(), fixture.key(), "overworld", 1337L));
|
||||
when(manifest.activeActivation()).thenReturn(activeActivation);
|
||||
Files.delete(fixture.root().resolve("iris/generation/semantics/index.isix"));
|
||||
assertThrows(IOException.class, () -> fixture.backend().validateLoadedRuntime(
|
||||
fixture.world(), fixture.key(), "overworld", 1337L));
|
||||
}
|
||||
}
|
||||
|
||||
private RuntimeFixture fixture() throws Exception {
|
||||
Path container = temporaryFolder.newFolder().toPath();
|
||||
Path root = container.resolve("world/dimensions/iris/probe");
|
||||
Path pack = root.resolve("iris/generation/epochs/" + "a".repeat(64) + "/pack");
|
||||
Files.createDirectories(pack);
|
||||
Path semantics = root.resolve("iris/generation/semantics");
|
||||
Files.createDirectories(semantics);
|
||||
Files.createFile(semantics.resolve("index.isix"));
|
||||
NamespacedKey key = new NamespacedKey("iris", "probe");
|
||||
World world = mock(World.class);
|
||||
BukkitChunkGenerator generator = mock(BukkitChunkGenerator.class);
|
||||
IrisEngine engine = mock(IrisEngine.class);
|
||||
IrisDimension dimension = mock(IrisDimension.class);
|
||||
IrisData data = mock(IrisData.class);
|
||||
GenerationHistory history = mock(GenerationHistory.class);
|
||||
GenerationHistoryRuntimeRouter router = mock(GenerationHistoryRuntimeRouter.class);
|
||||
GenerationEpoch epoch = mock(GenerationEpoch.class);
|
||||
GenerationActivation activation = mock(GenerationActivation.class);
|
||||
GenerationEpoch.DimensionContract contract = mock(GenerationEpoch.DimensionContract.class);
|
||||
when(world.getGenerator()).thenReturn(generator);
|
||||
when(world.getKey()).thenReturn(key);
|
||||
when(generator.getStartupReady()).thenReturn(CompletableFuture.completedFuture(null));
|
||||
when(generator.getEngine()).thenReturn(engine);
|
||||
when(generator.getGenerationHistory()).thenReturn(history);
|
||||
when(generator.getDimensionKey()).thenReturn("overworld");
|
||||
when(engine.getGenerationHistoryRuntimeRouter()).thenReturn(Optional.of(router));
|
||||
when(router.history()).thenReturn(history);
|
||||
when(history.paths()).thenReturn(GenerationHistoryPaths.forDimension(root));
|
||||
when(history.activeEpoch()).thenReturn(epoch);
|
||||
when(history.activeActivation()).thenReturn(activation);
|
||||
when(history.pendingActivation()).thenReturn(Optional.empty());
|
||||
when(epoch.worldSeed()).thenReturn(1337L);
|
||||
when(epoch.dimensionContract()).thenReturn(contract);
|
||||
when(contract.dimensionKey()).thenReturn("overworld");
|
||||
when(engine.getWorld()).thenReturn(IrisWorld.builder().platformIdentity(key.toString()).worldFolder(root.toFile()).seed(1337L).build());
|
||||
when(engine.getDimension()).thenReturn(dimension);
|
||||
when(dimension.getLoadKey()).thenReturn("overworld");
|
||||
when(engine.getData()).thenReturn(data);
|
||||
when(data.getDataFolder()).thenReturn(pack.toFile());
|
||||
Class<?> backendClass = Class.forName(BukkitWorldReconciler.class.getName() + "$BukkitBackend");
|
||||
Constructor<?> constructor = backendClass.getDeclaredConstructor(Iris.class);
|
||||
constructor.setAccessible(true);
|
||||
BukkitWorldReconciler.Backend backend = (BukkitWorldReconciler.Backend) constructor.newInstance(mock(Iris.class));
|
||||
return new RuntimeFixture(backend, world, key, generator, engine, history, root, pack);
|
||||
}
|
||||
|
||||
private record RuntimeFixture(BukkitWorldReconciler.Backend backend, World world, NamespacedKey key,
|
||||
BukkitChunkGenerator generator, IrisEngine engine, GenerationHistory history,
|
||||
Path root, Path pack) {
|
||||
}
|
||||
}
|
||||
+107
@@ -71,6 +71,98 @@ public class BukkitWorldReconcilerTest {
|
||||
assertTrue(coordinator.isIdle());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void startupBatchReusesValidatedLoadedRuntime() throws Exception {
|
||||
File configuration = temporaryFolder.newFile("loaded.yml");
|
||||
BukkitWorldConfiguration.register(configuration, "probe", "overworld", 1337L);
|
||||
FakeBackend backend = new FakeBackend();
|
||||
backend.loaded = Optional.of(world(backend.worldKey));
|
||||
backend.runtimeStatus = BukkitWorldReconciler.LoadedRuntimeStatus.READY;
|
||||
backend.validationFailure = new IllegalStateException("unexpected disk reopen");
|
||||
LifecycleOperationCoordinator coordinator = coordinator();
|
||||
BukkitWorldReconciler reconciler = new BukkitWorldReconciler(backend, coordinator);
|
||||
|
||||
BukkitWorldReconciler.LoadResult result = reconciler.loadConfiguredWorld(
|
||||
configuration, "probe", backend.worldKey, "overworld", 1337L).join();
|
||||
|
||||
assertEquals(BukkitWorldReconciler.ReconciliationStatus.ALREADY_LOADED, result.status());
|
||||
assertEquals(BukkitWorldConfiguration.Registration.UNCHANGED, result.registration());
|
||||
assertEquals(1, backend.runtimeValidationCount);
|
||||
assertEquals(0, backend.validationCount);
|
||||
assertEquals(0, backend.createCount.get());
|
||||
assertTrue(coordinator.isIdle());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void startupBatchFallsBackWhenLoadedRuntimeIsUnavailable() throws Exception {
|
||||
File configuration = temporaryFolder.newFile("unavailable.yml");
|
||||
FakeBackend backend = new FakeBackend();
|
||||
backend.loaded = Optional.of(world(backend.worldKey));
|
||||
LifecycleOperationCoordinator coordinator = coordinator();
|
||||
BukkitWorldReconciler reconciler = new BukkitWorldReconciler(backend, coordinator);
|
||||
|
||||
BukkitWorldReconciler.LoadResult result = reconciler.loadConfiguredWorld(
|
||||
configuration, "probe", backend.worldKey, "overworld", null).join();
|
||||
|
||||
assertEquals(BukkitWorldReconciler.ReconciliationStatus.ALREADY_LOADED, result.status());
|
||||
assertEquals(1, backend.runtimeValidationCount);
|
||||
assertEquals(1, backend.validationCount);
|
||||
assertTrue(coordinator.isIdle());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void startupBatchRejectsChangedRuntimeStorage() throws Exception {
|
||||
File configuration = temporaryFolder.newFile("changed-storage.yml");
|
||||
FakeBackend backend = new FakeBackend();
|
||||
backend.loaded = Optional.of(world(backend.worldKey));
|
||||
backend.runtimeFailure = new IllegalStateException("saved storage changed");
|
||||
LifecycleOperationCoordinator coordinator = coordinator();
|
||||
BukkitWorldReconciler reconciler = new BukkitWorldReconciler(backend, coordinator);
|
||||
|
||||
BukkitWorldReconciler.LoadResult result = reconciler.loadConfiguredWorld(
|
||||
configuration, "probe", backend.worldKey, "overworld", null).join();
|
||||
|
||||
assertEquals(BukkitWorldReconciler.ReconciliationStatus.DIMENSION_UNRESOLVED, result.status());
|
||||
assertEquals(0, backend.validationCount);
|
||||
assertEquals(0, backend.createCount.get());
|
||||
assertEquals(0L, configuration.length());
|
||||
assertTrue(coordinator.isIdle());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void startupBatchKeepsConfigurationConflictChecks() throws Exception {
|
||||
File configuration = temporaryFolder.newFile("changed-config.yml");
|
||||
BukkitWorldConfiguration.register(configuration, "probe", "other", 7L);
|
||||
FakeBackend backend = new FakeBackend();
|
||||
backend.loaded = Optional.of(world(backend.worldKey));
|
||||
backend.runtimeStatus = BukkitWorldReconciler.LoadedRuntimeStatus.READY;
|
||||
LifecycleOperationCoordinator coordinator = coordinator();
|
||||
BukkitWorldReconciler reconciler = new BukkitWorldReconciler(backend, coordinator);
|
||||
|
||||
BukkitWorldReconciler.LoadResult result = reconciler.loadConfiguredWorld(
|
||||
configuration, "probe", backend.worldKey, "overworld", 1337L).join();
|
||||
|
||||
assertEquals(BukkitWorldReconciler.ReconciliationStatus.CONFIGURATION_FAILED, result.status());
|
||||
assertEquals("Iris:other", YamlConfiguration.loadConfiguration(configuration).getString("worlds.probe.generator"));
|
||||
assertTrue(coordinator.isIdle());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void explicitLoadStillPerformsFullValidationForLoadedWorlds() throws Exception {
|
||||
File configuration = temporaryFolder.newFile("explicit-loaded.yml");
|
||||
FakeBackend backend = new FakeBackend();
|
||||
backend.loaded = Optional.of(world(backend.worldKey));
|
||||
backend.runtimeStatus = BukkitWorldReconciler.LoadedRuntimeStatus.READY;
|
||||
backend.validationFailure = new IllegalStateException("pack changed");
|
||||
BukkitWorldReconciler reconciler = new BukkitWorldReconciler(backend, coordinator());
|
||||
|
||||
BukkitWorldReconciler.LoadResult result = reconciler.loadWorld(configuration, backend.worldKey.toString()).join();
|
||||
|
||||
assertEquals(BukkitWorldReconciler.ReconciliationStatus.DIMENSION_UNRESOLVED, result.status());
|
||||
assertEquals(0, backend.runtimeValidationCount);
|
||||
assertEquals(1, backend.validationCount);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void failedCreationRollsBackOnlyNewRegistration() throws Exception {
|
||||
File configuration = temporaryFolder.newFile("bukkit.yml");
|
||||
@@ -356,6 +448,10 @@ public class BukkitWorldReconcilerTest {
|
||||
private Long configuredSeed;
|
||||
private BukkitWorldReconciler.DimensionResolution dimensionResolution;
|
||||
private RuntimeException validationFailure;
|
||||
private BukkitWorldReconciler.LoadedRuntimeStatus runtimeStatus = BukkitWorldReconciler.LoadedRuntimeStatus.UNAVAILABLE;
|
||||
private RuntimeException runtimeFailure;
|
||||
private int validationCount;
|
||||
private int runtimeValidationCount;
|
||||
|
||||
private FakeBackend() {
|
||||
worldKey = new NamespacedKey("iris", "probe");
|
||||
@@ -404,6 +500,16 @@ public class BukkitWorldReconcilerTest {
|
||||
return irisWorld;
|
||||
}
|
||||
|
||||
@Override
|
||||
public BukkitWorldReconciler.LoadedRuntimeStatus validateLoadedRuntime(
|
||||
World world, NamespacedKey requestedWorldKey, String dimension, Long seed) {
|
||||
runtimeValidationCount++;
|
||||
if (runtimeFailure != null) {
|
||||
throw runtimeFailure;
|
||||
}
|
||||
return runtimeStatus;
|
||||
}
|
||||
|
||||
@Override
|
||||
public BukkitWorldReconciler.DimensionResolution resolveDimension(NamespacedKey requestedWorldKey) {
|
||||
return dimensionResolution;
|
||||
@@ -411,6 +517,7 @@ public class BukkitWorldReconcilerTest {
|
||||
|
||||
@Override
|
||||
public void requireDimensionLoadable(NamespacedKey requestedWorldKey, String dimension) {
|
||||
validationCount++;
|
||||
if (validationFailure != null) {
|
||||
throw validationFailure;
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user