mirror of
https://github.com/VolmitSoftware/Iris.git
synced 2026-10-12 03:24:14 +00:00
Studio fixes.
- Fixes the Object and Jigsaw studio errors from the coretapper i made.
This commit is contained in:
@@ -159,6 +159,17 @@ public final class ObjectStudioLayout {
|
||||
c.getInt("h"),
|
||||
c.getInt("d")
|
||||
);
|
||||
IrisData source = sources.get(pack);
|
||||
File objectFile = source == null ? null : source.getObjectLoader().findFile(cell.key());
|
||||
if (objectFile == null) {
|
||||
return null;
|
||||
}
|
||||
IrisBlockVector size = IrisObject.sampleSize(objectFile);
|
||||
if (cell.w() != Math.max(1, size.getBlockX())
|
||||
|| cell.h() != Math.max(1, size.getBlockY())
|
||||
|| cell.d() != Math.max(1, size.getBlockZ())) {
|
||||
return null;
|
||||
}
|
||||
stored.add(cell);
|
||||
storedIds.add(cell.pack() + "/" + cell.key());
|
||||
}
|
||||
@@ -217,6 +228,26 @@ public final class ObjectStudioLayout {
|
||||
return padding;
|
||||
}
|
||||
|
||||
public ObjectStudioLayout atFloor(int floorY) {
|
||||
int originY = Math.addExact(floorY, 1);
|
||||
boolean unchanged = true;
|
||||
for (GridCell cell : cells) {
|
||||
if (cell.originY() != originY) {
|
||||
unchanged = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (unchanged) {
|
||||
return this;
|
||||
}
|
||||
List<GridCell> placed = new ArrayList<>(cells.size());
|
||||
for (GridCell cell : cells) {
|
||||
placed.add(new GridCell(cell.pack(), cell.key(), cell.originX(), originY, cell.originZ(),
|
||||
cell.w(), cell.h(), cell.d()));
|
||||
}
|
||||
return new ObjectStudioLayout(padding, rowWidthCap, placed);
|
||||
}
|
||||
|
||||
public List<GridCell> cells() {
|
||||
return cells;
|
||||
}
|
||||
|
||||
@@ -3815,16 +3815,15 @@ public final class JigsawStudioService implements IrisService, JigsawStudioMenuC
|
||||
message(player, "Iris Jigsaw Studio is not active in this world.");
|
||||
return false;
|
||||
}
|
||||
List<JigsawStudioBounds> bounds = new ArrayList<>(pieces.size());
|
||||
List<JigsawStudioPreviewRenderer.PreviewBounds> bounds = new ArrayList<>(pieces.size());
|
||||
for (PlacedStructurePiece piece : pieces) {
|
||||
int width = Math.addExact(Math.subtractExact(piece.getMaxX(), piece.getMinX()), 1);
|
||||
int height = Math.addExact(Math.subtractExact(piece.getMaxY(), piece.getMinY()), 1);
|
||||
int depth = Math.addExact(Math.subtractExact(piece.getMaxZ(), piece.getMinZ()), 1);
|
||||
bounds.add(new JigsawStudioBounds(
|
||||
bounds.add(new JigsawStudioPreviewRenderer.PreviewBounds(
|
||||
piece.getMinX(),
|
||||
piece.getMinY(),
|
||||
piece.getMinZ(),
|
||||
new JigsawStudioCellDimensions(width, height, depth)));
|
||||
piece.getMaxX(),
|
||||
piece.getMaxY(),
|
||||
piece.getMaxZ()));
|
||||
}
|
||||
assemblyPreviews.put(player.getUniqueId(), new AssemblyPreview(
|
||||
player.getWorld().getUID(),
|
||||
@@ -7552,10 +7551,6 @@ public final class JigsawStudioService implements IrisService, JigsawStudioMenuC
|
||||
return;
|
||||
}
|
||||
JigsawStudioPreviewRenderer.PreviewBounds preview = evaluation.previewBounds();
|
||||
JigsawStudioCellDimensions dimensions = new JigsawStudioCellDimensions(
|
||||
preview.maximumX() - preview.minimumX() + 1,
|
||||
preview.maximumY() - preview.minimumY() + 1,
|
||||
preview.maximumZ() - preview.minimumZ() + 1);
|
||||
Color color = switch (evaluation.state()) {
|
||||
case VALID -> ASSEMBLY_PREVIEW_COLOR;
|
||||
case PENDING, WARNING, STALE -> LIVE_PREVIEW_WARNING_COLOR;
|
||||
@@ -7564,11 +7559,7 @@ public final class JigsawStudioService implements IrisService, JigsawStudioMenuC
|
||||
drawBounds(
|
||||
player,
|
||||
playerLocation,
|
||||
new JigsawStudioBounds(
|
||||
preview.minimumX(),
|
||||
preview.minimumY(),
|
||||
preview.minimumZ(),
|
||||
dimensions),
|
||||
preview,
|
||||
color,
|
||||
0.85F,
|
||||
2.0D,
|
||||
@@ -7589,7 +7580,7 @@ public final class JigsawStudioService implements IrisService, JigsawStudioMenuC
|
||||
assemblyPreviews.remove(player.getUniqueId(), preview);
|
||||
return;
|
||||
}
|
||||
for (JigsawStudioBounds bounds : preview.bounds()) {
|
||||
for (JigsawStudioPreviewRenderer.PreviewBounds bounds : preview.bounds()) {
|
||||
drawBounds(player, playerLocation, bounds, ASSEMBLY_PREVIEW_COLOR, 0.85F, 2.0D, budget);
|
||||
if (budget.empty()) {
|
||||
return;
|
||||
@@ -7682,12 +7673,26 @@ public final class JigsawStudioService implements IrisService, JigsawStudioMenuC
|
||||
double step,
|
||||
ParticleBudget budget
|
||||
) {
|
||||
double minX = bounds.originX();
|
||||
double minY = bounds.originY();
|
||||
double minZ = bounds.originZ();
|
||||
double maxX = bounds.maxX() + 1.0D;
|
||||
double maxY = bounds.maxY() + 1.0D;
|
||||
double maxZ = bounds.maxZ() + 1.0D;
|
||||
drawBounds(player, playerLocation, new JigsawStudioPreviewRenderer.PreviewBounds(
|
||||
bounds.originX(), bounds.originY(), bounds.originZ(), bounds.maxX(), bounds.maxY(), bounds.maxZ()),
|
||||
color, size, step, budget);
|
||||
}
|
||||
|
||||
private static void drawBounds(
|
||||
Player player,
|
||||
Location playerLocation,
|
||||
JigsawStudioPreviewRenderer.PreviewBounds bounds,
|
||||
Color color,
|
||||
float size,
|
||||
double step,
|
||||
ParticleBudget budget
|
||||
) {
|
||||
double minX = bounds.minimumX();
|
||||
double minY = bounds.minimumY();
|
||||
double minZ = bounds.minimumZ();
|
||||
double maxX = bounds.maximumX() + 1.0D;
|
||||
double maxY = bounds.maximumY() + 1.0D;
|
||||
double maxZ = bounds.maximumZ() + 1.0D;
|
||||
drawLine(player, playerLocation, minX, minY, minZ, maxX, minY, minZ, color, size, step, budget);
|
||||
drawLine(player, playerLocation, minX, minY, maxZ, maxX, minY, maxZ, color, size, step, budget);
|
||||
drawLine(player, playerLocation, minX, maxY, minZ, maxX, maxY, minZ, color, size, step, budget);
|
||||
@@ -8657,7 +8662,7 @@ public final class JigsawStudioService implements IrisService, JigsawStudioMenuC
|
||||
private record AssemblyPreview(
|
||||
UUID worldId,
|
||||
long expiresAtMillis,
|
||||
List<JigsawStudioBounds> bounds
|
||||
List<JigsawStudioPreviewRenderer.PreviewBounds> bounds
|
||||
) {
|
||||
AssemblyPreview {
|
||||
Objects.requireNonNull(worldId, "Jigsaw Studio preview world");
|
||||
|
||||
@@ -107,7 +107,7 @@ public class ObjectStudioSaveService implements IrisService {
|
||||
}
|
||||
|
||||
String packKey = engine.getDimension() == null ? null : engine.getDimension().getLoadKey();
|
||||
studios.put(world.getUID(), new ActiveStudio(world.getUID(), layout, objectsDirs, packKey));
|
||||
studios.put(world.getUID(), new ActiveStudio(world.getUID(), generator, objectsDirs, packKey));
|
||||
IrisLogging.debug("Object Studio live-save registered: world=%s cells=%d packs=%d",
|
||||
world.getName(), layout.cells().size(), objectsDirs.size());
|
||||
}
|
||||
@@ -219,7 +219,7 @@ public class ObjectStudioSaveService implements IrisService {
|
||||
return;
|
||||
}
|
||||
|
||||
IrisObject snapshot = new IrisObject(cell.w(), cell.h(), cell.d());
|
||||
IrisObject snapshot = studio.generator.createCapture(cell);
|
||||
int originX = cell.originX();
|
||||
int originY = cell.originY();
|
||||
int originZ = cell.originZ();
|
||||
@@ -324,14 +324,16 @@ public class ObjectStudioSaveService implements IrisService {
|
||||
|
||||
private static final class ActiveStudio {
|
||||
final UUID worldId;
|
||||
final ObjectStudioGenerator generator;
|
||||
final ObjectStudioLayout layout;
|
||||
final Map<String, File> objectsDirs;
|
||||
final String packKey;
|
||||
final Map<String, Long> hashes = new ConcurrentHashMap<>();
|
||||
|
||||
ActiveStudio(UUID worldId, ObjectStudioLayout layout, Map<String, File> objectsDirs, String packKey) {
|
||||
ActiveStudio(UUID worldId, ObjectStudioGenerator generator, Map<String, File> objectsDirs, String packKey) {
|
||||
this.worldId = worldId;
|
||||
this.layout = layout;
|
||||
this.generator = generator;
|
||||
this.layout = generator.getLayout();
|
||||
this.objectsDirs = objectsDirs;
|
||||
this.packKey = packKey;
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@ package art.arcane.iris.engine;
|
||||
|
||||
import art.arcane.iris.core.IrisSettings;
|
||||
import art.arcane.iris.engine.data.cache.Cache;
|
||||
import art.arcane.iris.engine.history.SavedBiomeUnavailableException;
|
||||
import art.arcane.iris.engine.platform.EngineBukkitOps;
|
||||
import art.arcane.iris.platform.bukkit.BukkitWorldBinding;
|
||||
import art.arcane.iris.spi.IrisLogging;
|
||||
@@ -262,7 +263,7 @@ final class WorldChunkMaintenance {
|
||||
}
|
||||
}
|
||||
|
||||
private void updateChunkRegion(World world, int chunkX, int chunkZ) {
|
||||
void updateChunkRegion(World world, int chunkX, int chunkZ) {
|
||||
if (world == null || !world.isChunkLoaded(chunkX, chunkZ) || !Chunks.isSafe(world, chunkX, chunkZ)) {
|
||||
return;
|
||||
}
|
||||
@@ -274,7 +275,14 @@ final class WorldChunkMaintenance {
|
||||
warmupMantleChunkAsync(chunkX, chunkZ);
|
||||
return;
|
||||
}
|
||||
EngineBukkitOps.updateChunk(manager.getEngine(), chunk);
|
||||
try {
|
||||
EngineBukkitOps.updateChunk(manager.getEngine(), chunk);
|
||||
} catch (SavedBiomeUnavailableException unavailable) {
|
||||
if (!unavailable.isLoading() || unavailable.getSuppressed().length != 0) {
|
||||
throw unavailable;
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (manager.entitySpawner.isEntitySpawningEnabledForCurrentWorld()) {
|
||||
|
||||
@@ -54,6 +54,7 @@ import java.util.concurrent.TimeoutException;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.function.Predicate;
|
||||
import java.util.function.Supplier;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
@@ -93,22 +94,22 @@ final class WorldEntitySpawner {
|
||||
}
|
||||
|
||||
if (manager.cl.flip()) {
|
||||
CompletableFuture<Integer> future = new CompletableFuture<>();
|
||||
try {
|
||||
World realWorld = BukkitWorldBinding.world(manager.getEngine().getWorld());
|
||||
if (realWorld == null) {
|
||||
manager.entityCount = 0;
|
||||
manager.entityCountValid = false;
|
||||
} else {
|
||||
CompletableFuture<Integer> future = new CompletableFuture<>();
|
||||
boolean scheduled = J.runGlobal(() -> {
|
||||
try {
|
||||
future.complete(livingEntityCount(realWorld));
|
||||
} catch (Throwable ex) {
|
||||
future.completeExceptionally(ex);
|
||||
}
|
||||
});
|
||||
boolean scheduled = J.runGlobal(managedCompletion(
|
||||
"bukkit_world_manager_entity_count", future, () -> livingEntityCount(realWorld)));
|
||||
if (scheduled) {
|
||||
manager.entityCount = future.get(2, TimeUnit.SECONDS);
|
||||
Integer count = future.get(2, TimeUnit.SECONDS);
|
||||
if (count == null || manager.getEngine().isClosing() || manager.getEngine().isClosed()) {
|
||||
manager.entityCountValid = false;
|
||||
return false;
|
||||
}
|
||||
manager.entityCount = count;
|
||||
manager.entityCountValid = true;
|
||||
resetEntityCountFailures();
|
||||
} else {
|
||||
@@ -126,6 +127,8 @@ final class WorldEntitySpawner {
|
||||
reportEntityCountFailure("Failed to count entities; pausing Iris entity spawning until a complete count is available.", cause);
|
||||
} catch (Throwable e) {
|
||||
reportEntityCountFailure("Failed to count entities; pausing Iris entity spawning until a complete count is available.", e);
|
||||
} finally {
|
||||
future.complete(null);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -193,7 +196,7 @@ final class WorldEntitySpawner {
|
||||
return false;
|
||||
}
|
||||
|
||||
CompletableFuture<Void> future = new CompletableFuture<>();
|
||||
CompletableFuture<Boolean> future = new CompletableFuture<>();
|
||||
AtomicBoolean failureReported = new AtomicBoolean();
|
||||
future.whenComplete((ignored, failure) -> {
|
||||
if (failure != null) {
|
||||
@@ -202,32 +205,28 @@ final class WorldEntitySpawner {
|
||||
});
|
||||
boolean scheduled;
|
||||
try {
|
||||
scheduled = J.runRegion(world, chunkX, chunkZ, () -> {
|
||||
try {
|
||||
if (!world.isChunkLoaded(chunkX, chunkZ) || !Chunks.isSafe(world, chunkX, chunkZ)) {
|
||||
future.complete(null);
|
||||
return;
|
||||
}
|
||||
|
||||
spawnIn(world.getChunkAt(chunkX, chunkZ), initial);
|
||||
future.complete(null);
|
||||
} catch (Throwable e) {
|
||||
future.completeExceptionally(e);
|
||||
}
|
||||
});
|
||||
scheduled = J.runRegion(world, chunkX, chunkZ, managedCompletion(
|
||||
"bukkit_world_manager_entity_spawn", future, () -> {
|
||||
if (!world.isChunkLoaded(chunkX, chunkZ) || !Chunks.isSafe(world, chunkX, chunkZ)) {
|
||||
return true;
|
||||
}
|
||||
spawnIn(world.getChunkAt(chunkX, chunkZ), initial);
|
||||
return true;
|
||||
}));
|
||||
} catch (Throwable e) {
|
||||
future.complete(null);
|
||||
IrisLogging.reportError("Failed to schedule an Iris entity spawn for chunk " + chunkX + "," + chunkZ + ".", e);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!scheduled) {
|
||||
future.complete(null);
|
||||
IrisLogging.debug("Skipped Iris entity spawning because the region task was not accepted for chunk " + chunkX + "," + chunkZ + ".");
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
future.get(5, TimeUnit.SECONDS);
|
||||
return true;
|
||||
return Boolean.TRUE.equals(future.get(5, TimeUnit.SECONDS));
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
return false;
|
||||
@@ -238,9 +237,28 @@ final class WorldEntitySpawner {
|
||||
Throwable cause = e.getCause() == null ? e : e.getCause();
|
||||
reportSpawnFailure(chunkX, chunkZ, cause, failureReported);
|
||||
return false;
|
||||
} finally {
|
||||
future.complete(null);
|
||||
}
|
||||
}
|
||||
|
||||
private <T> Runnable managedCompletion(String operation, CompletableFuture<T> future, Supplier<T> task) {
|
||||
Runnable managed = manager.managedTask(operation, () -> {
|
||||
if (!future.isDone()) {
|
||||
future.complete(task.get());
|
||||
}
|
||||
}, () -> future.complete(null));
|
||||
return () -> {
|
||||
try {
|
||||
managed.run();
|
||||
} catch (Throwable failure) {
|
||||
if (!future.completeExceptionally(failure)) {
|
||||
IrisLogging.reportError("Failed to finish " + operation + ".", failure);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private void reportEntityCountFailure(String message, Throwable error) {
|
||||
manager.entityCountValid = false;
|
||||
if (error != null) {
|
||||
|
||||
@@ -629,6 +629,7 @@ public class BukkitChunkGenerator extends ChunkGenerator implements PlatformChun
|
||||
lock.lock();
|
||||
|
||||
try {
|
||||
throwIfInitializationFailed();
|
||||
if (setup.get()) {
|
||||
return getEngine();
|
||||
}
|
||||
@@ -636,6 +637,19 @@ public class BukkitChunkGenerator extends ChunkGenerator implements PlatformChun
|
||||
|
||||
getWorld().setRawWorldSeed(world.getSeed());
|
||||
setupEngine();
|
||||
try {
|
||||
computeStudioGenerator();
|
||||
} catch (Throwable failure) {
|
||||
initializationFailure = failure;
|
||||
try {
|
||||
engine.close();
|
||||
} catch (Throwable closeFailure) {
|
||||
if (failure != closeFailure) {
|
||||
failure.addSuppressed(closeFailure);
|
||||
}
|
||||
}
|
||||
throw propagateEngineSetupFailure(failure);
|
||||
}
|
||||
setup.set(true);
|
||||
this.hotloader = shouldRunStudioHotload(studio, closing, jigsawStudioActive) ? new Looper() {
|
||||
@Override
|
||||
@@ -935,11 +949,11 @@ public class BukkitChunkGenerator extends ChunkGenerator implements PlatformChun
|
||||
if (engine != expectedEngine) {
|
||||
throw new IllegalStateException("Iris noise generation belongs to a replaced engine runtime.");
|
||||
}
|
||||
computeStudioGenerator();
|
||||
StudioGenerator selected = computeStudioGenerator();
|
||||
if (engine != expectedEngine) {
|
||||
throw new IllegalStateException("Iris noise generation changed engine runtime during Studio resolution.");
|
||||
}
|
||||
return studioGenerator;
|
||||
return selected;
|
||||
}
|
||||
|
||||
public void withExclusiveControl(Runnable r) {
|
||||
@@ -1077,10 +1091,10 @@ public class BukkitChunkGenerator extends ChunkGenerator implements PlatformChun
|
||||
|
||||
try {
|
||||
Engine engine = getEngine(world);
|
||||
computeStudioGenerator();
|
||||
StudioGenerator selected = computeStudioGenerator();
|
||||
TerrainChunk tc = TerrainChunk.create(d);
|
||||
if (studioGenerator != null) {
|
||||
studioGenerator.generateChunk(engine, tc, x, z);
|
||||
if (selected != null) {
|
||||
selected.generateChunk(engine, tc, x, z);
|
||||
} else {
|
||||
ChunkDataHunkHolder blocks = new ChunkDataHunkHolder(d);
|
||||
Hunk<PlatformBiome> biomes = Hunk.viewBiomes(tc);
|
||||
@@ -1191,7 +1205,14 @@ public class BukkitChunkGenerator extends ChunkGenerator implements PlatformChun
|
||||
}
|
||||
}
|
||||
|
||||
private void computeStudioGenerator() {
|
||||
private StudioGenerator computeStudioGenerator() {
|
||||
if (!studio) {
|
||||
return studioGenerator;
|
||||
}
|
||||
return computeStudioGeneratorForStudio();
|
||||
}
|
||||
|
||||
private synchronized StudioGenerator computeStudioGeneratorForStudio() {
|
||||
String packKey = getEngine().getDimension().getLoadKey();
|
||||
JigsawStudioActivation.Request jigsawRequest = studio
|
||||
? JigsawStudioActivation.getGeneratorRequest(packKey)
|
||||
@@ -1204,7 +1225,7 @@ public class BukkitChunkGenerator extends ChunkGenerator implements PlatformChun
|
||||
lastJigsawStudioRequestId = jigsawRequest.requestId();
|
||||
lastMode = null;
|
||||
}
|
||||
return;
|
||||
return studioGenerator;
|
||||
}
|
||||
}
|
||||
if (lastJigsawStudioRequestId != null) {
|
||||
@@ -1221,9 +1242,10 @@ public class BukkitChunkGenerator extends ChunkGenerator implements PlatformChun
|
||||
desired = StudioMode.OBJECT_BUFFET;
|
||||
}
|
||||
if (!desired.equals(lastMode)) {
|
||||
lastMode = desired;
|
||||
desired.inject(this);
|
||||
lastMode = desired;
|
||||
}
|
||||
return studioGenerator;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
|
||||
@@ -60,6 +60,7 @@ import art.arcane.volmlib.util.matter.MatterCavern;
|
||||
import art.arcane.volmlib.util.matter.MatterUpdate;
|
||||
import art.arcane.volmlib.util.scheduling.PrecisionStopwatch;
|
||||
import io.papermc.lib.PaperLib;
|
||||
import it.unimi.dsi.fastutil.ints.IntArrayList;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.Chunk;
|
||||
import org.bukkit.Location;
|
||||
@@ -228,12 +229,33 @@ public final class EngineBukkitOps {
|
||||
}
|
||||
}
|
||||
|
||||
mantleChunk.iterate(MatterUpdate.class, (x, yf, z, value) -> {
|
||||
int y = yf + engine.getWorld().minHeight();
|
||||
if (value != null && value.isUpdate()) {
|
||||
dispatcher.update(x, y, z);
|
||||
IntArrayList completed = new IntArrayList();
|
||||
try {
|
||||
mantleChunk.iterate(MatterUpdate.class, (x, yf, z, value) -> {
|
||||
int y = yf + engine.getWorld().minHeight();
|
||||
if (value != null && value.isUpdate()) {
|
||||
dispatcher.update(x, y, z);
|
||||
completed.add((yf << 8) | ((x & 15) << 4) | (z & 15));
|
||||
}
|
||||
});
|
||||
} catch (RuntimeException | Error failure) {
|
||||
try {
|
||||
for (int index = 0; index < completed.size(); index++) {
|
||||
int position = completed.getInt(index);
|
||||
int y = position >>> 8;
|
||||
Matter section = mantleChunk.get(y >> 4);
|
||||
if (section != null && section.hasSlice(MatterUpdate.class)) {
|
||||
section.<MatterUpdate>getSlice(MatterUpdate.class)
|
||||
.set((position >> 4) & 15, y & 15, position & 15, null);
|
||||
}
|
||||
}
|
||||
} catch (RuntimeException | Error cleanupFailure) {
|
||||
if (cleanupFailure != failure) {
|
||||
failure.addSuppressed(cleanupFailure);
|
||||
}
|
||||
}
|
||||
});
|
||||
throw failure;
|
||||
}
|
||||
mantleChunk.deleteSlices(MatterUpdate.class);
|
||||
engine.getMetrics().getUpdates().put(stopwatch.getMilliseconds());
|
||||
}
|
||||
|
||||
+48
-16
@@ -60,7 +60,7 @@ public class ObjectStudioGenerator extends EnginedStudioGenerator {
|
||||
private final PlatformBlockState marker;
|
||||
private final AtomicBoolean layoutBuilt = new AtomicBoolean(false);
|
||||
private final Object layoutLock = new Object();
|
||||
private final Map<String, IrisObject> objectCache = new ConcurrentHashMap<>();
|
||||
private final Map<String, DisplayObject> objectCache = new ConcurrentHashMap<>();
|
||||
private final Map<String, IrisData> packData = new ConcurrentHashMap<>();
|
||||
private volatile ObjectStudioLayout layout;
|
||||
|
||||
@@ -130,7 +130,7 @@ public class ObjectStudioGenerator extends EnginedStudioGenerator {
|
||||
|
||||
paintFrame(cell, tc, plinthY, chunkWorldX, chunkWorldZ, minHeight, maxHeight);
|
||||
|
||||
IrisObject object = loadObject(cell);
|
||||
DisplayObject object = loadObject(cell);
|
||||
if (object != null) {
|
||||
placeSlice(object, cell, tc, chunkWorldX, chunkWorldZ, minHeight, maxHeight);
|
||||
}
|
||||
@@ -196,24 +196,19 @@ public class ObjectStudioGenerator extends EnginedStudioGenerator {
|
||||
}
|
||||
}
|
||||
|
||||
private void placeSlice(IrisObject object, GridCell cell, TerrainChunk tc, int chunkWorldX, int chunkWorldZ, int minHeight, int maxHeight) {
|
||||
VectorMap<PlatformBlockState> blocks = object.getBlocks();
|
||||
private void placeSlice(DisplayObject object, GridCell cell, TerrainChunk tc, int chunkWorldX, int chunkWorldZ, int minHeight, int maxHeight) {
|
||||
VectorMap<PlatformBlockState> blocks = object.source().getBlocks();
|
||||
if (blocks == null || blocks.isEmpty()) return;
|
||||
|
||||
Vector3i center = object.getCenter();
|
||||
int centerX = center == null ? 0 : center.getBlockX();
|
||||
int centerY = center == null ? 0 : center.getBlockY();
|
||||
int centerZ = center == null ? 0 : center.getBlockZ();
|
||||
|
||||
int originX = cell.originX();
|
||||
int originY = cell.originY();
|
||||
int originZ = cell.originZ();
|
||||
|
||||
for (Map.Entry<IrisBlockVector, PlatformBlockState> entry : blocks) {
|
||||
IrisBlockVector signed = entry.getKey();
|
||||
int worldX = originX + signed.getBlockX() + centerX;
|
||||
int worldY = originY + signed.getBlockY() + centerY;
|
||||
int worldZ = originZ + signed.getBlockZ() + centerZ;
|
||||
int worldX = originX + signed.getBlockX() - object.minimumX();
|
||||
int worldY = originY + signed.getBlockY() - object.minimumY();
|
||||
int worldZ = originZ + signed.getBlockZ() - object.minimumZ();
|
||||
|
||||
if (worldX < chunkWorldX || worldX > chunkWorldX + 15) continue;
|
||||
if (worldZ < chunkWorldZ || worldZ > chunkWorldZ + 15) continue;
|
||||
@@ -226,17 +221,29 @@ public class ObjectStudioGenerator extends EnginedStudioGenerator {
|
||||
}
|
||||
}
|
||||
|
||||
private IrisObject loadObject(GridCell cell) {
|
||||
public IrisObject createCapture(GridCell cell) {
|
||||
DisplayObject displayed = loadObject(cell);
|
||||
if (displayed == null) {
|
||||
throw new IllegalStateException("Object Studio source is unavailable: " + cell.pack() + "/" + cell.key());
|
||||
}
|
||||
IrisObject capture = new IrisObject(cell.w(), cell.h(), cell.d());
|
||||
capture.setCenter(new Vector3i(-displayed.minimumX(), -displayed.minimumY(), -displayed.minimumZ()));
|
||||
return capture;
|
||||
}
|
||||
|
||||
private DisplayObject loadObject(GridCell cell) {
|
||||
String cacheKey = cell.pack() + "/" + cell.key();
|
||||
IrisObject cached = objectCache.get(cacheKey);
|
||||
DisplayObject cached = objectCache.get(cacheKey);
|
||||
if (cached != null) return cached;
|
||||
IrisData data = packData.get(cell.pack());
|
||||
if (data == null) return null;
|
||||
IrisObject loaded = data.getObjectLoader().load(cell.key());
|
||||
if (loaded != null) {
|
||||
objectCache.put(cacheKey, loaded);
|
||||
DisplayObject displayed = DisplayObject.of(loaded);
|
||||
DisplayObject existing = objectCache.putIfAbsent(cacheKey, displayed);
|
||||
return existing == null ? displayed : existing;
|
||||
}
|
||||
return loaded;
|
||||
return null;
|
||||
}
|
||||
|
||||
public Map<String, IrisData> getPackData() {
|
||||
@@ -257,6 +264,9 @@ public class ObjectStudioGenerator extends EnginedStudioGenerator {
|
||||
layout = resumed;
|
||||
} else {
|
||||
layout = ObjectStudioLayout.build(sources, padding);
|
||||
}
|
||||
layout = layout.atFloor(Math.max(engine.getMinHeight(), ObjectStudioLayout.FLOOR_Y));
|
||||
if (layout != resumed) {
|
||||
layout.save(layoutFile);
|
||||
}
|
||||
layoutBuilt.set(true);
|
||||
@@ -300,4 +310,26 @@ public class ObjectStudioGenerator extends EnginedStudioGenerator {
|
||||
}
|
||||
return new IrisBlockVector(maxX, 0, maxZ);
|
||||
}
|
||||
|
||||
private record DisplayObject(IrisObject source, int minimumX, int minimumY, int minimumZ) {
|
||||
private static DisplayObject of(IrisObject object) {
|
||||
int minimumX = Integer.MAX_VALUE;
|
||||
int minimumY = Integer.MAX_VALUE;
|
||||
int minimumZ = Integer.MAX_VALUE;
|
||||
for (Map.Entry<IrisBlockVector, PlatformBlockState> entry : object.getBlocks()) {
|
||||
if (entry.getValue() == null) {
|
||||
continue;
|
||||
}
|
||||
IrisBlockVector position = entry.getKey();
|
||||
minimumX = Math.min(minimumX, position.getBlockX());
|
||||
minimumY = Math.min(minimumY, position.getBlockY());
|
||||
minimumZ = Math.min(minimumZ, position.getBlockZ());
|
||||
}
|
||||
if (minimumX == Integer.MAX_VALUE) {
|
||||
Vector3i center = object.getCenter();
|
||||
return new DisplayObject(object, -center.getBlockX(), -center.getBlockY(), -center.getBlockZ());
|
||||
}
|
||||
return new DisplayObject(object, minimumX, minimumY, minimumZ);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,104 @@
|
||||
package art.arcane.iris.core.runtime;
|
||||
|
||||
import art.arcane.iris.core.loader.IrisData;
|
||||
import art.arcane.iris.core.loader.ResourceLoader;
|
||||
import art.arcane.iris.engine.object.IrisObject;
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.junit.rules.TemporaryFolder;
|
||||
|
||||
import java.io.DataOutputStream;
|
||||
import java.io.File;
|
||||
import java.nio.file.Files;
|
||||
import java.util.Map;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertNotNull;
|
||||
import static org.junit.Assert.assertNull;
|
||||
import static org.junit.Assert.assertSame;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
public class ObjectStudioLayoutTest {
|
||||
@Rule
|
||||
public final TemporaryFolder temporary = new TemporaryFolder();
|
||||
|
||||
@Test
|
||||
public void changedObjectHeaderInvalidatesSavedLayoutWithoutChangingItsKey() throws Exception {
|
||||
File objectFile = temporary.newFile("jungleclutt6.iob");
|
||||
File layoutFile = temporary.newFile("layout.json");
|
||||
IrisData data = source(objectFile);
|
||||
Map<String, IrisData> sources = Map.of("overworld", data);
|
||||
int[][] dimensions = {{6, 3, 4}, {5, 4, 4}, {5, 3, 5}};
|
||||
for (int[] changed : dimensions) {
|
||||
header(objectFile, 5, 3, 4);
|
||||
ObjectStudioLayout.build(sources, 2).save(layoutFile);
|
||||
assertNotNull(ObjectStudioLayout.load(layoutFile, sources, 2));
|
||||
|
||||
header(objectFile, changed[0], changed[1], changed[2]);
|
||||
|
||||
assertNull(ObjectStudioLayout.load(layoutFile, sources, 2));
|
||||
ObjectStudioLayout.GridCell rebuilt = ObjectStudioLayout.build(sources, 2).get("jungleclutt6");
|
||||
assertEquals(changed[0], rebuilt.w());
|
||||
assertEquals(changed[1], rebuilt.h());
|
||||
assertEquals(changed[2], rebuilt.d());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void highFloorMovesCellsWithoutChangingTheirPackingAndPersists() throws Exception {
|
||||
File objectFile = temporary.newFile("jungleclutt6.iob");
|
||||
File layoutFile = temporary.newFile("layout.json");
|
||||
header(objectFile, 5, 3, 4);
|
||||
Map<String, IrisData> sources = Map.of("overworld", source(objectFile));
|
||||
ObjectStudioLayout original = ObjectStudioLayout.build(sources, 3, 32);
|
||||
ObjectStudioLayout.GridCell before = original.get("jungleclutt6");
|
||||
|
||||
ObjectStudioLayout raised = original.atFloor(200);
|
||||
ObjectStudioLayout.GridCell after = raised.get("overworld/jungleclutt6");
|
||||
|
||||
assertEquals(201, after.originY());
|
||||
assertEquals(65, before.originY());
|
||||
assertEquals(before.originX(), after.originX());
|
||||
assertEquals(before.originZ(), after.originZ());
|
||||
assertEquals(before.w(), after.w());
|
||||
assertEquals(before.h(), after.h());
|
||||
assertEquals(before.d(), after.d());
|
||||
assertSame(raised, raised.atFloor(200));
|
||||
raised.save(layoutFile);
|
||||
ObjectStudioLayout restored = ObjectStudioLayout.load(layoutFile, sources, 3);
|
||||
assertNotNull(restored);
|
||||
assertEquals(after, restored.get("jungleclutt6"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void removedSourceObjectInvalidatesItsSavedCell() throws Exception {
|
||||
File objectFile = temporary.newFile("jungleclutt6.iob");
|
||||
File layoutFile = temporary.newFile("layout.json");
|
||||
header(objectFile, 5, 3, 4);
|
||||
IrisData data = source(objectFile);
|
||||
Map<String, IrisData> sources = Map.of("overworld", data);
|
||||
ObjectStudioLayout.build(sources, 2).save(layoutFile);
|
||||
when(data.getObjectLoader().findFile("jungleclutt6")).thenReturn(null);
|
||||
|
||||
assertNull(ObjectStudioLayout.load(layoutFile, sources, 2));
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private static IrisData source(File objectFile) {
|
||||
IrisData data = mock(IrisData.class);
|
||||
ResourceLoader<IrisObject> loader = mock(ResourceLoader.class);
|
||||
when(data.getObjectLoader()).thenReturn(loader);
|
||||
when(loader.getPossibleKeys()).thenReturn(new String[]{"jungleclutt6"});
|
||||
when(loader.findFile("jungleclutt6")).thenReturn(objectFile);
|
||||
return data;
|
||||
}
|
||||
|
||||
private static void header(File file, int width, int height, int depth) throws Exception {
|
||||
try (DataOutputStream output = new DataOutputStream(Files.newOutputStream(file.toPath()))) {
|
||||
output.writeInt(width);
|
||||
output.writeInt(height);
|
||||
output.writeInt(depth);
|
||||
}
|
||||
}
|
||||
}
|
||||
+222
@@ -0,0 +1,222 @@
|
||||
package art.arcane.iris.core.service;
|
||||
|
||||
import art.arcane.iris.core.runtime.jigsaw.JigsawStudioActivation;
|
||||
import art.arcane.iris.core.runtime.jigsaw.JigsawStudioBounds;
|
||||
import art.arcane.iris.core.runtime.jigsaw.JigsawStudioCellDimensions;
|
||||
import art.arcane.iris.engine.framework.Engine;
|
||||
import art.arcane.iris.engine.framework.PlacedStructurePiece;
|
||||
import art.arcane.iris.engine.platform.studio.generators.JigsawStudioGenerator;
|
||||
import art.arcane.iris.util.common.scheduling.J;
|
||||
import org.bukkit.Color;
|
||||
import org.bukkit.Location;
|
||||
import org.bukkit.Particle;
|
||||
import org.bukkit.World;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.junit.Test;
|
||||
import org.mockito.MockedStatic;
|
||||
import org.mockito.invocation.Invocation;
|
||||
|
||||
import java.lang.reflect.Constructor;
|
||||
import java.lang.reflect.Field;
|
||||
import java.lang.reflect.InvocationTargetException;
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.atomic.AtomicLong;
|
||||
|
||||
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.Mockito.clearInvocations;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.mockStatic;
|
||||
import static org.mockito.Mockito.mockingDetails;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
public class JigsawStudioVisualizationBoundsTest {
|
||||
@Test
|
||||
public void livePreviewUsesFullAssemblyBoundsBeyondEveryCellLimit() throws Exception {
|
||||
List<JigsawStudioPreviewRenderer.PreviewBounds> bounds = List.of(
|
||||
new JigsawStudioPreviewRenderer.PreviewBounds(-300, 60, -400, 300, 80, 400),
|
||||
new JigsawStudioPreviewRenderer.PreviewBounds(-10, -50, -10, 10, 220, 10),
|
||||
new JigsawStudioPreviewRenderer.PreviewBounds(0, 0, 0, 127, 191, 127));
|
||||
Fixture fixture = fixture();
|
||||
for (JigsawStudioPreviewRenderer.PreviewBounds preview : bounds) {
|
||||
Location viewer = new Location(fixture.world(),
|
||||
preview.maximumX() + 1.0D, preview.maximumY() + 1.0D, preview.maximumZ() + 1.0D);
|
||||
evaluate(fixture, preview);
|
||||
clearInvocations(fixture.player());
|
||||
|
||||
drawLive(fixture, viewer, 384);
|
||||
|
||||
List<Emission> emissions = emissions(fixture.player());
|
||||
assertFalse(emissions.isEmpty());
|
||||
assertTrue(emissions.size() <= 384);
|
||||
assertTrue(emissions.stream().anyMatch(emission ->
|
||||
emission.x() == viewer.getX() && emission.y() == viewer.getY()
|
||||
&& emission.z() == viewer.getZ()));
|
||||
for (Emission emission : emissions) {
|
||||
double x = emission.x() - viewer.getX();
|
||||
double y = emission.y() - viewer.getY();
|
||||
double z = emission.z() - viewer.getZ();
|
||||
assertTrue(x * x + y * y + z * z <= 96.0D * 96.0D);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void ordinaryPreviewKeepsCellOutlineParticleOrderAndAppearance() throws Exception {
|
||||
Fixture fixture = fixture();
|
||||
JigsawStudioBounds bounds = new JigsawStudioBounds(-7, 60, -11,
|
||||
new JigsawStudioCellDimensions(13, 9, 17));
|
||||
evaluate(fixture, new JigsawStudioPreviewRenderer.PreviewBounds(
|
||||
bounds.originX(), bounds.originY(), bounds.originZ(), bounds.maxX(), bounds.maxY(), bounds.maxZ()));
|
||||
Location viewer = new Location(fixture.world(), 0, 64, 0);
|
||||
drawLive(fixture, viewer, 384);
|
||||
List<Emission> preview = emissions(fixture.player());
|
||||
clearInvocations(fixture.player());
|
||||
|
||||
Method draw = JigsawStudioService.class.getDeclaredMethod("drawBounds", Player.class, Location.class,
|
||||
JigsawStudioBounds.class, Color.class, float.class, double.class, nested("ParticleBudget"));
|
||||
draw.setAccessible(true);
|
||||
Field color = JigsawStudioService.class.getDeclaredField("ASSEMBLY_PREVIEW_COLOR");
|
||||
color.setAccessible(true);
|
||||
invoke(draw, null, fixture.player(), viewer, bounds, color.get(null), 0.85F, 2.0D, budget(384));
|
||||
|
||||
assertFalse(preview.isEmpty());
|
||||
assertEquals(preview, emissions(fixture.player()));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void livePreviewRefreshesChangedBoundsAndHonorsExhaustedBudget() throws Exception {
|
||||
Fixture fixture = fixture();
|
||||
Location viewer = new Location(fixture.world(), 201, 65, 201);
|
||||
evaluate(fixture, JigsawStudioPreviewRenderer.PreviewBounds.empty());
|
||||
drawLive(fixture, viewer, 384);
|
||||
assertTrue(emissions(fixture.player()).isEmpty());
|
||||
|
||||
evaluate(fixture, new JigsawStudioPreviewRenderer.PreviewBounds(0, 64, 0, 200, 64, 200));
|
||||
drawLive(fixture, viewer, 0);
|
||||
assertTrue(emissions(fixture.player()).isEmpty());
|
||||
drawLive(fixture, viewer, 7);
|
||||
assertEquals(7, emissions(fixture.player()).size());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void explicitAssemblyPreviewAcceptsLargePlacedBounds() throws Exception {
|
||||
Fixture fixture = fixture();
|
||||
PlacedStructurePiece piece = mock(PlacedStructurePiece.class);
|
||||
when(piece.getMinX()).thenReturn(-200);
|
||||
when(piece.getMinY()).thenReturn(64);
|
||||
when(piece.getMinZ()).thenReturn(-200);
|
||||
when(piece.getMaxX()).thenReturn(200);
|
||||
when(piece.getMaxY()).thenReturn(64);
|
||||
when(piece.getMaxZ()).thenReturn(200);
|
||||
try (MockedStatic<J> scheduling = mockStatic(J.class)) {
|
||||
scheduling.when(() -> J.isOwnedByCurrentRegion(fixture.player())).thenReturn(true);
|
||||
assertTrue(fixture.service().showAssemblyPreview(fixture.player(), List.of(piece)));
|
||||
}
|
||||
|
||||
Method draw = JigsawStudioService.class.getDeclaredMethod("drawAssemblyPreview", Player.class,
|
||||
Location.class, nested("ParticleBudget"));
|
||||
draw.setAccessible(true);
|
||||
invoke(draw, fixture.service(), fixture.player(), new Location(fixture.world(), 201, 65, 201), budget(384));
|
||||
|
||||
assertTrue(emissions(fixture.player()).stream().anyMatch(emission ->
|
||||
emission.x() == 201 && emission.y() == 65 && emission.z() == 201));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void authoredCellLimitsRemainEnforced() {
|
||||
assertThrows(IllegalArgumentException.class, () -> new JigsawStudioCellDimensions(129, 1, 1));
|
||||
assertThrows(IllegalArgumentException.class, () -> new JigsawStudioCellDimensions(1, 1, 129));
|
||||
assertThrows(IllegalArgumentException.class, () -> new JigsawStudioCellDimensions(1, 193, 1));
|
||||
assertThrows(IllegalArgumentException.class, () -> new JigsawStudioCellDimensions(128, 192, 128));
|
||||
}
|
||||
|
||||
private static Fixture fixture() throws Exception {
|
||||
UUID requestId = UUID.randomUUID();
|
||||
UUID worldId = UUID.randomUUID();
|
||||
World world = mock(World.class);
|
||||
when(world.getUID()).thenReturn(worldId);
|
||||
Player player = mock(Player.class);
|
||||
when(player.getUniqueId()).thenReturn(UUID.randomUUID());
|
||||
when(player.getWorld()).thenReturn(world);
|
||||
JigsawStudioActivation.Request request = mock(JigsawStudioActivation.Request.class);
|
||||
when(request.requestId()).thenReturn(requestId);
|
||||
JigsawStudioGenerator generator = mock(JigsawStudioGenerator.class);
|
||||
when(generator.getRequest()).thenReturn(request);
|
||||
Constructor<?> constructor = nested("ActiveStudio").getDeclaredConstructor(UUID.class, World.class,
|
||||
Engine.class, JigsawStudioGenerator.class, ConcurrentHashMap.class, Set.class, AtomicLong.class);
|
||||
constructor.setAccessible(true);
|
||||
Object studio = constructor.newInstance(worldId, world, mock(Engine.class), generator,
|
||||
new ConcurrentHashMap<>(), ConcurrentHashMap.newKeySet(), new AtomicLong());
|
||||
JigsawStudioService service = new JigsawStudioService();
|
||||
map(service, "studios").put(worldId, studio);
|
||||
return new Fixture(service, player, world, requestId, studio);
|
||||
}
|
||||
|
||||
private static void evaluate(Fixture fixture, JigsawStudioPreviewRenderer.PreviewBounds bounds) throws Exception {
|
||||
map(fixture.service(), "evaluations").put(fixture.requestId(), new JigsawStudioGraphEvaluation(
|
||||
fixture.requestId(), 1, 1337, JigsawStudioEvaluationState.VALID, "", 2, "", bounds));
|
||||
}
|
||||
|
||||
private static void drawLive(Fixture fixture, Location viewer, int remaining) throws Exception {
|
||||
Method draw = JigsawStudioService.class.getDeclaredMethod("drawLivePreview", Player.class, Location.class,
|
||||
nested("ActiveStudio"), nested("ParticleBudget"));
|
||||
draw.setAccessible(true);
|
||||
invoke(draw, fixture.service(), fixture.player(), viewer, fixture.studio(), budget(remaining));
|
||||
}
|
||||
|
||||
private static Object budget(int remaining) throws Exception {
|
||||
Constructor<?> constructor = nested("ParticleBudget").getDeclaredConstructor(int.class);
|
||||
constructor.setAccessible(true);
|
||||
return constructor.newInstance(remaining);
|
||||
}
|
||||
|
||||
private static Class<?> nested(String name) throws ClassNotFoundException {
|
||||
return Class.forName(JigsawStudioService.class.getName() + "$" + name);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private static Map<UUID, Object> map(JigsawStudioService service, String name) throws Exception {
|
||||
Field field = JigsawStudioService.class.getDeclaredField(name);
|
||||
field.setAccessible(true);
|
||||
return (Map<UUID, Object>) field.get(service);
|
||||
}
|
||||
|
||||
private static void invoke(Method method, Object target, Object... arguments) throws Exception {
|
||||
try {
|
||||
method.invoke(target, arguments);
|
||||
} catch (InvocationTargetException failure) {
|
||||
if (failure.getCause() instanceof Exception cause) {
|
||||
throw cause;
|
||||
}
|
||||
throw failure;
|
||||
}
|
||||
}
|
||||
|
||||
private static List<Emission> emissions(Player player) {
|
||||
List<Emission> emissions = new ArrayList<>();
|
||||
for (Invocation invocation : mockingDetails(player).getInvocations()) {
|
||||
if (!invocation.getMethod().getName().equals("spawnParticle")) {
|
||||
continue;
|
||||
}
|
||||
Particle.DustOptions dust = invocation.getArgument(9);
|
||||
emissions.add(new Emission(invocation.getArgument(1), invocation.getArgument(2),
|
||||
invocation.getArgument(3), dust.getColor(), dust.getSize()));
|
||||
}
|
||||
return emissions;
|
||||
}
|
||||
|
||||
private record Fixture(JigsawStudioService service, Player player, World world, UUID requestId, Object studio) {
|
||||
}
|
||||
|
||||
private record Emission(double x, double y, double z, Color color, float size) {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
package art.arcane.iris.engine;
|
||||
|
||||
import art.arcane.iris.core.IrisSettings;
|
||||
import art.arcane.iris.engine.framework.Engine;
|
||||
import art.arcane.iris.engine.history.SavedBiomeUnavailableException;
|
||||
import art.arcane.iris.engine.platform.EngineBukkitOps;
|
||||
import art.arcane.volmlib.util.mantle.runtime.Mantle;
|
||||
import art.arcane.volmlib.util.matter.Matter;
|
||||
import org.bukkit.Chunk;
|
||||
import org.bukkit.World;
|
||||
import org.junit.Test;
|
||||
import org.mockito.MockedStatic;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
|
||||
import static org.junit.Assert.assertSame;
|
||||
import static org.junit.Assert.assertThrows;
|
||||
import static org.mockito.ArgumentMatchers.anyInt;
|
||||
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 WorldChunkMaintenanceSavedBiomeTest {
|
||||
@Test
|
||||
public void loadingDefersInitialSpawnUntilTheNextSuccessfulPass() throws Exception {
|
||||
verifyFailure(new SavedBiomeUnavailableException("Loading", true), true);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void unavailableSavedDataRemainsAnOperatorFailure() throws Exception {
|
||||
verifyFailure(new SavedBiomeUnavailableException("Missing biome", false), false);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void unrelatedFailuresAreNotSuppressed() throws Exception {
|
||||
verifyFailure(new IllegalStateException("Broken update"), false);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void loadingWithFailedMarkerCleanupRemainsAnOperatorFailure() throws Exception {
|
||||
SavedBiomeUnavailableException loading = new SavedBiomeUnavailableException("Loading", true);
|
||||
IllegalStateException cleanup = new IllegalStateException("Marker cleanup failed");
|
||||
loading.addSuppressed(cleanup);
|
||||
|
||||
verifyFailure(loading, false);
|
||||
|
||||
assertSame(cleanup, loading.getSuppressed()[0]);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private void verifyFailure(RuntimeException failure, boolean loading) throws Exception {
|
||||
Engine engine = mock(Engine.class);
|
||||
IrisWorldManager manager = mock(IrisWorldManager.class);
|
||||
Mantle<Matter> mantle = mock(Mantle.class);
|
||||
WorldEntitySpawner spawner = mock(WorldEntitySpawner.class);
|
||||
Field field = IrisWorldManager.class.getDeclaredField("entitySpawner");
|
||||
field.setAccessible(true);
|
||||
field.set(manager, spawner);
|
||||
when(manager.getEngine()).thenReturn(engine);
|
||||
when(manager.getMantle()).thenReturn(mantle);
|
||||
when(mantle.isChunkLoaded(2, -1)).thenReturn(true);
|
||||
when(spawner.isEntitySpawningEnabledForCurrentWorld()).thenReturn(true);
|
||||
World world = mock(World.class);
|
||||
Chunk chunk = mock(Chunk.class);
|
||||
when(world.isChunkLoaded(anyInt(), anyInt())).thenReturn(true);
|
||||
when(world.getChunkAt(2, -1)).thenReturn(chunk);
|
||||
IrisSettings settings = new IrisSettings();
|
||||
settings.getWorld().setPostLoadBlockUpdates(true);
|
||||
WorldChunkMaintenance maintenance = new WorldChunkMaintenance(manager);
|
||||
|
||||
try (MockedStatic<IrisSettings> configured = mockStatic(IrisSettings.class);
|
||||
MockedStatic<EngineBukkitOps> updates = mockStatic(EngineBukkitOps.class)) {
|
||||
configured.when(IrisSettings::get).thenReturn(settings);
|
||||
updates.when(() -> EngineBukkitOps.updateChunk(engine, chunk))
|
||||
.thenThrow(failure).thenAnswer(invocation -> null);
|
||||
|
||||
if (loading) {
|
||||
maintenance.updateChunkRegion(world, 2, -1);
|
||||
} else {
|
||||
assertSame(failure, assertThrows(RuntimeException.class,
|
||||
() -> maintenance.updateChunkRegion(world, 2, -1)));
|
||||
}
|
||||
verify(spawner, never()).spawnInitially(chunk);
|
||||
|
||||
maintenance.updateChunkRegion(world, 2, -1);
|
||||
|
||||
verify(spawner).spawnInitially(chunk);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,320 @@
|
||||
package art.arcane.iris.engine;
|
||||
|
||||
import art.arcane.iris.core.IrisSettings;
|
||||
import art.arcane.iris.core.gui.PregeneratorJob;
|
||||
import art.arcane.iris.core.tools.IrisToolbelt;
|
||||
import art.arcane.iris.engine.framework.Engine;
|
||||
import art.arcane.iris.engine.framework.EngineAssignedWorldManager;
|
||||
import art.arcane.iris.engine.framework.EngineTarget;
|
||||
import art.arcane.iris.engine.framework.GenerationSessionException;
|
||||
import art.arcane.iris.engine.framework.GenerationSessionLease;
|
||||
import art.arcane.iris.engine.framework.GenerationSessionManager;
|
||||
import art.arcane.iris.engine.object.IrisWorld;
|
||||
import art.arcane.iris.platform.bukkit.BukkitWorldBinding;
|
||||
import art.arcane.iris.spi.IrisLogging;
|
||||
import art.arcane.iris.util.common.scheduling.J;
|
||||
import art.arcane.iris.util.project.context.IrisContext;
|
||||
import art.arcane.volmlib.util.scheduling.ChronoLatch;
|
||||
import org.bukkit.World;
|
||||
import org.bukkit.entity.LivingEntity;
|
||||
import org.junit.Test;
|
||||
import org.mockito.MockedStatic;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import java.lang.reflect.InvocationTargetException;
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.BlockingQueue;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.Future;
|
||||
import java.util.concurrent.LinkedBlockingQueue;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertNotNull;
|
||||
import static org.junit.Assert.assertSame;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.anyInt;
|
||||
import static org.mockito.ArgumentMatchers.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 WorldEntitySpawnerLifecycleTest {
|
||||
@Test
|
||||
public void queuedSpawnIsDiscardedWhenTheManagerCloses() throws Exception {
|
||||
try (Fixture fixture = new Fixture()) {
|
||||
Future<Boolean> waiting = fixture.spawn(false, true);
|
||||
Runnable task = fixture.nextTask();
|
||||
fixture.manager.close();
|
||||
|
||||
task.run();
|
||||
|
||||
assertFalse(waiting.get(1L, TimeUnit.SECONDS));
|
||||
verify(fixture.world, never()).isChunkLoaded(anyInt(), anyInt());
|
||||
assertEquals(0, fixture.sessions.activeLeases());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void queuedSpawnIsDiscardedWhenEngineDrainBegins() throws Exception {
|
||||
try (Fixture fixture = new Fixture()) {
|
||||
Future<Boolean> waiting = fixture.spawn(false, true);
|
||||
Runnable task = fixture.nextTask();
|
||||
when(fixture.engine.isClosing()).thenReturn(true);
|
||||
fixture.sessions.sealAndAwait("closing", 1_000L);
|
||||
|
||||
task.run();
|
||||
|
||||
assertFalse(waiting.get(1L, TimeUnit.SECONDS));
|
||||
verify(fixture.world, never()).isChunkLoaded(anyInt(), anyInt());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void interruptedSpawnWaitDiscardsItsQueuedWork() throws Exception {
|
||||
try (Fixture fixture = new Fixture()) {
|
||||
Future<Boolean> waiting = fixture.spawn(true, true);
|
||||
Runnable task = fixture.nextTask();
|
||||
assertFalse(waiting.get(1L, TimeUnit.SECONDS));
|
||||
assertTrue(fixture.interrupted.get());
|
||||
|
||||
task.run();
|
||||
|
||||
verify(fixture.world, never()).isChunkLoaded(anyInt(), anyInt());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void rejectedSpawnDoesNotLeaveExecutableWork() throws Exception {
|
||||
try (Fixture fixture = new Fixture()) {
|
||||
Future<Boolean> waiting = fixture.spawn(false, false);
|
||||
Runnable task = fixture.nextTask();
|
||||
assertFalse(waiting.get(1L, TimeUnit.SECONDS));
|
||||
|
||||
task.run();
|
||||
|
||||
verify(fixture.world, never()).isChunkLoaded(anyInt(), anyInt());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void admittedSpawnHoldsTheEngineDrainOnItsOwnerThread() throws Exception {
|
||||
try (Fixture fixture = new Fixture()) {
|
||||
CountDownLatch entered = new CountDownLatch(1);
|
||||
CountDownLatch release = new CountDownLatch(1);
|
||||
AtomicReference<Thread> observed = new AtomicReference<>();
|
||||
when(fixture.world.isChunkLoaded(7, -4)).thenAnswer(invocation -> {
|
||||
observed.set(Thread.currentThread());
|
||||
assertSame(fixture.engine, IrisContext.get().getEngine());
|
||||
assertEquals(1, fixture.sessions.activeLeases());
|
||||
entered.countDown();
|
||||
assertTrue(release.await(2L, TimeUnit.SECONDS));
|
||||
return false;
|
||||
});
|
||||
Future<Boolean> waiting = fixture.spawn(false, true);
|
||||
Runnable task = fixture.nextTask();
|
||||
ExecutorService owner = Executors.newSingleThreadExecutor();
|
||||
ExecutorService closer = Executors.newSingleThreadExecutor();
|
||||
try {
|
||||
Future<?> running = owner.submit(task);
|
||||
assertTrue(entered.await(1L, TimeUnit.SECONDS));
|
||||
CountDownLatch closing = new CountDownLatch(1);
|
||||
Future<?> drained = closer.submit(() -> {
|
||||
closing.countDown();
|
||||
fixture.sessions.sealAndAwait("closing", 1_500L);
|
||||
return null;
|
||||
});
|
||||
assertTrue(closing.await(1L, TimeUnit.SECONDS));
|
||||
waitForSeal(fixture.sessions);
|
||||
assertFalse(drained.isDone());
|
||||
assertEquals(1, fixture.sessions.activeLeases());
|
||||
release.countDown();
|
||||
running.get(1L, TimeUnit.SECONDS);
|
||||
drained.get(1L, TimeUnit.SECONDS);
|
||||
assertTrue(waiting.get(1L, TimeUnit.SECONDS));
|
||||
assertNotNull(observed.get());
|
||||
assertFalse(observed.get() == fixture.callerThread.get());
|
||||
assertEquals(0, fixture.sessions.activeLeases());
|
||||
} finally {
|
||||
release.countDown();
|
||||
owner.shutdownNow();
|
||||
closer.shutdownNow();
|
||||
assertTrue(owner.awaitTermination(2L, TimeUnit.SECONDS));
|
||||
assertTrue(closer.awaitTermination(2L, TimeUnit.SECONDS));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void unexpectedSpawnFailureIsReportedWithItsCause() throws Exception {
|
||||
try (Fixture fixture = new Fixture()) {
|
||||
IllegalStateException failure = new IllegalStateException("unexpected chunk failure");
|
||||
when(fixture.world.isChunkLoaded(7, -4)).thenThrow(failure);
|
||||
Future<Boolean> waiting = fixture.spawn(false, true);
|
||||
Runnable task = fixture.nextTask();
|
||||
|
||||
try (MockedStatic<IrisLogging> logging = mockStatic(IrisLogging.class)) {
|
||||
task.run();
|
||||
logging.verify(() -> IrisLogging.reportError(
|
||||
eq("Failed to spawn Iris entities in chunk 7,-4."), same(failure)));
|
||||
}
|
||||
|
||||
assertFalse(waiting.get(1L, TimeUnit.SECONDS));
|
||||
assertEquals(0, fixture.sessions.activeLeases());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void queuedCountIsDiscardedWithoutPublishingAfterClose() throws Exception {
|
||||
try (Fixture fixture = new Fixture()) {
|
||||
fixture.manager.entityCount = 19;
|
||||
fixture.manager.entityCountValid = true;
|
||||
Future<Boolean> waiting = fixture.count();
|
||||
Runnable task = fixture.nextTask();
|
||||
fixture.manager.close();
|
||||
|
||||
task.run();
|
||||
|
||||
assertFalse(waiting.get(1L, TimeUnit.SECONDS));
|
||||
assertFalse(fixture.manager.entityCountValid);
|
||||
assertEquals(19, fixture.manager.entityCount);
|
||||
verify(fixture.world, never()).getLivingEntities();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void timedOutCountCannotRunLaterAndANewCountRecovers() throws Exception {
|
||||
try (Fixture fixture = new Fixture()) {
|
||||
Future<Boolean> waiting = fixture.count();
|
||||
Runnable expired = fixture.nextTask();
|
||||
assertFalse(waiting.get(3L, TimeUnit.SECONDS));
|
||||
assertFalse(fixture.manager.entityCountValid);
|
||||
|
||||
expired.run();
|
||||
verify(fixture.world, never()).getLivingEntities();
|
||||
when(fixture.world.getLivingEntities()).thenReturn(List.of(mock(LivingEntity.class)));
|
||||
setField(IrisWorldManager.class, fixture.manager, "cl", new ChronoLatch(1L));
|
||||
Future<Boolean> retry = fixture.count();
|
||||
fixture.nextTask().run();
|
||||
|
||||
assertFalse(retry.get(1L, TimeUnit.SECONDS));
|
||||
assertTrue(fixture.manager.entityCountValid);
|
||||
assertEquals(1, fixture.manager.entityCount);
|
||||
assertEquals(0, fixture.sessions.activeLeases());
|
||||
}
|
||||
}
|
||||
|
||||
private static void waitForSeal(GenerationSessionManager sessions) throws Exception {
|
||||
long deadline = System.nanoTime() + TimeUnit.SECONDS.toNanos(1L);
|
||||
while (System.nanoTime() < deadline) {
|
||||
try (GenerationSessionLease ignored = sessions.acquire("seal_probe")) {
|
||||
Thread.onSpinWait();
|
||||
} catch (GenerationSessionException expected) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
throw new AssertionError("Generation session did not seal within one second.");
|
||||
}
|
||||
|
||||
private static void setField(Class<?> owner, Object target, String name, Object value) throws Exception {
|
||||
Field field = owner.getDeclaredField(name);
|
||||
field.setAccessible(true);
|
||||
field.set(target, value);
|
||||
}
|
||||
|
||||
private static final class Fixture implements AutoCloseable {
|
||||
private final Engine engine = mock(Engine.class);
|
||||
private final World world = mock(World.class);
|
||||
private final IrisWorld irisWorld = mock(IrisWorld.class);
|
||||
private final GenerationSessionManager sessions = new GenerationSessionManager();
|
||||
private final IrisWorldManager manager;
|
||||
private final WorldEntitySpawner spawner;
|
||||
private final ExecutorService caller = Executors.newSingleThreadExecutor();
|
||||
private final BlockingQueue<Runnable> queued = new LinkedBlockingQueue<>();
|
||||
private final AtomicBoolean interrupted = new AtomicBoolean();
|
||||
private final AtomicReference<Thread> callerThread = new AtomicReference<>();
|
||||
|
||||
private Fixture() throws Exception {
|
||||
EngineTarget target = mock(EngineTarget.class);
|
||||
when(irisWorld.name()).thenReturn("spawner-lifecycle");
|
||||
when(irisWorld.hasPlatformWorld()).thenReturn(true);
|
||||
when(target.getWorld()).thenReturn(irisWorld);
|
||||
when(engine.getTarget()).thenReturn(target);
|
||||
when(engine.getWorld()).thenReturn(irisWorld);
|
||||
when(engine.getGenerationSessions()).thenReturn(sessions);
|
||||
manager = new IrisWorldManager(engine);
|
||||
setField(EngineAssignedWorldManager.class, manager, "started", new AtomicBoolean(true));
|
||||
spawner = manager.entitySpawner;
|
||||
}
|
||||
|
||||
private Future<Boolean> spawn(boolean interrupt, boolean accepted) {
|
||||
return caller.submit(() -> {
|
||||
callerThread.set(Thread.currentThread());
|
||||
try (MockedStatic<J> scheduler = mockStatic(J.class)) {
|
||||
scheduler.when(() -> J.runRegion(eq(world), eq(7), eq(-4), any(Runnable.class)))
|
||||
.thenAnswer(invocation -> {
|
||||
queued.add(invocation.getArgument(3));
|
||||
return accepted;
|
||||
});
|
||||
if (interrupt) {
|
||||
Thread.currentThread().interrupt();
|
||||
}
|
||||
Method method = WorldEntitySpawner.class.getDeclaredMethod(
|
||||
"spawnChunkSafely", World.class, int.class, int.class, boolean.class);
|
||||
method.setAccessible(true);
|
||||
try {
|
||||
return (Boolean) method.invoke(spawner, world, 7, -4, false);
|
||||
} catch (InvocationTargetException failure) {
|
||||
throw new IllegalStateException(failure.getCause());
|
||||
} finally {
|
||||
interrupted.set(Thread.interrupted());
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private Future<Boolean> count() {
|
||||
return caller.submit(() -> {
|
||||
IrisSettings settings = new IrisSettings();
|
||||
settings.getWorld().setTargetSpawnEntitiesPerChunk(0.0D);
|
||||
try (MockedStatic<J> scheduler = mockStatic(J.class);
|
||||
MockedStatic<BukkitWorldBinding> bindings = mockStatic(BukkitWorldBinding.class);
|
||||
MockedStatic<IrisToolbelt> toolbelt = mockStatic(IrisToolbelt.class);
|
||||
MockedStatic<PregeneratorJob> pregenerator = mockStatic(PregeneratorJob.class);
|
||||
MockedStatic<IrisSettings> configured = mockStatic(IrisSettings.class);
|
||||
MockedStatic<IrisLogging> logging = mockStatic(IrisLogging.class)) {
|
||||
bindings.when(() -> BukkitWorldBinding.world(irisWorld)).thenReturn(world);
|
||||
configured.when(IrisSettings::get).thenReturn(settings);
|
||||
scheduler.when(() -> J.runGlobal(any(Runnable.class))).thenAnswer(invocation -> {
|
||||
queued.add(invocation.getArgument(0));
|
||||
return true;
|
||||
});
|
||||
return spawner.onAsyncTick();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private Runnable nextTask() throws InterruptedException {
|
||||
Runnable task = queued.poll(5L, TimeUnit.SECONDS);
|
||||
assertNotNull("Owner callback was not scheduled", task);
|
||||
return task;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() throws Exception {
|
||||
caller.shutdownNow();
|
||||
assertTrue(caller.awaitTermination(2L, TimeUnit.SECONDS));
|
||||
manager.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
+215
@@ -0,0 +1,215 @@
|
||||
package art.arcane.iris.engine.platform;
|
||||
|
||||
import art.arcane.iris.engine.IrisEngine;
|
||||
import art.arcane.iris.engine.data.cache.AtomicCache;
|
||||
import art.arcane.iris.engine.framework.Engine;
|
||||
import art.arcane.iris.engine.framework.EngineTarget;
|
||||
import art.arcane.iris.engine.object.IrisDimension;
|
||||
import art.arcane.iris.engine.object.IrisDimensionRuntimeContract;
|
||||
import art.arcane.iris.engine.object.IrisWorld;
|
||||
import art.arcane.iris.engine.object.StudioMode;
|
||||
import art.arcane.iris.engine.platform.studio.StudioGenerator;
|
||||
import art.arcane.volmlib.util.collection.KList;
|
||||
import org.bukkit.World;
|
||||
import org.junit.Test;
|
||||
import org.mockito.MockedConstruction;
|
||||
import org.mockito.MockedStatic;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import java.lang.reflect.InvocationTargetException;
|
||||
import java.lang.reflect.Method;
|
||||
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 java.util.concurrent.TimeoutException;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.concurrent.locks.ReentrantLock;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertNull;
|
||||
import static org.junit.Assert.assertSame;
|
||||
import static org.junit.Assert.assertThrows;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.mockito.Mockito.CALLS_REAL_METHODS;
|
||||
import static org.mockito.Mockito.doAnswer;
|
||||
import static org.mockito.Mockito.doReturn;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.mockConstruction;
|
||||
import static org.mockito.Mockito.mockStatic;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.times;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
public class BukkitChunkGeneratorStudioSelectionTest {
|
||||
@Test
|
||||
public void concurrentInitialSelectionWaitsForPublishedGenerator() throws Exception {
|
||||
Fixture fixture = new Fixture();
|
||||
CountDownLatch injecting = new CountDownLatch(1);
|
||||
CountDownLatch release = new CountDownLatch(1);
|
||||
CountDownLatch secondAttempting = new CountDownLatch(1);
|
||||
doAnswer(invocation -> {
|
||||
injecting.countDown();
|
||||
assertTrue(release.await(5L, TimeUnit.SECONDS));
|
||||
fixture.generator.setStudioGenerator(fixture.selected);
|
||||
return null;
|
||||
}).when(fixture.mode).inject(fixture.generator);
|
||||
ExecutorService workers = Executors.newFixedThreadPool(2);
|
||||
try {
|
||||
Future<StudioGenerator> first = workers.submit(() -> resolve(fixture.generator));
|
||||
assertTrue(injecting.await(5L, TimeUnit.SECONDS));
|
||||
Future<StudioGenerator> second = workers.submit(() -> {
|
||||
secondAttempting.countDown();
|
||||
return resolve(fixture.generator);
|
||||
});
|
||||
assertTrue(secondAttempting.await(5L, TimeUnit.SECONDS));
|
||||
assertThrows(TimeoutException.class, () -> second.get(150L, TimeUnit.MILLISECONDS));
|
||||
release.countDown();
|
||||
|
||||
assertSame(fixture.selected, first.get(5L, TimeUnit.SECONDS));
|
||||
assertSame(fixture.selected, second.get(5L, TimeUnit.SECONDS));
|
||||
verify(fixture.mode).inject(fixture.generator);
|
||||
} finally {
|
||||
release.countDown();
|
||||
workers.shutdownNow();
|
||||
assertTrue(workers.awaitTermination(5L, TimeUnit.SECONDS));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void failedSelectionDoesNotPublishTheModeOrLoseThePreviousGenerator() throws Exception {
|
||||
Fixture fixture = new Fixture();
|
||||
StudioGenerator previous = mock(StudioGenerator.class);
|
||||
fixture.generator.setStudioGenerator(previous);
|
||||
IllegalStateException failure = new IllegalStateException("studio selection failed");
|
||||
doAnswer(invocation -> {
|
||||
throw failure;
|
||||
}).doAnswer(invocation -> {
|
||||
fixture.generator.setStudioGenerator(fixture.selected);
|
||||
return null;
|
||||
}).when(fixture.mode).inject(fixture.generator);
|
||||
|
||||
assertSame(failure, assertThrows(IllegalStateException.class, () -> resolve(fixture.generator)));
|
||||
assertSame(StudioMode.NORMAL, fixture.generator.getLastMode());
|
||||
assertSame(previous, fixture.generator.getStudioGenerator());
|
||||
assertSame(fixture.selected, resolve(fixture.generator));
|
||||
assertSame(fixture.mode, fixture.generator.getLastMode());
|
||||
verify(fixture.mode, times(2)).inject(fixture.generator);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void initialWorldTouchSelectsStudioBeforePublishingSetup() throws Exception {
|
||||
Fixture fixture = new Fixture();
|
||||
doAnswer(invocation -> {
|
||||
assertFalse(fixture.setup.get());
|
||||
fixture.generator.setStudioGenerator(fixture.selected);
|
||||
return null;
|
||||
}).when(fixture.mode).inject(fixture.generator);
|
||||
try (MockedStatic<BukkitChunkGenerator> methods = mockStatic(BukkitChunkGenerator.class, CALLS_REAL_METHODS);
|
||||
MockedConstruction<IrisEngine> engines = mockConstruction(IrisEngine.class,
|
||||
(created, context) -> when(created.getDimension()).thenReturn(fixture.dimension))) {
|
||||
methods.when(() -> BukkitChunkGenerator.shouldRunStudioHotload(true, false, false)).thenReturn(false);
|
||||
|
||||
fixture.generator.touch(mock(World.class));
|
||||
|
||||
assertTrue(fixture.setup.get());
|
||||
assertSame(fixture.selected, fixture.generator.getStudioGenerator());
|
||||
assertSame(fixture.mode, fixture.generator.getLastMode());
|
||||
assertEquals(1, engines.constructed().size());
|
||||
verify(fixture.mode).inject(fixture.generator);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void failedInitialSelectionClosesItsEngineAndRejectsSubsequentTouches() throws Exception {
|
||||
Fixture fixture = new Fixture();
|
||||
IllegalStateException failure = new IllegalStateException("initial studio selection failed");
|
||||
IllegalStateException closeFailure = new IllegalStateException("engine close failed");
|
||||
doAnswer(invocation -> {
|
||||
throw failure;
|
||||
}).when(fixture.mode).inject(fixture.generator);
|
||||
try (MockedStatic<BukkitChunkGenerator> methods = mockStatic(BukkitChunkGenerator.class, CALLS_REAL_METHODS);
|
||||
MockedConstruction<IrisEngine> engines = mockConstruction(IrisEngine.class, (created, context) -> {
|
||||
when(created.getDimension()).thenReturn(fixture.dimension);
|
||||
doAnswer(invocation -> {
|
||||
throw closeFailure;
|
||||
}).when(created).close();
|
||||
})) {
|
||||
methods.when(() -> BukkitChunkGenerator.shouldRunStudioHotload(true, false, false)).thenReturn(false);
|
||||
|
||||
assertSame(failure, assertThrows(IllegalStateException.class,
|
||||
() -> fixture.generator.touch(mock(World.class))));
|
||||
assertFalse(fixture.setup.get());
|
||||
assertSame(failure, assertThrows(IllegalStateException.class,
|
||||
() -> fixture.generator.touch(mock(World.class))));
|
||||
assertEquals(1, engines.constructed().size());
|
||||
assertEquals(1, failure.getSuppressed().length);
|
||||
assertSame(closeFailure, failure.getSuppressed()[0]);
|
||||
verify(engines.constructed().getFirst()).close();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void productionTerrainIgnoresPackStudioMode() throws Exception {
|
||||
Fixture fixture = new Fixture();
|
||||
field(fixture.generator, "studio", false);
|
||||
|
||||
assertNull(resolve(fixture.generator));
|
||||
assertSame(StudioMode.NORMAL, fixture.generator.getLastMode());
|
||||
verify(fixture.mode, never()).inject(fixture.generator);
|
||||
}
|
||||
|
||||
private static StudioGenerator resolve(BukkitChunkGenerator generator) throws Exception {
|
||||
Method method = BukkitChunkGenerator.class.getDeclaredMethod("computeStudioGenerator");
|
||||
method.setAccessible(true);
|
||||
try {
|
||||
method.invoke(generator);
|
||||
return generator.getStudioGenerator();
|
||||
} catch (InvocationTargetException failure) {
|
||||
if (failure.getCause() instanceof RuntimeException runtimeFailure) {
|
||||
throw runtimeFailure;
|
||||
}
|
||||
throw failure;
|
||||
}
|
||||
}
|
||||
|
||||
private static void field(BukkitChunkGenerator generator, String name, Object value) throws Exception {
|
||||
Field field = BukkitChunkGenerator.class.getDeclaredField(name);
|
||||
field.setAccessible(true);
|
||||
field.set(generator, value);
|
||||
}
|
||||
|
||||
private static final class Fixture {
|
||||
private final BukkitChunkGenerator generator = mock(BukkitChunkGenerator.class, CALLS_REAL_METHODS);
|
||||
private final IrisDimension dimension = mock(IrisDimension.class);
|
||||
private final StudioMode mode = mock(StudioMode.class);
|
||||
private final StudioGenerator selected = mock(StudioGenerator.class);
|
||||
private final AtomicBoolean setup = new AtomicBoolean();
|
||||
|
||||
private Fixture() throws Exception {
|
||||
Engine engine = mock(Engine.class);
|
||||
EngineTarget target = mock(EngineTarget.class);
|
||||
IrisWorld world = mock(IrisWorld.class);
|
||||
when(world.hasPlatformWorld()).thenReturn(true);
|
||||
when(engine.getDimension()).thenReturn(dimension);
|
||||
when(target.getDimension()).thenReturn(dimension);
|
||||
when(dimension.getLoadKey()).thenReturn("studio-selection-test");
|
||||
when(dimension.getStudioMode()).thenReturn(mode);
|
||||
doReturn(target).when(generator).getTarget();
|
||||
field(generator, "studio", true);
|
||||
field(generator, "world", world);
|
||||
field(generator, "setup", setup);
|
||||
field(generator, "lock", new ReentrantLock());
|
||||
field(generator, "targetCache", new AtomicCache<EngineTarget>());
|
||||
field(generator, "populators", new KList<>());
|
||||
field(generator, "studioEntryBootstrapActive", new AtomicBoolean(true));
|
||||
field(generator, "validatedDimension", dimension);
|
||||
field(generator, "validatedWorldContract", new IrisDimensionRuntimeContract("iris:test", 0, 256, 256));
|
||||
generator.setEngine(engine);
|
||||
generator.setLastMode(StudioMode.NORMAL);
|
||||
}
|
||||
}
|
||||
}
|
||||
+154
@@ -1,10 +1,18 @@
|
||||
package art.arcane.iris.engine.platform;
|
||||
|
||||
import art.arcane.iris.core.link.Identifier;
|
||||
import art.arcane.iris.core.loader.IrisData;
|
||||
import art.arcane.iris.engine.IrisEngineMantle;
|
||||
import art.arcane.iris.engine.framework.Engine;
|
||||
import art.arcane.iris.engine.framework.EngineMetrics;
|
||||
import art.arcane.iris.engine.history.SavedBiomeUnavailableException;
|
||||
import art.arcane.iris.engine.object.IrisWorld;
|
||||
import art.arcane.iris.spi.IrisPlatform;
|
||||
import art.arcane.iris.spi.IrisPlatforms;
|
||||
import art.arcane.iris.spi.PlatformBlockState;
|
||||
import art.arcane.iris.spi.PlatformRegistries;
|
||||
import art.arcane.iris.util.project.matter.TileWrapper;
|
||||
import art.arcane.iris.util.project.matter.slices.PreObjectMatterTest;
|
||||
import art.arcane.volmlib.util.function.Consumer4;
|
||||
import art.arcane.volmlib.util.mantle.flag.MantleFlag;
|
||||
import art.arcane.volmlib.util.mantle.runtime.MantleChunk;
|
||||
@@ -22,10 +30,13 @@ import org.mockito.InOrder;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
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.assertNull;
|
||||
import static org.junit.Assert.assertSame;
|
||||
import static org.junit.Assert.assertThrows;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
@@ -35,10 +46,129 @@ import static org.mockito.Mockito.doThrow;
|
||||
import static org.mockito.Mockito.inOrder;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.spy;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
public class EngineBukkitOpsDeferredMaterializationTest {
|
||||
@Test(timeout = 5000)
|
||||
public void loadingRetryPreservesPendingUpdatesWithoutRepeatingCompletedUpdates() {
|
||||
MantleChunk<Matter> mantleChunk = pendingUpdates();
|
||||
IrisWorld irisWorld = mock(IrisWorld.class);
|
||||
when(irisWorld.minHeight()).thenReturn(-64);
|
||||
Engine engine = mock(Engine.class);
|
||||
when(engine.getWorld()).thenReturn(irisWorld);
|
||||
when(engine.getMetrics()).thenReturn(new EngineMetrics(8));
|
||||
Chunk chunk = mock(Chunk.class);
|
||||
AtomicInteger tileRuns = new AtomicInteger();
|
||||
AtomicInteger customRuns = new AtomicInteger();
|
||||
AtomicInteger attempts = new AtomicInteger();
|
||||
List<Integer> completed = new ArrayList<>();
|
||||
SavedBiomeUnavailableException loading = new SavedBiomeUnavailableException("Loading", true);
|
||||
Runnable updates = () -> EngineBukkitOps.materializeUpdates(engine, chunk, mantleChunk, (x, y, z) -> {
|
||||
if (y == -43 && attempts.incrementAndGet() <= 2) {
|
||||
throw loading;
|
||||
}
|
||||
completed.add(y);
|
||||
});
|
||||
Runnable pass = () -> EngineBukkitOps.runMaterializationPasses(mantleChunk,
|
||||
tileRuns::incrementAndGet, customRuns::incrementAndGet, updates);
|
||||
|
||||
for (int retry = 0; retry < 2; retry++) {
|
||||
assertSame(loading, assertThrows(SavedBiomeUnavailableException.class, pass::run));
|
||||
assertEquals(List.of(-59), completed);
|
||||
assertNull(mantleChunk.get(0).getSlice(MatterUpdate.class).get(3, 5, 7));
|
||||
assertTrue(mantleChunk.get(1).getSlice(MatterUpdate.class).get(3, 5, 7).isUpdate());
|
||||
assertTrue(mantleChunk.get(2).getSlice(MatterUpdate.class).get(3, 5, 7).isUpdate());
|
||||
assertTrue(mantleChunk.isFlagged(MantleFlag.TILE));
|
||||
assertTrue(mantleChunk.isFlagged(MantleFlag.CUSTOM));
|
||||
assertFalse(mantleChunk.isFlagged(MantleFlag.UPDATE));
|
||||
assertFalse(mantleChunk.isFlagged(MantleFlag.ETCHED));
|
||||
}
|
||||
|
||||
pass.run();
|
||||
|
||||
assertEquals(List.of(-59, -43, -27), completed);
|
||||
assertEquals(1, tileRuns.get());
|
||||
assertEquals(1, customRuns.get());
|
||||
assertTrue(mantleChunk.isFlagged(MantleFlag.UPDATE));
|
||||
assertTrue(mantleChunk.isFlagged(MantleFlag.ETCHED));
|
||||
for (int section = 0; section < 3; section++) {
|
||||
assertFalse(mantleChunk.get(section).hasSlice(MatterUpdate.class));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void permanentAndUnrelatedFailuresKeepTheirCauseAndPendingMarkers() {
|
||||
for (RuntimeException failure : List.of(
|
||||
new SavedBiomeUnavailableException("Saved biome is missing", false),
|
||||
new IllegalStateException("Broken loot"))) {
|
||||
MantleChunk<Matter> mantleChunk = pendingUpdates();
|
||||
IrisWorld irisWorld = mock(IrisWorld.class);
|
||||
when(irisWorld.minHeight()).thenReturn(-64);
|
||||
Engine engine = mock(Engine.class);
|
||||
when(engine.getWorld()).thenReturn(irisWorld);
|
||||
List<Integer> completed = new ArrayList<>();
|
||||
Runnable updates = () -> EngineBukkitOps.materializeUpdates(engine, mock(Chunk.class), mantleChunk,
|
||||
(x, y, z) -> {
|
||||
if (y == -43) {
|
||||
throw failure;
|
||||
}
|
||||
completed.add(y);
|
||||
});
|
||||
|
||||
assertSame(failure, assertThrows(RuntimeException.class,
|
||||
() -> EngineBukkitOps.runMaterializationPasses(mantleChunk, () -> {}, () -> {}, updates)));
|
||||
|
||||
assertEquals(List.of(-59), completed);
|
||||
assertNull(mantleChunk.get(0).getSlice(MatterUpdate.class).get(3, 5, 7));
|
||||
assertSame(UpdateMatter.ON, mantleChunk.get(1).getSlice(MatterUpdate.class).get(3, 5, 7));
|
||||
assertSame(UpdateMatter.ON, mantleChunk.get(2).getSlice(MatterUpdate.class).get(3, 5, 7));
|
||||
assertTrue(mantleChunk.isFlagged(MantleFlag.TILE));
|
||||
assertTrue(mantleChunk.isFlagged(MantleFlag.CUSTOM));
|
||||
assertFalse(mantleChunk.isFlagged(MantleFlag.UPDATE));
|
||||
assertFalse(mantleChunk.isFlagged(MantleFlag.ETCHED));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void cleanupFailurePreservesLoadingCauseAndUnfinishedFlags() {
|
||||
MantleChunk<Matter> mantleChunk = spy(pendingUpdates());
|
||||
SavedBiomeUnavailableException loading = new SavedBiomeUnavailableException("Loading", true);
|
||||
IllegalStateException cleanup = new IllegalStateException("Marker cleanup failed");
|
||||
AtomicBoolean failed = new AtomicBoolean();
|
||||
doAnswer(invocation -> {
|
||||
if (failed.get()) {
|
||||
throw cleanup;
|
||||
}
|
||||
return invocation.callRealMethod();
|
||||
}).when(mantleChunk).get(0);
|
||||
IrisWorld irisWorld = mock(IrisWorld.class);
|
||||
when(irisWorld.minHeight()).thenReturn(-64);
|
||||
Engine engine = mock(Engine.class);
|
||||
when(engine.getWorld()).thenReturn(irisWorld);
|
||||
List<Integer> completed = new ArrayList<>();
|
||||
Runnable updates = () -> EngineBukkitOps.materializeUpdates(engine, mock(Chunk.class), mantleChunk,
|
||||
(x, y, z) -> {
|
||||
if (y == -43) {
|
||||
failed.set(true);
|
||||
throw loading;
|
||||
}
|
||||
completed.add(y);
|
||||
});
|
||||
|
||||
assertSame(loading, assertThrows(SavedBiomeUnavailableException.class,
|
||||
() -> EngineBukkitOps.runMaterializationPasses(mantleChunk, () -> {}, () -> {}, updates)));
|
||||
|
||||
assertEquals(List.of(-59), completed);
|
||||
assertEquals(1, loading.getSuppressed().length);
|
||||
assertSame(cleanup, loading.getSuppressed()[0]);
|
||||
assertTrue(mantleChunk.isFlagged(MantleFlag.TILE));
|
||||
assertTrue(mantleChunk.isFlagged(MantleFlag.CUSTOM));
|
||||
assertFalse(mantleChunk.isFlagged(MantleFlag.UPDATE));
|
||||
assertFalse(mantleChunk.isFlagged(MantleFlag.ETCHED));
|
||||
}
|
||||
|
||||
@Test
|
||||
@SuppressWarnings("unchecked")
|
||||
public void updateMarkerUsesWorldHeightAndDeletesSliceAfterDispatch() {
|
||||
@@ -167,4 +297,28 @@ public class EngineBukkitOpsDeferredMaterializationTest {
|
||||
|
||||
verify(mantleChunk, never()).deleteSlices(Identifier.class);
|
||||
}
|
||||
|
||||
private static MantleChunk<Matter> pendingUpdates() {
|
||||
PreObjectMatterTest.setUpBukkit();
|
||||
IrisPlatform previous = IrisPlatforms.isBound() ? IrisPlatforms.get() : null;
|
||||
IrisPlatform platform = mock(IrisPlatform.class);
|
||||
PlatformRegistries registries = mock(PlatformRegistries.class);
|
||||
when(registries.block("AIR")).thenReturn(mock(PlatformBlockState.class));
|
||||
when(platform.registries()).thenReturn(registries);
|
||||
IrisPlatforms.unbind();
|
||||
IrisPlatforms.bind(platform);
|
||||
try {
|
||||
MantleChunk<Matter> chunk = new MantleChunk<>(4, 2, -1,
|
||||
IrisEngineMantle.createRuntimeDataAdapter(mock(IrisData.class)), MantleHooks.NONE);
|
||||
for (int section = 0; section < 3; section++) {
|
||||
chunk.getOrCreate(section).slice(MatterUpdate.class).set(3, 5, 7, UpdateMatter.ON);
|
||||
}
|
||||
return chunk;
|
||||
} finally {
|
||||
IrisPlatforms.unbind();
|
||||
if (previous != null) {
|
||||
IrisPlatforms.bind(previous);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+298
@@ -0,0 +1,298 @@
|
||||
package art.arcane.iris.engine.platform.studio.generators;
|
||||
|
||||
import art.arcane.iris.core.loader.IrisData;
|
||||
import art.arcane.iris.core.loader.ResourceLoader;
|
||||
import art.arcane.iris.core.runtime.ObjectStudioLayout;
|
||||
import art.arcane.iris.core.service.ObjectStudioSaveService;
|
||||
import art.arcane.iris.engine.data.chunk.TerrainChunk;
|
||||
import art.arcane.iris.engine.framework.Engine;
|
||||
import art.arcane.iris.engine.framework.EngineTarget;
|
||||
import art.arcane.iris.engine.framework.GenerationSessionLease;
|
||||
import art.arcane.iris.engine.object.IrisObject;
|
||||
import art.arcane.iris.engine.object.IrisWorld;
|
||||
import art.arcane.iris.engine.object.TileData;
|
||||
import art.arcane.iris.spi.PlatformBlockState;
|
||||
import art.arcane.iris.util.common.math.IrisBlockVector;
|
||||
import art.arcane.iris.util.project.matter.slices.PreObjectMatterTest;
|
||||
import art.arcane.volmlib.util.collection.KMap;
|
||||
import io.papermc.paper.InternalAPIBridge;
|
||||
import io.papermc.paper.registry.RegistryAccess;
|
||||
import net.kyori.adventure.key.Key;
|
||||
import org.bukkit.NamespacedKey;
|
||||
import org.bukkit.Registry;
|
||||
import org.bukkit.block.Biome;
|
||||
import org.junit.BeforeClass;
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.junit.rules.TemporaryFolder;
|
||||
import org.mockito.MockedStatic;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.DataInputStream;
|
||||
import java.io.DataOutputStream;
|
||||
import java.io.File;
|
||||
import java.lang.reflect.Constructor;
|
||||
import java.lang.reflect.Proxy;
|
||||
import java.nio.file.Files;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertNotNull;
|
||||
import static org.junit.Assert.assertSame;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.anyInt;
|
||||
import static org.mockito.ArgumentMatchers.anyString;
|
||||
import static org.mockito.Mockito.RETURNS_DEFAULTS;
|
||||
import static org.mockito.Mockito.doAnswer;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.mockStatic;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
public class ObjectStudioGeometryTest {
|
||||
@Rule
|
||||
public final TemporaryFolder temporary = new TemporaryFolder();
|
||||
|
||||
@BeforeClass
|
||||
@SuppressWarnings("unchecked")
|
||||
public static void initializeBukkit() throws Exception {
|
||||
PreObjectMatterTest.setUpBukkit();
|
||||
AtomicReference<Registry<Biome>> registered = new AtomicReference<>();
|
||||
RegistryAccess access = mock(RegistryAccess.class, invocation -> {
|
||||
if (invocation.getMethod().getName().equals("getRegistry")) {
|
||||
if (registered.get() == null) {
|
||||
Registry<Biome> biomes = mock(Registry.class, lookup -> {
|
||||
if (lookup.getMethod().getName().equals("getOrThrow")) {
|
||||
Key key = lookup.getArgument(0);
|
||||
return biome(key);
|
||||
}
|
||||
return RETURNS_DEFAULTS.answer(lookup);
|
||||
});
|
||||
registered.set(biomes);
|
||||
}
|
||||
return registered.get();
|
||||
}
|
||||
return RETURNS_DEFAULTS.answer(invocation);
|
||||
});
|
||||
InternalAPIBridge bridge = mock(InternalAPIBridge.class, invocation -> {
|
||||
if (invocation.getMethod().getName().equals("constructLegacyCustomBiome")) {
|
||||
return biome(Key.key("minecraft:custom"));
|
||||
}
|
||||
return RETURNS_DEFAULTS.answer(invocation);
|
||||
});
|
||||
try (MockedStatic<RegistryAccess> registries = mockStatic(RegistryAccess.class);
|
||||
MockedStatic<InternalAPIBridge> internals = mockStatic(InternalAPIBridge.class)) {
|
||||
registries.when(RegistryAccess::registryAccess).thenReturn(access);
|
||||
internals.when(InternalAPIBridge::get).thenReturn(bridge);
|
||||
Class.forName(ObjectStudioGenerator.class.getName(), true, ObjectStudioGenerator.class.getClassLoader());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void jungleClutterSignedBoundsStayInsideTheEditableCellAboveTheFloor() throws Exception {
|
||||
IrisObject source = filled(5, 3, 4, new Position(-3, -2, -2), new Position(1, -1, 1));
|
||||
Fixture fixture = fixture(source, 64, new Position(14, 65, 14));
|
||||
|
||||
Map<Position, PlatformBlockState> rendered = render(fixture);
|
||||
|
||||
assertPlacement(fixture, rendered, new Position(-3, -2, -2));
|
||||
assertEquals(40, source.getBlocks().size());
|
||||
assertTrue(rendered.containsKey(new Position(14, 65, 14)));
|
||||
assertTrue(rendered.containsKey(new Position(18, 66, 17)));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void oddEvenAndNegativeChunkPlacementPreservesSignedBlocksAndTileBytes() throws Exception {
|
||||
int[][] dimensions = {{5, 3, 4}, {4, 4, 6}, {1, 1, 1}};
|
||||
for (int[] size : dimensions) {
|
||||
Position minimum = new Position(-3, -2, -4);
|
||||
IrisObject source = filled(size[0], size[1], size[2], minimum,
|
||||
new Position(minimum.x() + size[0] - 1, minimum.y() + size[1] - 1,
|
||||
minimum.z() + size[2] - 1));
|
||||
KMap<String, Object> properties = new KMap<>();
|
||||
properties.put("CustomName", "Stored tile");
|
||||
TileData tile = new TileData("minecraft:chest", properties);
|
||||
source.getStates().put(new IrisBlockVector(minimum.x(), minimum.y(), minimum.z()), tile);
|
||||
Fixture fixture = fixture(source, 64, new Position(-18, 65, -17));
|
||||
Map<Position, PlatformBlockState> rendered = render(fixture);
|
||||
assertPlacement(fixture, rendered, minimum);
|
||||
|
||||
IrisObject captured = fixture.generator().createCapture(fixture.cell());
|
||||
for (Map.Entry<Position, PlatformBlockState> entry : rendered.entrySet()) {
|
||||
Position position = entry.getKey();
|
||||
captured.setUnsigned(position.x() - fixture.cell().originX(),
|
||||
position.y() - fixture.cell().originY(), position.z() - fixture.cell().originZ(),
|
||||
entry.getValue());
|
||||
}
|
||||
captured.setUnsignedTile(0, 0, 0, tile);
|
||||
|
||||
assertSame(tile, captured.getStates().get(new IrisBlockVector(minimum.x(), minimum.y(), minimum.z())));
|
||||
assertEquals(serialized(source), serialized(captured));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void highWorldFloorRaisesBothObjectAndCaptureCell() throws Exception {
|
||||
IrisObject source = filled(5, 3, 4, new Position(-3, -2, -2), new Position(1, -1, 1));
|
||||
Fixture fixture = fixture(source, 160, new Position(2, 65, 2));
|
||||
|
||||
Map<Position, PlatformBlockState> rendered = render(fixture);
|
||||
|
||||
assertEquals(161, fixture.cell().originY());
|
||||
assertPlacement(fixture, rendered, new Position(-3, -2, -2));
|
||||
assertTrue(rendered.containsKey(new Position(2, 161, 2)));
|
||||
}
|
||||
|
||||
private static IrisObject filled(int width, int height, int depth, Position minimum, Position maximum) {
|
||||
PlatformBlockState block = mock(PlatformBlockState.class);
|
||||
when(block.key()).thenReturn("minecraft:oak_leaves[persistent=true]");
|
||||
IrisObject object = new IrisObject(width, height, depth);
|
||||
for (int x = minimum.x(); x <= maximum.x(); x++) {
|
||||
for (int y = minimum.y(); y <= maximum.y(); y++) {
|
||||
for (int z = minimum.z(); z <= maximum.z(); z++) {
|
||||
object.getBlocks().put(new IrisBlockVector(x, y, z), block);
|
||||
}
|
||||
}
|
||||
}
|
||||
return object;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private Fixture fixture(IrisObject source, int minHeight, Position origin) throws Exception {
|
||||
File worldFolder = temporary.newFolder();
|
||||
File sourceFolder = temporary.newFolder();
|
||||
File objectFile = new File(sourceFolder, "jungleclutt6.iob");
|
||||
try (DataOutputStream header = new DataOutputStream(Files.newOutputStream(objectFile.toPath()))) {
|
||||
header.writeInt(source.getW());
|
||||
header.writeInt(source.getH());
|
||||
header.writeInt(source.getD());
|
||||
}
|
||||
IrisData data = mock(IrisData.class);
|
||||
ResourceLoader<IrisObject> loader = mock(ResourceLoader.class);
|
||||
when(data.getDataFolder()).thenReturn(sourceFolder);
|
||||
when(data.getObjectLoader()).thenReturn(loader);
|
||||
when(loader.getPossibleKeys()).thenReturn(new String[]{"jungleclutt6"});
|
||||
when(loader.findFile("jungleclutt6")).thenReturn(objectFile);
|
||||
when(loader.load("jungleclutt6")).thenReturn(source);
|
||||
ObjectStudioLayout.GridCell cell = new ObjectStudioLayout.GridCell(sourceFolder.getName(), "jungleclutt6",
|
||||
origin.x(), origin.y(), origin.z(), source.getW(), source.getH(), source.getD());
|
||||
Constructor<ObjectStudioLayout> constructor = ObjectStudioLayout.class.getDeclaredConstructor(
|
||||
int.class, int.class, List.class);
|
||||
constructor.setAccessible(true);
|
||||
constructor.newInstance(2, 160, List.of(cell)).save(new File(worldFolder, ".iris/object-studio-layout.json"));
|
||||
Engine engine = mock(Engine.class);
|
||||
EngineTarget target = mock(EngineTarget.class);
|
||||
IrisWorld world = mock(IrisWorld.class);
|
||||
when(world.worldFolder()).thenReturn(worldFolder);
|
||||
when(target.getWorld()).thenReturn(world);
|
||||
when(engine.getTarget()).thenReturn(target);
|
||||
when(engine.getData()).thenReturn(data);
|
||||
when(engine.getMinHeight()).thenReturn(minHeight);
|
||||
when(engine.getMaxHeight()).thenReturn(320);
|
||||
when(engine.acquireGenerationLease(anyString())).thenAnswer(invocation -> GenerationSessionLease.noop());
|
||||
ObjectStudioGenerator generator = new ObjectStudioGenerator(engine, 2,
|
||||
mock(PlatformBlockState.class), mock(PlatformBlockState.class), mock(PlatformBlockState.class));
|
||||
TerrainChunk initial = mock(TerrainChunk.class);
|
||||
when(initial.getMaxHeight()).thenReturn(320);
|
||||
try (MockedStatic<ObjectStudioSaveService> saves = mockStatic(ObjectStudioSaveService.class)) {
|
||||
saves.when(ObjectStudioSaveService::get).thenReturn(mock(ObjectStudioSaveService.class));
|
||||
generator.generateChunk(engine, initial, cell.chunkMinX(), cell.chunkMinZ());
|
||||
}
|
||||
ObjectStudioLayout.GridCell placed = generator.getLayout().get("jungleclutt6");
|
||||
assertNotNull(placed);
|
||||
return new Fixture(generator, engine, placed, source);
|
||||
}
|
||||
|
||||
private static Map<Position, PlatformBlockState> render(Fixture fixture) throws Exception {
|
||||
Map<Position, PlatformBlockState> rendered = new HashMap<>();
|
||||
PlatformBlockState objectBlock = fixture.source().getBlocks().values().iterator().next();
|
||||
for (int chunkX = fixture.cell().chunkMinX(); chunkX <= fixture.cell().chunkMaxX(); chunkX++) {
|
||||
for (int chunkZ = fixture.cell().chunkMinZ(); chunkZ <= fixture.cell().chunkMaxZ(); chunkZ++) {
|
||||
int originX = chunkX << 4;
|
||||
int originZ = chunkZ << 4;
|
||||
TerrainChunk chunk = mock(TerrainChunk.class);
|
||||
when(chunk.getMaxHeight()).thenReturn(320);
|
||||
doAnswer(invocation -> {
|
||||
int x = invocation.getArgument(0);
|
||||
int y = invocation.getArgument(1);
|
||||
int z = invocation.getArgument(2);
|
||||
assertTrue(x >= 0 && x < 16 && z >= 0 && z < 16);
|
||||
PlatformBlockState block = invocation.getArgument(3);
|
||||
if (block == objectBlock) {
|
||||
rendered.put(new Position(originX + x, y, originZ + z), block);
|
||||
}
|
||||
return null;
|
||||
}).when(chunk).setBlock(anyInt(), anyInt(), anyInt(), any());
|
||||
fixture.generator().generateChunk(fixture.engine(), chunk, chunkX, chunkZ);
|
||||
}
|
||||
}
|
||||
return rendered;
|
||||
}
|
||||
|
||||
private static void assertPlacement(Fixture fixture, Map<Position, PlatformBlockState> rendered, Position minimum) {
|
||||
assertEquals(fixture.source().getBlocks().size(), rendered.size());
|
||||
for (Map.Entry<IrisBlockVector, PlatformBlockState> entry : fixture.source().getBlocks()) {
|
||||
IrisBlockVector signed = entry.getKey();
|
||||
Position expected = new Position(fixture.cell().originX() + signed.getBlockX() - minimum.x(),
|
||||
fixture.cell().originY() + signed.getBlockY() - minimum.y(),
|
||||
fixture.cell().originZ() + signed.getBlockZ() - minimum.z());
|
||||
assertSame(entry.getValue(), rendered.get(expected));
|
||||
}
|
||||
}
|
||||
|
||||
private static SerializedObject serialized(IrisObject object) throws Exception {
|
||||
ByteArrayOutputStream output = new ByteArrayOutputStream();
|
||||
object.write(output);
|
||||
try (DataInputStream input = new DataInputStream(new ByteArrayInputStream(output.toByteArray()))) {
|
||||
int width = input.readInt();
|
||||
int height = input.readInt();
|
||||
int depth = input.readInt();
|
||||
assertEquals("Iris V2 IOB;", input.readUTF());
|
||||
String[] palette = new String[input.readShort()];
|
||||
for (int index = 0; index < palette.length; index++) {
|
||||
palette[index] = input.readUTF();
|
||||
}
|
||||
int blockCount = input.readInt();
|
||||
Map<Position, String> blocks = new HashMap<>();
|
||||
for (int index = 0; index < blockCount; index++) {
|
||||
Position position = new Position(input.readShort(), input.readShort(), input.readShort());
|
||||
blocks.put(position, palette[input.readShort()]);
|
||||
}
|
||||
int tileCount = input.readInt();
|
||||
Map<Position, TileData> tiles = new HashMap<>();
|
||||
for (int index = 0; index < tileCount; index++) {
|
||||
Position position = new Position(input.readShort(), input.readShort(), input.readShort());
|
||||
tiles.put(position, TileData.read(input));
|
||||
}
|
||||
assertEquals(-1, input.read());
|
||||
return new SerializedObject(width, height, depth, blocks, tiles);
|
||||
}
|
||||
}
|
||||
|
||||
private static Biome biome(Key key) {
|
||||
NamespacedKey namespaced = new NamespacedKey(key.namespace(), key.value());
|
||||
return (Biome) Proxy.newProxyInstance(Biome.class.getClassLoader(), new Class<?>[]{Biome.class},
|
||||
(proxy, method, arguments) -> switch (method.getName()) {
|
||||
case "getKey" -> namespaced;
|
||||
case "hashCode" -> namespaced.hashCode();
|
||||
case "equals" -> proxy == arguments[0];
|
||||
case "toString", "name" -> key.value();
|
||||
default -> null;
|
||||
});
|
||||
}
|
||||
|
||||
private record Position(int x, int y, int z) {
|
||||
}
|
||||
|
||||
private record Fixture(ObjectStudioGenerator generator, Engine engine, ObjectStudioLayout.GridCell cell,
|
||||
IrisObject source) {
|
||||
}
|
||||
|
||||
private record SerializedObject(int width, int height, int depth, Map<Position, String> blocks,
|
||||
Map<Position, TileData> tiles) {
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user