This commit is contained in:
Brian Neumann-Fopiano
2026-08-11 14:54:53 -04:00
parent dfae45663a
commit 82290a3090
60 changed files with 2901 additions and 716 deletions
@@ -1209,6 +1209,11 @@ public class NMSBinding implements INMSBinding {
worldGenContext.structureManager(), worldGenContext.lightEngine(), worldGenContext.mainThreadExecutor(), worldGenContext.unsavedListener());
worldGenContextField.set(chunkMap, newContext);
net.minecraft.world.level.chunk.ChunkGenerator activeGenerator = level.getChunkSource().getGenerator();
if (activeGenerator != irisGenerator) {
throw new IllegalStateException("Iris generator injection did not become the active Paper chunk generator; active="
+ activeGenerator.getClass().getName());
}
retargetStructureCheck(level, irisGenerator);
}
@@ -213,6 +213,27 @@ public class NMSBindingDatapackStructureScopeTest {
assertFalse(method.contains("retained.fullState()"));
}
@Test
public void injectionVerifiesTheCanonicalPaperGeneratorBeforeStructureRetargeting() throws IOException {
Path chunkGeneratorSource = Path.of(System.getProperty("iris.nmsChunkGeneratorSource"));
String source = Files.readString(chunkGeneratorSource.resolveSibling("NMSBinding.java"));
int methodStart = source.indexOf("public void inject(long seed, Engine engine, World world)");
int methodEnd = source.indexOf("\n @Override\n public DatapackStructureScopeResult", methodStart);
assertTrue(methodStart >= 0);
assertTrue(methodEnd > methodStart);
String method = source.substring(methodStart, methodEnd);
int publication = method.indexOf("worldGenContextField.set(chunkMap, newContext);");
int canonicalRead = method.indexOf("level.getChunkSource().getGenerator()", publication);
int identityGate = method.indexOf("activeGenerator != irisGenerator", canonicalRead);
int structureRetarget = method.indexOf("retargetStructureCheck(level, irisGenerator)", identityGate);
assertTrue(publication >= 0);
assertTrue(canonicalRead > publication);
assertTrue(identityGate > canonicalRead);
assertTrue(structureRetarget > identityGate);
}
private static DatapackStructureScopeIndex index(
List<String> structureKeys,
List<String> structureSetKeys
+2
View File
@@ -15,6 +15,7 @@ dependencies {
}
compileOnly(libs.paper.api)
testImplementation('junit:junit:4.13.2')
testImplementation('org.mockito:mockito-core:5.23.0')
testImplementation(libs.paper.api)
testImplementation(libs.bstats)
testImplementation(libs.sentry)
@@ -66,5 +67,6 @@ tasks.named('test').configure {
systemProperty('iris.terrainSvcSource', file('src/main/java/art/arcane/iris/core/service/IrisTerrainSVC.java').absolutePath)
systemProperty('iris.apiEventSvcSource', file('src/main/java/art/arcane/iris/core/service/IrisApiEventSVC.java').absolutePath)
systemProperty('iris.worldInfoFactorySource', file('src/main/java/art/arcane/iris/core/service/terrain/IrisWorldInfoFactory.java').absolutePath)
systemProperty('iris.startupSource', file('src/main/java/art/arcane/iris/Iris.java').absolutePath)
systemProperty('iris.readmeSource', rootProject.file('README.md').absolutePath)
}
@@ -27,12 +27,15 @@ import art.arcane.iris.engine.framework.EnginePlatformHooks;
import art.arcane.iris.engine.framework.EngineWorldManagerProvider;
import art.arcane.iris.core.splash.IrisSplashComposer;
import art.arcane.iris.core.IrisSettings;
import art.arcane.iris.core.IrisStartupValidation;
import art.arcane.iris.core.IrisStartupAdmissionListener;
import art.arcane.iris.core.BukkitWorldReconciler;
import art.arcane.iris.core.IrisWorldGeneratorResolver;
import art.arcane.iris.core.PendingWorldDeleteQueue;
import art.arcane.iris.core.SettingsHotloadWatch;
import art.arcane.iris.core.ServerConfigurator;
import art.arcane.iris.core.datapack.DatapackIngestService;
import art.arcane.iris.core.datapack.DatapackIngestService.StartupValidationOutcome;
import art.arcane.iris.core.lifecycle.PaperLibBootstrap;
import art.arcane.iris.core.lifecycle.WorldLifecycleService;
import art.arcane.iris.core.runtime.BukkitEnginePlatformHooks;
@@ -558,7 +561,10 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
IrisServices.register(IrisCompat.class, compat);
ServerConfigurator.configure();
IrisToolbelt.applyPregenPerformanceProfile();
generatorResolver.validateAllPacks();
StartupValidationOutcome datapackValidation = DatapackIngestService.validateOnStartup();
if (datapackValidation == StartupValidationOutcome.READY) {
generatorResolver.validateAllPacks();
}
IrisSafeguard.execute();
getSender().setTag(getTag());
splash();
@@ -593,7 +599,7 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
WorldLifecycleService.get();
WorldRuntimeControlService.get();
if (J.isFolia()) {
if (J.isFolia() && IrisStartupValidation.isReady()) {
J.s(() -> worldReconciler.checkForBukkitWorlds(s -> true), 1);
}
@@ -602,10 +608,11 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
J.ar(() -> settingsHotloadWatch.checkConfigHotload(configHotloadEngine), 60);
J.sr(this::tickQueue, 0);
J.s(this::setupPapi);
J.a(DatapackIngestService::autoIngestOnStartup, 60);
autoStartStudio();
if (!J.isFolia()) {
if (IrisStartupValidation.isReady()) {
J.a(DatapackIngestService::runPostStartupTasks, 60);
autoStartStudio();
}
if (!J.isFolia() && IrisStartupValidation.isReady()) {
worldReconciler.checkForBukkitWorlds(s -> true);
}
IrisToolbelt.retainMantleDataForSlice(String.class.getCanonicalName());
@@ -674,6 +681,8 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
public void onEnable() {
IrisPlatforms.bind(new BukkitPlatform());
IrisStartupValidation.begin();
Bukkit.getPluginManager().registerEvents(new IrisStartupAdmissionListener(), this);
enable();
BukkitGuiHost.install();
super.onEnable();
@@ -22,6 +22,7 @@ import art.arcane.iris.Iris;
import art.arcane.iris.core.lifecycle.BukkitWorldConfiguration;
import art.arcane.iris.core.lifecycle.LifecycleOperationCoordinator;
import art.arcane.iris.core.nms.INMS;
import art.arcane.iris.core.pack.PackValidationRegistry;
import art.arcane.iris.core.tools.IrisToolbelt;
import art.arcane.iris.engine.object.IrisDimension;
import art.arcane.iris.platform.bukkit.BukkitEnvironment;
@@ -204,6 +205,14 @@ public final class BukkitWorldReconciler {
LifecycleOperationCoordinator.Lease lease
) {
try {
IrisStartupValidation.requireWorldCreationReady();
backend.requireDimensionLoadable(worldKey, dimension);
} catch (Throwable failure) {
lease.close();
return CompletableFuture.completedFuture(LoadResult.dimensionFailure(worldKey, failure));
}
BukkitWorldConfiguration.Registration registration;
String worldName = IrisWorldStorage.logicalName(worldKey);
try {
@@ -424,6 +433,8 @@ public final class BukkitWorldReconciler {
boolean isIrisWorld(World world);
DimensionResolution resolveDimension(NamespacedKey worldKey);
void requireDimensionLoadable(NamespacedKey worldKey, String dimension);
}
public enum ReconciliationStatus {
@@ -727,5 +738,16 @@ public final class BukkitWorldReconciler {
"Multiple dimension definitions were found without an exact registered dimension: "
+ String.join(", ", dimensions)));
}
@Override
public void requireDimensionLoadable(NamespacedKey worldKey, String dimension) {
String worldName = IrisWorldStorage.logicalName(worldKey);
IrisDimension irisDimension = IrisWorldGeneratorResolver.loadDimension(worldName, dimension);
if (irisDimension == null) {
throw new IllegalStateException("Could not resolve the Iris dimension \"" + dimension + "\".");
}
PackValidationRegistry.requireLoadable(
irisDimension.getLoader().getDataFolder().getName());
}
}
}
@@ -0,0 +1,16 @@
package art.arcane.iris.core;
import net.kyori.adventure.text.Component;
import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority;
import org.bukkit.event.Listener;
import org.bukkit.event.player.AsyncPlayerPreLoginEvent;
public final class IrisStartupAdmissionListener implements Listener {
@EventHandler(priority = EventPriority.LOWEST)
public void onAsyncPlayerPreLogin(AsyncPlayerPreLoginEvent event) {
IrisStartupValidation.denialReason().ifPresent(reason -> event.disallow(
AsyncPlayerPreLoginEvent.Result.KICK_OTHER,
Component.text(reason + " Check the server console, correct the reported Iris state, and restart.")));
}
}
@@ -24,6 +24,7 @@ import art.arcane.iris.core.loader.IrisData;
import art.arcane.iris.core.pack.BrokenPackException;
import art.arcane.iris.core.pack.PackDownloader;
import art.arcane.iris.core.pack.PackDirectoryResolver;
import art.arcane.iris.core.pack.PackValidationCache;
import art.arcane.iris.core.pack.PackValidationRegistry;
import art.arcane.iris.core.pack.PackValidationResult;
import art.arcane.iris.core.pack.PackValidator;
@@ -42,7 +43,11 @@ import org.bukkit.generator.ChunkGenerator;
import org.jetbrains.annotations.Nullable;
import java.io.File;
import java.io.IOException;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.function.Supplier;
/**
@@ -60,31 +65,73 @@ public final class IrisWorldGeneratorResolver {
File packsRoot = plugin.getDataFolder("packs");
List<File> packDirs = PackDirectoryResolver.listVisiblePackDirectories(packsRoot);
PackValidationRegistry.clear();
if (packDirs.isEmpty()) {
return;
List<String> packNames = packDirs.stream().map(File::getName).sorted().toList();
Path cacheFile = IrisPlatforms.get().dataFile("cache", "pack-validation.json").toPath();
String contentFingerprint = "";
String contextFingerprint = "";
Optional<List<PackValidationResult>> cached = Optional.empty();
try {
contentFingerprint = PackValidationCache.contentFingerprint(packsRoot);
contextFingerprint = PackValidationCache.contextFingerprint();
cached = PackValidationCache.load(
cacheFile,
contentFingerprint,
contextFingerprint,
packNames);
} catch (RuntimeException exception) {
Iris.reportError("Could not evaluate the persisted pack-validation cache", exception);
}
for (File packDir : packDirs) {
try {
PackValidationResult result = PackValidator.validate(packDir);
PackValidationRegistry.publish(result);
if (!result.isLoadable()) {
Iris.error("Pack '" + result.getPackName() + "' FAILED validation - world/studio creation will be refused. Reasons:");
for (String reason : result.getBlockingErrors()) {
Iris.error(" - " + reason);
List<PackValidationResult> results;
if (cached.isPresent()) {
results = cached.get();
Iris.info("Reused persisted validation for " + results.size()
+ " unchanged Iris pack(s); full pack parsing was skipped.");
} else {
results = new ArrayList<>(packDirs.size());
for (File packDir : packDirs) {
try {
results.add(PackValidator.validate(packDir));
} catch (Throwable exception) {
Iris.reportError("Pack validation failed for '" + packDir.getName() + "'", exception);
String detail = exception.getMessage();
if (detail == null || detail.isBlank()) {
detail = exception.getClass().getSimpleName();
}
} else if (!result.getWarnings().isEmpty()) {
Iris.info("Pack '" + result.getPackName() + "' validated ("
+ result.getWarnings().size() + " warning(s)).");
for (String warning : result.getWarnings()) {
Iris.warn(" [" + result.getPackName() + "] " + warning);
}
} else {
Iris.success("Pack '" + result.getPackName() + "' validated.");
results.add(new PackValidationResult(
packDir.getName(),
List.of("Pack validation failed with " + exception.getClass().getSimpleName()
+ ": " + detail),
List.of(),
System.currentTimeMillis()));
}
} catch (Throwable e) {
Iris.reportError("Pack validation failed for '" + packDir.getName() + "'", e);
}
try {
PackValidationCache.save(cacheFile, contentFingerprint, contextFingerprint, results);
} catch (IOException exception) {
Iris.reportError("Could not persist Iris pack-validation results", exception);
}
}
for (PackValidationResult result : results) {
PackValidationRegistry.publish(result);
if (!result.isLoadable()) {
Iris.error("Pack '" + result.getPackName()
+ "' FAILED validation - world and Studio creation with this pack will be refused. Reasons:");
for (String reason : result.getBlockingErrors()) {
Iris.error(" - " + reason);
}
} else if (!result.getWarnings().isEmpty()) {
Iris.info("Pack '" + result.getPackName() + "' validated ("
+ result.getWarnings().size() + " warning(s)).");
for (String warning : result.getWarnings()) {
Iris.warn(" [" + result.getPackName() + "] " + warning);
}
} else if (cached.isEmpty()) {
Iris.success("Pack '" + result.getPackName() + "' validated.");
}
}
IrisStartupValidation.markPacksReady();
}
@Nullable
@@ -127,6 +174,7 @@ public final class IrisWorldGeneratorResolver {
}
public ChunkGenerator resolveDefaultWorldGenerator(String worldName, String id) {
IrisStartupValidation.requireWorldCreationReady();
ChunkGenerator stagedGenerator = WorldLifecycleStaging.consumeGenerator(worldName);
if (stagedGenerator != null) {
Iris.debug("Using staged runtime generator for " + worldName);
@@ -136,19 +184,20 @@ public final class IrisWorldGeneratorResolver {
if (id == null || id.isEmpty()) id = IrisSettings.get().getGenerator().getDefaultWorldType();
Iris.debug("Generator ID: " + id + " requested by bukkit/plugin");
PackValidationResult validation = PackValidationRegistry.get(id);
if (validation != null && !validation.isLoadable()) {
Iris.error("Refusing to create world '" + worldName + "' using broken pack '" + id + "':");
for (String reason : validation.getBlockingErrors()) {
Iris.error(" - " + reason);
}
throw new BrokenPackException(id, validation.getBlockingErrors());
}
IrisDimension dim = loadDimension(worldName, id);
if (dim == null) {
throw new RuntimeException("Can't find dimension " + id + "!");
}
String packName = dim.getLoader().getDataFolder().getName();
try {
PackValidationRegistry.requireLoadable(packName);
} catch (BrokenPackException exception) {
Iris.error("Refusing to create world '" + worldName + "' using broken pack '" + packName + "':");
for (String reason : exception.getReasons()) {
Iris.error(" - " + reason);
}
throw exception;
}
Iris.debug("Assuming IrisDimension: " + dim.getName());
NamespacedKey worldKey = IrisWorldStorage.keyFromName(worldName);
@@ -22,6 +22,7 @@ import art.arcane.iris.Iris;
import art.arcane.iris.core.BukkitWorldReconciler;
import art.arcane.iris.platform.bukkit.BukkitPlatform;
import art.arcane.iris.core.IrisSettings;
import art.arcane.iris.core.IrisStartupValidation;
import art.arcane.iris.core.DatapackInstallResult;
import art.arcane.iris.core.IrisWorldStorage;
import art.arcane.iris.core.IrisWorlds;
@@ -34,6 +35,7 @@ import art.arcane.iris.core.loader.IrisData;
import art.arcane.iris.core.pack.AtomicDirectoryPublisher;
import art.arcane.iris.core.pack.PackDownloader;
import art.arcane.iris.core.pack.PackDirectoryResolver;
import art.arcane.iris.core.pack.PackValidationRegistry;
import art.arcane.iris.core.service.StudioSVC;
import art.arcane.iris.core.tools.IrisToolbelt;
import art.arcane.iris.engine.platform.PlatformChunkGenerator;
@@ -394,6 +396,14 @@ public class CommandIris implements DirectorExecutor {
}
private boolean stageFoliaWorldCreation(String name, IrisDimension dimension, long seed, boolean main) {
try {
IrisStartupValidation.requireWorldCreationReady();
PackValidationRegistry.requireLoadable(
dimension.getLoader().getDataFolder().getName());
} catch (RuntimeException exception) {
sender().sendMessage(C.RED + exception.getMessage());
return false;
}
NamespacedKey worldKey = IrisWorldStorage.managedKeyFromName(name);
LifecycleOperationCoordinator.Lease worldLease = null;
File worldFolder = IrisWorldStorage.requireSafeManagedDimensionRoot(worldKey);
@@ -21,6 +21,7 @@ package art.arcane.iris.core.commands;
import art.arcane.iris.Iris;
import art.arcane.iris.core.pack.PackDirectoryResolver;
import art.arcane.iris.core.pack.PackResourceCleanup;
import art.arcane.iris.core.pack.PackValidationCache;
import art.arcane.iris.core.pack.PackValidationRegistry;
import art.arcane.iris.core.pack.PackValidationResult;
import art.arcane.iris.core.pack.PackValidator;
@@ -31,6 +32,8 @@ import art.arcane.volmlib.util.director.annotations.Param;
import art.arcane.volmlib.util.localization.TextKey;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import art.arcane.iris.core.localization.IrisLanguage;
@@ -64,6 +67,7 @@ public class CommandPack implements DirectorExecutor {
broken++;
}
}
persistValidationCache(packsRoot);
s.sendMessage(IrisLanguage.text(BukkitRuntimeMessages.COMMAND_PACK_VALIDATION_COMPLETE_BROKEN_PACKS, MessageArgument.untrusted("broken", String.valueOf(broken)), MessageArgument.untrusted("value", String.valueOf(dirs.size()))));
return;
}
@@ -74,6 +78,7 @@ public class CommandPack implements DirectorExecutor {
return;
}
runValidate(s, target);
persistValidationCache(packsRoot);
}
@Director(description = "Preview or apply unused-resource cleanup", descriptionKey = "iris.director.commandpack.director.preview_apply_unused_resource_cleanup", aliases = {"c"})
@@ -102,6 +107,7 @@ public class CommandPack implements DirectorExecutor {
s.sendMessage(IrisLanguage.text(BukkitRuntimeMessages.COMMAND_PACK_NO_CLEANUP_CANDIDATES_FOUND_PACK, MessageArgument.untrusted("pack", String.valueOf(pack))));
return;
}
PackValidationRegistry.remove(packFolder.getName());
s.sendMessage(IrisLanguage.text(BukkitRuntimeMessages.COMMAND_PACK_QUARANTINED_CLEANUP_CANDIDATE_S_UNDER, MessageArgument.untrusted("size", String.valueOf(result.quarantinedPaths().size())), MessageArgument.untrusted("quarantinePath", String.valueOf(result.quarantinePath()))));
reportPaths(s, result.quarantinedPaths(), BukkitRuntimeMessages.COMMAND_PACK_PATH_QUARANTINED);
return;
@@ -157,6 +163,7 @@ public class CommandPack implements DirectorExecutor {
s.sendMessage(IrisLanguage.text(BukkitRuntimeMessages.COMMAND_PACK_NOTHING_RESTORE_PACK, MessageArgument.untrusted("pack", String.valueOf(pack))));
return;
}
PackValidationRegistry.remove(packFolder.getName());
s.sendMessage(IrisLanguage.text(BukkitRuntimeMessages.COMMAND_PACK_RESTORED_FILE_S_FROM, MessageArgument.untrusted("size", String.valueOf(result.restoredPaths().size())), MessageArgument.untrusted("dumpPath", String.valueOf(result.dumpPath()))));
reportPaths(s, result.restoredPaths(), BukkitRuntimeMessages.COMMAND_PACK_PATH_RESTORED);
return;
@@ -227,8 +234,38 @@ public class CommandPack implements DirectorExecutor {
return result;
} catch (Throwable e) {
Iris.reportError("Pack validation failed for '" + packFolder.getName() + "'", e);
s.sendMessage(IrisLanguage.text(BukkitRuntimeMessages.COMMAND_PACK_VALIDATION_FAILED, MessageArgument.untrusted("name", String.valueOf(packFolder.getName())), MessageArgument.untrusted("error", String.valueOf(e.getMessage()))));
return null;
String detail = e.getMessage() == null || e.getMessage().isBlank()
? e.getClass().getSimpleName()
: e.getMessage();
PackValidationResult result = new PackValidationResult(
packFolder.getName(),
List.of("Pack validation failed with " + e.getClass().getSimpleName() + ": " + detail),
List.of(),
System.currentTimeMillis());
PackValidationRegistry.publish(result);
reportResult(s, result);
return result;
}
}
private void persistValidationCache(File packsRoot) {
List<File> packDirectories = PackDirectoryResolver.listVisiblePackDirectories(packsRoot);
List<PackValidationResult> results = new ArrayList<>(packDirectories.size());
for (File packDirectory : packDirectories) {
PackValidationResult result = PackValidationRegistry.get(packDirectory.getName());
if (result == null) {
return;
}
results.add(result);
}
try {
PackValidationCache.save(
Iris.instance.getDataFile("cache", "pack-validation.json").toPath(),
PackValidationCache.contentFingerprint(packsRoot),
PackValidationCache.contextFingerprint(),
results);
} catch (IOException | RuntimeException e) {
Iris.reportError("Could not persist refreshed pack-validation results", e);
}
}
@@ -5,6 +5,7 @@ import art.arcane.iris.core.lifecycle.LifecycleOperationCoordinator;
import org.bukkit.NamespacedKey;
import org.bukkit.World;
import org.bukkit.configuration.file.YamlConfiguration;
import org.junit.After;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TemporaryFolder;
@@ -29,6 +30,11 @@ public class BukkitWorldReconcilerTest {
@Rule
public TemporaryFolder temporaryFolder = new TemporaryFolder();
@After
public void disableStartupValidation() {
IrisStartupValidation.disable();
}
@Test
public void loadLeaseCoversRegistrationThroughExactWorldCompletion() throws Exception {
File configuration = temporaryFolder.newFile("bukkit.yml");
@@ -221,6 +227,38 @@ public class BukkitWorldReconcilerTest {
assertEquals(0L, configuration.length());
}
@Test
public void pendingStartupValidationRefusesWorldLoadBeforeRegistration() throws Exception {
File configuration = temporaryFolder.newFile("startup-pending.yml");
FakeBackend backend = new FakeBackend();
BukkitWorldReconciler reconciler = new BukkitWorldReconciler(backend, coordinator());
IrisStartupValidation.begin();
BukkitWorldReconciler.LoadResult result = reconciler
.loadWorld(configuration, backend.worldKey.toString())
.join();
assertEquals(BukkitWorldReconciler.ReconciliationStatus.DIMENSION_UNRESOLVED, result.status());
assertEquals(0, backend.createCount.get());
assertEquals(0L, configuration.length());
}
@Test
public void invalidDimensionRefusesWorldLoadBeforeRegistration() throws Exception {
File configuration = temporaryFolder.newFile("invalid-dimension.yml");
FakeBackend backend = new FakeBackend();
backend.validationFailure = new IllegalStateException("pack is invalid");
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.createCount.get());
assertEquals(0L, configuration.length());
}
@Test
public void terminalCreateTimeoutWinsOverLateWorldCompletion() {
NamespacedKey worldKey = new NamespacedKey("iris", "probe");
@@ -317,6 +355,7 @@ public class BukkitWorldReconcilerTest {
private boolean irisWorld;
private Long configuredSeed;
private BukkitWorldReconciler.DimensionResolution dimensionResolution;
private RuntimeException validationFailure;
private FakeBackend() {
worldKey = new NamespacedKey("iris", "probe");
@@ -326,6 +365,7 @@ public class BukkitWorldReconcilerTest {
irisWorld = true;
configuredSeed = null;
dimensionResolution = BukkitWorldReconciler.DimensionResolution.resolved("overworld");
validationFailure = null;
}
@Override
@@ -358,5 +398,12 @@ public class BukkitWorldReconcilerTest {
public BukkitWorldReconciler.DimensionResolution resolveDimension(NamespacedKey requestedWorldKey) {
return dimensionResolution;
}
@Override
public void requireDimensionLoadable(NamespacedKey requestedWorldKey, String dimension) {
if (validationFailure != null) {
throw validationFailure;
}
}
}
}
@@ -0,0 +1,78 @@
package art.arcane.iris.core;
import com.destroystokyo.paper.profile.PlayerProfile;
import org.bukkit.event.player.AsyncPlayerPreLoginEvent;
import org.junit.After;
import org.junit.Test;
import java.net.InetAddress;
import java.util.UUID;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.mockito.Mockito.mock;
public class IrisStartupAdmissionListenerTest {
private final IrisStartupAdmissionListener listener = new IrisStartupAdmissionListener();
@After
public void disableValidation() {
IrisStartupValidation.disable();
}
@Test
public void pendingStartupValidationDeniesLogin() throws Exception {
IrisStartupValidation.begin();
AsyncPlayerPreLoginEvent event = event();
listener.onAsyncPlayerPreLogin(event);
assertEquals(AsyncPlayerPreLoginEvent.Result.KICK_OTHER, event.getLoginResult());
assertNotNull(event.kickMessage());
}
@Test
public void invalidDatapacksDenyLogin() throws Exception {
IrisStartupValidation.begin();
IrisStartupValidation.markDatapacksInvalid("datapack failure");
IrisStartupValidation.markPacksReady();
AsyncPlayerPreLoginEvent event = event();
listener.onAsyncPlayerPreLogin(event);
assertEquals(AsyncPlayerPreLoginEvent.Result.KICK_OTHER, event.getLoginResult());
}
@Test
public void restartRequiredDeniesLogin() throws Exception {
IrisStartupValidation.begin();
IrisStartupValidation.requireRestart("restart required");
IrisStartupValidation.markPacksReady();
AsyncPlayerPreLoginEvent event = event();
listener.onAsyncPlayerPreLogin(event);
assertEquals(AsyncPlayerPreLoginEvent.Result.KICK_OTHER, event.getLoginResult());
}
@Test
public void readyValidationAllowsLogin() throws Exception {
IrisStartupValidation.begin();
IrisStartupValidation.markDatapacksReady();
IrisStartupValidation.markPacksReady();
AsyncPlayerPreLoginEvent event = event();
listener.onAsyncPlayerPreLogin(event);
assertEquals(AsyncPlayerPreLoginEvent.Result.ALLOWED, event.getLoginResult());
}
private AsyncPlayerPreLoginEvent event() throws Exception {
return new AsyncPlayerPreLoginEvent(
"ValidationTest",
InetAddress.getLoopbackAddress(),
UUID.randomUUID(),
false,
mock(PlayerProfile.class));
}
}
@@ -0,0 +1,49 @@
package art.arcane.iris.core;
import org.junit.Test;
import java.nio.file.Files;
import java.nio.file.Path;
import static org.junit.Assert.assertTrue;
public class IrisStartupOrderingTest {
@Test
public void admissionGateIsRegisteredBeforeStartupValidationBegins() throws Exception {
String source = Files.readString(Path.of(System.getProperty("iris.startupSource")));
String onEnable = section(source, "public void onEnable()", "public void onDisable()");
assertOrdered(onEnable,
"IrisStartupValidation.begin();",
"registerEvents(new IrisStartupAdmissionListener(), this);",
"enable();");
}
@Test
public void externalDatapacksValidateBeforeDimensionPacks() throws Exception {
String source = Files.readString(Path.of(System.getProperty("iris.startupSource")));
String enable = section(source, "private void enable()", "public void addShutdownHook()");
assertOrdered(enable,
"DatapackIngestService.validateOnStartup();",
"generatorResolver.validateAllPacks();");
}
private static String section(String source, String startMarker, String endMarker) {
int start = source.indexOf(startMarker);
int end = source.indexOf(endMarker, start);
assertTrue("Missing source section starting with " + startMarker, start >= 0);
assertTrue("Missing source section ending with " + endMarker, end > start);
return source.substring(start, end);
}
private static void assertOrdered(String source, String... markers) {
int previous = -1;
for (String marker : markers) {
int current = source.indexOf(marker);
assertTrue("Missing source marker " + marker, current >= 0);
assertTrue("Source marker is out of order: " + marker, current > previous);
previous = current;
}
}
}