mirror of
https://github.com/VolmitSoftware/Iris.git
synced 2026-10-26 22:10:18 +00:00
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:
1 parent
0782bbf0b4
commit
4de1aea8b9
44 files changed
+3161
-244
No files matched your search
+9
-3
@@ -8,6 +8,7 @@ import java.util.function.LongPredicate;
|
|||||||
import art.arcane.iris.engine.DimensionStackContext;
|
import art.arcane.iris.engine.DimensionStackContext;
|
||||||
import art.arcane.iris.engine.DimensionStackLayout;
|
import art.arcane.iris.engine.DimensionStackLayout;
|
||||||
import art.arcane.iris.engine.framework.Engine;
|
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.GenerationSessionException;
|
||||||
import art.arcane.iris.engine.framework.GenerationSessionLease;
|
import art.arcane.iris.engine.framework.GenerationSessionLease;
|
||||||
import art.arcane.iris.engine.framework.IrisStructureLocator;
|
import art.arcane.iris.engine.framework.IrisStructureLocator;
|
||||||
@@ -930,6 +931,13 @@ public class IrisChunkGenerator extends CustomChunkGenerator implements LongPred
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public WeightedList<MobSpawnSettings.SpawnerData> getMobsAt(Holder<Biome> holder, StructureManager structuremanager, MobCategory enumcreaturetype, BlockPos blockposition) {
|
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(
|
NativeBiomeSpawnSelection selection = NativeBiomeSpawnSelection.at(
|
||||||
engine, blockposition.getX(), blockposition.getY(), blockposition.getZ(),
|
engine, blockposition.getX(), blockposition.getY(), blockposition.getZ(),
|
||||||
holder.unwrapKey().map(key -> key.identifier().toString()).orElse(""));
|
holder.unwrapKey().map(key -> key.identifier().toString()).orElse(""));
|
||||||
@@ -937,9 +945,7 @@ public class IrisChunkGenerator extends CustomChunkGenerator implements LongPred
|
|||||||
return WeightedList.of(List.of());
|
return WeightedList.of(List.of());
|
||||||
}
|
}
|
||||||
try (GenerationHistoryRuntimeRouter.CoordinateScope route = openHistoryCoordinateScope(
|
try (GenerationHistoryRuntimeRouter.CoordinateScope route = openHistoryCoordinateScope(
|
||||||
blockposition.getX(), blockposition.getZ(), "bukkit_nms_mob_spawns");
|
blockposition.getX(), blockposition.getZ(), "bukkit_nms_mob_spawns")) {
|
||||||
GenerationSessionLease lease = requireGenerationLease("bukkit_nms_mob_spawns");
|
|
||||||
IrisContext.Scope ignored = IrisContext.open(engine, lease.sessionId(), null)) {
|
|
||||||
return getMobsAtWithActiveRuntime(holder, structuremanager, enumcreaturetype, blockposition, selection);
|
return getMobsAtWithActiveRuntime(holder, structuremanager, enumcreaturetype, blockposition, selection);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+73
-20
@@ -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.NativeTerrainReceipt;
|
||||||
import art.arcane.iris.engine.history.SavedTerrainChunk;
|
import art.arcane.iris.engine.history.SavedTerrainChunk;
|
||||||
import art.arcane.iris.util.common.scheduling.J;
|
import art.arcane.iris.util.common.scheduling.J;
|
||||||
|
import ca.spottedleaf.concurrentutil.util.Priority;
|
||||||
import ca.spottedleaf.moonrise.common.PlatformHooks;
|
import ca.spottedleaf.moonrise.common.PlatformHooks;
|
||||||
import ca.spottedleaf.moonrise.patches.chunk_system.io.MoonriseRegionFileIO;
|
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 ca.spottedleaf.moonrise.patches.chunk_system.scheduling.NewChunkHolder;
|
||||||
import net.minecraft.core.registries.BuiltInRegistries;
|
import net.minecraft.core.registries.BuiltInRegistries;
|
||||||
import net.minecraft.nbt.CompoundTag;
|
import net.minecraft.nbt.CompoundTag;
|
||||||
import net.minecraft.nbt.NbtIo;
|
import net.minecraft.nbt.NbtIo;
|
||||||
import net.minecraft.server.level.ServerLevel;
|
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.ChunkAccess;
|
||||||
import net.minecraft.world.level.chunk.storage.SerializableChunkData;
|
import net.minecraft.world.level.chunk.storage.SerializableChunkData;
|
||||||
import org.bukkit.World;
|
import org.bukkit.World;
|
||||||
@@ -18,12 +21,17 @@ import org.bukkit.craftbukkit.CraftWorld;
|
|||||||
import java.io.ByteArrayOutputStream;
|
import java.io.ByteArrayOutputStream;
|
||||||
import java.io.DataOutputStream;
|
import java.io.DataOutputStream;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
import java.nio.file.Path;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.concurrent.CompletableFuture;
|
import java.util.concurrent.CompletableFuture;
|
||||||
import java.util.concurrent.CompletionException;
|
import java.util.concurrent.CompletionException;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
import java.util.concurrent.locks.LockSupport;
|
||||||
|
|
||||||
final class NmsSavedTerrainCapture {
|
final class NmsSavedTerrainCapture {
|
||||||
|
private static final long WRITE_TIMEOUT_NANOS = TimeUnit.SECONDS.toNanos(30L);
|
||||||
|
|
||||||
private NmsSavedTerrainCapture() {
|
private NmsSavedTerrainCapture() {
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -53,9 +61,12 @@ final class NmsSavedTerrainCapture {
|
|||||||
});
|
});
|
||||||
return snapshot.thenApplyAsync(data -> {
|
return snapshot.thenApplyAsync(data -> {
|
||||||
try {
|
try {
|
||||||
|
Checkpoint checkpoint = checkpoint(chunkX, chunkZ, data);
|
||||||
|
awaitWrite(level, checkpoint.chunk(), System.nanoTime() + WRITE_TIMEOUT_NANOS);
|
||||||
if (data == null) {
|
if (data == null) {
|
||||||
return SavedTerrainChunk.read(world.getWorldFolder().toPath(), chunkX, chunkZ, minimumY, height);
|
return SavedTerrainChunk.read(world.getWorldFolder().toPath(), chunkX, chunkZ, minimumY, height);
|
||||||
}
|
}
|
||||||
|
verifyCheckpoint(world.getWorldFolder().toPath(), checkpoint);
|
||||||
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
|
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
|
||||||
try (DataOutputStream output = new DataOutputStream(bytes)) {
|
try (DataOutputStream output = new DataOutputStream(bytes)) {
|
||||||
NbtIo.write(data, output);
|
NbtIo.write(data, output);
|
||||||
@@ -77,17 +88,8 @@ final class NmsSavedTerrainCapture {
|
|||||||
J.runRegionFuture(world, holder.chunkX, holder.chunkZ, () -> {
|
J.runRegionFuture(world, holder.chunkX, holder.chunkZ, () -> {
|
||||||
try {
|
try {
|
||||||
ChunkAccess chunk = holder.getCurrentChunk();
|
ChunkAccess chunk = holder.getCurrentChunk();
|
||||||
if (chunk == null || !chunk.isUnsaved()) {
|
saved.complete(checkpoint(holder.chunkX, holder.chunkZ,
|
||||||
saved.complete(null);
|
chunk != null && chunk.isUnsaved() ? saveSnapshot(level, chunk) : 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)));
|
|
||||||
} catch (Throwable failure) {
|
} catch (Throwable failure) {
|
||||||
saved.completeExceptionally(failure);
|
saved.completeExceptionally(failure);
|
||||||
}
|
}
|
||||||
@@ -99,22 +101,70 @@ final class NmsSavedTerrainCapture {
|
|||||||
}
|
}
|
||||||
return CompletableFuture.allOf(saves.toArray(CompletableFuture[]::new)).thenRunAsync(() -> {
|
return CompletableFuture.allOf(saves.toArray(CompletableFuture[]::new)).thenRunAsync(() -> {
|
||||||
try {
|
try {
|
||||||
MoonriseRegionFileIO.flush(level, MoonriseRegionFileIO.RegionFileType.CHUNK_DATA);
|
List<Checkpoint> checkpoints = new ArrayList<>(saves.size());
|
||||||
MoonriseRegionFileIO.flushRegionStorages(level, MoonriseRegionFileIO.RegionFileType.CHUNK_DATA);
|
|
||||||
for (CompletableFuture<Checkpoint> future : saves) {
|
for (CompletableFuture<Checkpoint> future : saves) {
|
||||||
Checkpoint saved = future.join();
|
checkpoints.add(future.join());
|
||||||
if (saved == null) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
SavedTerrainChunk.verifyCheckpoint(world.getWorldFolder().toPath(), saved.chunkX(), saved.chunkZ(),
|
|
||||||
saved.status(), saved.receipt(), saved.structureActivation());
|
|
||||||
}
|
}
|
||||||
|
finishCheckpoint(level, world.getWorldFolder().toPath(), checkpoints);
|
||||||
} catch (IOException failure) {
|
} catch (IOException failure) {
|
||||||
throw new CompletionException(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) {
|
private static CompoundTag saveSnapshot(ServerLevel level, ChunkAccess chunk) {
|
||||||
SerializableChunkData snapshot = SerializableChunkData.copyOf(level, chunk);
|
SerializableChunkData snapshot = SerializableChunkData.copyOf(level, chunk);
|
||||||
PlatformHooks.get().chunkSyncSave(level, chunk, snapshot);
|
PlatformHooks.get().chunkSyncSave(level, chunk, snapshot);
|
||||||
@@ -124,6 +174,9 @@ final class NmsSavedTerrainCapture {
|
|||||||
return data;
|
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) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+144
@@ -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());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+114
@@ -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));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+135
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
+85
@@ -26,12 +26,16 @@ import java.util.Map;
|
|||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import java.util.concurrent.CountDownLatch;
|
import java.util.concurrent.CountDownLatch;
|
||||||
|
import java.util.concurrent.CompletableFuture;
|
||||||
import java.util.concurrent.CopyOnWriteArraySet;
|
import java.util.concurrent.CopyOnWriteArraySet;
|
||||||
import java.util.concurrent.ExecutorService;
|
import java.util.concurrent.ExecutorService;
|
||||||
import java.util.concurrent.Executors;
|
import java.util.concurrent.Executors;
|
||||||
import java.util.concurrent.Future;
|
import java.util.concurrent.Future;
|
||||||
|
import java.util.concurrent.ForkJoinPool;
|
||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
|
import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
import java.util.concurrent.atomic.AtomicInteger;
|
import java.util.concurrent.atomic.AtomicInteger;
|
||||||
|
import java.util.concurrent.atomic.AtomicReference;
|
||||||
import java.util.concurrent.locks.LockSupport;
|
import java.util.concurrent.locks.LockSupport;
|
||||||
import java.util.function.IntConsumer;
|
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
|
@Test
|
||||||
public void distantColdQueriesCanResolveOriginsConcurrently() throws Exception {
|
public void distantColdQueriesCanResolveOriginsConcurrently() throws Exception {
|
||||||
BlockingResolver resolver = new BlockingResolver();
|
BlockingResolver resolver = new BlockingResolver();
|
||||||
|
|||||||
+13
-2
@@ -26,6 +26,7 @@ import art.arcane.iris.core.pack.PackDirectoryResolver;
|
|||||||
import art.arcane.iris.core.runtime.ObjectStudioActivation;
|
import art.arcane.iris.core.runtime.ObjectStudioActivation;
|
||||||
import art.arcane.iris.core.runtime.StudioOpenCoordinator;
|
import art.arcane.iris.core.runtime.StudioOpenCoordinator;
|
||||||
import art.arcane.iris.core.runtime.WorldRuntimeControlService;
|
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.ObjectSVC;
|
||||||
import art.arcane.iris.core.service.StudioSVC;
|
import art.arcane.iris.core.service.StudioSVC;
|
||||||
import art.arcane.iris.core.service.WandSVC;
|
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.IrisObjectScale;
|
||||||
import art.arcane.iris.engine.object.TileData;
|
import art.arcane.iris.engine.object.TileData;
|
||||||
import art.arcane.iris.platform.bukkit.BukkitBlockState;
|
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.iris.spi.PlatformBlockState;
|
||||||
import art.arcane.volmlib.util.data.Cuboid;
|
import art.arcane.volmlib.util.data.Cuboid;
|
||||||
import art.arcane.iris.util.common.data.IrisCustomData;
|
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
|
//Prevent blocks being set in or bellow bedrock
|
||||||
if (y <= world.getMinHeight() || block.getType() == Material.BEDROCK) return;
|
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 (d instanceof IrisCustomData data) {
|
||||||
|
if (external != null && external.placeBlock(block, data.getCustom())) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
block.setBlockData(data.getBase(), false);
|
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);
|
} else block.setBlockData(d, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -53,11 +53,11 @@ dependencies:
|
|||||||
required: false
|
required: false
|
||||||
join-classpath: false
|
join-classpath: false
|
||||||
MythicLib:
|
MythicLib:
|
||||||
load: BEFORE
|
load: OMIT
|
||||||
required: false
|
required: false
|
||||||
join-classpath: true
|
join-classpath: true
|
||||||
MMOItems:
|
MMOItems:
|
||||||
load: BEFORE
|
load: OMIT
|
||||||
required: false
|
required: false
|
||||||
join-classpath: true
|
join-classpath: true
|
||||||
eco:
|
eco:
|
||||||
@@ -69,11 +69,11 @@ dependencies:
|
|||||||
required: false
|
required: false
|
||||||
join-classpath: true
|
join-classpath: true
|
||||||
MythicMobs:
|
MythicMobs:
|
||||||
load: BEFORE
|
load: OMIT
|
||||||
required: false
|
required: false
|
||||||
join-classpath: true
|
join-classpath: true
|
||||||
MythicCrucible:
|
MythicCrucible:
|
||||||
load: BEFORE
|
load: OMIT
|
||||||
required: false
|
required: false
|
||||||
join-classpath: true
|
join-classpath: true
|
||||||
KGenerators:
|
KGenerators:
|
||||||
|
|||||||
@@ -11,12 +11,8 @@ softdepend:
|
|||||||
- ItemsAdder
|
- ItemsAdder
|
||||||
- SCore
|
- SCore
|
||||||
- ExecutableItems
|
- ExecutableItems
|
||||||
- MythicLib
|
|
||||||
- MMOItems
|
|
||||||
- eco
|
- eco
|
||||||
- EcoItems
|
- EcoItems
|
||||||
- MythicMobs
|
|
||||||
- MythicCrucible
|
|
||||||
- KGenerators
|
- KGenerators
|
||||||
- WorldEdit
|
- WorldEdit
|
||||||
loadbefore: [ Multiverse-Core ]
|
loadbefore: [ Multiverse-Core ]
|
||||||
|
|||||||
@@ -41,8 +41,11 @@ public class PaperPluginMetadataTest {
|
|||||||
private static final List<String> JOINED_PLUGIN_IDS = OPTIONAL_PLUGIN_IDS.stream()
|
private static final List<String> JOINED_PLUGIN_IDS = OPTIONAL_PLUGIN_IDS.stream()
|
||||||
.filter(pluginId -> !"ExecutableItems".equals(pluginId) && !"Multiverse-Core".equals(pluginId))
|
.filter(pluginId -> !"ExecutableItems".equals(pluginId) && !"Multiverse-Core".equals(pluginId))
|
||||||
.toList();
|
.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()
|
private static final List<String> BUKKIT_SOFT_DEPEND_IDS = OPTIONAL_PLUGIN_IDS.stream()
|
||||||
.filter(pluginId -> !"Multiverse-Core".equals(pluginId))
|
.filter(pluginId -> !"Multiverse-Core".equals(pluginId))
|
||||||
|
.filter(pluginId -> !UNORDERED_PLUGIN_IDS.contains(pluginId))
|
||||||
.toList();
|
.toList();
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -72,7 +75,8 @@ public class PaperPluginMetadataTest {
|
|||||||
+ " description: Allows survival players to fell Iris-managed trees with an axe.\n"
|
+ " description: Allows survival players to fell Iris-managed trees with an axe.\n"
|
||||||
+ " default: op"));
|
+ " default: op"));
|
||||||
for (String pluginId : JOINED_PLUGIN_IDS) {
|
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("ExecutableItems", "BEFORE", false)));
|
||||||
assertTrue(metadata.contains(optionalDependencyBlock("Multiverse-Core", "AFTER", true)));
|
assertTrue(metadata.contains(optionalDependencyBlock("Multiverse-Core", "AFTER", true)));
|
||||||
|
|||||||
+80
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+49
-1
@@ -39,6 +39,9 @@ import java.util.Set;
|
|||||||
import java.util.WeakHashMap;
|
import java.util.WeakHashMap;
|
||||||
import java.util.concurrent.CompletableFuture;
|
import java.util.concurrent.CompletableFuture;
|
||||||
import java.util.concurrent.ConcurrentHashMap;
|
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.concurrent.locks.ReentrantLock;
|
||||||
import java.util.function.IntBinaryOperator;
|
import java.util.function.IntBinaryOperator;
|
||||||
import java.util.function.IntConsumer;
|
import java.util.function.IntConsumer;
|
||||||
@@ -516,11 +519,30 @@ public final class NativeStructureVolumeIndex {
|
|||||||
long pending = stripeMask;
|
long pending = stripeMask;
|
||||||
while (pending != 0L) {
|
while (pending != 0L) {
|
||||||
int stripe = Long.numberOfTrailingZeros(pending);
|
int stripe = Long.numberOfTrailingZeros(pending);
|
||||||
originWindowStripes[stripe].lock();
|
lockOriginStripe(originWindowStripes[stripe]);
|
||||||
pending &= pending - 1L;
|
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) {
|
private void unlockOriginWindow(long stripeMask) {
|
||||||
long pending = stripeMask;
|
long pending = stripeMask;
|
||||||
while (pending != 0L) {
|
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(
|
private static GenerationHistoryRuntimeRouter.CoordinateScope openHistoryCoordinateScope(
|
||||||
Engine engine,
|
Engine engine,
|
||||||
int chunkX,
|
int chunkX,
|
||||||
|
|||||||
@@ -453,6 +453,18 @@ sourceSets {
|
|||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
|
externalProviderTestImplementation(libs.craftengine.core) {
|
||||||
|
transitive = false
|
||||||
|
}
|
||||||
|
externalProviderTestImplementation(libs.craftengine.bukkit) {
|
||||||
|
transitive = false
|
||||||
|
}
|
||||||
|
externalProviderTestImplementation('net.momirealms:sparrow-nbt:0.22')
|
||||||
|
externalProviderTestImplementation('net.momirealms:sparrow-util:0.121')
|
||||||
|
externalProviderTestImplementation('net.momirealms:sparrow-reflection:0.33')
|
||||||
|
externalProviderTestImplementation('net.momirealms:sparrow-yaml:1.0.18')
|
||||||
|
externalProviderTestImplementation('net.momirealms:antigrieflib:1.0.17')
|
||||||
|
externalProviderTestImplementation('org.incendo:cloud-core:2.0.0')
|
||||||
externalProviderTestImplementation(libs.oraxen) {
|
externalProviderTestImplementation(libs.oraxen) {
|
||||||
transitive = false
|
transitive = false
|
||||||
}
|
}
|
||||||
|
|||||||
+371
@@ -0,0 +1,371 @@
|
|||||||
|
package art.arcane.iris.core.link.data;
|
||||||
|
|
||||||
|
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.engine.framework.SeedManager;
|
||||||
|
import art.arcane.iris.platform.bukkit.BukkitBlockResolution;
|
||||||
|
import art.arcane.iris.spi.IrisServices;
|
||||||
|
import art.arcane.iris.testsupport.BukkitTestServer;
|
||||||
|
import art.arcane.iris.util.common.data.IrisCustomData;
|
||||||
|
import art.arcane.iris.util.common.data.registry.RegistryUtil;
|
||||||
|
import art.arcane.volmlib.util.collection.KMap;
|
||||||
|
import net.momirealms.craftengine.bukkit.api.CraftEngineBlocks;
|
||||||
|
import net.momirealms.craftengine.bukkit.api.CraftEngineFurniture;
|
||||||
|
import net.momirealms.craftengine.bukkit.api.CraftEngineItems;
|
||||||
|
import net.momirealms.craftengine.bukkit.api.event.CraftEngineReloadEvent;
|
||||||
|
import net.momirealms.craftengine.bukkit.item.BukkitItemDefinition;
|
||||||
|
import net.momirealms.craftengine.bukkit.plugin.BukkitCraftEngine;
|
||||||
|
import net.momirealms.craftengine.core.block.BlockDefinition;
|
||||||
|
import net.momirealms.craftengine.core.block.ImmutableBlockState;
|
||||||
|
import net.momirealms.craftengine.core.block.property.BooleanProperty;
|
||||||
|
import net.momirealms.craftengine.core.entity.furniture.FurnitureDefinition;
|
||||||
|
import net.momirealms.craftengine.core.entity.furniture.FurnitureVariant;
|
||||||
|
import net.momirealms.craftengine.core.registry.Holder;
|
||||||
|
import net.momirealms.craftengine.core.util.Key;
|
||||||
|
import org.bukkit.Bukkit;
|
||||||
|
import org.bukkit.Location;
|
||||||
|
import org.bukkit.Material;
|
||||||
|
import org.bukkit.block.Block;
|
||||||
|
import org.bukkit.block.data.BlockData;
|
||||||
|
import org.bukkit.inventory.ItemStack;
|
||||||
|
import org.bukkit.plugin.Plugin;
|
||||||
|
import org.junit.Test;
|
||||||
|
import org.mockito.MockedStatic;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
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.ArgumentMatchers.any;
|
||||||
|
import static org.mockito.ArgumentMatchers.eq;
|
||||||
|
import static org.mockito.Mockito.doReturn;
|
||||||
|
import static org.mockito.Mockito.mock;
|
||||||
|
import static org.mockito.Mockito.mockStatic;
|
||||||
|
import static org.mockito.Mockito.spy;
|
||||||
|
import static org.mockito.Mockito.times;
|
||||||
|
import static org.mockito.Mockito.verify;
|
||||||
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
|
public class CraftEngineDataProviderTest {
|
||||||
|
@Test
|
||||||
|
public void startupWaitsForReloadEventAndEmptyLoadedRegistriesAreReady() {
|
||||||
|
CraftEngineDataProvider provider = spy(new CraftEngineDataProvider());
|
||||||
|
Plugin plugin = mock(Plugin.class);
|
||||||
|
doReturn(plugin).when(provider).getPlugin();
|
||||||
|
when(plugin.isEnabled()).thenReturn(true);
|
||||||
|
try (MockedStatic<BukkitCraftEngine> engine = mockStatic(BukkitCraftEngine.class);
|
||||||
|
MockedStatic<CraftEngineBlocks> blocks = mockStatic(CraftEngineBlocks.class);
|
||||||
|
MockedStatic<CraftEngineFurniture> furniture = mockStatic(CraftEngineFurniture.class);
|
||||||
|
MockedStatic<CraftEngineItems> items = mockStatic(CraftEngineItems.class)) {
|
||||||
|
provider.init();
|
||||||
|
|
||||||
|
assertFalse(provider.isReady());
|
||||||
|
assertTrue(provider.getTypes(DataType.BLOCK).isEmpty());
|
||||||
|
assertFalse(provider.isValidProvider(new Identifier("test", "lamp"), DataType.BLOCK));
|
||||||
|
blocks.verifyNoInteractions();
|
||||||
|
furniture.verifyNoInteractions();
|
||||||
|
items.verifyNoInteractions();
|
||||||
|
|
||||||
|
provider.onReload(mock(CraftEngineReloadEvent.class));
|
||||||
|
|
||||||
|
assertTrue(provider.isReady());
|
||||||
|
when(plugin.isEnabled()).thenReturn(false);
|
||||||
|
assertFalse(provider.isReady());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void lateInitializationUsesFullyLoadedFlagAndRefreshesExactClaims() {
|
||||||
|
CraftEngineDataProvider provider = new CraftEngineDataProvider();
|
||||||
|
BukkitCraftEngine plugin = mock(BukkitCraftEngine.class);
|
||||||
|
when(plugin.isFullyLoaded()).thenReturn(true);
|
||||||
|
Key oldKey = Key.of("test:old");
|
||||||
|
Key newKey = Key.of("test:new");
|
||||||
|
try (MockedStatic<BukkitCraftEngine> engine = mockStatic(BukkitCraftEngine.class);
|
||||||
|
MockedStatic<CraftEngineBlocks> blocks = mockStatic(CraftEngineBlocks.class);
|
||||||
|
MockedStatic<CraftEngineFurniture> furniture = mockStatic(CraftEngineFurniture.class);
|
||||||
|
MockedStatic<CraftEngineItems> items = mockStatic(CraftEngineItems.class);
|
||||||
|
MockedStatic<IrisServices> services = mockStatic(IrisServices.class)) {
|
||||||
|
ExternalDataSVC service = mock(ExternalDataSVC.class);
|
||||||
|
services.when(() -> IrisServices.getOrNull(ExternalDataSVC.class)).thenReturn(service);
|
||||||
|
engine.when(BukkitCraftEngine::instance).thenReturn(plugin);
|
||||||
|
blocks.when(CraftEngineBlocks::loadedBlocks).thenReturn(Map.of(oldKey, mock(BlockDefinition.class)));
|
||||||
|
items.when(CraftEngineItems::loadedItems).thenReturn(Map.of(oldKey, mock(BukkitItemDefinition.class)));
|
||||||
|
provider.init();
|
||||||
|
|
||||||
|
assertTrue(provider.isValidProvider(new Identifier("test", "old"), DataType.BLOCK));
|
||||||
|
assertTrue(provider.isValidProvider(new Identifier("test", "old"), DataType.ITEM));
|
||||||
|
assertFalse(provider.isValidProvider(new Identifier("test", "missing"), DataType.ITEM));
|
||||||
|
assertFalse(provider.isValidProvider(new Identifier("test", "old"), DataType.ENTITY));
|
||||||
|
|
||||||
|
blocks.when(CraftEngineBlocks::loadedBlocks).thenReturn(Map.of());
|
||||||
|
items.when(CraftEngineItems::loadedItems).thenReturn(Map.of(newKey, mock(BukkitItemDefinition.class)));
|
||||||
|
furniture.when(CraftEngineFurniture::loadedFurniture).thenReturn(Map.of(newKey, mock(FurnitureDefinition.class)));
|
||||||
|
when(plugin.isFullyLoaded()).thenReturn(false);
|
||||||
|
when(plugin.isReloading()).thenReturn(true);
|
||||||
|
provider.onReload(mock(CraftEngineReloadEvent.class));
|
||||||
|
|
||||||
|
assertFalse(provider.isValidProvider(new Identifier("test", "old"), DataType.BLOCK));
|
||||||
|
assertFalse(provider.isValidProvider(new Identifier("test", "old"), DataType.ITEM));
|
||||||
|
assertEquals(Set.of(new Identifier("test", "new")), provider.getTypes(DataType.BLOCK));
|
||||||
|
assertEquals(Set.of(new Identifier("test", "new")), provider.getTypes(DataType.ITEM));
|
||||||
|
verify(service).notifyContentChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void blockPropertiesProduceNativeCarrierAndCanonicalIdentifier() {
|
||||||
|
Identifier id = new Identifier("test", "lamp");
|
||||||
|
BooleanProperty property = BooleanProperty.create("lit", false);
|
||||||
|
BlockDefinition definition = mock(BlockDefinition.class);
|
||||||
|
ImmutableBlockState initial = blockState(definition, "test:lamp[lit=false]");
|
||||||
|
ImmutableBlockState lit = blockState(definition, "test:lamp[lit=true]");
|
||||||
|
BlockData data = mock(BlockData.class);
|
||||||
|
when(definition.defaultState()).thenReturn(initial);
|
||||||
|
doReturn(property).when(definition).getProperty("lit");
|
||||||
|
when(initial.with(property, true)).thenReturn(lit);
|
||||||
|
KMap<String, String> properties = new KMap<>();
|
||||||
|
properties.put("lit", "true");
|
||||||
|
try (MockedStatic<CraftEngineBlocks> blocks = mockStatic(CraftEngineBlocks.class)) {
|
||||||
|
blocks.when(() -> CraftEngineBlocks.byId(Key.of("test:lamp"))).thenReturn(definition);
|
||||||
|
blocks.when(() -> CraftEngineBlocks.getBukkitBlockData(lit)).thenReturn(data);
|
||||||
|
|
||||||
|
IrisCustomData result = (IrisCustomData) new CraftEngineDataProvider().getBlockData(id, properties);
|
||||||
|
|
||||||
|
assertSame(data, result.getBase());
|
||||||
|
assertEquals(new Identifier("test", "lamp[lit=true]"), result.getCustom());
|
||||||
|
verify(initial).with(property, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void unknownOrInvalidBlockPropertiesFailBeforeNativeConversion() {
|
||||||
|
Identifier id = new Identifier("test", "lamp");
|
||||||
|
BlockDefinition definition = mock(BlockDefinition.class);
|
||||||
|
ImmutableBlockState initial = blockState(definition, "test:lamp[lit=false]");
|
||||||
|
when(definition.defaultState()).thenReturn(initial);
|
||||||
|
doReturn(BooleanProperty.create("lit", false)).when(definition).getProperty("lit");
|
||||||
|
try (MockedStatic<CraftEngineBlocks> blocks = mockStatic(CraftEngineBlocks.class)) {
|
||||||
|
blocks.when(() -> CraftEngineBlocks.byId(Key.of("test:lamp"))).thenReturn(definition);
|
||||||
|
for (Map.Entry<String, String> entry : List.of(Map.entry("missing", "true"), Map.entry("lit", "yes"))) {
|
||||||
|
KMap<String, String> properties = new KMap<>();
|
||||||
|
properties.put(entry.getKey(), entry.getValue());
|
||||||
|
assertThrows(MissingResourceException.class, () -> new CraftEngineDataProvider().getBlockData(id, properties));
|
||||||
|
}
|
||||||
|
blocks.verify(() -> CraftEngineBlocks.getBukkitBlockData(any()), times(0));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void reverseCaptureUsesSemanticStateInsteadOfSyntheticNativeName() {
|
||||||
|
BlockData nativeData = mock(BlockData.class);
|
||||||
|
when(nativeData.getAsString()).thenReturn("craftengine:custom_12");
|
||||||
|
ImmutableBlockState state = blockState(mock(BlockDefinition.class), "test:lamp[lit=true]");
|
||||||
|
try (MockedStatic<CraftEngineBlocks> blocks = mockStatic(CraftEngineBlocks.class)) {
|
||||||
|
blocks.when(() -> CraftEngineBlocks.getCustomBlockState(nativeData)).thenReturn(state);
|
||||||
|
CraftEngineDataProvider provider = new CraftEngineDataProvider();
|
||||||
|
|
||||||
|
assertEquals(new Identifier("test", "lamp[lit=true]"), provider.identifyBlock(nativeData).orElseThrow());
|
||||||
|
assertTrue(provider.identifyBlock(mock(BlockData.class)).isEmpty());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void currentSyntheticSlotResolvesOnlyWhenBoundToCustomContent() {
|
||||||
|
CraftEngineDataProvider provider = new CraftEngineDataProvider();
|
||||||
|
Identifier id = new Identifier("craftengine", "custom_12");
|
||||||
|
BlockData data = mock(BlockData.class);
|
||||||
|
ImmutableBlockState state = blockState(mock(BlockDefinition.class), "test:lamp[lit=true]");
|
||||||
|
try (MockedStatic<Bukkit> bukkit = mockStatic(Bukkit.class);
|
||||||
|
MockedStatic<CraftEngineBlocks> blocks = mockStatic(CraftEngineBlocks.class);
|
||||||
|
MockedStatic<CraftEngineFurniture> furniture = mockStatic(CraftEngineFurniture.class);
|
||||||
|
MockedStatic<CraftEngineItems> items = mockStatic(CraftEngineItems.class)) {
|
||||||
|
bukkit.when(() -> Bukkit.createBlockData("craftengine:custom_12")).thenReturn(data);
|
||||||
|
blocks.when(() -> CraftEngineBlocks.getCustomBlockState(data)).thenReturn(state);
|
||||||
|
blocks.when(() -> CraftEngineBlocks.getBukkitBlockData(state)).thenReturn(data);
|
||||||
|
provider.onReload(mock(CraftEngineReloadEvent.class));
|
||||||
|
|
||||||
|
assertTrue(provider.isValidProvider(id, DataType.BLOCK));
|
||||||
|
IrisCustomData result = (IrisCustomData) provider.getBlockData(id, new KMap<>());
|
||||||
|
assertEquals(new Identifier("test", "lamp[lit=true]"), result.getCustom());
|
||||||
|
|
||||||
|
blocks.when(() -> CraftEngineBlocks.getCustomBlockState(data)).thenReturn(null);
|
||||||
|
assertFalse(provider.isValidProvider(id, DataType.BLOCK));
|
||||||
|
assertThrows(MissingResourceException.class, () -> provider.getBlockData(id, new KMap<>()));
|
||||||
|
bukkit.when(() -> Bukkit.createBlockData("craftengine:custom_12")).thenThrow(new IllegalArgumentException("Unknown block"));
|
||||||
|
assertFalse(provider.isValidProvider(id, DataType.BLOCK));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void deferredPlacementUsesCraftEngineAndAcceptsAlreadyMatchingState() {
|
||||||
|
Identifier id = new Identifier("test", "lamp");
|
||||||
|
BlockDefinition definition = mock(BlockDefinition.class);
|
||||||
|
ImmutableBlockState state = blockState(definition, "test:lamp");
|
||||||
|
when(definition.defaultState()).thenReturn(state);
|
||||||
|
Block block = mock(Block.class);
|
||||||
|
Location location = new Location(null, 17, 80, -12);
|
||||||
|
when(block.getLocation()).thenReturn(location);
|
||||||
|
try (MockedStatic<CraftEngineBlocks> blocks = mockStatic(CraftEngineBlocks.class)) {
|
||||||
|
blocks.when(() -> CraftEngineBlocks.byId(Key.of("test:lamp"))).thenReturn(definition);
|
||||||
|
blocks.when(() -> CraftEngineBlocks.getCustomBlockState(block)).thenReturn(state);
|
||||||
|
|
||||||
|
new CraftEngineDataProvider().processUpdate(mock(Engine.class), block, id);
|
||||||
|
|
||||||
|
blocks.verify(() -> CraftEngineBlocks.place(location, state, false));
|
||||||
|
blocks.when(() -> CraftEngineBlocks.getCustomBlockState(block)).thenReturn(null);
|
||||||
|
assertThrows(MissingResourceException.class, () -> new CraftEngineDataProvider().processUpdate(mock(Engine.class), block, id));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void missingBlockAndFurnitureCannotBePlaced() {
|
||||||
|
try (MockedStatic<CraftEngineBlocks> blocks = mockStatic(CraftEngineBlocks.class);
|
||||||
|
MockedStatic<CraftEngineFurniture> furniture = mockStatic(CraftEngineFurniture.class)) {
|
||||||
|
assertThrows(MissingResourceException.class, () -> new CraftEngineDataProvider().processUpdate(
|
||||||
|
mock(Engine.class), mock(Block.class), new Identifier("test", "missing")));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void furnitureValidatesPropertiesAndStoresDeterministicDefaultVariant() {
|
||||||
|
BukkitTestServer.install();
|
||||||
|
FurnitureDefinition definition = furnitureDefinition();
|
||||||
|
Identifier id = new Identifier("test", "chair");
|
||||||
|
BlockData air = mock(BlockData.class);
|
||||||
|
try (MockedStatic<CraftEngineBlocks> blocks = mockStatic(CraftEngineBlocks.class);
|
||||||
|
MockedStatic<CraftEngineFurniture> furniture = mockStatic(CraftEngineFurniture.class);
|
||||||
|
MockedStatic<RegistryUtil> registry = materialRegistry();
|
||||||
|
MockedStatic<BukkitBlockResolution> resolution = mockStatic(BukkitBlockResolution.class)) {
|
||||||
|
furniture.when(() -> CraftEngineFurniture.byId(Key.of("test:chair"))).thenReturn(definition);
|
||||||
|
resolution.when(BukkitBlockResolution::getAir).thenReturn(air);
|
||||||
|
CraftEngineDataProvider provider = new CraftEngineDataProvider();
|
||||||
|
IrisCustomData result = (IrisCustomData) provider.getBlockData(id, new KMap<>());
|
||||||
|
|
||||||
|
assertSame(air, result.getBase());
|
||||||
|
assertEquals("test:chair[variant=alpha]", result.getCustom().toString());
|
||||||
|
assertEquals("alpha", provider.getBlockProperties(id).stream()
|
||||||
|
.filter(property -> property.name().equals("variant")).findFirst().orElseThrow().defaultValue());
|
||||||
|
for (Map.Entry<String, String> invalid : List.of(Map.entry("variant", "missing"), Map.entry("randomYaw", "yes"),
|
||||||
|
Map.entry("randomPitch", "1"), Map.entry("yaw", "NaN"), Map.entry("yaw", "Infinity"),
|
||||||
|
Map.entry("pitch", "360"), Map.entry("pitch", "-1"), Map.entry("yaw", "word"), Map.entry("unknown", "true"))) {
|
||||||
|
KMap<String, String> properties = new KMap<>();
|
||||||
|
properties.put(invalid.getKey(), invalid.getValue());
|
||||||
|
assertThrows(MissingResourceException.class, () -> provider.getBlockData(id, properties));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void furniturePlacementUsesRepeatablePositionSeededRotation() {
|
||||||
|
FurnitureDefinition definition = furnitureDefinition();
|
||||||
|
Engine engine = mock(Engine.class);
|
||||||
|
SeedManager seeds = mock(SeedManager.class);
|
||||||
|
when(engine.getSeedManager()).thenReturn(seeds);
|
||||||
|
when(seeds.getSeed()).thenReturn(1234L);
|
||||||
|
Block block = mock(Block.class);
|
||||||
|
when(block.getX()).thenReturn(7);
|
||||||
|
when(block.getY()).thenReturn(80);
|
||||||
|
when(block.getZ()).thenReturn(-9);
|
||||||
|
when(block.getLocation()).thenAnswer(invocation -> new Location(null, 7, 80, -9));
|
||||||
|
Identifier id = new Identifier("test", "chair[randomYaw=true,randomPitch=true,variant=beta]");
|
||||||
|
try (MockedStatic<CraftEngineBlocks> blocks = mockStatic(CraftEngineBlocks.class);
|
||||||
|
MockedStatic<CraftEngineFurniture> furniture = mockStatic(CraftEngineFurniture.class)) {
|
||||||
|
furniture.when(() -> CraftEngineFurniture.byId(Key.of("test:chair"))).thenReturn(definition);
|
||||||
|
Location[] observed = new Location[2];
|
||||||
|
int[] count = new int[1];
|
||||||
|
furniture.when(() -> CraftEngineFurniture.place(any(Location.class), eq(definition), eq("beta"), eq(false)))
|
||||||
|
.thenAnswer(invocation -> {
|
||||||
|
observed[count[0]++] = invocation.getArgument(0, Location.class).clone();
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
CraftEngineDataProvider provider = new CraftEngineDataProvider();
|
||||||
|
assertThrows(MissingResourceException.class, () -> provider.processUpdate(engine, block, id));
|
||||||
|
assertThrows(MissingResourceException.class, () -> provider.processUpdate(engine, block, id));
|
||||||
|
|
||||||
|
assertEquals(observed[0], observed[1]);
|
||||||
|
assertTrue(observed[0].getYaw() >= 0 && observed[0].getYaw() < 360);
|
||||||
|
assertTrue(observed[0].getPitch() >= 0 && observed[0].getPitch() < 360);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void itemsAreBuiltThroughCraftEngineAndMissingItemsFail() {
|
||||||
|
BukkitItemDefinition definition = mock(BukkitItemDefinition.class);
|
||||||
|
ItemStack stack = mock(ItemStack.class);
|
||||||
|
when(definition.buildBukkitItem()).thenReturn(stack);
|
||||||
|
try (MockedStatic<CraftEngineItems> items = mockStatic(CraftEngineItems.class)) {
|
||||||
|
items.when(() -> CraftEngineItems.byId(Key.of("test:lamp"))).thenReturn(definition);
|
||||||
|
CraftEngineDataProvider provider = new CraftEngineDataProvider();
|
||||||
|
|
||||||
|
assertSame(stack, provider.getItemStack(new Identifier("test", "lamp"), new KMap<>()));
|
||||||
|
assertThrows(MissingResourceException.class, () -> provider.getItemStack(new Identifier("test", "missing"), new KMap<>()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void directPlacementUsesCustomBlockPropertiesWithoutAnEngine() {
|
||||||
|
BlockDefinition definition = mock(BlockDefinition.class);
|
||||||
|
BooleanProperty property = BooleanProperty.create("lit", false);
|
||||||
|
ImmutableBlockState initial = blockState(definition, "test:lamp[lit=false]");
|
||||||
|
ImmutableBlockState lit = blockState(definition, "test:lamp[lit=true]");
|
||||||
|
when(definition.defaultState()).thenReturn(initial);
|
||||||
|
doReturn(property).when(definition).getProperty("lit");
|
||||||
|
when(initial.with(property, true)).thenReturn(lit);
|
||||||
|
Block block = mock(Block.class);
|
||||||
|
Location location = new Location(null, 7, 80, -9);
|
||||||
|
when(block.getLocation()).thenReturn(location);
|
||||||
|
try (MockedStatic<CraftEngineBlocks> blocks = mockStatic(CraftEngineBlocks.class)) {
|
||||||
|
blocks.when(() -> CraftEngineBlocks.byId(Key.of("test:lamp"))).thenReturn(definition);
|
||||||
|
blocks.when(() -> CraftEngineBlocks.place(location, lit, false)).thenReturn(true);
|
||||||
|
|
||||||
|
assertTrue(new CraftEngineDataProvider().placeBlock(block, new Identifier("test", "lamp[lit=true]")));
|
||||||
|
|
||||||
|
blocks.verify(() -> CraftEngineBlocks.place(location, lit, false));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void directFurniturePlacementDoesNotSpawnEntities() {
|
||||||
|
FurnitureDefinition definition = furnitureDefinition();
|
||||||
|
Block block = mock(Block.class);
|
||||||
|
try (MockedStatic<CraftEngineBlocks> blocks = mockStatic(CraftEngineBlocks.class);
|
||||||
|
MockedStatic<CraftEngineFurniture> furniture = mockStatic(CraftEngineFurniture.class)) {
|
||||||
|
furniture.when(() -> CraftEngineFurniture.byId(Key.of("test:chair"))).thenReturn(definition);
|
||||||
|
|
||||||
|
assertFalse(new CraftEngineDataProvider().placeBlock(block, new Identifier("test", "chair[variant=alpha]")));
|
||||||
|
assertThrows(MissingResourceException.class, () -> new CraftEngineDataProvider().placeBlock(
|
||||||
|
block, new Identifier("test", "chair[variant=missing]")));
|
||||||
|
|
||||||
|
furniture.verify(() -> CraftEngineFurniture.place(any(Location.class), any(FurnitureDefinition.class), any(String.class), eq(false)), times(0));
|
||||||
|
verify(block, times(0)).getLocation();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static MockedStatic<RegistryUtil> materialRegistry() {
|
||||||
|
MockedStatic<RegistryUtil> registry = mockStatic(RegistryUtil.class);
|
||||||
|
registry.when(() -> RegistryUtil.find(Material.class, "grass", "short_grass")).thenReturn(Material.SHORT_GRASS);
|
||||||
|
return registry;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static ImmutableBlockState blockState(BlockDefinition definition, String semanticId) {
|
||||||
|
ImmutableBlockState state = mock(ImmutableBlockState.class);
|
||||||
|
when(state.owner()).thenReturn(Holder.direct(definition));
|
||||||
|
when(state.toString()).thenReturn(semanticId);
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static FurnitureDefinition furnitureDefinition() {
|
||||||
|
FurnitureDefinition definition = mock(FurnitureDefinition.class);
|
||||||
|
when(definition.id()).thenReturn(Key.of("test:chair"));
|
||||||
|
when(definition.variants()).thenReturn(Map.of("beta", mock(FurnitureVariant.class), "alpha", mock(FurnitureVariant.class)));
|
||||||
|
return definition;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -27,6 +27,7 @@ import java.util.Arrays;
|
|||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.MissingResourceException;
|
import java.util.MissingResourceException;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
@Getter
|
@Getter
|
||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
@@ -46,6 +47,14 @@ public abstract class ExternalDataProvider {
|
|||||||
|
|
||||||
public abstract void init();
|
public abstract void init();
|
||||||
|
|
||||||
|
public Optional<Identifier> identifyBlock(@NotNull BlockData blockData) {
|
||||||
|
return Optional.empty();
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean placeBlock(@NotNull Block block, @NotNull Identifier blockId) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @see ExternalDataProvider#getBlockData(Identifier, KMap)
|
* @see ExternalDataProvider#getBlockData(Identifier, KMap)
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -1,20 +1,24 @@
|
|||||||
package art.arcane.iris.core.link.data;
|
package art.arcane.iris.core.link.data;
|
||||||
|
|
||||||
import art.arcane.iris.platform.bukkit.BukkitBlockResolution;
|
|
||||||
|
|
||||||
import art.arcane.iris.core.link.ExternalDataProvider;
|
import art.arcane.iris.core.link.ExternalDataProvider;
|
||||||
import art.arcane.iris.core.link.Identifier;
|
import art.arcane.iris.core.link.Identifier;
|
||||||
import art.arcane.iris.core.nms.container.BlockProperty;
|
import art.arcane.iris.core.nms.container.BlockProperty;
|
||||||
|
import art.arcane.iris.core.nms.container.Pair;
|
||||||
import art.arcane.iris.core.service.ExternalDataSVC;
|
import art.arcane.iris.core.service.ExternalDataSVC;
|
||||||
import art.arcane.iris.engine.data.cache.Cache;
|
import art.arcane.iris.engine.data.cache.Cache;
|
||||||
import art.arcane.iris.engine.framework.Engine;
|
import art.arcane.iris.engine.framework.Engine;
|
||||||
|
import art.arcane.iris.platform.bukkit.BukkitBlockResolution;
|
||||||
|
import art.arcane.iris.spi.IrisLogging;
|
||||||
|
import art.arcane.iris.spi.IrisServices;
|
||||||
import art.arcane.iris.util.common.data.IrisCustomData;
|
import art.arcane.iris.util.common.data.IrisCustomData;
|
||||||
import art.arcane.volmlib.util.collection.KMap;
|
import art.arcane.volmlib.util.collection.KMap;
|
||||||
import art.arcane.volmlib.util.math.RNG;
|
import art.arcane.volmlib.util.math.RNG;
|
||||||
import net.momirealms.craftengine.bukkit.api.CraftEngineBlocks;
|
import net.momirealms.craftengine.bukkit.api.CraftEngineBlocks;
|
||||||
import net.momirealms.craftengine.bukkit.api.CraftEngineFurniture;
|
import net.momirealms.craftengine.bukkit.api.CraftEngineFurniture;
|
||||||
import net.momirealms.craftengine.bukkit.api.CraftEngineItems;
|
import net.momirealms.craftengine.bukkit.api.CraftEngineItems;
|
||||||
|
import net.momirealms.craftengine.bukkit.api.event.CraftEngineReloadEvent;
|
||||||
import net.momirealms.craftengine.bukkit.item.BukkitItemDefinition;
|
import net.momirealms.craftengine.bukkit.item.BukkitItemDefinition;
|
||||||
|
import net.momirealms.craftengine.bukkit.plugin.BukkitCraftEngine;
|
||||||
import net.momirealms.craftengine.core.block.BlockDefinition;
|
import net.momirealms.craftengine.core.block.BlockDefinition;
|
||||||
import net.momirealms.craftengine.core.block.ImmutableBlockState;
|
import net.momirealms.craftengine.core.block.ImmutableBlockState;
|
||||||
import net.momirealms.craftengine.core.block.property.BooleanProperty;
|
import net.momirealms.craftengine.core.block.property.BooleanProperty;
|
||||||
@@ -22,27 +26,36 @@ import net.momirealms.craftengine.core.block.property.IntegerProperty;
|
|||||||
import net.momirealms.craftengine.core.block.property.Property;
|
import net.momirealms.craftengine.core.block.property.Property;
|
||||||
import net.momirealms.craftengine.core.entity.furniture.FurnitureDefinition;
|
import net.momirealms.craftengine.core.entity.furniture.FurnitureDefinition;
|
||||||
import net.momirealms.craftengine.core.util.Key;
|
import net.momirealms.craftengine.core.util.Key;
|
||||||
|
import org.bukkit.Bukkit;
|
||||||
import org.bukkit.Location;
|
import org.bukkit.Location;
|
||||||
import org.bukkit.block.Block;
|
import org.bukkit.block.Block;
|
||||||
import org.bukkit.block.data.BlockData;
|
import org.bukkit.block.data.BlockData;
|
||||||
|
import org.bukkit.event.EventHandler;
|
||||||
|
import org.bukkit.event.Listener;
|
||||||
import org.bukkit.inventory.ItemStack;
|
import org.bukkit.inventory.ItemStack;
|
||||||
import org.jetbrains.annotations.NotNull;
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
|
||||||
import java.util.Arrays;
|
import java.util.ArrayList;
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
|
import java.util.LinkedHashSet;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.MissingResourceException;
|
import java.util.MissingResourceException;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.Set;
|
||||||
import java.util.function.Function;
|
import java.util.function.Function;
|
||||||
import java.util.stream.Stream;
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
public class CraftEngineDataProvider extends ExternalDataProvider {
|
public class CraftEngineDataProvider extends ExternalDataProvider implements Listener {
|
||||||
private static final BlockProperty[] FURNITURE_PROPERTIES = new BlockProperty[]{
|
private static final List<BlockProperty> FURNITURE_PROPERTIES = List.of(
|
||||||
BlockProperty.ofBoolean("randomYaw", false),
|
BlockProperty.ofBoolean("randomYaw", false),
|
||||||
BlockProperty.ofDouble("yaw", 0, 0, 360f, false, true),
|
BlockProperty.ofDouble("yaw", 0, 0, 360f, false, true),
|
||||||
BlockProperty.ofBoolean("randomPitch", false),
|
BlockProperty.ofBoolean("randomPitch", false),
|
||||||
BlockProperty.ofDouble("pitch", 0, 0, 360f, false, true),
|
BlockProperty.ofDouble("pitch", 0, 0, 360f, false, true)
|
||||||
};
|
);
|
||||||
|
private static final ContentRegistry EMPTY_CONTENT = new ContentRegistry(Set.of(), Set.of(), false);
|
||||||
|
|
||||||
|
private volatile ContentRegistry content = EMPTY_CONTENT;
|
||||||
|
|
||||||
public CraftEngineDataProvider() {
|
public CraftEngineDataProvider() {
|
||||||
super("CraftEngine");
|
super("CraftEngine");
|
||||||
@@ -50,130 +63,244 @@ public class CraftEngineDataProvider extends ExternalDataProvider {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void init() {
|
public void init() {
|
||||||
|
content = EMPTY_CONTENT;
|
||||||
|
BukkitCraftEngine plugin = BukkitCraftEngine.instance();
|
||||||
|
if (plugin != null && plugin.isFullyLoaded()) {
|
||||||
|
updateContent();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isReady() {
|
||||||
|
return content.ready() && super.isReady();
|
||||||
|
}
|
||||||
|
|
||||||
|
@EventHandler
|
||||||
|
public void onReload(CraftEngineReloadEvent event) {
|
||||||
|
if (!updateContent()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
ExternalDataSVC service = IrisServices.getOrNull(ExternalDataSVC.class);
|
||||||
|
if (service != null) {
|
||||||
|
service.notifyContentChanged();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public @NotNull List<BlockProperty> getBlockProperties(@NotNull Identifier blockId) throws MissingResourceException {
|
public @NotNull List<BlockProperty> getBlockProperties(@NotNull Identifier blockId) throws MissingResourceException {
|
||||||
Key key = Key.of(blockId.namespace(), blockId.key());
|
ImmutableBlockState blockState = findBlockState(blockId);
|
||||||
BlockDefinition block = CraftEngineBlocks.byId(key);
|
if (blockState != null) {
|
||||||
if (block != null) {
|
return blockState.owner().value().properties().stream().map(CraftEngineDataProvider::convert).toList();
|
||||||
return block.properties().stream().map(CraftEngineDataProvider::convert).toList();
|
|
||||||
}
|
}
|
||||||
|
FurnitureDefinition furniture = requireFurniture(blockId);
|
||||||
FurnitureDefinition furniture = CraftEngineFurniture.byId(key);
|
List<BlockProperty> properties = new ArrayList<>(FURNITURE_PROPERTIES);
|
||||||
if (furniture != null) {
|
properties.add(new BlockProperty("variant", String.class, defaultVariant(furniture),
|
||||||
BlockProperty[] properties = Arrays.copyOf(FURNITURE_PROPERTIES, 5);
|
furniture.variants().keySet(), Function.identity()));
|
||||||
properties[4] = new BlockProperty(
|
return List.copyOf(properties);
|
||||||
"variant",
|
|
||||||
String.class,
|
|
||||||
furniture.anyVariantName(),
|
|
||||||
furniture.variants().keySet(),
|
|
||||||
Function.identity()
|
|
||||||
);
|
|
||||||
return List.of(properties);
|
|
||||||
}
|
|
||||||
|
|
||||||
throw new MissingResourceException("Failed to find BlockData!", blockId.namespace(), blockId.key());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public @NotNull ItemStack getItemStack(@NotNull Identifier itemId, @NotNull KMap<String, Object> customNbt) throws MissingResourceException {
|
public @NotNull ItemStack getItemStack(@NotNull Identifier itemId, @NotNull KMap<String, Object> customNbt) throws MissingResourceException {
|
||||||
BukkitItemDefinition item = CraftEngineItems.byId(Key.of(itemId.namespace(), itemId.key()));
|
BukkitItemDefinition item = CraftEngineItems.byId(Key.of(itemId.namespace(), itemId.key()));
|
||||||
if (item == null) {
|
if (item == null) {
|
||||||
throw new MissingResourceException("Failed to find ItemData!", itemId.namespace(), itemId.key());
|
throw missing(itemId, "Failed to find CraftEngine item.");
|
||||||
}
|
}
|
||||||
|
|
||||||
return item.buildBukkitItem();
|
return item.buildBukkitItem();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public @NotNull BlockData getBlockData(@NotNull Identifier blockId, @NotNull KMap<String, String> state) throws MissingResourceException {
|
public @NotNull BlockData getBlockData(@NotNull Identifier blockId, @NotNull KMap<String, String> state) throws MissingResourceException {
|
||||||
Key key = Key.of(blockId.namespace(), blockId.key());
|
ImmutableBlockState blockState = findBlockState(blockId);
|
||||||
if (CraftEngineBlocks.byId(key) == null && CraftEngineFurniture.byId(key) == null) {
|
if (blockState != null) {
|
||||||
throw new MissingResourceException("Failed to find BlockData!", blockId.namespace(), blockId.key());
|
ImmutableBlockState resolved = applyProperties(blockId, blockState, state);
|
||||||
|
return IrisCustomData.of(CraftEngineBlocks.getBukkitBlockData(resolved), Identifier.fromString(resolved.toString()));
|
||||||
}
|
}
|
||||||
|
FurnitureDefinition furniture = requireFurniture(blockId);
|
||||||
|
KMap<String, String> properties = validateFurnitureProperties(blockId, furniture, state);
|
||||||
|
return IrisCustomData.of(BukkitBlockResolution.getAir(), ExternalDataSVC.buildState(blockId, properties));
|
||||||
|
}
|
||||||
|
|
||||||
return IrisCustomData.of(BukkitBlockResolution.getAir(), ExternalDataSVC.buildState(blockId, state));
|
@Override
|
||||||
|
public Optional<Identifier> identifyBlock(@NotNull BlockData blockData) {
|
||||||
|
BlockData nativeData = blockData instanceof IrisCustomData custom ? custom.getBase() : blockData;
|
||||||
|
ImmutableBlockState state = CraftEngineBlocks.getCustomBlockState(nativeData);
|
||||||
|
return state == null || state.isEmpty() ? Optional.empty() : Optional.of(Identifier.fromString(state.toString()));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean placeBlock(@NotNull Block block, @NotNull Identifier blockId) {
|
||||||
|
Pair<Identifier, KMap<String, String>> parsed = ExternalDataSVC.parseState(blockId);
|
||||||
|
ImmutableBlockState state = findBlockState(parsed.getA());
|
||||||
|
if (state == null) {
|
||||||
|
validateFurnitureProperties(parsed.getA(), requireFurniture(parsed.getA()), parsed.getB());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
placeBlockState(block, applyProperties(parsed.getA(), state, parsed.getB()), blockId);
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void processUpdate(@NotNull Engine engine, @NotNull Block block, @NotNull Identifier blockId) {
|
public void processUpdate(@NotNull Engine engine, @NotNull Block block, @NotNull Identifier blockId) {
|
||||||
art.arcane.iris.core.nms.container.Pair<Identifier, KMap<String, String>> statePair = ExternalDataSVC.parseState(blockId);
|
Pair<Identifier, KMap<String, String>> statePair = ExternalDataSVC.parseState(blockId);
|
||||||
Identifier baseBlockId = statePair.getA();
|
Identifier baseBlockId = statePair.getA();
|
||||||
KMap<String, String> state = statePair.getB();
|
KMap<String, String> state = statePair.getB();
|
||||||
Key key = Key.of(baseBlockId.namespace(), baseBlockId.key());
|
ImmutableBlockState blockState = findBlockState(baseBlockId);
|
||||||
|
if (blockState != null) {
|
||||||
|
placeBlockState(block, applyProperties(baseBlockId, blockState, state), blockId);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
FurnitureDefinition furniture = requireFurniture(baseBlockId);
|
||||||
|
KMap<String, String> properties = validateFurnitureProperties(baseBlockId, furniture, state);
|
||||||
|
Location location = parseYawAndPitch(engine, block, properties);
|
||||||
|
if (CraftEngineFurniture.place(location, furniture, properties.get("variant"), false) == null) {
|
||||||
|
throw missing(blockId, "Failed to place CraftEngine furniture.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
BlockDefinition customBlock = CraftEngineBlocks.byId(key);
|
@Override
|
||||||
if (customBlock != null) {
|
public @NotNull Collection<@NotNull Identifier> getTypes(@NotNull DataType dataType) {
|
||||||
ImmutableBlockState blockState = customBlock.defaultState();
|
return switch (dataType) {
|
||||||
|
case ENTITY -> List.of();
|
||||||
|
case ITEM -> content.items();
|
||||||
|
case BLOCK -> content.blocks();
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
for (Map.Entry<String, String> entry : state.entrySet()) {
|
@Override
|
||||||
Property<?> property = customBlock.getProperty(entry.getKey());
|
public boolean isValidProvider(@NotNull Identifier id, DataType dataType) {
|
||||||
if (property == null) {
|
ContentRegistry registry = content;
|
||||||
continue;
|
if (!registry.ready()) {
|
||||||
}
|
return false;
|
||||||
|
}
|
||||||
|
return switch (dataType) {
|
||||||
|
case ENTITY -> false;
|
||||||
|
case ITEM -> registry.items().contains(id);
|
||||||
|
case BLOCK -> registry.blocks().contains(id) || findRuntimeBlockState(id) != null;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
Comparable<?> tag = property.optional(entry.getValue()).orElse(null);
|
private boolean updateContent() {
|
||||||
if (tag == null) {
|
try {
|
||||||
continue;
|
Set<Identifier> blocks = new LinkedHashSet<>();
|
||||||
}
|
for (Key key : CraftEngineBlocks.loadedBlocks().keySet()) {
|
||||||
|
blocks.add(new Identifier(key.namespace(), key.value()));
|
||||||
blockState = ImmutableBlockState.with(blockState, property, tag);
|
|
||||||
}
|
}
|
||||||
|
for (Key key : CraftEngineFurniture.loadedFurniture().keySet()) {
|
||||||
CraftEngineBlocks.place(block.getLocation(), blockState, false);
|
blocks.add(new Identifier(key.namespace(), key.value()));
|
||||||
return;
|
}
|
||||||
|
Set<Identifier> items = CraftEngineItems.loadedItems().keySet().stream()
|
||||||
|
.map(key -> new Identifier(key.namespace(), key.value())).collect(Collectors.toUnmodifiableSet());
|
||||||
|
content = new ContentRegistry(items, Set.copyOf(blocks), true);
|
||||||
|
return true;
|
||||||
|
} catch (RuntimeException | LinkageError error) {
|
||||||
|
content = EMPTY_CONTENT;
|
||||||
|
IrisLogging.reportError("Failed to update CraftEngine content registry.", error);
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
FurnitureDefinition furniture = CraftEngineFurniture.byId(key);
|
private static ImmutableBlockState findBlockState(Identifier blockId) {
|
||||||
|
BlockDefinition block = CraftEngineBlocks.byId(Key.of(blockId.namespace(), blockId.key()));
|
||||||
|
return block != null ? block.defaultState() : findRuntimeBlockState(blockId);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static ImmutableBlockState findRuntimeBlockState(Identifier blockId) {
|
||||||
|
if (!Key.CRAFTENGINE_NAMESPACE.equals(blockId.namespace())) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
BlockData data;
|
||||||
|
try {
|
||||||
|
data = Bukkit.createBlockData(blockId.toString());
|
||||||
|
} catch (IllegalArgumentException error) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
ImmutableBlockState state = CraftEngineBlocks.getCustomBlockState(data);
|
||||||
|
return state == null || state.isEmpty() ? null : state;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static ImmutableBlockState applyProperties(Identifier id, ImmutableBlockState state, Map<String, String> properties) {
|
||||||
|
BlockDefinition block = state.owner().value();
|
||||||
|
for (Map.Entry<String, String> entry : properties.entrySet()) {
|
||||||
|
Property<?> property = block.getProperty(entry.getKey());
|
||||||
|
if (property == null) {
|
||||||
|
throw missing(id, "Unknown CraftEngine block property '" + entry.getKey() + "'.");
|
||||||
|
}
|
||||||
|
Comparable<?> value = property.optional(entry.getValue()).orElse(null);
|
||||||
|
if (value == null) {
|
||||||
|
throw missing(id, "Invalid CraftEngine block property '" + entry.getKey() + "=" + entry.getValue() + "'.");
|
||||||
|
}
|
||||||
|
state = ImmutableBlockState.with(state, property, value);
|
||||||
|
}
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void placeBlockState(Block block, ImmutableBlockState state, Identifier id) {
|
||||||
|
if (!CraftEngineBlocks.place(block.getLocation(), state, false)
|
||||||
|
&& !state.equals(CraftEngineBlocks.getCustomBlockState(block))) {
|
||||||
|
throw missing(id, "Failed to place CraftEngine block.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static FurnitureDefinition requireFurniture(Identifier id) {
|
||||||
|
FurnitureDefinition furniture = CraftEngineFurniture.byId(Key.of(id.namespace(), id.key()));
|
||||||
if (furniture == null) {
|
if (furniture == null) {
|
||||||
return;
|
throw missing(id, "Failed to find CraftEngine block or furniture.");
|
||||||
}
|
}
|
||||||
|
return furniture;
|
||||||
|
}
|
||||||
|
|
||||||
Location location = parseYawAndPitch(engine, block, state);
|
private static KMap<String, String> validateFurnitureProperties(Identifier id, FurnitureDefinition furniture, Map<String, String> state) {
|
||||||
String variant = state.getOrDefault("variant", furniture.anyVariantName());
|
KMap<String, String> properties = new KMap<>();
|
||||||
CraftEngineFurniture.place(location, furniture, variant, false);
|
for (Map.Entry<String, String> entry : state.entrySet()) {
|
||||||
|
String value = entry.getValue();
|
||||||
|
switch (entry.getKey()) {
|
||||||
|
case "randomYaw", "randomPitch" -> {
|
||||||
|
if (!"true".equals(value) && !"false".equals(value)) {
|
||||||
|
throw missing(id, "Invalid CraftEngine furniture boolean '" + entry.getKey() + "=" + value + "'.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case "yaw", "pitch" -> validateAngle(id, entry.getKey(), value);
|
||||||
|
case "variant" -> {
|
||||||
|
if (!furniture.variants().containsKey(value)) {
|
||||||
|
throw missing(id, "Unknown CraftEngine furniture variant '" + value + "'.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
default -> throw missing(id, "Unknown CraftEngine furniture property '" + entry.getKey() + "'.");
|
||||||
|
}
|
||||||
|
properties.put(entry.getKey(), value);
|
||||||
|
}
|
||||||
|
properties.putIfAbsent("variant", defaultVariant(furniture));
|
||||||
|
return properties;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void validateAngle(Identifier id, String property, String value) {
|
||||||
|
float angle;
|
||||||
|
try {
|
||||||
|
angle = Float.parseFloat(value);
|
||||||
|
} catch (NumberFormatException error) {
|
||||||
|
throw missing(id, "Invalid CraftEngine furniture angle '" + property + "=" + value + "'.");
|
||||||
|
}
|
||||||
|
if (!Float.isFinite(angle) || angle < 0 || angle >= 360) {
|
||||||
|
throw missing(id, "CraftEngine furniture angle '" + property + "' must be at least 0 and below 360.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String defaultVariant(FurnitureDefinition furniture) {
|
||||||
|
return furniture.variants().keySet().stream().sorted().findFirst()
|
||||||
|
.orElseThrow(() -> missing(Identifier.fromString(furniture.id().toString()), "CraftEngine furniture has no variants."));
|
||||||
}
|
}
|
||||||
|
|
||||||
private static Location parseYawAndPitch(@NotNull Engine engine, @NotNull Block block, @NotNull Map<String, String> state) {
|
private static Location parseYawAndPitch(@NotNull Engine engine, @NotNull Block block, @NotNull Map<String, String> state) {
|
||||||
Location location = block.getLocation();
|
Location location = block.getLocation();
|
||||||
long seed = engine.getSeedManager().getSeed() + Cache.key(block.getX(), block.getZ()) + block.getY();
|
long seed = engine.getSeedManager().getSeed() + Cache.key(block.getX(), block.getZ()) + block.getY();
|
||||||
RNG rng = new RNG(seed);
|
RNG rng = new RNG(seed);
|
||||||
|
location.setYaw("true".equals(state.get("randomYaw")) ? rng.f(0, 360) : Float.parseFloat(state.getOrDefault("yaw", "0")));
|
||||||
if ("true".equals(state.get("randomYaw"))) {
|
location.setPitch("true".equals(state.get("randomPitch")) ? rng.f(0, 360) : Float.parseFloat(state.getOrDefault("pitch", "0")));
|
||||||
location.setYaw(rng.f(0, 360));
|
|
||||||
} else if (state.containsKey("yaw")) {
|
|
||||||
location.setYaw(Float.parseFloat(state.get("yaw")));
|
|
||||||
}
|
|
||||||
|
|
||||||
if ("true".equals(state.get("randomPitch"))) {
|
|
||||||
location.setPitch(rng.f(0, 360));
|
|
||||||
} else if (state.containsKey("pitch")) {
|
|
||||||
location.setPitch(Float.parseFloat(state.get("pitch")));
|
|
||||||
}
|
|
||||||
|
|
||||||
return location;
|
return location;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
private static MissingResourceException missing(Identifier id, String message) {
|
||||||
public @NotNull Collection<@NotNull Identifier> getTypes(@NotNull DataType dataType) {
|
return new MissingResourceException(message, id.namespace(), id.key());
|
||||||
Stream<Key> keys = switch (dataType) {
|
|
||||||
case ENTITY -> Stream.<Key>empty();
|
|
||||||
case ITEM -> CraftEngineItems.loadedItems().keySet().stream();
|
|
||||||
case BLOCK -> Stream.concat(CraftEngineBlocks.loadedBlocks().keySet().stream(),
|
|
||||||
CraftEngineFurniture.loadedFurniture().keySet().stream());
|
|
||||||
};
|
|
||||||
return keys.map(key -> new Identifier(key.namespace(), key.value())).toList();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean isValidProvider(@NotNull Identifier id, DataType dataType) {
|
|
||||||
Key key = Key.of(id.namespace(), id.key());
|
|
||||||
return switch (dataType) {
|
|
||||||
case ENTITY -> false;
|
|
||||||
case ITEM -> CraftEngineItems.byId(key) != null;
|
|
||||||
case BLOCK -> CraftEngineBlocks.byId(key) != null || CraftEngineFurniture.byId(key) != null;
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static <T extends Comparable<T>> BlockProperty convert(Property<T> raw) {
|
private static <T extends Comparable<T>> BlockProperty convert(Property<T> raw) {
|
||||||
@@ -183,4 +310,7 @@ public class CraftEngineDataProvider extends ExternalDataProvider {
|
|||||||
default -> new BlockProperty(raw.name(), raw.valueClass(), raw.defaultValue(), raw.possibleValues(), raw::valueName);
|
default -> new BlockProperty(raw.name(), raw.valueClass(), raw.defaultValue(), raw.possibleValues(), raw::valueName);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private record ContentRegistry(Set<Identifier> items, Set<Identifier> blocks, boolean ready) {
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -27,6 +27,7 @@ import art.arcane.iris.core.loader.IrisData;
|
|||||||
import art.arcane.iris.core.tools.IrisToolbelt;
|
import art.arcane.iris.core.tools.IrisToolbelt;
|
||||||
import art.arcane.iris.engine.framework.BiomeEnvironment;
|
import art.arcane.iris.engine.framework.BiomeEnvironment;
|
||||||
import art.arcane.iris.engine.framework.Engine;
|
import art.arcane.iris.engine.framework.Engine;
|
||||||
|
import art.arcane.iris.engine.framework.EngineLifecycleTasks;
|
||||||
import art.arcane.iris.engine.history.SavedBiomeUnavailableException;
|
import art.arcane.iris.engine.history.SavedBiomeUnavailableException;
|
||||||
import art.arcane.iris.engine.platform.PlatformChunkGenerator;
|
import art.arcane.iris.engine.platform.PlatformChunkGenerator;
|
||||||
import art.arcane.volmlib.util.board.Board;
|
import art.arcane.volmlib.util.board.Board;
|
||||||
@@ -498,8 +499,13 @@ public class BoardSVC implements IrisService, BoardProvider {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
EngineLifecycleTasks.run(engine, "bukkit_studio_scoreboard",
|
||||||
|
() -> updateOrdinary(engine, loc, world.getMinHeight()));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updateOrdinary(Engine engine, Location loc, int minimumY) {
|
||||||
int x = loc.getBlockX();
|
int x = loc.getBlockX();
|
||||||
int y = loc.getBlockY() - world.getMinHeight();
|
int y = loc.getBlockY() - minimumY;
|
||||||
int z = loc.getBlockZ();
|
int z = loc.getBlockZ();
|
||||||
|
|
||||||
List<String> lines = new ArrayList<>(this.lines.size());
|
List<String> lines = new ArrayList<>(this.lines.size());
|
||||||
|
|||||||
@@ -180,6 +180,20 @@ public class ExternalDataSVC implements IrisService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public BlockData captureBlockData(BlockData blockData) {
|
||||||
|
Objects.requireNonNull(blockData);
|
||||||
|
if (blockData instanceof IrisCustomData) {
|
||||||
|
return blockData;
|
||||||
|
}
|
||||||
|
for (ExternalDataProvider provider : activeProviders) {
|
||||||
|
Optional<Identifier> identifier = provider.identifyBlock(blockData);
|
||||||
|
if (identifier.isPresent()) {
|
||||||
|
return IrisCustomData.of(blockData, qualifyBlockId(provider, identifier.get()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return blockData;
|
||||||
|
}
|
||||||
|
|
||||||
public Optional<BlockData> getBlockData(final Identifier key) {
|
public Optional<BlockData> getBlockData(final Identifier key) {
|
||||||
Pair<Identifier, KMap<String, String>> pair;
|
Pair<Identifier, KMap<String, String>> pair;
|
||||||
try {
|
try {
|
||||||
@@ -244,6 +258,15 @@ public class ExternalDataSVC implements IrisService {
|
|||||||
match.provider().processUpdate(engine, block, buildState(match.identifier(), state.getB()));
|
match.provider().processUpdate(engine, block, buildState(match.identifier(), state.getB()));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public boolean placeBlock(Block block, Identifier blockId) {
|
||||||
|
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());
|
||||||
|
}
|
||||||
|
return match.provider().placeBlock(block, buildState(match.identifier(), state.getB()));
|
||||||
|
}
|
||||||
|
|
||||||
public Entity spawnMob(Location location, Identifier mobId) {
|
public Entity spawnMob(Location location, Identifier mobId) {
|
||||||
Optional<ExternalDataProvider> provider = activeProviders.stream().filter(p -> p.isValidProvider(mobId, DataType.ENTITY)).findFirst();
|
Optional<ExternalDataProvider> provider = activeProviders.stream().filter(p -> p.isValidProvider(mobId, DataType.ENTITY)).findFirst();
|
||||||
if (provider.isEmpty()) {
|
if (provider.isEmpty()) {
|
||||||
|
|||||||
@@ -18,6 +18,9 @@
|
|||||||
|
|
||||||
package art.arcane.iris.core.service;
|
package art.arcane.iris.core.service;
|
||||||
|
|
||||||
|
import art.arcane.iris.spi.IrisLogging;
|
||||||
|
import art.arcane.iris.spi.IrisServices;
|
||||||
|
import art.arcane.iris.util.common.data.IrisCustomData;
|
||||||
import art.arcane.iris.util.common.plugin.IrisService;
|
import art.arcane.iris.util.common.plugin.IrisService;
|
||||||
import art.arcane.iris.util.common.scheduling.J;
|
import art.arcane.iris.util.common.scheduling.J;
|
||||||
import lombok.Getter;
|
import lombok.Getter;
|
||||||
@@ -81,7 +84,11 @@ public class ObjectSVC implements IrisService {
|
|||||||
int amount = 0;
|
int amount = 0;
|
||||||
while (it.hasNext()) {
|
while (it.hasNext()) {
|
||||||
Map.Entry<Block, BlockData> entry = it.next();
|
Map.Entry<Block, BlockData> entry = it.next();
|
||||||
entry.getKey().setBlockData(entry.getValue(), false);
|
Block block = entry.getKey();
|
||||||
|
BlockData data = entry.getValue();
|
||||||
|
if (!J.runAt(block.getLocation(), () -> restoreBlock(block, data))) {
|
||||||
|
IrisLogging.warn("Could not schedule object undo at %d, %d, %d.", block.getX(), block.getY(), block.getZ());
|
||||||
|
}
|
||||||
it.remove();
|
it.remove();
|
||||||
|
|
||||||
if (++amount >= 200) {
|
if (++amount >= 200) {
|
||||||
@@ -94,4 +101,16 @@ public class ObjectSVC implements IrisService {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static void restoreBlock(Block block, BlockData data) {
|
||||||
|
if (data instanceof IrisCustomData custom) {
|
||||||
|
ExternalDataSVC external = IrisServices.getOrNull(ExternalDataSVC.class);
|
||||||
|
if (external != null && external.placeBlock(block, custom.getCustom())) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
block.setBlockData(custom.getBase(), false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
block.setBlockData(data, false);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -277,6 +277,13 @@ final class EngineRuntimeBuilder {
|
|||||||
throw propagate(failure);
|
throw propagate(failure);
|
||||||
}
|
}
|
||||||
EngineRuntime next = new EngineRuntime(previous.generation(), effects, worldManager);
|
EngineRuntime next = new EngineRuntime(previous.generation(), effects, worldManager);
|
||||||
|
try {
|
||||||
|
worldManager.start();
|
||||||
|
} catch (Throwable failure) {
|
||||||
|
failure = EngineShutdownSequence.runCleanup(failure, worldManager::close);
|
||||||
|
failure = EngineShutdownSequence.runCleanup(failure, effects::close);
|
||||||
|
throw new IllegalStateException("Failed to start the replacement Studio world manager.", failure);
|
||||||
|
}
|
||||||
Throwable retirementFailure = EngineShutdownSequence.runCleanup(null, previous.worldManager()::close);
|
Throwable retirementFailure = EngineShutdownSequence.runCleanup(null, previous.worldManager()::close);
|
||||||
retirementFailure = EngineShutdownSequence.runCleanup(retirementFailure, previous.effects()::close);
|
retirementFailure = EngineShutdownSequence.runCleanup(retirementFailure, previous.effects()::close);
|
||||||
if (retirementFailure != null) {
|
if (retirementFailure != null) {
|
||||||
@@ -285,7 +292,6 @@ final class EngineRuntimeBuilder {
|
|||||||
throw new IllegalStateException("Failed to retire Studio runtime services.", retirementFailure);
|
throw new IllegalStateException("Failed to retire Studio runtime services.", retirementFailure);
|
||||||
}
|
}
|
||||||
engine.runtime = next;
|
engine.runtime = next;
|
||||||
worldManager.start();
|
|
||||||
engine.getGenerationSessions().activateNextSession();
|
engine.getGenerationSessions().activateNextSession();
|
||||||
engine.lifecycleState = LifecycleState.RUNNING;
|
engine.lifecycleState = LifecycleState.RUNNING;
|
||||||
engine.getClosing().set(false);
|
engine.getClosing().set(false);
|
||||||
|
|||||||
@@ -1459,6 +1459,13 @@ public class IrisEngine implements Engine {
|
|||||||
return closing.get();
|
return closing.get();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isShuttingDown() {
|
||||||
|
LifecycleState state = lifecycleState;
|
||||||
|
return closed || state == LifecycleState.CLOSING
|
||||||
|
|| state == LifecycleState.CLOSED || state == LifecycleState.FAILED;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void recycle() {
|
public void recycle() {
|
||||||
if (closing.get() || closed) {
|
if (closing.get() || closed) {
|
||||||
|
|||||||
@@ -195,6 +195,10 @@ public interface Engine extends DataProvider, Fallible, BlockUpdater, Renderer,
|
|||||||
return isClosed();
|
return isClosed();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
default boolean isShuttingDown() {
|
||||||
|
return isClosed() || isClosing();
|
||||||
|
}
|
||||||
|
|
||||||
double getMaxBiomeObjectDensity();
|
double getMaxBiomeObjectDensity();
|
||||||
|
|
||||||
double getMaxBiomeDecoratorDensity();
|
double getMaxBiomeDecoratorDensity();
|
||||||
|
|||||||
@@ -131,8 +131,9 @@ public abstract class EngineAssignedWorldManager extends EngineAssignedComponent
|
|||||||
|
|
||||||
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
|
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
|
||||||
public void on(PlayerTeleportEvent e) {
|
public void on(PlayerTeleportEvent e) {
|
||||||
|
Engine engine = getEngine();
|
||||||
if (!BukkitPlatform.isPaperServer()
|
if (!BukkitPlatform.isPaperServer()
|
||||||
|| !getEngine().isStudio()
|
|| !engine.isStudio()
|
||||||
|| e.getCause() != PlayerTeleportEvent.TeleportCause.COMMAND) {
|
|| e.getCause() != PlayerTeleportEvent.TeleportCause.COMMAND) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -142,6 +143,10 @@ public abstract class EngineAssignedWorldManager extends EngineAssignedComponent
|
|||||||
if (destination == null || targetWorld == null || !targetWorld.equals(destination.getWorld())) {
|
if (destination == null || targetWorld == null || !targetWorld.equals(destination.getWorld())) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (engine.isShuttingDown()) {
|
||||||
|
e.setCancelled(true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
int chunkX = destination.getBlockX() >> 4;
|
int chunkX = destination.getBlockX() >> 4;
|
||||||
int chunkZ = destination.getBlockZ() >> 4;
|
int chunkZ = destination.getBlockZ() >> 4;
|
||||||
@@ -149,7 +154,7 @@ public abstract class EngineAssignedWorldManager extends EngineAssignedComponent
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
runManagerTask("bukkit_world_manager_teleport_event", () -> teleportAsync(e));
|
teleportAsync(e);
|
||||||
}
|
}
|
||||||
|
|
||||||
@EventHandler
|
@EventHandler
|
||||||
|
|||||||
@@ -619,6 +619,7 @@ public final class GenerationBoundaryStore {
|
|||||||
private final long[] regionKeys;
|
private final long[] regionKeys;
|
||||||
private final Long2ObjectOpenHashMap<String> shardHashes;
|
private final Long2ObjectOpenHashMap<String> shardHashes;
|
||||||
private final LinkedHashMap<Long, byte[]> masks;
|
private final LinkedHashMap<Long, byte[]> masks;
|
||||||
|
private final ThreadLocal<RegionMask> lastRegion = new ThreadLocal<>();
|
||||||
|
|
||||||
private MaskSource(
|
private MaskSource(
|
||||||
Path directory,
|
Path directory,
|
||||||
@@ -634,13 +635,21 @@ public final class GenerationBoundaryStore {
|
|||||||
@Override
|
@Override
|
||||||
public boolean contains(int chunkX, int chunkZ) throws IOException {
|
public boolean contains(int chunkX, int chunkZ) throws IOException {
|
||||||
long regionKey = packRegion(chunkX >> 5, chunkZ >> 5);
|
long regionKey = packRegion(chunkX >> 5, chunkZ >> 5);
|
||||||
String hash = shardHashes.get(regionKey);
|
RegionMask region = lastRegion.get();
|
||||||
if (hash == null) {
|
if (region == null || region.key() != regionKey) {
|
||||||
return false;
|
String hash = shardHashes.get(regionKey);
|
||||||
|
byte[] loaded = null;
|
||||||
|
if (hash != null) {
|
||||||
|
synchronized (this) {
|
||||||
|
loaded = load(regionKey, hash);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
region = new RegionMask(regionKey, loaded);
|
||||||
|
lastRegion.set(region);
|
||||||
}
|
}
|
||||||
byte[] mask;
|
byte[] mask = region.mask();
|
||||||
synchronized (this) {
|
if (mask == null) {
|
||||||
mask = load(regionKey, hash);
|
return false;
|
||||||
}
|
}
|
||||||
int localIndex = (Math.floorMod(chunkZ, REGION_SIDE) << 5)
|
int localIndex = (Math.floorMod(chunkZ, REGION_SIDE) << 5)
|
||||||
| Math.floorMod(chunkX, REGION_SIDE);
|
| Math.floorMod(chunkX, REGION_SIDE);
|
||||||
@@ -686,6 +695,9 @@ public final class GenerationBoundaryStore {
|
|||||||
}
|
}
|
||||||
return loaded;
|
return loaded;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private record RegionMask(long key, byte[] mask) {
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static final class LimitedInputStream extends InputStream {
|
private static final class LimitedInputStream extends InputStream {
|
||||||
|
|||||||
+61
-29
@@ -46,6 +46,9 @@ import java.util.Optional;
|
|||||||
import java.util.OptionalInt;
|
import java.util.OptionalInt;
|
||||||
import java.util.PriorityQueue;
|
import java.util.PriorityQueue;
|
||||||
import java.util.TreeSet;
|
import java.util.TreeSet;
|
||||||
|
import java.util.concurrent.CompletableFuture;
|
||||||
|
import java.util.concurrent.CompletionException;
|
||||||
|
import java.util.concurrent.atomic.LongAdder;
|
||||||
import java.util.zip.CRC32;
|
import java.util.zip.CRC32;
|
||||||
|
|
||||||
public final class TerrainBoundarySignatureStore {
|
public final class TerrainBoundarySignatureStore {
|
||||||
@@ -936,8 +939,9 @@ public final class TerrainBoundarySignatureStore {
|
|||||||
private final Long2ObjectOpenHashMap<CellReference> cells;
|
private final Long2ObjectOpenHashMap<CellReference> cells;
|
||||||
private final Long2ObjectOpenHashMap<long[]> cellsBySuperCell;
|
private final Long2ObjectOpenHashMap<long[]> cellsBySuperCell;
|
||||||
private final LinkedHashMap<Long, List<TerrainBoundarySignature>> cache;
|
private final LinkedHashMap<Long, List<TerrainBoundarySignature>> cache;
|
||||||
private long catalogProbes;
|
private final Map<Long, CompletableFuture<List<TerrainBoundarySignature>>> pendingLoads = new HashMap<>();
|
||||||
private long shardLoads;
|
private final LongAdder catalogProbes = new LongAdder();
|
||||||
|
private final LongAdder shardLoads = new LongAdder();
|
||||||
|
|
||||||
private CellSource(
|
private CellSource(
|
||||||
Path directory,
|
Path directory,
|
||||||
@@ -951,7 +955,7 @@ public final class TerrainBoundarySignatureStore {
|
|||||||
this.cache = new LinkedHashMap<>(MAXIMUM_CACHED_SHARDS, 0.75F, true);
|
this.cache = new LinkedHashMap<>(MAXIMUM_CACHED_SHARDS, 0.75F, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
private synchronized TerrainBoundarySignature signatureAt(int blockX, int blockZ) throws IOException {
|
private TerrainBoundarySignature signatureAt(int blockX, int blockZ) throws IOException {
|
||||||
int cellX = Math.floorDiv(blockX, CELL_SIZE);
|
int cellX = Math.floorDiv(blockX, CELL_SIZE);
|
||||||
int cellZ = Math.floorDiv(blockZ, CELL_SIZE);
|
int cellZ = Math.floorDiv(blockZ, CELL_SIZE);
|
||||||
long cellKey = pack(cellX, cellZ);
|
long cellKey = pack(cellX, cellZ);
|
||||||
@@ -967,7 +971,7 @@ public final class TerrainBoundarySignatureStore {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
private synchronized List<TerrainBoundarySignature> all() throws IOException {
|
private List<TerrainBoundarySignature> all() throws IOException {
|
||||||
ArrayList<TerrainBoundarySignature> signatures = new ArrayList<>();
|
ArrayList<TerrainBoundarySignature> signatures = new ArrayList<>();
|
||||||
for (long cellKey : cellKeys) {
|
for (long cellKey : cellKeys) {
|
||||||
signatures.addAll(load(cellKey, cells.get(cellKey)));
|
signatures.addAll(load(cellKey, cells.get(cellKey)));
|
||||||
@@ -978,7 +982,7 @@ public final class TerrainBoundarySignatureStore {
|
|||||||
return List.copyOf(signatures);
|
return List.copyOf(signatures);
|
||||||
}
|
}
|
||||||
|
|
||||||
private synchronized List<TerrainBoundarySignature> nearestCandidatesForChunk(
|
private List<TerrainBoundarySignature> nearestCandidatesForChunk(
|
||||||
int chunkX,
|
int chunkX,
|
||||||
int chunkZ,
|
int chunkZ,
|
||||||
int searchWidth
|
int searchWidth
|
||||||
@@ -1019,7 +1023,7 @@ public final class TerrainBoundarySignatureStore {
|
|||||||
return nearest.candidates();
|
return nearest.candidates();
|
||||||
}
|
}
|
||||||
|
|
||||||
private synchronized boolean intersectsTerrainBand(BlockBounds footprint, int width) throws IOException {
|
private boolean intersectsTerrainBand(BlockBounds footprint, int width) throws IOException {
|
||||||
double distanceSquared = (double) width * width;
|
double distanceSquared = (double) width * width;
|
||||||
return visitSuperCells(footprint.expanded(width - 1L), superKey -> {
|
return visitSuperCells(footprint.expanded(width - 1L), superKey -> {
|
||||||
if (footprint.distanceSquared(BlockBounds.cell(superKey, CELL_SIZE * SUPER_CELL_SIZE))
|
if (footprint.distanceSquared(BlockBounds.cell(superKey, CELL_SIZE * SUPER_CELL_SIZE))
|
||||||
@@ -1050,7 +1054,7 @@ public final class TerrainBoundarySignatureStore {
|
|||||||
long height = (long) maximumZ - minimumZ + 1L;
|
long height = (long) maximumZ - minimumZ + 1L;
|
||||||
if (width > cellsBySuperCell.size() / height) {
|
if (width > cellsBySuperCell.size() / height) {
|
||||||
for (long key : cellsBySuperCell.keySet()) {
|
for (long key : cellsBySuperCell.keySet()) {
|
||||||
catalogProbes++;
|
catalogProbes.increment();
|
||||||
if (cellX(key) >= minimumX && cellX(key) <= maximumX
|
if (cellX(key) >= minimumX && cellX(key) <= maximumX
|
||||||
&& cellZ(key) >= minimumZ && cellZ(key) <= maximumZ
|
&& cellZ(key) >= minimumZ && cellZ(key) <= maximumZ
|
||||||
&& visitor.visit(key)) {
|
&& visitor.visit(key)) {
|
||||||
@@ -1061,7 +1065,7 @@ public final class TerrainBoundarySignatureStore {
|
|||||||
}
|
}
|
||||||
for (long x = minimumX; x <= maximumX; x++) {
|
for (long x = minimumX; x <= maximumX; x++) {
|
||||||
for (long z = minimumZ; z <= maximumZ; z++) {
|
for (long z = minimumZ; z <= maximumZ; z++) {
|
||||||
catalogProbes++;
|
catalogProbes.increment();
|
||||||
long key = pack((int) x, (int) z);
|
long key = pack((int) x, (int) z);
|
||||||
if (cellsBySuperCell.containsKey(key) && visitor.visit(key)) {
|
if (cellsBySuperCell.containsKey(key) && visitor.visit(key)) {
|
||||||
return true;
|
return true;
|
||||||
@@ -1072,33 +1076,61 @@ public final class TerrainBoundarySignatureStore {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private List<TerrainBoundarySignature> load(long cellKey, CellReference reference) throws IOException {
|
private List<TerrainBoundarySignature> load(long cellKey, CellReference reference) throws IOException {
|
||||||
List<TerrainBoundarySignature> cached = cache.get(cellKey);
|
CompletableFuture<List<TerrainBoundarySignature>> pending;
|
||||||
if (cached != null) {
|
boolean loader;
|
||||||
return cached;
|
synchronized (this) {
|
||||||
|
List<TerrainBoundarySignature> cached = cache.get(cellKey);
|
||||||
|
if (cached != null) {
|
||||||
|
return cached;
|
||||||
|
}
|
||||||
|
pending = pendingLoads.get(cellKey);
|
||||||
|
loader = pending == null;
|
||||||
|
if (loader) {
|
||||||
|
pending = new CompletableFuture<>();
|
||||||
|
pendingLoads.put(cellKey, pending);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Path shard = directory.resolve(shardFileName(reference.hash()));
|
if (!loader) {
|
||||||
requireRegularFile(shard, "Terrain boundary shard");
|
try {
|
||||||
List<TerrainBoundarySignature> loaded = CellShard.read(
|
return pending.join();
|
||||||
shard,
|
} catch (CompletionException failure) {
|
||||||
reference.hash(),
|
if (failure.getCause() instanceof IOException io) {
|
||||||
cellX(cellKey),
|
throw io;
|
||||||
cellZ(cellKey)
|
}
|
||||||
);
|
throw failure;
|
||||||
if (loaded.size() != reference.count()) {
|
}
|
||||||
throw new IOException("Terrain boundary shard count does not match its catalog entry");
|
}
|
||||||
|
try {
|
||||||
|
Path shard = directory.resolve(shardFileName(reference.hash()));
|
||||||
|
requireRegularFile(shard, "Terrain boundary shard");
|
||||||
|
List<TerrainBoundarySignature> loaded = CellShard.read(
|
||||||
|
shard, reference.hash(), cellX(cellKey), cellZ(cellKey));
|
||||||
|
if (loaded.size() != reference.count()) {
|
||||||
|
throw new IOException("Terrain boundary shard count does not match its catalog entry");
|
||||||
|
}
|
||||||
|
synchronized (this) {
|
||||||
|
shardLoads.increment();
|
||||||
|
cache.put(cellKey, loaded);
|
||||||
|
trim(cache);
|
||||||
|
}
|
||||||
|
pending.complete(loaded);
|
||||||
|
return loaded;
|
||||||
|
} catch (IOException | RuntimeException | Error failure) {
|
||||||
|
pending.completeExceptionally(failure);
|
||||||
|
throw failure;
|
||||||
|
} finally {
|
||||||
|
synchronized (this) {
|
||||||
|
pendingLoads.remove(cellKey, pending);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
shardLoads++;
|
|
||||||
cache.put(cellKey, loaded);
|
|
||||||
trim(cache);
|
|
||||||
return loaded;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private synchronized long catalogProbeCount() {
|
private long catalogProbeCount() {
|
||||||
return catalogProbes;
|
return catalogProbes.sum();
|
||||||
}
|
}
|
||||||
|
|
||||||
private synchronized long shardLoadCount() {
|
private long shardLoadCount() {
|
||||||
return shardLoads;
|
return shardLoads.sum();
|
||||||
}
|
}
|
||||||
|
|
||||||
private synchronized int cachedShardCount() {
|
private synchronized int cachedShardCount() {
|
||||||
|
|||||||
@@ -3,21 +3,26 @@ package art.arcane.iris.engine.history;
|
|||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.Comparator;
|
import java.util.Comparator;
|
||||||
|
import java.util.HashMap;
|
||||||
import java.util.Iterator;
|
import java.util.Iterator;
|
||||||
import java.util.LinkedHashMap;
|
import java.util.LinkedHashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
|
import java.util.concurrent.CompletableFuture;
|
||||||
|
|
||||||
final class TransitionBoundarySampler {
|
final class TransitionBoundarySampler {
|
||||||
private static final int NEIGHBOUR_COUNT = 4;
|
private static final int NEIGHBOUR_COUNT = 4;
|
||||||
private static final int MAXIMUM_CACHED_CHUNKS = 8;
|
private static final int MAXIMUM_CACHED_CHUNKS = 512;
|
||||||
|
private static final int MAXIMUM_CACHED_CANDIDATES = 32_768;
|
||||||
|
|
||||||
private final int terrainWidth;
|
private final int terrainWidth;
|
||||||
private final int searchWidth;
|
private final int searchWidth;
|
||||||
private final TerrainBoundarySignatureStore.Snapshot signatures;
|
private final TerrainBoundarySignatureStore.Snapshot signatures;
|
||||||
private final LinkedHashMap<Long, CandidateIndex> chunkCandidates;
|
private final LinkedHashMap<Long, CandidateIndex> chunkCandidates;
|
||||||
|
private final Map<Long, CompletableFuture<CandidateIndex>> pendingCandidates = new HashMap<>();
|
||||||
private final ThreadLocal<ChunkCache> cache;
|
private final ThreadLocal<ChunkCache> cache;
|
||||||
|
private int cachedCandidates;
|
||||||
private long candidateBuildCount;
|
private long candidateBuildCount;
|
||||||
|
|
||||||
TransitionBoundarySampler(
|
TransitionBoundarySampler(
|
||||||
@@ -35,12 +40,7 @@ final class TransitionBoundarySampler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
TransitionGenerationPlan.TerrainSample sample(int blockX, int blockZ) {
|
TransitionGenerationPlan.TerrainSample sample(int blockX, int blockZ) {
|
||||||
ChunkCache chunkCache = cache.get();
|
ChunkCache chunkCache = chunkCache(blockX, blockZ);
|
||||||
int chunkX = Math.floorDiv(blockX, GenerationBoundary.CHUNK_SIZE);
|
|
||||||
int chunkZ = Math.floorDiv(blockZ, GenerationBoundary.CHUNK_SIZE);
|
|
||||||
if (chunkCache.chunkX != chunkX || chunkCache.chunkZ != chunkZ) {
|
|
||||||
chunkCache.reset(chunkX, chunkZ, candidatesForChunk(chunkX, chunkZ));
|
|
||||||
}
|
|
||||||
int localX = Math.floorMod(blockX, GenerationBoundary.CHUNK_SIZE);
|
int localX = Math.floorMod(blockX, GenerationBoundary.CHUNK_SIZE);
|
||||||
int localZ = Math.floorMod(blockZ, GenerationBoundary.CHUNK_SIZE);
|
int localZ = Math.floorMod(blockZ, GenerationBoundary.CHUNK_SIZE);
|
||||||
int index = localX * GenerationBoundary.CHUNK_SIZE + localZ;
|
int index = localX * GenerationBoundary.CHUNK_SIZE + localZ;
|
||||||
@@ -58,9 +58,7 @@ final class TransitionBoundarySampler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
BoundaryGeometryInfluence geometryAt(int blockX, int blockZ) {
|
BoundaryGeometryInfluence geometryAt(int blockX, int blockZ) {
|
||||||
CandidateIndex candidates = candidatesForChunk(
|
CandidateIndex candidates = chunkCache(blockX, blockZ).candidates;
|
||||||
Math.floorDiv(blockX, GenerationBoundary.CHUNK_SIZE),
|
|
||||||
Math.floorDiv(blockZ, GenerationBoundary.CHUNK_SIZE));
|
|
||||||
Nearest nearest = candidates.nearest(blockX, blockZ, square(terrainWidth));
|
Nearest nearest = candidates.nearest(blockX, blockZ, square(terrainWidth));
|
||||||
if (nearest.count == 0 || nearest.distancesSquared[0] >= square(terrainWidth)) {
|
if (nearest.count == 0 || nearest.distancesSquared[0] >= square(terrainWidth)) {
|
||||||
return BoundaryGeometryInfluence.none();
|
return BoundaryGeometryInfluence.none();
|
||||||
@@ -132,33 +130,59 @@ final class TransitionBoundarySampler {
|
|||||||
|
|
||||||
private CandidateIndex candidatesForChunk(int chunkX, int chunkZ) {
|
private CandidateIndex candidatesForChunk(int chunkX, int chunkZ) {
|
||||||
long chunkKey = pack(chunkX, chunkZ);
|
long chunkKey = pack(chunkX, chunkZ);
|
||||||
|
CompletableFuture<CandidateIndex> pending;
|
||||||
|
boolean builder;
|
||||||
synchronized (this) {
|
synchronized (this) {
|
||||||
CandidateIndex existing = chunkCandidates.get(chunkKey);
|
CandidateIndex existing = chunkCandidates.get(chunkKey);
|
||||||
if (existing != null) {
|
if (existing != null) {
|
||||||
return existing;
|
return existing;
|
||||||
}
|
}
|
||||||
|
pending = pendingCandidates.get(chunkKey);
|
||||||
|
builder = pending == null;
|
||||||
|
if (builder) {
|
||||||
|
pending = new CompletableFuture<>();
|
||||||
|
pendingCandidates.put(chunkKey, pending);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
CandidateIndex built = CandidateIndex.build(signatures.nearestCandidatesForChunk(
|
if (!builder) {
|
||||||
chunkX,
|
return pending.join();
|
||||||
chunkZ,
|
}
|
||||||
searchWidth
|
try {
|
||||||
));
|
CandidateIndex built = CandidateIndex.build(signatures.nearestCandidatesForChunk(
|
||||||
synchronized (this) {
|
chunkX, chunkZ, searchWidth));
|
||||||
CandidateIndex raced = chunkCandidates.get(chunkKey);
|
synchronized (this) {
|
||||||
if (raced != null) {
|
chunkCandidates.put(chunkKey, built);
|
||||||
return raced;
|
cachedCandidates += built.size;
|
||||||
}
|
candidateBuildCount++;
|
||||||
chunkCandidates.put(chunkKey, built);
|
while (chunkCandidates.size() > MAXIMUM_CACHED_CHUNKS
|
||||||
candidateBuildCount++;
|
|| cachedCandidates > MAXIMUM_CACHED_CANDIDATES) {
|
||||||
while (chunkCandidates.size() > MAXIMUM_CACHED_CHUNKS) {
|
Iterator<Map.Entry<Long, CandidateIndex>> entries = chunkCandidates.entrySet().iterator();
|
||||||
Iterator<Map.Entry<Long, CandidateIndex>> entries = chunkCandidates.entrySet().iterator();
|
cachedCandidates -= entries.next().getValue().size;
|
||||||
entries.next();
|
entries.remove();
|
||||||
entries.remove();
|
}
|
||||||
}
|
}
|
||||||
|
pending.complete(built);
|
||||||
return built;
|
return built;
|
||||||
|
} catch (RuntimeException | Error failure) {
|
||||||
|
pending.completeExceptionally(failure);
|
||||||
|
throw failure;
|
||||||
|
} finally {
|
||||||
|
synchronized (this) {
|
||||||
|
pendingCandidates.remove(chunkKey, pending);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private ChunkCache chunkCache(int blockX, int blockZ) {
|
||||||
|
ChunkCache chunkCache = cache.get();
|
||||||
|
int chunkX = Math.floorDiv(blockX, GenerationBoundary.CHUNK_SIZE);
|
||||||
|
int chunkZ = Math.floorDiv(blockZ, GenerationBoundary.CHUNK_SIZE);
|
||||||
|
if (chunkCache.chunkX != chunkX || chunkCache.chunkZ != chunkZ) {
|
||||||
|
chunkCache.reset(chunkX, chunkZ, candidatesForChunk(chunkX, chunkZ));
|
||||||
|
}
|
||||||
|
return chunkCache;
|
||||||
|
}
|
||||||
|
|
||||||
private static double weightedHeight(Nearest nearest, boolean surface) {
|
private static double weightedHeight(Nearest nearest, boolean surface) {
|
||||||
if (nearest.distancesSquared[0] <= 1D) {
|
if (nearest.distancesSquared[0] <= 1D) {
|
||||||
return surface
|
return surface
|
||||||
@@ -208,12 +232,14 @@ final class TransitionBoundarySampler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private static final class CandidateIndex {
|
private static final class CandidateIndex {
|
||||||
private static final CandidateIndex EMPTY = new CandidateIndex(null);
|
private static final CandidateIndex EMPTY = new CandidateIndex(null, 0);
|
||||||
|
|
||||||
private final Node root;
|
private final Node root;
|
||||||
|
private final int size;
|
||||||
|
|
||||||
private CandidateIndex(Node root) {
|
private CandidateIndex(Node root, int size) {
|
||||||
this.root = root;
|
this.root = root;
|
||||||
|
this.size = size;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static CandidateIndex build(List<TerrainBoundarySignature> candidates) {
|
private static CandidateIndex build(List<TerrainBoundarySignature> candidates) {
|
||||||
@@ -221,7 +247,7 @@ final class TransitionBoundarySampler {
|
|||||||
return EMPTY;
|
return EMPTY;
|
||||||
}
|
}
|
||||||
TerrainBoundarySignature[] values = candidates.toArray(new TerrainBoundarySignature[0]);
|
TerrainBoundarySignature[] values = candidates.toArray(new TerrainBoundarySignature[0]);
|
||||||
return new CandidateIndex(build(values, 0, values.length, 0));
|
return new CandidateIndex(build(values, 0, values.length, 0), values.length);
|
||||||
}
|
}
|
||||||
|
|
||||||
private Nearest nearest(int blockX, int blockZ, double maximumDistanceSquared) {
|
private Nearest nearest(int blockX, int blockZ, double maximumDistanceSquared) {
|
||||||
|
|||||||
@@ -22,13 +22,16 @@ import art.arcane.iris.core.loader.IrisData;
|
|||||||
import art.arcane.iris.core.compat.CompatStatus;
|
import art.arcane.iris.core.compat.CompatStatus;
|
||||||
import art.arcane.iris.core.compat.ContentGate;
|
import art.arcane.iris.core.compat.ContentGate;
|
||||||
import art.arcane.iris.core.loader.IrisRegistrant;
|
import art.arcane.iris.core.loader.IrisRegistrant;
|
||||||
|
import art.arcane.iris.core.service.ExternalDataSVC;
|
||||||
import art.arcane.iris.engine.data.cache.AtomicCache;
|
import art.arcane.iris.engine.data.cache.AtomicCache;
|
||||||
import art.arcane.iris.platform.bukkit.BukkitBlockState;
|
import art.arcane.iris.platform.bukkit.BukkitBlockState;
|
||||||
import art.arcane.iris.spi.IrisLogging;
|
import art.arcane.iris.spi.IrisLogging;
|
||||||
import art.arcane.iris.spi.IrisPlatform;
|
import art.arcane.iris.spi.IrisPlatform;
|
||||||
import art.arcane.iris.spi.IrisPlatforms;
|
import art.arcane.iris.spi.IrisPlatforms;
|
||||||
|
import art.arcane.iris.spi.IrisServices;
|
||||||
import art.arcane.iris.spi.PlatformBlockState;
|
import art.arcane.iris.spi.PlatformBlockState;
|
||||||
import art.arcane.iris.util.common.data.B;
|
import art.arcane.iris.util.common.data.B;
|
||||||
|
import art.arcane.iris.util.common.data.IrisCustomData;
|
||||||
import art.arcane.iris.util.common.data.VectorMap;
|
import art.arcane.iris.util.common.data.VectorMap;
|
||||||
import art.arcane.iris.util.common.math.AxisAlignedBB;
|
import art.arcane.iris.util.common.math.AxisAlignedBB;
|
||||||
import art.arcane.iris.util.common.math.IrisBlockVector;
|
import art.arcane.iris.util.common.math.IrisBlockVector;
|
||||||
@@ -288,6 +291,10 @@ public class IrisObject extends IrisRegistrant {
|
|||||||
states.remove(v);
|
states.remove(v);
|
||||||
} else {
|
} else {
|
||||||
BlockData data = block.getBlockData();
|
BlockData data = block.getBlockData();
|
||||||
|
ExternalDataSVC externalData = IrisServices.getOrNull(ExternalDataSVC.class);
|
||||||
|
if (externalData != null) {
|
||||||
|
data = externalData.captureBlockData(data);
|
||||||
|
}
|
||||||
blocks.put(v, BukkitBlockState.of(data));
|
blocks.put(v, BukkitBlockState.of(data));
|
||||||
TileData state = TileData.getTileState(block, legacy);
|
TileData state = TileData.getTileState(block, legacy);
|
||||||
if (state != null) {
|
if (state != null) {
|
||||||
@@ -363,10 +370,10 @@ public class IrisObject extends IrisRegistrant {
|
|||||||
public void place(Location at) {
|
public void place(Location at) {
|
||||||
readLock.lock();
|
readLock.lock();
|
||||||
try {
|
try {
|
||||||
for (var entry : blocks) {
|
for (Map.Entry<IrisBlockVector, PlatformBlockState> entry : blocks) {
|
||||||
var i = entry.getKey();
|
IrisBlockVector i = entry.getKey();
|
||||||
Block b = at.clone().add(0, getCenter().getY(), 0).add(i.getX(), i.getY(), i.getZ()).getBlock();
|
Block b = at.clone().add(0, getCenter().getY(), 0).add(i.getX(), i.getY(), i.getZ()).getBlock();
|
||||||
b.setBlockData((BlockData) Objects.requireNonNull(entry.getValue()).nativeHandle(), false);
|
placeBlock(b, (BlockData) Objects.requireNonNull(entry.getValue()).nativeHandle());
|
||||||
|
|
||||||
if (states.containsKey(i)) {
|
if (states.containsKey(i)) {
|
||||||
IrisLogging.info(Objects.requireNonNull(states.get(i)).toString());
|
IrisLogging.info(Objects.requireNonNull(states.get(i)).toString());
|
||||||
@@ -381,10 +388,10 @@ public class IrisObject extends IrisRegistrant {
|
|||||||
public void placeCenterY(Location at) {
|
public void placeCenterY(Location at) {
|
||||||
readLock.lock();
|
readLock.lock();
|
||||||
try {
|
try {
|
||||||
for (var entry : blocks) {
|
for (Map.Entry<IrisBlockVector, PlatformBlockState> entry : blocks) {
|
||||||
var i = entry.getKey();
|
IrisBlockVector i = entry.getKey();
|
||||||
Block b = at.clone().add(getCenter().getX(), getCenter().getY(), getCenter().getZ()).add(i.getX(), i.getY(), i.getZ()).getBlock();
|
Block b = at.clone().add(getCenter().getX(), getCenter().getY(), getCenter().getZ()).add(i.getX(), i.getY(), i.getZ()).getBlock();
|
||||||
b.setBlockData((BlockData) Objects.requireNonNull(entry.getValue()).nativeHandle(), false);
|
placeBlock(b, (BlockData) Objects.requireNonNull(entry.getValue()).nativeHandle());
|
||||||
|
|
||||||
if (states.containsKey(i)) {
|
if (states.containsKey(i)) {
|
||||||
Objects.requireNonNull(states.get(i)).toBukkitTry(b);
|
Objects.requireNonNull(states.get(i)).toBukkitTry(b);
|
||||||
@@ -406,6 +413,7 @@ public class IrisObject extends IrisRegistrant {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public int volume() {
|
public int volume() {
|
||||||
return blocks.size();
|
return blocks.size();
|
||||||
}
|
}
|
||||||
@@ -429,4 +437,18 @@ public class IrisObject extends IrisRegistrant {
|
|||||||
public CompatStatus evaluateCompat(ContentGate gate) {
|
public CompatStatus evaluateCompat(ContentGate gate) {
|
||||||
return CompatStatus.OK;
|
return CompatStatus.OK;
|
||||||
}
|
}
|
||||||
|
private static void placeBlock(Block block, BlockData data) {
|
||||||
|
if (data instanceof IrisCustomData custom) {
|
||||||
|
ExternalDataSVC external = IrisServices.getOrNull(ExternalDataSVC.class);
|
||||||
|
if (external != null && external.placeBlock(block, custom.getCustom())) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
block.setBlockData(custom.getBase(), false);
|
||||||
|
IrisLogging.warnOnce("object-paste:external:" + custom.getCustom(),
|
||||||
|
"Direct placement of %s is unavailable; only its base block was placed.", custom.getCustom());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
block.setBlockData(data, false);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -138,7 +138,7 @@ public final class IrisObjectIO {
|
|||||||
for (int i = 0; i < s; i++) {
|
for (int i = 0; i < s; i++) {
|
||||||
IrisBlockVector pos = new IrisBlockVector(din.readShort(), din.readShort(), din.readShort());
|
IrisBlockVector pos = new IrisBlockVector(din.readShort(), din.readShort(), din.readShort());
|
||||||
PlatformBlockState data = resolvePaletteState(self, din.readUTF());
|
PlatformBlockState data = resolvePaletteState(self, din.readUTF());
|
||||||
if (isStructureMarker(data)) {
|
if (isExcludedObjectBlock(data)) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
self.blocks.put(pos, data);
|
self.blocks.put(pos, data);
|
||||||
@@ -151,7 +151,7 @@ public final class IrisObjectIO {
|
|||||||
int size = din.readInt();
|
int size = din.readInt();
|
||||||
|
|
||||||
for (int i = 0; i < size; i++) {
|
for (int i = 0; i < size; i++) {
|
||||||
self.states.put(new IrisBlockVector(din.readShort(), din.readShort(), din.readShort()), TileData.read(din));
|
readTile(self, din);
|
||||||
}
|
}
|
||||||
} catch (Throwable e) {
|
} catch (Throwable e) {
|
||||||
IrisLogging.reportError(e);
|
IrisLogging.reportError(e);
|
||||||
@@ -189,7 +189,7 @@ public final class IrisObjectIO {
|
|||||||
for (i = 0; i < s; i++) {
|
for (i = 0; i < s; i++) {
|
||||||
IrisBlockVector pos = new IrisBlockVector(din.readShort(), din.readShort(), din.readShort());
|
IrisBlockVector pos = new IrisBlockVector(din.readShort(), din.readShort(), din.readShort());
|
||||||
PlatformBlockState data = resolved[din.readShort()];
|
PlatformBlockState data = resolved[din.readShort()];
|
||||||
if (isStructureMarker(data)) {
|
if (isExcludedObjectBlock(data)) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
self.blocks.put(pos, data);
|
self.blocks.put(pos, data);
|
||||||
@@ -198,7 +198,15 @@ public final class IrisObjectIO {
|
|||||||
s = din.readInt();
|
s = din.readInt();
|
||||||
|
|
||||||
for (i = 0; i < s; i++) {
|
for (i = 0; i < s; i++) {
|
||||||
self.states.put(new IrisBlockVector(din.readShort(), din.readShort(), din.readShort()), TileData.read(din));
|
readTile(self, din);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void readTile(IrisObject self, DataInputStream input) throws IOException {
|
||||||
|
IrisBlockVector position = new IrisBlockVector(input.readShort(), input.readShort(), input.readShort());
|
||||||
|
TileData tile = TileData.read(input);
|
||||||
|
if (self.blocks.get(position) != null) {
|
||||||
|
self.states.put(position, tile);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -251,12 +259,13 @@ public final class IrisObjectIO {
|
|||||||
return B.getState(key);
|
return B.getState(key);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static boolean isStructureMarker(PlatformBlockState data) {
|
private static boolean isExcludedObjectBlock(PlatformBlockState data) {
|
||||||
if (data == null) {
|
if (data == null) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
String material = IrisObjectShaping.materialKey(data);
|
String material = IrisObjectShaping.materialKey(data);
|
||||||
return material.equals("minecraft:jigsaw") || material.equals("minecraft:structure_block") || material.equals("minecraft:structure_void");
|
return material.equals("minecraft:jigsaw") || material.equals("minecraft:structure_block")
|
||||||
|
|| material.equals("minecraft:structure_void") || material.equals("minecraft:moving_piston");
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ import art.arcane.iris.core.compat.MissingBlockState;
|
|||||||
import art.arcane.iris.platform.bukkit.BukkitBlockState;
|
import art.arcane.iris.platform.bukkit.BukkitBlockState;
|
||||||
import art.arcane.iris.spi.IrisLogging;
|
import art.arcane.iris.spi.IrisLogging;
|
||||||
import art.arcane.iris.spi.PlatformBlockState;
|
import art.arcane.iris.spi.PlatformBlockState;
|
||||||
|
import art.arcane.iris.util.common.data.IrisCustomData;
|
||||||
import art.arcane.iris.util.common.math.IrisBlockVector;
|
import art.arcane.iris.util.common.math.IrisBlockVector;
|
||||||
import art.arcane.volmlib.util.collection.KList;
|
import art.arcane.volmlib.util.collection.KList;
|
||||||
import art.arcane.volmlib.util.collection.KMap;
|
import art.arcane.volmlib.util.collection.KMap;
|
||||||
@@ -72,6 +73,7 @@ public class IrisObjectRotation {
|
|||||||
|
|
||||||
private static final class Faces {
|
private static final class Faces {
|
||||||
private static final List<BlockFace> WALL_FACES = List.of(BlockFace.NORTH, BlockFace.SOUTH, BlockFace.EAST, BlockFace.WEST);
|
private static final List<BlockFace> WALL_FACES = List.of(BlockFace.NORTH, BlockFace.SOUTH, BlockFace.EAST, BlockFace.WEST);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Desc("If this rotator is enabled or not")
|
@Desc("If this rotator is enabled or not")
|
||||||
@@ -298,7 +300,16 @@ public class IrisObjectRotation {
|
|||||||
}
|
}
|
||||||
|
|
||||||
BlockData original = (BlockData) state.nativeHandle();
|
BlockData original = (BlockData) state.nativeHandle();
|
||||||
if (!canRotate() || !canRotateBlockData(original)) {
|
if (!canRotate()) {
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
if (original instanceof IrisCustomData custom) {
|
||||||
|
BlockData rotated = BukkitBlockState.rotateCustomData(this, custom, spinx, spiny, spinz);
|
||||||
|
if (rotated != null) {
|
||||||
|
return rotated == original ? state : BukkitBlockState.of(rotated);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!canRotateBlockData(original)) {
|
||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -317,6 +328,12 @@ public class IrisObjectRotation {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public BlockData rotate(BlockData dd, int spinxx, int spinyy, int spinzz) {
|
public BlockData rotate(BlockData dd, int spinxx, int spinyy, int spinzz) {
|
||||||
|
if (canRotate() && dd instanceof IrisCustomData custom) {
|
||||||
|
BlockData rotated = BukkitBlockState.rotateCustomData(this, custom, spinxx, spinyy, spinzz);
|
||||||
|
if (rotated != null) {
|
||||||
|
return rotated;
|
||||||
|
}
|
||||||
|
}
|
||||||
BlockData d = dd;
|
BlockData d = dd;
|
||||||
try {
|
try {
|
||||||
int spinx = (int) (90D * (Math.ceil(Math.abs((spinxx % 360D) / 90D))));
|
int spinx = (int) (90D * (Math.ceil(Math.abs((spinxx % 360D) / 90D))));
|
||||||
|
|||||||
@@ -1122,13 +1122,23 @@ public class BukkitChunkGenerator extends ChunkGenerator implements PlatformChun
|
|||||||
Runnable activeOperation = Objects.requireNonNull(operation, "Exclusive control operation");
|
Runnable activeOperation = Objects.requireNonNull(operation, "Exclusive control operation");
|
||||||
CompletableFuture<Void> outward = Objects.requireNonNull(future, "Exclusive control future");
|
CompletableFuture<Void> outward = Objects.requireNonNull(future, "Exclusive control future");
|
||||||
TimeUnit activeUnit = Objects.requireNonNull(unit, "Exclusive control timeout unit");
|
TimeUnit activeUnit = Objects.requireNonNull(unit, "Exclusive control timeout unit");
|
||||||
|
if (outward.isCancelled()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
ExclusiveControlAcquisition acquisition = new ExclusiveControlAcquisition(Thread.currentThread());
|
||||||
|
outward.whenComplete((ignored, completionFailure) -> {
|
||||||
|
if (outward.isCancelled()) {
|
||||||
|
acquisition.cancel();
|
||||||
|
}
|
||||||
|
});
|
||||||
boolean acquired = false;
|
boolean acquired = false;
|
||||||
Throwable failure = null;
|
Throwable failure = null;
|
||||||
try {
|
try {
|
||||||
acquired = activeGate.tryAcquireExclusive(
|
try {
|
||||||
acquisitionTimeout,
|
acquired = activeGate.tryAcquireExclusive(acquisitionTimeout, activeUnit);
|
||||||
activeUnit,
|
} finally {
|
||||||
outward::isCancelled);
|
acquisition.stopWaiting();
|
||||||
|
}
|
||||||
if (!acquired) {
|
if (!acquired) {
|
||||||
if (!outward.isCancelled()) {
|
if (!outward.isCancelled()) {
|
||||||
failure = new TimeoutException("Timed out waiting for exclusive Iris generation control after "
|
failure = new TimeoutException("Timed out waiting for exclusive Iris generation control after "
|
||||||
@@ -1138,7 +1148,9 @@ public class BukkitChunkGenerator extends ChunkGenerator implements PlatformChun
|
|||||||
activeOperation.run();
|
activeOperation.run();
|
||||||
}
|
}
|
||||||
} catch (InterruptedException e) {
|
} catch (InterruptedException e) {
|
||||||
Thread.currentThread().interrupt();
|
if (!acquisition.interruptedByCancellation()) {
|
||||||
|
Thread.currentThread().interrupt();
|
||||||
|
}
|
||||||
if (!outward.isCancelled()) {
|
if (!outward.isCancelled()) {
|
||||||
failure = e;
|
failure = e;
|
||||||
}
|
}
|
||||||
@@ -1447,30 +1459,12 @@ public class BukkitChunkGenerator extends ChunkGenerator implements PlatformChun
|
|||||||
permits.acquire(permitCount);
|
permits.acquire(permitCount);
|
||||||
}
|
}
|
||||||
|
|
||||||
boolean tryAcquireExclusive(
|
boolean tryAcquireExclusive(long timeout, TimeUnit unit) throws InterruptedException {
|
||||||
long timeout,
|
|
||||||
TimeUnit unit,
|
|
||||||
BooleanSupplier cancelled
|
|
||||||
) throws InterruptedException {
|
|
||||||
if (timeout <= 0L) {
|
if (timeout <= 0L) {
|
||||||
throw new IllegalArgumentException("Exclusive generation control timeout must be positive.");
|
throw new IllegalArgumentException("Exclusive generation control timeout must be positive.");
|
||||||
}
|
}
|
||||||
TimeUnit activeUnit = Objects.requireNonNull(unit, "Exclusive generation control timeout unit");
|
TimeUnit activeUnit = Objects.requireNonNull(unit, "Exclusive generation control timeout unit");
|
||||||
BooleanSupplier cancellation = Objects.requireNonNull(cancelled, "Exclusive generation control cancellation");
|
return permits.tryAcquire(permitCount, timeout, activeUnit);
|
||||||
long timeoutNanos = activeUnit.toNanos(timeout);
|
|
||||||
long started = System.nanoTime();
|
|
||||||
long remainingNanos = timeoutNanos;
|
|
||||||
long cancellationPollNanos = TimeUnit.MILLISECONDS.toNanos(50L);
|
|
||||||
while (!cancellation.getAsBoolean() && remainingNanos > 0L) {
|
|
||||||
if (permits.tryAcquire(
|
|
||||||
permitCount,
|
|
||||||
Math.min(remainingNanos, cancellationPollNanos),
|
|
||||||
TimeUnit.NANOSECONDS)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
remainingNanos = timeoutNanos - (System.nanoTime() - started);
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void releaseExclusive() {
|
void releaseExclusive() {
|
||||||
@@ -1491,6 +1485,34 @@ public class BukkitChunkGenerator extends ChunkGenerator implements PlatformChun
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static final class ExclusiveControlAcquisition {
|
||||||
|
private final Thread waiter;
|
||||||
|
private boolean waiting = true;
|
||||||
|
private boolean cancellationInterrupt;
|
||||||
|
|
||||||
|
private ExclusiveControlAcquisition(Thread waiter) {
|
||||||
|
this.waiter = waiter;
|
||||||
|
}
|
||||||
|
|
||||||
|
private synchronized void cancel() {
|
||||||
|
if (waiting && !waiter.isInterrupted()) {
|
||||||
|
cancellationInterrupt = true;
|
||||||
|
waiter.interrupt();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private synchronized void stopWaiting() {
|
||||||
|
waiting = false;
|
||||||
|
if (cancellationInterrupt) {
|
||||||
|
Thread.interrupted();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private synchronized boolean interruptedByCancellation() {
|
||||||
|
return cancellationInterrupt;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
static final class GenerationStageExclusivePermit implements AutoCloseable {
|
static final class GenerationStageExclusivePermit implements AutoCloseable {
|
||||||
private final Semaphore permits;
|
private final Semaphore permits;
|
||||||
private final int permitCount;
|
private final int permitCount;
|
||||||
|
|||||||
@@ -18,14 +18,24 @@
|
|||||||
|
|
||||||
package art.arcane.iris.platform.bukkit;
|
package art.arcane.iris.platform.bukkit;
|
||||||
|
|
||||||
|
import art.arcane.iris.core.link.Identifier;
|
||||||
import art.arcane.iris.core.nms.INMS;
|
import art.arcane.iris.core.nms.INMS;
|
||||||
|
import art.arcane.iris.core.nms.container.Pair;
|
||||||
|
import art.arcane.iris.core.service.ExternalDataSVC;
|
||||||
|
import art.arcane.iris.engine.object.IrisObjectRotation;
|
||||||
import art.arcane.iris.spi.PlatformBlockState;
|
import art.arcane.iris.spi.PlatformBlockState;
|
||||||
import art.arcane.iris.util.common.data.IrisCustomData;
|
import art.arcane.iris.util.common.data.IrisCustomData;
|
||||||
|
import art.arcane.iris.util.common.math.IrisBlockVector;
|
||||||
|
import art.arcane.volmlib.util.collection.KMap;
|
||||||
|
import org.bukkit.Axis;
|
||||||
import org.bukkit.Bukkit;
|
import org.bukkit.Bukkit;
|
||||||
import org.bukkit.Tag;
|
import org.bukkit.Tag;
|
||||||
|
import org.bukkit.block.BlockFace;
|
||||||
import org.bukkit.block.data.BlockData;
|
import org.bukkit.block.data.BlockData;
|
||||||
|
|
||||||
import java.util.LinkedHashMap;
|
import java.util.LinkedHashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Locale;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.concurrent.ConcurrentHashMap;
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
|
||||||
@@ -33,6 +43,15 @@ import java.util.concurrent.ConcurrentHashMap;
|
|||||||
* Interned Bukkit adapter for a neutral block state backed by BlockData.
|
* Interned Bukkit adapter for a neutral block state backed by BlockData.
|
||||||
*/
|
*/
|
||||||
public final class BukkitBlockState implements PlatformBlockState {
|
public final class BukkitBlockState implements PlatformBlockState {
|
||||||
|
private static final Map<String, BlockFace> CUSTOM_NAMED_FACES = Map.of(
|
||||||
|
"north", BlockFace.NORTH, "south", BlockFace.SOUTH,
|
||||||
|
"east", BlockFace.EAST, "west", BlockFace.WEST,
|
||||||
|
"up", BlockFace.UP, "down", BlockFace.DOWN);
|
||||||
|
private static final List<BlockFace> CUSTOM_ROTATION_FACES = List.of(
|
||||||
|
BlockFace.SOUTH, BlockFace.SOUTH_SOUTH_WEST, BlockFace.SOUTH_WEST, BlockFace.WEST_SOUTH_WEST,
|
||||||
|
BlockFace.WEST, BlockFace.WEST_NORTH_WEST, BlockFace.NORTH_WEST, BlockFace.NORTH_NORTH_WEST,
|
||||||
|
BlockFace.NORTH, BlockFace.NORTH_NORTH_EAST, BlockFace.NORTH_EAST, BlockFace.EAST_NORTH_EAST,
|
||||||
|
BlockFace.EAST, BlockFace.EAST_SOUTH_EAST, BlockFace.SOUTH_EAST, BlockFace.SOUTH_SOUTH_EAST);
|
||||||
private static final ConcurrentHashMap<String, BukkitBlockState> CACHE = new ConcurrentHashMap<>();
|
private static final ConcurrentHashMap<String, BukkitBlockState> CACHE = new ConcurrentHashMap<>();
|
||||||
// Front cache keyed on the BlockData itself (CraftBlockData equals/hashCode delegate to
|
// Front cache keyed on the BlockData itself (CraftBlockData equals/hashCode delegate to
|
||||||
// the canonical NMS state): a hit skips getAsString(), which built the full property
|
// the canonical NMS state): a hit skips getAsString(), which built the full property
|
||||||
@@ -82,6 +101,64 @@ public final class BukkitBlockState implements PlatformBlockState {
|
|||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static BlockData rotateCustomData(IrisObjectRotation objectRotation, IrisCustomData custom, int spinxx, int spinyy, int spinzz) {
|
||||||
|
Pair<Identifier, KMap<String, String>> parsed = ExternalDataSVC.parseState(custom.getCustom());
|
||||||
|
KMap<String, String> original = parsed.getB();
|
||||||
|
if (original.isEmpty()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
KMap<String, String> rotated = new KMap<>(original);
|
||||||
|
int spinx = (int) (90D * Math.ceil(Math.abs((spinxx % 360D) / 90D)));
|
||||||
|
int spiny = (int) (90D * Math.ceil(Math.abs((spinyy % 360D) / 90D)));
|
||||||
|
int spinz = (int) (90D * Math.ceil(Math.abs((spinzz % 360D) / 90D)));
|
||||||
|
boolean oriented = false;
|
||||||
|
String facing = original.get("facing");
|
||||||
|
if (facing != null && CUSTOM_NAMED_FACES.containsKey(facing)) {
|
||||||
|
BlockFace face = objectRotation.getFace(rotateFace(objectRotation, CUSTOM_NAMED_FACES.get(facing), spinx, spiny, spinz));
|
||||||
|
rotated.put("facing", face.name().toLowerCase(Locale.ROOT));
|
||||||
|
oriented = true;
|
||||||
|
}
|
||||||
|
Axis axis = switch (original.getOrDefault("axis", "")) {
|
||||||
|
case "x" -> Axis.X;
|
||||||
|
case "y" -> Axis.Y;
|
||||||
|
case "z" -> Axis.Z;
|
||||||
|
default -> null;
|
||||||
|
};
|
||||||
|
if (axis != null) {
|
||||||
|
Axis result = objectRotation.getAxis(rotateFace(objectRotation, objectRotation.faceForAxis(axis), spinx, spiny, spinz));
|
||||||
|
rotated.put("axis", result.name().toLowerCase(Locale.ROOT));
|
||||||
|
oriented = true;
|
||||||
|
}
|
||||||
|
int rotation = rotationIndex(original.get("rotation"));
|
||||||
|
if (rotation >= 0) {
|
||||||
|
BlockFace face = objectRotation.getHexFace(rotateFace(objectRotation, CUSTOM_ROTATION_FACES.get(rotation), spinx, spiny, spinz));
|
||||||
|
int result = CUSTOM_ROTATION_FACES.indexOf(face);
|
||||||
|
if (result >= 0) {
|
||||||
|
rotated.put("rotation", Integer.toString(result));
|
||||||
|
}
|
||||||
|
oriented = true;
|
||||||
|
}
|
||||||
|
for (Map.Entry<String, BlockFace> entry : CUSTOM_NAMED_FACES.entrySet()) {
|
||||||
|
String value = original.get(entry.getKey());
|
||||||
|
if (value == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
String destination = objectRotation.getFace(rotateFace(objectRotation, entry.getValue(), spinx, spiny, spinz)).name().toLowerCase(Locale.ROOT);
|
||||||
|
if (original.containsKey(destination)) {
|
||||||
|
rotated.put(destination, value);
|
||||||
|
}
|
||||||
|
oriented = true;
|
||||||
|
}
|
||||||
|
if (!oriented) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (rotated.equals(original)) {
|
||||||
|
return custom;
|
||||||
|
}
|
||||||
|
BlockData resolved = BukkitBlockResolution.resolveOrNull(ExternalDataSVC.buildState(parsed.getA(), rotated).toString());
|
||||||
|
return resolved instanceof IrisCustomData ? resolved : custom;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean equals(Object other) {
|
public boolean equals(Object other) {
|
||||||
if (this == other) {
|
if (this == other) {
|
||||||
@@ -369,9 +446,17 @@ public final class BukkitBlockState implements PlatformBlockState {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public PlatformBlockState withProperty(String name, String value) {
|
public PlatformBlockState withProperty(String name, String value) {
|
||||||
// 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) {
|
if (data instanceof IrisCustomData custom) {
|
||||||
|
if (ExternalDataSVC.parseState(custom.getCustom()).getB().containsKey(name)) {
|
||||||
|
String merged = mergeProperty(key, name, value);
|
||||||
|
BlockData resolved = BukkitBlockResolution.resolveOrNull(merged);
|
||||||
|
if (!(resolved instanceof IrisCustomData)) {
|
||||||
|
throw new IllegalArgumentException("Cannot resolve custom block state " + merged);
|
||||||
|
}
|
||||||
|
return of(resolved);
|
||||||
|
}
|
||||||
|
// 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.
|
||||||
String merged = mergeProperty(custom.getBase().getAsString(), name, value);
|
String merged = mergeProperty(custom.getBase().getAsString(), name, value);
|
||||||
BlockData resolved = Bukkit.createBlockData(merged);
|
BlockData resolved = Bukkit.createBlockData(merged);
|
||||||
return of(IrisCustomData.of(resolved, custom.getCustom()));
|
return of(IrisCustomData.of(resolved, custom.getCustom()));
|
||||||
@@ -383,4 +468,20 @@ public final class BukkitBlockState implements PlatformBlockState {
|
|||||||
public Object nativeHandle() {
|
public Object nativeHandle() {
|
||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static IrisBlockVector rotateFace(IrisObjectRotation objectRotation, BlockFace face, int spinx, int spiny, int spinz) {
|
||||||
|
return objectRotation.rotate(new IrisBlockVector(face.getModX(), face.getModY(), face.getModZ()), spinx, spiny, spinz);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int rotationIndex(String value) {
|
||||||
|
if (value == null) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
int rotation = Integer.parseInt(value);
|
||||||
|
return rotation >= 0 && rotation < CUSTOM_ROTATION_FACES.size() ? rotation : -1;
|
||||||
|
} catch (NumberFormatException ignored) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -7,6 +7,8 @@ import art.arcane.iris.core.tools.IrisToolbelt;
|
|||||||
import art.arcane.iris.engine.IrisComplex;
|
import art.arcane.iris.engine.IrisComplex;
|
||||||
import art.arcane.iris.engine.framework.BiomeEnvironment;
|
import art.arcane.iris.engine.framework.BiomeEnvironment;
|
||||||
import art.arcane.iris.engine.framework.Engine;
|
import art.arcane.iris.engine.framework.Engine;
|
||||||
|
import art.arcane.iris.engine.framework.GenerationSessionManager;
|
||||||
|
import art.arcane.iris.engine.framework.GenerationTransitionGate;
|
||||||
import art.arcane.iris.engine.history.SavedBiomeUnavailableException;
|
import art.arcane.iris.engine.history.SavedBiomeUnavailableException;
|
||||||
import art.arcane.iris.engine.mantle.EngineMantle;
|
import art.arcane.iris.engine.mantle.EngineMantle;
|
||||||
import art.arcane.iris.engine.object.IrisBiome;
|
import art.arcane.iris.engine.object.IrisBiome;
|
||||||
@@ -35,7 +37,14 @@ import org.mockito.MockedStatic;
|
|||||||
|
|
||||||
import java.lang.reflect.Field;
|
import java.lang.reflect.Field;
|
||||||
import java.util.ArrayDeque;
|
import java.util.ArrayDeque;
|
||||||
|
import java.util.List;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
|
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 static org.junit.Assert.assertEquals;
|
import static org.junit.Assert.assertEquals;
|
||||||
import static org.junit.Assert.assertFalse;
|
import static org.junit.Assert.assertFalse;
|
||||||
@@ -195,6 +204,70 @@ public class BoardSVCSavedBiomeTest {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void ordinaryRefreshLeavesOwnerThreadAvailableForStudioCheckpoint() throws Exception {
|
||||||
|
GenerationSessionManager sessions = new GenerationSessionManager(true);
|
||||||
|
AtomicInteger height = new AtomicInteger(70);
|
||||||
|
AtomicInteger checkpoints = new AtomicInteger();
|
||||||
|
ExecutorService owner = Executors.newSingleThreadExecutor();
|
||||||
|
ExecutorService reloader = Executors.newSingleThreadExecutor();
|
||||||
|
Fixture fixture = null;
|
||||||
|
try {
|
||||||
|
fixture = owner.submit(() -> {
|
||||||
|
Fixture created = new Fixture();
|
||||||
|
when(created.engine.getGenerationSessions()).thenReturn(sessions);
|
||||||
|
when(created.engine.getHeight(8, 8)).thenAnswer(invocation -> {
|
||||||
|
try (GenerationTransitionGate.Participation ignored = sessions.transitionGate().enter()) {
|
||||||
|
return height.get();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
created.service.updatePlayer(created.player);
|
||||||
|
return created;
|
||||||
|
}).get(3L, TimeUnit.SECONDS);
|
||||||
|
Fixture board = fixture;
|
||||||
|
List<String> previous = board.service.getLines(board.player);
|
||||||
|
assertTrue(board.lines().contains("70"));
|
||||||
|
|
||||||
|
CompletableFuture<Void> cutover = CompletableFuture.runAsync(() -> {
|
||||||
|
try (GenerationTransitionGate.Transition ignored = sessions.transitionGate().beginTransition(2_000L)) {
|
||||||
|
sessions.sealAndAwait("Studio generation cutover", 2_000L);
|
||||||
|
Future<?> refresh = owner.submit(board::tick);
|
||||||
|
owner.submit(checkpoints::incrementAndGet).get(2L, TimeUnit.SECONDS);
|
||||||
|
refresh.get(2L, TimeUnit.SECONDS);
|
||||||
|
assertEquals(previous, board.service.getLines(board.player));
|
||||||
|
verify(board.engine, times(2)).getGeneratedPerSecond();
|
||||||
|
verify(board.engine, times(1)).getBiomeOrMantleEnvironment(8, 160, 8);
|
||||||
|
verify(board.engine, times(1)).getHeight(8, 8);
|
||||||
|
height.set(85);
|
||||||
|
sessions.activateNextSession();
|
||||||
|
} catch (Exception failure) {
|
||||||
|
throw new IllegalStateException(failure);
|
||||||
|
}
|
||||||
|
}, reloader);
|
||||||
|
cutover.get(3L, TimeUnit.SECONDS);
|
||||||
|
|
||||||
|
assertEquals(1, checkpoints.get());
|
||||||
|
owner.submit(board::tick).get(2L, TimeUnit.SECONDS);
|
||||||
|
assertTrue(board.lines().contains("85"));
|
||||||
|
assertEquals(1, board.ticks.size());
|
||||||
|
assertEquals(0, sessions.activeLeases());
|
||||||
|
verify(board.engine, times(2)).getBiomeOrMantleEnvironment(8, 160, 8);
|
||||||
|
verify(board.engine, times(2)).getHeight(8, 8);
|
||||||
|
} finally {
|
||||||
|
try {
|
||||||
|
if (fixture != null) {
|
||||||
|
Fixture board = fixture;
|
||||||
|
owner.submit(board::close).get(2L, TimeUnit.SECONDS);
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
owner.shutdownNow();
|
||||||
|
reloader.shutdownNow();
|
||||||
|
assertTrue(owner.awaitTermination(2L, TimeUnit.SECONDS));
|
||||||
|
assertTrue(reloader.awaitTermination(2L, TimeUnit.SECONDS));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private static BiomeEnvironment environment(String regionName, String biomeName) {
|
private static BiomeEnvironment environment(String regionName, String biomeName) {
|
||||||
IrisRegion region = mock(IrisRegion.class);
|
IrisRegion region = mock(IrisRegion.class);
|
||||||
IrisBiome biome = mock(IrisBiome.class);
|
IrisBiome biome = mock(IrisBiome.class);
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import org.junit.Test;
|
|||||||
import java.lang.reflect.Field;
|
import java.lang.reflect.Field;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.MissingResourceException;
|
import java.util.MissingResourceException;
|
||||||
|
import java.util.Optional;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
|
||||||
import static org.junit.Assert.assertEquals;
|
import static org.junit.Assert.assertEquals;
|
||||||
@@ -29,6 +30,43 @@ import static org.mockito.Mockito.verify;
|
|||||||
import static org.mockito.Mockito.when;
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
public class ExternalDataSVCBlockRoutingTest {
|
public class ExternalDataSVCBlockRoutingTest {
|
||||||
|
@Test
|
||||||
|
public void directPlacementRetainsQualifiedProviderAndProperties() throws Exception {
|
||||||
|
Identifier nativeId = Identifier.fromString("default:palm_log");
|
||||||
|
Identifier stateId = Identifier.fromString("default:palm_log[axis=x]");
|
||||||
|
ExternalDataProvider provider = provider("CraftEngine", nativeId);
|
||||||
|
Block block = mock(Block.class);
|
||||||
|
when(provider.placeBlock(block, stateId)).thenReturn(true);
|
||||||
|
|
||||||
|
assertEquals(true, service(provider).placeBlock(block,
|
||||||
|
Identifier.fromString("craftengine:default/palm_log[axis=x]")));
|
||||||
|
verify(provider).placeBlock(block, stateId);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void capturedBlocksRetainSemanticPropertiesAndProviderIdentity() throws Exception {
|
||||||
|
Identifier id = Identifier.fromString("example:palm_log[axis=x]");
|
||||||
|
ExternalDataProvider provider = provider("CraftEngine", id);
|
||||||
|
BlockData base = mock(BlockData.class);
|
||||||
|
when(base.getAsString()).thenReturn("craftengine:custom_18");
|
||||||
|
when(provider.identifyBlock(base)).thenReturn(Optional.of(id));
|
||||||
|
ExternalDataSVC service = service(provider);
|
||||||
|
|
||||||
|
IrisCustomData captured = (IrisCustomData) service.captureBlockData(base);
|
||||||
|
|
||||||
|
assertSame(base, captured.getBase());
|
||||||
|
assertEquals("craftengine:example/palm_log[axis=x]", captured.getCustom().toString());
|
||||||
|
assertSame(captured, service.captureBlockData(captured));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void vanillaCaptureKeepsOriginalBlockData() throws Exception {
|
||||||
|
ExternalDataProvider provider = provider("CraftEngine", Identifier.fromString("example:wood"));
|
||||||
|
BlockData base = mock(BlockData.class);
|
||||||
|
|
||||||
|
assertSame(base, service(provider).captureBlockData(base));
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void nativeLookupPinsProviderAndRetainsPropertiesUntilPlacement() throws Exception {
|
public void nativeLookupPinsProviderAndRetainsPropertiesUntilPlacement() throws Exception {
|
||||||
Identifier nativeId = Identifier.fromString("example:wood");
|
Identifier nativeId = Identifier.fromString("example:wood");
|
||||||
|
|||||||
@@ -0,0 +1,67 @@
|
|||||||
|
package art.arcane.iris.core.service;
|
||||||
|
|
||||||
|
import art.arcane.iris.core.link.Identifier;
|
||||||
|
import art.arcane.iris.spi.IrisServices;
|
||||||
|
import art.arcane.iris.util.common.data.IrisCustomData;
|
||||||
|
import art.arcane.iris.util.common.scheduling.J;
|
||||||
|
import org.bukkit.Location;
|
||||||
|
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.ArrayList;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
import static org.junit.Assert.assertEquals;
|
||||||
|
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.verifyNoInteractions;
|
||||||
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
|
public class ObjectSVCUndoCustomBlockTest {
|
||||||
|
@Test
|
||||||
|
public void undoRestoresSemanticCustomStateOnItsOwningRegion() {
|
||||||
|
Block block = mock(Block.class);
|
||||||
|
Location location = new Location(mock(World.class), 7, 80, -9);
|
||||||
|
when(block.getLocation()).thenReturn(location);
|
||||||
|
Identifier id = new Identifier("craftengine", "test/lamp[lit=false]");
|
||||||
|
BlockData custom = IrisCustomData.of(mock(BlockData.class), id);
|
||||||
|
ExternalDataSVC external = mock(ExternalDataSVC.class);
|
||||||
|
when(external.placeBlock(block, id)).thenReturn(true);
|
||||||
|
List<Runnable> ownerTasks = new ArrayList<>();
|
||||||
|
try (MockedStatic<IrisServices> services = mockStatic(IrisServices.class);
|
||||||
|
MockedStatic<J> scheduling = mockStatic(J.class)) {
|
||||||
|
services.when(() -> IrisServices.getOrNull(ExternalDataSVC.class)).thenReturn(external);
|
||||||
|
scheduling.when(() -> J.runGlobal(any(Runnable.class))).thenAnswer(invocation -> {
|
||||||
|
invocation.getArgument(0, Runnable.class).run();
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
scheduling.when(() -> J.s(any(Runnable.class))).thenAnswer(invocation -> {
|
||||||
|
invocation.getArgument(0, Runnable.class).run();
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
scheduling.when(() -> J.runAt(any(Location.class), any(Runnable.class))).thenAnswer(invocation -> {
|
||||||
|
ownerTasks.add(invocation.getArgument(1, Runnable.class));
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
ObjectSVC undo = new ObjectSVC();
|
||||||
|
undo.addChanges(new HashMap<>(Map.of(block, custom)));
|
||||||
|
|
||||||
|
undo.revertChanges(1);
|
||||||
|
|
||||||
|
assertEquals(1, ownerTasks.size());
|
||||||
|
verifyNoInteractions(external);
|
||||||
|
ownerTasks.getFirst().run();
|
||||||
|
verify(external).placeBlock(block, id);
|
||||||
|
verify(block, times(0)).setBlockData(any(BlockData.class), anyBoolean());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,287 @@
|
|||||||
|
package art.arcane.iris.engine;
|
||||||
|
|
||||||
|
import art.arcane.iris.engine.framework.Engine;
|
||||||
|
import art.arcane.iris.engine.framework.EngineAssignedWorldManager;
|
||||||
|
import art.arcane.iris.engine.framework.EngineEffects;
|
||||||
|
import art.arcane.iris.engine.framework.EngineEffectsProvider;
|
||||||
|
import art.arcane.iris.engine.framework.EngineTarget;
|
||||||
|
import art.arcane.iris.engine.framework.EngineWorldManagerProvider;
|
||||||
|
import art.arcane.iris.engine.framework.GenerationSessionManager;
|
||||||
|
import art.arcane.iris.engine.framework.GenerationTransitionGate;
|
||||||
|
import art.arcane.iris.engine.object.IrisWorld;
|
||||||
|
import art.arcane.iris.platform.bukkit.BukkitPlatform;
|
||||||
|
import art.arcane.iris.platform.bukkit.BukkitWorldBinding;
|
||||||
|
import art.arcane.iris.spi.IrisServices;
|
||||||
|
import org.bukkit.Chunk;
|
||||||
|
import org.bukkit.Location;
|
||||||
|
import org.bukkit.World;
|
||||||
|
import org.bukkit.entity.Player;
|
||||||
|
import org.bukkit.event.block.BlockBreakEvent;
|
||||||
|
import org.bukkit.event.block.BlockPlaceEvent;
|
||||||
|
import org.bukkit.event.player.PlayerTeleportEvent;
|
||||||
|
import org.junit.Test;
|
||||||
|
import org.mockito.MockedStatic;
|
||||||
|
|
||||||
|
import java.lang.reflect.Field;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.concurrent.CompletableFuture;
|
||||||
|
import java.util.concurrent.CopyOnWriteArrayList;
|
||||||
|
import java.util.concurrent.ExecutorService;
|
||||||
|
import java.util.concurrent.Executors;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
|
import java.util.concurrent.atomic.AtomicInteger;
|
||||||
|
|
||||||
|
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.assertThrows;
|
||||||
|
import static org.junit.Assert.assertTrue;
|
||||||
|
import static org.mockito.ArgumentMatchers.any;
|
||||||
|
import static org.mockito.ArgumentMatchers.eq;
|
||||||
|
import static org.mockito.ArgumentMatchers.same;
|
||||||
|
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 EngineRuntimeBuilderStudioServiceTest {
|
||||||
|
@Test
|
||||||
|
public void replacementInterceptsCommandsWhileOldManagerRetires() throws Exception {
|
||||||
|
ExecutorService owner = Executors.newSingleThreadExecutor();
|
||||||
|
try (Fixture fixture = new Fixture()) {
|
||||||
|
CompletableFuture<Boolean> teleport = new CompletableFuture<>();
|
||||||
|
AtomicInteger requests = new AtomicInteger();
|
||||||
|
fixture.previous.afterUnregister = () -> {
|
||||||
|
try {
|
||||||
|
owner.submit(() -> fixture.teleportDuringRetirement(teleport, requests))
|
||||||
|
.get(2L, TimeUnit.SECONDS);
|
||||||
|
} catch (Exception failure) {
|
||||||
|
throw new IllegalStateException(failure);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
try (GenerationTransitionGate.Transition ignored = fixture.sessions.transitionGate().beginTransition(2_000L)) {
|
||||||
|
fixture.sessions.sealAndAwait("Studio generation cutover", 2_000L);
|
||||||
|
fixture.builder.refreshStudioRuntimeServices();
|
||||||
|
}
|
||||||
|
|
||||||
|
assertSame(fixture.replacement, fixture.engine.runtime.worldManager());
|
||||||
|
assertEquals(List.of(fixture.replacement), fixture.registered);
|
||||||
|
assertEquals(1, requests.get());
|
||||||
|
assertFalse(teleport.isDone());
|
||||||
|
assertTrue(teleport.complete(true));
|
||||||
|
fixture.previous.tick.run();
|
||||||
|
fixture.replacement.tick.run();
|
||||||
|
assertEquals(0, fixture.previous.updates);
|
||||||
|
assertEquals(1, fixture.replacement.updates);
|
||||||
|
verify(fixture.previousEffects).close();
|
||||||
|
verify(fixture.nextEffects, never()).close();
|
||||||
|
} finally {
|
||||||
|
owner.shutdownNow();
|
||||||
|
assertTrue(owner.awaitTermination(2L, TimeUnit.SECONDS));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void failedReplacementStartKeepsThePreviousListenerRegistered() throws Exception {
|
||||||
|
try (Fixture fixture = new Fixture()) {
|
||||||
|
fixture.replacement.failRegistration = true;
|
||||||
|
EngineRuntime previous = fixture.engine.runtime;
|
||||||
|
try (GenerationTransitionGate.Transition ignored = fixture.sessions.transitionGate().beginTransition(2_000L)) {
|
||||||
|
fixture.sessions.sealAndAwait("Studio generation cutover", 2_000L);
|
||||||
|
|
||||||
|
assertThrows(IllegalStateException.class, fixture.builder::refreshStudioRuntimeServices);
|
||||||
|
}
|
||||||
|
|
||||||
|
assertSame(previous, fixture.engine.runtime);
|
||||||
|
assertEquals(List.of(fixture.previous), fixture.registered);
|
||||||
|
assertEquals(0, fixture.previous.closes);
|
||||||
|
assertEquals(1, fixture.replacement.closes);
|
||||||
|
assertEquals(1L, fixture.sessions.currentSessionId());
|
||||||
|
verify(fixture.previousEffects, never()).close();
|
||||||
|
verify(fixture.nextEffects).close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final class Fixture implements AutoCloseable {
|
||||||
|
private final IrisEngine engine = mock(IrisEngine.class);
|
||||||
|
private final GenerationSessionManager sessions = new GenerationSessionManager(true);
|
||||||
|
private final AtomicBoolean closing = new AtomicBoolean(true);
|
||||||
|
private final List<TestWorldManager> registered = new CopyOnWriteArrayList<>();
|
||||||
|
private final EngineEffects previousEffects = mock(EngineEffects.class);
|
||||||
|
private final EngineEffects nextEffects = mock(EngineEffects.class);
|
||||||
|
private final IrisWorld irisWorld = mock(IrisWorld.class);
|
||||||
|
private final World world = mock(World.class);
|
||||||
|
private final TestWorldManager previous;
|
||||||
|
private final TestWorldManager replacement;
|
||||||
|
private final EngineRuntimeBuilder builder;
|
||||||
|
private final MockedStatic<IrisServices> services;
|
||||||
|
|
||||||
|
private Fixture() throws Exception {
|
||||||
|
EngineTarget target = mock(EngineTarget.class);
|
||||||
|
when(engine.getTarget()).thenReturn(target);
|
||||||
|
when(target.getWorld()).thenReturn(irisWorld);
|
||||||
|
when(engine.isStudio()).thenReturn(true);
|
||||||
|
when(engine.isClosing()).thenAnswer(invocation -> closing.get());
|
||||||
|
when(engine.isShuttingDown()).thenCallRealMethod();
|
||||||
|
when(engine.getClosing()).thenReturn(closing);
|
||||||
|
when(engine.getGenerationSessions()).thenReturn(sessions);
|
||||||
|
engine.lifecycleState = IrisEngine.LifecycleState.HOTLOADING;
|
||||||
|
EngineBackgroundTasks background = mock(EngineBackgroundTasks.class);
|
||||||
|
when(background.scheduleTrackedTask(any(Runnable.class))).thenReturn(true);
|
||||||
|
Field backgroundField = IrisEngine.class.getDeclaredField("backgroundTasks");
|
||||||
|
backgroundField.setAccessible(true);
|
||||||
|
backgroundField.set(engine, background);
|
||||||
|
previous = new TestWorldManager(engine, registered);
|
||||||
|
replacement = new TestWorldManager(engine, registered);
|
||||||
|
engine.runtime = new EngineRuntime(mock(GenerationRuntime.class), previousEffects, previous);
|
||||||
|
previous.start();
|
||||||
|
builder = new EngineRuntimeBuilder(engine);
|
||||||
|
EngineEffectsProvider effectsProvider = mock(EngineEffectsProvider.class);
|
||||||
|
EngineWorldManagerProvider managerProvider = mock(EngineWorldManagerProvider.class);
|
||||||
|
when(effectsProvider.create(engine)).thenReturn(nextEffects);
|
||||||
|
when(managerProvider.create(engine)).thenReturn(replacement);
|
||||||
|
services = mockStatic(IrisServices.class);
|
||||||
|
services.when(() -> IrisServices.get(EngineEffectsProvider.class)).thenReturn(effectsProvider);
|
||||||
|
services.when(() -> IrisServices.get(EngineWorldManagerProvider.class)).thenReturn(managerProvider);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void teleportDuringRetirement(CompletableFuture<Boolean> teleport, AtomicInteger requests) {
|
||||||
|
Player player = mock(Player.class);
|
||||||
|
Location destination = new Location(world, 1024.5D, 309D, 0.5D, 45F, 10F);
|
||||||
|
PlayerTeleportEvent event = new PlayerTeleportEvent(player,
|
||||||
|
new Location(world, 0.5D, 100D, 0.5D), destination,
|
||||||
|
PlayerTeleportEvent.TeleportCause.COMMAND);
|
||||||
|
try (MockedStatic<BukkitPlatform> platform = mockStatic(BukkitPlatform.class);
|
||||||
|
MockedStatic<BukkitWorldBinding> binding = mockStatic(BukkitWorldBinding.class)) {
|
||||||
|
platform.when(BukkitPlatform::isPaperServer).thenReturn(true);
|
||||||
|
binding.when(() -> BukkitWorldBinding.world(irisWorld)).thenReturn(world);
|
||||||
|
platform.when(() -> BukkitPlatform.teleportAsync(same(player), any(Location.class),
|
||||||
|
eq(PlayerTeleportEvent.TeleportCause.COMMAND)))
|
||||||
|
.thenAnswer(invocation -> {
|
||||||
|
assertTrue(event.isCancelled());
|
||||||
|
Location requested = invocation.getArgument(1, Location.class);
|
||||||
|
assertEquals(destination, requested);
|
||||||
|
assertNotSame(destination, requested);
|
||||||
|
requests.incrementAndGet();
|
||||||
|
return teleport;
|
||||||
|
});
|
||||||
|
assertEquals(List.of(replacement), registered);
|
||||||
|
for (TestWorldManager manager : registered) {
|
||||||
|
if (!event.isCancelled()) {
|
||||||
|
manager.on(event);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
assertTrue(event.isCancelled());
|
||||||
|
previous.tick.run();
|
||||||
|
replacement.tick.run();
|
||||||
|
assertEquals(0, previous.updates);
|
||||||
|
assertEquals(0, replacement.updates);
|
||||||
|
assertEquals(0, sessions.activeLeases());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void close() {
|
||||||
|
previous.afterUnregister = null;
|
||||||
|
previous.close();
|
||||||
|
replacement.close();
|
||||||
|
services.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final class TestWorldManager extends EngineAssignedWorldManager {
|
||||||
|
private final List<TestWorldManager> registered;
|
||||||
|
private final WorldTeleportWarmup warmup = new WorldTeleportWarmup();
|
||||||
|
private Runnable tick;
|
||||||
|
private Runnable afterUnregister;
|
||||||
|
private boolean failRegistration;
|
||||||
|
private int updates;
|
||||||
|
private int closes;
|
||||||
|
|
||||||
|
private TestWorldManager(Engine engine, List<TestWorldManager> registered) {
|
||||||
|
super(engine);
|
||||||
|
this.registered = registered;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void close() {
|
||||||
|
closes++;
|
||||||
|
super.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void registerManagerListener() {
|
||||||
|
registered.add(this);
|
||||||
|
if (failRegistration) {
|
||||||
|
throw new IllegalStateException("listener registration failed");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void unregisterManagerListener() {
|
||||||
|
registered.remove(this);
|
||||||
|
if (afterUnregister != null) {
|
||||||
|
afterUnregister.run();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected int scheduleManagerTick(Runnable tick) {
|
||||||
|
this.tick = tick;
|
||||||
|
return 42;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void cancelManagerTick(int scheduledTaskId) {
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getEntityCount() {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getChunkCount() {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public double getEntitySaturation() {
|
||||||
|
return 0D;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onTick() {
|
||||||
|
updates++;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onSave() {
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onBlockBreak(BlockBreakEvent event) {
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onBlockPlace(BlockPlaceEvent event) {
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onChunkLoad(Chunk chunk, boolean generated) {
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onChunkUnload(Chunk chunk) {
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void teleportAsync(PlayerTeleportEvent event) {
|
||||||
|
warmup.teleportAsync(event);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,8 +6,38 @@ import java.util.concurrent.TimeoutException;
|
|||||||
|
|
||||||
import static org.junit.Assert.assertFalse;
|
import static org.junit.Assert.assertFalse;
|
||||||
import static org.junit.Assert.assertTrue;
|
import static org.junit.Assert.assertTrue;
|
||||||
|
import static org.mockito.Mockito.mock;
|
||||||
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
public class IrisEngineLifecycleGateTest {
|
public class IrisEngineLifecycleGateTest {
|
||||||
|
@Test
|
||||||
|
public void studioSessionSealDoesNotMeanWorldShutdown() {
|
||||||
|
IrisEngine engine = mock(IrisEngine.class);
|
||||||
|
when(engine.isShuttingDown()).thenCallRealMethod();
|
||||||
|
when(engine.isClosing()).thenReturn(true);
|
||||||
|
engine.lifecycleState = IrisEngine.LifecycleState.HOTLOADING;
|
||||||
|
|
||||||
|
assertTrue(engine.isClosing());
|
||||||
|
assertFalse(engine.isShuttingDown());
|
||||||
|
|
||||||
|
engine.lifecycleState = IrisEngine.LifecycleState.CLOSING;
|
||||||
|
assertTrue(engine.isShuttingDown());
|
||||||
|
engine.lifecycleState = IrisEngine.LifecycleState.CLOSED;
|
||||||
|
assertTrue(engine.isShuttingDown());
|
||||||
|
engine.lifecycleState = IrisEngine.LifecycleState.FAILED;
|
||||||
|
assertTrue(engine.isShuttingDown());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void closedWorldRejectsNavigationRegardlessOfLifecyclePublication() {
|
||||||
|
IrisEngine engine = mock(IrisEngine.class);
|
||||||
|
when(engine.isShuttingDown()).thenCallRealMethod();
|
||||||
|
engine.lifecycleState = IrisEngine.LifecycleState.RUNNING;
|
||||||
|
engine.closed = true;
|
||||||
|
|
||||||
|
assertTrue(engine.isShuttingDown());
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void incompleteBackgroundDrainBlocksResourceRelease() {
|
public void incompleteBackgroundDrainBlocksResourceRelease() {
|
||||||
EngineBackgroundTasks.BackgroundTaskDrain drain = new EngineBackgroundTasks.BackgroundTaskDrain(
|
EngineBackgroundTasks.BackgroundTaskDrain drain = new EngineBackgroundTasks.BackgroundTaskDrain(
|
||||||
|
|||||||
+93
-3
@@ -12,10 +12,20 @@ import org.bukkit.event.player.PlayerTeleportEvent;
|
|||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
import org.mockito.MockedStatic;
|
import org.mockito.MockedStatic;
|
||||||
|
|
||||||
|
import java.util.concurrent.CountDownLatch;
|
||||||
|
import java.util.concurrent.ExecutorService;
|
||||||
|
import java.util.concurrent.Executors;
|
||||||
|
import java.util.concurrent.Future;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
|
import static org.junit.Assert.assertEquals;
|
||||||
import static org.junit.Assert.assertNull;
|
import static org.junit.Assert.assertNull;
|
||||||
import static org.junit.Assert.assertSame;
|
import static org.junit.Assert.assertSame;
|
||||||
|
import static org.junit.Assert.assertTrue;
|
||||||
import static org.mockito.Mockito.mock;
|
import static org.mockito.Mockito.mock;
|
||||||
import static org.mockito.Mockito.mockStatic;
|
import static org.mockito.Mockito.mockStatic;
|
||||||
|
import static org.mockito.Mockito.never;
|
||||||
|
import static org.mockito.Mockito.verify;
|
||||||
import static org.mockito.Mockito.when;
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
public class EngineAssignedWorldManagerTeleportTest {
|
public class EngineAssignedWorldManagerTeleportTest {
|
||||||
@@ -30,6 +40,68 @@ public class EngineAssignedWorldManagerTeleportTest {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void commandTeleportStillConvertsDuringSealedStudioCutover() throws Exception {
|
||||||
|
ExecutorService reloader = Executors.newSingleThreadExecutor();
|
||||||
|
CountDownLatch entered = new CountDownLatch(1);
|
||||||
|
CountDownLatch release = new CountDownLatch(1);
|
||||||
|
try (TeleportHarness harness = new TeleportHarness(false, true)) {
|
||||||
|
when(harness.engine.isClosing()).thenReturn(true);
|
||||||
|
when(harness.engine.isShuttingDown()).thenReturn(false);
|
||||||
|
Location destination = harness.event.getTo();
|
||||||
|
Future<?> cutover = reloader.submit(() -> {
|
||||||
|
try (GenerationTransitionGate.Transition ignored = harness.sessions.transitionGate().beginTransition(2_000L)) {
|
||||||
|
harness.sessions.sealAndAwait("Studio generation cutover", 2_000L);
|
||||||
|
entered.countDown();
|
||||||
|
assertTrue(release.await(2L, TimeUnit.SECONDS));
|
||||||
|
harness.sessions.activateNextSession();
|
||||||
|
} catch (Exception failure) {
|
||||||
|
throw new IllegalStateException(failure);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
assertTrue(entered.await(2L, TimeUnit.SECONDS));
|
||||||
|
|
||||||
|
harness.manager.on(harness.event);
|
||||||
|
|
||||||
|
assertSame(harness.event, harness.manager.teleportEvent);
|
||||||
|
assertSame(destination, harness.manager.teleportEvent.getTo());
|
||||||
|
assertEquals(PlayerTeleportEvent.TeleportCause.COMMAND, harness.manager.teleportEvent.getCause());
|
||||||
|
assertEquals(0, harness.sessions.activeLeases());
|
||||||
|
release.countDown();
|
||||||
|
cutover.get(2L, TimeUnit.SECONDS);
|
||||||
|
} finally {
|
||||||
|
release.countDown();
|
||||||
|
reloader.shutdownNow();
|
||||||
|
assertTrue(reloader.awaitTermination(2L, TimeUnit.SECONDS));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void closingStudioCancelsCommandInsteadOfFallingThroughToVanilla() {
|
||||||
|
try (TeleportHarness harness = new TeleportHarness(false, true)) {
|
||||||
|
when(harness.engine.isClosing()).thenReturn(true);
|
||||||
|
when(harness.engine.isShuttingDown()).thenReturn(true);
|
||||||
|
|
||||||
|
harness.manager.on(harness.event);
|
||||||
|
|
||||||
|
verify(harness.event).setCancelled(true);
|
||||||
|
assertNull(harness.manager.teleportEvent);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void closedStudioCancelsEvenAnAlreadyLoadedDestination() {
|
||||||
|
try (TeleportHarness harness = new TeleportHarness(true, true)) {
|
||||||
|
when(harness.engine.isClosed()).thenReturn(true);
|
||||||
|
when(harness.engine.isShuttingDown()).thenReturn(true);
|
||||||
|
|
||||||
|
harness.manager.on(harness.event);
|
||||||
|
|
||||||
|
verify(harness.event).setCancelled(true);
|
||||||
|
assertNull(harness.manager.teleportEvent);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void loadedIrisDestinationContinuesWithoutInterception() {
|
public void loadedIrisDestinationContinuesWithoutInterception() {
|
||||||
TeleportHarness harness = new TeleportHarness(true, true);
|
TeleportHarness harness = new TeleportHarness(true, true);
|
||||||
@@ -80,16 +152,20 @@ public class EngineAssignedWorldManagerTeleportTest {
|
|||||||
public void otherWorldDestinationContinuesWithoutInterception() {
|
public void otherWorldDestinationContinuesWithoutInterception() {
|
||||||
TeleportHarness harness = new TeleportHarness(false, true);
|
TeleportHarness harness = new TeleportHarness(false, true);
|
||||||
World otherWorld = mock(World.class);
|
World otherWorld = mock(World.class);
|
||||||
|
when(harness.engine.isClosing()).thenReturn(true);
|
||||||
|
when(harness.engine.isShuttingDown()).thenReturn(true);
|
||||||
when(harness.event.getTo()).thenReturn(new Location(otherWorld, 400.5D, 96D, 2.5D));
|
when(harness.event.getTo()).thenReturn(new Location(otherWorld, 400.5D, 96D, 2.5D));
|
||||||
|
|
||||||
try (harness) {
|
try (harness) {
|
||||||
harness.manager.on(harness.event);
|
harness.manager.on(harness.event);
|
||||||
|
|
||||||
assertNull(harness.manager.teleportEvent);
|
assertNull(harness.manager.teleportEvent);
|
||||||
|
verify(harness.event, never()).setCancelled(true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static final class TeleportHarness implements AutoCloseable {
|
private static final class TeleportHarness implements AutoCloseable {
|
||||||
|
private final GenerationSessionManager sessions = new GenerationSessionManager(true);
|
||||||
private final Engine engine;
|
private final Engine engine;
|
||||||
private final TestWorldManager manager;
|
private final TestWorldManager manager;
|
||||||
private final PlayerTeleportEvent event;
|
private final PlayerTeleportEvent event;
|
||||||
@@ -106,6 +182,7 @@ public class EngineAssignedWorldManagerTeleportTest {
|
|||||||
|
|
||||||
when(engine.getTarget()).thenReturn(target);
|
when(engine.getTarget()).thenReturn(target);
|
||||||
when(engine.isStudio()).thenReturn(true);
|
when(engine.isStudio()).thenReturn(true);
|
||||||
|
when(engine.getGenerationSessions()).thenReturn(sessions);
|
||||||
when(target.getWorld()).thenReturn(irisWorld);
|
when(target.getWorld()).thenReturn(irisWorld);
|
||||||
when(event.getTo()).thenReturn(destination);
|
when(event.getTo()).thenReturn(destination);
|
||||||
when(event.getCause()).thenReturn(PlayerTeleportEvent.TeleportCause.COMMAND);
|
when(event.getCause()).thenReturn(PlayerTeleportEvent.TeleportCause.COMMAND);
|
||||||
@@ -116,10 +193,12 @@ public class EngineAssignedWorldManagerTeleportTest {
|
|||||||
binding = mockStatic(BukkitWorldBinding.class);
|
binding = mockStatic(BukkitWorldBinding.class);
|
||||||
binding.when(() -> BukkitWorldBinding.world(irisWorld)).thenReturn(world);
|
binding.when(() -> BukkitWorldBinding.world(irisWorld)).thenReturn(world);
|
||||||
manager = new TestWorldManager(engine);
|
manager = new TestWorldManager(engine);
|
||||||
|
manager.start();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void close() {
|
public void close() {
|
||||||
|
manager.close();
|
||||||
binding.close();
|
binding.close();
|
||||||
platform.close();
|
platform.close();
|
||||||
}
|
}
|
||||||
@@ -133,9 +212,20 @@ public class EngineAssignedWorldManagerTeleportTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected boolean runManagerTask(String operation, Runnable task) {
|
protected void registerManagerListener() {
|
||||||
task.run();
|
}
|
||||||
return true;
|
|
||||||
|
@Override
|
||||||
|
protected int scheduleManagerTick(Runnable tick) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void unregisterManagerListener() {
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void cancelManagerTick(int scheduledTaskId) {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|||||||
+224
@@ -0,0 +1,224 @@
|
|||||||
|
package art.arcane.iris.engine.history;
|
||||||
|
|
||||||
|
import art.arcane.iris.testsupport.DurabilityMode;
|
||||||
|
import org.junit.ClassRule;
|
||||||
|
import org.junit.Rule;
|
||||||
|
import org.junit.Test;
|
||||||
|
import org.junit.rules.TemporaryFolder;
|
||||||
|
|
||||||
|
import java.lang.reflect.Field;
|
||||||
|
import java.util.AbstractList;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.OptionalInt;
|
||||||
|
import java.util.concurrent.CountDownLatch;
|
||||||
|
import java.util.concurrent.CyclicBarrier;
|
||||||
|
import java.util.concurrent.ExecutorService;
|
||||||
|
import java.util.concurrent.Executors;
|
||||||
|
import java.util.concurrent.Future;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
|
import static org.junit.Assert.assertEquals;
|
||||||
|
import static org.junit.Assert.assertFalse;
|
||||||
|
import static org.junit.Assert.assertThrows;
|
||||||
|
import static org.junit.Assert.assertTrue;
|
||||||
|
import static org.mockito.ArgumentMatchers.anyInt;
|
||||||
|
import static org.mockito.Mockito.mock;
|
||||||
|
import static org.mockito.Mockito.times;
|
||||||
|
import static org.mockito.Mockito.verify;
|
||||||
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
|
public class BoundarySamplingConcurrencyTest {
|
||||||
|
private static final BoundaryColumnGeometry GEOMETRY = new BoundaryColumnGeometry(0,
|
||||||
|
List.of(new BoundaryColumnGeometry.Voxel("minecraft:stone", BoundaryColumnGeometry.Phase.SOLID,
|
||||||
|
"", false)), new int[]{65}, new short[]{0});
|
||||||
|
|
||||||
|
@ClassRule
|
||||||
|
public static final DurabilityMode DURABILITY = DurabilityMode.relaxed();
|
||||||
|
|
||||||
|
@Rule
|
||||||
|
public TemporaryFolder temporaryFolder = new TemporaryFolder();
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void simultaneousQueriesBuildOneCandidateIndex() throws Exception {
|
||||||
|
TerrainBoundarySignatureStore.Snapshot snapshot = mock(TerrainBoundarySignatureStore.Snapshot.class);
|
||||||
|
CountDownLatch scanning = new CountDownLatch(1);
|
||||||
|
CountDownLatch release = new CountDownLatch(1);
|
||||||
|
when(snapshot.nearestCandidatesForChunk(0, 0, 32)).thenAnswer(invocation -> {
|
||||||
|
scanning.countDown();
|
||||||
|
assertTrue(release.await(5, TimeUnit.SECONDS));
|
||||||
|
return List.of(signature(0, 0));
|
||||||
|
});
|
||||||
|
TransitionBoundarySampler sampler = new TransitionBoundarySampler(32, snapshot);
|
||||||
|
ExecutorService workers = Executors.newFixedThreadPool(8);
|
||||||
|
CyclicBarrier start = new CyclicBarrier(8);
|
||||||
|
List<Future<BoundaryGeometryInfluence>> queries = new ArrayList<>();
|
||||||
|
try {
|
||||||
|
for (int index = 0; index < 8; index++) {
|
||||||
|
queries.add(workers.submit(() -> {
|
||||||
|
start.await(5, TimeUnit.SECONDS);
|
||||||
|
return sampler.geometryAt(1, 1);
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
assertTrue(scanning.await(5, TimeUnit.SECONDS));
|
||||||
|
release.countDown();
|
||||||
|
BoundaryGeometryInfluence expected = queries.getFirst().get(5, TimeUnit.SECONDS);
|
||||||
|
for (Future<BoundaryGeometryInfluence> query : queries) {
|
||||||
|
assertEquals(expected, query.get(5, TimeUnit.SECONDS));
|
||||||
|
}
|
||||||
|
verify(snapshot, times(1)).nearestCandidatesForChunk(0, 0, 32);
|
||||||
|
assertEquals(1L, sampler.candidateBuildCount());
|
||||||
|
} finally {
|
||||||
|
release.countDown();
|
||||||
|
workers.shutdownNow();
|
||||||
|
assertTrue(workers.awaitTermination(5, TimeUnit.SECONDS));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void failedCandidateBuildCanBeRetried() {
|
||||||
|
TerrainBoundarySignatureStore.Snapshot snapshot = mock(TerrainBoundarySignatureStore.Snapshot.class);
|
||||||
|
when(snapshot.nearestCandidatesForChunk(0, 0, 32))
|
||||||
|
.thenThrow(new IllegalStateException("Unavailable boundary shard"))
|
||||||
|
.thenReturn(List.of(signature(0, 0)));
|
||||||
|
TransitionBoundarySampler sampler = new TransitionBoundarySampler(32, snapshot);
|
||||||
|
|
||||||
|
assertThrows(IllegalStateException.class, () -> sampler.sample(1, 1));
|
||||||
|
assertEquals(64D, sampler.sample(1, 1).historicalSurfaceHeight(), 0D);
|
||||||
|
verify(snapshot, times(2)).nearestCandidatesForChunk(0, 0, 32);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void generationWindowReusesCandidatesAndEvictsPastItsChunkBound() {
|
||||||
|
TerrainBoundarySignatureStore.Snapshot snapshot = mock(TerrainBoundarySignatureStore.Snapshot.class);
|
||||||
|
when(snapshot.nearestCandidatesForChunk(anyInt(), anyInt(), anyInt())).thenReturn(List.of());
|
||||||
|
TransitionBoundarySampler sampler = new TransitionBoundarySampler(32, snapshot);
|
||||||
|
for (int chunk = 0; chunk < 512; chunk++) {
|
||||||
|
sampler.geometryAt(chunk * 16, 0);
|
||||||
|
}
|
||||||
|
sampler.geometryAt(0, 0);
|
||||||
|
assertEquals(512L, sampler.candidateBuildCount());
|
||||||
|
sampler.geometryAt(512 * 16, 0);
|
||||||
|
sampler.geometryAt(16, 0);
|
||||||
|
assertEquals(514L, sampler.candidateBuildCount());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void denseCandidateIndexesRespectTheReferenceBudget() {
|
||||||
|
List<TerrainBoundarySignature> dense = new ArrayList<>();
|
||||||
|
for (int x = 0; x < 32; x++) {
|
||||||
|
for (int z = 0; z < 32; z++) {
|
||||||
|
dense.add(signature(x, z));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
TerrainBoundarySignatureStore.Snapshot snapshot = mock(TerrainBoundarySignatureStore.Snapshot.class);
|
||||||
|
when(snapshot.nearestCandidatesForChunk(anyInt(), anyInt(), anyInt())).thenReturn(dense);
|
||||||
|
TransitionBoundarySampler sampler = new TransitionBoundarySampler(32, snapshot);
|
||||||
|
for (int chunk = 0; chunk < 33; chunk++) {
|
||||||
|
sampler.geometryAt(chunk * 16, 0);
|
||||||
|
}
|
||||||
|
sampler.geometryAt(0, 0);
|
||||||
|
assertEquals(34L, sampler.candidateBuildCount());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void activeGeometryColumnKeepsItsCandidatesAfterSharedCacheEviction() throws Exception {
|
||||||
|
TerrainBoundarySignatureStore.Snapshot snapshot = mock(TerrainBoundarySignatureStore.Snapshot.class);
|
||||||
|
when(snapshot.nearestCandidatesForChunk(anyInt(), anyInt(), anyInt())).thenReturn(List.of(signature(0, 0)));
|
||||||
|
TransitionBoundarySampler sampler = new TransitionBoundarySampler(32, snapshot);
|
||||||
|
BoundaryGeometryInfluence first = sampler.geometryAt(1, 1);
|
||||||
|
ExecutorService other = Executors.newSingleThreadExecutor();
|
||||||
|
try {
|
||||||
|
other.submit(() -> {
|
||||||
|
for (int chunk = 1; chunk <= 512; chunk++) {
|
||||||
|
sampler.geometryAt(chunk * 16, 0);
|
||||||
|
}
|
||||||
|
}).get(5, TimeUnit.SECONDS);
|
||||||
|
assertEquals(first, sampler.geometryAt(1, 1));
|
||||||
|
verify(snapshot, times(1)).nearestCandidatesForChunk(0, 0, 32);
|
||||||
|
} finally {
|
||||||
|
other.shutdownNow();
|
||||||
|
assertTrue(other.awaitTermination(5, TimeUnit.SECONDS));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void distinctBoundaryScansDoNotHoldTheShardCacheMonitor() throws Exception {
|
||||||
|
TerrainBoundarySignatureStore store = new TerrainBoundarySignatureStore(
|
||||||
|
temporaryFolder.newFolder("parallel-scans").toPath());
|
||||||
|
TerrainBoundarySignatureStore.Snapshot snapshot = store.publish(1L,
|
||||||
|
List.of(signature(0, 0), signature(512, 512)));
|
||||||
|
snapshot.signatureAt(0, 0).orElseThrow();
|
||||||
|
snapshot.signatureAt(512, 512).orElseThrow();
|
||||||
|
Map<Long, List<TerrainBoundarySignature>> cache = shardCache(snapshot);
|
||||||
|
BlockingSignatures blocked = new BlockingSignatures(cache.get(0L));
|
||||||
|
cache.put(0L, blocked);
|
||||||
|
ExecutorService workers = Executors.newFixedThreadPool(2);
|
||||||
|
try {
|
||||||
|
Future<List<TerrainBoundarySignature>> first = workers.submit(
|
||||||
|
() -> snapshot.nearestCandidatesForChunk(0, 0, 16));
|
||||||
|
assertTrue(blocked.entered.await(5, TimeUnit.SECONDS));
|
||||||
|
Future<List<TerrainBoundarySignature>> distant = workers.submit(
|
||||||
|
() -> snapshot.nearestCandidatesForChunk(32, 32, 16));
|
||||||
|
assertEquals(512, distant.get(3, TimeUnit.SECONDS).getFirst().blockX());
|
||||||
|
assertFalse(first.isDone());
|
||||||
|
assertTrue(workers.submit(() -> snapshot.intersectsTerrainBand(
|
||||||
|
new TerrainBoundarySignatureStore.BlockBounds(512, 512, 527, 527), 16))
|
||||||
|
.get(3, TimeUnit.SECONDS));
|
||||||
|
blocked.release.countDown();
|
||||||
|
assertEquals(0, first.get(3, TimeUnit.SECONDS).getFirst().blockX());
|
||||||
|
assertEquals(2L, snapshot.shardLoadCount());
|
||||||
|
} finally {
|
||||||
|
blocked.release.countDown();
|
||||||
|
workers.shutdownNow();
|
||||||
|
assertTrue(workers.awaitTermination(5, TimeUnit.SECONDS));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
private static Map<Long, List<TerrainBoundarySignature>> shardCache(
|
||||||
|
TerrainBoundarySignatureStore.Snapshot snapshot) throws Exception {
|
||||||
|
Field sourceField = TerrainBoundarySignatureStore.Snapshot.class.getDeclaredField("source");
|
||||||
|
sourceField.setAccessible(true);
|
||||||
|
Object source = sourceField.get(snapshot);
|
||||||
|
Field cacheField = source.getClass().getDeclaredField("cache");
|
||||||
|
cacheField.setAccessible(true);
|
||||||
|
return (Map<Long, List<TerrainBoundarySignature>>) cacheField.get(source);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static TerrainBoundarySignature signature(int x, int z) {
|
||||||
|
return new TerrainBoundarySignature(
|
||||||
|
new TerrainBoundarySignature.Column(x, z, 64, 64, OptionalInt.empty(), OptionalInt.empty()),
|
||||||
|
new TerrainBoundarySignature.Samples(new TerrainBoundarySignature.VerticalLayout(0, 4, 0),
|
||||||
|
new TerrainBoundarySignature.BiomeEncoding(List.of(), new short[0])),
|
||||||
|
GEOMETRY);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final class BlockingSignatures extends AbstractList<TerrainBoundarySignature> {
|
||||||
|
private final List<TerrainBoundarySignature> values;
|
||||||
|
private final CountDownLatch entered = new CountDownLatch(1);
|
||||||
|
private final CountDownLatch release = new CountDownLatch(1);
|
||||||
|
|
||||||
|
private BlockingSignatures(List<TerrainBoundarySignature> values) {
|
||||||
|
this.values = values;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public TerrainBoundarySignature get(int index) {
|
||||||
|
entered.countDown();
|
||||||
|
try {
|
||||||
|
assertTrue(release.await(5, TimeUnit.SECONDS));
|
||||||
|
} catch (InterruptedException interruption) {
|
||||||
|
Thread.currentThread().interrupt();
|
||||||
|
throw new AssertionError(interruption);
|
||||||
|
}
|
||||||
|
return values.get(index);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int size() {
|
||||||
|
return values.size();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -262,6 +262,76 @@ public class GenerationBoundaryStoreTest {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void repeatedRegionQueriesDoNotWaitForBoundaryIteration() throws Exception {
|
||||||
|
GenerationBoundaryStore store = new GenerationBoundaryStore(
|
||||||
|
temporaryFolder.newFolder("boundary-warm-region").toPath());
|
||||||
|
GenerationBoundary boundary = store.publishPacked(1L,
|
||||||
|
new long[]{GenerationBoundary.packChunk(-1, -1)});
|
||||||
|
CountDownLatch entered = new CountDownLatch(1);
|
||||||
|
CountDownLatch release = new CountDownLatch(1);
|
||||||
|
ExecutorService queries = Executors.newSingleThreadExecutor();
|
||||||
|
ExecutorService iterator = Executors.newSingleThreadExecutor();
|
||||||
|
try {
|
||||||
|
assertTrue(queries.submit(() -> boundary.isHistoricalChunk(-1, -1)).get(5, TimeUnit.SECONDS));
|
||||||
|
Future<?> iteration = iterator.submit(() -> {
|
||||||
|
boundary.forEachHistoricalChunk((x, z) -> {
|
||||||
|
entered.countDown();
|
||||||
|
try {
|
||||||
|
assertTrue(release.await(5, TimeUnit.SECONDS));
|
||||||
|
} catch (InterruptedException interruption) {
|
||||||
|
Thread.currentThread().interrupt();
|
||||||
|
throw new IOException("Interrupted boundary iteration", interruption);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
assertTrue(entered.await(5, TimeUnit.SECONDS));
|
||||||
|
Future<Boolean> warm = queries.submit(() -> {
|
||||||
|
for (int x = -32; x < -1; x++) {
|
||||||
|
assertFalse(boundary.isHistoricalChunk(x, -1));
|
||||||
|
}
|
||||||
|
return boundary.isHistoricalChunk(-1, -1);
|
||||||
|
});
|
||||||
|
assertTrue(warm.get(3, TimeUnit.SECONDS));
|
||||||
|
assertFalse(iteration.isDone());
|
||||||
|
release.countDown();
|
||||||
|
iteration.get(5, TimeUnit.SECONDS);
|
||||||
|
assertEquals(1, boundary.cachedRegionCount());
|
||||||
|
} finally {
|
||||||
|
release.countDown();
|
||||||
|
queries.shutdownNow();
|
||||||
|
iterator.shutdownNow();
|
||||||
|
assertTrue(queries.awaitTermination(5, TimeUnit.SECONDS));
|
||||||
|
assertTrue(iterator.awaitTermination(5, TimeUnit.SECONDS));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void lastRegionChangesPreserveMissingBitsAndCoordinateExtremes() throws Exception {
|
||||||
|
GenerationBoundaryStore store = new GenerationBoundaryStore(
|
||||||
|
temporaryFolder.newFolder("boundary-region-transitions").toPath());
|
||||||
|
List<GenerationBoundary.ChunkCoordinate> chunks = List.of(
|
||||||
|
new GenerationBoundary.ChunkCoordinate(-33, -1),
|
||||||
|
new GenerationBoundary.ChunkCoordinate(-32, -32),
|
||||||
|
new GenerationBoundary.ChunkCoordinate(0, 0),
|
||||||
|
new GenerationBoundary.ChunkCoordinate(31, -32),
|
||||||
|
new GenerationBoundary.ChunkCoordinate(32, 31),
|
||||||
|
new GenerationBoundary.ChunkCoordinate(Integer.MIN_VALUE, Integer.MAX_VALUE),
|
||||||
|
new GenerationBoundary.ChunkCoordinate(Integer.MAX_VALUE, Integer.MIN_VALUE));
|
||||||
|
GenerationBoundary boundary = store.publish(1L, chunks);
|
||||||
|
for (int pass = 0; pass < 3; pass++) {
|
||||||
|
for (GenerationBoundary.ChunkCoordinate chunk : chunks) {
|
||||||
|
assertTrue(boundary.isHistoricalChunk(chunk.chunkX(), chunk.chunkZ()));
|
||||||
|
int neighbor = chunk.chunkX() == Integer.MAX_VALUE ? chunk.chunkX() - 1 : chunk.chunkX() + 1;
|
||||||
|
assertFalse(boundary.isHistoricalChunk(neighbor, chunk.chunkZ()));
|
||||||
|
assertFalse(boundary.isHistoricalChunk(1024, -5555));
|
||||||
|
assertTrue(boundary.isHistoricalChunk(chunk.chunkX(), chunk.chunkZ()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
assertTrue(boundary.cachedRegionCount() <= 64);
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void knownRegionHitsKeepAccessOrderAndEvictedMasksAreRevalidated() throws Exception {
|
public void knownRegionHitsKeepAccessOrderAndEvictedMasksAreRevalidated() throws Exception {
|
||||||
GenerationBoundaryStore store = new GenerationBoundaryStore(temporaryFolder.newFolder("boundary-cache-revalidation").toPath());
|
GenerationBoundaryStore store = new GenerationBoundaryStore(temporaryFolder.newFolder("boundary-cache-revalidation").toPath());
|
||||||
|
|||||||
@@ -0,0 +1,72 @@
|
|||||||
|
package art.arcane.iris.engine.object;
|
||||||
|
|
||||||
|
import art.arcane.iris.core.link.Identifier;
|
||||||
|
import art.arcane.iris.core.service.ExternalDataSVC;
|
||||||
|
import art.arcane.iris.spi.IrisServices;
|
||||||
|
import art.arcane.iris.spi.PlatformBlockState;
|
||||||
|
import art.arcane.iris.util.common.data.IrisCustomData;
|
||||||
|
import org.bukkit.Location;
|
||||||
|
import org.bukkit.World;
|
||||||
|
import org.bukkit.block.Block;
|
||||||
|
import org.bukkit.block.data.BlockData;
|
||||||
|
import org.junit.Test;
|
||||||
|
import org.mockito.MockedStatic;
|
||||||
|
|
||||||
|
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 IrisObjectDirectPlacementTest {
|
||||||
|
@Test
|
||||||
|
public void bothDirectPlacementMethodsUseProviderWithoutPassingProxyToBukkit() {
|
||||||
|
BlockData base = mock(BlockData.class);
|
||||||
|
Identifier id = new Identifier("craftengine", "test/lamp[lit=true]");
|
||||||
|
IrisCustomData custom = IrisCustomData.of(base, id);
|
||||||
|
IrisObject object = object(custom);
|
||||||
|
World world = mock(World.class);
|
||||||
|
Block block = mock(Block.class);
|
||||||
|
when(world.getBlockAt(any(Location.class))).thenReturn(block);
|
||||||
|
ExternalDataSVC external = mock(ExternalDataSVC.class);
|
||||||
|
when(external.placeBlock(block, id)).thenReturn(true);
|
||||||
|
try (MockedStatic<IrisServices> services = mockStatic(IrisServices.class)) {
|
||||||
|
services.when(() -> IrisServices.getOrNull(ExternalDataSVC.class)).thenReturn(external);
|
||||||
|
|
||||||
|
object.place(new Location(world, 7, 80, -9));
|
||||||
|
object.placeCenterY(new Location(world, 7, 80, -9));
|
||||||
|
|
||||||
|
verify(external, times(2)).placeBlock(block, id);
|
||||||
|
verify(block, times(0)).setBlockData(any(BlockData.class), anyBoolean());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void unsupportedProviderPlacesOnlyBaseDataAndVanillaStillPlacesNormally() {
|
||||||
|
BlockData base = mock(BlockData.class);
|
||||||
|
Identifier id = new Identifier("craftengine", "test/chair[variant=alpha]");
|
||||||
|
World world = mock(World.class);
|
||||||
|
Block block = mock(Block.class);
|
||||||
|
when(world.getBlockAt(any(Location.class))).thenReturn(block);
|
||||||
|
ExternalDataSVC external = mock(ExternalDataSVC.class);
|
||||||
|
try (MockedStatic<IrisServices> services = mockStatic(IrisServices.class)) {
|
||||||
|
services.when(() -> IrisServices.getOrNull(ExternalDataSVC.class)).thenReturn(external);
|
||||||
|
|
||||||
|
object(IrisCustomData.of(base, id)).place(new Location(world, 7, 80, -9));
|
||||||
|
object(base).placeCenterY(new Location(world, 7, 80, -9));
|
||||||
|
|
||||||
|
verify(external).placeBlock(block, id);
|
||||||
|
verify(block, times(2)).setBlockData(base, false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static IrisObject object(BlockData data) {
|
||||||
|
IrisObject object = new IrisObject(1, 1, 1);
|
||||||
|
PlatformBlockState state = mock(PlatformBlockState.class);
|
||||||
|
when(state.nativeHandle()).thenReturn(data);
|
||||||
|
object.setUnsigned(0, 0, 0, state);
|
||||||
|
return object;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,158 @@
|
|||||||
|
package art.arcane.iris.engine.object;
|
||||||
|
|
||||||
|
import art.arcane.iris.spi.PlatformBlockState;
|
||||||
|
import art.arcane.iris.testsupport.PlatformBinding;
|
||||||
|
import art.arcane.iris.util.common.math.IrisBlockVector;
|
||||||
|
import art.arcane.volmlib.util.collection.KMap;
|
||||||
|
import art.arcane.volmlib.util.math.RNG;
|
||||||
|
import com.google.gson.Gson;
|
||||||
|
import com.google.gson.reflect.TypeToken;
|
||||||
|
import org.junit.After;
|
||||||
|
import org.junit.Before;
|
||||||
|
import org.junit.Rule;
|
||||||
|
import org.junit.Test;
|
||||||
|
|
||||||
|
import java.io.ByteArrayInputStream;
|
||||||
|
import java.io.ByteArrayOutputStream;
|
||||||
|
import java.io.DataOutputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
import static org.junit.Assert.assertEquals;
|
||||||
|
import static org.junit.Assert.assertFalse;
|
||||||
|
import static org.junit.Assert.assertNotNull;
|
||||||
|
import static org.junit.Assert.assertNull;
|
||||||
|
import static org.mockito.ArgumentMatchers.anyString;
|
||||||
|
import static org.mockito.ArgumentMatchers.eq;
|
||||||
|
import static org.mockito.Mockito.mock;
|
||||||
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
|
public class IrisObjectTransientBlockTest {
|
||||||
|
private static final List<String> PALETTE = List.of(
|
||||||
|
"minecraft:moving_piston[facing=down,type=normal]", "minecraft:oak_log[axis=y]",
|
||||||
|
"minecraft:oak_leaves[persistent=true]", "minecraft:chest[facing=north,type=single,waterlogged=false]",
|
||||||
|
"minecraft:piston[facing=north,extended=false]", "minecraft:sticky_piston[facing=north,extended=false]",
|
||||||
|
"minecraft:jigsaw[orientation=north_up]");
|
||||||
|
private static final TypeToken<KMap<String, Object>> TILE_PROPERTIES = new TypeToken<>() { };
|
||||||
|
|
||||||
|
@Rule
|
||||||
|
public final PlatformBinding platform = PlatformBinding.mockPlatform();
|
||||||
|
|
||||||
|
private TileData.TileReader previousReader;
|
||||||
|
|
||||||
|
@Before
|
||||||
|
public void bindStatesAndTiles() {
|
||||||
|
when(platform.registries().blockOrNull(anyString(), eq(false)))
|
||||||
|
.thenAnswer(invocation -> state(invocation.getArgument(0)));
|
||||||
|
Gson gson = new Gson();
|
||||||
|
previousReader = TileData.bindPlatformReader(input -> new TileData(input.readUTF(),
|
||||||
|
gson.fromJson(input.readUTF(), TILE_PROPERTIES)));
|
||||||
|
}
|
||||||
|
|
||||||
|
@After
|
||||||
|
public void restoreTiles() {
|
||||||
|
TileData.restorePlatformReader(previousReader);
|
||||||
|
IrisObjectScale.invalidate(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void bothObjectFormatsOmitTransientCellsAndTheirTiles() throws Throwable {
|
||||||
|
for (boolean legacy : new boolean[]{false, true}) {
|
||||||
|
IrisObject object = loadFixture(legacy);
|
||||||
|
|
||||||
|
assertEquals(5, object.getBlocks().size());
|
||||||
|
assertNull(object.getBlocks().get(new IrisBlockVector(0, 0, 0)));
|
||||||
|
assertNull(object.getBlocks().get(new IrisBlockVector(6, 0, 0)));
|
||||||
|
assertEquals("minecraft:oak_log", object.getBlocks().get(new IrisBlockVector(1, 0, 0)).materialKey());
|
||||||
|
assertEquals("minecraft:oak_leaves", object.getBlocks().get(new IrisBlockVector(2, 0, 0)).materialKey());
|
||||||
|
assertEquals("minecraft:piston", object.getBlocks().get(new IrisBlockVector(4, 0, 0)).materialKey());
|
||||||
|
assertEquals("minecraft:sticky_piston", object.getBlocks().get(new IrisBlockVector(5, 0, 0)).materialKey());
|
||||||
|
assertEquals(1, object.getStates().size());
|
||||||
|
assertEquals("chest contents", object.getStates().get(new IrisBlockVector(3, 0, 0))
|
||||||
|
.getProperties().get("payload"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void scalingLoadedObjectsKeepsRealBlockEntitiesWithoutRevivingTransientCells() throws Throwable {
|
||||||
|
IrisObject source = loadFixture(false);
|
||||||
|
IrisObjectPlacement placement = new IrisObjectPlacement();
|
||||||
|
for (double factor : new double[]{0.5D, 1D, 2D}) {
|
||||||
|
IrisObject object = placement.scaleObject(new RNG(1337), source,
|
||||||
|
new IrisDimension().setAllObjectScaleFactor(factor));
|
||||||
|
assertEquals(factor == 2D ? 8 : 1, object.getStates().size());
|
||||||
|
for (Map.Entry<IrisBlockVector, PlatformBlockState> entry : object.getBlocks()) {
|
||||||
|
assertFalse(entry.getValue().materialKey().equals("minecraft:moving_piston"));
|
||||||
|
if (entry.getValue().materialKey().equals("minecraft:chest")) {
|
||||||
|
assertNotNull(object.getStates().get(entry.getKey()));
|
||||||
|
assertEquals("chest contents", object.getStates().get(entry.getKey()).getProperties().get("payload"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
assertEquals(5, source.getBlocks().size());
|
||||||
|
assertEquals(1, source.getStates().size());
|
||||||
|
}
|
||||||
|
|
||||||
|
private static IrisObject loadFixture(boolean legacy) throws Throwable {
|
||||||
|
IrisObject object = new IrisObject();
|
||||||
|
ByteArrayInputStream input = new ByteArrayInputStream(fixture(legacy));
|
||||||
|
if (legacy) {
|
||||||
|
IrisObjectIO.readLegacy(object, input);
|
||||||
|
} else {
|
||||||
|
IrisObjectIO.read(object, input);
|
||||||
|
}
|
||||||
|
return object;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static byte[] fixture(boolean legacy) throws IOException {
|
||||||
|
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
|
||||||
|
try (DataOutputStream output = new DataOutputStream(bytes)) {
|
||||||
|
output.writeInt(16);
|
||||||
|
output.writeInt(4);
|
||||||
|
output.writeInt(4);
|
||||||
|
if (!legacy) {
|
||||||
|
output.writeUTF("Iris V2 IOB;");
|
||||||
|
output.writeShort(PALETTE.size());
|
||||||
|
for (String key : PALETTE) {
|
||||||
|
output.writeUTF(key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
output.writeInt(PALETTE.size());
|
||||||
|
for (int index = 0; index < PALETTE.size(); index++) {
|
||||||
|
writePosition(output, index);
|
||||||
|
if (legacy) {
|
||||||
|
output.writeUTF(PALETTE.get(index));
|
||||||
|
} else {
|
||||||
|
output.writeShort(index);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
output.writeInt(4);
|
||||||
|
writeTile(output, 0, "minecraft:moving_piston", "transient");
|
||||||
|
writeTile(output, 3, "minecraft:chest", "chest contents");
|
||||||
|
writeTile(output, 6, "minecraft:jigsaw", "marker");
|
||||||
|
writeTile(output, 7, "minecraft:chest", "orphan");
|
||||||
|
}
|
||||||
|
return bytes.toByteArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void writeTile(DataOutputStream output, int x, String material, String payload) throws IOException {
|
||||||
|
writePosition(output, x);
|
||||||
|
output.writeUTF(material);
|
||||||
|
output.writeUTF("{\"payload\":\"" + payload + "\"}");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void writePosition(DataOutputStream output, int x) throws IOException {
|
||||||
|
output.writeShort(x);
|
||||||
|
output.writeShort(0);
|
||||||
|
output.writeShort(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static PlatformBlockState state(String key) {
|
||||||
|
String material = key.substring(0, key.indexOf('['));
|
||||||
|
PlatformBlockState state = mock(PlatformBlockState.class);
|
||||||
|
when(state.key()).thenReturn(key);
|
||||||
|
when(state.materialKey()).thenReturn(material);
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
}
|
||||||
+147
-7
@@ -313,6 +313,68 @@ public class BukkitChunkGeneratorGenerationStageGateTest {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void timedExclusiveRetainsPriorityDuringStaggeredGenerationDrain() throws Exception {
|
||||||
|
BukkitChunkGenerator.GenerationStageGate gate =
|
||||||
|
new BukkitChunkGenerator.GenerationStageGate(3, () -> false);
|
||||||
|
BukkitChunkGenerator.GenerationStagePermit first = gate.acquireStage("first-active");
|
||||||
|
BukkitChunkGenerator.GenerationStagePermit last = gate.acquireStage("last-active");
|
||||||
|
CompletableFuture<Void> outward = new CompletableFuture<>();
|
||||||
|
CountDownLatch exclusiveEntered = new CountDownLatch(1);
|
||||||
|
CountDownLatch releaseExclusive = new CountDownLatch(1);
|
||||||
|
CountDownLatch readerEntered = new CountDownLatch(1);
|
||||||
|
List<String> order = new CopyOnWriteArrayList<>();
|
||||||
|
ExecutorService executor = Executors.newFixedThreadPool(3);
|
||||||
|
|
||||||
|
try {
|
||||||
|
Future<?> exclusive = executor.submit(() -> BukkitChunkGenerator.completeExclusiveControlFuture(
|
||||||
|
gate,
|
||||||
|
() -> {
|
||||||
|
order.add("exclusive");
|
||||||
|
exclusiveEntered.countDown();
|
||||||
|
try {
|
||||||
|
assertTrue(releaseExclusive.await(2L, TimeUnit.SECONDS));
|
||||||
|
} catch (InterruptedException failure) {
|
||||||
|
Thread.currentThread().interrupt();
|
||||||
|
throw new IllegalStateException(failure);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
outward,
|
||||||
|
2L,
|
||||||
|
TimeUnit.SECONDS));
|
||||||
|
awaitQueueLength(gate, 1);
|
||||||
|
Runnable reader = () -> {
|
||||||
|
try (BukkitChunkGenerator.GenerationStagePermit ignored = gate.acquireStage("later-reader")) {
|
||||||
|
order.add("reader");
|
||||||
|
readerEntered.countDown();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
Future<?> firstReader = executor.submit(reader);
|
||||||
|
Future<?> secondReader = executor.submit(reader);
|
||||||
|
awaitQueueLength(gate, 3);
|
||||||
|
|
||||||
|
assertFalse(readerEntered.await(150L, TimeUnit.MILLISECONDS));
|
||||||
|
first.close();
|
||||||
|
assertFalse(readerEntered.await(150L, TimeUnit.MILLISECONDS));
|
||||||
|
last.close();
|
||||||
|
assertTrue(exclusiveEntered.await(2L, TimeUnit.SECONDS));
|
||||||
|
assertEquals(1L, readerEntered.getCount());
|
||||||
|
releaseExclusive.countDown();
|
||||||
|
exclusive.get(2L, TimeUnit.SECONDS);
|
||||||
|
outward.get(2L, TimeUnit.SECONDS);
|
||||||
|
firstReader.get(2L, TimeUnit.SECONDS);
|
||||||
|
secondReader.get(2L, TimeUnit.SECONDS);
|
||||||
|
assertEquals(List.of("exclusive", "reader", "reader"), order);
|
||||||
|
assertEquals(3, gate.availablePermits());
|
||||||
|
} finally {
|
||||||
|
first.close();
|
||||||
|
last.close();
|
||||||
|
releaseExclusive.countDown();
|
||||||
|
executor.shutdownNow();
|
||||||
|
assertTrue(executor.awaitTermination(2L, TimeUnit.SECONDS));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void timedExclusiveControlDoesNotReleaseAnUnacquiredPermit() throws Exception {
|
public void timedExclusiveControlDoesNotReleaseAnUnacquiredPermit() throws Exception {
|
||||||
AtomicBoolean closing = new AtomicBoolean(false);
|
AtomicBoolean closing = new AtomicBoolean(false);
|
||||||
@@ -355,16 +417,19 @@ public class BukkitChunkGeneratorGenerationStageGateTest {
|
|||||||
BukkitChunkGenerator.GenerationStagePermit stage = gate.acquireStage("cancellation-holder");
|
BukkitChunkGenerator.GenerationStagePermit stage = gate.acquireStage("cancellation-holder");
|
||||||
CompletableFuture<Void> outward = new CompletableFuture<>();
|
CompletableFuture<Void> outward = new CompletableFuture<>();
|
||||||
AtomicBoolean operationRan = new AtomicBoolean(false);
|
AtomicBoolean operationRan = new AtomicBoolean(false);
|
||||||
|
AtomicBoolean interruptedAfterCancellation = new AtomicBoolean(true);
|
||||||
ExecutorService executor = Executors.newSingleThreadExecutor();
|
ExecutorService executor = Executors.newSingleThreadExecutor();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
Future<?> operation = executor.submit(() ->
|
Future<?> operation = executor.submit(() -> {
|
||||||
BukkitChunkGenerator.completeExclusiveControlFuture(
|
BukkitChunkGenerator.completeExclusiveControlFuture(
|
||||||
gate,
|
gate,
|
||||||
() -> operationRan.set(true),
|
() -> operationRan.set(true),
|
||||||
outward,
|
outward,
|
||||||
30L,
|
30L,
|
||||||
TimeUnit.SECONDS));
|
TimeUnit.SECONDS);
|
||||||
|
interruptedAfterCancellation.set(Thread.currentThread().isInterrupted());
|
||||||
|
});
|
||||||
awaitQueueLength(gate, 1);
|
awaitQueueLength(gate, 1);
|
||||||
|
|
||||||
assertTrue(outward.cancel(true));
|
assertTrue(outward.cancel(true));
|
||||||
@@ -372,6 +437,7 @@ public class BukkitChunkGeneratorGenerationStageGateTest {
|
|||||||
|
|
||||||
assertTrue(outward.isCancelled());
|
assertTrue(outward.isCancelled());
|
||||||
assertFalse(operationRan.get());
|
assertFalse(operationRan.get());
|
||||||
|
assertFalse(interruptedAfterCancellation.get());
|
||||||
assertEquals(0, gate.availablePermits());
|
assertEquals(0, gate.availablePermits());
|
||||||
} finally {
|
} finally {
|
||||||
stage.close();
|
stage.close();
|
||||||
@@ -380,6 +446,80 @@ public class BukkitChunkGeneratorGenerationStageGateTest {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void externalInterruptStillRestoresTheAcquisitionWorkerFlag() throws Exception {
|
||||||
|
BukkitChunkGenerator.GenerationStageGate gate =
|
||||||
|
new BukkitChunkGenerator.GenerationStageGate(1, () -> false);
|
||||||
|
BukkitChunkGenerator.GenerationStagePermit stage = gate.acquireStage("interrupt-holder");
|
||||||
|
CompletableFuture<Void> outward = new CompletableFuture<>();
|
||||||
|
AtomicReference<Thread> worker = new AtomicReference<>();
|
||||||
|
AtomicBoolean interruptedAfterAcquisition = new AtomicBoolean(false);
|
||||||
|
AtomicBoolean operationRan = new AtomicBoolean(false);
|
||||||
|
ExecutorService executor = Executors.newSingleThreadExecutor();
|
||||||
|
|
||||||
|
try {
|
||||||
|
Future<?> operation = executor.submit(() -> {
|
||||||
|
worker.set(Thread.currentThread());
|
||||||
|
BukkitChunkGenerator.completeExclusiveControlFuture(gate, () -> operationRan.set(true),
|
||||||
|
outward, 30L, TimeUnit.SECONDS);
|
||||||
|
interruptedAfterAcquisition.set(Thread.currentThread().isInterrupted());
|
||||||
|
});
|
||||||
|
awaitQueueLength(gate, 1);
|
||||||
|
worker.get().interrupt();
|
||||||
|
operation.get(2L, TimeUnit.SECONDS);
|
||||||
|
|
||||||
|
ExecutionException failure = assertThrows(ExecutionException.class,
|
||||||
|
() -> outward.get(2L, TimeUnit.SECONDS));
|
||||||
|
assertTrue(failure.getCause() instanceof InterruptedException);
|
||||||
|
assertTrue(interruptedAfterAcquisition.get());
|
||||||
|
assertFalse(operationRan.get());
|
||||||
|
assertEquals(0, gate.availablePermits());
|
||||||
|
} finally {
|
||||||
|
stage.close();
|
||||||
|
executor.shutdownNow();
|
||||||
|
assertTrue(executor.awaitTermination(2L, TimeUnit.SECONDS));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void cancellationAfterAcquisitionDoesNotInterruptTheRunningOperation() throws Exception {
|
||||||
|
BukkitChunkGenerator.GenerationStageGate gate =
|
||||||
|
new BukkitChunkGenerator.GenerationStageGate(1, () -> false);
|
||||||
|
CompletableFuture<Void> outward = new CompletableFuture<>();
|
||||||
|
CountDownLatch entered = new CountDownLatch(1);
|
||||||
|
CountDownLatch release = new CountDownLatch(1);
|
||||||
|
AtomicBoolean interrupted = new AtomicBoolean(false);
|
||||||
|
ExecutorService executor = Executors.newSingleThreadExecutor();
|
||||||
|
|
||||||
|
try {
|
||||||
|
Future<?> operation = executor.submit(() -> {
|
||||||
|
BukkitChunkGenerator.completeExclusiveControlFuture(gate, () -> {
|
||||||
|
entered.countDown();
|
||||||
|
try {
|
||||||
|
assertTrue(release.await(2L, TimeUnit.SECONDS));
|
||||||
|
} catch (InterruptedException failure) {
|
||||||
|
interrupted.set(true);
|
||||||
|
Thread.currentThread().interrupt();
|
||||||
|
}
|
||||||
|
}, outward, 30L, TimeUnit.SECONDS);
|
||||||
|
interrupted.compareAndSet(false, Thread.currentThread().isInterrupted());
|
||||||
|
});
|
||||||
|
assertTrue(entered.await(2L, TimeUnit.SECONDS));
|
||||||
|
assertTrue(outward.cancel(true));
|
||||||
|
assertEquals(0, gate.availablePermits());
|
||||||
|
release.countDown();
|
||||||
|
operation.get(2L, TimeUnit.SECONDS);
|
||||||
|
|
||||||
|
assertFalse(interrupted.get());
|
||||||
|
assertTrue(outward.isCancelled());
|
||||||
|
assertEquals(1, gate.availablePermits());
|
||||||
|
} finally {
|
||||||
|
release.countDown();
|
||||||
|
executor.shutdownNow();
|
||||||
|
assertTrue(executor.awaitTermination(2L, TimeUnit.SECONDS));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void queuedStageRemainsAdmittedWhileShutdownIsOnlyQuiesced() throws Exception {
|
public void queuedStageRemainsAdmittedWhileShutdownIsOnlyQuiesced() throws Exception {
|
||||||
AtomicBoolean closing = new AtomicBoolean(false);
|
AtomicBoolean closing = new AtomicBoolean(false);
|
||||||
|
|||||||
+159
@@ -0,0 +1,159 @@
|
|||||||
|
package art.arcane.iris.platform.bukkit;
|
||||||
|
|
||||||
|
import art.arcane.iris.core.link.Identifier;
|
||||||
|
import art.arcane.iris.core.service.ExternalDataSVC;
|
||||||
|
import art.arcane.iris.engine.object.IrisObjectRotation;
|
||||||
|
import art.arcane.iris.spi.PlatformBlockState;
|
||||||
|
import art.arcane.iris.testsupport.BukkitTestServer;
|
||||||
|
import art.arcane.iris.util.common.data.IrisCustomData;
|
||||||
|
import art.arcane.iris.util.common.data.registry.RegistryUtil;
|
||||||
|
import org.bukkit.Material;
|
||||||
|
import org.bukkit.block.data.BlockData;
|
||||||
|
import org.junit.BeforeClass;
|
||||||
|
import org.junit.Test;
|
||||||
|
import org.mockito.MockedStatic;
|
||||||
|
|
||||||
|
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.ArgumentMatchers.anyString;
|
||||||
|
import static org.mockito.Mockito.mockStatic;
|
||||||
|
|
||||||
|
public class BukkitCustomBlockTransformTest {
|
||||||
|
@BeforeClass
|
||||||
|
public static void installServer() {
|
||||||
|
BukkitTestServer.install();
|
||||||
|
try (MockedStatic<RegistryUtil> registry = mockStatic(RegistryUtil.class)) {
|
||||||
|
registry.when(() -> RegistryUtil.find(Material.class, "grass", "short_grass")).thenReturn(Material.SHORT_GRASS);
|
||||||
|
BukkitBlockResolution.getAir();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void waterloggingUpdatesSemanticIdentityAndResolvedCarrier() {
|
||||||
|
IrisCustomData original = custom("craftengine:forest/amber_slab[axis=x,waterlogged=false]", 1);
|
||||||
|
IrisCustomData expected = custom("craftengine:forest/amber_slab[axis=x,waterlogged=true]", 2);
|
||||||
|
PlatformBlockState state = BukkitBlockState.of(original);
|
||||||
|
|
||||||
|
try (MockedStatic<BukkitBlockResolution> resolution = mockStatic(BukkitBlockResolution.class)) {
|
||||||
|
resolveTo(resolution, expected);
|
||||||
|
PlatformBlockState result = state.withProperty("waterlogged", "true");
|
||||||
|
|
||||||
|
assertSame(expected, result.nativeHandle());
|
||||||
|
assertEquals(expected.getCustom().toString(), result.deferredPlacementKey());
|
||||||
|
assertEquals(expected.getBase().getAsString(), result.placementBaseState().key());
|
||||||
|
assertEquals("craftengine:forest/amber_slab[axis=x,waterlogged=false]", state.key());
|
||||||
|
resolution.verify(() -> BukkitBlockResolution.resolveOrNull(anyString()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void customPropertyRejectsAnUnresolvableValue() {
|
||||||
|
PlatformBlockState state = BukkitBlockState.of(custom("craftengine:forest/amber_log[axis=x]", 1));
|
||||||
|
|
||||||
|
try (MockedStatic<BukkitBlockResolution> resolution = mockStatic(BukkitBlockResolution.class)) {
|
||||||
|
IllegalArgumentException error = assertThrows(IllegalArgumentException.class,
|
||||||
|
() -> state.withProperty("axis", "invalid"));
|
||||||
|
|
||||||
|
assertTrue(error.getMessage().contains("axis=invalid"));
|
||||||
|
assertEquals("craftengine:forest/amber_log[axis=x]", state.key());
|
||||||
|
resolution.verify(() -> BukkitBlockResolution.resolveOrNull("craftengine:forest/amber_log[axis=invalid]"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void semanticFacingRotatesWithoutDirectionalCarrier() {
|
||||||
|
IrisCustomData original = custom("craftengine:forest/amber_lamp[facing=north,waterlogged=true]", 1);
|
||||||
|
IrisCustomData expected = custom("craftengine:forest/amber_lamp[facing=west,waterlogged=true]", 2);
|
||||||
|
PlatformBlockState state = BukkitBlockState.of(original);
|
||||||
|
|
||||||
|
try (MockedStatic<BukkitBlockResolution> resolution = mockStatic(BukkitBlockResolution.class)) {
|
||||||
|
resolveTo(resolution, expected);
|
||||||
|
PlatformBlockState result = IrisObjectRotation.of(0, 90, 0).rotate(state, 0, 0, 0);
|
||||||
|
|
||||||
|
assertSame(expected, result.nativeHandle());
|
||||||
|
assertEquals(expected.getCustom().toString(), result.key());
|
||||||
|
assertEquals("craftengine:forest/amber_lamp[facing=north,waterlogged=true]", state.key());
|
||||||
|
resolution.verify(() -> BukkitBlockResolution.resolveOrNull(anyString()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void semanticAxisRotatesThroughRawBlockDataEntryPoint() {
|
||||||
|
IrisCustomData original = custom("craftengine:forest/amber_log[axis=x,waterlogged=false]", 1);
|
||||||
|
IrisCustomData expected = custom("craftengine:forest/amber_log[axis=z,waterlogged=false]", 2);
|
||||||
|
|
||||||
|
try (MockedStatic<BukkitBlockResolution> resolution = mockStatic(BukkitBlockResolution.class)) {
|
||||||
|
resolveTo(resolution, expected);
|
||||||
|
BlockData result = IrisObjectRotation.of(0, 90, 0).rotate(original, 0, 0, 0);
|
||||||
|
|
||||||
|
assertSame(expected, result);
|
||||||
|
assertEquals("craftengine:forest/amber_log[axis=x,waterlogged=false]", original.getCustom().toString());
|
||||||
|
resolution.verify(() -> BukkitBlockResolution.resolveOrNull(anyString()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void semanticRotationPreservesSixteenWayOrientation() {
|
||||||
|
PlatformBlockState state = BukkitBlockState.of(custom("craftengine:forest/amber_sign[rotation=0]", 1));
|
||||||
|
IrisCustomData expected = custom("craftengine:forest/amber_sign[rotation=12]", 2);
|
||||||
|
|
||||||
|
try (MockedStatic<BukkitBlockResolution> resolution = mockStatic(BukkitBlockResolution.class)) {
|
||||||
|
resolveTo(resolution, expected);
|
||||||
|
PlatformBlockState result = IrisObjectRotation.of(0, 90, 0).rotate(state, 0, 0, 0);
|
||||||
|
|
||||||
|
assertSame(expected, result.nativeHandle());
|
||||||
|
resolution.verify(() -> BukkitBlockResolution.resolveOrNull(anyString()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void semanticConnectionsRotateTogether() {
|
||||||
|
PlatformBlockState state = BukkitBlockState.of(custom(
|
||||||
|
"craftengine:forest/amber_fence[north=true,east=false,south=false,west=false]", 1));
|
||||||
|
IrisCustomData expected = custom(
|
||||||
|
"craftengine:forest/amber_fence[north=false,east=false,south=false,west=true]", 2);
|
||||||
|
|
||||||
|
try (MockedStatic<BukkitBlockResolution> resolution = mockStatic(BukkitBlockResolution.class)) {
|
||||||
|
resolveTo(resolution, expected);
|
||||||
|
PlatformBlockState result = IrisObjectRotation.of(0, 90, 0).rotate(state, 0, 0, 0);
|
||||||
|
|
||||||
|
assertSame(expected, result.nativeHandle());
|
||||||
|
resolution.verify(() -> BukkitBlockResolution.resolveOrNull(anyString()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void unsupportedRotationRetainsTheOriginalCustomBlock() {
|
||||||
|
PlatformBlockState state = BukkitBlockState.of(custom("craftengine:forest/amber_lamp[facing=north]", 1));
|
||||||
|
|
||||||
|
try (MockedStatic<BukkitBlockResolution> resolution = mockStatic(BukkitBlockResolution.class)) {
|
||||||
|
assertSame(state, IrisObjectRotation.of(90, 0, 0).rotate(state, 0, 0, 0));
|
||||||
|
resolution.verify(() -> BukkitBlockResolution.resolveOrNull(anyString()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void unchangedSemanticAxisDoesNotResolveOrMutateCarrier() {
|
||||||
|
PlatformBlockState state = BukkitBlockState.of(custom("craftengine:forest/amber_log[axis=y]", 1));
|
||||||
|
|
||||||
|
try (MockedStatic<BukkitBlockResolution> resolution = mockStatic(BukkitBlockResolution.class)) {
|
||||||
|
assertSame(state, IrisObjectRotation.of(0, 90, 0).rotate(state, 0, 0, 0));
|
||||||
|
resolution.verifyNoInteractions();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static IrisCustomData custom(String key, int note) {
|
||||||
|
BlockData carrier = BukkitTestServer.blockData("minecraft:note_block[instrument=harp,note=" + note + ",powered=false]");
|
||||||
|
return IrisCustomData.of(carrier, Identifier.fromString(key));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void resolveTo(MockedStatic<BukkitBlockResolution> resolution, IrisCustomData expected) {
|
||||||
|
resolution.when(() -> BukkitBlockResolution.resolveOrNull(anyString())).thenAnswer(invocation -> {
|
||||||
|
Identifier actual = Identifier.fromString(invocation.getArgument(0, String.class));
|
||||||
|
assertEquals(ExternalDataSVC.parseState(expected.getCustom()), ExternalDataSVC.parseState(actual));
|
||||||
|
return expected;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -46,7 +46,7 @@ mythic = "5.12.1"
|
|||||||
mythic-crucible = "2.2.0"
|
mythic-crucible = "2.2.0"
|
||||||
kgenerators = "7.3" # https://repo.codemc.io/repository/maven-public/me/kryniowesegryderiusz/kgenerators-core/maven-metadata.xml
|
kgenerators = "7.3" # https://repo.codemc.io/repository/maven-public/me/kryniowesegryderiusz/kgenerators-core/maven-metadata.xml
|
||||||
multiverseCore = "5.8.0"
|
multiverseCore = "5.8.0"
|
||||||
craftengine = "26.7.3" # https://github.com/Xiao-MoMi/craft-engine/releases
|
craftengine = "26.8.2" # https://github.com/Xiao-MoMi/craft-engine/releases
|
||||||
|
|
||||||
# Fabric API (net.fabricmc.fabric-api) - each module is independently versioned
|
# Fabric API (net.fabricmc.fabric-api) - each module is independently versioned
|
||||||
fabricApi-base = "2.0.4+ece063239e"
|
fabricApi-base = "2.0.4+ece063239e"
|
||||||
|
|||||||
Reference in new issue
Block a user