mirror of
https://github.com/VolmitSoftware/Iris.git
synced 2026-10-12 03:24:14 +00:00
IA Fixes
- Thsi fixes an integration between Itemsadder and Oraxen. - Waiting on oraxen folks
This commit is contained in:
+32
@@ -2,6 +2,10 @@ package art.arcane.iris.nativegen;
|
||||
|
||||
import art.arcane.iris.engine.history.BoundaryColumnGeometry;
|
||||
import art.arcane.iris.engine.history.TerrainBoundarySignature;
|
||||
import art.arcane.iris.spi.IrisPlatform;
|
||||
import art.arcane.iris.spi.IrisPlatforms;
|
||||
import art.arcane.iris.spi.PlatformBlockState;
|
||||
import art.arcane.iris.spi.PlatformRegistries;
|
||||
import net.minecraft.SharedConstants;
|
||||
import net.minecraft.core.Direction;
|
||||
import net.minecraft.server.Bootstrap;
|
||||
@@ -13,6 +17,7 @@ import net.minecraft.world.level.block.LiquidBlock;
|
||||
import net.minecraft.world.level.levelgen.Heightmap;
|
||||
import org.junit.BeforeClass;
|
||||
import org.junit.Test;
|
||||
import org.mockito.MockedStatic;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.OptionalInt;
|
||||
@@ -20,6 +25,9 @@ import java.util.OptionalInt;
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertThrows;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.mockStatic;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
public final class NativeTransitionColumnTest {
|
||||
@BeforeClass
|
||||
@@ -28,6 +36,30 @@ public final class NativeTransitionColumnTest {
|
||||
Bootstrap.bootStrap();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void customKeysUseCarrierForNativeColumnsAndHeightmaps() {
|
||||
String key = "itemsadder:rocks/ruby_ore";
|
||||
PlatformBlockState custom = mock(PlatformBlockState.class);
|
||||
PlatformBlockState carrier = mock(PlatformBlockState.class);
|
||||
when(custom.placementBaseState()).thenReturn(carrier);
|
||||
when(carrier.key()).thenReturn("minecraft:oak_log[axis=z]");
|
||||
IrisPlatform platform = mock(IrisPlatform.class);
|
||||
PlatformRegistries registries = mock(PlatformRegistries.class);
|
||||
when(platform.registries()).thenReturn(registries);
|
||||
when(registries.blockOrNull(key)).thenReturn(custom);
|
||||
LevelHeightAccessor height = LevelHeightAccessor.create(-16, 32);
|
||||
TerrainBoundarySignature signature = signature(key);
|
||||
|
||||
try (MockedStatic<IrisPlatforms> platforms = mockStatic(IrisPlatforms.class)) {
|
||||
platforms.when(IrisPlatforms::isBound).thenReturn(true);
|
||||
platforms.when(IrisPlatforms::get).thenReturn(platform);
|
||||
NoiseColumn column = NativeTransitionColumn.column(signature, height);
|
||||
|
||||
assertEquals(Direction.Axis.Z, column.getBlock(-15).getValue(RotatedPillarBlock.AXIS));
|
||||
assertEquals(-14, NativeTransitionColumn.height(signature, Heightmap.Types.OCEAN_FLOOR_WG, height));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void preservesNativePropertiesCavesAndFluids() {
|
||||
NoiseColumn column = NativeTransitionColumn.column(signature("minecraft:oak_log[axis=x]"),
|
||||
|
||||
@@ -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();
|
||||
|
||||
+181
-4
@@ -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);
|
||||
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();
|
||||
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",
|
||||
|
||||
+11
@@ -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")));
|
||||
|
||||
+57
@@ -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(
|
||||
|
||||
+11
-1
@@ -1,6 +1,8 @@
|
||||
package art.arcane.iris.nativegen;
|
||||
|
||||
import art.arcane.iris.engine.history.TerrainBoundarySignature;
|
||||
import art.arcane.iris.spi.IrisPlatforms;
|
||||
import art.arcane.iris.spi.PlatformBlockState;
|
||||
import com.mojang.brigadier.StringReader;
|
||||
import com.mojang.brigadier.exceptions.CommandSyntaxException;
|
||||
import net.minecraft.commands.arguments.blocks.BlockStateParser;
|
||||
@@ -39,7 +41,15 @@ public final class NativeTransitionColumn {
|
||||
}
|
||||
|
||||
private static BlockState parse(String key) {
|
||||
StringReader reader = new StringReader(key);
|
||||
String nativeKey = key;
|
||||
if (!key.startsWith("minecraft:") && IrisPlatforms.isBound()) {
|
||||
PlatformBlockState logicalState = IrisPlatforms.get().registries().blockOrNull(key);
|
||||
PlatformBlockState baseState = logicalState == null ? null : logicalState.placementBaseState();
|
||||
if (baseState != null) {
|
||||
nativeKey = baseState.key();
|
||||
}
|
||||
}
|
||||
StringReader reader = new StringReader(nativeKey);
|
||||
try {
|
||||
BlockState state = BlockStateParser.parseForBlock(BuiltInRegistries.BLOCK, reader, false).blockState();
|
||||
if (reader.canRead()) {
|
||||
|
||||
@@ -703,6 +703,7 @@ allprojects {
|
||||
maven { url = uri('https://jitpack.io') } // score
|
||||
maven { url = uri('https://repo.auxilor.io/repository/maven-releases/') } // eco
|
||||
maven { url = uri('https://repo.nexomc.com/releases/') } // nexo
|
||||
maven { url = uri('https://repo.oraxen.com/releases/') }
|
||||
maven { url = uri('https://maven.devs.beer/') } // itemsadder
|
||||
maven { url = uri('https://repo.extendedclip.com/releases/') } // placeholderapi
|
||||
maven { url = uri('https://mvn.lumine.io/repository/maven-public/') } // mythic
|
||||
|
||||
@@ -81,6 +81,9 @@ dependencies {
|
||||
|
||||
// Third Party Integrations
|
||||
compileOnly(libs.nexo)
|
||||
compileOnly(libs.oraxen) {
|
||||
transitive = false
|
||||
}
|
||||
compileOnly(libs.itemsadder)
|
||||
compileOnly(libs.score)
|
||||
compileOnly(libs.mmoitems)
|
||||
@@ -481,6 +484,29 @@ sourceSets {
|
||||
srcDir(generateTemplates.map { it.outputs })
|
||||
}
|
||||
}
|
||||
externalProviderTest {
|
||||
compileClasspath += sourceSets.test.compileClasspath + sourceSets.main.output
|
||||
runtimeClasspath += sourceSets.test.runtimeClasspath + sourceSets.main.output
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
externalProviderTestImplementation(libs.oraxen) {
|
||||
transitive = false
|
||||
}
|
||||
externalProviderTestImplementation(libs.itemsadder) {
|
||||
transitive = false
|
||||
}
|
||||
}
|
||||
|
||||
tasks.register('externalProviderTest', Test) {
|
||||
testClassesDirs = sourceSets.externalProviderTest.output.classesDirs
|
||||
classpath = sourceSets.externalProviderTest.runtimeClasspath
|
||||
jvmArgs('--add-modules', 'jdk.incubator.vector')
|
||||
}
|
||||
|
||||
tasks.named('check').configure {
|
||||
dependsOn('externalProviderTest')
|
||||
}
|
||||
|
||||
tasks.register('bukkitPurityRatchet') {
|
||||
|
||||
@@ -35,6 +35,7 @@ art/arcane/iris/core/link/data/MMOItemsDataProvider.java
|
||||
art/arcane/iris/core/link/data/MythicCrucibleDataProvider.java
|
||||
art/arcane/iris/core/link/data/MythicMobsDataProvider.java
|
||||
art/arcane/iris/core/link/data/NexoDataProvider.java
|
||||
art/arcane/iris/core/link/data/OraxenDataProvider.java
|
||||
art/arcane/iris/core/nms/INMS.java
|
||||
art/arcane/iris/core/nms/INMSBinding.java
|
||||
art/arcane/iris/core/nms/MinecraftVersion.java
|
||||
|
||||
+190
@@ -0,0 +1,190 @@
|
||||
package art.arcane.iris.core.link.data;
|
||||
|
||||
import art.arcane.iris.core.link.Identifier;
|
||||
import art.arcane.iris.engine.framework.Engine;
|
||||
import art.arcane.iris.util.common.data.IrisCustomData;
|
||||
import art.arcane.volmlib.util.collection.KMap;
|
||||
import dev.lone.itemsadder.api.CustomBlock;
|
||||
import dev.lone.itemsadder.api.CustomStack;
|
||||
import dev.lone.itemsadder.api.Events.ItemsAdderLoadDataEvent;
|
||||
import dev.lone.itemsadder.api.ItemsAdder;
|
||||
import org.bukkit.Location;
|
||||
import org.bukkit.block.Block;
|
||||
import org.bukkit.block.data.BlockData;
|
||||
import org.bukkit.plugin.Plugin;
|
||||
import org.junit.Test;
|
||||
import org.mockito.MockedStatic;
|
||||
|
||||
import java.util.MissingResourceException;
|
||||
import java.util.Set;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertSame;
|
||||
import static org.junit.Assert.assertThrows;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.mockStatic;
|
||||
import static org.mockito.Mockito.doReturn;
|
||||
import static org.mockito.Mockito.spy;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.verifyNoMoreInteractions;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
public class ItemAdderDataProviderTest {
|
||||
@Test
|
||||
public void loadedEmptyRegistryIsReadyAndDisabledPluginIsNot() {
|
||||
ItemAdderDataProvider provider = spy(new ItemAdderDataProvider());
|
||||
Plugin plugin = mock(Plugin.class);
|
||||
doReturn(plugin).when(provider).getPlugin();
|
||||
when(plugin.isEnabled()).thenReturn(true);
|
||||
try (MockedStatic<ItemsAdder> api = mockStatic(ItemsAdder.class);
|
||||
MockedStatic<CustomStack> items = mockStatic(CustomStack.class);
|
||||
MockedStatic<CustomBlock> blocks = mockStatic(CustomBlock.class)) {
|
||||
api.when(ItemsAdder::areItemsLoaded).thenReturn(false);
|
||||
provider.init();
|
||||
assertFalse(provider.isReady());
|
||||
|
||||
items.when(CustomStack::getNamespacedIdsInRegistry).thenReturn(Set.of());
|
||||
blocks.when(CustomBlock::getNamespacedIdsInRegistry).thenReturn(Set.of());
|
||||
provider.onLoadData(mock(ItemsAdderLoadDataEvent.class));
|
||||
|
||||
assertTrue(provider.isReady());
|
||||
assertTrue(provider.getTypes(DataType.BLOCK).isEmpty());
|
||||
when(plugin.isEnabled()).thenReturn(false);
|
||||
assertFalse(provider.isReady());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void startupWaitsForLoadEventBeforeReadingRegistries() {
|
||||
ItemAdderDataProvider provider = new ItemAdderDataProvider();
|
||||
try (MockedStatic<ItemsAdder> api = mockStatic(ItemsAdder.class);
|
||||
MockedStatic<CustomStack> items = mockStatic(CustomStack.class);
|
||||
MockedStatic<CustomBlock> blocks = mockStatic(CustomBlock.class)) {
|
||||
api.when(ItemsAdder::areItemsLoaded).thenReturn(false);
|
||||
|
||||
provider.init();
|
||||
|
||||
assertTrue(provider.getTypes(DataType.ITEM).isEmpty());
|
||||
assertTrue(provider.getTypes(DataType.BLOCK).isEmpty());
|
||||
assertFalse(provider.isValidProvider(new Identifier("rocks", "ruby_ore"), DataType.BLOCK));
|
||||
items.verifyNoInteractions();
|
||||
blocks.verifyNoInteractions();
|
||||
|
||||
items.when(CustomStack::getNamespacedIdsInRegistry).thenReturn(Set.of("rocks:ruby_ore"));
|
||||
blocks.when(CustomBlock::getNamespacedIdsInRegistry).thenReturn(Set.of("rocks:ruby_ore"));
|
||||
provider.onLoadData(mock(ItemsAdderLoadDataEvent.class));
|
||||
|
||||
assertEquals(Set.of(new Identifier("rocks", "ruby_ore")), provider.getTypes(DataType.ITEM));
|
||||
assertEquals(Set.of(new Identifier("rocks", "ruby_ore")), provider.getTypes(DataType.BLOCK));
|
||||
assertTrue(provider.isValidProvider(new Identifier("rocks", "ruby_ore"), DataType.BLOCK));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void blockClaimsUseExactRegistryIds() {
|
||||
ItemAdderDataProvider provider = new ItemAdderDataProvider();
|
||||
try (MockedStatic<ItemsAdder> api = mockStatic(ItemsAdder.class);
|
||||
MockedStatic<CustomStack> items = mockStatic(CustomStack.class);
|
||||
MockedStatic<CustomBlock> blocks = mockStatic(CustomBlock.class)) {
|
||||
api.when(ItemsAdder::areItemsLoaded).thenReturn(true);
|
||||
items.when(CustomStack::getNamespacedIdsInRegistry).thenReturn(Set.of("rocks:ruby_ore", "rocks:ruby_sword"));
|
||||
blocks.when(CustomBlock::getNamespacedIdsInRegistry).thenReturn(Set.of("rocks:ruby_ore"));
|
||||
|
||||
provider.init();
|
||||
|
||||
assertTrue(provider.isValidProvider(new Identifier("rocks", "ruby_ore"), DataType.BLOCK));
|
||||
assertFalse(provider.isValidProvider(new Identifier("rocks", "ruby_sword"), DataType.BLOCK));
|
||||
assertFalse(provider.isValidProvider(new Identifier("rocks", "unknown"), DataType.BLOCK));
|
||||
assertFalse(provider.isValidProvider(new Identifier("other", "ruby_ore"), DataType.BLOCK));
|
||||
assertFalse(provider.isValidProvider(new Identifier("rocks", "ruby_ore"), DataType.ENTITY));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void loadEventReplacesBlockClaimsAfterRegistryChanges() {
|
||||
ItemAdderDataProvider provider = new ItemAdderDataProvider();
|
||||
try (MockedStatic<ItemsAdder> api = mockStatic(ItemsAdder.class);
|
||||
MockedStatic<CustomStack> items = mockStatic(CustomStack.class);
|
||||
MockedStatic<CustomBlock> blocks = mockStatic(CustomBlock.class)) {
|
||||
api.when(ItemsAdder::areItemsLoaded).thenReturn(true);
|
||||
items.when(CustomStack::getNamespacedIdsInRegistry).thenReturn(Set.of("rocks:ruby_ore"));
|
||||
blocks.when(CustomBlock::getNamespacedIdsInRegistry).thenReturn(Set.of("rocks:ruby_ore"));
|
||||
provider.init();
|
||||
items.when(CustomStack::getNamespacedIdsInRegistry).thenReturn(Set.of("gems:sapphire_ore"));
|
||||
blocks.when(CustomBlock::getNamespacedIdsInRegistry).thenReturn(Set.of("rocks:sapphire_ore"));
|
||||
|
||||
assertEquals(Set.of(new Identifier("rocks", "ruby_ore")), provider.getTypes(DataType.ITEM));
|
||||
assertEquals(Set.of(new Identifier("rocks", "ruby_ore")), provider.getTypes(DataType.BLOCK));
|
||||
items.verify(CustomStack::getNamespacedIdsInRegistry);
|
||||
blocks.verify(CustomBlock::getNamespacedIdsInRegistry);
|
||||
|
||||
provider.onLoadData(mock(ItemsAdderLoadDataEvent.class));
|
||||
|
||||
assertFalse(provider.isValidProvider(new Identifier("rocks", "ruby_ore"), DataType.BLOCK));
|
||||
assertTrue(provider.isValidProvider(new Identifier("rocks", "sapphire_ore"), DataType.BLOCK));
|
||||
assertFalse(provider.isValidProvider(new Identifier("rocks", "ruby_ore"), DataType.ITEM));
|
||||
assertTrue(provider.isValidProvider(new Identifier("gems", "sapphire_ore"), DataType.ITEM));
|
||||
assertEquals(Set.of(new Identifier("gems", "sapphire_ore")), provider.getTypes(DataType.ITEM));
|
||||
assertEquals(Set.of(new Identifier("rocks", "sapphire_ore")), provider.getTypes(DataType.BLOCK));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void resolutionRetainsCarrierAndNativeIdentifier() {
|
||||
Identifier identifier = new Identifier("rocks", "ruby_ore");
|
||||
CustomBlock customBlock = mock(CustomBlock.class);
|
||||
BlockData carrier = mock(BlockData.class);
|
||||
when(customBlock.getBaseBlockData()).thenReturn(carrier);
|
||||
try (MockedStatic<CustomBlock> blocks = mockStatic(CustomBlock.class)) {
|
||||
blocks.when(() -> CustomBlock.getInstance("rocks:ruby_ore")).thenReturn(customBlock);
|
||||
|
||||
BlockData result = new ItemAdderDataProvider().getBlockData(identifier, new KMap<>());
|
||||
|
||||
assertTrue(result instanceof IrisCustomData);
|
||||
IrisCustomData custom = (IrisCustomData) result;
|
||||
assertSame(carrier, custom.getBase());
|
||||
assertEquals(identifier, custom.getCustom());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void placementDoesNotOverwriteProviderInitializedBlock() {
|
||||
Block block = mock(Block.class);
|
||||
Location location = new Location(null, 7, 81, -6);
|
||||
CustomBlock placed = mock(CustomBlock.class);
|
||||
when(block.getLocation()).thenReturn(location);
|
||||
try (MockedStatic<CustomBlock> blocks = mockStatic(CustomBlock.class)) {
|
||||
blocks.when(() -> CustomBlock.place("rocks:ruby_ore", location)).thenReturn(placed);
|
||||
|
||||
new ItemAdderDataProvider().processUpdate(mock(Engine.class), block, new Identifier("rocks", "ruby_ore"));
|
||||
|
||||
blocks.verify(() -> CustomBlock.place("rocks:ruby_ore", location));
|
||||
verify(block).getLocation();
|
||||
verifyNoMoreInteractions(block, placed);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void missingPlacementReportsFailure() {
|
||||
Block block = mock(Block.class);
|
||||
when(block.getLocation()).thenReturn(new Location(null, 7, 81, -6));
|
||||
try (MockedStatic<CustomBlock> blocks = mockStatic(CustomBlock.class)) {
|
||||
assertThrows(MissingResourceException.class, () -> new ItemAdderDataProvider().processUpdate(
|
||||
mock(Engine.class), block, new Identifier("rocks", "unknown")));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void unsupportedStateDoesNotResolveDefaultBlockSilently() {
|
||||
KMap<String, String> state = new KMap<>();
|
||||
state.put("facing", "north");
|
||||
try (MockedStatic<CustomBlock> blocks = mockStatic(CustomBlock.class)) {
|
||||
assertThrows(MissingResourceException.class, () -> new ItemAdderDataProvider().getBlockData(
|
||||
new Identifier("rocks", "ruby_ore"), state));
|
||||
|
||||
blocks.verifyNoInteractions();
|
||||
}
|
||||
}
|
||||
}
|
||||
+91
@@ -0,0 +1,91 @@
|
||||
package art.arcane.iris.core.link.data;
|
||||
|
||||
import art.arcane.iris.core.link.Identifier;
|
||||
import art.arcane.iris.engine.framework.Engine;
|
||||
import art.arcane.iris.util.common.data.IrisCustomData;
|
||||
import art.arcane.volmlib.util.collection.KMap;
|
||||
import io.th0rgal.oraxen.api.OraxenBlocks;
|
||||
import org.bukkit.Location;
|
||||
import org.bukkit.block.Block;
|
||||
import org.bukkit.block.data.BlockData;
|
||||
import org.junit.Test;
|
||||
import org.mockito.MockedStatic;
|
||||
|
||||
import java.util.MissingResourceException;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertSame;
|
||||
import static org.junit.Assert.assertThrows;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.mockStatic;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
public class OraxenDataProviderTest {
|
||||
@Test
|
||||
public void blockResolutionRetainsProviderIdentityForDeferredPlacement() {
|
||||
Identifier identifier = new Identifier("oraxen", "ruby_ore");
|
||||
BlockData carrier = mock(BlockData.class);
|
||||
OraxenDataProvider provider = new OraxenDataProvider();
|
||||
try (MockedStatic<OraxenBlocks> blocks = mockStatic(OraxenBlocks.class)) {
|
||||
blocks.when(() -> OraxenBlocks.getOraxenBlockData("ruby_ore")).thenReturn(carrier);
|
||||
|
||||
BlockData result = provider.getBlockData(identifier, new KMap<>());
|
||||
|
||||
assertTrue(result instanceof IrisCustomData);
|
||||
IrisCustomData custom = (IrisCustomData) result;
|
||||
assertSame(carrier, custom.getBase());
|
||||
assertEquals(identifier, custom.getCustom());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void missingBlockDoesNotProduceCarrierData() {
|
||||
OraxenDataProvider provider = new OraxenDataProvider();
|
||||
try (MockedStatic<OraxenBlocks> blocks = mockStatic(OraxenBlocks.class)) {
|
||||
assertThrows(MissingResourceException.class,
|
||||
() -> provider.getBlockData(new Identifier("oraxen", "unknown"), new KMap<>()));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void unsupportedStateDoesNotResolveDefaultBlockSilently() {
|
||||
OraxenDataProvider provider = new OraxenDataProvider();
|
||||
KMap<String, String> state = new KMap<>();
|
||||
state.put("facing", "north");
|
||||
try (MockedStatic<OraxenBlocks> blocks = mockStatic(OraxenBlocks.class)) {
|
||||
assertThrows(MissingResourceException.class,
|
||||
() -> provider.getBlockData(new Identifier("oraxen", "ruby_ore"), state));
|
||||
|
||||
blocks.verifyNoInteractions();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void placementUsesProviderApiAtTargetLocation() {
|
||||
OraxenDataProvider provider = new OraxenDataProvider();
|
||||
Block block = mock(Block.class);
|
||||
Location location = new Location(null, 31, 80, -18);
|
||||
when(block.getLocation()).thenReturn(location);
|
||||
try (MockedStatic<OraxenBlocks> blocks = mockStatic(OraxenBlocks.class)) {
|
||||
provider.processUpdate(mock(Engine.class), block, new Identifier("oraxen", "ruby_ore"));
|
||||
|
||||
blocks.verify(() -> OraxenBlocks.place("ruby_ore", location));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void blockClaimsRequireExactRegisteredMechanic() {
|
||||
OraxenDataProvider provider = new OraxenDataProvider();
|
||||
try (MockedStatic<OraxenBlocks> blocks = mockStatic(OraxenBlocks.class)) {
|
||||
blocks.when(() -> OraxenBlocks.isOraxenBlock("ruby_ore")).thenReturn(true);
|
||||
|
||||
assertTrue(provider.isValidProvider(new Identifier("oraxen", "ruby_ore"), DataType.BLOCK));
|
||||
assertFalse(provider.isValidProvider(new Identifier("oraxen", "ruby_sword"), DataType.BLOCK));
|
||||
assertFalse(provider.isValidProvider(new Identifier("nexo", "ruby_ore"), DataType.BLOCK));
|
||||
assertFalse(provider.isValidProvider(new Identifier("oraxen", "ruby_ore"), DataType.ENTITY));
|
||||
assertTrue(provider.getTypes(DataType.ENTITY).isEmpty());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -382,21 +382,24 @@ public final class ContentGate {
|
||||
return merged.isEmpty() ? Map.of() : Collections.unmodifiableMap(merged);
|
||||
}
|
||||
|
||||
/** {@code namespace:name[props]} lowercased with the default namespace applied; null for blank input. */
|
||||
/** Normalizes block names and vanilla properties, preserving external property case; null for blank input. */
|
||||
public static String normalizeState(String state) {
|
||||
if (state == null) {
|
||||
return null;
|
||||
}
|
||||
String trimmed = state.trim().toLowerCase(Locale.ROOT);
|
||||
String trimmed = state.trim();
|
||||
if (trimmed.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
int props = trimmed.indexOf('[');
|
||||
String base = props < 0 ? trimmed : trimmed.substring(0, props);
|
||||
String base = (props < 0 ? trimmed : trimmed.substring(0, props)).toLowerCase(Locale.ROOT);
|
||||
String rest = props < 0 ? "" : trimmed.substring(props);
|
||||
if (base.indexOf(':') < 0) {
|
||||
base = DEFAULT_NAMESPACE + ':' + base;
|
||||
}
|
||||
if (base.startsWith(DEFAULT_NAMESPACE + ':')) {
|
||||
rest = rest.toLowerCase(Locale.ROOT);
|
||||
}
|
||||
return base + rest;
|
||||
}
|
||||
|
||||
|
||||
@@ -3,12 +3,15 @@ package art.arcane.iris.core.link.data;
|
||||
import art.arcane.iris.spi.IrisLogging;
|
||||
import art.arcane.iris.core.link.ExternalDataProvider;
|
||||
import art.arcane.iris.core.link.Identifier;
|
||||
import art.arcane.iris.core.service.ExternalDataSVC;
|
||||
import art.arcane.iris.engine.framework.Engine;
|
||||
import art.arcane.iris.spi.IrisServices;
|
||||
import art.arcane.volmlib.util.collection.KMap;
|
||||
import art.arcane.iris.util.common.data.IrisCustomData;
|
||||
import dev.lone.itemsadder.api.CustomBlock;
|
||||
import dev.lone.itemsadder.api.CustomStack;
|
||||
import dev.lone.itemsadder.api.Events.ItemsAdderLoadDataEvent;
|
||||
import dev.lone.itemsadder.api.ItemsAdder;
|
||||
import org.bukkit.block.Block;
|
||||
import org.bukkit.block.data.BlockData;
|
||||
import org.bukkit.event.EventHandler;
|
||||
@@ -24,8 +27,7 @@ import java.util.stream.Collectors;
|
||||
|
||||
public class ItemAdderDataProvider extends ExternalDataProvider implements Listener {
|
||||
|
||||
private volatile Set<String> itemNamespaces = Set.of();
|
||||
private volatile Set<String> blockNamespaces = Set.of();
|
||||
private volatile ContentRegistry content = new ContentRegistry(Set.of(), Set.of(), Set.of(), false);
|
||||
|
||||
public ItemAdderDataProvider() {
|
||||
super("ItemsAdder");
|
||||
@@ -33,22 +35,42 @@ public class ItemAdderDataProvider extends ExternalDataProvider implements Liste
|
||||
|
||||
@Override
|
||||
public void init() {
|
||||
updateNamespaces();
|
||||
if (ItemsAdder.areItemsLoaded()) {
|
||||
updateContent();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isReady() {
|
||||
return super.isReady() && content.ready();
|
||||
}
|
||||
|
||||
@EventHandler
|
||||
public void onLoadData(ItemsAdderLoadDataEvent event) {
|
||||
updateNamespaces();
|
||||
if (!updateContent()) {
|
||||
return;
|
||||
}
|
||||
ExternalDataSVC service = IrisServices.getOrNull(ExternalDataSVC.class);
|
||||
if (service != null) {
|
||||
service.notifyContentChanged();
|
||||
}
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
public BlockData getBlockData(@NotNull Identifier blockId, @NotNull KMap<String, String> state) throws MissingResourceException {
|
||||
if (!state.isEmpty()) {
|
||||
throw new MissingResourceException("ItemsAdder blocks do not expose Iris block properties.", blockId.namespace(), blockId.key());
|
||||
}
|
||||
CustomBlock block = CustomBlock.getInstance(blockId.toString());
|
||||
if (block == null) {
|
||||
throw new MissingResourceException("Failed to find BlockData!", blockId.namespace(), blockId.key());
|
||||
}
|
||||
return IrisCustomData.of(block.getBaseBlockData(), blockId);
|
||||
BlockData base = block.getBaseBlockData();
|
||||
if (base == null) {
|
||||
throw new MissingResourceException("Failed to find ItemsAdder block data.", blockId.namespace(), blockId.key());
|
||||
}
|
||||
return IrisCustomData.of(base, blockId);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@@ -63,46 +85,42 @@ public class ItemAdderDataProvider extends ExternalDataProvider implements Liste
|
||||
|
||||
@Override
|
||||
public void processUpdate(@NotNull Engine engine, @NotNull Block block, @NotNull Identifier blockId) {
|
||||
CustomBlock custom;
|
||||
if ((custom = CustomBlock.place(blockId.toString(), block.getLocation())) == null)
|
||||
return;
|
||||
block.setBlockData(custom.getBaseBlockData(), false);
|
||||
if (CustomBlock.place(blockId.toString(), block.getLocation()) == null) {
|
||||
throw new MissingResourceException("Failed to place ItemsAdder block.", blockId.namespace(), blockId.key());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NotNull Collection<@NotNull Identifier> getTypes(@NotNull DataType dataType) {
|
||||
return switch (dataType) {
|
||||
case ENTITY -> List.of();
|
||||
case ITEM -> CustomStack.getNamespacedIdsInRegistry()
|
||||
.stream()
|
||||
.map(Identifier::fromString)
|
||||
.toList();
|
||||
case BLOCK -> CustomBlock.getNamespacedIdsInRegistry()
|
||||
.stream()
|
||||
.map(Identifier::fromString)
|
||||
.toList();
|
||||
case ITEM -> content.items();
|
||||
case BLOCK -> content.blocks();
|
||||
};
|
||||
}
|
||||
|
||||
private void updateNamespaces() {
|
||||
try {
|
||||
updateNamespaces(DataType.ITEM);
|
||||
updateNamespaces(DataType.BLOCK);
|
||||
} catch (Throwable e) {
|
||||
IrisLogging.warn("Failed to update ItemAdder namespaces: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private void updateNamespaces(DataType dataType) {
|
||||
Set<String> namespaces = getTypes(dataType).stream().map(Identifier::namespace).collect(Collectors.toSet());
|
||||
if (dataType == DataType.ITEM) itemNamespaces = namespaces;
|
||||
else blockNamespaces = namespaces;
|
||||
IrisLogging.debug("Updated ItemAdder namespaces: " + dataType + " - " + namespaces);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isValidProvider(@NotNull Identifier id, DataType dataType) {
|
||||
if (dataType == DataType.ENTITY) return false;
|
||||
return dataType == DataType.ITEM ? itemNamespaces.contains(id.namespace()) : blockNamespaces.contains(id.namespace());
|
||||
return dataType == DataType.ITEM ? content.itemNamespaces().contains(id.namespace()) : content.blocks().contains(id);
|
||||
}
|
||||
|
||||
private boolean updateContent() {
|
||||
try {
|
||||
Set<Identifier> items = CustomStack.getNamespacedIdsInRegistry().stream()
|
||||
.map(Identifier::fromString).collect(Collectors.toUnmodifiableSet());
|
||||
Set<Identifier> blocks = CustomBlock.getNamespacedIdsInRegistry().stream()
|
||||
.map(Identifier::fromString).collect(Collectors.toUnmodifiableSet());
|
||||
Set<String> itemNamespaces = items.stream().map(Identifier::namespace).collect(Collectors.toUnmodifiableSet());
|
||||
content = new ContentRegistry(items, blocks, itemNamespaces, true);
|
||||
IrisLogging.debug("Updated ItemsAdder content registry: " + items.size() + " items, " + blocks.size() + " blocks");
|
||||
return true;
|
||||
} catch (RuntimeException | LinkageError e) {
|
||||
IrisLogging.reportError("Failed to update ItemsAdder content registry.", e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private record ContentRegistry(Set<Identifier> items, Set<Identifier> blocks, Set<String> itemNamespaces, boolean ready) {
|
||||
}
|
||||
}
|
||||
|
||||
@@ -71,7 +71,9 @@ public class KGeneratorsDataProvider extends ExternalDataProvider {
|
||||
|
||||
@Override
|
||||
public boolean isValidProvider(@NotNull Identifier id, DataType dataType) {
|
||||
if (dataType == DataType.ENTITY) return false;
|
||||
return "kgenerators".equalsIgnoreCase(id.namespace());
|
||||
if (dataType == DataType.ENTITY || !"kgenerators".equalsIgnoreCase(id.namespace())) {
|
||||
return false;
|
||||
}
|
||||
return dataType == DataType.ITEM || Main.getGenerators().get(id.key()) != null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -116,8 +116,20 @@ public class MMOItemsDataProvider extends ExternalDataProvider {
|
||||
|
||||
@Override
|
||||
public boolean isValidProvider(@NotNull Identifier id, DataType dataType) {
|
||||
if (dataType == DataType.ENTITY) return false;
|
||||
return dataType == DataType.ITEM ? id.namespace().split("_", 2).length == 2 : id.namespace().equals("mmoitems");
|
||||
if (dataType == DataType.ENTITY) {
|
||||
return false;
|
||||
}
|
||||
if (dataType == DataType.ITEM) {
|
||||
return id.namespace().split("_", 2).length == 2;
|
||||
}
|
||||
if (!id.namespace().equals("mmoitems")) {
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
return api().getCustomBlocks().getBlock(Integer.parseInt(id.key())) != null;
|
||||
} catch (NumberFormatException ignored) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private MMOItems api() {
|
||||
|
||||
@@ -138,7 +138,13 @@ public class MythicCrucibleDataProvider extends ExternalDataProvider {
|
||||
|
||||
@Override
|
||||
public boolean isValidProvider(@NotNull Identifier key, DataType dataType) {
|
||||
if (dataType == DataType.ENTITY) return false;
|
||||
return key.namespace().equalsIgnoreCase("crucible");
|
||||
if (dataType == DataType.ENTITY || !key.namespace().equalsIgnoreCase("crucible")) {
|
||||
return false;
|
||||
}
|
||||
if (dataType == DataType.ITEM) {
|
||||
return true;
|
||||
}
|
||||
CrucibleItem item = itemManager.getItem(key.key()).orElse(null);
|
||||
return item != null && (item.getBlockData() != null || item.getFurnitureData() != null);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -140,7 +140,13 @@ public class NexoDataProvider extends ExternalDataProvider {
|
||||
|
||||
@Override
|
||||
public boolean isValidProvider(@NotNull Identifier id, DataType dataType) {
|
||||
if (dataType == DataType.ENTITY) return false;
|
||||
return "nexo".equalsIgnoreCase(id.namespace());
|
||||
if (!"nexo".equalsIgnoreCase(id.namespace())) {
|
||||
return false;
|
||||
}
|
||||
return switch (dataType) {
|
||||
case BLOCK -> NexoBlocks.isCustomBlock(id.key()) || NexoFurniture.isFurniture(id.key());
|
||||
case ITEM -> NexoItems.exists(id.key());
|
||||
case ENTITY -> false;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
package art.arcane.iris.core.link.data;
|
||||
|
||||
import art.arcane.iris.core.link.ExternalDataProvider;
|
||||
import art.arcane.iris.core.link.Identifier;
|
||||
import art.arcane.iris.core.service.ExternalDataSVC;
|
||||
import art.arcane.iris.engine.framework.Engine;
|
||||
import art.arcane.iris.spi.IrisServices;
|
||||
import art.arcane.iris.util.common.data.IrisCustomData;
|
||||
import art.arcane.volmlib.util.collection.KMap;
|
||||
import io.th0rgal.oraxen.api.OraxenBlocks;
|
||||
import io.th0rgal.oraxen.api.OraxenItems;
|
||||
import io.th0rgal.oraxen.api.events.OraxenItemsLoadedEvent;
|
||||
import io.th0rgal.oraxen.items.ItemBuilder;
|
||||
import org.bukkit.block.Block;
|
||||
import org.bukkit.block.data.BlockData;
|
||||
import org.bukkit.inventory.ItemStack;
|
||||
import org.bukkit.event.EventHandler;
|
||||
import org.bukkit.event.Listener;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.MissingResourceException;
|
||||
|
||||
public final class OraxenDataProvider extends ExternalDataProvider implements Listener {
|
||||
public OraxenDataProvider() {
|
||||
super("Oraxen");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void init() {
|
||||
}
|
||||
|
||||
@EventHandler
|
||||
public void onItemsLoaded(OraxenItemsLoadedEvent event) {
|
||||
ExternalDataSVC service = IrisServices.getOrNull(ExternalDataSVC.class);
|
||||
if (service != null) {
|
||||
service.notifyContentChanged();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NotNull BlockData getBlockData(@NotNull Identifier blockId, @NotNull KMap<String, String> state) {
|
||||
if (!state.isEmpty()) {
|
||||
throw new MissingResourceException("Oraxen blocks do not expose Iris block properties.", blockId.namespace(), blockId.key());
|
||||
}
|
||||
BlockData data = OraxenBlocks.getOraxenBlockData(blockId.key());
|
||||
if (data == null) {
|
||||
throw new MissingResourceException("Failed to find BlockData!", blockId.namespace(), blockId.key());
|
||||
}
|
||||
return IrisCustomData.of(data, blockId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NotNull ItemStack getItemStack(@NotNull Identifier itemId, @NotNull KMap<String, Object> customNbt) {
|
||||
ItemBuilder builder = OraxenItems.getItemById(itemId.key());
|
||||
if (builder == null) {
|
||||
throw new MissingResourceException("Failed to find ItemData!", itemId.namespace(), itemId.key());
|
||||
}
|
||||
return builder.build();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void processUpdate(@NotNull Engine engine, @NotNull Block block, @NotNull Identifier blockId) {
|
||||
OraxenBlocks.place(blockId.key(), block.getLocation());
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NotNull Collection<@NotNull Identifier> getTypes(@NotNull DataType dataType) {
|
||||
if (dataType == DataType.ENTITY) {
|
||||
return List.of();
|
||||
}
|
||||
return OraxenItems.getNames().stream()
|
||||
.filter(id -> dataType == DataType.ITEM || OraxenBlocks.isOraxenBlock(id))
|
||||
.map(id -> new Identifier("oraxen", id))
|
||||
.toList();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isValidProvider(@NotNull Identifier id, DataType dataType) {
|
||||
if (!"oraxen".equalsIgnoreCase(id.namespace())) {
|
||||
return false;
|
||||
}
|
||||
return switch (dataType) {
|
||||
case BLOCK -> OraxenBlocks.isOraxenBlock(id.key());
|
||||
case ITEM -> OraxenItems.exists(id.key());
|
||||
case ENTITY -> false;
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -198,6 +198,21 @@ public class IrisData implements ExclusionStrategy, TypeAdapterFactory {
|
||||
}
|
||||
}
|
||||
|
||||
public static void invalidateLoadedContentRegistries(File source) {
|
||||
Path requested = dataFolderIdentity(Objects.requireNonNull(source, "authoring pack source"));
|
||||
for (Map.Entry<File, IrisData> entry : dataLoaders.entrySet()) {
|
||||
IrisData data = entry.getValue();
|
||||
if (dataFolderIdentity(entry.getKey()).equals(requested)) {
|
||||
synchronized (data) {
|
||||
if (data.generationRegistryContract == null && data.getEngines().isEmpty()) {
|
||||
data.invalidateAuthoringResources();
|
||||
data.compatReport.clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private synchronized void invalidateAuthoringResources() {
|
||||
if (generationRegistryContract != null) {
|
||||
throw new IllegalStateException("Cannot invalidate an immutable generation pack as an authoring source.");
|
||||
|
||||
@@ -88,7 +88,7 @@ public final class ContentKeyValidator {
|
||||
return List.of();
|
||||
}
|
||||
|
||||
List<String> blockKeys = normalizeKeyList(registries.blockKeys());
|
||||
List<String> blockKeys = normalizeKeyList(registries.blockTypeKeys());
|
||||
List<String> itemKeys = normalizeKeyList(registries.itemKeys());
|
||||
List<String> entityKeys = normalizeKeyList(registries.entityKeys());
|
||||
|
||||
@@ -432,7 +432,7 @@ public final class ContentKeyValidator {
|
||||
if (registries == null) {
|
||||
return ContentKeyIssues.none();
|
||||
}
|
||||
List<String> blockKeys = registries.blockKeys();
|
||||
List<String> blockKeys = registries.blockTypeKeys();
|
||||
List<String> itemKeys = registries.itemKeys();
|
||||
List<String> entityKeys = registries.entityKeys();
|
||||
if (blockKeys == null || blockKeys.isEmpty() || itemKeys == null || itemKeys.isEmpty() || entityKeys == null || entityKeys.isEmpty()) {
|
||||
|
||||
@@ -57,6 +57,7 @@ public final class PackValidationCache {
|
||||
PlatformRegistries registries = Objects.requireNonNull(
|
||||
platform.registries(), "Pack validation platform registries");
|
||||
updateSorted(digest, registries.blockKeys());
|
||||
updateSorted(digest, registries.blockTypeKeys());
|
||||
updateSorted(digest, registries.biomeKeys());
|
||||
updateSorted(digest, registries.itemKeys());
|
||||
updateSorted(digest, registries.entityKeys());
|
||||
|
||||
@@ -314,6 +314,10 @@ public class SchemaBuilder {
|
||||
return a;
|
||||
}
|
||||
|
||||
private static String definitionReference(String key) {
|
||||
return "#/definitions/" + key.replace("~", "~0").replace("/", "~1");
|
||||
}
|
||||
|
||||
private static String stripNamespace(String key) {
|
||||
int idx = key.indexOf(':');
|
||||
return idx < 0 ? key : key.substring(idx + 1);
|
||||
@@ -566,12 +570,12 @@ public class SchemaBuilder {
|
||||
.put("properties", new JSONObject()
|
||||
.put(blockType, new JSONObject()
|
||||
.put("type", "string")
|
||||
.put("$ref", "#/definitions/" + enumKey))))
|
||||
.put("$ref", definitionReference(enumKey)))))
|
||||
.put("then", new JSONObject()
|
||||
.put("properties", new JSONObject()
|
||||
.put(k.getName(), new JSONObject()
|
||||
.put("type", "object")
|
||||
.put("$ref", "#/definitions/" + propertiesKey))))
|
||||
.put("$ref", definitionReference(propertiesKey)))))
|
||||
.put("else", false));
|
||||
|
||||
if (!definitions.containsKey(enumKey)) {
|
||||
|
||||
@@ -26,7 +26,7 @@ import art.arcane.iris.core.link.data.DataType;
|
||||
import art.arcane.iris.core.nms.container.BlockProperty;
|
||||
import art.arcane.iris.core.nms.container.Pair;
|
||||
import art.arcane.iris.engine.framework.Engine;
|
||||
import art.arcane.volmlib.util.collection.KList;
|
||||
import art.arcane.iris.util.common.data.IrisCustomData;
|
||||
import art.arcane.volmlib.util.collection.KMap;
|
||||
import art.arcane.iris.util.common.plugin.IrisService;
|
||||
import org.bukkit.Bukkit;
|
||||
@@ -41,10 +41,16 @@ import org.bukkit.inventory.ItemStack;
|
||||
import org.bukkit.plugin.Plugin;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.MissingResourceException;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.CopyOnWriteArrayList;
|
||||
import java.util.concurrent.atomic.AtomicLong;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
public class ExternalDataSVC implements IrisService {
|
||||
@@ -52,6 +58,7 @@ public class ExternalDataSVC implements IrisService {
|
||||
private static final List<ProviderDefinition> BUILT_IN_PROVIDERS = List.of(
|
||||
new ProviderDefinition("CraftEngine", PROVIDER_PACKAGE + "CraftEngineDataProvider"),
|
||||
new ProviderDefinition("Nexo", PROVIDER_PACKAGE + "NexoDataProvider"),
|
||||
new ProviderDefinition("Oraxen", PROVIDER_PACKAGE + "OraxenDataProvider"),
|
||||
new ProviderDefinition("ItemsAdder", PROVIDER_PACKAGE + "ItemAdderDataProvider"),
|
||||
new ProviderDefinition("ExecutableItems", PROVIDER_PACKAGE + "ExecutableItemsDataProvider"),
|
||||
new ProviderDefinition("MMOItems", PROVIDER_PACKAGE + "MMOItemsDataProvider"),
|
||||
@@ -60,9 +67,13 @@ public class ExternalDataSVC implements IrisService {
|
||||
new ProviderDefinition("MythicCrucible", PROVIDER_PACKAGE + "MythicCrucibleDataProvider"),
|
||||
new ProviderDefinition("KGenerators", PROVIDER_PACKAGE + "KGeneratorsDataProvider")
|
||||
);
|
||||
private static final Set<String> BLOCK_PROVIDER_PLUGINS = Set.of(
|
||||
"craftengine", "nexo", "oraxen", "itemsadder", "mmoitems", "mythiccrucible", "kgenerators");
|
||||
|
||||
private final KList<ExternalDataProvider> providers = new KList<>();
|
||||
private final KList<ExternalDataProvider> activeProviders = new KList<>();
|
||||
private final List<ExternalDataProvider> providers = new CopyOnWriteArrayList<>();
|
||||
private final List<ExternalDataProvider> activeProviders = new CopyOnWriteArrayList<>();
|
||||
private final AtomicLong contentRevision = new AtomicLong();
|
||||
private volatile Runnable contentChangeListener;
|
||||
|
||||
@Override
|
||||
public void onEnable() {
|
||||
@@ -77,10 +88,67 @@ public class ExternalDataSVC implements IrisService {
|
||||
|
||||
@Override
|
||||
public void onDisable() {
|
||||
contentChangeListener = null;
|
||||
activeProviders.clear();
|
||||
providers.clear();
|
||||
}
|
||||
|
||||
public void setContentChangeListener(Runnable listener) {
|
||||
contentChangeListener = Objects.requireNonNull(listener);
|
||||
}
|
||||
|
||||
public void notifyContentChanged() {
|
||||
contentRevision.incrementAndGet();
|
||||
Runnable listener = contentChangeListener;
|
||||
if (listener != null) {
|
||||
listener.run();
|
||||
}
|
||||
}
|
||||
|
||||
public long contentRevision() {
|
||||
return contentRevision.get();
|
||||
}
|
||||
|
||||
public boolean hasPendingBlockProvider(String blockKey) {
|
||||
Identifier identifier = Identifier.fromString(blockKey);
|
||||
if ("minecraft".equals(identifier.namespace())) {
|
||||
return false;
|
||||
}
|
||||
String namespace = identifier.namespace().toLowerCase(Locale.ROOT);
|
||||
Optional<ExternalDataProvider> qualified = findProvider(namespace);
|
||||
if (qualified.isPresent()) {
|
||||
return !qualified.get().isReady();
|
||||
}
|
||||
if (BLOCK_PROVIDER_PLUGINS.contains(namespace)) {
|
||||
return isInstalledProviderPending(namespace);
|
||||
}
|
||||
for (String pluginId : BLOCK_PROVIDER_PLUGINS) {
|
||||
if (isInstalledProviderPending(pluginId)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
for (ExternalDataProvider provider : providers) {
|
||||
if (!provider.isReady()) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private boolean isInstalledProviderPending(String pluginId) {
|
||||
String installedName = BUILT_IN_PROVIDERS.stream()
|
||||
.map(ProviderDefinition::pluginId)
|
||||
.filter(name -> name.equalsIgnoreCase(pluginId))
|
||||
.findFirst()
|
||||
.orElse(pluginId);
|
||||
Plugin plugin = Bukkit.getPluginManager().getPlugin(installedName);
|
||||
if (plugin == null) {
|
||||
return false;
|
||||
}
|
||||
Optional<ExternalDataProvider> provider = findProvider(pluginId);
|
||||
return !plugin.isEnabled() || provider.isEmpty() || !provider.get().isReady();
|
||||
}
|
||||
|
||||
@EventHandler
|
||||
public void onPluginEnable(PluginEnableEvent event) {
|
||||
String pluginId = event.getPlugin().getName();
|
||||
@@ -106,7 +174,8 @@ public class ExternalDataSVC implements IrisService {
|
||||
}
|
||||
|
||||
providers.add(registeredProvider);
|
||||
if (registeredProvider.isReady()) {
|
||||
Plugin plugin = registeredProvider.getPlugin();
|
||||
if (plugin != null && plugin.isEnabled()) {
|
||||
activateProvider(registeredProvider);
|
||||
}
|
||||
}
|
||||
@@ -119,28 +188,35 @@ public class ExternalDataSVC implements IrisService {
|
||||
IrisLogging.error(e.getMessage());
|
||||
return Optional.empty();
|
||||
}
|
||||
Identifier mod = pair.getA();
|
||||
|
||||
Optional<ExternalDataProvider> provider = activeProviders.stream().filter(p -> p.isValidProvider(mod, DataType.BLOCK)).findFirst();
|
||||
if (provider.isEmpty())
|
||||
return Optional.empty();
|
||||
try {
|
||||
return Optional.of(provider.get().getBlockData(mod, pair.getB()));
|
||||
BlockProvider match = findBlockProvider(pair.getA());
|
||||
if (match == null) {
|
||||
return Optional.empty();
|
||||
}
|
||||
BlockData resolved = match.provider().getBlockData(match.identifier(), pair.getB());
|
||||
if (resolved instanceof IrisCustomData custom) {
|
||||
resolved = IrisCustomData.of(custom.getBase(), qualifyBlockId(match.provider(), custom.getCustom()));
|
||||
}
|
||||
return Optional.of(resolved);
|
||||
} catch (MissingResourceException e) {
|
||||
IrisLogging.error(e.getMessage() + " - [" + e.getClassName() + ":" + e.getKey() + "]");
|
||||
return Optional.empty();
|
||||
} catch (RuntimeException | LinkageError e) {
|
||||
IrisLogging.reportError("Failed to resolve external block " + key + ".", e);
|
||||
return Optional.empty();
|
||||
}
|
||||
}
|
||||
|
||||
public Optional<List<BlockProperty>> getBlockProperties(final Identifier key) {
|
||||
Optional<ExternalDataProvider> provider = activeProviders.stream().filter(p -> p.isValidProvider(key, DataType.BLOCK)).findFirst();
|
||||
if (provider.isEmpty())
|
||||
return Optional.empty();
|
||||
try {
|
||||
return Optional.of(provider.get().getBlockProperties(key));
|
||||
BlockProvider match = findBlockProvider(parseState(key).getA());
|
||||
return match == null ? Optional.empty() : Optional.of(match.provider().getBlockProperties(match.identifier()));
|
||||
} catch (MissingResourceException e) {
|
||||
IrisLogging.error(e.getMessage() + " - [" + e.getClassName() + ":" + e.getKey() + "]");
|
||||
return Optional.empty();
|
||||
} catch (RuntimeException | LinkageError e) {
|
||||
IrisLogging.reportError("Failed to resolve external block properties for " + key + ".", e);
|
||||
return Optional.empty();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -160,13 +236,12 @@ public class ExternalDataSVC implements IrisService {
|
||||
}
|
||||
|
||||
public void processUpdate(Engine engine, Block block, Identifier blockId) {
|
||||
Optional<ExternalDataProvider> provider = activeProviders.stream().filter(p -> p.isValidProvider(blockId, DataType.BLOCK)).findFirst();
|
||||
if (provider.isEmpty()) {
|
||||
IrisLogging.warnOnce("external-provider:block:" + blockId,
|
||||
"No matching Provider found for modded material \"%s\"!", blockId);
|
||||
return;
|
||||
Pair<Identifier, KMap<String, String>> state = parseState(blockId);
|
||||
BlockProvider match = findBlockProvider(state.getA());
|
||||
if (match == null) {
|
||||
throw new MissingResourceException("No matching provider found for external block placement.", blockId.namespace(), blockId.key());
|
||||
}
|
||||
provider.get().processUpdate(engine, block, blockId);
|
||||
match.provider().processUpdate(engine, block, buildState(match.identifier(), state.getB()));
|
||||
}
|
||||
|
||||
public Entity spawnMob(Location location, Identifier mobId) {
|
||||
@@ -185,36 +260,61 @@ public class ExternalDataSVC implements IrisService {
|
||||
}
|
||||
|
||||
public Collection<Identifier> getAllIdentifiers(DataType dataType) {
|
||||
if (dataType == DataType.BLOCK) {
|
||||
Set<Identifier> identifiers = new LinkedHashSet<>();
|
||||
for (ExternalDataProvider provider : activeProviders) {
|
||||
for (Identifier identifier : providerBlockTypes(provider)) {
|
||||
identifiers.add(identifier);
|
||||
identifiers.add(qualifyBlockId(provider, identifier));
|
||||
}
|
||||
}
|
||||
return List.copyOf(identifiers);
|
||||
}
|
||||
return activeProviders.stream()
|
||||
.flatMap(p -> p.getTypes(dataType).stream())
|
||||
.toList();
|
||||
}
|
||||
|
||||
private Collection<Identifier> providerBlockTypes(ExternalDataProvider provider) {
|
||||
try {
|
||||
return provider.getTypes(DataType.BLOCK);
|
||||
} catch (RuntimeException | LinkageError error) {
|
||||
IrisLogging.reportError("Failed to enumerate blocks from " + provider.getPluginId() + ".", error);
|
||||
return List.of();
|
||||
}
|
||||
}
|
||||
|
||||
public Collection<Pair<Identifier, List<BlockProperty>>> getAllBlockProperties() {
|
||||
return activeProviders.stream()
|
||||
.flatMap(p -> p.getTypes(DataType.BLOCK)
|
||||
.stream()
|
||||
.map(id -> new Pair<>(id, p.getBlockProperties(id))))
|
||||
.toList();
|
||||
List<Pair<Identifier, List<BlockProperty>>> properties = new ArrayList<>();
|
||||
for (Identifier identifier : getAllIdentifiers(DataType.BLOCK)) {
|
||||
getBlockProperties(identifier).ifPresent(values -> properties.add(new Pair<>(identifier, values)));
|
||||
}
|
||||
return properties;
|
||||
}
|
||||
|
||||
public static Pair<Identifier, KMap<String, String>> parseState(Identifier key) {
|
||||
String raw = key.key();
|
||||
int open = raw.indexOf('[');
|
||||
int close = raw.lastIndexOf(']');
|
||||
if (open < 0 || close < open) {
|
||||
if (open < 0 && close < 0) {
|
||||
return new Pair<>(key, new KMap<>());
|
||||
}
|
||||
if (open <= 0 || close != raw.length() - 1 || close < open
|
||||
|| raw.indexOf('[', open + 1) >= 0 || raw.indexOf(']') != close) {
|
||||
throw new IllegalArgumentException("Malformed block state \"" + key + "\" (expected block[key=value])");
|
||||
}
|
||||
|
||||
String state = raw.substring(open + 1, close);
|
||||
KMap<String, String> stateMap = new KMap<>();
|
||||
if (!state.isEmpty()) {
|
||||
for (String entry : state.split(",")) {
|
||||
for (String entry : state.split(",", -1)) {
|
||||
String[] pair = entry.split("=", 2);
|
||||
if (pair.length != 2 || pair[0].isBlank() || pair[1].isBlank()) {
|
||||
throw new IllegalArgumentException("Malformed block state \"" + entry + "\" in \"" + key + "\" (expected key=value)");
|
||||
}
|
||||
stateMap.put(pair[0], pair[1]);
|
||||
if (stateMap.put(pair[0].trim(), pair[1].trim()) != null) {
|
||||
throw new IllegalArgumentException("Duplicate block property \"" + pair[0] + "\" in \"" + key + "\"");
|
||||
}
|
||||
}
|
||||
}
|
||||
return new Pair<>(new Identifier(key.namespace(), raw.substring(0, open)), stateMap);
|
||||
@@ -237,6 +337,45 @@ public class ExternalDataSVC implements IrisService {
|
||||
.findFirst();
|
||||
}
|
||||
|
||||
private BlockProvider findBlockProvider(Identifier identifier) {
|
||||
int separator = identifier.key().indexOf('/');
|
||||
if (separator > 0) {
|
||||
for (ExternalDataProvider provider : activeProviders) {
|
||||
if (!provider.getPluginId().equalsIgnoreCase(identifier.namespace())) {
|
||||
continue;
|
||||
}
|
||||
Identifier nativeId = new Identifier(identifier.key().substring(0, separator), identifier.key().substring(separator + 1));
|
||||
if (provider.isValidProvider(nativeId, DataType.BLOCK)) {
|
||||
return new BlockProvider(provider, nativeId);
|
||||
}
|
||||
return provider.isValidProvider(identifier, DataType.BLOCK) ? new BlockProvider(provider, identifier) : null;
|
||||
}
|
||||
if (BUILT_IN_PROVIDERS.stream().anyMatch(definition -> definition.pluginId().equalsIgnoreCase(identifier.namespace()))
|
||||
|| findProvider(identifier.namespace()).isPresent()) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
BlockProvider match = null;
|
||||
for (ExternalDataProvider provider : activeProviders) {
|
||||
if (!provider.isValidProvider(identifier, DataType.BLOCK)) {
|
||||
continue;
|
||||
}
|
||||
if (match != null) {
|
||||
IrisLogging.warnOnce("external-provider:ambiguous-block:" + identifier,
|
||||
"Multiple providers own block %s. Use %s or %s to select one.", identifier,
|
||||
qualifyBlockId(match.provider(), identifier), qualifyBlockId(provider, identifier));
|
||||
return null;
|
||||
}
|
||||
match = new BlockProvider(provider, identifier);
|
||||
}
|
||||
return match;
|
||||
}
|
||||
|
||||
private static Identifier qualifyBlockId(ExternalDataProvider provider, Identifier identifier) {
|
||||
return new Identifier(provider.getPluginId().toLowerCase(Locale.ROOT), identifier.namespace() + "/" + identifier.key());
|
||||
}
|
||||
|
||||
private void activateConfiguredProvider(ProviderDefinition definition) {
|
||||
if (!isPluginEnabled(definition.pluginId()) || findProvider(definition.pluginId()).isPresent()) {
|
||||
return;
|
||||
@@ -267,6 +406,7 @@ public class ExternalDataSVC implements IrisService {
|
||||
}
|
||||
activeProviders.add(provider);
|
||||
IrisLogging.debug("Enabled ExternalDataProvider for %s.", provider.getPluginId());
|
||||
notifyContentChanged();
|
||||
} catch (Throwable error) {
|
||||
IrisLogging.reportError("Failed to enable Iris external data provider " + provider.getPluginId() + ".", error);
|
||||
}
|
||||
@@ -279,4 +419,7 @@ public class ExternalDataSVC implements IrisService {
|
||||
|
||||
private record ProviderDefinition(String pluginId, String className) {
|
||||
}
|
||||
|
||||
private record BlockProvider(ExternalDataProvider provider, Identifier identifier) {
|
||||
}
|
||||
}
|
||||
|
||||
@@ -56,6 +56,7 @@ final class EngineShutdownSequence {
|
||||
private final Set<EngineTarget> incompleteTargets = Collections.synchronizedSet(
|
||||
Collections.newSetFromMap(new IdentityHashMap<>()));
|
||||
private boolean runtimeReleased;
|
||||
private boolean worldManagerStopped;
|
||||
private boolean targetReleased;
|
||||
private boolean engineDataReleased;
|
||||
private boolean preservationReleased;
|
||||
@@ -72,7 +73,10 @@ final class EngineShutdownSequence {
|
||||
if (!engine.beginShutdown()) {
|
||||
return;
|
||||
}
|
||||
Throwable routerFailure = runCleanup(null, engine::closeAttachedGenerationHistoryRuntimeRouter);
|
||||
Throwable routerFailure = stopWorldManager();
|
||||
if (routerFailure == null) {
|
||||
routerFailure = runCleanup(null, engine::closeAttachedGenerationHistoryRuntimeRouter);
|
||||
}
|
||||
if (routerFailure != null) {
|
||||
synchronized (engine.lifecycleLock) {
|
||||
engine.lifecycleState = LifecycleState.FAILED;
|
||||
@@ -111,20 +115,16 @@ final class EngineShutdownSequence {
|
||||
drainFailure = e;
|
||||
}
|
||||
if (drainFailure != null) {
|
||||
// A drain timeout must not abandon teardown: the world manager is the lease
|
||||
// producer, so stop it first, then re-drain briefly.
|
||||
// A drain timeout must not abandon teardown: give remaining admitted work
|
||||
// one final bounded drain before deciding whether resources can be released.
|
||||
IrisLogging.warn("Iris generation did not drain for close on " + engine.getWorld().name()
|
||||
+ "; stopping the world manager and retrying.");
|
||||
Throwable managerFailure = engine.runtime == null
|
||||
? null
|
||||
: runCleanup(null, engine.runtime.worldManager()::close);
|
||||
+ "; waiting briefly for remaining work.");
|
||||
try {
|
||||
engine.getGenerationSessions().sealAndAwait("close-retry", CLOSE_RETRY_DRAIN_TIMEOUT_MILLIS, true);
|
||||
drainFailure = null;
|
||||
} catch (GenerationSessionException e) {
|
||||
drainFailure = appendFailure(drainFailure, e);
|
||||
}
|
||||
drainFailure = appendFailure(drainFailure, managerFailure);
|
||||
}
|
||||
if (drainFailure != null) {
|
||||
// A live lease may be mid-write, so the mantle must not be closed at it — but
|
||||
@@ -322,7 +322,9 @@ final class EngineShutdownSequence {
|
||||
if (engineRuntime == null) {
|
||||
return failure;
|
||||
}
|
||||
Throwable serviceFailure = runCleanup(null, engineRuntime.worldManager()::close);
|
||||
Throwable serviceFailure = engineRuntime == engine.runtime
|
||||
? stopWorldManager()
|
||||
: runCleanup(null, engineRuntime.worldManager()::close);
|
||||
serviceFailure = runCleanup(serviceFailure, engineRuntime.effects()::close);
|
||||
if (serviceFailure != null) {
|
||||
return appendFailure(failure, serviceFailure);
|
||||
@@ -414,6 +416,17 @@ final class EngineShutdownSequence {
|
||||
return failure;
|
||||
}
|
||||
|
||||
private Throwable stopWorldManager() {
|
||||
if (worldManagerStopped || engine.runtime == null) {
|
||||
return null;
|
||||
}
|
||||
Throwable failure = runCleanup(null, engine.runtime.worldManager()::close);
|
||||
if (failure == null) {
|
||||
worldManagerStopped = true;
|
||||
}
|
||||
return failure;
|
||||
}
|
||||
|
||||
private Throwable closeAssemblyResources(RuntimeAssembly assembly) {
|
||||
Throwable failure = runCleanup(null, () -> {
|
||||
if (assembly.worldManager != null) {
|
||||
|
||||
@@ -275,7 +275,7 @@ public class IrisEngineMantle implements EngineMantle {
|
||||
return true;
|
||||
}
|
||||
|
||||
private static Mantle<Matter> createMantle(
|
||||
public static Mantle<Matter> createMantle(
|
||||
Engine engine,
|
||||
Path storageDirectory,
|
||||
Supplier<IrisData> dataSupplier
|
||||
|
||||
@@ -213,6 +213,9 @@ public class IrisWorldManager extends EngineAssignedWorldManager {
|
||||
return;
|
||||
}
|
||||
|
||||
if (generated) {
|
||||
chunkMaintenance.invalidateMaterialization(e.getX(), e.getZ());
|
||||
}
|
||||
INMS.get().reconcileNativeStructurePois(e);
|
||||
|
||||
if (cleanupServiceStopped || cleanupService == null || cleanupService.isShutdown()) {
|
||||
@@ -274,10 +277,15 @@ public class IrisWorldManager extends EngineAssignedWorldManager {
|
||||
public synchronized void close() {
|
||||
Throwable failure = null;
|
||||
try {
|
||||
super.close();
|
||||
chunkMaintenance.close();
|
||||
} catch (Throwable e) {
|
||||
failure = e;
|
||||
}
|
||||
try {
|
||||
super.close();
|
||||
} catch (Throwable e) {
|
||||
failure = appendCloseFailure(failure, e);
|
||||
}
|
||||
if (!looperStopped) {
|
||||
try {
|
||||
if (looper != null) {
|
||||
|
||||
@@ -20,6 +20,7 @@ package art.arcane.iris.engine;
|
||||
|
||||
import art.arcane.iris.core.loader.IrisData;
|
||||
import art.arcane.iris.core.service.tree.BlockDropRouter;
|
||||
import art.arcane.iris.engine.history.SavedBiomeUnavailableException;
|
||||
import art.arcane.iris.engine.object.IrisBiome;
|
||||
import art.arcane.iris.engine.object.IrisBlockDrops;
|
||||
import art.arcane.iris.engine.object.IrisMarker;
|
||||
@@ -52,21 +53,26 @@ final class WorldBlockDropRouter {
|
||||
}
|
||||
|
||||
void onBlockBreak(BlockBreakEvent e) {
|
||||
if (e.isCancelled()) {
|
||||
return;
|
||||
}
|
||||
if (e.getBlock().getWorld().equals(BukkitWorldBinding.world(manager.getTarget().getWorld()))) {
|
||||
int blockX = e.getBlock().getX();
|
||||
int mantleY = toMantleY(e.getBlock().getY(), manager.getEngine().getWorld().minHeight());
|
||||
int blockZ = e.getBlock().getZ();
|
||||
|
||||
KList<ItemStack> d = new KList<>();
|
||||
IrisBiome b = EngineBukkitOps.getBiome(manager.getEngine(), e.getBlock().getLocation());
|
||||
List<IrisBlockDrops> dropProviders = filterDrops(b.getBlockDrops(), e, manager.getData());
|
||||
|
||||
if (dropProviders.stream().noneMatch(IrisBlockDrops::isSkipParents)) {
|
||||
IrisRegion r = EngineBukkitOps.getRegion(manager.getEngine(), e.getBlock().getLocation());
|
||||
dropProviders.addAll(filterDrops(r.getBlockDrops(), e, manager.getData()));
|
||||
dropProviders.addAll(filterDrops(manager.getEngine().getDimension().getBlockDrops(), e, manager.getData()));
|
||||
List<IrisBlockDrops> dropProviders;
|
||||
try {
|
||||
dropProviders = resolveDropProviders(e);
|
||||
} catch (SavedBiomeUnavailableException unavailable) {
|
||||
e.setCancelled(true);
|
||||
if (!unavailable.isLoading() || unavailable.getSuppressed().length != 0) {
|
||||
throw unavailable;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
KList<ItemStack> d = new KList<>();
|
||||
dropProviders.forEach(provider -> provider.fillDrops(false, d));
|
||||
|
||||
if (dropProviders.stream().anyMatch(IrisBlockDrops::isReplaceVanillaDrops)) {
|
||||
@@ -131,4 +137,15 @@ final class WorldBlockDropRouter {
|
||||
private List<IrisBlockDrops> filterDrops(KList<IrisBlockDrops> drops, BlockBreakEvent e, IrisData data) {
|
||||
return new KList<>(drops.stream().filter(d -> d.shouldDropFor(e.getBlock().getBlockData(), data)).toList());
|
||||
}
|
||||
|
||||
private List<IrisBlockDrops> resolveDropProviders(BlockBreakEvent event) {
|
||||
IrisBiome biome = EngineBukkitOps.getBiome(manager.getEngine(), event.getBlock().getLocation());
|
||||
List<IrisBlockDrops> providers = filterDrops(biome.getBlockDrops(), event, manager.getData());
|
||||
if (providers.stream().noneMatch(IrisBlockDrops::isSkipParents)) {
|
||||
IrisRegion region = EngineBukkitOps.getRegion(manager.getEngine(), event.getBlock().getLocation());
|
||||
providers.addAll(filterDrops(region.getBlockDrops(), event, manager.getData()));
|
||||
providers.addAll(filterDrops(manager.getEngine().getDimension().getBlockDrops(), event, manager.getData()));
|
||||
}
|
||||
return providers;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,6 +20,8 @@ package art.arcane.iris.engine;
|
||||
|
||||
import art.arcane.iris.core.IrisSettings;
|
||||
import art.arcane.iris.engine.data.cache.Cache;
|
||||
import art.arcane.iris.engine.history.GenerationHistoryRuntimeRouter;
|
||||
import art.arcane.iris.engine.history.GenerationHistoryRuntimeRouter.SavedChunkMantle;
|
||||
import art.arcane.iris.engine.history.SavedBiomeUnavailableException;
|
||||
import art.arcane.iris.engine.platform.EngineBukkitOps;
|
||||
import art.arcane.iris.platform.bukkit.BukkitWorldBinding;
|
||||
@@ -28,6 +30,7 @@ import art.arcane.iris.util.common.plugin.Chunks;
|
||||
import art.arcane.iris.util.common.scheduling.J;
|
||||
import art.arcane.volmlib.util.mantle.flag.MantleFlag;
|
||||
import art.arcane.volmlib.util.mantle.runtime.Mantle;
|
||||
import art.arcane.volmlib.util.mantle.runtime.MantleChunk;
|
||||
import art.arcane.volmlib.util.math.PowerOfTwoCoordinates;
|
||||
import art.arcane.volmlib.util.math.Position2;
|
||||
import art.arcane.volmlib.util.matter.Matter;
|
||||
@@ -36,10 +39,14 @@ import org.bukkit.World;
|
||||
import org.bukkit.entity.Player;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.ForkJoinPool;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
/**
|
||||
@@ -50,21 +57,47 @@ import java.util.concurrent.atomic.AtomicBoolean;
|
||||
*/
|
||||
final class WorldChunkMaintenance {
|
||||
private static final int MAX_FORCED_CHUNK_UPDATES = 128;
|
||||
private static final int MAX_PENDING_MATERIALIZATIONS = 128;
|
||||
private static final int MAX_COMPLETED_MATERIALIZATIONS = 8192;
|
||||
|
||||
private final IrisWorldManager manager;
|
||||
private final Set<Long> mantleWarmupQueue = ConcurrentHashMap.newKeySet();
|
||||
private final Set<Long> markerFlagQueue = ConcurrentHashMap.newKeySet();
|
||||
private final Set<Long> discoveredFlagQueue = ConcurrentHashMap.newKeySet();
|
||||
private final Set<Long> chunkUpdateQueue = ConcurrentHashMap.newKeySet();
|
||||
private final Map<Long, RoutedMaterialization> materializations = new ConcurrentHashMap<>();
|
||||
private final Set<Long> completedMaterializations = Collections.synchronizedSet(new LinkedHashSet<>());
|
||||
private final AtomicBoolean chunkUpdateScanScheduled = new AtomicBoolean();
|
||||
private final AtomicBoolean chunkDiscoveryScanScheduled = new AtomicBoolean();
|
||||
private volatile Position2[] loadedChunkPositions = new Position2[0];
|
||||
private int forcedChunkUpdateCursor = 0;
|
||||
private volatile boolean closed;
|
||||
|
||||
WorldChunkMaintenance(IrisWorldManager manager) {
|
||||
this.manager = manager;
|
||||
}
|
||||
|
||||
void close() {
|
||||
synchronized (materializations) {
|
||||
closed = true;
|
||||
for (RoutedMaterialization materialization : materializations.values()) {
|
||||
materialization.cancel();
|
||||
}
|
||||
}
|
||||
completedMaterializations.clear();
|
||||
}
|
||||
|
||||
void invalidateMaterialization(int chunkX, int chunkZ) {
|
||||
long key = Cache.key(chunkX, chunkZ);
|
||||
synchronized (materializations) {
|
||||
RoutedMaterialization materialization = materializations.get(key);
|
||||
if (materialization != null) {
|
||||
materialization.cancel();
|
||||
}
|
||||
completedMaterializations.remove(key);
|
||||
}
|
||||
}
|
||||
|
||||
void discoverChunks() {
|
||||
World world = BukkitWorldBinding.world(manager.getEngine().getWorld());
|
||||
if (world == null) {
|
||||
@@ -264,32 +297,84 @@ final class WorldChunkMaintenance {
|
||||
}
|
||||
|
||||
void updateChunkRegion(World world, int chunkX, int chunkZ) {
|
||||
if (world == null || !world.isChunkLoaded(chunkX, chunkZ) || !Chunks.isSafe(world, chunkX, chunkZ)) {
|
||||
if (closed || world == null || !world.isChunkLoaded(chunkX, chunkZ) || !Chunks.isSafe(world, chunkX, chunkZ)) {
|
||||
return;
|
||||
}
|
||||
|
||||
Chunk chunk = world.getChunkAt(chunkX, chunkZ);
|
||||
|
||||
if (IrisSettings.get().getWorld().isPostLoadBlockUpdates()) {
|
||||
if (!manager.getMantle().isChunkLoaded(chunkX, chunkZ)) {
|
||||
warmupMantleChunkAsync(chunkX, chunkZ);
|
||||
GenerationHistoryRuntimeRouter router = manager.getEngine() instanceof IrisEngine engine
|
||||
? engine.getGenerationHistoryRuntimeRouter().orElse(null) : null;
|
||||
if (router != null && !completedMaterializations.contains(Cache.key(chunkX, chunkZ))) {
|
||||
scheduleMaterialization(new MaterializationTarget(world, chunkX, chunkZ), router);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
EngineBukkitOps.updateChunk(manager.getEngine(), chunk);
|
||||
} catch (SavedBiomeUnavailableException unavailable) {
|
||||
if (!unavailable.isLoading() || unavailable.getSuppressed().length != 0) {
|
||||
throw unavailable;
|
||||
}
|
||||
if (router == null && !updateCurrentChunk(chunk, chunkX, chunkZ)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
spawnInitially(chunk);
|
||||
}
|
||||
|
||||
private boolean updateCurrentChunk(Chunk chunk, int chunkX, int chunkZ) {
|
||||
if (!manager.getMantle().isChunkLoaded(chunkX, chunkZ)) {
|
||||
warmupMantleChunkAsync(chunkX, chunkZ);
|
||||
return false;
|
||||
}
|
||||
return materializeChunk(chunk, manager.getMantle());
|
||||
}
|
||||
|
||||
private boolean materializeChunk(Chunk chunk, Mantle<Matter> mantle) {
|
||||
try {
|
||||
EngineBukkitOps.updateChunk(manager.getEngine(), chunk, mantle);
|
||||
} catch (SavedBiomeUnavailableException unavailable) {
|
||||
if (!unavailable.isLoading() || unavailable.getSuppressed().length != 0) {
|
||||
throw unavailable;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private void spawnInitially(Chunk chunk) {
|
||||
if (manager.entitySpawner.isEntitySpawningEnabledForCurrentWorld()) {
|
||||
manager.entitySpawner.spawnInitially(chunk);
|
||||
}
|
||||
}
|
||||
|
||||
private void scheduleMaterialization(MaterializationTarget target, GenerationHistoryRuntimeRouter router) {
|
||||
RoutedMaterialization materialization;
|
||||
synchronized (materializations) {
|
||||
if (closed || materializations.size() >= MAX_PENDING_MATERIALIZATIONS
|
||||
|| materializations.containsKey(target.key())) {
|
||||
return;
|
||||
}
|
||||
materialization = new RoutedMaterialization(target, router);
|
||||
materializations.put(target.key(), materialization);
|
||||
}
|
||||
try {
|
||||
J.a(manager.managedTask("bukkit_world_manager_materialization_load",
|
||||
materialization::prepare, materialization::cancel));
|
||||
} catch (Throwable failure) {
|
||||
materialization.cancel();
|
||||
IrisLogging.reportError(failure);
|
||||
}
|
||||
}
|
||||
|
||||
private void recordMaterialized(long key) {
|
||||
synchronized (completedMaterializations) {
|
||||
if (closed) {
|
||||
return;
|
||||
}
|
||||
if (completedMaterializations.size() >= MAX_COMPLETED_MATERIALIZATIONS) {
|
||||
completedMaterializations.remove(completedMaterializations.iterator().next());
|
||||
}
|
||||
completedMaterializations.add(key);
|
||||
}
|
||||
}
|
||||
|
||||
void raiseInitialSpawnMarkerFlag(World world, int chunkX, int chunkZ, Runnable onFirstRaise) {
|
||||
if (world == null || onFirstRaise == null) {
|
||||
return;
|
||||
@@ -355,4 +440,156 @@ final class WorldChunkMaintenance {
|
||||
}
|
||||
return loadedChunkPositions;
|
||||
}
|
||||
|
||||
private record MaterializationTarget(World world, int x, int z) {
|
||||
private long key() {
|
||||
return Cache.key(x, z);
|
||||
}
|
||||
}
|
||||
|
||||
private final class RoutedMaterialization {
|
||||
private final MaterializationTarget target;
|
||||
private final GenerationHistoryRuntimeRouter router;
|
||||
private SavedChunkMantle savedMantle;
|
||||
private MantleChunk<Matter> mantleChunk;
|
||||
private boolean cancelled;
|
||||
private boolean preparing;
|
||||
private boolean applying;
|
||||
private boolean releasing;
|
||||
|
||||
private RoutedMaterialization(MaterializationTarget target, GenerationHistoryRuntimeRouter router) {
|
||||
this.target = target;
|
||||
this.router = router;
|
||||
}
|
||||
|
||||
private void prepare() {
|
||||
SavedChunkMantle opened = null;
|
||||
MantleChunk<Matter> loaded = null;
|
||||
try {
|
||||
synchronized (this) {
|
||||
if (cancelled) {
|
||||
return;
|
||||
}
|
||||
preparing = true;
|
||||
}
|
||||
opened = router.openSavedChunkMantle(target.x(), target.z());
|
||||
loaded = opened.mantle().getChunk(target.x(), target.z()).use();
|
||||
opened.detachThread();
|
||||
synchronized (this) {
|
||||
if (cancelled) {
|
||||
return;
|
||||
}
|
||||
savedMantle = opened;
|
||||
mantleChunk = loaded;
|
||||
opened = null;
|
||||
loaded = null;
|
||||
}
|
||||
boolean scheduled = J.runRegion(target.world(), target.x(), target.z(),
|
||||
manager.managedTask("bukkit_world_manager_materialization_apply", this::apply, this::cancel));
|
||||
if (!scheduled) {
|
||||
cancel();
|
||||
}
|
||||
} catch (Throwable failure) {
|
||||
cancel();
|
||||
IrisLogging.reportError("Failed to load saved chunk materialization at " + target.x() + "," + target.z() + ".", failure);
|
||||
} finally {
|
||||
try {
|
||||
try {
|
||||
if (loaded != null) {
|
||||
loaded.release();
|
||||
}
|
||||
} finally {
|
||||
if (opened != null) {
|
||||
opened.close();
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
boolean cleanup;
|
||||
synchronized (this) {
|
||||
preparing = false;
|
||||
cleanup = cancelled;
|
||||
}
|
||||
if (cleanup) {
|
||||
cancel();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void apply() {
|
||||
SavedChunkMantle active;
|
||||
synchronized (this) {
|
||||
if (cancelled || savedMantle == null) {
|
||||
return;
|
||||
}
|
||||
applying = true;
|
||||
active = savedMantle;
|
||||
}
|
||||
try {
|
||||
World world = target.world();
|
||||
if (closed || !world.isChunkLoaded(target.x(), target.z())
|
||||
|| !Chunks.isSafe(world, target.x(), target.z())) {
|
||||
return;
|
||||
}
|
||||
Chunk chunk = world.getChunkAt(target.x(), target.z());
|
||||
boolean completed;
|
||||
try (SavedChunkMantle.Scope ignored = active.openScope()) {
|
||||
Mantle<Matter> mantle = active.mantle();
|
||||
if (!materializeChunk(chunk, mantle)) {
|
||||
return;
|
||||
}
|
||||
completed = mantle.hasFlag(target.x(), target.z(), MantleFlag.ETCHED);
|
||||
}
|
||||
synchronized (this) {
|
||||
if (completed && !cancelled) {
|
||||
recordMaterialized(target.key());
|
||||
}
|
||||
}
|
||||
spawnInitially(chunk);
|
||||
} catch (Throwable failure) {
|
||||
IrisLogging.reportError("Failed to materialize saved chunk at " + target.x() + "," + target.z() + ".", failure);
|
||||
} finally {
|
||||
synchronized (this) {
|
||||
applying = false;
|
||||
}
|
||||
cancel();
|
||||
}
|
||||
}
|
||||
|
||||
private void cancel() {
|
||||
SavedChunkMantle retained;
|
||||
MantleChunk<Matter> loaded;
|
||||
synchronized (this) {
|
||||
cancelled = true;
|
||||
if (preparing || applying || releasing) {
|
||||
return;
|
||||
}
|
||||
retained = savedMantle;
|
||||
loaded = mantleChunk;
|
||||
savedMantle = null;
|
||||
mantleChunk = null;
|
||||
releasing = retained != null;
|
||||
}
|
||||
if (retained != null) {
|
||||
ForkJoinPool.commonPool().execute(() -> release(retained, loaded));
|
||||
} else {
|
||||
materializations.remove(target.key(), this);
|
||||
}
|
||||
}
|
||||
|
||||
private void release(SavedChunkMantle retained, MantleChunk<Matter> loaded) {
|
||||
try {
|
||||
try {
|
||||
loaded.release();
|
||||
} finally {
|
||||
retained.close();
|
||||
}
|
||||
} catch (Throwable failure) {
|
||||
completedMaterializations.remove(target.key());
|
||||
IrisLogging.reportError("Failed to release saved chunk materialization at " + target.x() + "," + target.z() + ".", failure);
|
||||
} finally {
|
||||
materializations.remove(target.key(), this);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+2
-1
@@ -138,6 +138,7 @@ public final class IrisTransitionGeometryActuator extends EngineAssignedComponen
|
||||
}
|
||||
|
||||
private static BoundaryColumnGeometry.Voxel encode(PlatformBlockState state) {
|
||||
String stateKey = state.key();
|
||||
boolean custom = state.isCustom();
|
||||
PlatformBlockState nativeState = state.placementBaseState();
|
||||
if (nativeState != null) {
|
||||
@@ -149,7 +150,7 @@ public final class IrisTransitionGeometryActuator extends EngineAssignedComponen
|
||||
: state.isWaterLogged() ? "minecraft:water[level=0]" : "";
|
||||
boolean protectedContent = custom || state.isDecorant() || state.isFoliage() || state.isTreeBlock()
|
||||
|| state.hasTileEntity();
|
||||
return new BoundaryColumnGeometry.Voxel(state.key(), phase, fluidKey, protectedContent);
|
||||
return new BoundaryColumnGeometry.Voxel(stateKey, phase, fluidKey, protectedContent);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -574,11 +574,19 @@ public final class GenerationHistory {
|
||||
}
|
||||
|
||||
public GenerationStage openStage(int chunkX, int chunkZ) throws IOException {
|
||||
return openStage(chunkX, chunkZ, false);
|
||||
}
|
||||
|
||||
GenerationStage openSavedChunkStage(int chunkX, int chunkZ) throws IOException {
|
||||
return openStage(chunkX, chunkZ, true);
|
||||
}
|
||||
|
||||
private GenerationStage openStage(int chunkX, int chunkZ, boolean savedChunk) throws IOException {
|
||||
GenerationAdmission.StageLease lease = admission.enterStage();
|
||||
try {
|
||||
GenerationManifest manifest = store.manifest();
|
||||
GenerationActivation activation = manifest.activeActivation();
|
||||
GenerationEpoch epoch = manifest.activeEpoch();
|
||||
GenerationActivation activation = savedChunk ? resolveActivation(chunkX, chunkZ) : manifest.activeActivation();
|
||||
GenerationEpoch epoch = savedChunk ? requireEpoch(activation.epochId()) : manifest.activeEpoch();
|
||||
return new GenerationStage(
|
||||
this,
|
||||
chunkX,
|
||||
|
||||
+334
@@ -2,9 +2,12 @@ package art.arcane.iris.engine.history;
|
||||
|
||||
import art.arcane.iris.core.loader.IrisData;
|
||||
import art.arcane.iris.engine.IrisEngine;
|
||||
import art.arcane.iris.engine.IrisEngineMantle;
|
||||
import art.arcane.iris.engine.framework.EngineTarget;
|
||||
import art.arcane.iris.engine.framework.GenerationTransitionGate;
|
||||
import art.arcane.iris.engine.object.IrisDimension;
|
||||
import art.arcane.volmlib.util.mantle.runtime.Mantle;
|
||||
import art.arcane.volmlib.util.matter.Matter;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Path;
|
||||
@@ -28,6 +31,7 @@ public final class GenerationHistoryRuntimeRouter implements AutoCloseable {
|
||||
private final ActivationRuntimeFactory runtimeFactory;
|
||||
private final LinkedHashMap<Long, RuntimeCacheEntry> bindings;
|
||||
private final Map<Long, RuntimeRetirement> retiringBindings;
|
||||
private final Map<Long, SavedMantleEntry> savedMantles = new LinkedHashMap<>();
|
||||
private final ReentrantLock stateLock;
|
||||
private final Condition inactive;
|
||||
private final ThreadLocal<Integer> operationDepth;
|
||||
@@ -265,6 +269,29 @@ public final class GenerationHistoryRuntimeRouter implements AutoCloseable {
|
||||
}
|
||||
}
|
||||
|
||||
public SavedChunkMantle openSavedChunkMantle(int chunkX, int chunkZ) throws IOException {
|
||||
GenerationTransitionGate.Participation participation = engine.getGenerationSessions().transitionGate().enter();
|
||||
boolean operation = false;
|
||||
try {
|
||||
enterRouteOperation();
|
||||
operation = true;
|
||||
GenerationHistory.GenerationStage stage = history.openSavedChunkStage(chunkX, chunkZ);
|
||||
try {
|
||||
SavedMantleAccess access = acquireSavedMantle(stage.activation(), stage.epoch());
|
||||
return new SavedChunkMantle(this, new SavedChunkResources(stage, participation, access));
|
||||
} catch (Throwable failure) {
|
||||
stage.close();
|
||||
throw failure;
|
||||
}
|
||||
} catch (Throwable failure) {
|
||||
if (operation) {
|
||||
leaveRouteOperation();
|
||||
}
|
||||
participation.close();
|
||||
throw propagate(failure, "Unable to open saved Iris chunk mantle.");
|
||||
}
|
||||
}
|
||||
|
||||
public RuntimeStage openStage(int chunkX, int chunkZ) throws IOException {
|
||||
RuntimeRoute route = openRoute(chunkX, chunkZ);
|
||||
try {
|
||||
@@ -369,6 +396,7 @@ public final class GenerationHistoryRuntimeRouter implements AutoCloseable {
|
||||
throw new IllegalStateException("Cannot close the generation-history router from an active route.");
|
||||
}
|
||||
List<RuntimeRetirement> retired = null;
|
||||
List<SavedMantleEntry> saved = null;
|
||||
stateLock.lock();
|
||||
try {
|
||||
closed = true;
|
||||
@@ -389,6 +417,7 @@ public final class GenerationHistoryRuntimeRouter implements AutoCloseable {
|
||||
}
|
||||
}
|
||||
bindings.clear();
|
||||
saved = new ArrayList<>(savedMantles.values());
|
||||
break;
|
||||
}
|
||||
} finally {
|
||||
@@ -411,6 +440,13 @@ public final class GenerationHistoryRuntimeRouter implements AutoCloseable {
|
||||
} catch (Throwable retirementFailure) {
|
||||
failure = appendFailure(failure, retirementFailure);
|
||||
}
|
||||
for (SavedMantleEntry entry : saved) {
|
||||
try {
|
||||
closeSavedMantle(entry);
|
||||
} catch (Throwable closeFailure) {
|
||||
failure = appendFailure(failure, closeFailure);
|
||||
}
|
||||
}
|
||||
|
||||
stateLock.lock();
|
||||
try {
|
||||
@@ -431,6 +467,164 @@ public final class GenerationHistoryRuntimeRouter implements AutoCloseable {
|
||||
return signatureSampler.open(engine);
|
||||
}
|
||||
|
||||
private SavedMantleAccess acquireSavedMantle(GenerationActivation activation, GenerationEpoch epoch) throws IOException {
|
||||
SavedMantleEntry saved = null;
|
||||
RuntimeCacheEntry runtime;
|
||||
boolean loader = false;
|
||||
stateLock.lock();
|
||||
try {
|
||||
while (true) {
|
||||
awaitRetirementLocked(activation.activationId());
|
||||
saved = savedMantles.get(activation.activationId());
|
||||
if (saved == null || !saved.retiring) {
|
||||
break;
|
||||
}
|
||||
awaitSavedMantleChange(saved);
|
||||
}
|
||||
runtime = bindings.get(activation.activationId());
|
||||
if (runtime != null) {
|
||||
runtime.leases++;
|
||||
} else {
|
||||
if (saved == null) {
|
||||
saved = new SavedMantleEntry(activation.activationId());
|
||||
saved.loading = true;
|
||||
savedMantles.put(activation.activationId(), saved);
|
||||
activeLoads++;
|
||||
loader = true;
|
||||
}
|
||||
saved.leases++;
|
||||
}
|
||||
} finally {
|
||||
stateLock.unlock();
|
||||
}
|
||||
if (runtime != null) {
|
||||
RuntimeLease lease = awaitRuntime(runtime, activation.activationId());
|
||||
try (IrisEngine.GenerationRuntimeScope ignored = engine.openGenerationRuntimeScope(lease.binding())) {
|
||||
return new SavedMantleAccess(engine.getMantle().getMantle(), lease::close);
|
||||
} catch (Throwable failure) {
|
||||
lease.close();
|
||||
throw failure;
|
||||
}
|
||||
}
|
||||
SavedMantleEntry entry = saved;
|
||||
if (loader) {
|
||||
loadSavedMantle(entry, activation, epoch);
|
||||
}
|
||||
stateLock.lock();
|
||||
try {
|
||||
while (entry.loading) {
|
||||
try {
|
||||
inactive.await();
|
||||
} catch (InterruptedException failure) {
|
||||
Thread.currentThread().interrupt();
|
||||
entry.leases--;
|
||||
inactive.signalAll();
|
||||
throw new IOException("Interrupted while loading saved mantle for activation " + entry.activationId + ".", failure);
|
||||
}
|
||||
}
|
||||
if (entry.failure != null) {
|
||||
entry.leases--;
|
||||
throw propagate(entry.failure, "Unable to load saved mantle for activation " + entry.activationId + ".");
|
||||
}
|
||||
return new SavedMantleAccess(entry.mantle, () -> releaseSavedMantle(entry));
|
||||
} finally {
|
||||
stateLock.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
private void loadSavedMantle(SavedMantleEntry entry, GenerationActivation activation, GenerationEpoch epoch) {
|
||||
IrisData data = null;
|
||||
Mantle<Matter> mantle = null;
|
||||
Throwable failure = null;
|
||||
try {
|
||||
data = IrisData.openRuntime(history.packRoot(activation.activationId()).toFile());
|
||||
data.bindGenerationRegistryContract(epoch.registryContract());
|
||||
data.registerEngine(engine);
|
||||
IrisData savedData = data;
|
||||
mantle = IrisEngineMantle.createMantle(engine, history.paths().activationMantleRoot(activation.activationId()), () -> savedData);
|
||||
} catch (Throwable loadFailure) {
|
||||
failure = loadFailure;
|
||||
if (data != null) {
|
||||
try {
|
||||
data.unregisterEngine(engine);
|
||||
data.close();
|
||||
} catch (Throwable closeFailure) {
|
||||
failure = appendFailure(failure, closeFailure);
|
||||
}
|
||||
}
|
||||
}
|
||||
stateLock.lock();
|
||||
try {
|
||||
entry.data = data;
|
||||
entry.mantle = mantle;
|
||||
entry.failure = failure;
|
||||
entry.loading = false;
|
||||
activeLoads--;
|
||||
if (failure != null) {
|
||||
savedMantles.remove(entry.activationId, entry);
|
||||
}
|
||||
inactive.signalAll();
|
||||
} finally {
|
||||
stateLock.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
private void releaseSavedMantle(SavedMantleEntry entry) {
|
||||
stateLock.lock();
|
||||
try {
|
||||
if (entry.leases <= 0) {
|
||||
throw new IllegalStateException("Saved mantle has no active lease.");
|
||||
}
|
||||
entry.leases--;
|
||||
if (entry.leases > 0) {
|
||||
return;
|
||||
}
|
||||
entry.retiring = true;
|
||||
} finally {
|
||||
stateLock.unlock();
|
||||
}
|
||||
closeSavedMantle(entry);
|
||||
}
|
||||
|
||||
private void closeSavedMantle(SavedMantleEntry entry) {
|
||||
Throwable failure = null;
|
||||
try {
|
||||
entry.mantle.close();
|
||||
entry.data.unregisterEngine(engine);
|
||||
entry.data.close();
|
||||
} catch (Throwable closeFailure) {
|
||||
failure = closeFailure;
|
||||
}
|
||||
stateLock.lock();
|
||||
try {
|
||||
entry.failure = failure;
|
||||
if (failure == null) {
|
||||
savedMantles.remove(entry.activationId, entry);
|
||||
}
|
||||
inactive.signalAll();
|
||||
} finally {
|
||||
stateLock.unlock();
|
||||
}
|
||||
if (failure != null) {
|
||||
throw new IllegalStateException("Failed to close saved mantle for activation " + entry.activationId + ".", failure);
|
||||
}
|
||||
}
|
||||
|
||||
private void awaitSavedMantleChange(SavedMantleEntry entry) throws IOException {
|
||||
if (entry.failure != null) {
|
||||
throw propagate(entry.failure, "Saved mantle for activation " + entry.activationId + " is unavailable.");
|
||||
}
|
||||
if (closed) {
|
||||
throw new IllegalStateException("Generation-history runtime router is closed.");
|
||||
}
|
||||
try {
|
||||
inactive.await();
|
||||
} catch (InterruptedException failure) {
|
||||
Thread.currentThread().interrupt();
|
||||
throw new IOException("Interrupted while awaiting saved mantle for activation " + entry.activationId + ".", failure);
|
||||
}
|
||||
}
|
||||
|
||||
private RuntimeLease acquireRuntime(
|
||||
GenerationActivation activation,
|
||||
GenerationEpoch epoch
|
||||
@@ -439,7 +633,14 @@ public final class GenerationHistoryRuntimeRouter implements AutoCloseable {
|
||||
boolean loader = false;
|
||||
stateLock.lock();
|
||||
try {
|
||||
while (true) {
|
||||
awaitRetirementLocked(activation.activationId());
|
||||
SavedMantleEntry saved = savedMantles.get(activation.activationId());
|
||||
if (saved == null) {
|
||||
break;
|
||||
}
|
||||
awaitSavedMantleChange(saved);
|
||||
}
|
||||
entry = bindings.get(activation.activationId());
|
||||
if (entry == null) {
|
||||
entry = new RuntimeCacheEntry(activation.activationId());
|
||||
@@ -823,6 +1024,139 @@ public final class GenerationHistoryRuntimeRouter implements AutoCloseable {
|
||||
throw new IllegalStateException("Failed to close the generation-history runtime router.", failure);
|
||||
}
|
||||
|
||||
private static final class SavedMantleEntry {
|
||||
private final long activationId;
|
||||
private IrisData data;
|
||||
private Mantle<Matter> mantle;
|
||||
private Throwable failure;
|
||||
private int leases;
|
||||
private boolean loading;
|
||||
private boolean retiring;
|
||||
|
||||
private SavedMantleEntry(long activationId) {
|
||||
this.activationId = activationId;
|
||||
}
|
||||
}
|
||||
|
||||
private record SavedMantleAccess(Mantle<Matter> mantle, Runnable release) {
|
||||
}
|
||||
|
||||
private record SavedChunkResources(GenerationHistory.GenerationStage stage,
|
||||
GenerationTransitionGate.Participation participation,
|
||||
SavedMantleAccess access) {
|
||||
}
|
||||
|
||||
public static final class SavedChunkMantle implements AutoCloseable {
|
||||
private final GenerationHistoryRuntimeRouter router;
|
||||
private final SavedChunkResources resources;
|
||||
private int activeScopes;
|
||||
private boolean closed;
|
||||
|
||||
private SavedChunkMantle(GenerationHistoryRuntimeRouter router, SavedChunkResources resources) {
|
||||
this.router = router;
|
||||
this.resources = resources;
|
||||
}
|
||||
|
||||
public Mantle<Matter> mantle() {
|
||||
return resources.access().mantle();
|
||||
}
|
||||
|
||||
public void detachThread() {
|
||||
resources.participation().detachThread();
|
||||
}
|
||||
|
||||
public Scope openScope() {
|
||||
synchronized (this) {
|
||||
if (closed) {
|
||||
throw new IllegalStateException("Saved chunk mantle is closed.");
|
||||
}
|
||||
activeScopes++;
|
||||
}
|
||||
try {
|
||||
GenerationTransitionGate.Participation participation = resources.participation().attach();
|
||||
router.enterRuntimeScope();
|
||||
return new Scope(this, participation);
|
||||
} catch (Throwable failure) {
|
||||
releaseScope();
|
||||
throw failure;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized void close() {
|
||||
if (closed) {
|
||||
return;
|
||||
}
|
||||
if (activeScopes > 0) {
|
||||
throw new IllegalStateException("Saved chunk mantle still has active scopes.");
|
||||
}
|
||||
closed = true;
|
||||
Throwable failure = null;
|
||||
try {
|
||||
resources.access().release().run();
|
||||
} catch (Throwable closeFailure) {
|
||||
failure = closeFailure;
|
||||
}
|
||||
try {
|
||||
resources.stage().close();
|
||||
} catch (Throwable closeFailure) {
|
||||
failure = appendFailure(failure, closeFailure);
|
||||
}
|
||||
try {
|
||||
router.leaveRouteOperation();
|
||||
} catch (Throwable closeFailure) {
|
||||
failure = appendFailure(failure, closeFailure);
|
||||
}
|
||||
try {
|
||||
resources.participation().close();
|
||||
} catch (Throwable closeFailure) {
|
||||
failure = appendFailure(failure, closeFailure);
|
||||
}
|
||||
if (failure != null) {
|
||||
throw new IllegalStateException("Failed to close saved chunk mantle.", failure);
|
||||
}
|
||||
}
|
||||
|
||||
private synchronized void releaseScope() {
|
||||
if (activeScopes <= 0) {
|
||||
throw new IllegalStateException("Saved chunk mantle has no active scope.");
|
||||
}
|
||||
activeScopes--;
|
||||
}
|
||||
|
||||
public static final class Scope implements AutoCloseable {
|
||||
private final SavedChunkMantle handle;
|
||||
private final GenerationTransitionGate.Participation participation;
|
||||
private final Thread owner = Thread.currentThread();
|
||||
private boolean closed;
|
||||
|
||||
private Scope(SavedChunkMantle handle, GenerationTransitionGate.Participation participation) {
|
||||
this.handle = handle;
|
||||
this.participation = participation;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
if (closed) {
|
||||
return;
|
||||
}
|
||||
if (Thread.currentThread() != owner) {
|
||||
throw new IllegalStateException("Saved chunk mantle scope closed from a different thread.");
|
||||
}
|
||||
closed = true;
|
||||
try {
|
||||
handle.router.leaveRuntimeScope();
|
||||
} finally {
|
||||
try {
|
||||
handle.releaseScope();
|
||||
} finally {
|
||||
participation.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static final class RuntimeCacheEntry {
|
||||
private final long activationId;
|
||||
private IrisEngine.GenerationRuntimeBinding binding;
|
||||
|
||||
@@ -41,6 +41,7 @@ import lombok.EqualsAndHashCode;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.experimental.Accessors;
|
||||
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
|
||||
@Accessors(chain = true)
|
||||
@@ -76,12 +77,17 @@ public class IrisBlockData extends IrisRegistrant {
|
||||
|
||||
public static IrisBlockData from(String j) {
|
||||
IrisBlockData b = new IrisBlockData();
|
||||
String v = j.toLowerCase().trim();
|
||||
String v = j.trim();
|
||||
int bracket = v.indexOf('[');
|
||||
String block = (bracket < 0 ? v : v.substring(0, bracket)).toLowerCase(Locale.ROOT);
|
||||
b.setBlock(block);
|
||||
|
||||
if (v.contains("[")) {
|
||||
if (bracket >= 0) {
|
||||
KList<String> props = new KList<>();
|
||||
String rp = v.split("\\Q[\\E")[1].replaceAll("\\Q]\\E", "");
|
||||
b.setBlock(v.split("\\Q[\\E")[0]);
|
||||
String rp = v.substring(bracket + 1).replace("]", "");
|
||||
if (!block.contains(":") || block.startsWith("minecraft:")) {
|
||||
rp = rp.toLowerCase(Locale.ROOT);
|
||||
}
|
||||
|
||||
if (rp.contains(",")) {
|
||||
props.add(rp.split("\\Q,\\E"));
|
||||
@@ -93,8 +99,6 @@ public class IrisBlockData extends IrisRegistrant {
|
||||
Object kg = filter(i.split("\\Q=\\E")[1]);
|
||||
b.data.put(i.split("\\Q=\\E")[0], kg);
|
||||
}
|
||||
} else {
|
||||
b.setBlock(v);
|
||||
}
|
||||
|
||||
return b;
|
||||
@@ -116,7 +120,11 @@ public class IrisBlockData extends IrisRegistrant {
|
||||
}
|
||||
|
||||
try {
|
||||
return Double.valueOf(string).intValue();
|
||||
double value = Double.parseDouble(string);
|
||||
if (value >= Integer.MIN_VALUE && value <= Integer.MAX_VALUE && value == Math.rint(value)) {
|
||||
return (int) value;
|
||||
}
|
||||
return value;
|
||||
} catch (Throwable ignored) {
|
||||
// Checks
|
||||
}
|
||||
@@ -156,19 +164,13 @@ public class IrisBlockData extends IrisRegistrant {
|
||||
PlatformBlockState customState = customData.getBlockData(data);
|
||||
|
||||
if (customState != null) {
|
||||
String st = customState.key();
|
||||
|
||||
if (st.contains("[")) {
|
||||
st = st.split("\\Q[\\E")[0];
|
||||
if (getData().isEmpty()) {
|
||||
return customState;
|
||||
}
|
||||
|
||||
KMap<String, Object> cdata = customData.getData().copy();
|
||||
|
||||
for (String i : getData().keySet()) {
|
||||
cdata.put(i, getData().get(i));
|
||||
}
|
||||
|
||||
String sx = keyify(st) + computeProperties(cdata);
|
||||
IrisBlockData merged = IrisBlockData.from(customState.key());
|
||||
merged.getData().putAll(getData());
|
||||
String sx = merged.stateKey();
|
||||
|
||||
if (debug) {
|
||||
IrisLogging.debug("Block Data used " + sx + " (CUSTOM)");
|
||||
|
||||
@@ -146,6 +146,9 @@ public class BukkitChunkGenerator extends ChunkGenerator implements PlatformChun
|
||||
private volatile Throwable initializationFailure;
|
||||
private volatile IrisDimension validatedDimension;
|
||||
private volatile IrisDimensionRuntimeContract validatedWorldContract;
|
||||
private volatile EngineTarget startupTarget;
|
||||
private volatile CompletableFuture<Void> startupContentReady = CompletableFuture.completedFuture(null);
|
||||
private volatile CompletableFuture<Void> startupReady = CompletableFuture.completedFuture(null);
|
||||
private volatile boolean closing;
|
||||
@Setter
|
||||
private volatile StudioGenerator studioGenerator;
|
||||
@@ -195,6 +198,11 @@ public class BukkitChunkGenerator extends ChunkGenerator implements PlatformChun
|
||||
}
|
||||
BukkitPlatform.volmitPlugin().unregisterListener(this);
|
||||
world.setRawWorldSeed(event.getWorld().getSeed());
|
||||
if (startupTarget != null) {
|
||||
startupContentReady.whenComplete((ignored, failure) ->
|
||||
J.s(() -> resumeDeferredStartup(event.getWorld(), failure)));
|
||||
return;
|
||||
}
|
||||
if (initialize(event.getWorld())) return;
|
||||
|
||||
IrisLogging.warn("Failed to get Engine for " + event.getWorld().getName() + " re-trying...");
|
||||
@@ -205,6 +213,73 @@ public class BukkitChunkGenerator extends ChunkGenerator implements PlatformChun
|
||||
}, 10);
|
||||
}
|
||||
|
||||
public void deferStartup(EngineTarget target, CompletableFuture<Void> contentReady) {
|
||||
if (setup.get() || startupTarget != null) {
|
||||
throw new IllegalStateException("Iris generator startup has already begun.");
|
||||
}
|
||||
startupTarget = Objects.requireNonNull(target);
|
||||
startupContentReady = Objects.requireNonNull(contentReady);
|
||||
startupReady = new CompletableFuture<>();
|
||||
}
|
||||
|
||||
private void resumeDeferredStartup(World runtimeWorld, Throwable failure) {
|
||||
lock.lock();
|
||||
try {
|
||||
closeStartupTarget();
|
||||
if (closing) {
|
||||
startupReady.completeExceptionally(new IllegalStateException("Iris generator closed during startup."));
|
||||
return;
|
||||
}
|
||||
if (failure != null) {
|
||||
initializationFailure = failure;
|
||||
spawnChunks.completeExceptionally(failure);
|
||||
initialSpawnReady.completeExceptionally(failure);
|
||||
startupReady.completeExceptionally(failure);
|
||||
IrisLogging.reportError("External block providers did not become available for Iris world '"
|
||||
+ world.name() + "'; generation remains locked.", failure);
|
||||
Bukkit.shutdown();
|
||||
return;
|
||||
}
|
||||
if (!initialize(runtimeWorld)) {
|
||||
throw new IllegalStateException("Iris engine initialization did not complete for '" + world.name() + "'.");
|
||||
}
|
||||
startupReady.complete(null);
|
||||
} catch (RuntimeException | Error initializationError) {
|
||||
initializationFailure = initializationError;
|
||||
spawnChunks.completeExceptionally(initializationError);
|
||||
initialSpawnReady.completeExceptionally(initializationError);
|
||||
startupReady.completeExceptionally(initializationError);
|
||||
Bukkit.shutdown();
|
||||
throw initializationError;
|
||||
} finally {
|
||||
lock.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
private void closeStartupTarget() {
|
||||
EngineTarget target = startupTarget;
|
||||
startupTarget = null;
|
||||
if (target != null) {
|
||||
target.getData().close();
|
||||
}
|
||||
}
|
||||
|
||||
private void requireStartupContentReady() {
|
||||
if (startupReady.isCompletedExceptionally()) {
|
||||
startupReady.join();
|
||||
}
|
||||
boolean initializationPending = startupTarget != null
|
||||
|| !startupReady.isDone() && !lock.isHeldByCurrentThread();
|
||||
requireStartupContentReady(startupContentReady, initializationPending);
|
||||
}
|
||||
|
||||
static void requireStartupContentReady(CompletableFuture<Void> ready, boolean initializationPending) {
|
||||
if (!ready.isDone() || initializationPending) {
|
||||
throw new IllegalStateException("Iris generation is waiting for external block providers to initialize.");
|
||||
}
|
||||
ready.join();
|
||||
}
|
||||
|
||||
private boolean initialize(World world) {
|
||||
try {
|
||||
Engine engine = getEngine(world);
|
||||
@@ -579,6 +654,10 @@ public class BukkitChunkGenerator extends ChunkGenerator implements PlatformChun
|
||||
@Override
|
||||
public EngineTarget getTarget() {
|
||||
if (engine != null) return engine.getTarget();
|
||||
EngineTarget pendingTarget = startupTarget;
|
||||
if (pendingTarget != null) {
|
||||
return pendingTarget;
|
||||
}
|
||||
|
||||
return targetCache.aquireOrThrow(() -> {
|
||||
if (generationHistory != null) {
|
||||
@@ -621,6 +700,7 @@ public class BukkitChunkGenerator extends ChunkGenerator implements PlatformChun
|
||||
|
||||
private Engine getEngine(WorldInfo world) {
|
||||
throwIfInitializationFailed();
|
||||
requireStartupContentReady();
|
||||
validateAndBindWorld(world);
|
||||
if (setup.get()) {
|
||||
return getEngine();
|
||||
@@ -763,6 +843,8 @@ public class BukkitChunkGenerator extends ChunkGenerator implements PlatformChun
|
||||
}
|
||||
folder.clear();
|
||||
populators.clear();
|
||||
closeStartupTarget();
|
||||
startupReady.completeExceptionally(new IllegalStateException("Iris generator closed during startup."));
|
||||
});
|
||||
} catch (Throwable throwable) {
|
||||
if (!alreadyClosing) {
|
||||
@@ -1079,6 +1161,9 @@ public class BukkitChunkGenerator extends ChunkGenerator implements PlatformChun
|
||||
}
|
||||
|
||||
public void touch(World world) {
|
||||
if (!startupReady.isDone() || startupReady.isCompletedExceptionally()) {
|
||||
return;
|
||||
}
|
||||
getEngine(world);
|
||||
}
|
||||
|
||||
@@ -1274,7 +1359,7 @@ public class BukkitChunkGenerator extends ChunkGenerator implements PlatformChun
|
||||
return shouldGenerateNativeStructures(
|
||||
isAuthoringStudio(),
|
||||
studioEntryBootstrapActive.get(),
|
||||
initializationFailure != null);
|
||||
initializationFailure != null || startupTarget != null);
|
||||
}
|
||||
|
||||
static boolean shouldGenerateNativeStructures(
|
||||
|
||||
@@ -101,6 +101,10 @@ public final class EngineBukkitOps {
|
||||
}
|
||||
|
||||
public static void updateChunk(Engine engine, Chunk c) {
|
||||
updateChunk(engine, c, engine.getMantle().getMantle());
|
||||
}
|
||||
|
||||
public static void updateChunk(Engine engine, Chunk c, Mantle<Matter> mantle) {
|
||||
for (int x = -1; x <= 1; x++) {
|
||||
for (int z = -1; z <= 1; z++) {
|
||||
if (c.getWorld().isChunkLoaded(c.getX() + x, c.getZ() + z))
|
||||
@@ -110,7 +114,6 @@ public final class EngineBukkitOps {
|
||||
return;
|
||||
}
|
||||
}
|
||||
Mantle<Matter> mantle = engine.getMantle().getMantle();
|
||||
if (!mantle.isLoaded(c)) {
|
||||
String msg = "Mantle Chunk " + c.getX() + "," + c.getZ() + " is not loaded";
|
||||
IrisLogging.debug(msg);
|
||||
@@ -118,7 +121,7 @@ public final class EngineBukkitOps {
|
||||
}
|
||||
|
||||
if (!J.isFolia() && !J.isPrimaryThread()) {
|
||||
CompletableFuture<?> scheduled = J.sfut(() -> updateChunk(engine, c));
|
||||
CompletableFuture<?> scheduled = J.sfut(() -> updateChunk(engine, c, mantle));
|
||||
if (scheduled != null) {
|
||||
try {
|
||||
scheduled.join();
|
||||
|
||||
@@ -70,7 +70,7 @@ public final class BukkitBlockState implements PlatformBlockState {
|
||||
|
||||
public static BukkitBlockState of(BlockData data) {
|
||||
if (data instanceof IrisCustomData custom) {
|
||||
return new BukkitBlockState(data, custom.getAsString());
|
||||
return new BukkitBlockState(data, custom.getCustom().toString());
|
||||
}
|
||||
BukkitBlockState fast = DATA_CACHE.get(data);
|
||||
if (fast != null) {
|
||||
@@ -369,15 +369,14 @@ public final class BukkitBlockState implements PlatformBlockState {
|
||||
|
||||
@Override
|
||||
public PlatformBlockState withProperty(String name, String value) {
|
||||
String merged = mergeProperty(key, name, value);
|
||||
BlockData resolved = Bukkit.createBlockData(merged);
|
||||
// Re-attach the custom identity (as the proxy's own merge/clone cases do): the key of
|
||||
// a custom state is the BASE block's string, so rebuilding from it alone silently
|
||||
// downgraded custom blocks to vanilla on e.g. auto-waterlogging.
|
||||
// Re-attach the custom identity (as the proxy's own merge/clone cases do) after
|
||||
// editing the base block, so auto-waterlogging cannot turn custom blocks into vanilla.
|
||||
if (data instanceof IrisCustomData custom) {
|
||||
String merged = mergeProperty(custom.getBase().getAsString(), name, value);
|
||||
BlockData resolved = Bukkit.createBlockData(merged);
|
||||
return of(IrisCustomData.of(resolved, custom.getCustom()));
|
||||
}
|
||||
return of(resolved);
|
||||
return of(Bukkit.createBlockData(mergeProperty(key, name, value)));
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -94,17 +94,17 @@ final class CompatFixtures {
|
||||
@Override
|
||||
public PlatformBlockState blockOrNull(String key, boolean warn) {
|
||||
blockLookups++;
|
||||
String normalized = key.trim().toLowerCase(Locale.ROOT);
|
||||
String normalized = key.trim();
|
||||
int props = normalized.indexOf('[');
|
||||
String base = props < 0 ? normalized : normalized.substring(0, props);
|
||||
String base = (props < 0 ? normalized : normalized.substring(0, props)).toLowerCase(Locale.ROOT);
|
||||
String properties = props < 0 ? "" : normalized.substring(props);
|
||||
if (base.indexOf(':') < 0) {
|
||||
base = "minecraft:" + base;
|
||||
normalized = base + (props < 0 ? "" : normalized.substring(props));
|
||||
}
|
||||
if (missingBlocks.contains(base)) {
|
||||
return null;
|
||||
}
|
||||
return new FakeBlockState(normalized);
|
||||
return new FakeBlockState(base + (base.startsWith("minecraft:") ? properties.toLowerCase(Locale.ROOT) : properties));
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -165,6 +165,8 @@ public class ContentGateTest {
|
||||
assertEquals("minecraft:stone", ContentGate.normalizeState(" STONE "));
|
||||
assertEquals("minecraft:oak_log[axis=y]", ContentGate.normalizeState("Oak_Log[AXIS=Y]"));
|
||||
assertEquals("create:cogwheel", ContentGate.normalizeState("create:cogwheel"));
|
||||
assertEquals("craftengine:forest/chair[randomYaw=true,variant=OakLarge]",
|
||||
ContentGate.normalizeState("CraftEngine:Forest/Chair[randomYaw=true,variant=OakLarge]"));
|
||||
assertNull(ContentGate.normalizeState(null));
|
||||
assertNull(ContentGate.normalizeState(" "));
|
||||
assertEquals("minecraft:oak_log", ContentGate.baseKey("minecraft:oak_log[axis=y]"));
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
package art.arcane.iris.core.compat;
|
||||
|
||||
import art.arcane.iris.core.loader.IrisData;
|
||||
import art.arcane.iris.engine.framework.Engine;
|
||||
import art.arcane.iris.engine.object.IrisBlockData;
|
||||
import art.arcane.iris.spi.PlatformBlockState;
|
||||
import org.junit.AfterClass;
|
||||
import org.junit.BeforeClass;
|
||||
import org.junit.Test;
|
||||
@@ -11,7 +13,10 @@ import java.nio.file.Files;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertNotSame;
|
||||
import static org.junit.Assert.assertSame;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.mockito.Mockito.mock;
|
||||
|
||||
public class IrisBlockDataResolutionTest {
|
||||
private static CompatFixtures.FakeRegistries registries;
|
||||
@@ -93,4 +98,119 @@ public class IrisBlockDataResolutionTest {
|
||||
assertEquals(CompatAction.SUBSTITUTED, data.getCompatReport().findings().getFirst().action());
|
||||
assertEquals("block", data.getCompatReport().findings().getFirst().subjectType());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void nestedPackBlocksPreserveInheritedProviderProperties() throws Exception {
|
||||
IrisData data = pack(null, "{\"block\":\"base\",\"data\":{\"variant\":\"large\"}}");
|
||||
File blocks = new File(data.getDataFolder(), "blocks");
|
||||
Files.writeString(new File(blocks, "base.json").toPath(),
|
||||
"{\"block\":\"craftengine:forest/amber_log\",\"data\":{\"axis\":\"y\",\"yaw\":22.5}}");
|
||||
IrisBlockData entry = new IrisBlockData("fancy");
|
||||
entry.getData().put("axis", "x");
|
||||
|
||||
PlatformBlockState resolved = entry.getBlockData(data);
|
||||
IrisBlockData parsed = IrisBlockData.from(resolved.key());
|
||||
|
||||
assertEquals("craftengine:forest/amber_log", parsed.getBlock());
|
||||
assertEquals("x", parsed.getData().get("axis"));
|
||||
assertEquals("large", parsed.getData().get("variant"));
|
||||
assertEquals(22.5, (Double) parsed.getData().get("yaw"), 0.0);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void providerPropertiesKeepTheirCaseThroughDirectResolution() throws Exception {
|
||||
IrisData data = pack(null, null);
|
||||
IrisBlockData entry = new IrisBlockData("craftengine:forest/chair");
|
||||
entry.getData().put("randomYaw", true);
|
||||
entry.getData().put("variant", "OakLarge");
|
||||
|
||||
PlatformBlockState resolved = entry.getBlockData(data);
|
||||
IrisBlockData parsed = IrisBlockData.from(resolved.key());
|
||||
|
||||
assertEquals(true, parsed.getData().get("randomYaw"));
|
||||
assertEquals("OakLarge", parsed.getData().get("variant"));
|
||||
assertTrue(resolved.key().contains("randomYaw=true"));
|
||||
assertTrue(resolved.key().contains("variant=OakLarge"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void nestedPackBlocksKeepInheritedProviderPropertyCase() throws Exception {
|
||||
IrisData data = pack(null, "{\"block\":\"base\",\"data\":{\"yaw\":22.5}}");
|
||||
File blocks = new File(data.getDataFolder(), "blocks");
|
||||
Files.writeString(new File(blocks, "base.json").toPath(),
|
||||
"{\"block\":\"craftengine:forest/chair\",\"data\":{\"randomYaw\":true,\"variant\":\"OakLarge\"}}");
|
||||
IrisBlockData entry = new IrisBlockData("fancy");
|
||||
entry.getData().put("pitch", 12.75);
|
||||
|
||||
IrisBlockData parsed = IrisBlockData.from(entry.getBlockData(data).key());
|
||||
|
||||
assertEquals(true, parsed.getData().get("randomYaw"));
|
||||
assertEquals("OakLarge", parsed.getData().get("variant"));
|
||||
assertEquals(22.5, (Double) parsed.getData().get("yaw"), 0.0);
|
||||
assertEquals(12.75, (Double) parsed.getData().get("pitch"), 0.0);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void parsedVanillaPropertiesStillNormalizeCase() {
|
||||
IrisBlockData entry = IrisBlockData.from("OAK_LOG[AXIS=Y]");
|
||||
|
||||
assertEquals("minecraft:oak_log[axis=y]", entry.stateKey());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void decimalProviderPropertiesRetainPrecision() {
|
||||
IrisBlockData entry = IrisBlockData.from("craftengine:forest/lantern[yaw=22.5,pitch=-12.75]");
|
||||
|
||||
assertEquals(22.5, (Double) entry.getData().get("yaw"), 0.0);
|
||||
assertEquals(-12.75, (Double) entry.getData().get("pitch"), 0.0);
|
||||
assertTrue(entry.stateKey().contains("yaw=22.5"));
|
||||
assertTrue(entry.stateKey().contains("pitch=-12.75"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void wholeNumberPropertiesKeepIntegerSyntax() {
|
||||
IrisBlockData entry = new IrisBlockData("minecraft:wheat");
|
||||
entry.getData().put("age", 4.0);
|
||||
|
||||
assertEquals("minecraft:wheat[age=4]", entry.stateKey());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void providerReadinessClearsAuthoringBlockAndGateMisses() throws Exception {
|
||||
String blockKey = "lateblocks:example/stone";
|
||||
registries.missingBlocks.add(blockKey);
|
||||
IrisData data = pack(null, "{\"block\":\"" + blockKey + "\"}");
|
||||
try {
|
||||
IrisBlockData missing = data.getBlockLoader().load("fancy", false);
|
||||
assertTrue(missing.getBlockData(data).isAir());
|
||||
ContentGate originalGate = data.getContentGate();
|
||||
registries.missingBlocks.remove(blockKey);
|
||||
|
||||
IrisData.invalidateLoadedContentRegistries(data.getDataFolder());
|
||||
|
||||
IrisBlockData resolved = data.getBlockLoader().load("fancy", false);
|
||||
assertNotSame(missing, resolved);
|
||||
assertNotSame(originalGate, data.getContentGate());
|
||||
assertEquals(blockKey, resolved.getBlockData(data).key());
|
||||
} finally {
|
||||
registries.missingBlocks.remove(blockKey);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void providerReadinessLeavesEngineAttachedCachesIntact() throws Exception {
|
||||
IrisData data = pack(null, "{\"block\":\"minecraft:stone\"}");
|
||||
IrisBlockData block = data.getBlockLoader().load("fancy", false);
|
||||
ContentGate gate = data.getContentGate();
|
||||
Engine engine = mock(Engine.class);
|
||||
data.registerEngine(engine);
|
||||
try {
|
||||
IrisData.invalidateLoadedContentRegistries(data.getDataFolder());
|
||||
|
||||
assertSame(block, data.getBlockLoader().load("fancy", false));
|
||||
assertSame(gate, data.getContentGate());
|
||||
} finally {
|
||||
data.unregisterEngine(engine);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,6 +18,8 @@ import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertNull;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
public class ContentKeyValidatorTest {
|
||||
private static PlatformRegistries registries() {
|
||||
@@ -87,6 +89,37 @@ public class ContentKeyValidatorTest {
|
||||
assertTrue(error.namespaceLoaded());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void validateAcceptsNativeAndQualifiedExternalBlockTypes() {
|
||||
PlatformRegistries registries = mock(PlatformRegistries.class);
|
||||
when(registries.blockKeys()).thenReturn(List.of("minecraft:stone"));
|
||||
when(registries.blockTypeKeys()).thenReturn(List.of("minecraft:stone", "oraxen:caveblock",
|
||||
"oraxen:oraxen/caveblock", "custom:marble", "itemsadder:custom/marble"));
|
||||
|
||||
List<ContentKeyError> errors = ContentKeyValidator.validate(registries,
|
||||
List.of("stone", "oraxen:caveblock", "oraxen:oraxen/caveblock",
|
||||
"custom:marble", "itemsadder:custom/marble"), List.of(), List.of());
|
||||
|
||||
assertTrue(errors.toString(), errors.isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void validateStillReportsMissingExternalBlockTypes() {
|
||||
PlatformRegistries registries = mock(PlatformRegistries.class);
|
||||
when(registries.blockKeys()).thenReturn(List.of("minecraft:stone"));
|
||||
when(registries.blockTypeKeys()).thenReturn(List.of("minecraft:stone", "oraxen:oraxen/caveblock"));
|
||||
|
||||
List<ContentKeyError> errors = ContentKeyValidator.validate(registries,
|
||||
List.of("oraxen:oraxen/caveblok", "itemsadder:custom/marble"), List.of(), List.of());
|
||||
|
||||
assertEquals(2, errors.size());
|
||||
assertEquals("oraxen:oraxen/caveblok", errors.get(0).key());
|
||||
assertTrue(errors.get(0).namespaceLoaded());
|
||||
assertEquals("oraxen:oraxen/caveblock", errors.get(0).suggestion());
|
||||
assertEquals("itemsadder:custom/marble", errors.get(1).key());
|
||||
assertFalse(errors.get(1).namespaceLoaded());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void validateFlagsUnloadedNamespaceWhenNamespaceMissing() {
|
||||
List<ContentKeyError> errors = ContentKeyValidator.validate(registries(),
|
||||
@@ -275,7 +308,7 @@ public class ContentKeyValidatorTest {
|
||||
|
||||
@Override
|
||||
public List<String> blockTypeKeys() {
|
||||
return List.of();
|
||||
return blocks;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -3,6 +3,10 @@ package art.arcane.iris.core.pack;
|
||||
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.spi.IrisPlatform;
|
||||
import art.arcane.iris.spi.IrisPlatforms;
|
||||
import art.arcane.iris.spi.PlatformRegistries;
|
||||
import art.arcane.iris.spi.PlatformStructureHooks;
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.junit.Assume;
|
||||
@@ -22,6 +26,8 @@ import static org.junit.Assert.assertNotEquals;
|
||||
import static org.junit.Assert.assertNull;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.junit.Assert.fail;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
public class PackValidationCacheTest {
|
||||
@Rule
|
||||
@@ -214,4 +220,28 @@ public class PackValidationCacheTest {
|
||||
PackValidationRegistry.clear();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void externalBlockAvailabilityChangesValidationContext() {
|
||||
IrisPlatform previous = IrisPlatforms.isBound() ? IrisPlatforms.get() : null;
|
||||
IrisPlatforms.unbind();
|
||||
PlatformRegistries registries = mock(PlatformRegistries.class);
|
||||
IrisPlatform platform = mock(IrisPlatform.class);
|
||||
when(platform.registries()).thenReturn(registries);
|
||||
when(platform.structureHooks()).thenReturn(mock(PlatformStructureHooks.class));
|
||||
when(registries.blockKeys()).thenReturn(List.of("minecraft:stone"));
|
||||
when(registries.blockTypeKeys()).thenReturn(List.of("minecraft:stone"));
|
||||
IrisPlatforms.bind(platform);
|
||||
try {
|
||||
String before = PackValidationCache.contextFingerprint();
|
||||
when(registries.blockTypeKeys()).thenReturn(List.of("minecraft:stone", "oraxen:oraxen/caveblock"));
|
||||
|
||||
assertNotEquals(before, PackValidationCache.contextFingerprint());
|
||||
} finally {
|
||||
IrisPlatforms.unbind();
|
||||
if (previous != null) {
|
||||
IrisPlatforms.bind(previous);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
package art.arcane.iris.core.project;
|
||||
|
||||
import art.arcane.iris.core.loader.IrisData;
|
||||
import art.arcane.iris.core.loader.ResourceLoader;
|
||||
import art.arcane.iris.engine.object.IrisBlockData;
|
||||
import art.arcane.iris.spi.IrisPlatform;
|
||||
import art.arcane.iris.spi.IrisPlatforms;
|
||||
import art.arcane.iris.spi.PlatformBlockProperty;
|
||||
import art.arcane.iris.spi.PlatformRegistries;
|
||||
import art.arcane.volmlib.util.json.JSONObject;
|
||||
import org.junit.After;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
public class ExternalBlockSchemaTest {
|
||||
private static final String QUALIFIED_BLOCK = "craftengine:example/ores/copper";
|
||||
|
||||
private IrisPlatform previous;
|
||||
|
||||
@Before
|
||||
public void bindPlatform() {
|
||||
previous = IrisPlatforms.isBound() ? IrisPlatforms.get() : null;
|
||||
IrisPlatforms.unbind();
|
||||
PlatformRegistries registries = mock(PlatformRegistries.class);
|
||||
when(registries.blockTypeKeys()).thenReturn(List.of(QUALIFIED_BLOCK));
|
||||
when(registries.blockStateProperties()).thenReturn(Map.of(QUALIFIED_BLOCK,
|
||||
List.of(new PlatformBlockProperty("axis", "string", "y", List.of("x", "y", "z"), null))));
|
||||
IrisPlatform platform = mock(IrisPlatform.class);
|
||||
when(platform.registries()).thenReturn(registries);
|
||||
IrisPlatforms.bind(platform);
|
||||
}
|
||||
|
||||
@After
|
||||
public void restorePlatform() {
|
||||
IrisPlatforms.unbind();
|
||||
if (previous != null) {
|
||||
IrisPlatforms.bind(previous);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void qualifiedBlockReferencesResolveToItsEnumAndPropertySchema() {
|
||||
IrisData data = mock(IrisData.class);
|
||||
ResourceLoader<IrisBlockData> blocks = mock(ResourceLoader.class);
|
||||
when(blocks.getPossibleKeys()).thenReturn(new String[0]);
|
||||
when(data.getBlockLoader()).thenReturn(blocks);
|
||||
|
||||
JSONObject schema = new SchemaBuilder(IrisBlockData.class, data).construct();
|
||||
JSONObject branch = schema.getJSONArray("allOf").getJSONObject(0)
|
||||
.getJSONArray("anyOf").getJSONObject(0);
|
||||
String enumReference = branch.getJSONObject("if").getJSONObject("properties")
|
||||
.getJSONObject("block").getString("$ref");
|
||||
String propertyReference = branch.getJSONObject("then").getJSONObject("properties")
|
||||
.getJSONObject("data").getString("$ref");
|
||||
|
||||
assertEquals(QUALIFIED_BLOCK, resolve(schema, enumReference).getJSONArray("enum").getString(0));
|
||||
JSONObject axis = resolve(schema, propertyReference).getJSONObject("properties").getJSONObject("axis");
|
||||
assertEquals("string", axis.getString("type"));
|
||||
assertEquals("x", axis.getJSONArray("enum").getString(0));
|
||||
assertEquals("z", axis.getJSONArray("enum").getString(2));
|
||||
}
|
||||
|
||||
private static JSONObject resolve(JSONObject schema, String reference) {
|
||||
JSONObject resolved = schema;
|
||||
for (String token : reference.substring(2).split("/")) {
|
||||
resolved = resolved.getJSONObject(token.replace("~1", "/").replace("~0", "~"));
|
||||
}
|
||||
return resolved;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,179 @@
|
||||
package art.arcane.iris.core.service;
|
||||
|
||||
import art.arcane.iris.core.link.ExternalDataProvider;
|
||||
import art.arcane.iris.core.link.Identifier;
|
||||
import art.arcane.iris.core.link.data.DataType;
|
||||
import art.arcane.iris.core.nms.container.BlockProperty;
|
||||
import art.arcane.iris.core.nms.container.Pair;
|
||||
import art.arcane.iris.engine.framework.Engine;
|
||||
import art.arcane.iris.util.common.data.IrisCustomData;
|
||||
import art.arcane.volmlib.util.collection.KMap;
|
||||
import org.bukkit.block.Block;
|
||||
import org.bukkit.block.data.BlockData;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import java.util.List;
|
||||
import java.util.MissingResourceException;
|
||||
import java.util.Set;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertSame;
|
||||
import static org.junit.Assert.assertThrows;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
public class ExternalDataSVCBlockRoutingTest {
|
||||
@Test
|
||||
public void nativeLookupPinsProviderAndRetainsPropertiesUntilPlacement() throws Exception {
|
||||
Identifier nativeId = Identifier.fromString("example:wood");
|
||||
ExternalDataProvider provider = provider("CraftEngine", nativeId);
|
||||
BlockData base = mock(BlockData.class);
|
||||
KMap<String, String> properties = new KMap<>();
|
||||
properties.put("facing", "east");
|
||||
when(provider.getBlockData(nativeId, properties)).thenReturn(IrisCustomData.of(base,
|
||||
Identifier.fromString("example:wood[facing=east]")));
|
||||
ExternalDataSVC service = service(provider);
|
||||
|
||||
IrisCustomData resolved = (IrisCustomData) service.getBlockData(
|
||||
Identifier.fromString("example:wood[facing=east]")).orElseThrow();
|
||||
|
||||
assertSame(base, resolved.getBase());
|
||||
assertEquals("craftengine:example/wood[facing=east]", resolved.getCustom().toString());
|
||||
ExternalDataProvider competing = provider("ItemsAdder", nativeId);
|
||||
addProvider(service, competing);
|
||||
Engine engine = mock(Engine.class);
|
||||
Block block = mock(Block.class);
|
||||
service.processUpdate(engine, block, resolved.getCustom());
|
||||
verify(provider).processUpdate(engine, block, Identifier.fromString("example:wood[facing=east]"));
|
||||
verify(competing, never()).processUpdate(any(), any(), any());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void qualifiedLookupSelectsProviderDespiteNativeCollision() throws Exception {
|
||||
Identifier nativeId = Identifier.fromString("example:wood");
|
||||
ExternalDataProvider first = provider("CraftEngine", nativeId);
|
||||
ExternalDataProvider second = provider("ItemsAdder", nativeId);
|
||||
BlockData base = mock(BlockData.class);
|
||||
when(second.getBlockData(eq(nativeId), any())).thenReturn(IrisCustomData.of(base, nativeId));
|
||||
ExternalDataSVC service = service(first, second);
|
||||
|
||||
assertFalse(service.getBlockData(nativeId).isPresent());
|
||||
IrisCustomData resolved = (IrisCustomData) service.getBlockData(
|
||||
Identifier.fromString("itemsadder:example/wood")).orElseThrow();
|
||||
|
||||
assertEquals("itemsadder:example/wood", resolved.getCustom().toString());
|
||||
verify(first, never()).getBlockData(any(), any());
|
||||
assertFalse(service.getBlockProperties(nativeId).isPresent());
|
||||
assertEquals(List.of(), service.getBlockProperties(Identifier.fromString("itemsadder:example/wood")).orElseThrow());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void missingQualifiedProviderDoesNotLeakIntoAnotherNamespaceOwner() throws Exception {
|
||||
Identifier id = Identifier.fromString("itemsadder:example/wood");
|
||||
ExternalDataProvider unrelated = provider("OtherBlocks", id);
|
||||
ExternalDataSVC service = service(unrelated);
|
||||
|
||||
assertFalse(service.getBlockData(id).isPresent());
|
||||
verify(unrelated, never()).getBlockData(any(), any());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void qualifiedNestedKeysAndPropertiesAppearInSchemas() throws Exception {
|
||||
Identifier nativeId = Identifier.fromString("example:ores/copper");
|
||||
ExternalDataProvider provider = provider("ItemsAdder", nativeId);
|
||||
List<BlockProperty> properties = List.of(BlockProperty.ofBoolean("lit", false));
|
||||
when(provider.getBlockProperties(nativeId)).thenReturn(properties);
|
||||
BlockData base = mock(BlockData.class);
|
||||
when(provider.getBlockData(eq(nativeId), any())).thenReturn(base);
|
||||
ExternalDataSVC service = service(provider);
|
||||
Identifier qualified = Identifier.fromString("itemsadder:example/ores/copper");
|
||||
|
||||
assertSame(base, service.getBlockData(qualified).orElseThrow());
|
||||
assertEquals(Set.of(nativeId, qualified), Set.copyOf(service.getAllIdentifiers(DataType.BLOCK)));
|
||||
assertEquals(properties, service.getBlockProperties(qualified).orElseThrow());
|
||||
assertEquals(2, service.getAllBlockProperties().size());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void nativeProviderKeysCanContainSlashes() throws Exception {
|
||||
Identifier nativeId = Identifier.fromString("oraxen:ores/copper");
|
||||
ExternalDataProvider provider = provider("Oraxen", nativeId);
|
||||
BlockData base = mock(BlockData.class);
|
||||
when(provider.getBlockData(eq(nativeId), any())).thenReturn(base);
|
||||
ExternalDataSVC service = service(provider);
|
||||
|
||||
assertSame(base, service.getBlockData(nativeId).orElseThrow());
|
||||
assertSame(base, service.getBlockData(Identifier.fromString("oraxen:oraxen/ores/copper")).orElseThrow());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void providerFailureDoesNotEscapeIntoVanillaFallbackParsing() throws Exception {
|
||||
Identifier id = Identifier.fromString("example:stone");
|
||||
ExternalDataProvider provider = provider("ItemsAdder", id);
|
||||
when(provider.getBlockData(eq(id), any())).thenThrow(new IllegalStateException("Registry is unavailable"));
|
||||
ExternalDataSVC service = service(provider);
|
||||
|
||||
assertFalse(service.getBlockData(id).isPresent());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void unavailableProviderDoesNotReportPlacementSuccess() throws Exception {
|
||||
ExternalDataSVC service = service();
|
||||
|
||||
assertThrows(MissingResourceException.class, () -> service.processUpdate(
|
||||
mock(Engine.class), mock(Block.class), Identifier.fromString("itemsadder:example/wood")));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void malformedStateSuffixesFailBeforeProviderLookup() throws Exception {
|
||||
ExternalDataProvider provider = provider("ItemsAdder", Identifier.fromString("example:wood"));
|
||||
ExternalDataSVC service = service(provider);
|
||||
for (String state : List.of("wood[", "wood]", "wood[a=b]tail", "wood[a=b,]", "wood[a=b,a=c]", "wood[[a=b]]", "wood[a]")) {
|
||||
Identifier id = new Identifier("example", state);
|
||||
assertThrows(state, IllegalArgumentException.class, () -> ExternalDataSVC.parseState(id));
|
||||
assertFalse(state, service.getBlockData(id).isPresent());
|
||||
}
|
||||
verify(provider, never()).getBlockData(any(), any());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void stateParserPreservesDecimalsAndTrimsProperties() {
|
||||
Pair<Identifier, KMap<String, String>> parsed = ExternalDataSVC.parseState(
|
||||
Identifier.fromString("craftengine:example/chair[ yaw = 22.5 ]"));
|
||||
|
||||
assertEquals(Identifier.fromString("craftengine:example/chair"), parsed.getA());
|
||||
assertEquals("22.5", parsed.getB().get("yaw"));
|
||||
assertEquals("craftengine:example/chair[yaw=22.5]", ExternalDataSVC.buildState(parsed.getA(), parsed.getB()).toString());
|
||||
}
|
||||
|
||||
private static ExternalDataProvider provider(String pluginId, Identifier id) {
|
||||
ExternalDataProvider provider = mock(ExternalDataProvider.class);
|
||||
when(provider.getPluginId()).thenReturn(pluginId);
|
||||
when(provider.isValidProvider(id, DataType.BLOCK)).thenReturn(true);
|
||||
when(provider.getTypes(DataType.BLOCK)).thenReturn(List.of(id));
|
||||
when(provider.getBlockProperties(id)).thenReturn(List.of());
|
||||
return provider;
|
||||
}
|
||||
|
||||
private static ExternalDataSVC service(ExternalDataProvider... providers) throws Exception {
|
||||
ExternalDataSVC service = new ExternalDataSVC();
|
||||
for (ExternalDataProvider provider : providers) {
|
||||
addProvider(service, provider);
|
||||
}
|
||||
return service;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private static void addProvider(ExternalDataSVC service, ExternalDataProvider provider) throws Exception {
|
||||
Field field = ExternalDataSVC.class.getDeclaredField("activeProviders");
|
||||
field.setAccessible(true);
|
||||
List<ExternalDataProvider> providers = (List<ExternalDataProvider>) field.get(service);
|
||||
providers.add(provider);
|
||||
}
|
||||
}
|
||||
+116
@@ -0,0 +1,116 @@
|
||||
package art.arcane.iris.core.service;
|
||||
|
||||
import art.arcane.iris.core.link.ExternalDataProvider;
|
||||
import art.arcane.iris.core.link.Identifier;
|
||||
import art.arcane.iris.core.link.data.DataType;
|
||||
import org.junit.Test;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.plugin.Plugin;
|
||||
import org.bukkit.plugin.PluginManager;
|
||||
import org.mockito.MockedStatic;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.mockStatic;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
public class ExternalDataSVCContentChangeTest {
|
||||
@Test
|
||||
public void enabledPluginIsPendingUntilItsProviderActivates() {
|
||||
ExternalDataSVC service = new ExternalDataSVC();
|
||||
Plugin plugin = mock(Plugin.class);
|
||||
PluginManager plugins = mock(PluginManager.class);
|
||||
when(plugin.isEnabled()).thenReturn(true);
|
||||
when(plugins.getPlugin("ItemsAdder")).thenReturn(plugin);
|
||||
|
||||
try (MockedStatic<Bukkit> bukkit = mockStatic(Bukkit.class)) {
|
||||
bukkit.when(Bukkit::getPluginManager).thenReturn(plugins);
|
||||
|
||||
assertTrue(service.hasPendingBlockProvider("itemsadder:example/stone"));
|
||||
assertTrue(service.hasPendingBlockProvider("example:stone"));
|
||||
assertFalse(service.hasPendingBlockProvider("minecraft:stone"));
|
||||
assertFalse(service.hasPendingBlockProvider("oraxen:oraxen/stone"));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void contentChangesAdvanceTheRevisionWithoutAListener() {
|
||||
ExternalDataSVC service = new ExternalDataSVC();
|
||||
long revision = service.contentRevision();
|
||||
|
||||
service.notifyContentChanged();
|
||||
|
||||
assertEquals(revision + 1L, service.contentRevision());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void activationAndRegistryReloadNotifyTheListener() {
|
||||
ExternalDataSVC service = new ExternalDataSVC();
|
||||
service.notifyContentChanged();
|
||||
AtomicInteger changes = new AtomicInteger();
|
||||
service.setContentChangeListener(changes::incrementAndGet);
|
||||
ExternalDataProvider provider = enabledProvider("TestBlocks");
|
||||
|
||||
service.registerProvider(provider);
|
||||
assertEquals(1, changes.get());
|
||||
service.notifyContentChanged();
|
||||
assertEquals(2, changes.get());
|
||||
service.onDisable();
|
||||
service.notifyContentChanged();
|
||||
assertEquals(2, changes.get());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void unavailableRegistryDoesNotHideOtherProviderBlocks() {
|
||||
ExternalDataSVC service = new ExternalDataSVC();
|
||||
ExternalDataProvider loading = enabledProvider("LoadingBlocks");
|
||||
when(loading.getTypes(DataType.BLOCK)).thenThrow(new IllegalStateException("Content is still loading"));
|
||||
service.registerProvider(loading);
|
||||
ExternalDataProvider ready = enabledProvider("ReadyBlocks");
|
||||
when(ready.getTypes(DataType.BLOCK)).thenReturn(List.of(Identifier.fromString("example:stone")));
|
||||
service.registerProvider(ready);
|
||||
|
||||
assertEquals(Set.of(Identifier.fromString("example:stone"), Identifier.fromString("readyblocks:example/stone")),
|
||||
Set.copyOf(service.getAllIdentifiers(DataType.BLOCK)));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void delayedRegistryInitializesBeforeItsContentIsReady() {
|
||||
ExternalDataSVC service = new ExternalDataSVC();
|
||||
ExternalDataProvider provider = enabledProvider("DelayedBlocks");
|
||||
when(provider.isReady()).thenReturn(false);
|
||||
when(provider.getTypes(DataType.BLOCK)).thenReturn(List.of());
|
||||
|
||||
service.registerProvider(provider);
|
||||
|
||||
verify(provider).init();
|
||||
assertTrue(service.hasPendingBlockProvider("delayedblocks:example/stone"));
|
||||
assertTrue(service.getAllIdentifiers(DataType.BLOCK).isEmpty());
|
||||
|
||||
Identifier identifier = Identifier.fromString("example:stone");
|
||||
when(provider.isReady()).thenReturn(true);
|
||||
when(provider.getTypes(DataType.BLOCK)).thenReturn(List.of(identifier));
|
||||
service.notifyContentChanged();
|
||||
|
||||
assertFalse(service.hasPendingBlockProvider("delayedblocks:example/stone"));
|
||||
assertEquals(Set.of(identifier, Identifier.fromString("delayedblocks:example/stone")),
|
||||
Set.copyOf(service.getAllIdentifiers(DataType.BLOCK)));
|
||||
}
|
||||
|
||||
private static ExternalDataProvider enabledProvider(String pluginId) {
|
||||
Plugin plugin = mock(Plugin.class);
|
||||
when(plugin.isEnabled()).thenReturn(true);
|
||||
ExternalDataProvider provider = mock(ExternalDataProvider.class);
|
||||
when(provider.getPluginId()).thenReturn(pluginId);
|
||||
when(provider.getPlugin()).thenReturn(plugin);
|
||||
when(provider.isReady()).thenReturn(true);
|
||||
return provider;
|
||||
}
|
||||
}
|
||||
@@ -31,6 +31,7 @@ public class ExternalDataSVCRegistryTest {
|
||||
assertEquals(List.of(
|
||||
"CraftEngine",
|
||||
"Nexo",
|
||||
"Oraxen",
|
||||
"ItemsAdder",
|
||||
"ExecutableItems",
|
||||
"MMOItems",
|
||||
|
||||
@@ -93,6 +93,50 @@ public class EngineShutdownDrainTest {
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void queuedWorldManagerRoutesAreCancelledBeforeRouterDrain() throws Exception {
|
||||
ShutdownFixture fixture = new ShutdownFixture();
|
||||
AtomicBoolean queuedRoute = new AtomicBoolean(true);
|
||||
doAnswer(invocation -> {
|
||||
queuedRoute.set(false);
|
||||
return null;
|
||||
}).when(fixture.manager).close();
|
||||
doAnswer(invocation -> {
|
||||
assertFalse("Router drain must not wait for a queued region callback", queuedRoute.get());
|
||||
return null;
|
||||
}).when(fixture.engine).closeAttachedGenerationHistoryRuntimeRouter();
|
||||
|
||||
try (MockedStatic<NativeStructureOwnershipStore> ownership = mockStatic(NativeStructureOwnershipStore.class)) {
|
||||
fixture.shutdown.close();
|
||||
}
|
||||
|
||||
InOrder shutdownOrder = inOrder(fixture.manager, fixture.engine, fixture.sessions);
|
||||
shutdownOrder.verify(fixture.manager).close();
|
||||
shutdownOrder.verify(fixture.engine).closeAttachedGenerationHistoryRuntimeRouter();
|
||||
shutdownOrder.verify(fixture.sessions).sealAndAwait("close", IrisEngine.SESSION_DRAIN_TIMEOUT_MILLIS, true);
|
||||
verify(fixture.manager).close();
|
||||
assertTrue(fixture.engine.closed);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void failedWorldManagerStopPreventsRouterDrainAndCanBeRetried() throws Exception {
|
||||
ShutdownFixture fixture = new ShutdownFixture();
|
||||
doThrow(new IllegalStateException("World manager still has work")).doNothing().when(fixture.manager).close();
|
||||
|
||||
assertThrows(IllegalStateException.class, fixture.shutdown::close);
|
||||
verify(fixture.engine, never()).closeAttachedGenerationHistoryRuntimeRouter();
|
||||
verify(fixture.mantle, never()).close();
|
||||
assertFalse(fixture.engine.closed);
|
||||
|
||||
try (MockedStatic<NativeStructureOwnershipStore> ownership = mockStatic(NativeStructureOwnershipStore.class)) {
|
||||
fixture.shutdown.close();
|
||||
}
|
||||
|
||||
verify(fixture.manager, times(2)).close();
|
||||
verify(fixture.engine).closeAttachedGenerationHistoryRuntimeRouter();
|
||||
assertTrue(fixture.engine.closed);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void successfulEngineCloseAllowsTheSameWorldToPrepareGenerationAgain() throws Exception {
|
||||
GenerationHistory history = history();
|
||||
|
||||
@@ -7,6 +7,8 @@ import org.bukkit.Chunk;
|
||||
import org.junit.Test;
|
||||
import org.mockito.MockedStatic;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
|
||||
import static org.junit.Assert.assertSame;
|
||||
import static org.junit.Assert.assertThrows;
|
||||
import static org.mockito.Mockito.CALLS_REAL_METHODS;
|
||||
@@ -34,8 +36,11 @@ public class IrisWorldManagerNativePoiTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void chunkLoadPropagatesPoiFailuresToTheRuntimeEventReporter() {
|
||||
public void chunkLoadPropagatesPoiFailuresToTheRuntimeEventReporter() throws Exception {
|
||||
IrisWorldManager manager = mock(IrisWorldManager.class, CALLS_REAL_METHODS);
|
||||
Field maintenance = IrisWorldManager.class.getDeclaredField("chunkMaintenance");
|
||||
maintenance.setAccessible(true);
|
||||
maintenance.set(manager, new WorldChunkMaintenance(manager));
|
||||
Engine engine = mock(Engine.class);
|
||||
Chunk chunk = mock(Chunk.class);
|
||||
INMSBinding binding = mock(INMSBinding.class);
|
||||
|
||||
@@ -35,6 +35,7 @@ import java.util.Optional;
|
||||
import java.util.OptionalInt;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
@@ -58,6 +59,57 @@ import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
public final class ResolvedTerrainProviderTest {
|
||||
@Test
|
||||
public void cachedTransitionTerrainRetainsDeferredBlocksWhenCopied() {
|
||||
Fixture fixture = new Fixture();
|
||||
String key = "itemsadder:rocks/ruby_ore";
|
||||
PlatformBlockState custom = Fixture.block(key, false);
|
||||
when(custom.isCustom()).thenReturn(true);
|
||||
when(custom.placementBaseState()).thenReturn(fixture.stone);
|
||||
when(custom.deferredPlacementKey()).thenReturn(key);
|
||||
TransitionGenerationPlan plan = mock(TransitionGenerationPlan.class);
|
||||
when(plan.hasTransitionAtChunk(0, 0)).thenReturn(true);
|
||||
when(plan.newEpochWeightAt(anyInt(), anyInt())).thenReturn(1D);
|
||||
when(fixture.complex.getTransitionGenerationPlan()).thenReturn(plan);
|
||||
fixture.terrainOverride = (x, z, blocks, biomes, multicore, context) -> {
|
||||
for (int localX = 0; localX < 16; localX++) {
|
||||
for (int localZ = 0; localZ < 16; localZ++) {
|
||||
context.setTerrainHeight(localX, localZ, 2);
|
||||
for (int y = 0; y < 16; y++) {
|
||||
blocks.setRaw(localX, y, localZ, y == 2 ? custom : y < 2 ? fixture.stone : fixture.air);
|
||||
biomes.setRaw(localX, y, localZ, fixture.biome);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
TerrainBoundarySignature captured = fixture.provider.column(0, 0);
|
||||
assertEquals(key, captured.geometry().voxelAt(2).stateKey());
|
||||
IrisPlatform platform = mock(IrisPlatform.class);
|
||||
PlatformRegistries registries = mock(PlatformRegistries.class);
|
||||
when(platform.registries()).thenReturn(registries);
|
||||
when(registries.blockOrNull(key)).thenReturn(custom);
|
||||
when(registries.blockOrNull("minecraft:stone")).thenReturn(fixture.stone);
|
||||
when(registries.blockOrNull("minecraft:air")).thenReturn(fixture.air);
|
||||
when(registries.biome("minecraft:plains")).thenReturn(fixture.biome);
|
||||
ChunkContext context = new ChunkContext(0, 0, fixture.complex, false, ChunkContext.PrefillPlan.NONE, null);
|
||||
AtomicInteger customWrites = new AtomicInteger();
|
||||
Hunk<PlatformBlockState> blocks = Hunk.<PlatformBlockState>newArrayHunk(16, 16, 16).listen((x, y, z, state) -> {
|
||||
if (state.isCustom()) {
|
||||
customWrites.incrementAndGet();
|
||||
}
|
||||
});
|
||||
try (MockedStatic<IrisPlatforms> platforms = mockStatic(IrisPlatforms.class);
|
||||
IrisContext.Scope ignored = IrisContext.open(fixture.engine, 5L, context)) {
|
||||
platforms.when(IrisPlatforms::get).thenReturn(platform);
|
||||
fixture.provider.generate(fixture.mode, 0, 0, blocks, Hunk.newArrayHunk(16, 16, 16), false, context);
|
||||
}
|
||||
|
||||
assertEquals(256, customWrites.get());
|
||||
assertSame(custom, blocks.getRaw(0, 2, 0));
|
||||
assertEquals(key, blocks.getRaw(0, 2, 0).deferredPlacementKey());
|
||||
assertEquals(Integer.valueOf(1), fixture.computations.get("0,0"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void cachedSpeculativeFloatingIdentityIsPublishedOnlyByActualGeneration() throws Exception {
|
||||
Fixture fixture = new Fixture();
|
||||
|
||||
@@ -0,0 +1,266 @@
|
||||
package art.arcane.iris.engine;
|
||||
|
||||
import art.arcane.iris.core.loader.IrisData;
|
||||
import art.arcane.iris.engine.framework.Engine;
|
||||
import art.arcane.iris.engine.framework.EngineTarget;
|
||||
import art.arcane.iris.engine.history.SavedBiomeUnavailableException;
|
||||
import art.arcane.iris.engine.object.IrisBiome;
|
||||
import art.arcane.iris.engine.object.IrisBlockDrops;
|
||||
import art.arcane.iris.engine.object.IrisDimension;
|
||||
import art.arcane.iris.engine.object.IrisRegion;
|
||||
import art.arcane.iris.engine.object.IrisWorld;
|
||||
import art.arcane.iris.engine.platform.EngineBukkitOps;
|
||||
import art.arcane.iris.platform.bukkit.BukkitWorldBinding;
|
||||
import art.arcane.iris.util.common.scheduling.J;
|
||||
import art.arcane.volmlib.util.collection.KList;
|
||||
import org.bukkit.Location;
|
||||
import org.bukkit.World;
|
||||
import org.bukkit.block.Block;
|
||||
import org.bukkit.block.data.BlockData;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.event.block.BlockBreakEvent;
|
||||
import org.bukkit.inventory.ItemStack;
|
||||
import org.junit.Test;
|
||||
import org.mockito.MockedStatic;
|
||||
|
||||
import java.util.ArrayDeque;
|
||||
import java.util.Queue;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertSame;
|
||||
import static org.junit.Assert.assertThrows;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.anyBoolean;
|
||||
import static org.mockito.ArgumentMatchers.anyString;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.doAnswer;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.mockStatic;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
public class WorldBlockDropRouterTest {
|
||||
@Test
|
||||
public void coldBiomeCancelsBeforeChangingDropsOrSchedulingWork() {
|
||||
try (Fixture fixture = new Fixture()) {
|
||||
fixture.queries.when(() -> EngineBukkitOps.getBiome(fixture.engine, fixture.location))
|
||||
.thenThrow(new SavedBiomeUnavailableException("Biome loading", true));
|
||||
BlockBreakEvent event = fixture.event();
|
||||
|
||||
fixture.router.onBlockBreak(event);
|
||||
|
||||
assertTrue(event.isCancelled());
|
||||
assertTrue(event.isDropItems());
|
||||
fixture.assertNoSideEffects();
|
||||
fixture.queries.verify(() -> EngineBukkitOps.getRegion(fixture.engine, fixture.location), never());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void coldRegionCancelsBeforeFillingAlreadyMatchedBiomeDrops() {
|
||||
try (Fixture fixture = new Fixture()) {
|
||||
fixture.queries.when(() -> EngineBukkitOps.getRegion(fixture.engine, fixture.location))
|
||||
.thenThrow(new SavedBiomeUnavailableException("Region loading", true));
|
||||
BlockBreakEvent event = fixture.event();
|
||||
|
||||
fixture.router.onBlockBreak(event);
|
||||
|
||||
verify(fixture.drops).shouldDropFor(fixture.blockData, fixture.data);
|
||||
assertTrue(event.isCancelled());
|
||||
assertTrue(event.isDropItems());
|
||||
fixture.assertNoSideEffects();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void alreadyCancelledBreakSkipsSavedLookups() {
|
||||
try (Fixture fixture = new Fixture()) {
|
||||
BlockBreakEvent event = fixture.event();
|
||||
event.setCancelled(true);
|
||||
|
||||
fixture.router.onBlockBreak(event);
|
||||
|
||||
assertTrue(event.isCancelled());
|
||||
fixture.queries.verifyNoInteractions();
|
||||
fixture.assertNoSideEffects();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void otherWorldBreakSkipsSavedLookups() {
|
||||
try (Fixture fixture = new Fixture()) {
|
||||
when(fixture.block.getWorld()).thenReturn(mock(World.class));
|
||||
BlockBreakEvent event = fixture.event();
|
||||
|
||||
fixture.router.onBlockBreak(event);
|
||||
|
||||
assertFalse(event.isCancelled());
|
||||
fixture.queries.verifyNoInteractions();
|
||||
fixture.assertNoSideEffects();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void freshAttemptAfterLoadingProducesDropsOnceWithoutReplayingTheCancelledEvent() {
|
||||
try (Fixture fixture = new Fixture()) {
|
||||
fixture.queries.when(() -> EngineBukkitOps.getBiome(fixture.engine, fixture.location))
|
||||
.thenThrow(new SavedBiomeUnavailableException("Biome loading", true))
|
||||
.thenReturn(fixture.biome);
|
||||
BlockBreakEvent cold = fixture.event();
|
||||
fixture.router.onBlockBreak(cold);
|
||||
fixture.assertNoSideEffects();
|
||||
|
||||
BlockBreakEvent retry = fixture.event();
|
||||
fixture.router.onBlockBreak(retry);
|
||||
|
||||
assertTrue(cold.isCancelled());
|
||||
assertTrue(cold.isDropItems());
|
||||
assertFalse(retry.isCancelled());
|
||||
assertFalse(retry.isDropItems());
|
||||
assertEquals(1, fixture.delayed.size());
|
||||
verify(fixture.drops).fillDrops(eq(false), any());
|
||||
fixture.delayed.remove().run();
|
||||
|
||||
verify(fixture.world).dropItemNaturally(fixture.location.clone().add(.5, .5, .5), fixture.item);
|
||||
assertEquals(1, fixture.markers.size());
|
||||
assertTrue(fixture.delayed.isEmpty());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void permanentSavedBiomeFailureCancelsAndPropagates() {
|
||||
verifyReportedFailure(new SavedBiomeUnavailableException("Saved biome missing", false));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void loadingWithSuppressedFailureCancelsAndPropagates() {
|
||||
SavedBiomeUnavailableException failure = new SavedBiomeUnavailableException("Biome loading", true);
|
||||
failure.addSuppressed(new IllegalStateException("Saved storage cleanup failed"));
|
||||
verifyReportedFailure(failure);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void protectionProbeCancelledAfterDispatchProducesNoFinalDropsOrMarkerCleanup() {
|
||||
try (Fixture fixture = new Fixture()) {
|
||||
BlockBreakEvent probe = new ProtectionProbe(fixture.block, fixture.player);
|
||||
probe.setDropItems(false);
|
||||
|
||||
fixture.router.onBlockBreak(probe);
|
||||
assertFalse(probe.isCancelled());
|
||||
assertFalse(probe.isDropItems());
|
||||
assertEquals(1, fixture.delayed.size());
|
||||
probe.setCancelled(true);
|
||||
fixture.delayed.remove().run();
|
||||
|
||||
verify(fixture.world, never()).dropItemNaturally(any(Location.class), any(ItemStack.class));
|
||||
assertTrue(fixture.markers.isEmpty());
|
||||
verify(fixture.manager, never()).getMantle();
|
||||
}
|
||||
}
|
||||
|
||||
private void verifyReportedFailure(SavedBiomeUnavailableException failure) {
|
||||
try (Fixture fixture = new Fixture()) {
|
||||
fixture.queries.when(() -> EngineBukkitOps.getBiome(fixture.engine, fixture.location)).thenThrow(failure);
|
||||
BlockBreakEvent event = fixture.event();
|
||||
|
||||
assertSame(failure, assertThrows(SavedBiomeUnavailableException.class,
|
||||
() -> fixture.router.onBlockBreak(event)));
|
||||
|
||||
assertTrue(event.isCancelled());
|
||||
assertTrue(event.isDropItems());
|
||||
fixture.assertNoSideEffects();
|
||||
}
|
||||
}
|
||||
|
||||
private static final class ProtectionProbe extends BlockBreakEvent {
|
||||
private ProtectionProbe(Block block, Player player) {
|
||||
super(block, player);
|
||||
}
|
||||
}
|
||||
|
||||
private static final class Fixture implements AutoCloseable {
|
||||
private final IrisWorldManager manager = mock(IrisWorldManager.class);
|
||||
private final Engine engine = mock(Engine.class);
|
||||
private final IrisData data = mock(IrisData.class);
|
||||
private final World world = mock(World.class);
|
||||
private final Block block = mock(Block.class);
|
||||
private final BlockData blockData = mock(BlockData.class);
|
||||
private final Player player = mock(Player.class);
|
||||
private final Location location = new Location(world, 16, 72, 32);
|
||||
private final IrisBiome biome = mock(IrisBiome.class);
|
||||
private final IrisBlockDrops drops = mock(IrisBlockDrops.class);
|
||||
private final ItemStack item = mock(ItemStack.class);
|
||||
private final WorldBlockDropRouter router = new WorldBlockDropRouter(manager);
|
||||
private final Queue<Runnable> delayed = new ArrayDeque<>();
|
||||
private final Queue<Runnable> markers = new ArrayDeque<>();
|
||||
private final MockedStatic<BukkitWorldBinding> binding = mockStatic(BukkitWorldBinding.class);
|
||||
private final MockedStatic<EngineBukkitOps> queries = mockStatic(EngineBukkitOps.class);
|
||||
private final MockedStatic<J> scheduling = mockStatic(J.class);
|
||||
|
||||
private Fixture() {
|
||||
IrisWorld irisWorld = mock(IrisWorld.class);
|
||||
EngineTarget target = mock(EngineTarget.class);
|
||||
IrisRegion region = mock(IrisRegion.class);
|
||||
IrisDimension dimension = mock(IrisDimension.class);
|
||||
when(manager.getEngine()).thenReturn(engine);
|
||||
when(manager.getTarget()).thenReturn(target);
|
||||
when(manager.getData()).thenReturn(data);
|
||||
when(target.getWorld()).thenReturn(irisWorld);
|
||||
when(engine.getWorld()).thenReturn(irisWorld);
|
||||
when(irisWorld.minHeight()).thenReturn(-64);
|
||||
when(engine.getDimension()).thenReturn(dimension);
|
||||
binding.when(() -> BukkitWorldBinding.world(irisWorld)).thenReturn(world);
|
||||
when(block.getWorld()).thenReturn(world);
|
||||
when(block.getX()).thenReturn(16);
|
||||
when(block.getY()).thenReturn(72);
|
||||
when(block.getZ()).thenReturn(32);
|
||||
when(block.getLocation()).thenReturn(location);
|
||||
when(block.getBlockData()).thenReturn(blockData);
|
||||
when(biome.getBlockDrops()).thenReturn(new KList<>(drops));
|
||||
when(region.getBlockDrops()).thenReturn(new KList<>());
|
||||
when(dimension.getBlockDrops()).thenReturn(new KList<>());
|
||||
when(drops.shouldDropFor(blockData, data)).thenReturn(true);
|
||||
when(drops.isReplaceVanillaDrops()).thenReturn(true);
|
||||
doAnswer(invocation -> {
|
||||
KList<ItemStack> results = invocation.getArgument(1);
|
||||
results.add(item);
|
||||
return null;
|
||||
}).when(drops).fillDrops(eq(false), any());
|
||||
queries.when(() -> EngineBukkitOps.getBiome(engine, location)).thenReturn(biome);
|
||||
queries.when(() -> EngineBukkitOps.getRegion(engine, location)).thenReturn(region);
|
||||
when(manager.managedTask(anyString(), any(Runnable.class)))
|
||||
.thenAnswer(invocation -> invocation.getArgument(1));
|
||||
scheduling.when(() -> J.runAt(eq(location), any(Runnable.class), eq(1))).thenAnswer(invocation -> {
|
||||
delayed.add(invocation.getArgument(1));
|
||||
return true;
|
||||
});
|
||||
scheduling.when(() -> J.a(any(Runnable.class))).thenAnswer(invocation -> {
|
||||
markers.add(invocation.getArgument(0));
|
||||
return null;
|
||||
});
|
||||
}
|
||||
|
||||
private BlockBreakEvent event() {
|
||||
return new BlockBreakEvent(block, player);
|
||||
}
|
||||
|
||||
private void assertNoSideEffects() {
|
||||
verify(drops, never()).fillDrops(anyBoolean(), any());
|
||||
verify(world, never()).dropItemNaturally(any(Location.class), any(ItemStack.class));
|
||||
verify(manager, never()).getMantle();
|
||||
scheduling.verifyNoInteractions();
|
||||
assertTrue(delayed.isEmpty());
|
||||
assertTrue(markers.isEmpty());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
scheduling.close();
|
||||
queries.close();
|
||||
binding.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,391 @@
|
||||
package art.arcane.iris.engine;
|
||||
|
||||
import art.arcane.iris.core.IrisSettings;
|
||||
import art.arcane.iris.engine.history.GenerationHistoryRuntimeRouter;
|
||||
import art.arcane.iris.engine.history.GenerationHistoryRuntimeRouter.SavedChunkMantle;
|
||||
import art.arcane.iris.engine.mantle.EngineMantle;
|
||||
import art.arcane.iris.engine.platform.EngineBukkitOps;
|
||||
import art.arcane.iris.spi.IrisLogging;
|
||||
import art.arcane.iris.spi.IrisPlatform;
|
||||
import art.arcane.iris.spi.IrisPlatforms;
|
||||
import art.arcane.iris.spi.PlatformBlockState;
|
||||
import art.arcane.iris.spi.PlatformRegistries;
|
||||
import art.arcane.iris.util.common.scheduling.J;
|
||||
import art.arcane.volmlib.util.mantle.flag.MantleFlag;
|
||||
import art.arcane.volmlib.util.mantle.runtime.Mantle;
|
||||
import art.arcane.volmlib.util.mantle.runtime.MantleChunk;
|
||||
import art.arcane.volmlib.util.matter.Matter;
|
||||
import org.bukkit.Chunk;
|
||||
import org.bukkit.World;
|
||||
import org.junit.Test;
|
||||
import org.junit.AfterClass;
|
||||
import org.junit.BeforeClass;
|
||||
import org.mockito.InOrder;
|
||||
import org.mockito.MockedStatic;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import java.util.ArrayDeque;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.Queue;
|
||||
import java.util.concurrent.Semaphore;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertNotSame;
|
||||
import static org.junit.Assert.assertSame;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.anyInt;
|
||||
import static org.mockito.ArgumentMatchers.anyString;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.doAnswer;
|
||||
import static org.mockito.Mockito.inOrder;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.mockStatic;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.times;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
public class WorldChunkMaintenanceRoutingTest {
|
||||
private static IrisPlatform previousPlatform;
|
||||
|
||||
@BeforeClass
|
||||
public static void bindPlatform() {
|
||||
previousPlatform = IrisPlatforms.isBound() ? IrisPlatforms.get() : null;
|
||||
IrisPlatforms.unbind();
|
||||
IrisPlatform platform = mock(IrisPlatform.class);
|
||||
PlatformRegistries registries = mock(PlatformRegistries.class);
|
||||
when(platform.registries()).thenReturn(registries);
|
||||
when(registries.block(anyString())).thenReturn(mock(PlatformBlockState.class));
|
||||
IrisPlatforms.bind(platform);
|
||||
}
|
||||
|
||||
@AfterClass
|
||||
public static void restorePlatform() {
|
||||
IrisPlatforms.unbind();
|
||||
if (previousPlatform != null) {
|
||||
IrisPlatforms.bind(previousPlatform);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void materializesTheSavedOwnerAndKeepsSpawningAfterCompletion() throws Exception {
|
||||
try (Fixture fixture = new Fixture()) {
|
||||
fixture.schedule();
|
||||
verify(fixture.currentMantle, never()).getChunk(2, -1);
|
||||
fixture.prepare();
|
||||
|
||||
verify(fixture.savedChunk).use();
|
||||
verify(fixture.saved).detachThread();
|
||||
verify(fixture.saved, never()).close();
|
||||
verify(fixture.savedChunk, never()).release();
|
||||
|
||||
fixture.apply();
|
||||
fixture.awaitRelease();
|
||||
|
||||
fixture.updates.verify(() -> EngineBukkitOps.updateChunk(fixture.engine, fixture.chunk, fixture.savedMantle));
|
||||
InOrder lifetime = inOrder(fixture.savedChunk, fixture.saved);
|
||||
lifetime.verify(fixture.savedChunk).use();
|
||||
lifetime.verify(fixture.saved).detachThread();
|
||||
lifetime.verify(fixture.savedChunk).release();
|
||||
lifetime.verify(fixture.saved).close();
|
||||
assertNotSame(Thread.currentThread(), fixture.releaseThread.get());
|
||||
assertFalse(fixture.scoped.get());
|
||||
|
||||
fixture.schedule();
|
||||
assertTrue(fixture.async.isEmpty());
|
||||
verify(fixture.router).openSavedChunkMantle(2, -1);
|
||||
verify(fixture.spawner, times(2)).spawnInitially(fixture.chunk);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void managerCloseReleasesQueuedStorageWithoutRunningItsRegionCallback() throws Exception {
|
||||
try (Fixture fixture = new Fixture()) {
|
||||
fixture.schedule();
|
||||
fixture.prepare();
|
||||
|
||||
fixture.maintenance.close();
|
||||
fixture.awaitRelease();
|
||||
fixture.apply();
|
||||
fixture.schedule();
|
||||
|
||||
fixture.updates.verifyNoInteractions();
|
||||
verify(fixture.saved).close();
|
||||
verify(fixture.spawner, never()).spawnInitially(fixture.chunk);
|
||||
assertTrue(fixture.async.isEmpty());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void managerCloseDuringPreparationReleasesLocallyHeldStorage() throws Exception {
|
||||
try (Fixture fixture = new Fixture()) {
|
||||
when(fixture.savedMantle.getChunk(2, -1)).thenAnswer(invocation -> {
|
||||
fixture.maintenance.close();
|
||||
return fixture.savedChunk;
|
||||
});
|
||||
fixture.schedule();
|
||||
|
||||
fixture.prepare();
|
||||
fixture.awaitRelease();
|
||||
|
||||
fixture.updates.verifyNoInteractions();
|
||||
assertTrue(fixture.regions.isEmpty());
|
||||
verify(fixture.savedChunk).release();
|
||||
verify(fixture.saved).close();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void rejectedRegionSchedulingReleasesStorageAndAllowsRetry() throws Exception {
|
||||
try (Fixture fixture = new Fixture()) {
|
||||
fixture.acceptRegion.set(false);
|
||||
fixture.schedule();
|
||||
fixture.prepare();
|
||||
fixture.awaitRelease();
|
||||
|
||||
fixture.acceptRegion.set(true);
|
||||
fixture.schedule();
|
||||
assertEquals(1, fixture.async.size());
|
||||
fixture.updates.verifyNoInteractions();
|
||||
verify(fixture.savedChunk).release();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void rejectedLifecycleAdmissionReleasesPreparedStorage() throws Exception {
|
||||
try (Fixture fixture = new Fixture()) {
|
||||
fixture.schedule();
|
||||
fixture.prepare();
|
||||
fixture.admitted.set(false);
|
||||
|
||||
fixture.apply();
|
||||
fixture.awaitRelease();
|
||||
|
||||
fixture.updates.verifyNoInteractions();
|
||||
verify(fixture.saved).close();
|
||||
verify(fixture.savedChunk).release();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void unloadedChunkDoesNotBecomeCompleted() throws Exception {
|
||||
try (Fixture fixture = new Fixture()) {
|
||||
fixture.schedule();
|
||||
fixture.prepare();
|
||||
when(fixture.world.isChunkLoaded(2, -1)).thenReturn(false);
|
||||
|
||||
fixture.apply();
|
||||
fixture.awaitRelease();
|
||||
|
||||
when(fixture.world.isChunkLoaded(2, -1)).thenReturn(true);
|
||||
fixture.schedule();
|
||||
assertEquals(1, fixture.async.size());
|
||||
fixture.updates.verifyNoInteractions();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void failedMaterializationRetainsRetryAndReportsTheFailure() throws Exception {
|
||||
try (Fixture fixture = new Fixture()) {
|
||||
IllegalStateException failure = new IllegalStateException("Provider placement failed");
|
||||
fixture.updates.when(() -> EngineBukkitOps.updateChunk(fixture.engine, fixture.chunk, fixture.savedMantle)).thenThrow(failure);
|
||||
fixture.schedule();
|
||||
fixture.prepare();
|
||||
|
||||
fixture.apply();
|
||||
fixture.awaitRelease();
|
||||
|
||||
fixture.logging.verify(() -> IrisLogging.reportError(anyString(), eq(failure)));
|
||||
fixture.schedule();
|
||||
assertEquals(1, fixture.async.size());
|
||||
verify(fixture.spawner, never()).spawnInitially(fixture.chunk);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void skippedMaterializationRetainsRetryWithoutSuppressingSpawning() throws Exception {
|
||||
try (Fixture fixture = new Fixture()) {
|
||||
fixture.updates.when(() -> EngineBukkitOps.updateChunk(fixture.engine, fixture.chunk, fixture.savedMantle))
|
||||
.thenAnswer(invocation -> null);
|
||||
fixture.schedule();
|
||||
fixture.prepare();
|
||||
|
||||
fixture.apply();
|
||||
fixture.awaitRelease();
|
||||
|
||||
fixture.schedule();
|
||||
assertEquals(1, fixture.async.size());
|
||||
verify(fixture.spawner).spawnInitially(fixture.chunk);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void regeneratedChunkInvalidatesCompletedMaterialization() throws Exception {
|
||||
try (Fixture fixture = new Fixture()) {
|
||||
fixture.schedule();
|
||||
fixture.prepare();
|
||||
fixture.apply();
|
||||
fixture.awaitRelease();
|
||||
|
||||
fixture.maintenance.invalidateMaterialization(2, -1);
|
||||
fixture.schedule();
|
||||
|
||||
assertEquals(1, fixture.async.size());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void pendingMaterializationIsBoundedAndCancelledBeforePreparation() throws Exception {
|
||||
try (Fixture fixture = new Fixture()) {
|
||||
for (int x = 0; x < 256; x++) {
|
||||
fixture.maintenance.updateChunkRegion(fixture.world, x, 0);
|
||||
}
|
||||
assertEquals(128, fixture.async.size());
|
||||
|
||||
fixture.maintenance.close();
|
||||
while (!fixture.async.isEmpty()) {
|
||||
fixture.prepare();
|
||||
}
|
||||
|
||||
verify(fixture.router, never()).openSavedChunkMantle(anyInt(), anyInt());
|
||||
}
|
||||
}
|
||||
|
||||
private static final class Fixture implements AutoCloseable {
|
||||
private final IrisEngine engine = mock(IrisEngine.class);
|
||||
private final IrisWorldManager manager = mock(IrisWorldManager.class);
|
||||
private final GenerationHistoryRuntimeRouter router = mock(GenerationHistoryRuntimeRouter.class);
|
||||
private final SavedChunkMantle saved = mock(SavedChunkMantle.class);
|
||||
private final SavedChunkMantle.Scope scope = mock(SavedChunkMantle.Scope.class);
|
||||
private final EngineMantle currentEngineMantle = mock(EngineMantle.class);
|
||||
private final Mantle<Matter> currentMantle;
|
||||
private final Mantle<Matter> savedMantle;
|
||||
private final MantleChunk<Matter> savedChunk;
|
||||
private final WorldEntitySpawner spawner = mock(WorldEntitySpawner.class);
|
||||
private final World world = mock(World.class);
|
||||
private final Chunk chunk = mock(Chunk.class);
|
||||
private final WorldChunkMaintenance maintenance = new WorldChunkMaintenance(manager);
|
||||
private final Map<?, ?> pending;
|
||||
private final Queue<Runnable> async = new ArrayDeque<>();
|
||||
private final Queue<Runnable> regions = new ArrayDeque<>();
|
||||
private final AtomicBoolean etched = new AtomicBoolean();
|
||||
private final AtomicBoolean scoped = new AtomicBoolean();
|
||||
private final AtomicBoolean acceptRegion = new AtomicBoolean(true);
|
||||
private final AtomicBoolean admitted = new AtomicBoolean(true);
|
||||
private final AtomicReference<Thread> releaseThread = new AtomicReference<>();
|
||||
private final Semaphore released = new Semaphore(0);
|
||||
private final MockedStatic<IrisSettings> configured = mockStatic(IrisSettings.class);
|
||||
private final MockedStatic<J> scheduling = mockStatic(J.class);
|
||||
private final MockedStatic<EngineBukkitOps> updates = mockStatic(EngineBukkitOps.class);
|
||||
private final MockedStatic<IrisLogging> logging = mockStatic(IrisLogging.class);
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private Fixture() throws Exception {
|
||||
currentMantle = mock(Mantle.class);
|
||||
savedMantle = mock(Mantle.class);
|
||||
savedChunk = mock(MantleChunk.class);
|
||||
Field pendingField = WorldChunkMaintenance.class.getDeclaredField("materializations");
|
||||
pendingField.setAccessible(true);
|
||||
pending = (Map<?, ?>) pendingField.get(maintenance);
|
||||
Field field = IrisWorldManager.class.getDeclaredField("entitySpawner");
|
||||
field.setAccessible(true);
|
||||
field.set(manager, spawner);
|
||||
when(manager.getEngine()).thenReturn(engine);
|
||||
when(engine.getGenerationHistoryRuntimeRouter()).thenReturn(Optional.of(router));
|
||||
when(manager.getMantle()).thenReturn(currentMantle);
|
||||
when(engine.getMantle()).thenReturn(currentEngineMantle);
|
||||
when(currentEngineMantle.getMantle()).thenReturn(currentMantle);
|
||||
when(saved.mantle()).thenReturn(savedMantle);
|
||||
when(savedMantle.getChunk(2, -1)).thenReturn(savedChunk);
|
||||
when(savedChunk.use()).thenReturn(savedChunk);
|
||||
when(savedMantle.isChunkLoaded(2, -1)).thenReturn(true);
|
||||
when(savedMantle.hasFlag(2, -1, MantleFlag.ETCHED)).thenAnswer(invocation -> etched.get());
|
||||
when(router.openSavedChunkMantle(2, -1)).thenReturn(saved);
|
||||
when(saved.openScope()).thenAnswer(invocation -> {
|
||||
assertFalse(scoped.getAndSet(true));
|
||||
return scope;
|
||||
});
|
||||
doAnswer(invocation -> {
|
||||
assertTrue(scoped.getAndSet(false));
|
||||
return null;
|
||||
}).when(scope).close();
|
||||
doAnswer(invocation -> {
|
||||
releaseThread.set(Thread.currentThread());
|
||||
released.release();
|
||||
return null;
|
||||
}).when(saved).close();
|
||||
when(world.isChunkLoaded(anyInt(), anyInt())).thenReturn(true);
|
||||
when(world.getChunkAt(anyInt(), anyInt())).thenReturn(chunk);
|
||||
when(spawner.isEntitySpawningEnabledForCurrentWorld()).thenReturn(true);
|
||||
when(manager.managedTask(anyString(), any(Runnable.class), any(Runnable.class)))
|
||||
.thenAnswer(invocation -> {
|
||||
Runnable task = invocation.getArgument(1);
|
||||
Runnable rejected = invocation.getArgument(2);
|
||||
return (Runnable) () -> {
|
||||
if (admitted.get()) {
|
||||
task.run();
|
||||
} else {
|
||||
rejected.run();
|
||||
}
|
||||
};
|
||||
});
|
||||
IrisSettings settings = new IrisSettings();
|
||||
settings.getWorld().setPostLoadBlockUpdates(true);
|
||||
configured.when(IrisSettings::get).thenReturn(settings);
|
||||
scheduling.when(() -> J.a(any(Runnable.class))).thenAnswer(invocation -> {
|
||||
async.add(invocation.getArgument(0));
|
||||
return null;
|
||||
});
|
||||
scheduling.when(() -> J.runRegion(eq(world), eq(2), eq(-1), any(Runnable.class)))
|
||||
.thenAnswer(invocation -> {
|
||||
if (!acceptRegion.get()) {
|
||||
return false;
|
||||
}
|
||||
regions.add(invocation.getArgument(3));
|
||||
return true;
|
||||
});
|
||||
updates.when(() -> EngineBukkitOps.updateChunk(engine, chunk, savedMantle)).thenAnswer(invocation -> {
|
||||
assertTrue(scoped.get());
|
||||
assertSame(currentMantle, engine.getMantle().getMantle());
|
||||
etched.set(true);
|
||||
return null;
|
||||
});
|
||||
}
|
||||
|
||||
private void schedule() {
|
||||
maintenance.updateChunkRegion(world, 2, -1);
|
||||
}
|
||||
|
||||
private void prepare() {
|
||||
async.remove().run();
|
||||
}
|
||||
|
||||
private void apply() {
|
||||
regions.remove().run();
|
||||
}
|
||||
|
||||
private void awaitRelease() throws InterruptedException {
|
||||
assertTrue("Saved mantle handle was not released", released.tryAcquire(5L, TimeUnit.SECONDS));
|
||||
long deadline = System.nanoTime() + TimeUnit.SECONDS.toNanos(5L);
|
||||
while (!pending.isEmpty() && System.nanoTime() < deadline) {
|
||||
Thread.sleep(1L);
|
||||
}
|
||||
assertTrue("Saved mantle cleanup did not finish", pending.isEmpty());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
maintenance.close();
|
||||
logging.close();
|
||||
updates.close();
|
||||
scheduling.close();
|
||||
configured.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -73,7 +73,7 @@ public class WorldChunkMaintenanceSavedBiomeTest {
|
||||
try (MockedStatic<IrisSettings> configured = mockStatic(IrisSettings.class);
|
||||
MockedStatic<EngineBukkitOps> updates = mockStatic(EngineBukkitOps.class)) {
|
||||
configured.when(IrisSettings::get).thenReturn(settings);
|
||||
updates.when(() -> EngineBukkitOps.updateChunk(engine, chunk))
|
||||
updates.when(() -> EngineBukkitOps.updateChunk(engine, chunk, mantle))
|
||||
.thenThrow(failure).thenAnswer(invocation -> null);
|
||||
|
||||
if (loading) {
|
||||
|
||||
+32
@@ -4,6 +4,8 @@ import art.arcane.iris.engine.IrisComplex;
|
||||
import art.arcane.iris.engine.framework.Engine;
|
||||
import art.arcane.iris.engine.history.BoundaryColumnGeometry;
|
||||
import art.arcane.iris.engine.history.BoundaryGeometryInfluence;
|
||||
import art.arcane.iris.engine.history.NativeTerrainReceipt;
|
||||
import art.arcane.iris.engine.history.SavedTerrainChunk;
|
||||
import art.arcane.iris.engine.history.TransitionGenerationPlan;
|
||||
import art.arcane.iris.spi.IrisPlatform;
|
||||
import art.arcane.iris.spi.IrisPlatforms;
|
||||
@@ -20,6 +22,8 @@ import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.Assert.assertSame;
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.junit.Assert.assertThrows;
|
||||
import static org.mockito.ArgumentMatchers.anyInt;
|
||||
import static org.mockito.Mockito.mock;
|
||||
@@ -55,6 +59,34 @@ public class IrisTransitionGeometryActuatorTest {
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void boundaryReceiptRetainsCustomKeyAndCarrierPhysics() throws Exception {
|
||||
String key = "itemsadder:rocks/ruby_ore";
|
||||
PlatformBlockState carrier = state("minecraft:oak_slab[type=bottom,waterlogged=true]", false);
|
||||
when(carrier.isWaterLogged()).thenReturn(true);
|
||||
PlatformBlockState custom = state(key, false);
|
||||
when(custom.isCustom()).thenReturn(true);
|
||||
when(custom.placementBaseState()).thenReturn(carrier);
|
||||
Hunk<PlatformBlockState> blocks = filled();
|
||||
blocks.setRaw(0, 3, 0, custom);
|
||||
Hunk<PlatformBiome> biomes = Hunk.newArrayHunk(16, 16, 16);
|
||||
PlatformBiome biome = mock(PlatformBiome.class);
|
||||
when(biome.key()).thenReturn("minecraft:plains");
|
||||
biomes.fill(biome);
|
||||
ChunkContext context = mock(ChunkContext.class);
|
||||
when(context.getRoundedHeight(anyInt(), anyInt())).thenReturn(3);
|
||||
|
||||
SavedTerrainChunk captured = IrisTransitionGeometryActuator.capture(0, 0, blocks, biomes, 0, context, true);
|
||||
byte[] encoded = NativeTerrainReceipt.encode(captured, 7, "epoch");
|
||||
SavedTerrainChunk restored = NativeTerrainReceipt.decode(encoded, "minecraft:noise").terrain();
|
||||
BoundaryColumnGeometry.Voxel voxel = restored.column(0, 0).geometry().voxelAt(3);
|
||||
|
||||
assertEquals(key, voxel.stateKey());
|
||||
assertEquals(BoundaryColumnGeometry.Phase.SOLID, voxel.phase());
|
||||
assertEquals("minecraft:water[level=0]", voxel.fluidStateKey());
|
||||
assertTrue(voxel.protectedContent());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void appliesCaveAndIslandGeometryThenUpdatesDecorationHeight() {
|
||||
Hunk<PlatformBlockState> blocks = filled();
|
||||
|
||||
+136
@@ -1,7 +1,9 @@
|
||||
package art.arcane.iris.engine.history;
|
||||
|
||||
import art.arcane.iris.core.loader.IrisData;
|
||||
import art.arcane.iris.core.pack.AtomicDirectoryPublisher;
|
||||
import art.arcane.iris.engine.IrisEngine;
|
||||
import art.arcane.iris.engine.IrisEngineMantle;
|
||||
import art.arcane.iris.engine.IrisComplex;
|
||||
import art.arcane.iris.engine.object.IrisBiome;
|
||||
import art.arcane.iris.engine.object.IrisRegion;
|
||||
@@ -17,6 +19,7 @@ import org.junit.Test;
|
||||
import org.mockito.MockedStatic;
|
||||
import org.junit.rules.TemporaryFolder;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
@@ -44,6 +47,7 @@ import static org.junit.Assert.assertThrows;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.anyInt;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.doAnswer;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.mockStatic;
|
||||
@@ -180,6 +184,138 @@ public final class GenerationHistoryRuntimeRouterTest {
|
||||
router.close();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void savedChunkMantlesShareRecordedStorageWithoutLoadingHistoricalRuntimes() throws Exception {
|
||||
Path world = temporaryFolder.newFolder("router-saved-chunk-world").toPath();
|
||||
GenerationHistory history = createThreeActivationHistory(world, "router-saved-chunk");
|
||||
int recordedChunks = history.explicitChunkCount();
|
||||
IrisEngine engine = mock(IrisEngine.class);
|
||||
FakeRuntimeFactory runtimes = new FakeRuntimeFactory();
|
||||
IrisEngine.GenerationRuntimeBinding active = runtimes.binding(history, history.activeActivation());
|
||||
when(engine.getActiveGenerationRuntimeBinding()).thenReturn(active);
|
||||
AtomicReference<IrisEngine.GenerationRuntimeBinding> scoped = installScopeTracking(engine);
|
||||
EngineMantle activeEngineMantle = mock(EngineMantle.class);
|
||||
Mantle<Matter> activeMantle = mock(Mantle.class);
|
||||
when(engine.getMantle()).thenReturn(activeEngineMantle);
|
||||
when(activeEngineMantle.getMantle()).thenReturn(activeMantle);
|
||||
Map<File, IrisData> dataByPack = new HashMap<>();
|
||||
Map<Path, Mantle<Matter>> storageByPath = new HashMap<>();
|
||||
for (long activationId : List.of(1L, 2L)) {
|
||||
dataByPack.put(history.packRoot(activationId).toFile(), mock(IrisData.class));
|
||||
storageByPath.put(history.paths().activationMantleRoot(activationId), mock(Mantle.class));
|
||||
}
|
||||
|
||||
try (MockedStatic<IrisData> data = mockStatic(IrisData.class);
|
||||
MockedStatic<IrisEngineMantle> storage = mockStatic(IrisEngineMantle.class);
|
||||
GenerationHistoryRuntimeRouter router = GenerationHistoryRuntimeRouter.attach(
|
||||
engine, history, (ignored, x, z) -> signature(x, z), runtimes)) {
|
||||
data.when(() -> IrisData.openRuntime(any(File.class)))
|
||||
.thenAnswer(invocation -> dataByPack.get(invocation.getArgument(0, File.class)));
|
||||
storage.when(() -> IrisEngineMantle.createMantle(any(), any(), any()))
|
||||
.thenAnswer(invocation -> storageByPath.get(invocation.getArgument(1, Path.class)));
|
||||
for (int chunkX = 0; chunkX < 3; chunkX++) {
|
||||
long activationId = chunkX + 1L;
|
||||
Mantle<Matter> expected = activationId == 3L ? activeMantle
|
||||
: storageByPath.get(history.paths().activationMantleRoot(activationId));
|
||||
try (GenerationHistoryRuntimeRouter.SavedChunkMantle first = router.openSavedChunkMantle(chunkX, 0)) {
|
||||
assertSame(expected, first.mantle());
|
||||
assertNull(scoped.get());
|
||||
try (GenerationHistoryRuntimeRouter.SavedChunkMantle second = router.openSavedChunkMantle(chunkX, 0)) {
|
||||
assertSame(first.mantle(), second.mantle());
|
||||
}
|
||||
verify(expected, never()).close();
|
||||
}
|
||||
if (activationId != 3L) {
|
||||
verify(expected).close();
|
||||
IrisData savedData = dataByPack.get(history.packRoot(activationId).toFile());
|
||||
verify(savedData).bindGenerationRegistryContract(history.resolveEpoch(chunkX, 0).registryContract());
|
||||
verify(savedData).registerEngine(engine);
|
||||
verify(savedData).unregisterEngine(engine);
|
||||
verify(savedData).close();
|
||||
}
|
||||
try (GenerationHistoryRuntimeRouter.RuntimeStage stage = router.openStage(chunkX, 0)) {
|
||||
assertEquals(3L, stage.activation().activationId());
|
||||
assertSame(active, scoped.get());
|
||||
}
|
||||
assertTrue(history.semantics(chunkX, 0).isEmpty());
|
||||
}
|
||||
storage.verify(() -> IrisEngineMantle.createMantle(any(), any(), any()), times(2));
|
||||
assertEquals(recordedChunks, history.explicitChunkCount());
|
||||
assertEquals(3L, history.activeActivation().activationId());
|
||||
assertEquals(1L, history.resolveActivation(0, 0).activationId());
|
||||
assertEquals(2L, history.resolveActivation(1, 0).activationId());
|
||||
assertEquals(Set.of(3L), runtimes.bindings.keySet());
|
||||
verify(activeMantle, never()).close();
|
||||
verify(engine, never()).setDefaultGenerationRuntime(any());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void savedChunkMantleOpensAfterItsGeneratorKernelIsNoLongerAvailable() throws Exception {
|
||||
Path world = temporaryFolder.newFolder("router-saved-old-kernel-world").toPath();
|
||||
Path pack = createPack("router-saved-old-kernel-pack", "alpha");
|
||||
GenerationKernelRegistry.Version oldVersion = new GenerationKernelRegistry.Version(1, 1, 1);
|
||||
GenerationKernelRegistry.Version currentVersion = new GenerationKernelRegistry.Version(2, 1, 1);
|
||||
GenerationKernelRegistry kernels = kernels(currentVersion);
|
||||
createHistory(world, pack, oldVersion, kernels);
|
||||
writeRegion(Files.createDirectories(world.resolve("region")).resolve("r.0.0.mca"), new int[][]{{0, 0}});
|
||||
GenerationHistory history = GenerationHistory.open(world,
|
||||
new GenerationKernelRegistry(currentVersion, List.of(kernels.requireSupported(currentVersion))));
|
||||
history.stageCurrentKernel(256);
|
||||
promoteWithSignatures(history);
|
||||
assertEquals(oldVersion, history.resolveEpoch(0, 0).kernelVersion());
|
||||
IrisEngine engine = mock(IrisEngine.class);
|
||||
FakeRuntimeFactory runtimes = new FakeRuntimeFactory();
|
||||
IrisEngine.GenerationRuntimeBinding active = runtimes.binding(history, history.activeActivation());
|
||||
when(engine.getActiveGenerationRuntimeBinding()).thenReturn(active);
|
||||
IrisData savedData = mock(IrisData.class);
|
||||
Mantle<Matter> savedStorage = mock(Mantle.class);
|
||||
try (MockedStatic<IrisData> data = mockStatic(IrisData.class);
|
||||
MockedStatic<IrisEngineMantle> storage = mockStatic(IrisEngineMantle.class);
|
||||
GenerationHistoryRuntimeRouter router = GenerationHistoryRuntimeRouter.attach(
|
||||
engine, history, (ignored, x, z) -> signature(x, z), runtimes)) {
|
||||
data.when(() -> IrisData.openRuntime(history.packRoot(1L).toFile())).thenReturn(savedData);
|
||||
storage.when(() -> IrisEngineMantle.createMantle(any(), any(), any())).thenReturn(savedStorage);
|
||||
try (GenerationHistoryRuntimeRouter.SavedChunkMantle saved = router.openSavedChunkMantle(0, 0)) {
|
||||
assertSame(savedStorage, saved.mantle());
|
||||
}
|
||||
verify(savedStorage).close();
|
||||
storage.verify(() -> IrisEngineMantle.createMantle(
|
||||
any(), eq(history.paths().activationMantleRoot(1L)), any()));
|
||||
assertEquals(Set.of(2L), runtimes.bindings.keySet());
|
||||
assertEquals(0, runtimes.loadCount(1L));
|
||||
verify(engine, never()).openGenerationRuntimeScope(any());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void savedChunkScopeRejectsReentrantCloseAndCutoverAfterThreadHandoff() throws Exception {
|
||||
Path world = temporaryFolder.newFolder("router-saved-scope-world").toPath();
|
||||
GenerationHistory history = createHistory(world, createPack("router-saved-scope-pack", "alpha"));
|
||||
IrisEngine engine = mock(IrisEngine.class);
|
||||
when(engine.isStudio()).thenReturn(true);
|
||||
FakeRuntimeFactory runtimes = new FakeRuntimeFactory();
|
||||
IrisEngine.GenerationRuntimeBinding active = runtimes.binding(history, history.activeActivation());
|
||||
when(engine.getActiveGenerationRuntimeBinding()).thenReturn(active);
|
||||
AtomicReference<IrisEngine.GenerationRuntimeBinding> scoped = installScopeTracking(engine);
|
||||
EngineMantle engineMantle = mock(EngineMantle.class);
|
||||
when(engine.getMantle()).thenReturn(engineMantle);
|
||||
when(engineMantle.getMantle()).thenReturn(mock(Mantle.class));
|
||||
try (GenerationHistoryRuntimeRouter router = GenerationHistoryRuntimeRouter.attach(
|
||||
engine, history, (ignored, x, z) -> signature(x, z), runtimes);
|
||||
GenerationHistoryRuntimeRouter.SavedChunkMantle saved = router.openSavedChunkMantle(0, 0)) {
|
||||
saved.detachThread();
|
||||
try (GenerationHistoryRuntimeRouter.SavedChunkMantle.Scope ignored = saved.openScope()) {
|
||||
assertNull(scoped.get());
|
||||
assertThrows(IllegalStateException.class, router::close);
|
||||
assertThrows(IllegalStateException.class, saved::close);
|
||||
assertThrows(IllegalStateException.class, () -> router.beginStudioCutover(1L));
|
||||
assertThrows(IllegalStateException.class,
|
||||
() -> engine.getGenerationSessions().transitionGate().beginTransition(1L));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void historicalTerrainRoutesWorkWhenFrozenPackFixturesAreMissing() throws Exception {
|
||||
Path world = temporaryFolder.newFolder("router-archived-world").toPath();
|
||||
|
||||
@@ -89,6 +89,7 @@ public class BukkitChunkGeneratorCloseTest {
|
||||
|
||||
private CloseFixture() throws Exception {
|
||||
setField(generator, "closeFuture", new AtomicReference<CompletableFuture<Void>>());
|
||||
setField(generator, "startupReady", CompletableFuture.completedFuture(null));
|
||||
setField(generator, "engine", engine);
|
||||
setField(generator, "folder", mock(ReactiveFolder.class));
|
||||
setField(generator, "populators", new KList<BlockPopulator>());
|
||||
|
||||
+139
@@ -0,0 +1,139 @@
|
||||
package art.arcane.iris.engine.platform;
|
||||
|
||||
import art.arcane.iris.core.loader.IrisData;
|
||||
import art.arcane.iris.engine.framework.EngineTarget;
|
||||
import art.arcane.iris.engine.object.IrisWorld;
|
||||
import art.arcane.iris.platform.bukkit.BukkitPlatform;
|
||||
import art.arcane.iris.spi.IrisLogging;
|
||||
import art.arcane.iris.util.common.plugin.VolmitPlugin;
|
||||
import art.arcane.iris.util.common.scheduling.J;
|
||||
import art.arcane.volmlib.util.bukkit.WorldIdentity;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.HeightMap;
|
||||
import org.bukkit.NamespacedKey;
|
||||
import org.bukkit.World;
|
||||
import org.bukkit.event.world.WorldInitEvent;
|
||||
import org.bukkit.generator.WorldInfo;
|
||||
import org.junit.Test;
|
||||
import org.mockito.MockedStatic;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Random;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.CompletionException;
|
||||
import java.util.concurrent.TimeoutException;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.concurrent.locks.ReentrantLock;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertSame;
|
||||
import static org.junit.Assert.assertThrows;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.CALLS_REAL_METHODS;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.mockStatic;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
public class BukkitChunkGeneratorStartupContentTest {
|
||||
@Test(timeout = 1000L)
|
||||
public void pendingContentRejectsGenerationWithoutWaitingOnTheServerThread() {
|
||||
CompletableFuture<Void> ready = new CompletableFuture<>();
|
||||
|
||||
assertThrows(IllegalStateException.class,
|
||||
() -> BukkitChunkGenerator.requireStartupContentReady(ready, true));
|
||||
assertFalse(ready.isDone());
|
||||
ready.complete(null);
|
||||
assertThrows(IllegalStateException.class,
|
||||
() -> BukkitChunkGenerator.requireStartupContentReady(ready, true));
|
||||
BukkitChunkGenerator.requireStartupContentReady(ready, false);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void failedContentNeverAdmitsGeneration() {
|
||||
IllegalStateException failure = new IllegalStateException("Required provider is unavailable");
|
||||
CompletableFuture<Void> ready = CompletableFuture.failedFuture(failure);
|
||||
|
||||
CompletionException rejected = assertThrows(CompletionException.class,
|
||||
() -> BukkitChunkGenerator.requireStartupContentReady(ready, false));
|
||||
|
||||
assertSame(failure, rejected.getCause());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void failedInitializationRejectsTerrainEvenAfterContentBecomesReady() throws Exception {
|
||||
BukkitChunkGenerator generator = mock(BukkitChunkGenerator.class, CALLS_REAL_METHODS);
|
||||
IllegalStateException failure = new IllegalStateException("Generator injection failed");
|
||||
setField(generator, "startupContentReady", CompletableFuture.completedFuture(null));
|
||||
setField(generator, "startupReady", CompletableFuture.failedFuture(failure));
|
||||
|
||||
CompletionException rejected = assertThrows(CompletionException.class,
|
||||
() -> generator.getBaseHeight(mock(WorldInfo.class), new Random(1L), 0, 0, HeightMap.WORLD_SURFACE));
|
||||
|
||||
assertSame(failure, rejected.getCause());
|
||||
verify(generator, never()).getTarget();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void pendingWorldExposesItsStemTargetAndFailsReadinessOnProviderTimeout() throws Exception {
|
||||
BukkitChunkGenerator generator = mock(BukkitChunkGenerator.class, CALLS_REAL_METHODS);
|
||||
IrisWorld irisWorld = mock(IrisWorld.class);
|
||||
World world = mock(World.class);
|
||||
EngineTarget target = mock(EngineTarget.class);
|
||||
IrisData data = mock(IrisData.class);
|
||||
VolmitPlugin plugin = mock(VolmitPlugin.class);
|
||||
CompletableFuture<Void> contentReady = new CompletableFuture<>();
|
||||
List<Runnable> tasks = new ArrayList<>();
|
||||
when(irisWorld.identity()).thenReturn("iris:test");
|
||||
when(irisWorld.name()).thenReturn("world_iris_test");
|
||||
when(world.getGenerator()).thenReturn(generator);
|
||||
when(target.getData()).thenReturn(data);
|
||||
setField(generator, "world", irisWorld);
|
||||
setField(generator, "setup", new AtomicBoolean());
|
||||
setField(generator, "lock", new ReentrantLock());
|
||||
setField(generator, "spawnChunks", new CompletableFuture<Integer>());
|
||||
setField(generator, "initialSpawnReady", new CompletableFuture<Void>());
|
||||
generator.deferStartup(target, contentReady);
|
||||
|
||||
assertSame(target, generator.getTarget());
|
||||
generator.touch(world);
|
||||
try (MockedStatic<J> scheduling = mockStatic(J.class);
|
||||
MockedStatic<BukkitPlatform> platform = mockStatic(BukkitPlatform.class);
|
||||
MockedStatic<WorldIdentity> identity = mockStatic(WorldIdentity.class);
|
||||
MockedStatic<IrisLogging> logging = mockStatic(IrisLogging.class);
|
||||
MockedStatic<Bukkit> bukkit = mockStatic(Bukkit.class)) {
|
||||
platform.when(BukkitPlatform::volmitPlugin).thenReturn(plugin);
|
||||
identity.when(() -> WorldIdentity.key(world)).thenReturn(NamespacedKey.fromString("iris:test"));
|
||||
scheduling.when(() -> J.s(any(Runnable.class))).thenAnswer(invocation -> {
|
||||
tasks.add(invocation.getArgument(0, Runnable.class));
|
||||
return null;
|
||||
});
|
||||
|
||||
generator.onWorldInit(new WorldInitEvent(world));
|
||||
assertTrue(tasks.isEmpty());
|
||||
TimeoutException failure = new TimeoutException("Provider startup expired");
|
||||
contentReady.completeExceptionally(failure);
|
||||
assertFalse(generator.getStartupReady().isDone());
|
||||
assertEquals(1, tasks.size());
|
||||
tasks.removeFirst().run();
|
||||
|
||||
assertTrue(generator.getStartupReady().isCompletedExceptionally());
|
||||
assertTrue(generator.getInitialSpawnReady().isCompletedExceptionally());
|
||||
assertSame(failure, generator.getInitializationFailure());
|
||||
generator.touch(world);
|
||||
verify(data).close();
|
||||
bukkit.verify(Bukkit::shutdown);
|
||||
}
|
||||
}
|
||||
|
||||
private static void setField(BukkitChunkGenerator generator, String name, Object value) throws Exception {
|
||||
Field field = BukkitChunkGenerator.class.getDeclaredField(name);
|
||||
field.setAccessible(true);
|
||||
field.set(generator, value);
|
||||
}
|
||||
}
|
||||
+3
@@ -19,6 +19,7 @@ import java.lang.reflect.Field;
|
||||
import java.lang.reflect.InvocationTargetException;
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.Future;
|
||||
@@ -202,6 +203,8 @@ public class BukkitChunkGeneratorStudioSelectionTest {
|
||||
field(generator, "studio", true);
|
||||
field(generator, "world", world);
|
||||
field(generator, "setup", setup);
|
||||
field(generator, "startupContentReady", CompletableFuture.completedFuture(null));
|
||||
field(generator, "startupReady", CompletableFuture.completedFuture(null));
|
||||
field(generator, "lock", new ReentrantLock());
|
||||
field(generator, "targetCache", new AtomicCache<EngineTarget>());
|
||||
field(generator, "populators", new KList<>());
|
||||
|
||||
+48
@@ -2,6 +2,7 @@ package art.arcane.iris.engine.platform;
|
||||
|
||||
import art.arcane.iris.core.link.Identifier;
|
||||
import art.arcane.iris.core.loader.IrisData;
|
||||
import art.arcane.iris.core.service.ExternalDataSVC;
|
||||
import art.arcane.iris.engine.IrisEngineMantle;
|
||||
import art.arcane.iris.engine.framework.Engine;
|
||||
import art.arcane.iris.engine.framework.EngineMetrics;
|
||||
@@ -9,12 +10,15 @@ import art.arcane.iris.engine.history.SavedBiomeUnavailableException;
|
||||
import art.arcane.iris.engine.object.IrisWorld;
|
||||
import art.arcane.iris.spi.IrisPlatform;
|
||||
import art.arcane.iris.spi.IrisPlatforms;
|
||||
import art.arcane.iris.spi.IrisServices;
|
||||
import art.arcane.iris.spi.PlatformBlockState;
|
||||
import art.arcane.iris.spi.PlatformRegistries;
|
||||
import art.arcane.iris.util.common.scheduling.J;
|
||||
import art.arcane.iris.util.project.matter.TileWrapper;
|
||||
import art.arcane.iris.util.project.matter.slices.PreObjectMatterTest;
|
||||
import art.arcane.volmlib.util.function.Consumer4;
|
||||
import art.arcane.volmlib.util.mantle.flag.MantleFlag;
|
||||
import art.arcane.volmlib.util.mantle.runtime.Mantle;
|
||||
import art.arcane.volmlib.util.mantle.runtime.MantleChunk;
|
||||
import art.arcane.volmlib.util.mantle.runtime.MantleDataAdapter;
|
||||
import art.arcane.volmlib.util.mantle.runtime.MantleHooks;
|
||||
@@ -23,10 +27,12 @@ import art.arcane.volmlib.util.matter.MatterUpdate;
|
||||
import art.arcane.volmlib.util.matter.slices.UpdateMatter;
|
||||
import org.bukkit.Chunk;
|
||||
import org.bukkit.Material;
|
||||
import org.bukkit.World;
|
||||
import org.bukkit.block.Block;
|
||||
import org.bukkit.block.data.BlockData;
|
||||
import org.junit.Test;
|
||||
import org.mockito.InOrder;
|
||||
import org.mockito.MockedStatic;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
@@ -41,16 +47,58 @@ import static org.junit.Assert.assertThrows;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.ArgumentMatchers.anyInt;
|
||||
import static org.mockito.Mockito.doAnswer;
|
||||
import static org.mockito.Mockito.doThrow;
|
||||
import static org.mockito.Mockito.inOrder;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.mockStatic;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.spy;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
public class EngineBukkitOpsDeferredMaterializationTest {
|
||||
@Test
|
||||
@SuppressWarnings("unchecked")
|
||||
public void savedMantlePlacementUsesCurrentProviderWithoutReadingCurrentMantle() {
|
||||
Mantle<Matter> saved = mock(Mantle.class);
|
||||
MantleDataAdapter<Matter> adapter = mock(MantleDataAdapter.class);
|
||||
MantleChunk<Matter> stored = spy(new MantleChunk<>(1, 0, 0, adapter, MantleHooks.NONE));
|
||||
Identifier identifier = Identifier.fromString("itemsadder:forest/amber_ore");
|
||||
doAnswer(invocation -> {
|
||||
Consumer4<Integer, Integer, Integer, Identifier> iterator = invocation.getArgument(1);
|
||||
iterator.accept(1, 2, 3, identifier);
|
||||
return null;
|
||||
}).when(stored).iterate(eq(Identifier.class), any());
|
||||
Engine engine = mock(Engine.class);
|
||||
when(engine.getWorld()).thenReturn(mock(IrisWorld.class));
|
||||
when(engine.getMetrics()).thenReturn(new EngineMetrics(8));
|
||||
World world = mock(World.class);
|
||||
when(world.isChunkLoaded(anyInt(), anyInt())).thenReturn(true);
|
||||
when(world.getMaxHeight()).thenReturn(16);
|
||||
Chunk chunk = mock(Chunk.class);
|
||||
when(chunk.getWorld()).thenReturn(world);
|
||||
Block block = mock(Block.class);
|
||||
when(chunk.getBlock(1, 2, 3)).thenReturn(block);
|
||||
when(saved.isLoaded(chunk)).thenReturn(true);
|
||||
when(saved.getChunk(chunk)).thenReturn(stored);
|
||||
ExternalDataSVC provider = mock(ExternalDataSVC.class);
|
||||
|
||||
try (MockedStatic<J> scheduling = mockStatic(J.class);
|
||||
MockedStatic<IrisServices> services = mockStatic(IrisServices.class)) {
|
||||
scheduling.when(J::isPrimaryThread).thenReturn(true);
|
||||
services.when(() -> IrisServices.get(ExternalDataSVC.class)).thenReturn(provider);
|
||||
|
||||
EngineBukkitOps.updateChunk(engine, chunk, saved);
|
||||
}
|
||||
|
||||
verify(provider).processUpdate(engine, block, identifier);
|
||||
verify(engine, never()).getMantle();
|
||||
verify(stored).deleteSlices(Identifier.class);
|
||||
assertTrue(stored.isFlagged(MantleFlag.ETCHED));
|
||||
}
|
||||
|
||||
@Test(timeout = 5000)
|
||||
public void loadingRetryPreservesPendingUpdatesWithoutRepeatingCompletedUpdates() {
|
||||
MantleChunk<Matter> mantleChunk = pendingUpdates();
|
||||
|
||||
@@ -18,11 +18,14 @@
|
||||
|
||||
package art.arcane.iris.platform.bukkit;
|
||||
|
||||
import art.arcane.iris.core.link.Identifier;
|
||||
import art.arcane.iris.spi.IrisPlatform;
|
||||
import art.arcane.iris.spi.IrisPlatforms;
|
||||
import art.arcane.iris.spi.PlatformBlockProperty;
|
||||
import art.arcane.iris.spi.PlatformBlockState;
|
||||
import art.arcane.iris.spi.PlatformRegistries;
|
||||
import art.arcane.iris.util.common.data.IrisCustomData;
|
||||
import art.arcane.iris.util.project.matter.slices.PlatformBlockMatter;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.Material;
|
||||
import org.bukkit.Server;
|
||||
@@ -34,6 +37,10 @@ import org.junit.BeforeClass;
|
||||
import org.junit.Test;
|
||||
import org.mockito.invocation.InvocationOnMock;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.DataInputStream;
|
||||
import java.io.DataOutputStream;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
@@ -120,6 +127,39 @@ public class BukkitSpiConformanceTest {
|
||||
assertEquals("iristest:key_block[facing=north,lit=true]", BukkitBlockState.of(data).key());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void customStateKeepsProviderIdentityAndProperties() throws Exception {
|
||||
BlockData base = blockData("minecraft:note_block[instrument=harp,note=5,powered=false]");
|
||||
Identifier identifier = Identifier.fromString("craftengine:forest/amber_log[axis=x]");
|
||||
PlatformBlockState state = BukkitBlockState.of(IrisCustomData.of(base, identifier));
|
||||
|
||||
assertEquals(identifier.toString(), state.key());
|
||||
assertEquals(identifier.toString(), state.deferredPlacementKey());
|
||||
assertEquals("craftengine", state.namespace());
|
||||
assertEquals("craftengine:forest/amber_log", state.materialKey());
|
||||
assertEquals(base.getAsString(), state.placementBaseState().key());
|
||||
assertTrue(state.isCustom());
|
||||
|
||||
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
|
||||
new PlatformBlockMatter().writeNode(state, new DataOutputStream(bytes));
|
||||
assertEquals(identifier.toString(), new DataInputStream(new ByteArrayInputStream(bytes.toByteArray())).readUTF());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void customCarrierPropertyChangeKeepsProviderPropertiesSeparate() {
|
||||
BlockData base = blockData("minecraft:oak_slab[type=bottom,waterlogged=false]");
|
||||
Identifier identifier = Identifier.fromString("itemsadder:forest/amber_slab");
|
||||
PlatformBlockState state = BukkitBlockState.of(IrisCustomData.of(base, identifier));
|
||||
|
||||
PlatformBlockState merged = state.withProperty("waterlogged", "true");
|
||||
|
||||
assertEquals(identifier.toString(), merged.key());
|
||||
assertEquals(identifier.toString(), merged.deferredPlacementKey());
|
||||
assertEquals("minecraft:oak_slab[type=bottom,waterlogged=true]", merged.placementBaseState().key());
|
||||
assertEquals("minecraft:oak_slab[type=bottom,waterlogged=false]", state.placementBaseState().key());
|
||||
assertTrue(merged.isCustom());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void blockStateNamespaceParsesWithAndWithoutProperties() {
|
||||
BlockData withProps = blockData("iristest:ns_block[axis=y]");
|
||||
|
||||
@@ -35,6 +35,7 @@ jaxen = "2.0.6" # https://central.sonatype.com/artifact/jaxen/jaxen
|
||||
|
||||
# Third Party Integrations
|
||||
nexo = "1.25.0" # https://repo.nexomc.com/#/releases/com/nexomc/nexo
|
||||
oraxen = "1.218.0"
|
||||
itemsadder = "4.0.10" # https://github.com/LoneDev6/API-ItemsAdder
|
||||
placeholderApi = "2.11.6" # https://repo.extendedclip.com/#/releases/me/clip/placeholderapi
|
||||
score = "5.25.3.9" # https://github.com/Ssomar-Developement/SCore
|
||||
@@ -95,6 +96,7 @@ jaxen = { module = "jaxen:jaxen", version.ref = "jaxen" }
|
||||
|
||||
# Third Party Integrations
|
||||
nexo = { module = "com.nexomc:nexo", version.ref = "nexo" }
|
||||
oraxen = { module = "io.th0rgal:oraxen", version.ref = "oraxen" }
|
||||
itemsadder = { module = "dev.lone:api-itemsadder", version.ref = "itemsadder" }
|
||||
placeholderApi = { module = "me.clip:placeholderapi", version.ref = "placeholderApi" }
|
||||
score = { module = "com.github.Ssomar-Developement:SCore", version.ref = "score" }
|
||||
|
||||
Reference in New Issue
Block a user