mirror of
https://github.com/VolmitSoftware/Iris.git
synced 2026-10-11 19:14:11 +00:00
Fixed pregen runtime bottlenecks
- Generation routing reduces metadata lock contention while retaining lifecycle ownership - Native biome wrappers avoid repeated reflection and key allocation - Studio scoreboards recover from pending saved biome reads with cave overrides intact - Pregeneration previews retain saved definitions on a bounded renderer worker - Full build and runtime checks cover scoreboard recovery and Leaf pregeneration
This commit is contained in:
@@ -33,6 +33,7 @@ import art.arcane.iris.core.pregenerator.PregenRates;
|
||||
import art.arcane.iris.core.pregenerator.PregenTask;
|
||||
import art.arcane.iris.core.pregenerator.PregeneratorMethod;
|
||||
import art.arcane.iris.engine.framework.Engine;
|
||||
import art.arcane.iris.engine.history.SavedBiomeUnavailableException;
|
||||
import art.arcane.volmlib.util.format.Form;
|
||||
import art.arcane.volmlib.util.format.MemoryMonitor;
|
||||
import art.arcane.volmlib.util.mantle.runtime.Mantle;
|
||||
@@ -492,18 +493,7 @@ public class PregeneratorJob implements PregenListener, PregenRenderSource {
|
||||
|
||||
@Override
|
||||
public void onChunkGenerated(int x, int z, boolean cached) {
|
||||
if (renderer == null || !renderer.isVisibleFrame()) return;
|
||||
if (service.isShutdown()) {
|
||||
return;
|
||||
}
|
||||
service.execute(() -> {
|
||||
if (engine != null) {
|
||||
draw(x, z, engine.draw((x << 4) + 8, (z << 4) + 8));
|
||||
return;
|
||||
}
|
||||
|
||||
draw(x, z, COLOR_GENERATED);
|
||||
});
|
||||
drawChunkPreview(x, z, COLOR_GENERATED);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -578,12 +568,7 @@ public class PregeneratorJob implements PregenListener, PregenRenderSource {
|
||||
|
||||
@Override
|
||||
public void onChunkExistsInRegionGen(int x, int z) {
|
||||
if (engine != null) {
|
||||
draw(x, z, engine.draw((x << 4) + 8, (z << 4) + 8));
|
||||
return;
|
||||
}
|
||||
|
||||
draw(x, z, COLOR_EXISTS);
|
||||
drawChunkPreview(x, z, COLOR_EXISTS);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -606,6 +591,35 @@ public class PregeneratorJob implements PregenListener, PregenRenderSource {
|
||||
return info;
|
||||
}
|
||||
|
||||
private void drawChunkPreview(int x, int z, Color statusColor) {
|
||||
PregenRenderer activeRenderer = renderer;
|
||||
if (activeRenderer == null || !activeRenderer.isVisibleFrame() || service.isShutdown()) {
|
||||
return;
|
||||
}
|
||||
draw(x, z, statusColor);
|
||||
if (engine != null) {
|
||||
service.execute(() -> renderChunkPreview(x, z));
|
||||
}
|
||||
}
|
||||
|
||||
private void renderChunkPreview(int x, int z) {
|
||||
PregenRenderer activeRenderer = renderer;
|
||||
if (activeRenderer == null || !activeRenderer.isVisibleFrame() || service.isShutdown() || engine.isClosing()) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
draw(x, z, engine.drawForPreview((x << 4) + 8, (z << 4) + 8));
|
||||
} catch (InterruptedException interruption) {
|
||||
Thread.currentThread().interrupt();
|
||||
} catch (SavedBiomeUnavailableException unavailable) {
|
||||
if (!unavailable.isLoading()) {
|
||||
IrisLogging.reportError("Unable to draw saved biome information for chunk " + x + "," + z + ".", unavailable);
|
||||
}
|
||||
} catch (RuntimeException failure) {
|
||||
IrisLogging.reportError("Unable to draw the pregeneration preview for chunk " + x + "," + z + ".", failure);
|
||||
}
|
||||
}
|
||||
|
||||
public record Configuration(
|
||||
PregenTask task,
|
||||
PregeneratorMethod method,
|
||||
|
||||
@@ -35,6 +35,14 @@ public final class BukkitUiMessages {
|
||||
"iris.bukkit.scoreboard.biome",
|
||||
C.AQUA + "Biome" + C.GRAY + ": {biome}"
|
||||
);
|
||||
public static final TextKey SCOREBOARD_BIOME_LOADING = TextKey.of(
|
||||
"iris.bukkit.scoreboard.biome_loading",
|
||||
"Loading"
|
||||
);
|
||||
public static final TextKey SCOREBOARD_BIOME_UNAVAILABLE = TextKey.of(
|
||||
"iris.bukkit.scoreboard.biome_unavailable",
|
||||
"Unavailable"
|
||||
);
|
||||
public static final TextKey SCOREBOARD_HEIGHT = TextKey.of(
|
||||
"iris.bukkit.scoreboard.height",
|
||||
C.AQUA + "Height" + C.GRAY + ": {height}"
|
||||
@@ -56,6 +64,8 @@ public final class BukkitUiMessages {
|
||||
SCOREBOARD_CARVING,
|
||||
SCOREBOARD_REGION,
|
||||
SCOREBOARD_BIOME,
|
||||
SCOREBOARD_BIOME_LOADING,
|
||||
SCOREBOARD_BIOME_UNAVAILABLE,
|
||||
SCOREBOARD_HEIGHT,
|
||||
SCOREBOARD_SLOPE,
|
||||
SCOREBOARD_BLOCK_UPDATES
|
||||
|
||||
@@ -25,7 +25,9 @@ import art.arcane.iris.spi.IrisLogging;
|
||||
import art.arcane.iris.core.IrisSettings;
|
||||
import art.arcane.iris.core.loader.IrisData;
|
||||
import art.arcane.iris.core.tools.IrisToolbelt;
|
||||
import art.arcane.iris.engine.framework.BiomeEnvironment;
|
||||
import art.arcane.iris.engine.framework.Engine;
|
||||
import art.arcane.iris.engine.history.SavedBiomeUnavailableException;
|
||||
import art.arcane.iris.engine.platform.PlatformChunkGenerator;
|
||||
import art.arcane.volmlib.util.board.Board;
|
||||
import art.arcane.volmlib.util.board.BoardProvider;
|
||||
@@ -358,6 +360,8 @@ public class BoardSVC implements IrisService, BoardProvider {
|
||||
private volatile BoardView view;
|
||||
private volatile boolean cancelled;
|
||||
private volatile boolean ordinaryTickScheduled;
|
||||
private UUID ordinaryWorldId;
|
||||
private BiomeFailureContext reportedBiomeFailure;
|
||||
|
||||
public PlayerBoard(Player player) {
|
||||
this.player = player;
|
||||
@@ -377,10 +381,12 @@ public class BoardSVC implements IrisService, BoardProvider {
|
||||
yieldBoard(player, this);
|
||||
return;
|
||||
}
|
||||
boolean switched = view != BoardView.ORDINARY;
|
||||
UUID worldId = player.getWorld().getUID();
|
||||
boolean switched = view != BoardView.ORDINARY || !worldId.equals(ordinaryWorldId);
|
||||
view = BoardView.ORDINARY;
|
||||
jigsawContext = null;
|
||||
if (switched) {
|
||||
ordinaryWorldId = worldId;
|
||||
updateOrdinary();
|
||||
board.update();
|
||||
}
|
||||
@@ -515,14 +521,42 @@ public class BoardSVC implements IrisService, BoardProvider {
|
||||
}
|
||||
|
||||
lines.add("&7&m ");
|
||||
lines.add(IrisLanguage.text(BukkitUiMessages.SCOREBOARD_REGION, MessageArgument.untrusted("region", engine.getRegion(x, y, z).getName())));
|
||||
lines.add(IrisLanguage.text(BukkitUiMessages.SCOREBOARD_BIOME, MessageArgument.untrusted("biome", engine.getBiomeOrMantle(x, y, z).getName())));
|
||||
String regionName;
|
||||
String biomeName;
|
||||
try {
|
||||
BiomeEnvironment environment = engine.getBiomeOrMantleEnvironment(x, y, z);
|
||||
regionName = environment.region().getName();
|
||||
biomeName = environment.biome().getName();
|
||||
reportedBiomeFailure = null;
|
||||
} catch (SavedBiomeUnavailableException failure) {
|
||||
regionName = IrisLanguage.text(failure.isLoading()
|
||||
? BukkitUiMessages.SCOREBOARD_BIOME_LOADING
|
||||
: BukkitUiMessages.SCOREBOARD_BIOME_UNAVAILABLE);
|
||||
biomeName = regionName;
|
||||
if (!failure.isLoading()) {
|
||||
reportBiomeFailure(failure, engine, loc);
|
||||
}
|
||||
}
|
||||
lines.add(IrisLanguage.text(BukkitUiMessages.SCOREBOARD_REGION, MessageArgument.untrusted("region", regionName)));
|
||||
lines.add(IrisLanguage.text(BukkitUiMessages.SCOREBOARD_BIOME, MessageArgument.untrusted("biome", biomeName)));
|
||||
lines.add(IrisLanguage.text(BukkitUiMessages.SCOREBOARD_HEIGHT, MessageArgument.trusted("height", Math.round(engine.getHeight(x, z)))));
|
||||
lines.add(IrisLanguage.text(BukkitUiMessages.SCOREBOARD_SLOPE, MessageArgument.trusted("slope", Form.f(engine.getSlope(x, z), 2))));
|
||||
lines.add(IrisLanguage.text(BukkitUiMessages.SCOREBOARD_BLOCK_UPDATES, MessageArgument.trusted("updates", Form.f(engine.getBlockUpdatesPerSecond()))));
|
||||
lines.add("&7&m ");
|
||||
this.lines = lines;
|
||||
}
|
||||
|
||||
private void reportBiomeFailure(SavedBiomeUnavailableException failure, Engine engine, Location location) {
|
||||
World world = Objects.requireNonNull(location.getWorld());
|
||||
BiomeFailureContext context = new BiomeFailureContext(world.getUID(), engine,
|
||||
location.getBlockX() >> 4, location.getBlockZ() >> 4, failure.getMessage());
|
||||
if (context.equals(reportedBiomeFailure)) {
|
||||
return;
|
||||
}
|
||||
reportedBiomeFailure = context;
|
||||
IrisLogging.reportError("Unable to display saved Iris biome information in " + world.getName()
|
||||
+ " at " + location.getBlockX() + ", " + location.getBlockY() + ", " + location.getBlockZ() + ".", failure);
|
||||
}
|
||||
}
|
||||
|
||||
private void yieldBoard(Player player, PlayerBoard playerBoard) {
|
||||
@@ -536,4 +570,7 @@ public class BoardSVC implements IrisService, BoardProvider {
|
||||
ORDINARY,
|
||||
JIGSAW
|
||||
}
|
||||
|
||||
private record BiomeFailureContext(UUID worldId, Engine engine, int chunkX, int chunkZ, String reason) {
|
||||
}
|
||||
}
|
||||
|
||||
@@ -77,6 +77,7 @@ import lombok.Data;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
|
||||
import java.awt.Color;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Path;
|
||||
import java.util.Collections;
|
||||
@@ -547,6 +548,26 @@ public class IrisEngine implements Engine {
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Color drawForPreview(int x, int z) throws InterruptedException {
|
||||
try (GenerationSessionLease lease = acquireGenerationLease("pregen_preview")) {
|
||||
if (!usesSavedBiomeEnvironment()) {
|
||||
return Engine.super.draw(x, z);
|
||||
}
|
||||
return generationHistoryRuntimeRouter.biomes().readSurfaceBiome(x, z, saved -> {
|
||||
if (saved.isPresent()) {
|
||||
return drawBiomeEnvironment(x, z, saved.get());
|
||||
}
|
||||
try (GenerationHistoryRuntimeRouter.CoordinateScope ignored =
|
||||
openGenerationHistoryCoordinateScopeUnchecked(x, z, "draw a pregeneration preview")) {
|
||||
return Engine.super.draw(x, z);
|
||||
}
|
||||
});
|
||||
} catch (GenerationSessionException failure) {
|
||||
throw new IllegalStateException("Pregeneration preview was rejected by the Iris lifecycle.", failure);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public BiomeEnvironment getBiomeEnvironment(int x, int y, int z) {
|
||||
Optional<BiomeEnvironment> saved = resolveSavedBiomeEnvironment(x, y, z, false);
|
||||
@@ -559,6 +580,18 @@ public class IrisEngine implements Engine {
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public BiomeEnvironment getBiomeOrMantleEnvironment(int x, int y, int z) {
|
||||
Optional<BiomeEnvironment> saved = resolveSavedBiomeEnvironment(x, y, z, false);
|
||||
if (saved.isPresent()) {
|
||||
return saved.get();
|
||||
}
|
||||
try (GenerationHistoryRuntimeRouter.CoordinateScope ignored =
|
||||
openGenerationHistoryCoordinateScopeUnchecked(x, z, "resolve a mantle biome environment")) {
|
||||
return Engine.super.getBiomeOrMantleEnvironment(x, y, z);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public BiomeEnvironment getSurfaceBiomeEnvironment(int x, int z) {
|
||||
Optional<BiomeEnvironment> saved = resolveSavedBiomeEnvironment(x, 0, z, true);
|
||||
|
||||
@@ -97,6 +97,10 @@ public interface Engine extends DataProvider, Fallible, BlockUpdater, Renderer,
|
||||
return new BiomeEnvironment(0L, getBiome(x, y, z), getRegion(x, y, z), getDimension(), getData());
|
||||
}
|
||||
|
||||
default BiomeEnvironment getBiomeOrMantleEnvironment(int x, int y, int z) {
|
||||
return new BiomeEnvironment(0L, getBiomeOrMantle(x, y, z), getRegion(x, y, z), getDimension(), getData());
|
||||
}
|
||||
|
||||
default BiomeEnvironment getSurfaceBiomeEnvironment(int x, int z) {
|
||||
return new BiomeEnvironment(0L, getSurfaceBiome(x, z), getRegion(x, z), getDimension(), getData());
|
||||
}
|
||||
@@ -262,11 +266,21 @@ public interface Engine extends DataProvider, Fallible, BlockUpdater, Renderer,
|
||||
|
||||
@BlockCoordinates
|
||||
default Color draw(double x, double z) {
|
||||
IrisRegion region = getRegion((int) x, (int) z);
|
||||
IrisBiome biome = getSurfaceBiome((int) x, (int) z);
|
||||
int height = getHeight((int) x, (int) z);
|
||||
return drawBiomeEnvironment((int) x, (int) z, getSurfaceBiomeEnvironment((int) x, (int) z));
|
||||
}
|
||||
|
||||
@BlockCoordinates
|
||||
default Color drawForPreview(int x, int z) throws InterruptedException {
|
||||
return draw(x, z);
|
||||
}
|
||||
|
||||
@BlockCoordinates
|
||||
default Color drawBiomeEnvironment(int x, int z, BiomeEnvironment environment) {
|
||||
IrisRegion region = environment.region();
|
||||
IrisBiome biome = environment.biome();
|
||||
int height = getHeight(x, z);
|
||||
double heightFactor = M.lerpInverse(0, getTarget().getHeight(), height);
|
||||
Color irc = region.getColor(this.getComplex(), RenderType.BIOME);
|
||||
Color irc = region.getColor(environment::data, RenderType.BIOME);
|
||||
Color ibc = biome.getColor(this, RenderType.BIOME);
|
||||
Color rc = irc != null ? irc : Color.GREEN.darker();
|
||||
Color bc = ibc != null ? ibc : biome.isAquatic() ? Color.BLUE : Color.YELLOW;
|
||||
|
||||
+7
-1
@@ -51,7 +51,7 @@ public final class GenerationHistoryRuntimeRouter implements AutoCloseable {
|
||||
this.runtimeFactory = Objects.requireNonNull(runtimeFactory, "activation runtime factory");
|
||||
this.bindings = new LinkedHashMap<>();
|
||||
this.retiringBindings = new LinkedHashMap<>();
|
||||
this.stateLock = new ReentrantLock(true);
|
||||
this.stateLock = new ReentrantLock();
|
||||
this.inactive = stateLock.newCondition();
|
||||
this.operationDepth = ThreadLocal.withInitial(() -> 0);
|
||||
this.scopedRoute = new ThreadLocal<>();
|
||||
@@ -449,6 +449,9 @@ public final class GenerationHistoryRuntimeRouter implements AutoCloseable {
|
||||
loader = true;
|
||||
}
|
||||
entry.leases++;
|
||||
if (!entry.loading && entry.loadFailure == null && entry.binding != null) {
|
||||
return new RuntimeLease(this, entry, entry.binding);
|
||||
}
|
||||
} finally {
|
||||
stateLock.unlock();
|
||||
}
|
||||
@@ -584,6 +587,9 @@ public final class GenerationHistoryRuntimeRouter implements AutoCloseable {
|
||||
}
|
||||
|
||||
private List<RuntimeRetirement> collectEvictionsLocked() {
|
||||
if (bindings.size() <= 1) {
|
||||
return List.of();
|
||||
}
|
||||
ArrayList<RuntimeRetirement> retired = new ArrayList<>();
|
||||
while (bindings.size() > 1) {
|
||||
RuntimeCacheEntry candidate = null;
|
||||
|
||||
@@ -20,10 +20,14 @@ import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.locks.ReentrantReadWriteLock;
|
||||
import java.util.function.Function;
|
||||
|
||||
public final class SavedBiomeRuntime implements AutoCloseable {
|
||||
private static final int MAXIMUM_QUERIES = 128;
|
||||
@@ -36,6 +40,7 @@ public final class SavedBiomeRuntime implements AutoCloseable {
|
||||
private final Map<String, Definitions> definitions = new ConcurrentHashMap<>();
|
||||
private final LinkedHashMap<Long, PreparedQuery> queries = new LinkedHashMap<>(32, 0.75F, true);
|
||||
private final Map<Long, PendingQuery> pending = new HashMap<>();
|
||||
private final ReentrantReadWriteLock consumption = new ReentrantReadWriteLock();
|
||||
private long queryBytes;
|
||||
private final ExecutorService reads = Executors.newFixedThreadPool(2, task -> {
|
||||
Thread thread = new Thread(task, "Iris Saved Biomes");
|
||||
@@ -54,6 +59,31 @@ public final class SavedBiomeRuntime implements AutoCloseable {
|
||||
return resolve(blockX, worldY, blockZ, surface ? QueryKind.SURFACE : QueryKind.VOLUME);
|
||||
}
|
||||
|
||||
public <T> T readSurfaceBiome(int blockX, int blockZ, Function<Optional<BiomeEnvironment>, T> reader)
|
||||
throws InterruptedException {
|
||||
Objects.requireNonNull(reader, "reader");
|
||||
PreparedQuery prepared;
|
||||
try {
|
||||
prepared = queryAsync(Math.floorDiv(blockX, 16), Math.floorDiv(blockZ, 16)).get();
|
||||
} catch (ExecutionException failure) {
|
||||
Throwable cause = failure.getCause();
|
||||
if (cause instanceof RuntimeException runtimeFailure) {
|
||||
throw runtimeFailure;
|
||||
}
|
||||
if (cause instanceof Error error) {
|
||||
throw error;
|
||||
}
|
||||
throw new IllegalStateException("Unable to prepare a saved biome preview.", cause);
|
||||
}
|
||||
consumption.readLock().lockInterruptibly();
|
||||
try {
|
||||
requireOpen();
|
||||
return reader.apply(resolve(prepared, blockX, 0, blockZ, QueryKind.SURFACE));
|
||||
} finally {
|
||||
consumption.readLock().unlock();
|
||||
}
|
||||
}
|
||||
|
||||
public NativeBiomeSpawnSelection nativeSpawnSelection(int blockX, int worldY, int blockZ, String physicalBiomeKey) {
|
||||
PreparedQuery prepared = query(Math.floorDiv(blockX, 16), Math.floorDiv(blockZ, 16));
|
||||
if (prepared.failure() != null) {
|
||||
@@ -89,7 +119,10 @@ public final class SavedBiomeRuntime implements AutoCloseable {
|
||||
private Optional<BiomeEnvironment> resolve(int blockX, int worldY, int blockZ, QueryKind kind) {
|
||||
int chunkX = Math.floorDiv(blockX, 16);
|
||||
int chunkZ = Math.floorDiv(blockZ, 16);
|
||||
PreparedQuery query = query(chunkX, chunkZ);
|
||||
return resolve(query(chunkX, chunkZ), blockX, worldY, blockZ, kind);
|
||||
}
|
||||
|
||||
private Optional<BiomeEnvironment> resolve(PreparedQuery query, int blockX, int worldY, int blockZ, QueryKind kind) {
|
||||
if (query.failure() != null) {
|
||||
throw query.failure();
|
||||
}
|
||||
@@ -184,22 +217,27 @@ public final class SavedBiomeRuntime implements AutoCloseable {
|
||||
pending.clear();
|
||||
queryBytes = 0L;
|
||||
}
|
||||
for (Definitions source : definitions.values()) {
|
||||
try {
|
||||
consumption.writeLock().lock();
|
||||
try {
|
||||
for (Definitions source : definitions.values()) {
|
||||
try {
|
||||
source.data().unregisterEngine(engine);
|
||||
} finally {
|
||||
source.data().close();
|
||||
}
|
||||
} catch (Throwable closeFailure) {
|
||||
if (failure == null) {
|
||||
failure = closeFailure;
|
||||
} else {
|
||||
failure.addSuppressed(closeFailure);
|
||||
try {
|
||||
source.data().unregisterEngine(engine);
|
||||
} finally {
|
||||
source.data().close();
|
||||
}
|
||||
} catch (Throwable closeFailure) {
|
||||
if (failure == null) {
|
||||
failure = closeFailure;
|
||||
} else {
|
||||
failure.addSuppressed(closeFailure);
|
||||
}
|
||||
}
|
||||
}
|
||||
definitions.clear();
|
||||
} finally {
|
||||
consumption.writeLock().unlock();
|
||||
}
|
||||
definitions.clear();
|
||||
if (failure != null) {
|
||||
throw new IllegalStateException("Unable to close saved biome definitions", failure);
|
||||
}
|
||||
@@ -229,20 +267,41 @@ public final class SavedBiomeRuntime implements AutoCloseable {
|
||||
}
|
||||
|
||||
private PreparedQuery query(int chunkX, int chunkZ) {
|
||||
synchronized (queries) {
|
||||
requireOpen();
|
||||
PreparedQuery cached = queries.get(key(chunkX, chunkZ));
|
||||
if (cached != null) {
|
||||
return cached;
|
||||
}
|
||||
queryAsync(chunkX, chunkZ);
|
||||
}
|
||||
throw loading(chunkX, chunkZ);
|
||||
}
|
||||
|
||||
private CompletableFuture<PreparedQuery> queryAsync(int chunkX, int chunkZ) {
|
||||
long key = key(chunkX, chunkZ);
|
||||
synchronized (queries) {
|
||||
requireOpen();
|
||||
PreparedQuery cached = queries.get(key);
|
||||
if (cached != null) {
|
||||
return cached;
|
||||
return CompletableFuture.completedFuture(cached);
|
||||
}
|
||||
if (!pending.containsKey(key) && pending.size() < MAXIMUM_PENDING_QUERIES) {
|
||||
PendingQuery loading = new PendingQuery();
|
||||
pending.put(key, loading);
|
||||
reads.execute(() -> readQuery(chunkX, chunkZ, loading));
|
||||
PendingQuery loading = pending.get(key);
|
||||
if (loading != null) {
|
||||
return loading.result;
|
||||
}
|
||||
if (pending.size() >= MAXIMUM_PENDING_QUERIES) {
|
||||
return CompletableFuture.failedFuture(loading(chunkX, chunkZ));
|
||||
}
|
||||
PendingQuery requested = new PendingQuery();
|
||||
pending.put(key, requested);
|
||||
reads.execute(() -> readQuery(chunkX, chunkZ, requested));
|
||||
return requested.result;
|
||||
}
|
||||
throw new SavedBiomeUnavailableException("Saved biome information is loading at chunk "
|
||||
}
|
||||
|
||||
private static SavedBiomeUnavailableException loading(int chunkX, int chunkZ) {
|
||||
return new SavedBiomeUnavailableException("Saved biome information is loading at chunk "
|
||||
+ chunkX + "," + chunkZ + ". Try again shortly.", true);
|
||||
}
|
||||
|
||||
@@ -269,11 +328,28 @@ public final class SavedBiomeRuntime implements AutoCloseable {
|
||||
}
|
||||
prepared = new PreparedQuery(Optional.empty(), Map.of(), Map.of(), new SavedBiomeUnavailableException(
|
||||
"Unable to read the saved biome at chunk " + chunkX + "," + chunkZ + ".", failure), 512L);
|
||||
} catch (Error failure) {
|
||||
loading.result.completeExceptionally(failure);
|
||||
throw failure;
|
||||
} finally {
|
||||
boolean refresh;
|
||||
synchronized (queries) {
|
||||
pending.remove(key, loading);
|
||||
if (!closed && !loading.invalidated && prepared != null) {
|
||||
cacheQuery(key, prepared);
|
||||
refresh = !closed && loading.invalidated;
|
||||
if (refresh) {
|
||||
loading.invalidated = false;
|
||||
reads.execute(() -> readQuery(chunkX, chunkZ, loading));
|
||||
} else {
|
||||
pending.remove(key, loading);
|
||||
if (!closed && prepared != null) {
|
||||
cacheQuery(key, prepared);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!refresh) {
|
||||
if (closed || prepared == null) {
|
||||
loading.result.completeExceptionally(new IllegalStateException("Saved biome runtime is closed."));
|
||||
} else {
|
||||
loading.result.complete(prepared);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -399,6 +475,7 @@ public final class SavedBiomeRuntime implements AutoCloseable {
|
||||
}
|
||||
|
||||
private static final class PendingQuery {
|
||||
private final CompletableFuture<PreparedQuery> result = new CompletableFuture<>();
|
||||
private boolean invalidated;
|
||||
}
|
||||
|
||||
|
||||
@@ -19,43 +19,33 @@
|
||||
package art.arcane.iris.platform.bukkit;
|
||||
|
||||
import art.arcane.iris.spi.PlatformBiome;
|
||||
import com.github.benmanes.caffeine.cache.Cache;
|
||||
import com.github.benmanes.caffeine.cache.Caffeine;
|
||||
import org.bukkit.NamespacedKey;
|
||||
import org.bukkit.block.Biome;
|
||||
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
/**
|
||||
* Interned Bukkit adapter for a neutral biome handle.
|
||||
*/
|
||||
public final class BukkitBiome implements PlatformBiome {
|
||||
private static final ConcurrentHashMap<String, BukkitBiome> CACHE = new ConcurrentHashMap<>();
|
||||
private static final Cache<Biome, BukkitBiome> CACHE = Caffeine.newBuilder()
|
||||
.weakKeys()
|
||||
.maximumSize(4_096)
|
||||
.build();
|
||||
|
||||
private final Biome biome;
|
||||
private final String key;
|
||||
private final String namespace;
|
||||
|
||||
private BukkitBiome(Biome biome, String key) {
|
||||
private BukkitBiome(Biome biome) {
|
||||
this.biome = biome;
|
||||
this.key = key;
|
||||
int colon = key.indexOf(':');
|
||||
this.namespace = colon >= 0 ? key.substring(0, colon) : "minecraft";
|
||||
NamespacedKey biomeKey = biome.getKey();
|
||||
this.key = biomeKey.toString();
|
||||
this.namespace = biomeKey.getNamespace();
|
||||
}
|
||||
|
||||
public static BukkitBiome of(Biome biome) {
|
||||
String key = biomeKey(biome);
|
||||
return CACHE.computeIfAbsent(key, (String k) -> new BukkitBiome(biome, k));
|
||||
}
|
||||
|
||||
private static String biomeKey(Biome biome) {
|
||||
for (String method : new String[]{"getKeyOrNull", "getKeyOrThrow", "getKey"}) {
|
||||
try {
|
||||
Object key = Biome.class.getMethod(method).invoke(biome);
|
||||
if (key != null) {
|
||||
return key.toString();
|
||||
}
|
||||
} catch (Throwable ignored) {
|
||||
}
|
||||
}
|
||||
return biome.toString();
|
||||
return CACHE.get(biome, BukkitBiome::new);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -1358,6 +1358,8 @@
|
||||
"iris.bukkit.scoreboard.carving": "§dCarving§7: {state}",
|
||||
"iris.bukkit.scoreboard.region": "§bRegion§7: {region}",
|
||||
"iris.bukkit.scoreboard.biome": "§bBiom§7: {biome}",
|
||||
"iris.bukkit.scoreboard.biome_loading": "Wird geladen",
|
||||
"iris.bukkit.scoreboard.biome_unavailable": "Nicht verfügbar",
|
||||
"iris.bukkit.scoreboard.height": "§bHöhe§7: {height}",
|
||||
"iris.bukkit.scoreboard.slope": "§bSteigung§7: {slope}",
|
||||
"iris.bukkit.scoreboard.block_updates": "§bBUD/s§7: {updates}",
|
||||
|
||||
@@ -1358,6 +1358,8 @@
|
||||
"iris.bukkit.scoreboard.carving": "§dTallado§7: {state}",
|
||||
"iris.bukkit.scoreboard.region": "§bRegión§7: {region}",
|
||||
"iris.bukkit.scoreboard.biome": "§bBioma§7: {biome}",
|
||||
"iris.bukkit.scoreboard.biome_loading": "Cargando",
|
||||
"iris.bukkit.scoreboard.biome_unavailable": "No disponible",
|
||||
"iris.bukkit.scoreboard.height": "§bAltura§7: {height}",
|
||||
"iris.bukkit.scoreboard.slope": "§bPendiente§7: {slope}",
|
||||
"iris.bukkit.scoreboard.block_updates": "§bBUD/s§7: {updates}",
|
||||
|
||||
@@ -1358,6 +1358,8 @@
|
||||
"iris.bukkit.scoreboard.carving": "§dKaiverrus§7: {state}",
|
||||
"iris.bukkit.scoreboard.region": "§bAlue§7: {region}",
|
||||
"iris.bukkit.scoreboard.biome": "§bBiometri§7: {biome}",
|
||||
"iris.bukkit.scoreboard.biome_loading": "Ladataan",
|
||||
"iris.bukkit.scoreboard.biome_unavailable": "Ei saatavilla",
|
||||
"iris.bukkit.scoreboard.height": "§bKorkeus§7: {height}",
|
||||
"iris.bukkit.scoreboard.slope": "§bRivi§7: {slope}",
|
||||
"iris.bukkit.scoreboard.block_updates": "§bBUDName/s§7: {updates}",
|
||||
|
||||
@@ -1358,6 +1358,8 @@
|
||||
"iris.bukkit.scoreboard.carving": "§dSculpture§7: {state}",
|
||||
"iris.bukkit.scoreboard.region": "§bRégion§7: {region}",
|
||||
"iris.bukkit.scoreboard.biome": "§bBiome§7: {biome}",
|
||||
"iris.bukkit.scoreboard.biome_loading": "Chargement",
|
||||
"iris.bukkit.scoreboard.biome_unavailable": "Indisponible",
|
||||
"iris.bukkit.scoreboard.height": "§bHauteur§7: {height}",
|
||||
"iris.bukkit.scoreboard.slope": "§bPente§7: {slope}",
|
||||
"iris.bukkit.scoreboard.block_updates": "§bBUD/s§7: {updates}",
|
||||
|
||||
@@ -1358,6 +1358,8 @@
|
||||
"iris.bukkit.scoreboard.carving": "§dעבודה§7: {state}",
|
||||
"iris.bukkit.scoreboard.region": "§bאזור§7: {region}",
|
||||
"iris.bukkit.scoreboard.biome": "§bביומי§7: {biome}",
|
||||
"iris.bukkit.scoreboard.biome_loading": "בטעינה",
|
||||
"iris.bukkit.scoreboard.biome_unavailable": "לא זמין",
|
||||
"iris.bukkit.scoreboard.height": "§bגובה§7: {height}",
|
||||
"iris.bukkit.scoreboard.slope": "§bסלייפ§7: {slope}",
|
||||
"iris.bukkit.scoreboard.block_updates": "§bBUD/s§7: {updates}",
|
||||
|
||||
@@ -1358,6 +1358,8 @@
|
||||
"iris.bukkit.scoreboard.carving": "§dCarving§7: {state}",
|
||||
"iris.bukkit.scoreboard.region": "§bRegione§7: {region}",
|
||||
"iris.bukkit.scoreboard.biome": "§bBioma§7: {biome}",
|
||||
"iris.bukkit.scoreboard.biome_loading": "Caricamento",
|
||||
"iris.bukkit.scoreboard.biome_unavailable": "Non disponibile",
|
||||
"iris.bukkit.scoreboard.height": "§bAltezza§7: {height}",
|
||||
"iris.bukkit.scoreboard.slope": "§bPendenza§7: {slope}",
|
||||
"iris.bukkit.scoreboard.block_updates": "§bBUD/s§7: {updates}",
|
||||
|
||||
@@ -1358,6 +1358,8 @@
|
||||
"iris.bukkit.scoreboard.carving": "§dカービング§7: {state}",
|
||||
"iris.bukkit.scoreboard.region": "§bリージョン§7: {region}",
|
||||
"iris.bukkit.scoreboard.biome": "§bバイオーム§7: {biome}",
|
||||
"iris.bukkit.scoreboard.biome_loading": "読み込み中",
|
||||
"iris.bukkit.scoreboard.biome_unavailable": "利用不可",
|
||||
"iris.bukkit.scoreboard.height": "§b高さ§7: {height}",
|
||||
"iris.bukkit.scoreboard.slope": "§bスロープ§7: {slope}",
|
||||
"iris.bukkit.scoreboard.block_updates": "§bBUD/s§7: {updates}",
|
||||
|
||||
@@ -1358,6 +1358,8 @@
|
||||
"iris.bukkit.scoreboard.carving": "§d조각 중§7: {state}",
|
||||
"iris.bukkit.scoreboard.region": "§b주요 특징§7: {region}",
|
||||
"iris.bukkit.scoreboard.biome": "§b바이오메트§7: {biome}",
|
||||
"iris.bukkit.scoreboard.biome_loading": "불러오는 중",
|
||||
"iris.bukkit.scoreboard.biome_unavailable": "사용 불가",
|
||||
"iris.bukkit.scoreboard.height": "§b고도:§7: {height}",
|
||||
"iris.bukkit.scoreboard.slope": "§b기울기§7: {slope}",
|
||||
"iris.bukkit.scoreboard.block_updates": "§bBUD/s§7: {updates}",
|
||||
|
||||
@@ -1358,6 +1358,8 @@
|
||||
"iris.bukkit.scoreboard.carving": "§dRaižyti§7: {state}",
|
||||
"iris.bukkit.scoreboard.region": "§bRegionas§7: {region}",
|
||||
"iris.bukkit.scoreboard.biome": "§bBiomas§7: {biome}",
|
||||
"iris.bukkit.scoreboard.biome_loading": "Įkeliama",
|
||||
"iris.bukkit.scoreboard.biome_unavailable": "Nepasiekiama",
|
||||
"iris.bukkit.scoreboard.height": "§bAukštis§7: {height}",
|
||||
"iris.bukkit.scoreboard.slope": "§bSpop§7: {slope}",
|
||||
"iris.bukkit.scoreboard.block_updates": "§bBID/s§7: {updates}",
|
||||
|
||||
@@ -1358,6 +1358,8 @@
|
||||
"iris.bukkit.scoreboard.carving": "§dsnijden§7: {state}",
|
||||
"iris.bukkit.scoreboard.region": "§bGebieden§7: {region}",
|
||||
"iris.bukkit.scoreboard.biome": "§bBiomeName§7: {biome}",
|
||||
"iris.bukkit.scoreboard.biome_loading": "Laden",
|
||||
"iris.bukkit.scoreboard.biome_unavailable": "Niet beschikbaar",
|
||||
"iris.bukkit.scoreboard.height": "§bHoogte§7: {height}",
|
||||
"iris.bukkit.scoreboard.slope": "§bHelling§7: {slope}",
|
||||
"iris.bukkit.scoreboard.block_updates": "§bBUD./s§7: {updates}",
|
||||
|
||||
@@ -1358,6 +1358,8 @@
|
||||
"iris.bukkit.scoreboard.carving": "§dRzeźba§7: {state}",
|
||||
"iris.bukkit.scoreboard.region": "§bObszar§7: {region}",
|
||||
"iris.bukkit.scoreboard.biome": "§bBioma§7: {biome}",
|
||||
"iris.bukkit.scoreboard.biome_loading": "Ładowanie",
|
||||
"iris.bukkit.scoreboard.biome_unavailable": "Niedostępne",
|
||||
"iris.bukkit.scoreboard.height": "§bWysokość§7: {height}",
|
||||
"iris.bukkit.scoreboard.slope": "§bNachylenie§7: {slope}",
|
||||
"iris.bukkit.scoreboard.block_updates": "§bPOD/s§7: {updates}",
|
||||
|
||||
@@ -1358,6 +1358,8 @@
|
||||
"iris.bukkit.scoreboard.carving": "§dEsculpir§7: {state}",
|
||||
"iris.bukkit.scoreboard.region": "§bRegião§7: {region}",
|
||||
"iris.bukkit.scoreboard.biome": "§bBioma§7: {biome}",
|
||||
"iris.bukkit.scoreboard.biome_loading": "A carregar",
|
||||
"iris.bukkit.scoreboard.biome_unavailable": "Indisponível",
|
||||
"iris.bukkit.scoreboard.height": "§bAltura§7: {height}",
|
||||
"iris.bukkit.scoreboard.slope": "§bDeslize§7: {slope}",
|
||||
"iris.bukkit.scoreboard.block_updates": "§bBUD./s§7: {updates}",
|
||||
|
||||
@@ -1358,6 +1358,8 @@
|
||||
"iris.bukkit.scoreboard.carving": "§dрезьба§7: {state}",
|
||||
"iris.bukkit.scoreboard.region": "§bрегион§7: {region}",
|
||||
"iris.bukkit.scoreboard.biome": "§bбиом§7: {biome}",
|
||||
"iris.bukkit.scoreboard.biome_loading": "Загрузка",
|
||||
"iris.bukkit.scoreboard.biome_unavailable": "Недоступно",
|
||||
"iris.bukkit.scoreboard.height": "§bвысота§7: {height}",
|
||||
"iris.bukkit.scoreboard.slope": "§bСклон§7: {slope}",
|
||||
"iris.bukkit.scoreboard.block_updates": "§bБУД/s§7: {updates}",
|
||||
|
||||
@@ -1358,6 +1358,8 @@
|
||||
"iris.bukkit.scoreboard.carving": "§dAraba§7: {state}",
|
||||
"iris.bukkit.scoreboard.region": "§bBölge Bölgesi§7: {region}",
|
||||
"iris.bukkit.scoreboard.biome": "§bBiyome§7: {biome}",
|
||||
"iris.bukkit.scoreboard.biome_loading": "Yükleniyor",
|
||||
"iris.bukkit.scoreboard.biome_unavailable": "Kullanılamıyor",
|
||||
"iris.bukkit.scoreboard.height": "§bYükseklik§7: {height}",
|
||||
"iris.bukkit.scoreboard.slope": "§bSlopee§7: {slope}",
|
||||
"iris.bukkit.scoreboard.block_updates": "§bBUDÜ/s§7: {updates}",
|
||||
|
||||
@@ -1358,6 +1358,8 @@
|
||||
"iris.bukkit.scoreboard.carving": "§dQuan tâm§7: {state}",
|
||||
"iris.bukkit.scoreboard.region": "§bVùng§7: {region}",
|
||||
"iris.bukkit.scoreboard.biome": "§bĐôi§7: {biome}",
|
||||
"iris.bukkit.scoreboard.biome_loading": "Đang tải",
|
||||
"iris.bukkit.scoreboard.biome_unavailable": "Không khả dụng",
|
||||
"iris.bukkit.scoreboard.height": "§bChiều cao§7: {height}",
|
||||
"iris.bukkit.scoreboard.slope": "§bTrượt§7: {slope}",
|
||||
"iris.bukkit.scoreboard.block_updates": "§bBAD/s§7: {updates}",
|
||||
|
||||
@@ -1358,6 +1358,8 @@
|
||||
"iris.bukkit.scoreboard.carving": "§d雕刻§7: {state}",
|
||||
"iris.bukkit.scoreboard.region": "§b地区§7: {region}",
|
||||
"iris.bukkit.scoreboard.biome": "§b生物团§7: {biome}",
|
||||
"iris.bukkit.scoreboard.biome_loading": "加载中",
|
||||
"iris.bukkit.scoreboard.biome_unavailable": "不可用",
|
||||
"iris.bukkit.scoreboard.height": "§b高度§7: {height}",
|
||||
"iris.bukkit.scoreboard.slope": "§b斜坡§7: {slope}",
|
||||
"iris.bukkit.scoreboard.block_updates": "§b学生会/s§7: {updates}",
|
||||
|
||||
@@ -1358,6 +1358,8 @@
|
||||
"iris.bukkit.scoreboard.carving": "§d雕刻§7: {state}",
|
||||
"iris.bukkit.scoreboard.region": "§b地區§7: {region}",
|
||||
"iris.bukkit.scoreboard.biome": "§b生物團§7: {biome}",
|
||||
"iris.bukkit.scoreboard.biome_loading": "載入中",
|
||||
"iris.bukkit.scoreboard.biome_unavailable": "無法使用",
|
||||
"iris.bukkit.scoreboard.height": "§b高度§7: {height}",
|
||||
"iris.bukkit.scoreboard.slope": "§b斜坡§7: {slope}",
|
||||
"iris.bukkit.scoreboard.block_updates": "§b學生會/s§7: {updates}",
|
||||
|
||||
@@ -0,0 +1,114 @@
|
||||
package art.arcane.iris.core.gui;
|
||||
|
||||
import art.arcane.iris.engine.framework.Engine;
|
||||
import art.arcane.iris.engine.history.SavedBiomeUnavailableException;
|
||||
import art.arcane.iris.spi.IrisLogging;
|
||||
import org.junit.Test;
|
||||
import org.mockito.MockedStatic;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import java.util.ArrayList;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.anyDouble;
|
||||
import static org.mockito.ArgumentMatchers.anyInt;
|
||||
import static org.mockito.Mockito.CALLS_REAL_METHODS;
|
||||
import static org.mockito.Mockito.doAnswer;
|
||||
import static org.mockito.Mockito.doThrow;
|
||||
import static org.mockito.Mockito.mockStatic;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
public class PregeneratorJobSavedBiomeTest {
|
||||
@Test
|
||||
public void generatedChunkLoadingDoesNotEscapeRendererWork() throws Exception {
|
||||
Fixture fixture = new Fixture();
|
||||
doThrow(new SavedBiomeUnavailableException("Saved biome information is loading.", true))
|
||||
.when(fixture.engine).draw(anyDouble(), anyDouble());
|
||||
|
||||
fixture.job.onChunkGenerated(2, -3, false);
|
||||
assertEquals(1, fixture.tasks.size());
|
||||
fixture.tasks.removeFirst().run();
|
||||
verify(fixture.renderer).submit(anyInt(), anyInt(), any());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void existingChunkPreviewRunsOnTheRendererWorker() throws Exception {
|
||||
Fixture fixture = new Fixture();
|
||||
doThrow(new SavedBiomeUnavailableException("Saved biome information is loading.", true))
|
||||
.when(fixture.engine).draw(anyDouble(), anyDouble());
|
||||
|
||||
fixture.job.onChunkExistsInRegionGen(2, -3);
|
||||
verify(fixture.engine, never()).draw(anyDouble(), anyDouble());
|
||||
assertEquals(1, fixture.tasks.size());
|
||||
fixture.tasks.removeFirst().run();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void existingChunksDoNotResolveBiomesWhileThePreviewIsHidden() throws Exception {
|
||||
Fixture fixture = new Fixture();
|
||||
when(fixture.renderer.isVisibleFrame()).thenReturn(false);
|
||||
|
||||
fixture.job.onChunkExistsInRegionGen(2, -3);
|
||||
|
||||
assertEquals(0, fixture.tasks.size());
|
||||
verify(fixture.engine, never()).draw(anyDouble(), anyDouble());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void queuedPreviewSkipsReadsWhenTheFrameBecomesHidden() throws Exception {
|
||||
Fixture fixture = new Fixture();
|
||||
fixture.job.onChunkGenerated(2, -3, false);
|
||||
when(fixture.renderer.isVisibleFrame()).thenReturn(false);
|
||||
|
||||
fixture.tasks.removeFirst().run();
|
||||
|
||||
verify(fixture.engine, never()).draw(anyDouble(), anyDouble());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void actualDrawFailureStillReportsWhenShutdownStartsDuringRendering() throws Exception {
|
||||
Fixture fixture = new Fixture();
|
||||
IllegalStateException failure = new IllegalStateException("Palette failure");
|
||||
doAnswer(invocation -> {
|
||||
when(fixture.engine.isClosing()).thenReturn(true);
|
||||
throw failure;
|
||||
}).when(fixture.engine).drawForPreview(anyInt(), anyInt());
|
||||
try (MockedStatic<IrisLogging> logging = mockStatic(IrisLogging.class)) {
|
||||
fixture.job.onChunkGenerated(2, -3, false);
|
||||
fixture.tasks.removeFirst().run();
|
||||
|
||||
logging.verify(() -> IrisLogging.reportError(
|
||||
"Unable to draw the pregeneration preview for chunk 2,-3.", failure));
|
||||
}
|
||||
}
|
||||
|
||||
private static final class Fixture {
|
||||
private final PregeneratorJob job = mock(PregeneratorJob.class, CALLS_REAL_METHODS);
|
||||
private final Engine engine = mock(Engine.class, CALLS_REAL_METHODS);
|
||||
private final PregenRenderer renderer = mock(PregenRenderer.class);
|
||||
private final ArrayList<Runnable> tasks = new ArrayList<>();
|
||||
|
||||
private Fixture() throws Exception {
|
||||
ExecutorService service = mock(ExecutorService.class);
|
||||
doAnswer(invocation -> {
|
||||
tasks.add(invocation.getArgument(0));
|
||||
return null;
|
||||
}).when(service).execute(any(Runnable.class));
|
||||
when(renderer.isVisibleFrame()).thenReturn(true);
|
||||
setField("engine", engine);
|
||||
setField("renderer", renderer);
|
||||
setField("service", service);
|
||||
}
|
||||
|
||||
private void setField(String name, Object value) throws Exception {
|
||||
Field field = PregeneratorJob.class.getDeclaredField(name);
|
||||
field.setAccessible(true);
|
||||
field.set(job, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,282 @@
|
||||
package art.arcane.iris.core.service;
|
||||
|
||||
import art.arcane.iris.core.IrisSettings;
|
||||
import art.arcane.iris.core.loader.IrisData;
|
||||
import art.arcane.iris.core.runtime.jigsaw.JigsawStudioMode;
|
||||
import art.arcane.iris.core.tools.IrisToolbelt;
|
||||
import art.arcane.iris.engine.framework.BiomeEnvironment;
|
||||
import art.arcane.iris.engine.framework.Engine;
|
||||
import art.arcane.iris.engine.history.SavedBiomeUnavailableException;
|
||||
import art.arcane.iris.engine.mantle.EngineMantle;
|
||||
import art.arcane.iris.engine.object.IrisBiome;
|
||||
import art.arcane.iris.engine.object.IrisDimension;
|
||||
import art.arcane.iris.engine.object.IrisRegion;
|
||||
import art.arcane.iris.engine.platform.PlatformChunkGenerator;
|
||||
import art.arcane.iris.spi.IrisLogging;
|
||||
import art.arcane.iris.spi.IrisPlatform;
|
||||
import art.arcane.iris.spi.IrisPlatforms;
|
||||
import art.arcane.iris.spi.PlatformBlockState;
|
||||
import art.arcane.iris.spi.PlatformRegistries;
|
||||
import art.arcane.iris.util.common.scheduling.J;
|
||||
import art.arcane.volmlib.util.board.Board;
|
||||
import art.arcane.volmlib.util.board.BoardSettings;
|
||||
import org.bukkit.Location;
|
||||
import org.bukkit.World;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.event.player.PlayerChangedWorldEvent;
|
||||
import org.junit.Test;
|
||||
import org.junit.AfterClass;
|
||||
import org.junit.BeforeClass;
|
||||
import org.mockito.MockedConstruction;
|
||||
import org.mockito.MockedStatic;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import java.util.ArrayDeque;
|
||||
import java.util.UUID;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.anyInt;
|
||||
import static org.mockito.ArgumentMatchers.anyString;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.ArgumentMatchers.same;
|
||||
import static org.mockito.Mockito.doAnswer;
|
||||
import static org.mockito.Mockito.doCallRealMethod;
|
||||
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 BoardSVCSavedBiomeTest {
|
||||
private static IrisPlatform previousPlatform;
|
||||
|
||||
@BeforeClass
|
||||
public static void bindPlatform() {
|
||||
previousPlatform = IrisPlatforms.isBound() ? IrisPlatforms.get() : null;
|
||||
IrisPlatforms.unbind();
|
||||
IrisPlatform platform = mock(IrisPlatform.class);
|
||||
PlatformRegistries registries = mock(PlatformRegistries.class);
|
||||
when(platform.registries()).thenReturn(registries);
|
||||
when(registries.block(anyString())).thenReturn(mock(PlatformBlockState.class));
|
||||
IrisPlatforms.bind(platform);
|
||||
}
|
||||
|
||||
@AfterClass
|
||||
public static void restorePlatform() {
|
||||
IrisPlatforms.unbind();
|
||||
if (previousPlatform != null) {
|
||||
IrisPlatforms.bind(previousPlatform);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void worldEntryLoadingKeepsRefreshAliveAndRecoversWithoutMovement() throws Exception {
|
||||
try (Fixture fixture = new Fixture()) {
|
||||
fixture.biomeFailure = new SavedBiomeUnavailableException("Saved biome is loading", true);
|
||||
|
||||
fixture.service.on(new PlayerChangedWorldEvent(fixture.player, mock(World.class)));
|
||||
|
||||
assertTrue(fixture.lines().contains("Loading"));
|
||||
assertEquals(1, fixture.ticks.size());
|
||||
fixture.logging.verifyNoInteractions();
|
||||
|
||||
fixture.biomeFailure = null;
|
||||
fixture.environment = environment("Saved region", "Saved biome");
|
||||
fixture.tick();
|
||||
|
||||
assertTrue(fixture.lines().contains("Saved region"));
|
||||
assertTrue(fixture.lines().contains("Saved biome"));
|
||||
assertFalse(fixture.lines().contains("Loading"));
|
||||
assertEquals(1, fixture.ticks.size());
|
||||
verify(fixture.engine, times(2)).getBiomeOrMantleEnvironment(8, 160, 8);
|
||||
verify(fixture.engine, never()).getRegion(anyInt(), anyInt(), anyInt());
|
||||
verify(fixture.engine, never()).getBiomeOrMantle(anyInt(), anyInt(), anyInt());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void unsavedBiomeUsesTheMantleOverride() throws Exception {
|
||||
try (Fixture fixture = new Fixture()) {
|
||||
IrisBiome natural = mock(IrisBiome.class);
|
||||
IrisBiome flooded = mock(IrisBiome.class);
|
||||
when(natural.getName()).thenReturn("Natural cave");
|
||||
when(flooded.getName()).thenReturn("Flooded cave");
|
||||
when(fixture.engine.getHeight(8, 8)).thenReturn(200);
|
||||
when(fixture.engine.getCaveBiome(8, 160, 8)).thenReturn(natural);
|
||||
when(fixture.engine.getCaveOrMantleBiome(8, 160, 8)).thenReturn(flooded);
|
||||
when(fixture.engine.getDimension()).thenReturn(fixture.environment.dimension());
|
||||
when(fixture.engine.getData()).thenReturn(fixture.environment.data());
|
||||
doCallRealMethod().when(fixture.engine).getBiome(8, 160, 8);
|
||||
doCallRealMethod().when(fixture.engine).getBiomeOrMantle(8, 160, 8);
|
||||
doCallRealMethod().when(fixture.engine).getBiomeEnvironment(8, 160, 8);
|
||||
doCallRealMethod().when(fixture.engine).getBiomeOrMantleEnvironment(8, 160, 8);
|
||||
|
||||
fixture.service.updatePlayer(fixture.player);
|
||||
|
||||
assertTrue(fixture.lines().contains("Flooded cave"));
|
||||
assertFalse(fixture.lines().contains("Natural cave"));
|
||||
verify(fixture.engine, never()).getBiomeEnvironment(anyInt(), anyInt(), anyInt());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void unavailableBiomeReportsOncePerContextAndKeepsRefreshing() throws Exception {
|
||||
try (Fixture fixture = new Fixture()) {
|
||||
fixture.biomeFailure = new SavedBiomeUnavailableException("Saved pack is missing", false);
|
||||
fixture.service.updatePlayer(fixture.player);
|
||||
|
||||
assertTrue(fixture.lines().contains("Unavailable"));
|
||||
assertFalse(fixture.lines().contains("Loading"));
|
||||
fixture.biomeFailure = new SavedBiomeUnavailableException("Saved pack is missing", false);
|
||||
fixture.tick();
|
||||
fixture.logging.verify(() -> IrisLogging.reportError(anyString(),
|
||||
any(SavedBiomeUnavailableException.class)), times(1));
|
||||
|
||||
fixture.location = new Location(fixture.world, 24, 96, 8);
|
||||
fixture.tick();
|
||||
|
||||
fixture.logging.verify(() -> IrisLogging.reportError(anyString(),
|
||||
any(SavedBiomeUnavailableException.class)), times(2));
|
||||
assertEquals(1, fixture.ticks.size());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void changingWorldClearsThePreviousBiomeWhileTheNewOneLoads() throws Exception {
|
||||
try (Fixture fixture = new Fixture()) {
|
||||
fixture.environment = environment("Previous region", "Previous biome");
|
||||
fixture.service.updatePlayer(fixture.player);
|
||||
assertTrue(fixture.lines().contains("Previous biome"));
|
||||
World previous = fixture.world;
|
||||
fixture.world = world("second-studio");
|
||||
fixture.location = new Location(fixture.world, 8, 96, 8);
|
||||
fixture.biomeFailure = new SavedBiomeUnavailableException("Saved biome is loading", true);
|
||||
|
||||
fixture.service.on(new PlayerChangedWorldEvent(fixture.player, previous));
|
||||
|
||||
assertTrue(fixture.lines().contains("Loading"));
|
||||
assertFalse(fixture.lines().contains("Previous region"));
|
||||
assertFalse(fixture.lines().contains("Previous biome"));
|
||||
assertEquals(1, fixture.ticks.size());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void jigsawViewDoesNotQueryBiomesAndOrdinaryViewStillRetries() throws Exception {
|
||||
try (Fixture fixture = new Fixture()) {
|
||||
fixture.biomeFailure = new SavedBiomeUnavailableException("Saved biome is loading", true);
|
||||
JigsawStudioBoardContext context = new JigsawStudioBoardContext(
|
||||
fixture.world.getUID(), UUID.randomUUID(), "village", JigsawStudioMode.PLANAR_JIGSAW,
|
||||
"Corner", "Corner", "mossy", JigsawStudioBoardState.SAVED, "Open the chest");
|
||||
|
||||
fixture.service.applyJigsawContext(fixture.player, context);
|
||||
|
||||
assertEquals(BoardSVC.jigsawLines(context), fixture.service.getLines(fixture.player));
|
||||
verify(fixture.engine, never()).getBiomeOrMantleEnvironment(anyInt(), anyInt(), anyInt());
|
||||
assertTrue(fixture.ticks.isEmpty());
|
||||
|
||||
fixture.service.clearJigsawContext(fixture.player);
|
||||
|
||||
assertTrue(fixture.lines().contains("Loading"));
|
||||
assertFalse(fixture.lines().contains("Jigsaw Studio"));
|
||||
assertEquals(1, fixture.ticks.size());
|
||||
}
|
||||
}
|
||||
|
||||
private static BiomeEnvironment environment(String regionName, String biomeName) {
|
||||
IrisRegion region = mock(IrisRegion.class);
|
||||
IrisBiome biome = mock(IrisBiome.class);
|
||||
when(region.getName()).thenReturn(regionName);
|
||||
when(biome.getName()).thenReturn(biomeName);
|
||||
return new BiomeEnvironment(1L, biome, region, mock(IrisDimension.class), mock(IrisData.class));
|
||||
}
|
||||
|
||||
private static World world(String name) {
|
||||
World world = mock(World.class);
|
||||
when(world.getUID()).thenReturn(UUID.randomUUID());
|
||||
when(world.getName()).thenReturn(name);
|
||||
when(world.getMinHeight()).thenReturn(-64);
|
||||
return world;
|
||||
}
|
||||
|
||||
private static void setField(BoardSVC service, String name, Object value) throws ReflectiveOperationException {
|
||||
Field field = BoardSVC.class.getDeclaredField(name);
|
||||
field.setAccessible(true);
|
||||
field.set(service, value);
|
||||
}
|
||||
|
||||
private static final class Fixture implements AutoCloseable {
|
||||
private final MockedStatic<J> scheduling = mockStatic(J.class);
|
||||
private final MockedStatic<IrisToolbelt> toolbelt = mockStatic(IrisToolbelt.class);
|
||||
private final MockedStatic<IrisSettings> settings = mockStatic(IrisSettings.class);
|
||||
private final MockedStatic<IrisLogging> logging = mockStatic(IrisLogging.class);
|
||||
private final MockedConstruction<Board> boards = mockConstruction(Board.class,
|
||||
(board, context) -> when(board.ownsScoreboardAssignment()).thenReturn(true));
|
||||
private final BoardSVC service = new BoardSVC();
|
||||
private final Player player = mock(Player.class);
|
||||
private final Engine engine = mock(Engine.class);
|
||||
private final ArrayDeque<Runnable> ticks = new ArrayDeque<>();
|
||||
private World world = world("first-studio");
|
||||
private Location location = new Location(world, 8, 96, 8);
|
||||
private BiomeEnvironment environment = environment("Ready region", "Ready biome");
|
||||
private SavedBiomeUnavailableException biomeFailure;
|
||||
|
||||
private Fixture() throws ReflectiveOperationException {
|
||||
setField(service, "boardEnabled", true);
|
||||
setField(service, "settings", BoardSettings.builder().boardProvider(service).build());
|
||||
settings.when(IrisSettings::get).thenReturn(new IrisSettings());
|
||||
when(player.getUniqueId()).thenReturn(UUID.randomUUID());
|
||||
when(player.isOnline()).thenReturn(true);
|
||||
when(player.getWorld()).thenAnswer(invocation -> world);
|
||||
when(player.getLocation()).thenAnswer(invocation -> location);
|
||||
scheduling.when(() -> J.isOwnedByCurrentRegion(player)).thenReturn(true);
|
||||
scheduling.when(() -> J.runEntity(same(player), any(Runnable.class))).thenAnswer(invocation -> {
|
||||
invocation.getArgument(1, Runnable.class).run();
|
||||
return true;
|
||||
});
|
||||
scheduling.when(() -> J.runEntity(same(player), any(Runnable.class), eq(20), any(Runnable.class)))
|
||||
.thenAnswer(invocation -> ticks.add(invocation.getArgument(1, Runnable.class)));
|
||||
PlatformChunkGenerator generator = mock(PlatformChunkGenerator.class);
|
||||
when(generator.isStudio()).thenReturn(true);
|
||||
when(generator.getEngine()).thenReturn(engine);
|
||||
toolbelt.when(() -> IrisToolbelt.access(any(World.class))).thenReturn(generator);
|
||||
when(engine.getGeneratedPerSecond()).thenReturn(10D);
|
||||
EngineMantle mantle = mock(EngineMantle.class);
|
||||
when(engine.getMantle()).thenReturn(mantle);
|
||||
doAnswer(invocation -> sampledEnvironment()).when(engine).getBiomeEnvironment(anyInt(), anyInt(), anyInt());
|
||||
doAnswer(invocation -> sampledEnvironment()).when(engine).getBiomeOrMantleEnvironment(anyInt(), anyInt(), anyInt());
|
||||
doAnswer(invocation -> sampledEnvironment().region()).when(engine).getRegion(anyInt(), anyInt(), anyInt());
|
||||
doAnswer(invocation -> sampledEnvironment().biome()).when(engine).getBiomeOrMantle(anyInt(), anyInt(), anyInt());
|
||||
}
|
||||
|
||||
private BiomeEnvironment sampledEnvironment() {
|
||||
if (biomeFailure != null) {
|
||||
throw biomeFailure;
|
||||
}
|
||||
return environment;
|
||||
}
|
||||
|
||||
private String lines() {
|
||||
return String.join("\n", service.getLines(player));
|
||||
}
|
||||
|
||||
private void tick() {
|
||||
assertFalse("The ordinary scoreboard must schedule its next refresh", ticks.isEmpty());
|
||||
ticks.remove().run();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
boards.close();
|
||||
logging.close();
|
||||
settings.close();
|
||||
toolbelt.close();
|
||||
scheduling.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -645,6 +645,7 @@ public class IrisEngineGenerationRuntimeScopeTest {
|
||||
when(saved.resolveCaveBase(19, -3)).thenReturn(Optional.of(environment));
|
||||
|
||||
assertSame(environment, engine.getBiomeEnvironment(19, 20, -3));
|
||||
assertSame(environment, engine.getBiomeOrMantleEnvironment(19, 20, -3));
|
||||
assertSame(environment, engine.getSurfaceBiomeEnvironment(19, -3));
|
||||
assertSame(environment.biome(), engine.getBiome(19, 20, -3));
|
||||
assertSame(environment.biome(), engine.getBiomeOrMantle(19, 20, -3));
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
package art.arcane.iris.engine;
|
||||
|
||||
import art.arcane.iris.core.loader.IrisData;
|
||||
import art.arcane.iris.engine.framework.BiomeEnvironment;
|
||||
import art.arcane.iris.engine.framework.GenerationSessionLease;
|
||||
import art.arcane.iris.engine.history.GenerationHistoryRuntimeRouter;
|
||||
import art.arcane.iris.engine.history.SavedBiomeRuntime;
|
||||
import art.arcane.iris.engine.object.IrisBiome;
|
||||
import art.arcane.iris.engine.object.IrisDimension;
|
||||
import art.arcane.iris.engine.object.IrisRegion;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.awt.Color;
|
||||
import java.lang.reflect.Field;
|
||||
import java.util.Optional;
|
||||
import java.util.function.Function;
|
||||
|
||||
import static org.junit.Assert.assertSame;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.anyInt;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.doCallRealMethod;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
public class IrisEnginePreviewTest {
|
||||
@Test
|
||||
public void previewRendersThePreparedEnvironmentWhileItIsRetained() throws Exception {
|
||||
IrisEngine engine = mock(IrisEngine.class);
|
||||
GenerationHistoryRuntimeRouter router = mock(GenerationHistoryRuntimeRouter.class);
|
||||
SavedBiomeRuntime biomes = mock(SavedBiomeRuntime.class);
|
||||
GenerationSessionLease lease = mock(GenerationSessionLease.class);
|
||||
BiomeEnvironment environment = new BiomeEnvironment(1L, mock(IrisBiome.class), mock(IrisRegion.class),
|
||||
mock(IrisDimension.class), mock(IrisData.class));
|
||||
Field field = IrisEngine.class.getDeclaredField("generationHistoryRuntimeRouter");
|
||||
field.setAccessible(true);
|
||||
field.set(engine, router);
|
||||
when(engine.acquireGenerationLease("pregen_preview")).thenReturn(lease);
|
||||
when(router.biomes()).thenReturn(biomes);
|
||||
when(biomes.readSurfaceBiome(eq(-24), eq(40), any())).thenAnswer(invocation -> {
|
||||
Function<Optional<BiomeEnvironment>, Color> reader = invocation.getArgument(2);
|
||||
Color color = reader.apply(Optional.of(environment));
|
||||
verify(engine).drawBiomeEnvironment(-24, 40, environment);
|
||||
return color;
|
||||
});
|
||||
when(engine.drawBiomeEnvironment(-24, 40, environment)).thenReturn(Color.RED);
|
||||
doCallRealMethod().when(engine).drawForPreview(-24, 40);
|
||||
|
||||
assertSame(Color.RED, engine.drawForPreview(-24, 40));
|
||||
|
||||
verify(lease).close();
|
||||
verify(engine, never()).getSurfaceBiomeEnvironment(anyInt(), anyInt());
|
||||
verify(engine, never()).getRegion(anyInt(), anyInt());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
package art.arcane.iris.engine.framework;
|
||||
|
||||
import art.arcane.iris.core.loader.IrisData;
|
||||
import art.arcane.iris.engine.object.IrisBiome;
|
||||
import art.arcane.iris.engine.object.IrisDimension;
|
||||
import art.arcane.iris.engine.object.IrisRegion;
|
||||
import art.arcane.iris.util.common.data.DataProvider;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.awt.Color;
|
||||
|
||||
import static org.junit.Assert.assertNotNull;
|
||||
import static org.junit.Assert.assertSame;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.anyInt;
|
||||
import static org.mockito.Mockito.CALLS_REAL_METHODS;
|
||||
import static org.mockito.Mockito.doReturn;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
public class EnginePreviewColorTest {
|
||||
@Test
|
||||
public void previewUsesOneSurfaceEnvironmentAndItsHistoricalRegionDefinitions() {
|
||||
Engine engine = mock(Engine.class, CALLS_REAL_METHODS);
|
||||
IrisData data = mock(IrisData.class);
|
||||
IrisRegion region = mock(IrisRegion.class);
|
||||
IrisBiome biome = mock(IrisBiome.class);
|
||||
BiomeEnvironment environment = new BiomeEnvironment(1L, biome, region, mock(IrisDimension.class), data);
|
||||
EngineTarget target = mock(EngineTarget.class);
|
||||
doReturn(environment).when(engine).getSurfaceBiomeEnvironment(-24, 40);
|
||||
doReturn(target).when(engine).getTarget();
|
||||
doReturn(100).when(engine).getHeight(-24, 40);
|
||||
when(target.getHeight()).thenReturn(384);
|
||||
when(region.getColor(any(), any())).thenAnswer(invocation -> {
|
||||
DataProvider provider = invocation.getArgument(0);
|
||||
assertSame(data, provider.getData());
|
||||
return Color.GREEN;
|
||||
});
|
||||
when(biome.getColor(any(), any())).thenReturn(Color.YELLOW);
|
||||
|
||||
assertNotNull(engine.draw(-24, 40));
|
||||
|
||||
verify(engine).getSurfaceBiomeEnvironment(-24, 40);
|
||||
verify(engine, never()).getRegion(anyInt(), anyInt());
|
||||
verify(engine, never()).getSurfaceBiome(anyInt(), anyInt());
|
||||
}
|
||||
}
|
||||
+47
@@ -389,6 +389,35 @@ public final class GenerationHistoryRuntimeRouterTest {
|
||||
verify(engine, never()).closeDetachedGenerationRuntime(active);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void closeCompletesWhileWorkersContinuouslyAcquireReadyRoutes() throws Exception {
|
||||
Path world = temporaryFolder.newFolder("router-contended-close-world").toPath();
|
||||
GenerationHistory history = createHistory(world, createPack("router-contended-close-pack", "alpha"));
|
||||
IrisEngine engine = mock(IrisEngine.class);
|
||||
FakeRuntimeFactory runtimes = new FakeRuntimeFactory();
|
||||
IrisEngine.GenerationRuntimeBinding active = runtimes.binding(history, history.activeActivation());
|
||||
when(engine.getActiveGenerationRuntimeBinding()).thenReturn(active);
|
||||
ExecutorService executor = Executors.newFixedThreadPool(9);
|
||||
CountDownLatch routing = new CountDownLatch(8);
|
||||
CompletableFuture<?>[] workers = new CompletableFuture<?>[8];
|
||||
GenerationHistoryRuntimeRouter router = GenerationHistoryRuntimeRouter.attach(
|
||||
engine, history, (ignored, x, z) -> signature(x, z), runtimes);
|
||||
try {
|
||||
for (int i = 0; i < workers.length; i++) {
|
||||
workers[i] = CompletableFuture.runAsync(() -> routeUntilClosed(router, routing), executor);
|
||||
}
|
||||
assertTrue(routing.await(5, TimeUnit.SECONDS));
|
||||
CompletableFuture.runAsync(router::close, executor).get(5, TimeUnit.SECONDS);
|
||||
CompletableFuture.allOf(workers).get(5, TimeUnit.SECONDS);
|
||||
verify(engine).detachGenerationHistoryRuntimeRouter(router);
|
||||
verify(engine, never()).closeDetachedGenerationRuntime(active);
|
||||
} finally {
|
||||
executor.shutdownNow();
|
||||
assertTrue(executor.awaitTermination(5, TimeUnit.SECONDS));
|
||||
router.close();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void closeLeavesTheSingleDefaultRuntimeOwnedByTheEngine() throws Exception {
|
||||
Path world = temporaryFolder.newFolder("router-close-current-world").toPath();
|
||||
@@ -894,6 +923,24 @@ public final class GenerationHistoryRuntimeRouterTest {
|
||||
router.close();
|
||||
}
|
||||
|
||||
private static void routeUntilClosed(GenerationHistoryRuntimeRouter router, CountDownLatch routing) {
|
||||
boolean started = false;
|
||||
while (!Thread.currentThread().isInterrupted()) {
|
||||
try (GenerationHistoryRuntimeRouter.RuntimeRoute route = router.openRoute(0, 0)) {
|
||||
assertEquals(1L, route.activation().activationId());
|
||||
if (!started) {
|
||||
started = true;
|
||||
routing.countDown();
|
||||
}
|
||||
} catch (IOException failure) {
|
||||
throw new AssertionError(failure);
|
||||
} catch (IllegalStateException closed) {
|
||||
assertEquals("Generation-history runtime router is closed.", closed.getMessage());
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private AtomicReference<IrisEngine.GenerationRuntimeBinding> installScopeTracking(IrisEngine engine) {
|
||||
AtomicReference<IrisEngine.GenerationRuntimeBinding> scoped = new AtomicReference<>();
|
||||
when(engine.openGenerationRuntimeScope(any())).thenAnswer(invocation -> {
|
||||
|
||||
@@ -11,6 +11,8 @@ import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.junit.rules.TemporaryFolder;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.lang.reflect.Field;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.ArrayList;
|
||||
@@ -22,6 +24,9 @@ import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.Future;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.TimeoutException;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.concurrent.locks.ReentrantReadWriteLock;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertSame;
|
||||
@@ -30,6 +35,8 @@ import static org.junit.Assert.assertTrue;
|
||||
import static org.mockito.ArgumentMatchers.anyInt;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
import static org.mockito.Mockito.times;
|
||||
import static org.mockito.Mockito.verify;
|
||||
|
||||
public class SavedBiomeRuntimeTest {
|
||||
@Rule
|
||||
@@ -217,9 +224,233 @@ public class SavedBiomeRuntimeTest {
|
||||
Files.delete(pack.resolve("regions/main.json"));
|
||||
assertSame(environment, runtime.resolve(0, 0, 0, true).orElseThrow());
|
||||
assertEquals("forest", environment.biome().getLoadKey());
|
||||
assertSame(environment, runtime.readSurfaceBiome(0, 0, Optional::orElseThrow));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void backgroundPreviewWaitsForTheReadWithoutRepeatingTheQuery() throws Exception {
|
||||
allowUnownedChunks();
|
||||
CountDownLatch entered = new CountDownLatch(1);
|
||||
CountDownLatch release = new CountDownLatch(1);
|
||||
when(store.get(0, 0)).thenAnswer(invocation -> {
|
||||
entered.countDown();
|
||||
assertTrue(release.await(5L, TimeUnit.SECONDS));
|
||||
return Optional.empty();
|
||||
});
|
||||
try (SavedBiomeRuntime runtime = new SavedBiomeRuntime(engine, history);
|
||||
ExecutorService caller = Executors.newSingleThreadExecutor()) {
|
||||
Future<Boolean> rendered = caller.submit(() -> runtime.readSurfaceBiome(0, 0, Optional::isEmpty));
|
||||
try {
|
||||
assertTrue(entered.await(5L, TimeUnit.SECONDS));
|
||||
assertThrows(TimeoutException.class, () -> rendered.get(100L, TimeUnit.MILLISECONDS));
|
||||
} finally {
|
||||
release.countDown();
|
||||
}
|
||||
assertTrue(rendered.get(5L, TimeUnit.SECONDS));
|
||||
verify(store).get(0, 0);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void backgroundPreviewRetainsItsResultAcrossQueryCacheEviction() throws Exception {
|
||||
allowUnownedChunks();
|
||||
try (SavedBiomeRuntime runtime = new SavedBiomeRuntime(engine, history);
|
||||
ExecutorService caller = Executors.newSingleThreadExecutor()) {
|
||||
Field field = SavedBiomeRuntime.class.getDeclaredField("consumption");
|
||||
field.setAccessible(true);
|
||||
ReentrantReadWriteLock consumption = (ReentrantReadWriteLock) field.get(runtime);
|
||||
consumption.writeLock().lock();
|
||||
Future<Boolean> rendered = caller.submit(() -> runtime.readSurfaceBiome(0, 0, Optional::isEmpty));
|
||||
try {
|
||||
long deadline = System.nanoTime() + TimeUnit.SECONDS.toNanos(5L);
|
||||
while (runtime.cachedQueryCount() == 0 && System.nanoTime() < deadline) {
|
||||
Thread.sleep(1L);
|
||||
}
|
||||
assertEquals(1, runtime.cachedQueryCount());
|
||||
for (int chunkX = 1; chunkX <= 130; chunkX++) {
|
||||
int requestedX = chunkX;
|
||||
assertTrue(assertThrows(SavedBiomeUnavailableException.class,
|
||||
() -> runtime.prepareChunk(requestedX, 0)).isLoading());
|
||||
awaitIdle(runtime);
|
||||
}
|
||||
assertThrows(TimeoutException.class, () -> rendered.get(100L, TimeUnit.MILLISECONDS));
|
||||
} finally {
|
||||
consumption.writeLock().unlock();
|
||||
}
|
||||
assertTrue(rendered.get(5L, TimeUnit.SECONDS));
|
||||
verify(store).get(0, 0);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void captureRefreshesThePendingPreviewWithoutAddingAnotherSlot() throws Exception {
|
||||
allowUnownedChunks();
|
||||
CountDownLatch entered = new CountDownLatch(1);
|
||||
CountDownLatch release = new CountDownLatch(1);
|
||||
AtomicInteger readCount = new AtomicInteger();
|
||||
when(store.get(0, 0)).thenAnswer(invocation -> {
|
||||
if (readCount.getAndIncrement() == 0) {
|
||||
entered.countDown();
|
||||
assertTrue(release.await(5L, TimeUnit.SECONDS));
|
||||
}
|
||||
return Optional.empty();
|
||||
});
|
||||
try (SavedBiomeRuntime runtime = new SavedBiomeRuntime(engine, history);
|
||||
ExecutorService caller = Executors.newSingleThreadExecutor()) {
|
||||
Future<Boolean> rendered = caller.submit(() -> runtime.readSurfaceBiome(0, 0, Optional::isEmpty));
|
||||
try {
|
||||
assertTrue(entered.await(5L, TimeUnit.SECONDS));
|
||||
runtime.capture(unresolvedChunk(1));
|
||||
assertEquals(1, runtime.pendingQueryCount());
|
||||
} finally {
|
||||
release.countDown();
|
||||
}
|
||||
assertTrue(rendered.get(5L, TimeUnit.SECONDS));
|
||||
verify(store, times(2)).get(0, 0);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void closeWaitsForBackgroundConsumptionBeforeClosingDefinitions() throws Exception {
|
||||
allowUnownedChunks();
|
||||
CountDownLatch consuming = new CountDownLatch(1);
|
||||
CountDownLatch release = new CountDownLatch(1);
|
||||
SavedBiomeRuntime runtime = new SavedBiomeRuntime(engine, history);
|
||||
try (ExecutorService callers = Executors.newFixedThreadPool(2)) {
|
||||
Future<Boolean> rendered = callers.submit(() -> runtime.readSurfaceBiome(0, 0, environment -> {
|
||||
consuming.countDown();
|
||||
try {
|
||||
assertTrue(release.await(5L, TimeUnit.SECONDS));
|
||||
} catch (InterruptedException failure) {
|
||||
Thread.currentThread().interrupt();
|
||||
throw new AssertionError(failure);
|
||||
}
|
||||
return environment.isEmpty();
|
||||
}));
|
||||
assertTrue(consuming.await(5L, TimeUnit.SECONDS));
|
||||
Future<?> closing = callers.submit(runtime::close);
|
||||
try {
|
||||
assertThrows(TimeoutException.class, () -> closing.get(100L, TimeUnit.MILLISECONDS));
|
||||
} finally {
|
||||
release.countDown();
|
||||
}
|
||||
assertTrue(rendered.get(5L, TimeUnit.SECONDS));
|
||||
closing.get(5L, TimeUnit.SECONDS);
|
||||
assertThrows(IllegalStateException.class, () -> runtime.readSurfaceBiome(0, 0, Optional::isEmpty));
|
||||
} finally {
|
||||
release.countDown();
|
||||
runtime.close();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void backgroundPreviewPreservesActualReadFailure() throws Exception {
|
||||
IOException failure = new IOException("Saved biome read failed");
|
||||
when(store.get(0, 0)).thenThrow(failure);
|
||||
try (SavedBiomeRuntime runtime = new SavedBiomeRuntime(engine, history)) {
|
||||
SavedBiomeUnavailableException unavailable = assertThrows(SavedBiomeUnavailableException.class,
|
||||
() -> runtime.readSurfaceBiome(0, 0, Optional::isEmpty));
|
||||
assertEquals(false, unavailable.isLoading());
|
||||
assertSame(failure, unavailable.getCause());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void closeCompletesWaitingPreviewWithoutConsumingClosedDefinitions() throws Exception {
|
||||
allowUnownedChunks();
|
||||
CountDownLatch entered = new CountDownLatch(1);
|
||||
CountDownLatch release = new CountDownLatch(1);
|
||||
when(store.get(0, 0)).thenAnswer(invocation -> {
|
||||
entered.countDown();
|
||||
assertTrue(release.await(5L, TimeUnit.SECONDS));
|
||||
return Optional.empty();
|
||||
});
|
||||
SavedBiomeRuntime runtime = new SavedBiomeRuntime(engine, history);
|
||||
AtomicInteger consumed = new AtomicInteger();
|
||||
try (ExecutorService callers = Executors.newFixedThreadPool(2)) {
|
||||
Future<Integer> rendered = callers.submit(() -> runtime.readSurfaceBiome(0, 0,
|
||||
environment -> consumed.incrementAndGet()));
|
||||
assertTrue(entered.await(5L, TimeUnit.SECONDS));
|
||||
Future<?> closing = callers.submit(runtime::close);
|
||||
try {
|
||||
assertThrows(TimeoutException.class, () -> closing.get(100L, TimeUnit.MILLISECONDS));
|
||||
} finally {
|
||||
release.countDown();
|
||||
}
|
||||
ExecutionException unavailable = assertThrows(ExecutionException.class,
|
||||
() -> rendered.get(5L, TimeUnit.SECONDS));
|
||||
assertTrue(unavailable.getCause() instanceof IllegalStateException);
|
||||
assertEquals(0, consumed.get());
|
||||
closing.get(5L, TimeUnit.SECONDS);
|
||||
} finally {
|
||||
release.countDown();
|
||||
runtime.close();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void fullReadQueueLeavesThePreviewPendingWithoutAddingWork() throws Exception {
|
||||
allowUnownedChunks();
|
||||
CountDownLatch entered = new CountDownLatch(2);
|
||||
CountDownLatch release = new CountDownLatch(1);
|
||||
when(store.get(anyInt(), anyInt())).thenAnswer(invocation -> {
|
||||
entered.countDown();
|
||||
assertTrue(release.await(5L, TimeUnit.SECONDS));
|
||||
return Optional.empty();
|
||||
});
|
||||
SavedBiomeRuntime runtime = new SavedBiomeRuntime(engine, history);
|
||||
try {
|
||||
for (int chunkX = 0; chunkX < 256; chunkX++) {
|
||||
int requestedX = chunkX;
|
||||
assertTrue(assertThrows(SavedBiomeUnavailableException.class,
|
||||
() -> runtime.prepareChunk(requestedX, 0)).isLoading());
|
||||
}
|
||||
assertTrue(entered.await(5L, TimeUnit.SECONDS));
|
||||
assertEquals(256, runtime.pendingQueryCount());
|
||||
assertTrue(assertThrows(SavedBiomeUnavailableException.class,
|
||||
() -> runtime.readSurfaceBiome(256 * 16, 0, Optional::isEmpty)).isLoading());
|
||||
assertEquals(256, runtime.pendingQueryCount());
|
||||
} finally {
|
||||
release.countDown();
|
||||
runtime.close();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void interruptingPreviewDoesNotCancelTheSharedRead() throws Exception {
|
||||
allowUnownedChunks();
|
||||
CountDownLatch entered = new CountDownLatch(1);
|
||||
CountDownLatch release = new CountDownLatch(1);
|
||||
when(store.get(0, 0)).thenAnswer(invocation -> {
|
||||
entered.countDown();
|
||||
assertTrue(release.await(5L, TimeUnit.SECONDS));
|
||||
return Optional.empty();
|
||||
});
|
||||
SavedBiomeRuntime runtime = new SavedBiomeRuntime(engine, history);
|
||||
try (ExecutorService caller = Executors.newSingleThreadExecutor()) {
|
||||
Future<Boolean> rendered = caller.submit(() -> runtime.readSurfaceBiome(0, 0, Optional::isEmpty));
|
||||
try {
|
||||
assertTrue(entered.await(5L, TimeUnit.SECONDS));
|
||||
assertTrue(rendered.cancel(true));
|
||||
assertEquals(1, runtime.pendingQueryCount());
|
||||
} finally {
|
||||
release.countDown();
|
||||
}
|
||||
assertTrue(runtime.readSurfaceBiome(0, 0, Optional::isEmpty));
|
||||
verify(store).get(0, 0);
|
||||
} finally {
|
||||
release.countDown();
|
||||
runtime.close();
|
||||
}
|
||||
}
|
||||
|
||||
private void allowUnownedChunks() throws IOException {
|
||||
when(store.get(anyInt(), anyInt())).thenReturn(Optional.empty());
|
||||
when(history.isActiveUnowned(anyInt(), anyInt())).thenReturn(true);
|
||||
when(history.semantics(anyInt(), anyInt())).thenReturn(Optional.empty());
|
||||
}
|
||||
|
||||
private static void awaitIdle(SavedBiomeRuntime runtime) throws InterruptedException {
|
||||
long deadline = System.nanoTime() + TimeUnit.SECONDS.toNanos(5L);
|
||||
while (runtime.pendingQueryCount() != 0 && System.nanoTime() < deadline) {
|
||||
|
||||
@@ -0,0 +1,154 @@
|
||||
package art.arcane.iris.platform.bukkit;
|
||||
|
||||
import art.arcane.iris.spi.PlatformBiome;
|
||||
import com.github.benmanes.caffeine.cache.Cache;
|
||||
import org.bukkit.NamespacedKey;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.objectweb.asm.ClassReader;
|
||||
import org.objectweb.asm.ClassVisitor;
|
||||
import org.objectweb.asm.ClassWriter;
|
||||
import org.objectweb.asm.MethodVisitor;
|
||||
import org.objectweb.asm.Opcodes;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.lang.reflect.Field;
|
||||
import java.lang.reflect.Method;
|
||||
import java.lang.reflect.Proxy;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertNotSame;
|
||||
import static org.junit.Assert.assertSame;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
public class BukkitBiomeTest {
|
||||
private Class<?> biomeType;
|
||||
private Method wrap;
|
||||
|
||||
@Before
|
||||
public void loadAdapterWithoutServerRegistryInitialization() throws Exception {
|
||||
ClassLoader loader = new BiomeClassLoader();
|
||||
biomeType = loader.loadClass("org.bukkit.block.Biome");
|
||||
wrap = loader.loadClass(BukkitBiome.class.getName()).getMethod("of", biomeType);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void cachedHandlesKeepCanonicalIdentityWithoutReadingTheKeyAgain() throws Exception {
|
||||
AtomicInteger keyReads = new AtomicInteger();
|
||||
Object biome = biome("iris", "riverbank", keyReads);
|
||||
|
||||
PlatformBiome wrapped = wrap(biome);
|
||||
assertEquals("iris:riverbank", wrapped.key());
|
||||
assertEquals("iris", wrapped.namespace());
|
||||
assertSame(biome, wrapped.nativeHandle());
|
||||
assertSame(wrapped, wrap(biome));
|
||||
assertSame(wrapped, wrap(biome));
|
||||
assertEquals(1, keyReads.get());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void replacementRegistryHandleCannotReturnThePreviousNativeBiome() throws Exception {
|
||||
Object previous = biome("iris", "registry_replacement", new AtomicInteger());
|
||||
Object replacement = biome("iris", "registry_replacement", new AtomicInteger());
|
||||
|
||||
PlatformBiome oldHandle = wrap(previous);
|
||||
PlatformBiome newHandle = wrap(replacement);
|
||||
|
||||
assertNotSame(oldHandle, newHandle);
|
||||
assertEquals(oldHandle.key(), newHandle.key());
|
||||
assertSame(previous, oldHandle.nativeHandle());
|
||||
assertSame(replacement, newHandle.nativeHandle());
|
||||
assertSame(newHandle, wrap(replacement));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void customAndVanillaNamespacesRemainDistinct() throws Exception {
|
||||
PlatformBiome vanilla = wrap(biome("minecraft", "plains", new AtomicInteger()));
|
||||
PlatformBiome custom = wrap(biome("iris", "plains", new AtomicInteger()));
|
||||
|
||||
assertEquals("minecraft:plains", vanilla.key());
|
||||
assertEquals("minecraft", vanilla.namespace());
|
||||
assertEquals("iris:plains", custom.key());
|
||||
assertNotSame(vanilla, custom);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void cacheEvictionPreservesBiomeKeysAndNativeHandles() throws Exception {
|
||||
Object retained = biome("iris", "retained", new AtomicInteger());
|
||||
PlatformBiome original = wrap(retained);
|
||||
for (int index = 0; index < 8_192; index++) {
|
||||
wrap(biome("iris", "eviction_" + index, new AtomicInteger()));
|
||||
}
|
||||
Field field = wrap.getDeclaringClass().getDeclaredField("CACHE");
|
||||
field.setAccessible(true);
|
||||
Cache<?, ?> cache = (Cache<?, ?>) field.get(null);
|
||||
cache.cleanUp();
|
||||
|
||||
assertTrue(cache.estimatedSize() <= 4_096);
|
||||
PlatformBiome restored = wrap(retained);
|
||||
assertEquals(original.key(), restored.key());
|
||||
assertEquals(original.namespace(), restored.namespace());
|
||||
assertSame(retained, restored.nativeHandle());
|
||||
}
|
||||
|
||||
private PlatformBiome wrap(Object biome) throws Exception {
|
||||
return (PlatformBiome) wrap.invoke(null, biome);
|
||||
}
|
||||
|
||||
private Object biome(String namespace, String name, AtomicInteger keyReads) {
|
||||
NamespacedKey key = new NamespacedKey(namespace, name);
|
||||
return Proxy.newProxyInstance(biomeType.getClassLoader(), new Class<?>[]{biomeType}, (proxy, method, arguments) -> switch (method.getName()) {
|
||||
case "getKey" -> {
|
||||
keyReads.incrementAndGet();
|
||||
yield key;
|
||||
}
|
||||
case "hashCode" -> System.identityHashCode(proxy);
|
||||
case "equals" -> proxy == arguments[0];
|
||||
case "toString" -> key.toString();
|
||||
default -> throw new UnsupportedOperationException(method.getName());
|
||||
});
|
||||
}
|
||||
|
||||
private static final class BiomeClassLoader extends ClassLoader {
|
||||
private BiomeClassLoader() {
|
||||
super(BukkitBiomeTest.class.getClassLoader());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
|
||||
if (!name.equals("org.bukkit.block.Biome") && !name.equals(BukkitBiome.class.getName())) {
|
||||
return super.loadClass(name, resolve);
|
||||
}
|
||||
synchronized (getClassLoadingLock(name)) {
|
||||
Class<?> loaded = findLoadedClass(name);
|
||||
if (loaded == null) {
|
||||
try (InputStream source = getParent().getResourceAsStream(name.replace('.', '/') + ".class")) {
|
||||
if (source == null) {
|
||||
throw new ClassNotFoundException(name);
|
||||
}
|
||||
byte[] bytecode = source.readAllBytes();
|
||||
if (name.equals("org.bukkit.block.Biome")) {
|
||||
ClassWriter writer = new ClassWriter(0);
|
||||
new ClassReader(bytecode).accept(new ClassVisitor(Opcodes.ASM9, writer) {
|
||||
@Override
|
||||
public MethodVisitor visitMethod(int access, String method, String descriptor, String signature, String[] exceptions) {
|
||||
return method.equals("<clinit>") ? null : super.visitMethod(access, method, descriptor, signature, exceptions);
|
||||
}
|
||||
}, 0);
|
||||
bytecode = writer.toByteArray();
|
||||
}
|
||||
loaded = defineClass(name, bytecode, 0, bytecode.length);
|
||||
} catch (IOException exception) {
|
||||
throw new ClassNotFoundException(name, exception);
|
||||
}
|
||||
}
|
||||
if (resolve) {
|
||||
resolveClass(loaded);
|
||||
}
|
||||
return loaded;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user