- Thsi fixes an integration between Itemsadder and Oraxen.
- Waiting on oraxen folks
This commit is contained in:
Brian Neumann-Fopiano committed 2026-09-07 19:22:59 -04:00
1 parent 894cef73a6
commit d3245aa1c2
61 files changed
+3497 -160

No files matched your search

@@ -671,9 +671,6 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
IrisServices.register(IrisCompat.class, compat);
ServerConfigurator.configure();
StartupValidationOutcome datapackValidation = DatapackIngestService.validateOnStartup();
if (datapackValidation == StartupValidationOutcome.READY) {
generatorResolver.validateAllPacks();
}
IrisSafeguard.execute();
getSender().setTag(getTag());
// A cosmetic banner must never abort the bootstrap.
@@ -727,12 +724,16 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
Iris.reportError("Failed to register listener for " + service.getClass().getSimpleName() + ".", e);
}
}
if (datapackValidation == StartupValidationOutcome.READY) {
IrisServices.get(ExternalDataSVC.class).setContentChangeListener(generatorResolver::requestExternalContentRefresh);
generatorResolver.validateAllPacks();
}
addShutdownHook();
pendingWorldReplacements.processPendingStartupReplacements();
pendingWorldDeletes.processPendingStartupWorldDeletes();
if (J.isFolia() && IrisStartupValidation.isReady()) {
J.s(() -> worldReconciler.checkForBukkitWorlds(s -> true), 1);
J.s(this::reconcileStartupWorlds, 1);
}
J.s(() -> {
@@ -747,7 +748,7 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
autoStartStudio();
}
if (!J.isFolia() && IrisStartupValidation.isReady()) {
worldReconciler.checkForBukkitWorlds(s -> true);
reconcileStartupWorlds();
}
IrisToolbelt.retainMantleDataForSlice(String.class.getCanonicalName());
// The mantle stores block values as PlatformBlockState, so a BlockData retention can never
@@ -757,6 +758,16 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
return true;
}
private void reconcileStartupWorlds() {
generatorResolver.startupWorldsReady().whenComplete((ignored, failure) -> {
if (failure != null) {
Iris.reportError("Could not resume Iris startup world reconciliation.", failure);
return;
}
J.s(() -> worldReconciler.checkForBukkitWorlds(s -> true));
});
}
public void addShutdownHook() {
removeShutdownHook();
serverLifecycleThread = Thread.currentThread();
@@ -20,6 +20,7 @@ package art.arcane.iris.core;
import art.arcane.iris.Iris;
import art.arcane.iris.core.lifecycle.WorldLifecycleStaging;
import art.arcane.iris.core.compat.CompatRegistry;
import art.arcane.iris.core.lifecycle.WorldReplacementSeed;
import art.arcane.iris.core.loader.IrisData;
import art.arcane.iris.core.pack.BrokenPackException;
@@ -29,8 +30,11 @@ 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;
import art.arcane.iris.core.service.ExternalDataSVC;
import art.arcane.iris.spi.IrisLogging;
import art.arcane.iris.spi.IrisPlatforms;
import art.arcane.iris.spi.IrisServices;
import art.arcane.iris.engine.framework.EngineTarget;
import art.arcane.iris.engine.object.IrisDimension;
import art.arcane.iris.engine.object.IrisWorld;
import art.arcane.iris.engine.history.GenerationEpoch;
@@ -42,6 +46,7 @@ import art.arcane.iris.engine.history.GenerationRegistryContract;
import art.arcane.iris.engine.history.GenerationRegistryContractFactory;
import art.arcane.iris.engine.platform.BukkitChunkGenerator;
import art.arcane.iris.util.common.plugin.VolmitPlugin;
import art.arcane.iris.util.common.scheduling.J;
import art.arcane.volmlib.util.bukkit.WorldIdentity;
import lombok.NonNull;
import org.bukkit.Bukkit;
@@ -60,6 +65,10 @@ import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.function.Supplier;
/**
@@ -72,17 +81,102 @@ public final class IrisWorldGeneratorResolver {
private static final Object SNAPSHOT_VALIDATION_LOCK = new Object();
private static final String IRIS_DIMENSION_NAMESPACE = "iris";
private static final String PLOT_SQUARED_DISCOVERY_WORLD = "CheckingPlotSquaredGenerator";
private static final long EXTERNAL_CONTENT_STARTUP_TIMEOUT_SECONDS = 120L;
private final VolmitPlugin plugin;
private final AtomicBoolean externalContentRefreshQueued = new AtomicBoolean();
private final AtomicBoolean externalContentRefreshRequested = new AtomicBoolean();
private final Map<Path, PendingSnapshot> pendingSnapshots = new ConcurrentHashMap<>();
public IrisWorldGeneratorResolver(VolmitPlugin plugin) {
this.plugin = plugin;
}
public void validateAllPacks() {
validateAllPacks(false);
}
public void requestExternalContentRefresh() {
externalContentRefreshRequested.set(true);
if (!externalContentRefreshQueued.compareAndSet(false, true)) {
return;
}
try {
J.a(this::runExternalContentRefresh);
} catch (RuntimeException | LinkageError error) {
externalContentRefreshQueued.set(false);
throw error;
}
}
public CompletableFuture<Void> startupWorldsReady() {
return CompletableFuture.allOf(pendingSnapshots.values().stream()
.map(PendingSnapshot::ready)
.toArray(CompletableFuture<?>[]::new));
}
private void runExternalContentRefresh() {
externalContentRefreshRequested.set(false);
try {
retryPendingSnapshots();
refreshExternalContent();
} catch (RuntimeException | LinkageError error) {
Iris.reportError("Failed to revalidate Iris packs after external content changed.", error);
} finally {
externalContentRefreshQueued.set(false);
if (externalContentRefreshRequested.get()) {
requestExternalContentRefresh();
}
}
}
private void retryPendingSnapshots() {
for (Map.Entry<Path, PendingSnapshot> entry : pendingSnapshots.entrySet()) {
PendingSnapshot pending = entry.getValue();
if (pending.contentReady().isDone()) {
continue;
}
try {
ExternalDataSVC external = IrisServices.getOrNull(ExternalDataSVC.class);
long revision = external == null ? 0L : external.contentRevision();
boolean wasPending = hasPendingExternalContent(PackValidationRegistry.get(entry.getKey()), external);
PackValidationRegistry.remove(entry.getKey());
IrisData.invalidateLoadedContentRegistries(entry.getKey().toFile());
PackValidationResult validation = validateSnapshot(entry.getKey().toFile());
if (external != null && revision != external.contentRevision()) {
requestExternalContentRefresh();
continue;
}
boolean stillPending = hasPendingExternalContent(validation, external);
if (wasPending && !stillPending) {
requestExternalContentRefresh();
continue;
}
if (stillPending) {
continue;
}
PackValidationRegistry.requireLoadable(entry.getKey());
requireHistoricalDimension(pending.history(), entry.getKey().toFile(), pending.dimensionKey());
pending.contentReady().complete(null);
} catch (RuntimeException | LinkageError failure) {
pending.contentReady().completeExceptionally(failure);
}
}
}
synchronized void refreshExternalContent() {
for (File pack : PackDirectoryResolver.listVisiblePackDirectories(plugin.getDataFolder("packs"))) {
IrisData.invalidateLoadedContentRegistries(pack);
}
validateAllPacks(true);
}
private synchronized void validateAllPacks(boolean externalContentChanged) {
File packsRoot = plugin.getDataFolder("packs");
List<File> packDirs = PackDirectoryResolver.listVisiblePackDirectories(packsRoot);
PackValidationRegistry.clear();
if (!externalContentChanged) {
PackValidationRegistry.clear();
}
List<String> packNames = packDirs.stream().map(File::getName).sorted().toList();
Path cacheFile = IrisPlatforms.get().dataFile("cache", "pack-validation.json").toPath();
ServerConfigurator.PackContentSnapshot contentSnapshot =
@@ -92,11 +186,13 @@ public final class IrisWorldGeneratorResolver {
try {
contentSnapshot = ServerConfigurator.computePackContentSnapshot(packsRoot);
contextFingerprint = PackValidationCache.contextFingerprint();
cached = PackValidationCache.load(
cacheFile,
contentSnapshot.content(),
contextFingerprint,
packNames);
if (!externalContentChanged) {
cached = PackValidationCache.load(
cacheFile,
contentSnapshot.content(),
contextFingerprint,
packNames);
}
} catch (RuntimeException exception) {
Iris.reportError("Could not evaluate the persisted pack-validation cache", exception);
}
@@ -136,6 +232,12 @@ public final class IrisWorldGeneratorResolver {
String compatSummary = PackValidator.compatSummary(result, minecraftVersion);
String compatSuffix = compatSummary.isEmpty() ? "" : " " + compatSummary;
if (!result.isLoadable()) {
if (hasPendingExternalContent(result, IrisServices.getOrNull(ExternalDataSVC.class))
&& packDirectory != null
&& PackValidator.validateForDatapackBootstrap(packDirectory).isLoadable()) {
Iris.info("Pack '" + result.getPackName() + "' is waiting for external block providers to initialize.");
continue;
}
Iris.error("Pack '" + result.getPackName()
+ "' FAILED validation - world and Studio creation with this pack will be refused. Reasons:");
for (String reason : result.getBlockingErrors()) {
@@ -226,6 +328,11 @@ public final class IrisWorldGeneratorResolver {
}
static PackValidationResult requireSnapshotLoadable(File packRoot) {
validateSnapshot(packRoot);
return PackValidationRegistry.requireLoadable(packRoot.toPath());
}
private static PackValidationResult validateSnapshot(File packRoot) {
Path normalizedRoot = packRoot.toPath().toAbsolutePath().normalize();
PackValidationResult result = PackValidationRegistry.get(normalizedRoot);
if (result == null) {
@@ -255,7 +362,7 @@ public final class IrisWorldGeneratorResolver {
}
}
}
return PackValidationRegistry.requireLoadable(normalizedRoot);
return result;
}
@Nullable
@@ -493,6 +600,10 @@ public final class IrisWorldGeneratorResolver {
long worldSeed = requireStoredWorldSeed(worldName, dimensionRoot);
GenerationHistory history = requireGenerationHistory(dimensionRoot, id, worldSeed);
File snapshotRoot = requireActivePack(history);
PackValidationResult validation = validateSnapshot(snapshotRoot);
if (hasPendingExternalContent(validation, IrisServices.getOrNull(ExternalDataSVC.class))) {
return deferFrozenWorldGenerator(worldName, worldKey, dimensionRoot, snapshotRoot, id, history);
}
try {
requireSnapshotLoadable(snapshotRoot);
} catch (BrokenPackException exception) {
@@ -522,6 +633,61 @@ public final class IrisWorldGeneratorResolver {
return new BukkitChunkGenerator(world, false, snapshotRoot, dimension.getLoadKey(), history);
}
static boolean hasPendingExternalContent(PackValidationResult validation, ExternalDataSVC external) {
return validation != null && external != null && validation.getCompatFindings().stream()
.anyMatch(finding -> finding.registry() == CompatRegistry.BLOCK
&& external.hasPendingBlockProvider(finding.key()));
}
private BukkitChunkGenerator deferFrozenWorldGenerator(
String worldName,
NamespacedKey worldKey,
File dimensionRoot,
File snapshotRoot,
String dimensionKey,
GenerationHistory history
) {
PackValidationResult structural = PackValidator.validateForDatapackBootstrap(snapshotRoot);
if (!structural.isLoadable()) {
throw new BrokenPackException(snapshotRoot.toString(), structural.getBlockingErrors());
}
IrisData data = IrisData.openDatapackCompiler(snapshotRoot);
try {
IrisDimension dimension = requireHistoricalDimension(history, data, dimensionKey);
GenerationEpoch.DimensionContract contract = history.activeEpoch().dimensionContract();
IrisWorld world = IrisWorld.builder()
.platformIdentity(worldKey.toString())
.name(worldName)
.seed(history.activeEpoch().worldSeed())
.worldFolder(dimensionRoot)
.minHeight(contract.minHeight())
.maxHeight(Math.addExact(contract.minHeight(), contract.height()))
.build();
Path snapshotPath = snapshotRoot.toPath().toAbsolutePath().normalize();
CompletableFuture<Void> contentReady = new CompletableFuture<>();
BukkitChunkGenerator generator = new BukkitChunkGenerator(
world, false, snapshotRoot, dimensionKey, history);
generator.deferStartup(new EngineTarget(world, dimension, data), contentReady);
PendingSnapshot pending = new PendingSnapshot(history, dimensionKey, contentReady, generator.getStartupReady());
if (pendingSnapshots.putIfAbsent(snapshotPath, pending) != null) {
generator.closeAsync();
throw new IllegalStateException("Iris snapshot already has a deferred startup: " + snapshotPath);
}
contentReady.orTimeout(EXTERNAL_CONTENT_STARTUP_TIMEOUT_SECONDS, TimeUnit.SECONDS);
pending.ready().whenComplete((ignored, failure) -> {
if (failure == null) {
pendingSnapshots.remove(snapshotPath, pending);
}
});
Iris.info("Waiting for external block providers before initializing Iris world '" + worldName + "'.");
requestExternalContentRefresh();
return generator;
} catch (RuntimeException | LinkageError failure) {
data.close();
throw failure;
}
}
private static GenerationHistory requireGenerationHistory(
File dimensionRoot,
String dimensionKey,
@@ -587,6 +753,14 @@ public final class IrisWorldGeneratorResolver {
GenerationHistory history,
File packRoot,
String dimensionKey
) {
return requireHistoricalDimension(history, IrisData.get(packRoot), dimensionKey);
}
private static IrisDimension requireHistoricalDimension(
GenerationHistory history,
IrisData data,
String dimensionKey
) {
GenerationEpoch epoch = history.activeEpoch();
if (!epoch.dimensionContract().dimensionKey().equals(dimensionKey)) {
@@ -594,21 +768,24 @@ public final class IrisWorldGeneratorResolver {
+ " does not match active generation history dimension "
+ epoch.dimensionContract().dimensionKey() + ".");
}
IrisData data = IrisData.get(packRoot);
IrisDimension dimension = data.getDimensionLoader().load(dimensionKey, false);
if (dimension == null) {
throw new IllegalStateException("Immutable Iris generation pack at " + packRoot
throw new IllegalStateException("Immutable Iris generation pack at " + data.getDataFolder()
+ " does not contain dimension " + dimensionKey + ".");
}
GenerationEpoch.DimensionContract actualContract = GenerationEpochContractFactory.createForEpoch(
dimension, dimensionTypeKey(data, dimension), epoch);
if (!epoch.dimensionContract().equals(actualContract)) {
throw new IllegalStateException("Immutable Iris generation pack dimension contract changed at "
+ packRoot + ".");
+ data.getDataFolder() + ".");
}
return dimension;
}
private record PendingSnapshot(GenerationHistory history, String dimensionKey,
CompletableFuture<Void> contentReady, CompletableFuture<Void> ready) {
}
private static GenerationEpoch.DimensionContract captureDimensionContract(
IrisData data,
IrisDimension dimension
@@ -36,6 +36,10 @@ dependencies:
load: BEFORE
required: false
join-classpath: true
Oraxen:
load: BEFORE
required: false
join-classpath: true
ItemsAdder:
load: BEFORE
required: false
@@ -7,6 +7,7 @@ softdepend:
- PlaceholderAPI
- CraftEngine
- Nexo
- Oraxen
- ItemsAdder
- SCore
- ExecutableItems
@@ -24,6 +24,7 @@ public class PaperPluginMetadataTest {
"PlaceholderAPI",
"CraftEngine",
"Nexo",
"Oraxen",
"ItemsAdder",
"SCore",
"ExecutableItems",
@@ -29,6 +29,17 @@ public class IrisStartupOrderingTest {
"generatorResolver.validateAllPacks();");
}
@Test
public void externalProvidersEnableBeforePackValidation() throws Exception {
String source = Files.readString(Path.of(System.getProperty("iris.startupSource")));
String enable = section(source, "private boolean enable()", "public void addShutdownHook()");
assertOrdered(enable,
"service.onEnable();",
"setContentChangeListener(generatorResolver::requestExternalContentRefresh)",
"generatorResolver.validateAllPacks();");
}
@Test
public void restartRequiredStartupTerminatesAfterPluginInitialization() throws Exception {
String source = Files.readString(Path.of(System.getProperty("iris.startupSource")));
@@ -2,9 +2,15 @@ package art.arcane.iris.core;
import art.arcane.iris.Iris;
import art.arcane.iris.core.lifecycle.WorldLifecycleStaging;
import art.arcane.iris.core.compat.CompatAction;
import art.arcane.iris.core.compat.CompatFinding;
import art.arcane.iris.core.compat.CompatRegistry;
import art.arcane.iris.core.service.ExternalDataSVC;
import art.arcane.iris.core.pack.BrokenPackException;
import art.arcane.iris.core.pack.PackValidationRegistry;
import art.arcane.iris.core.pack.PackValidationResult;
import art.arcane.iris.util.common.plugin.VolmitPlugin;
import art.arcane.iris.util.common.scheduling.J;
import org.junit.After;
import org.junit.Rule;
import org.junit.Test;
@@ -22,7 +28,9 @@ import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
import java.util.ArrayList;
import java.util.Random;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Supplier;
import static org.junit.Assert.assertEquals;
@@ -36,12 +44,31 @@ import static org.junit.Assert.assertTrue;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.mockStatic;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.when;
import static org.mockito.ArgumentMatchers.any;
public class IrisWorldGeneratorResolverTest {
@Rule
public TemporaryFolder temporaryFolder = new TemporaryFolder();
@Test
public void snapshotDeferralRequiresAnUnresolvedBlockWithAPendingProvider() {
ExternalDataSVC external = mock(ExternalDataSVC.class);
when(external.hasPendingBlockProvider("itemsadder:example/stone")).thenReturn(true);
PackValidationResult unavailable = new PackValidationResult("pack", List.of("unavailable"), List.of(), 1L,
List.of(new CompatFinding(CompatRegistry.BLOCK, "itemsadder:example/stone",
CompatAction.EXCLUDED, "biome", "main", "layers[0]")), "26.2");
assertTrue(IrisWorldGeneratorResolver.hasPendingExternalContent(unavailable, external));
when(external.hasPendingBlockProvider("itemsadder:example/stone")).thenReturn(false);
assertFalse(IrisWorldGeneratorResolver.hasPendingExternalContent(unavailable, external));
assertFalse(IrisWorldGeneratorResolver.hasPendingExternalContent(unavailable, null));
assertFalse(IrisWorldGeneratorResolver.hasPendingExternalContent(
new PackValidationResult("vanilla", List.of(), List.of(), 1L), external));
}
@After
public void clearValidationState() {
PackValidationRegistry.clear();
@@ -83,6 +110,36 @@ public class IrisWorldGeneratorResolverTest {
assertFalse(invalid.getBlockingErrors().toString(), invalid.isLoadable());
}
@Test
public void providerEventsCoalesceAndRefreshAgainAfterAnInFlightChange() {
IrisWorldGeneratorResolver resolver = spy(new IrisWorldGeneratorResolver(mock(VolmitPlugin.class)));
List<Runnable> tasks = new ArrayList<>();
AtomicInteger passes = new AtomicInteger();
doAnswer(invocation -> {
if (passes.incrementAndGet() == 1) {
resolver.requestExternalContentRefresh();
assertTrue(tasks.isEmpty());
}
return null;
}).when(resolver).refreshExternalContent();
try (MockedStatic<J> scheduler = mockStatic(J.class)) {
scheduler.when(() -> J.a(any(Runnable.class))).thenAnswer(invocation -> {
tasks.add(invocation.getArgument(0, Runnable.class));
return null;
});
resolver.requestExternalContentRefresh();
resolver.requestExternalContentRefresh();
assertEquals(1, tasks.size());
tasks.removeFirst().run();
assertEquals(1, passes.get());
assertEquals(1, tasks.size());
tasks.removeFirst().run();
assertEquals(2, passes.get());
assertTrue(tasks.isEmpty());
}
}
@Test
public void startupValidationPublishesFingerprintBoundExactRootResults() throws Exception {
String source = Files.readString(Path.of(