CrantEngine Tweak and a bit more.

- Fixed CE support
- Added some checks for things like paint and so on in the 3d Generator.
This commit is contained in:
Brian Neumann-Fopiano committed 2026-09-08 19:22:55 -04:00
1 parent 0782bbf0b4
commit 4de1aea8b9
44 files changed
+3161 -244

No files matched your search

@@ -8,6 +8,7 @@ import java.util.function.LongPredicate;
import art.arcane.iris.engine.DimensionStackContext;
import art.arcane.iris.engine.DimensionStackLayout;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.framework.EngineLifecycleTasks;
import art.arcane.iris.engine.framework.GenerationSessionException;
import art.arcane.iris.engine.framework.GenerationSessionLease;
import art.arcane.iris.engine.framework.IrisStructureLocator;
@@ -930,6 +931,13 @@ public class IrisChunkGenerator extends CustomChunkGenerator implements LongPred
@Override
public WeightedList<MobSpawnSettings.SpawnerData> getMobsAt(Holder<Biome> holder, StructureManager structuremanager, MobCategory enumcreaturetype, BlockPos blockposition) {
return EngineLifecycleTasks.call(engine, "bukkit_nms_mob_spawns",
() -> getAdmittedMobsAt(holder, structuremanager, enumcreaturetype, blockposition),
WeightedList.of(List.of()));
}
private WeightedList<MobSpawnSettings.SpawnerData> getAdmittedMobsAt(
Holder<Biome> holder, StructureManager structuremanager, MobCategory enumcreaturetype, BlockPos blockposition) {
NativeBiomeSpawnSelection selection = NativeBiomeSpawnSelection.at(
engine, blockposition.getX(), blockposition.getY(), blockposition.getZ(),
holder.unwrapKey().map(key -> key.identifier().toString()).orElse(""));
@@ -937,9 +945,7 @@ public class IrisChunkGenerator extends CustomChunkGenerator implements LongPred
return WeightedList.of(List.of());
}
try (GenerationHistoryRuntimeRouter.CoordinateScope route = openHistoryCoordinateScope(
blockposition.getX(), blockposition.getZ(), "bukkit_nms_mob_spawns");
GenerationSessionLease lease = requireGenerationLease("bukkit_nms_mob_spawns");
IrisContext.Scope ignored = IrisContext.open(engine, lease.sessionId(), null)) {
blockposition.getX(), blockposition.getZ(), "bukkit_nms_mob_spawns")) {
return getMobsAtWithActiveRuntime(holder, structuremanager, enumcreaturetype, blockposition, selection);
}
}
@@ -3,13 +3,16 @@ package art.arcane.iris.core.nms.v26_2_R1;
import art.arcane.iris.engine.history.NativeTerrainReceipt;
import art.arcane.iris.engine.history.SavedTerrainChunk;
import art.arcane.iris.util.common.scheduling.J;
import ca.spottedleaf.concurrentutil.util.Priority;
import ca.spottedleaf.moonrise.common.PlatformHooks;
import ca.spottedleaf.moonrise.patches.chunk_system.io.MoonriseRegionFileIO;
import ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkTaskScheduler;
import ca.spottedleaf.moonrise.patches.chunk_system.scheduling.NewChunkHolder;
import net.minecraft.core.registries.BuiltInRegistries;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.nbt.NbtIo;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.world.level.ChunkPos;
import net.minecraft.world.level.chunk.ChunkAccess;
import net.minecraft.world.level.chunk.storage.SerializableChunkData;
import org.bukkit.World;
@@ -18,12 +21,17 @@ import org.bukkit.craftbukkit.CraftWorld;
import java.io.ByteArrayOutputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.LockSupport;
final class NmsSavedTerrainCapture {
private static final long WRITE_TIMEOUT_NANOS = TimeUnit.SECONDS.toNanos(30L);
private NmsSavedTerrainCapture() {
}
@@ -53,9 +61,12 @@ final class NmsSavedTerrainCapture {
});
return snapshot.thenApplyAsync(data -> {
try {
Checkpoint checkpoint = checkpoint(chunkX, chunkZ, data);
awaitWrite(level, checkpoint.chunk(), System.nanoTime() + WRITE_TIMEOUT_NANOS);
if (data == null) {
return SavedTerrainChunk.read(world.getWorldFolder().toPath(), chunkX, chunkZ, minimumY, height);
}
verifyCheckpoint(world.getWorldFolder().toPath(), checkpoint);
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
try (DataOutputStream output = new DataOutputStream(bytes)) {
NbtIo.write(data, output);
@@ -77,17 +88,8 @@ final class NmsSavedTerrainCapture {
J.runRegionFuture(world, holder.chunkX, holder.chunkZ, () -> {
try {
ChunkAccess chunk = holder.getCurrentChunk();
if (chunk == null || !chunk.isUnsaved()) {
saved.complete(null);
return;
}
CompoundTag data = saveSnapshot(level, chunk);
byte[] receipt = data.getCompound("ChunkBukkitValues")
.flatMap(values -> values.getByteArray(NativeTerrainReceipt.NBT_KEY)).orElse(null);
saved.complete(new Checkpoint(holder.chunkX, holder.chunkZ,
BuiltInRegistries.CHUNK_STATUS.getKey(chunk.getPersistedStatus()).toString(), receipt,
data.getCompound("ChunkBukkitValues").map(values -> values.getLongOr(
NativeTerrainReceipt.STRUCTURE_ACTIVATION_KEY, 0)).orElse(0L)));
saved.complete(checkpoint(holder.chunkX, holder.chunkZ,
chunk != null && chunk.isUnsaved() ? saveSnapshot(level, chunk) : null));
} catch (Throwable failure) {
saved.completeExceptionally(failure);
}
@@ -99,22 +101,70 @@ final class NmsSavedTerrainCapture {
}
return CompletableFuture.allOf(saves.toArray(CompletableFuture[]::new)).thenRunAsync(() -> {
try {
MoonriseRegionFileIO.flush(level, MoonriseRegionFileIO.RegionFileType.CHUNK_DATA);
MoonriseRegionFileIO.flushRegionStorages(level, MoonriseRegionFileIO.RegionFileType.CHUNK_DATA);
List<Checkpoint> checkpoints = new ArrayList<>(saves.size());
for (CompletableFuture<Checkpoint> future : saves) {
Checkpoint saved = future.join();
if (saved == null) {
continue;
}
SavedTerrainChunk.verifyCheckpoint(world.getWorldFolder().toPath(), saved.chunkX(), saved.chunkZ(),
saved.status(), saved.receipt(), saved.structureActivation());
checkpoints.add(future.join());
}
finishCheckpoint(level, world.getWorldFolder().toPath(), checkpoints);
} catch (IOException failure) {
throw new CompletionException(failure);
}
});
}
static void finishCheckpoint(ServerLevel level, Path worldFolder, List<Checkpoint> checkpoints) throws IOException {
long deadline = System.nanoTime() + WRITE_TIMEOUT_NANOS;
for (Checkpoint checkpoint : checkpoints) {
awaitWrite(level, checkpoint.chunk(), deadline);
}
MoonriseRegionFileIO.flushRegionStorages(level, MoonriseRegionFileIO.RegionFileType.CHUNK_DATA);
for (Checkpoint checkpoint : checkpoints) {
verifyCheckpoint(worldFolder, checkpoint);
}
}
static Checkpoint checkpoint(int chunkX, int chunkZ, CompoundTag data) {
if (data == null) {
return new Checkpoint(new ChunkPos(chunkX, chunkZ), null);
}
byte[] receipt = data.getCompound("ChunkBukkitValues")
.flatMap(values -> values.getByteArray(NativeTerrainReceipt.NBT_KEY)).orElse(null);
long structureActivation = data.getCompound("ChunkBukkitValues").map(values -> values.getLongOr(
NativeTerrainReceipt.STRUCTURE_ACTIVATION_KEY, 0)).orElse(0L);
return new Checkpoint(new ChunkPos(chunkX, chunkZ), new Verification(
data.getStringOr("Status", ""), receipt, structureActivation));
}
private static void verifyCheckpoint(Path worldFolder, Checkpoint checkpoint) throws IOException {
Verification expected = checkpoint.verification();
if (expected == null) {
return;
}
SavedTerrainChunk.verifyCheckpoint(worldFolder, checkpoint.chunk().x(), checkpoint.chunk().z(),
expected.status(), expected.receipt(), expected.structureActivation());
}
static void awaitWrite(ServerLevel level, ChunkPos chunk, long deadlineNanos) throws IOException {
ChunkTaskScheduler scheduler = level.moonrise$getChunkTaskScheduler();
while (true) {
if (Thread.currentThread().isInterrupted()) {
throw new IOException("Interrupted while saving terrain checkpoint chunk " + chunk);
}
if (System.nanoTime() - deadlineNanos >= 0L) {
throw new IOException("Timed out saving terrain checkpoint chunk " + chunk);
}
if (MoonriseRegionFileIO.getPriority(level, chunk.x(), chunk.z(),
MoonriseRegionFileIO.RegionFileType.CHUNK_DATA) == Priority.COMPLETING) {
return;
}
boolean helped = scheduler.saveExecutor.executeTask();
helped |= scheduler.compressionExecutor.executeTask();
if (!helped) {
LockSupport.parkNanos(1_000_000L);
}
}
}
private static CompoundTag saveSnapshot(ServerLevel level, ChunkAccess chunk) {
SerializableChunkData snapshot = SerializableChunkData.copyOf(level, chunk);
PlatformHooks.get().chunkSyncSave(level, chunk, snapshot);
@@ -124,6 +174,9 @@ final class NmsSavedTerrainCapture {
return data;
}
private record Checkpoint(int chunkX, int chunkZ, String status, byte[] receipt, long structureActivation) {
record Checkpoint(ChunkPos chunk, Verification verification) {
}
record Verification(String status, byte[] receipt, long structureActivation) {
}
}
@@ -0,0 +1,144 @@
package art.arcane.iris.core.nms.v26_2_R1;
import art.arcane.iris.engine.IrisEngine;
import art.arcane.iris.engine.framework.GenerationSessionManager;
import art.arcane.iris.engine.framework.GenerationTransitionGate;
import art.arcane.iris.engine.history.GenerationHistoryRuntimeRouter;
import art.arcane.iris.engine.history.NativeBiomeSpawnSelection;
import art.arcane.iris.engine.history.SavedBiomeRuntime;
import art.arcane.iris.util.project.context.IrisContext;
import net.minecraft.SharedConstants;
import net.minecraft.core.BlockPos;
import net.minecraft.core.Holder;
import net.minecraft.server.Bootstrap;
import net.minecraft.util.random.WeightedList;
import net.minecraft.world.entity.MobCategory;
import net.minecraft.world.level.StructureManager;
import net.minecraft.world.level.biome.Biome;
import net.minecraft.world.level.biome.MobSpawnSettings;
import net.minecraft.world.level.chunk.ChunkGenerator;
import org.junit.BeforeClass;
import org.junit.Test;
import java.lang.reflect.Field;
import java.util.Optional;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicLong;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertTrue;
import static org.mockito.Mockito.CALLS_REAL_METHODS;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
public class IrisChunkGeneratorSpawnAdmissionTest {
@BeforeClass
public static void bootstrapMinecraftRegistries() {
SharedConstants.tryDetectVersion();
Bootstrap.bootStrap();
}
@Test
public void tickSpawnQueryLeavesOwnerThreadAvailableForBoundaryCheckpoint() throws Exception {
Fixture fixture = new Fixture();
ExecutorService owner = Executors.newSingleThreadExecutor();
ExecutorService reloader = Executors.newSingleThreadExecutor();
AtomicInteger snapshots = new AtomicInteger();
long previousSession = fixture.sessions.currentSessionId();
try {
assertSame(fixture.expected, owner.submit(fixture::query).get(2L, TimeUnit.SECONDS));
assertEquals(previousSession, fixture.readSession.get());
CompletableFuture<Void> cutover = CompletableFuture.runAsync(() -> {
try (GenerationTransitionGate.Transition ignored = fixture.sessions.transitionGate().beginTransition(2_000L)) {
fixture.sessions.sealAndAwait("Studio generation cutover", 2_000L);
Future<?> query = owner.submit(() -> {
assertTrue(fixture.query().isEmpty());
assertNull(IrisContext.get());
});
owner.submit(snapshots::incrementAndGet).get(2L, TimeUnit.SECONDS);
query.get(2L, TimeUnit.SECONDS);
assertEquals(1, fixture.savedReads.get());
assertEquals(1, fixture.routes.get());
fixture.sessions.activateNextSession();
} catch (Exception failure) {
throw new IllegalStateException(failure);
}
}, reloader);
cutover.get(3L, TimeUnit.SECONDS);
assertEquals(1, snapshots.get());
assertEquals(0, fixture.sessions.activeLeases());
assertSame(fixture.expected, owner.submit(fixture::query).get(2L, TimeUnit.SECONDS));
assertEquals(fixture.sessions.currentSessionId(), fixture.readSession.get());
assertTrue(fixture.readSession.get() > previousSession);
assertEquals(2, fixture.savedReads.get());
assertEquals(2, fixture.routes.get());
assertEquals(0, fixture.sessions.activeLeases());
} finally {
owner.shutdownNow();
reloader.shutdownNow();
assertTrue(owner.awaitTermination(2L, TimeUnit.SECONDS));
assertTrue(reloader.awaitTermination(2L, TimeUnit.SECONDS));
}
}
private static void setField(IrisChunkGenerator generator, String name, Object value) throws Exception {
Field field = IrisChunkGenerator.class.getDeclaredField(name);
field.setAccessible(true);
field.set(generator, value);
}
private static final class Fixture {
private final GenerationSessionManager sessions = new GenerationSessionManager(true);
private final IrisEngine engine = mock(IrisEngine.class);
private final IrisChunkGenerator generator = mock(IrisChunkGenerator.class, CALLS_REAL_METHODS);
private final Holder<Biome> biome = Holder.direct(mock(Biome.class));
private final StructureManager structures = mock(StructureManager.class);
private final WeightedList<MobSpawnSettings.SpawnerData> expected = mock(WeightedList.class);
private final AtomicInteger savedReads = new AtomicInteger();
private final AtomicInteger routes = new AtomicInteger();
private final AtomicLong readSession = new AtomicLong();
private Fixture() throws Exception {
GenerationHistoryRuntimeRouter router = mock(GenerationHistoryRuntimeRouter.class);
SavedBiomeRuntime saved = mock(SavedBiomeRuntime.class);
ChunkGenerator delegate = mock(ChunkGenerator.class);
CustomBiomeSource source = mock(CustomBiomeSource.class);
when(engine.getGenerationSessions()).thenReturn(sessions);
when(engine.getGenerationHistoryRuntimeRouter()).thenReturn(Optional.of(router));
when(router.biomes()).thenReturn(saved);
when(saved.nativeSpawnSelection(0, 64, 0, "")).thenAnswer(invocation -> {
assertAdmitted();
savedReads.incrementAndGet();
return new NativeBiomeSpawnSelection(NativeBiomeSpawnSelection.Mode.RETAINED, "minecraft:plains");
});
when(engine.openGenerationHistoryCoordinateScope(0, 0)).thenAnswer(invocation -> {
assertAdmitted();
routes.incrementAndGet();
return null;
});
when(delegate.getMobsAt(biome, structures, MobCategory.MONSTER, new BlockPos(0, 64, 0)))
.thenReturn(expected);
setField(generator, "engine", engine);
setField(generator, "delegate", delegate);
setField(generator, "customBiomeSource", source);
}
private WeightedList<MobSpawnSettings.SpawnerData> query() {
return generator.getMobsAt(biome, structures, MobCategory.MONSTER, new BlockPos(0, 64, 0));
}
private void assertAdmitted() {
assertEquals(1, sessions.activeLeases());
assertSame(engine, IrisContext.get().getEngine());
readSession.set(IrisContext.get().getGenerationSessionId());
}
}
}
@@ -0,0 +1,114 @@
package art.arcane.iris.core.nms.v26_2_R1;
import art.arcane.iris.engine.history.SavedTerrainChunk;
import ca.spottedleaf.concurrentutil.util.Priority;
import ca.spottedleaf.moonrise.patches.chunk_system.io.MoonriseRegionFileIO;
import net.minecraft.SharedConstants;
import net.minecraft.server.Bootstrap;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.world.level.ChunkPos;
import org.junit.BeforeClass;
import org.junit.Test;
import org.mockito.MockedStatic;
import java.io.IOException;
import java.nio.file.Path;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.concurrent.atomic.AtomicBoolean;
import static org.junit.Assert.assertEquals;
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.never;
public class NmsSavedTerrainCheckpointTest {
@BeforeClass
public static void bootstrapMinecraftRegistries() {
SharedConstants.tryDetectVersion();
Bootstrap.bootStrap();
}
@Test
public void checkpointWaitsForDirtyAndUnloadedChunkWritesBeforeFlushAndVerification() throws Exception {
ServerLevel level = mock(ServerLevel.class);
Path root = Path.of("checkpoint-world");
byte[] receipt = new byte[]{1, 2, 3};
NmsSavedTerrainCapture.Checkpoint saved = new NmsSavedTerrainCapture.Checkpoint(new ChunkPos(4, -5),
new NmsSavedTerrainCapture.Verification("minecraft:full", receipt, 7L));
NmsSavedTerrainCapture.Checkpoint unloaded = NmsSavedTerrainCapture.checkpoint(-8, 9, null);
Set<ChunkPos> completed = new HashSet<>();
AtomicBoolean flushed = new AtomicBoolean();
try (MockedStatic<MoonriseRegionFileIO> io = mockStatic(MoonriseRegionFileIO.class);
MockedStatic<SavedTerrainChunk> terrain = mockStatic(SavedTerrainChunk.class)) {
io.when(() -> MoonriseRegionFileIO.getPriority(level, 4, -5, MoonriseRegionFileIO.RegionFileType.CHUNK_DATA))
.thenAnswer(invocation -> {
completed.add(saved.chunk());
return Priority.COMPLETING;
});
io.when(() -> MoonriseRegionFileIO.getPriority(level, -8, 9, MoonriseRegionFileIO.RegionFileType.CHUNK_DATA))
.thenAnswer(invocation -> {
completed.add(unloaded.chunk());
return Priority.COMPLETING;
});
io.when(() -> MoonriseRegionFileIO.flushRegionStorages(level, MoonriseRegionFileIO.RegionFileType.CHUNK_DATA))
.thenAnswer(invocation -> {
assertEquals(Set.of(saved.chunk(), unloaded.chunk()), completed);
flushed.set(true);
return null;
});
terrain.when(() -> SavedTerrainChunk.verifyCheckpoint(root, 4, -5, "minecraft:full", receipt, 7L))
.thenAnswer(invocation -> {
assertTrue(flushed.get());
return null;
});
NmsSavedTerrainCapture.finishCheckpoint(level, root, List.of(saved, unloaded));
terrain.verify(() -> SavedTerrainChunk.verifyCheckpoint(root, 4, -5, "minecraft:full", receipt, 7L));
io.verify(() -> MoonriseRegionFileIO.flush(level, MoonriseRegionFileIO.RegionFileType.CHUNK_DATA), never());
io.verify(() -> MoonriseRegionFileIO.getPriority(level, 100, 100, MoonriseRegionFileIO.RegionFileType.CHUNK_DATA), never());
terrain.verifyNoMoreInteractions();
}
}
@Test
public void storageFlushFailureCannotCompleteTheCheckpoint() throws Exception {
ServerLevel level = mock(ServerLevel.class);
IOException failure = new IOException("storage flush failed");
try (MockedStatic<MoonriseRegionFileIO> io = mockStatic(MoonriseRegionFileIO.class);
MockedStatic<SavedTerrainChunk> terrain = mockStatic(SavedTerrainChunk.class)) {
io.when(() -> MoonriseRegionFileIO.flushRegionStorages(level, MoonriseRegionFileIO.RegionFileType.CHUNK_DATA))
.thenThrow(failure);
assertSame(failure, assertThrows(IOException.class,
() -> NmsSavedTerrainCapture.finishCheckpoint(level, Path.of("checkpoint-world"), List.of())));
terrain.verifyNoInteractions();
}
}
@Test
public void persistedReceiptMismatchCannotCompleteTheCheckpoint() throws Exception {
ServerLevel level = mock(ServerLevel.class);
Path root = Path.of("checkpoint-world");
byte[] receipt = new byte[]{4, 5};
IOException failure = new IOException("saved terrain receipt mismatch");
NmsSavedTerrainCapture.Checkpoint saved = new NmsSavedTerrainCapture.Checkpoint(new ChunkPos(1, 2),
new NmsSavedTerrainCapture.Verification("minecraft:noise", receipt, 3L));
try (MockedStatic<MoonriseRegionFileIO> io = mockStatic(MoonriseRegionFileIO.class);
MockedStatic<SavedTerrainChunk> terrain = mockStatic(SavedTerrainChunk.class)) {
io.when(() -> MoonriseRegionFileIO.getPriority(level, 1, 2, MoonriseRegionFileIO.RegionFileType.CHUNK_DATA))
.thenReturn(Priority.COMPLETING);
terrain.when(() -> SavedTerrainChunk.verifyCheckpoint(root, 1, 2, "minecraft:noise", receipt, 3L))
.thenThrow(failure);
assertSame(failure, assertThrows(IOException.class,
() -> NmsSavedTerrainCapture.finishCheckpoint(level, root, List.of(saved))));
io.verify(() -> MoonriseRegionFileIO.flushRegionStorages(level, MoonriseRegionFileIO.RegionFileType.CHUNK_DATA));
}
}
}
@@ -0,0 +1,135 @@
package art.arcane.iris.core.nms.v26_2_R1;
import ca.spottedleaf.concurrentutil.executor.thread.BalancedPrioritisedThreadPool;
import ca.spottedleaf.concurrentutil.util.Priority;
import ca.spottedleaf.moonrise.patches.chunk_system.io.MoonriseRegionFileIO;
import ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkTaskScheduler;
import net.minecraft.SharedConstants;
import net.minecraft.server.Bootstrap;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.world.level.ChunkPos;
import org.junit.BeforeClass;
import org.junit.Test;
import org.mockito.MockedStatic;
import java.io.IOException;
import java.lang.reflect.Field;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;
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 NmsSavedTerrainWriteBarrierTest {
@BeforeClass
public static void bootstrapMinecraftRegistries() {
SharedConstants.tryDetectVersion();
Bootstrap.bootStrap();
}
@Test
public void checkpointHelpsSaveAndCompressionWhileNativeGenerationOccupiesTheWorker() throws Exception {
BalancedPrioritisedThreadPool pool = new BalancedPrioritisedThreadPool(
TimeUnit.MILLISECONDS.toNanos(1), Executors.defaultThreadFactory());
BalancedPrioritisedThreadPool.OrderedStreamGroup group = pool.createOrderedStreamGroup();
BalancedPrioritisedThreadPool.OrderedStreamGroup.Queue generation = group.createExecutor();
BalancedPrioritisedThreadPool.OrderedStreamGroup.Queue saves = group.createExecutor();
BalancedPrioritisedThreadPool.OrderedStreamGroup.Queue compression = group.createExecutor();
ServerLevel level = level(saves, compression);
ChunkPos chunk = new ChunkPos(4, -9);
CountDownLatch generationEntered = new CountDownLatch(1);
CountDownLatch releaseGeneration = new CountDownLatch(1);
AtomicBoolean pendingWrite = new AtomicBoolean(true);
AtomicInteger serialized = new AtomicInteger();
AtomicInteger compressed = new AtomicInteger();
AtomicReference<Thread> saveThread = new AtomicReference<>();
AtomicReference<Thread> compressionThread = new AtomicReference<>();
try (MockedStatic<MoonriseRegionFileIO> io = mockStatic(MoonriseRegionFileIO.class)) {
io.when(() -> MoonriseRegionFileIO.getPriority(level, chunk.x(), chunk.z(),
MoonriseRegionFileIO.RegionFileType.CHUNK_DATA))
.thenAnswer(invocation -> pendingWrite.get() ? Priority.NORMAL : Priority.COMPLETING);
generation.queueTask(() -> {
generationEntered.countDown();
try {
assertTrue(releaseGeneration.await(5, TimeUnit.SECONDS));
} catch (InterruptedException interruption) {
Thread.currentThread().interrupt();
throw new AssertionError(interruption);
}
});
pool.adjustThreadCount(1);
assertTrue(generationEntered.await(5, TimeUnit.SECONDS));
saves.queueTask(() -> {
saveThread.set(Thread.currentThread());
serialized.incrementAndGet();
compression.queueTask(() -> {
compressionThread.set(Thread.currentThread());
compressed.incrementAndGet();
pendingWrite.set(false);
});
});
NmsSavedTerrainCapture.awaitWrite(level, chunk, System.nanoTime() + TimeUnit.SECONDS.toNanos(3));
assertEquals(1L, releaseGeneration.getCount());
assertEquals(1, serialized.get());
assertEquals(1, compressed.get());
assertFalse(pendingWrite.get());
assertSame(Thread.currentThread(), saveThread.get());
assertSame(Thread.currentThread(), compressionThread.get());
} finally {
releaseGeneration.countDown();
pool.halt(true);
assertTrue(pool.joinInterruptable(5_000));
}
}
@Test
public void waitingForNativeWriteHonorsTheSharedDeadline() throws Exception {
ServerLevel level = mock(ServerLevel.class);
ChunkPos chunk = new ChunkPos(2, 3);
IOException failure = assertThrows(IOException.class,
() -> NmsSavedTerrainCapture.awaitWrite(level, chunk, System.nanoTime() - 1L));
assertTrue(failure.getMessage().contains("Timed out"));
}
@Test
public void interruptedNativeWriteWaitPreservesInterruption() throws Exception {
ServerLevel level = mock(ServerLevel.class);
ChunkPos chunk = new ChunkPos(2, 3);
Thread.currentThread().interrupt();
try {
IOException failure = assertThrows(IOException.class,
() -> NmsSavedTerrainCapture.awaitWrite(level, chunk,
System.nanoTime() + TimeUnit.SECONDS.toNanos(3)));
assertTrue(failure.getMessage().contains("Interrupted"));
assertTrue(Thread.currentThread().isInterrupted());
} finally {
Thread.interrupted();
}
}
private static ServerLevel level(BalancedPrioritisedThreadPool.OrderedStreamGroup.Queue saves,
BalancedPrioritisedThreadPool.OrderedStreamGroup.Queue compression) throws Exception {
ServerLevel level = mock(ServerLevel.class);
ChunkTaskScheduler scheduler = mock(ChunkTaskScheduler.class);
Field savesField = ChunkTaskScheduler.class.getField("saveExecutor");
Field compressionField = ChunkTaskScheduler.class.getField("compressionExecutor");
savesField.setAccessible(true);
compressionField.setAccessible(true);
savesField.set(scheduler, saves);
compressionField.set(scheduler, compression);
when(level.moonrise$getChunkTaskScheduler()).thenReturn(scheduler);
return level;
}
}
@@ -26,12 +26,16 @@ import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CopyOnWriteArraySet;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.ForkJoinPool;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;
import java.util.concurrent.locks.LockSupport;
import java.util.function.IntConsumer;
@@ -291,6 +295,87 @@ public class NativeStructureVolumeIndexTest {
}
}
@Test
public void contendedNativeWindowsAllowQueuedTerrainWorkToRun() throws Exception {
assertQueuedTerrainCanComplete(false);
}
@Test
public void interruptedNativeWindowWaitStillAcquiresAndReleasesOnce() throws Exception {
assertQueuedTerrainCanComplete(true);
}
private static void assertQueuedTerrainCanComplete(boolean interruptWaiter) throws Exception {
CountDownLatch ownerEntered = new CountDownLatch(1);
CompletableFuture<Void> terrain = new CompletableFuture<>();
AtomicBoolean firstOrigin = new AtomicBoolean(true);
AtomicBoolean interruptedAfterQuery = new AtomicBoolean();
NativeStructureVolumeIndex index = NativeStructureVolumeIndex.forTesting((engine, chunkX, chunkZ) -> {
if (firstOrigin.compareAndSet(true, false)) {
ownerEntered.countDown();
terrain.join();
}
return new KList<>(new NativeStructureVolume(chunkX + ":" + chunkZ,
-256, 0, -256, 256, 100, 256));
});
ExecutorService owner = Executors.newSingleThreadExecutor();
ForkJoinPool workers = new ForkJoinPool(2);
AtomicReference<Thread> firstWaiter = new AtomicReference<>();
AtomicReference<Thread> secondWaiter = new AtomicReference<>();
try {
Future<KList<NativeStructureVolume>> original = owner.submit(
() -> index.resolve(null, 0, 0, 15, 15));
assertTrue(ownerEntered.await(5, TimeUnit.SECONDS));
Future<KList<NativeStructureVolume>> adjacent = workers.submit(() -> {
firstWaiter.set(Thread.currentThread());
KList<NativeStructureVolume> result = index.resolve(null, 16, 0, 31, 15);
interruptedAfterQuery.set(Thread.currentThread().isInterrupted());
return result;
});
Future<KList<NativeStructureVolume>> next = workers.submit(() -> {
secondWaiter.set(Thread.currentThread());
return index.resolve(null, 32, 0, 47, 15);
});
awaitOriginWindowWaiter(firstWaiter);
awaitOriginWindowWaiter(secondWaiter);
if (interruptWaiter) {
firstWaiter.get().interrupt();
}
Future<?> terrainTask = workers.submit(() -> terrain.complete(null));
terrainTask.get(3, TimeUnit.SECONDS);
assertEquals(289, original.get(3, TimeUnit.SECONDS).size());
KList<NativeStructureVolume> adjacentVolumes = adjacent.get(3, TimeUnit.SECONDS);
assertEquals(289, adjacentVolumes.size());
assertEquals(adjacentVolumes, index.resolve(null, 16, 0, 31, 15));
assertEquals(289, next.get(3, TimeUnit.SECONDS).size());
assertEquals(interruptWaiter, interruptedAfterQuery.get());
} finally {
terrain.complete(null);
owner.shutdownNow();
workers.shutdownNow();
assertTrue(owner.awaitTermination(5, TimeUnit.SECONDS));
assertTrue(workers.awaitTermination(5, TimeUnit.SECONDS));
}
}
private static void awaitOriginWindowWaiter(AtomicReference<Thread> reference) {
long deadline = System.nanoTime() + TimeUnit.SECONDS.toNanos(5);
while (System.nanoTime() < deadline) {
Thread thread = reference.get();
if (thread != null && thread.getState() == Thread.State.WAITING) {
for (StackTraceElement frame : thread.getStackTrace()) {
if (frame.getClassName().equals(NativeStructureVolumeIndex.class.getName())
&& frame.getMethodName().equals("lockOriginWindow")) {
return;
}
}
}
LockSupport.parkNanos(TimeUnit.MILLISECONDS.toNanos(1));
}
throw new AssertionError("Native query did not wait for the occupied origin window");
}
@Test
public void distantColdQueriesCanResolveOriginsConcurrently() throws Exception {
BlockingResolver resolver = new BlockingResolver();
@@ -26,6 +26,7 @@ import art.arcane.iris.core.pack.PackDirectoryResolver;
import art.arcane.iris.core.runtime.ObjectStudioActivation;
import art.arcane.iris.core.runtime.StudioOpenCoordinator;
import art.arcane.iris.core.runtime.WorldRuntimeControlService;
import art.arcane.iris.core.service.ExternalDataSVC;
import art.arcane.iris.core.service.ObjectSVC;
import art.arcane.iris.core.service.StudioSVC;
import art.arcane.iris.core.service.WandSVC;
@@ -41,6 +42,8 @@ import art.arcane.iris.engine.object.IrisObjectRotation;
import art.arcane.iris.engine.object.IrisObjectScale;
import art.arcane.iris.engine.object.TileData;
import art.arcane.iris.platform.bukkit.BukkitBlockState;
import art.arcane.iris.spi.IrisLogging;
import art.arcane.iris.spi.IrisServices;
import art.arcane.iris.spi.PlatformBlockState;
import art.arcane.volmlib.util.data.Cuboid;
import art.arcane.iris.util.common.data.IrisCustomData;
@@ -223,11 +226,19 @@ public class CommandObject implements DirectorExecutor {
//Prevent blocks being set in or bellow bedrock
if (y <= world.getMinHeight() || block.getType() == Material.BEDROCK) return;
futureBlockChanges.putIfAbsent(block, block.getBlockData());
ExternalDataSVC external = IrisServices.getOrNull(ExternalDataSVC.class);
futureBlockChanges.computeIfAbsent(block, target -> {
BlockData previous = target.getBlockData();
return external == null ? previous : external.captureBlockData(previous);
});
if (d instanceof IrisCustomData data) {
if (external != null && external.placeBlock(block, data.getCustom())) {
return;
}
block.setBlockData(data.getBase(), false);
Iris.warn("Tried to place custom block at " + x + ", " + y + ", " + z + " which is not supported!");
IrisLogging.warnOnce("object-paste:external:" + data.getCustom(),
"Direct placement of %s is unavailable; only its base block was placed.", data.getCustom());
} else block.setBlockData(d, false);
}
@@ -53,11 +53,11 @@ dependencies:
required: false
join-classpath: false
MythicLib:
load: BEFORE
load: OMIT
required: false
join-classpath: true
MMOItems:
load: BEFORE
load: OMIT
required: false
join-classpath: true
eco:
@@ -69,11 +69,11 @@ dependencies:
required: false
join-classpath: true
MythicMobs:
load: BEFORE
load: OMIT
required: false
join-classpath: true
MythicCrucible:
load: BEFORE
load: OMIT
required: false
join-classpath: true
KGenerators:
@@ -11,12 +11,8 @@ softdepend:
- ItemsAdder
- SCore
- ExecutableItems
- MythicLib
- MMOItems
- eco
- EcoItems
- MythicMobs
- MythicCrucible
- KGenerators
- WorldEdit
loadbefore: [ Multiverse-Core ]
@@ -41,8 +41,11 @@ public class PaperPluginMetadataTest {
private static final List<String> JOINED_PLUGIN_IDS = OPTIONAL_PLUGIN_IDS.stream()
.filter(pluginId -> !"ExecutableItems".equals(pluginId) && !"Multiverse-Core".equals(pluginId))
.toList();
private static final List<String> UNORDERED_PLUGIN_IDS = List.of(
"MythicLib", "MMOItems", "MythicMobs", "MythicCrucible");
private static final List<String> BUKKIT_SOFT_DEPEND_IDS = OPTIONAL_PLUGIN_IDS.stream()
.filter(pluginId -> !"Multiverse-Core".equals(pluginId))
.filter(pluginId -> !UNORDERED_PLUGIN_IDS.contains(pluginId))
.toList();
@Test
@@ -72,7 +75,8 @@ public class PaperPluginMetadataTest {
+ " description: Allows survival players to fell Iris-managed trees with an axe.\n"
+ " default: op"));
for (String pluginId : JOINED_PLUGIN_IDS) {
assertTrue(metadata.contains(optionalDependencyBlock(pluginId, "BEFORE", true)));
String loadOrder = UNORDERED_PLUGIN_IDS.contains(pluginId) ? "OMIT" : "BEFORE";
assertTrue(metadata.contains(optionalDependencyBlock(pluginId, loadOrder, true)));
}
assertTrue(metadata.contains(optionalDependencyBlock("ExecutableItems", "BEFORE", false)));
assertTrue(metadata.contains(optionalDependencyBlock("Multiverse-Core", "AFTER", true)));
@@ -0,0 +1,80 @@
package art.arcane.iris.core.commands;
import art.arcane.iris.core.link.Identifier;
import art.arcane.iris.core.service.ExternalDataSVC;
import art.arcane.iris.engine.object.IObjectPlacer;
import art.arcane.iris.spi.IrisServices;
import art.arcane.iris.spi.PlatformBlockState;
import art.arcane.iris.util.common.data.IrisCustomData;
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.MockedStatic;
import java.util.HashMap;
import java.util.Map;
import static org.junit.Assert.assertSame;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyBoolean;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.mockStatic;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
public class CommandObjectCustomBlockPlacementTest {
@Test
public void customPasteUsesProviderAndKeepsOriginalSemanticUndoState() {
World world = mock(World.class);
Block target = mock(Block.class);
when(world.getBlockAt(7, 80, -9)).thenReturn(target);
when(target.getType()).thenReturn(Material.STONE);
BlockData previous = mock(BlockData.class);
when(target.getBlockData()).thenReturn(previous);
BlockData previousCustom = IrisCustomData.of(previous, new Identifier("craftengine", "test/lamp[lit=false]"));
Identifier id = new Identifier("craftengine", "test/lamp[lit=true]");
BlockData base = mock(BlockData.class);
PlatformBlockState state = mock(PlatformBlockState.class);
when(state.nativeHandle()).thenReturn(IrisCustomData.of(base, id));
ExternalDataSVC external = mock(ExternalDataSVC.class);
when(external.captureBlockData(previous)).thenReturn(previousCustom);
when(external.placeBlock(target, id)).thenReturn(true);
Map<Block, BlockData> changes = new HashMap<>();
try (MockedStatic<IrisServices> services = mockStatic(IrisServices.class)) {
services.when(() -> IrisServices.getOrNull(ExternalDataSVC.class)).thenReturn(external);
IObjectPlacer placer = CommandObject.createPlacer(world, changes, null);
placer.set(7, 80, -9, state);
placer.set(7, 80, -9, state);
assertSame(previousCustom, changes.get(target));
verify(external).captureBlockData(previous);
verify(external, times(2)).placeBlock(target, id);
verify(target, times(0)).setBlockData(any(BlockData.class), anyBoolean());
}
}
@Test
public void unsupportedFurniturePreservesBaseOnlyPreview() {
World world = mock(World.class);
Block target = mock(Block.class);
when(world.getBlockAt(7, 80, -9)).thenReturn(target);
when(target.getType()).thenReturn(Material.STONE);
BlockData base = mock(BlockData.class);
Identifier id = new Identifier("craftengine", "test/chair[variant=alpha]");
PlatformBlockState state = mock(PlatformBlockState.class);
when(state.nativeHandle()).thenReturn(IrisCustomData.of(base, id));
ExternalDataSVC external = mock(ExternalDataSVC.class);
try (MockedStatic<IrisServices> services = mockStatic(IrisServices.class)) {
services.when(() -> IrisServices.getOrNull(ExternalDataSVC.class)).thenReturn(external);
CommandObject.createPlacer(world, new HashMap<>(), null).set(7, 80, -9, state);
verify(external).placeBlock(target, id);
verify(target).setBlockData(base, false);
}
}
}
@@ -39,6 +39,9 @@ import java.util.Set;
import java.util.WeakHashMap;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ForkJoinPool;
import java.util.concurrent.ForkJoinTask;
import java.util.concurrent.RejectedExecutionException;
import java.util.concurrent.locks.ReentrantLock;
import java.util.function.IntBinaryOperator;
import java.util.function.IntConsumer;
@@ -516,11 +519,30 @@ public final class NativeStructureVolumeIndex {
long pending = stripeMask;
while (pending != 0L) {
int stripe = Long.numberOfTrailingZeros(pending);
originWindowStripes[stripe].lock();
lockOriginStripe(originWindowStripes[stripe]);
pending &= pending - 1L;
}
}
private static void lockOriginStripe(ReentrantLock lock) {
if (lock.tryLock()) {
return;
}
if (!ForkJoinTask.inForkJoinPool()) {
lock.lock();
return;
}
OriginLockBlocker blocker = new OriginLockBlocker(lock);
try {
ForkJoinPool.managedBlock(blocker);
} catch (InterruptedException interruption) {
Thread.currentThread().interrupt();
blocker.block();
} catch (RejectedExecutionException exhaustedPool) {
blocker.block();
}
}
private void unlockOriginWindow(long stripeMask) {
long pending = stripeMask;
while (pending != 0L) {
@@ -530,6 +552,32 @@ public final class NativeStructureVolumeIndex {
}
}
private static final class OriginLockBlocker implements ForkJoinPool.ManagedBlocker {
private final ReentrantLock lock;
private boolean acquired;
private OriginLockBlocker(ReentrantLock lock) {
this.lock = lock;
}
@Override
public boolean block() {
if (!acquired) {
lock.lock();
acquired = true;
}
return true;
}
@Override
public boolean isReleasable() {
if (!acquired) {
acquired = lock.tryLock();
}
return acquired;
}
}
private static GenerationHistoryRuntimeRouter.CoordinateScope openHistoryCoordinateScope(
Engine engine,
int chunkX,