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:
Brian Neumann-Fopiano committed 2026-09-23 06:56:45 -04:00
1 parent 30c533feaf
commit 98ddb6644d
35 files changed
+3301 -197

No files matched your search

@@ -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");
@@ -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) {
@@ -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) {
}
}
@@ -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;
}