This commit is contained in:
Brian Neumann-Fopiano
2026-07-26 12:12:49 -05:00
parent de25b7d7ab
commit d5a55ccfcf
66 changed files with 6300 additions and 424 deletions
+7
View File
@@ -23,6 +23,9 @@ dependencies {
transitive = false
}
compileOnly(libs.placeholderApi)
testImplementation(libs.placeholderApi) {
transitive = false
}
}
tasks.named('processResources').configure {
@@ -50,4 +53,8 @@ tasks.named('test').configure {
systemProperty('iris.pregeneratorJobSource', rootProject.file('core/src/main/java/art/arcane/iris/core/gui/PregeneratorJob.java').absolutePath)
systemProperty('iris.bukkitEnginePlatformHooksSource', file('src/main/java/art/arcane/iris/core/runtime/BukkitEnginePlatformHooks.java').absolutePath)
systemProperty('iris.engineSvcSource', file('src/main/java/art/arcane/iris/core/service/IrisEngineSVC.java').absolutePath)
systemProperty('iris.terrainSvcSource', file('src/main/java/art/arcane/iris/core/service/IrisTerrainSVC.java').absolutePath)
systemProperty('iris.apiEventSvcSource', file('src/main/java/art/arcane/iris/core/service/IrisApiEventSVC.java').absolutePath)
systemProperty('iris.worldInfoFactorySource', file('src/main/java/art/arcane/iris/core/service/terrain/IrisWorldInfoFactory.java').absolutePath)
systemProperty('iris.readmeSource', rootProject.file('README.md').absolutePath)
}
@@ -37,7 +37,10 @@ import art.arcane.iris.core.runtime.BukkitEnginePlatformHooks;
import art.arcane.iris.core.runtime.TransientWorldCleanupSupport;
import art.arcane.iris.core.runtime.WorldRuntimeControlService;
import art.arcane.iris.core.lifecycle.WorldLifecycleStaging;
import art.arcane.iris.core.link.IrisPapiExpansion;
import art.arcane.iris.api.terrain.IrisTerrainService;
import art.arcane.iris.core.link.IrisPapiInstaller;
import art.arcane.iris.core.link.IrisPapiListener;
import art.arcane.iris.core.link.IrisPapiState;
import art.arcane.iris.core.localization.IrisLanguage;
import art.arcane.iris.core.link.MultiverseCoreLink;
import art.arcane.iris.core.loader.IrisData;
@@ -71,6 +74,7 @@ import art.arcane.iris.spi.IrisServices;
import art.arcane.iris.spi.LogLevel;
import art.arcane.volmlib.integration.ReloadAware;
import art.arcane.volmlib.util.bukkit.WorldIdentity;
import art.arcane.volmlib.util.bukkit.papi.PlaceholderRegistration;
import art.arcane.volmlib.util.collection.KList;
import art.arcane.volmlib.util.collection.KMap;
import art.arcane.volmlib.util.exceptions.IrisException;
@@ -173,6 +177,9 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
}
private final AtomicBoolean alreadyDrained = new AtomicBoolean(false);
private volatile PlaceholderRegistration papiRegistration;
private volatile IrisPapiListener papiListener;
private volatile IrisPapiState papiState;
private KMap<Class<? extends IrisService>, IrisService> services;
public static VolmitSender getSender() {
@@ -1000,6 +1007,7 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
}
public void onDisable() {
teardownPapi();
if (IrisSafeguard.isForceShutdown()) return;
if (alreadyDrained.compareAndSet(false, true)) {
drainWorldGenerators("onDisable", 30L);
@@ -1023,6 +1031,7 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
@Override
public void onPreUnload(ReloadAware.PreUnloadReason reason) {
teardownPapi();
if (!alreadyDrained.compareAndSet(false, true)) {
Iris.info("Pre-unload hook skipped; Iris already drained.");
return;
@@ -1083,8 +1092,53 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
}
private void setupPapi() {
if (Bukkit.getPluginManager().isPluginEnabled("PlaceholderAPI")) {
new IrisPapiExpansion().register();
if (!PlaceholderRegistration.isPlaceholderApiEnabled()) {
return;
}
IrisPapiState state = new IrisPapiState(() -> IrisServices.getOrNull(IrisTerrainService.class));
PlaceholderRegistration registration = new PlaceholderRegistration(getLogger());
if (!IrisPapiInstaller.install(registration, state, getLogger())) {
return;
}
IrisPapiListener listener = new IrisPapiListener(state);
try {
Bukkit.getPluginManager().registerEvents(listener, this);
} catch (Throwable failure) {
registration.unregister();
Iris.warn("Failed to attach the Iris PlaceholderAPI listener: "
+ failure.getClass().getName() + ": " + failure.getMessage());
return;
}
papiState = state;
papiListener = listener;
papiRegistration = registration;
}
private void teardownPapi() {
IrisPapiListener listener = papiListener;
papiListener = null;
if (listener != null) {
HandlerList.unregisterAll(listener);
}
PlaceholderRegistration registration = papiRegistration;
papiRegistration = null;
if (registration != null) {
registration.unregister();
}
IrisPapiState state = papiState;
papiState = null;
if (state != null) {
state.clear();
}
}
@@ -0,0 +1,11 @@
package art.arcane.iris.api.pregen;
public enum IrisPregenPhase {
STARTED,
TICK,
PAUSED,
RESUMED,
SAVING,
COMPLETED,
CANCELLED
}
@@ -0,0 +1,31 @@
package art.arcane.iris.api.pregen;
import java.util.Objects;
public record IrisPregenProgress(
String worldName,
String worldIdentity,
double percent,
long generatedChunks,
long totalChunks,
long remainingChunks,
long failedChunks,
double chunksPerSecond,
long etaMillis,
long elapsedMillis,
String method,
boolean paused) {
public IrisPregenProgress {
Objects.requireNonNull(worldIdentity, "worldIdentity");
worldName = worldName == null ? worldIdentity : worldName;
method = method == null ? "" : method;
percent = Double.isFinite(percent) ? Math.clamp(percent, 0D, 100D) : 0D;
generatedChunks = Math.max(0L, generatedChunks);
totalChunks = Math.max(0L, totalChunks);
remainingChunks = Math.max(0L, remainingChunks);
failedChunks = Math.max(0L, failedChunks);
chunksPerSecond = Double.isFinite(chunksPerSecond) ? Math.max(0D, chunksPerSecond) : 0D;
etaMillis = Math.max(0L, etaMillis);
elapsedMillis = Math.max(0L, elapsedMillis);
}
}
@@ -0,0 +1,35 @@
package art.arcane.iris.api.pregen;
import org.bukkit.event.Event;
import org.bukkit.event.HandlerList;
import java.util.Objects;
public class IrisPregenerationEvent extends Event {
private static final HandlerList HANDLERS = new HandlerList();
private final IrisPregenPhase phase;
private final IrisPregenProgress progress;
public IrisPregenerationEvent(IrisPregenPhase phase, IrisPregenProgress progress) {
this.phase = Objects.requireNonNull(phase, "phase");
this.progress = Objects.requireNonNull(progress, "progress");
}
public static HandlerList getHandlerList() {
return HANDLERS;
}
public IrisPregenPhase getPhase() {
return phase;
}
public IrisPregenProgress getProgress() {
return progress;
}
@Override
public HandlerList getHandlers() {
return HANDLERS;
}
}
@@ -0,0 +1,7 @@
package art.arcane.iris.api.terrain;
public enum IrisColumnField {
SURFACE_HEIGHT,
SURFACE_KIND,
BIOME_KEY
}
@@ -0,0 +1,61 @@
package art.arcane.iris.api.terrain;
import java.util.EnumSet;
import java.util.Objects;
public record IrisColumnQuery(
int minBlockX,
int minBlockZ,
int maxBlockX,
int maxBlockZ,
int strideBlocks,
EnumSet<IrisColumnField> fields) {
public IrisColumnQuery {
Objects.requireNonNull(fields, "fields");
if (fields.isEmpty()) {
throw new IllegalArgumentException("at least one field is required");
}
if (maxBlockX < minBlockX || maxBlockZ < minBlockZ) {
throw new IllegalArgumentException("query bounds are inverted");
}
if (strideBlocks < 1) {
throw new IllegalArgumentException("strideBlocks must be at least 1");
}
fields = EnumSet.copyOf(fields);
}
public static IrisColumnQuery rect(
int minBlockX,
int minBlockZ,
int maxBlockX,
int maxBlockZ,
int strideBlocks,
EnumSet<IrisColumnField> fields) {
return new IrisColumnQuery(minBlockX, minBlockZ, maxBlockX, maxBlockZ, strideBlocks, fields);
}
public long columnCount() {
long columnsX = (((long) maxBlockX - (long) minBlockX) / strideBlocks) + 1L;
long columnsZ = (((long) maxBlockZ - (long) minBlockZ) / strideBlocks) + 1L;
return saturatedProduct(columnsX, columnsZ);
}
public long chunkCount() {
long chunksX = ((long) (maxBlockX >> 4) - (long) (minBlockX >> 4)) + 1L;
long chunksZ = ((long) (maxBlockZ >> 4) - (long) (minBlockZ >> 4)) + 1L;
return saturatedProduct(chunksX, chunksZ);
}
private static long saturatedProduct(long left, long right) {
try {
return Math.multiplyExact(left, right);
} catch (ArithmeticException overflow) {
return Long.MAX_VALUE;
}
}
@Override
public EnumSet<IrisColumnField> fields() {
return EnumSet.copyOf(fields);
}
}
@@ -0,0 +1,6 @@
package art.arcane.iris.api.terrain;
@FunctionalInterface
public interface IrisColumnSink {
void accept(int blockX, int blockZ, int surfaceHeight, IrisSurfaceKind kind, String biomeKey);
}
@@ -0,0 +1,9 @@
package art.arcane.iris.api.terrain;
public enum IrisSurfaceKind {
UNKNOWN,
LAND,
SHORE,
OCEAN,
VOID
}
@@ -0,0 +1,32 @@
package art.arcane.iris.api.terrain;
import org.bukkit.World;
import java.util.Optional;
import java.util.OptionalInt;
public interface IrisTerrainService {
boolean isIrisWorld(World world);
Optional<IrisWorldInfo> worldInfo(World world);
OptionalInt surfaceHeight(World world, int blockX, int blockZ);
IrisSurfaceKind surfaceKind(World world, int blockX, int blockZ);
Optional<String> surfaceBiomeKey(World world, int blockX, int blockZ);
Optional<String> surfaceBiomeName(World world, int blockX, int blockZ);
Optional<String> biomeKey(World world, int blockX, int blockY, int blockZ);
Optional<String> regionKey(World world, int blockX, int blockZ);
Optional<String> regionName(World world, int blockX, int blockZ);
int maxSampleColumns();
int maxSampleChunks();
boolean sampleColumns(World world, IrisColumnQuery query, IrisColumnSink sink);
}
@@ -0,0 +1,24 @@
package art.arcane.iris.api.terrain;
import java.util.Objects;
public record IrisWorldInfo(
String dimensionKey,
String worldIdentity,
long seed,
int minHeight,
int maxHeight,
int fluidHeight,
boolean studio) {
public IrisWorldInfo {
Objects.requireNonNull(dimensionKey, "dimensionKey");
Objects.requireNonNull(worldIdentity, "worldIdentity");
if (maxHeight <= minHeight) {
throw new IllegalArgumentException("maxHeight must exceed minHeight");
}
}
public int height() {
return maxHeight - minHeight;
}
}
@@ -0,0 +1,44 @@
package art.arcane.iris.api.world;
import art.arcane.iris.api.terrain.IrisWorldInfo;
import org.bukkit.World;
import org.bukkit.event.Event;
import org.bukkit.event.HandlerList;
import java.util.Objects;
import java.util.Optional;
public class IrisWorldEngineEvent extends Event {
private static final HandlerList HANDLERS = new HandlerList();
private final World world;
private final IrisWorldPhase phase;
private final IrisWorldInfo info;
public IrisWorldEngineEvent(World world, IrisWorldPhase phase, IrisWorldInfo info) {
this.world = Objects.requireNonNull(world, "world");
this.phase = Objects.requireNonNull(phase, "phase");
this.info = info;
}
public static HandlerList getHandlerList() {
return HANDLERS;
}
public World getWorld() {
return world;
}
public IrisWorldPhase getPhase() {
return phase;
}
public Optional<IrisWorldInfo> getInfo() {
return Optional.ofNullable(info);
}
@Override
public HandlerList getHandlers() {
return HANDLERS;
}
}
@@ -0,0 +1,7 @@
package art.arcane.iris.api.world;
public enum IrisWorldPhase {
ENGINE_READY,
ENGINE_HOTLOADED,
ENGINE_CLOSING
}
@@ -18,100 +18,42 @@
package art.arcane.iris.core.link;
import art.arcane.iris.Iris;
import art.arcane.iris.core.tools.IrisToolbelt;
import art.arcane.iris.engine.object.IrisBiome;
import art.arcane.iris.engine.platform.EngineBukkitOps;
import art.arcane.iris.engine.platform.PlatformChunkGenerator;
import me.clip.placeholderapi.expansion.PlaceholderExpansion;
import org.bukkit.Location;
import org.bukkit.OfflinePlayer;
import org.jetbrains.annotations.NotNull;
import art.arcane.volmlib.util.bukkit.papi.PlaceholderKeyRegistry;
import art.arcane.volmlib.util.bukkit.papi.VolmitPlaceholderExpansion;
// See/update https://app.gitbook.com/@volmitsoftware/s/iris/compatability/papi/
public class IrisPapiExpansion extends PlaceholderExpansion {
@Override
public @NotNull String getIdentifier() {
return "iris";
import java.util.Objects;
import java.util.logging.Logger;
public final class IrisPapiExpansion extends VolmitPlaceholderExpansion {
public static final String IDENTIFIER = "iris";
public static final String AUTHOR = "Volmit Software";
public static final String VERSION = "2.0.0";
public static final String REQUIRED_PLUGIN = "Iris";
public IrisPapiExpansion(IrisPapiState state, Logger logger) {
super(IDENTIFIER, AUTHOR, VERSION, REQUIRED_PLUGIN, registry(state), logger);
}
@Override
public @NotNull String getAuthor() {
return "Volmit Software";
}
public static PlaceholderKeyRegistry registry(IrisPapiState state) {
Objects.requireNonNull(state, "state");
@Override
public @NotNull String getVersion() {
return Iris.instance.getDescription().getVersion();
}
@Override
public boolean persist() {
return true;
}
@Override
public String onRequest(OfflinePlayer player, String p) {
Location l = null;
PlatformChunkGenerator a = null;
if (player.isOnline() && player.getPlayer() != null) {
l = player.getPlayer().getLocation().add(0, 2, 0);
a = IrisToolbelt.access(l.getWorld());
}
if (p.equalsIgnoreCase("biome_name")) {
if (a != null) {
return getBiome(a, l).getName();
}
} else if (p.equalsIgnoreCase("biome_id")) {
if (a != null) {
return getBiome(a, l).getLoadKey();
}
} else if (p.equalsIgnoreCase("biome_file")) {
if (a != null) {
return getBiome(a, l).getLoadFile().getPath();
}
} else if (p.equalsIgnoreCase("region_name")) {
if (a != null) {
return EngineBukkitOps.getRegion(a.getEngine(), l).getName();
}
} else if (p.equalsIgnoreCase("region_id")) {
if (a != null) {
return EngineBukkitOps.getRegion(a.getEngine(), l).getLoadKey();
}
} else if (p.equalsIgnoreCase("region_file")) {
if (a != null) {
return EngineBukkitOps.getRegion(a.getEngine(), l).getLoadFile().getPath();
}
} else if (p.equalsIgnoreCase("terrain_slope")) {
if (a != null) {
return (a.getEngine())
.getComplex().getSlopeStream()
.get(l.getX(), l.getZ()) + "";
}
} else if (p.equalsIgnoreCase("terrain_height")) {
if (a != null) {
return Math.round(a.getEngine().getHeight(l.getBlockX(), l.getBlockZ())) + "";
}
} else if (p.equalsIgnoreCase("world_mode")) {
if (a != null) {
return a.isStudio() ? "Studio" : "Production";
}
} else if (p.equalsIgnoreCase("world_seed")) {
if (a != null) {
return a.getEngine().getSeedManager().getSeed() + "";
}
} else if (p.equalsIgnoreCase("world_speed")) {
if (a != null) {
return a.getEngine().getGeneratedPerSecond() + "/s";
}
}
return null;
}
private IrisBiome getBiome(PlatformChunkGenerator a, Location l) {
return a.getEngine().getBiome(l.getBlockX(), l.getBlockY() - l.getWorld().getMinHeight(), l.getBlockZ());
return PlaceholderKeyRegistry.builder()
.key("available", state::available)
.key("world.available", state::worldAvailable)
.key("world.biome", state::biome)
.key("world.biome-key", state::biomeKey)
.key("world.region", state::region)
.key("world.region-key", state::regionKey)
.key("world.dimension", state::dimension)
.key("pregen.available", state::pregenAvailable)
.key("pregen.world", state::pregenWorld)
.key("pregen.percent", state::pregenPercent)
.key("pregen.eta", state::pregenEta)
.key("pregen.eta-text", state::pregenEtaText)
.key("pregen.chunks", state::pregenChunks)
.key("pregen.total", state::pregenTotal)
.key("pregen.chunks-per-second", state::pregenChunksPerSecond)
.key("pregen.paused", state::pregenPaused)
.build();
}
}
@@ -0,0 +1,14 @@
package art.arcane.iris.core.link;
import art.arcane.volmlib.util.bukkit.papi.PlaceholderRegistration;
import java.util.logging.Logger;
public final class IrisPapiInstaller {
private IrisPapiInstaller() {
}
public static boolean install(PlaceholderRegistration registration, IrisPapiState state, Logger logger) {
return registration.register(() -> new IrisPapiExpansion(state, logger));
}
}
@@ -0,0 +1,104 @@
package art.arcane.iris.core.link;
import art.arcane.iris.api.pregen.IrisPregenerationEvent;
import org.bukkit.Location;
import org.bukkit.World;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority;
import org.bukkit.event.Listener;
import org.bukkit.event.player.PlayerChangedWorldEvent;
import org.bukkit.event.player.PlayerJoinEvent;
import org.bukkit.event.player.PlayerMoveEvent;
import org.bukkit.event.player.PlayerPortalEvent;
import org.bukkit.event.player.PlayerQuitEvent;
import org.bukkit.event.player.PlayerRespawnEvent;
import org.bukkit.event.player.PlayerTeleportEvent;
import java.util.Objects;
import java.util.UUID;
public final class IrisPapiListener implements Listener {
private final IrisPapiState state;
public IrisPapiListener(IrisPapiState state) {
this.state = Objects.requireNonNull(state, "state");
}
static void track(IrisPapiState state, UUID playerId, Location location) {
publish(state, playerId, location, false);
}
static void trackNow(IrisPapiState state, UUID playerId, Location location) {
publish(state, playerId, location, true);
}
private static void publish(IrisPapiState state, UUID playerId, Location location, boolean immediate) {
if (state == null || playerId == null || location == null) {
return;
}
World world = location.getWorld();
if (world == null) {
return;
}
if (immediate) {
state.trackPositionNow(playerId, world, location.getBlockX(), location.getBlockZ());
return;
}
state.trackPosition(playerId, world, location.getBlockX(), location.getBlockZ());
}
@EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
public void onPlayerMove(PlayerMoveEvent event) {
Player player = event.getPlayer();
track(state, player == null ? null : player.getUniqueId(), event.getTo());
}
@EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
public void onPlayerTeleport(PlayerTeleportEvent event) {
Player player = event.getPlayer();
trackNow(state, player == null ? null : player.getUniqueId(), event.getTo());
}
@EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
public void onPlayerPortal(PlayerPortalEvent event) {
Player player = event.getPlayer();
trackNow(state, player == null ? null : player.getUniqueId(), event.getTo());
}
@EventHandler(priority = EventPriority.MONITOR)
public void onPlayerRespawn(PlayerRespawnEvent event) {
Player player = event.getPlayer();
trackNow(state, player == null ? null : player.getUniqueId(), event.getRespawnLocation());
}
@EventHandler(priority = EventPriority.MONITOR)
public void onPlayerJoin(PlayerJoinEvent event) {
Player player = event.getPlayer();
trackNow(state, player == null ? null : player.getUniqueId(), player == null ? null : player.getLocation());
}
@EventHandler(priority = EventPriority.MONITOR)
public void onPlayerChangedWorld(PlayerChangedWorldEvent event) {
Player player = event.getPlayer();
trackNow(state, player == null ? null : player.getUniqueId(), player == null ? null : player.getLocation());
}
@EventHandler(priority = EventPriority.MONITOR)
public void onPlayerQuit(PlayerQuitEvent event) {
Player player = event.getPlayer();
if (player != null) {
state.release(player.getUniqueId());
}
}
@EventHandler(priority = EventPriority.MONITOR)
public void onPregeneration(IrisPregenerationEvent event) {
state.publishPregen(event.getPhase(), event.getProgress());
}
}
@@ -0,0 +1,9 @@
package art.arcane.iris.core.link;
import org.bukkit.World;
public record IrisPapiPosition(World world, int blockX, int blockZ, long publishedAtMs) {
public boolean sameColumn(World other, int otherBlockX, int otherBlockZ) {
return world == other && blockX == otherBlockX && blockZ == otherBlockZ;
}
}
@@ -0,0 +1,51 @@
package art.arcane.iris.core.link;
import art.arcane.iris.api.pregen.IrisPregenProgress;
import art.arcane.volmlib.util.bukkit.papi.PlaceholderValues;
public record IrisPapiPregenView(
String world,
String percent,
String eta,
String etaText,
String chunks,
String total,
String chunksPerSecond,
String paused) {
private static final long MILLIS_PER_SECOND = 1_000L;
private static final long SECONDS_PER_MINUTE = 60L;
private static final long SECONDS_PER_HOUR = 3_600L;
public static IrisPapiPregenView of(IrisPregenProgress progress) {
if (progress == null) {
return null;
}
return new IrisPapiPregenView(
PlaceholderValues.text(progress.worldName()),
PlaceholderValues.num(progress.percent()),
PlaceholderValues.count(progress.etaMillis() / MILLIS_PER_SECOND),
duration(progress.etaMillis()),
PlaceholderValues.count(progress.generatedChunks()),
PlaceholderValues.count(progress.totalChunks()),
PlaceholderValues.num(progress.chunksPerSecond()),
PlaceholderValues.bool(progress.paused()));
}
static String duration(long millis) {
long totalSeconds = millis / MILLIS_PER_SECOND;
long hours = totalSeconds / SECONDS_PER_HOUR;
long minutes = (totalSeconds % SECONDS_PER_HOUR) / SECONDS_PER_MINUTE;
long seconds = totalSeconds % SECONDS_PER_MINUTE;
if (hours > 0L) {
return hours + "h " + minutes + "m";
}
if (minutes > 0L) {
return minutes + "m " + seconds + "s";
}
return seconds + "s";
}
}
@@ -0,0 +1,204 @@
package art.arcane.iris.core.link;
import art.arcane.iris.api.pregen.IrisPregenPhase;
import art.arcane.iris.api.pregen.IrisPregenProgress;
import art.arcane.iris.api.terrain.IrisTerrainService;
import art.arcane.iris.api.terrain.IrisWorldInfo;
import art.arcane.volmlib.util.bukkit.papi.PlaceholderSnapshot;
import art.arcane.volmlib.util.bukkit.papi.PlaceholderValues;
import art.arcane.volmlib.util.bukkit.papi.PlayerSnapshotStore;
import org.bukkit.World;
import java.util.Objects;
import java.util.UUID;
import java.util.function.LongSupplier;
import java.util.function.Supplier;
public final class IrisPapiState {
static final long VIEW_TTL_MS = 1_000L;
static final long POSITION_INTERVAL_MS = 1_000L;
private final Supplier<IrisTerrainService> terrain;
private final LongSupplier clock;
private final PlayerSnapshotStore<IrisPapiPosition> positions = new PlayerSnapshotStore<>();
private final PlayerSnapshotStore<IrisPapiWorldView> views = new PlayerSnapshotStore<>();
private final PlaceholderSnapshot<IrisPapiPregenView> pregen = new PlaceholderSnapshot<>();
public IrisPapiState(Supplier<IrisTerrainService> terrain) {
this(terrain, System::currentTimeMillis);
}
public IrisPapiState(Supplier<IrisTerrainService> terrain, LongSupplier clock) {
this.terrain = Objects.requireNonNull(terrain, "terrain");
this.clock = Objects.requireNonNull(clock, "clock");
}
public void trackPosition(UUID playerId, World world, int blockX, int blockZ) {
publishPosition(playerId, world, blockX, blockZ, false);
}
public void trackPositionNow(UUID playerId, World world, int blockX, int blockZ) {
publishPosition(playerId, world, blockX, blockZ, true);
}
private void publishPosition(UUID playerId, World world, int blockX, int blockZ, boolean immediate) {
if (playerId == null || world == null) {
return;
}
IrisPapiPosition current = positions.get(playerId);
if (current != null && current.sameColumn(world, blockX, blockZ)) {
return;
}
long now = clock.getAsLong();
if (!immediate && current != null && current.world() == world
&& now - current.publishedAtMs() < POSITION_INTERVAL_MS) {
return;
}
positions.publish(playerId, new IrisPapiPosition(world, blockX, blockZ, now));
}
public void release(UUID playerId) {
positions.publish(playerId, null);
views.publish(playerId, null);
}
public void publishPregen(IrisPregenPhase phase, IrisPregenProgress progress) {
if (phase == null || progress == null) {
return;
}
pregen.publish(switch (phase) {
case STARTED, TICK, PAUSED, RESUMED, SAVING -> IrisPapiPregenView.of(progress);
case COMPLETED, CANCELLED -> null;
});
}
public void clear() {
positions.clear();
views.clear();
pregen.publish(null);
}
public String available(UUID playerId) {
return PlaceholderValues.bool(terrain.get() != null);
}
public String worldAvailable(UUID playerId) {
IrisPapiWorldView view = viewOf(playerId);
return view == null ? PlaceholderValues.FALSE : view.available();
}
public String biome(UUID playerId) {
IrisPapiWorldView view = viewOf(playerId);
return view == null ? PlaceholderValues.UNAVAILABLE : view.biome();
}
public String biomeKey(UUID playerId) {
IrisPapiWorldView view = viewOf(playerId);
return view == null ? PlaceholderValues.UNAVAILABLE : view.biomeKey();
}
public String region(UUID playerId) {
IrisPapiWorldView view = viewOf(playerId);
return view == null ? PlaceholderValues.UNAVAILABLE : view.region();
}
public String regionKey(UUID playerId) {
IrisPapiWorldView view = viewOf(playerId);
return view == null ? PlaceholderValues.UNAVAILABLE : view.regionKey();
}
public String dimension(UUID playerId) {
IrisPapiWorldView view = viewOf(playerId);
return view == null ? PlaceholderValues.UNAVAILABLE : view.dimension();
}
public String pregenAvailable(UUID playerId) {
return pregen.available();
}
public String pregenWorld(UUID playerId) {
IrisPapiPregenView view = pregen.get();
return view == null ? PlaceholderValues.UNAVAILABLE : view.world();
}
public String pregenPercent(UUID playerId) {
IrisPapiPregenView view = pregen.get();
return view == null ? PlaceholderValues.UNAVAILABLE : view.percent();
}
public String pregenEta(UUID playerId) {
IrisPapiPregenView view = pregen.get();
return view == null ? PlaceholderValues.UNAVAILABLE : view.eta();
}
public String pregenEtaText(UUID playerId) {
IrisPapiPregenView view = pregen.get();
return view == null ? PlaceholderValues.UNAVAILABLE : view.etaText();
}
public String pregenChunks(UUID playerId) {
IrisPapiPregenView view = pregen.get();
return view == null ? PlaceholderValues.UNAVAILABLE : view.chunks();
}
public String pregenTotal(UUID playerId) {
IrisPapiPregenView view = pregen.get();
return view == null ? PlaceholderValues.UNAVAILABLE : view.total();
}
public String pregenChunksPerSecond(UUID playerId) {
IrisPapiPregenView view = pregen.get();
return view == null ? PlaceholderValues.UNAVAILABLE : view.chunksPerSecond();
}
public String pregenPaused(UUID playerId) {
IrisPapiPregenView view = pregen.get();
return view == null ? PlaceholderValues.UNAVAILABLE : view.paused();
}
private IrisPapiWorldView viewOf(UUID playerId) {
IrisPapiPosition position = positions.get(playerId);
if (position == null) {
return null;
}
IrisPapiWorldView cached = views.get(playerId);
long now = clock.getAsLong();
if (cached != null && cached.position() == position && now - cached.builtAtMs() < VIEW_TTL_MS) {
return cached;
}
IrisPapiWorldView built = build(position, now);
views.publish(playerId, built);
return built;
}
private IrisPapiWorldView build(IrisPapiPosition position, long now) {
IrisTerrainService service = terrain.get();
World world = position.world();
if (service == null || !service.isIrisWorld(world)) {
return IrisPapiWorldView.absent(position, now);
}
int blockX = position.blockX();
int blockZ = position.blockZ();
return IrisPapiWorldView.present(
position,
now,
service.surfaceBiomeName(world, blockX, blockZ),
service.surfaceBiomeKey(world, blockX, blockZ),
service.regionName(world, blockX, blockZ),
service.regionKey(world, blockX, blockZ),
service.worldInfo(world).map(IrisWorldInfo::dimensionKey));
}
}
@@ -0,0 +1,50 @@
package art.arcane.iris.core.link;
import art.arcane.volmlib.util.bukkit.papi.PlaceholderValues;
import java.util.Optional;
public record IrisPapiWorldView(
IrisPapiPosition position,
long builtAtMs,
String available,
String biome,
String biomeKey,
String region,
String regionKey,
String dimension) {
public static IrisPapiWorldView absent(IrisPapiPosition position, long builtAtMs) {
return new IrisPapiWorldView(
position,
builtAtMs,
PlaceholderValues.FALSE,
PlaceholderValues.UNAVAILABLE,
PlaceholderValues.UNAVAILABLE,
PlaceholderValues.UNAVAILABLE,
PlaceholderValues.UNAVAILABLE,
PlaceholderValues.UNAVAILABLE);
}
public static IrisPapiWorldView present(
IrisPapiPosition position,
long builtAtMs,
Optional<String> biome,
Optional<String> biomeKey,
Optional<String> region,
Optional<String> regionKey,
Optional<String> dimension) {
return new IrisPapiWorldView(
position,
builtAtMs,
PlaceholderValues.TRUE,
text(biome),
text(biomeKey),
text(region),
text(regionKey),
text(dimension));
}
static String text(Optional<String> value) {
return value == null || value.isEmpty() ? PlaceholderValues.UNAVAILABLE : PlaceholderValues.text(value.get());
}
}
@@ -18,11 +18,13 @@
package art.arcane.iris.core.runtime;
import art.arcane.iris.api.world.IrisWorldPhase;
import art.arcane.iris.core.ServerConfigurator;
import art.arcane.iris.core.datapack.DatapackIngestService;
import art.arcane.iris.core.events.IrisEngineHotloadEvent;
import art.arcane.iris.core.gui.PregeneratorJob;
import art.arcane.iris.core.project.IrisProject;
import art.arcane.iris.core.service.IrisApiEventSVC;
import art.arcane.iris.core.tools.IrisToolbelt;
import art.arcane.iris.core.tools.WorldMaintenance;
import art.arcane.iris.engine.framework.Engine;
@@ -57,6 +59,7 @@ public final class BukkitEnginePlatformHooks implements EnginePlatformHooks {
@Override
public void fireHotloadEvent(Engine engine) {
IrisPlatforms.get().callEvent(new IrisEngineHotloadEvent(engine));
IrisApiEventSVC.fireWorldPhase(BukkitWorldBinding.world(engine.getWorld()), IrisWorldPhase.ENGINE_HOTLOADED);
}
@Override
@@ -0,0 +1,106 @@
package art.arcane.iris.core.service;
import art.arcane.iris.Iris;
import art.arcane.iris.api.pregen.IrisPregenPhase;
import art.arcane.iris.api.pregen.IrisPregenProgress;
import art.arcane.iris.api.pregen.IrisPregenerationEvent;
import art.arcane.iris.api.terrain.IrisWorldInfo;
import art.arcane.iris.api.world.IrisWorldEngineEvent;
import art.arcane.iris.api.world.IrisWorldPhase;
import art.arcane.iris.core.gui.PregeneratorJob;
import art.arcane.iris.core.pregenerator.PregenApiPhase;
import art.arcane.iris.core.pregenerator.PregenApiSink;
import art.arcane.iris.core.service.terrain.IrisWorldInfoFactory;
import art.arcane.iris.spi.IrisLogging;
import art.arcane.iris.spi.IrisServices;
import art.arcane.iris.util.common.plugin.IrisService;
import org.bukkit.Bukkit;
import org.bukkit.World;
import org.bukkit.event.Event;
public class IrisApiEventSVC implements IrisService, PregenApiSink {
public static void fireWorldPhase(World world, IrisWorldPhase phase) {
if (phase == null) {
return;
}
if (world == null) {
IrisLogging.debug("Iris world API skipped phase " + phase
+ " because the engine has no bound platform world.");
return;
}
try {
deliver(new IrisWorldEngineEvent(world, phase, describe(world, phase)));
} catch (Throwable error) {
IrisLogging.reportError("Iris world API dispatch failed for phase " + phase
+ " on world \"" + world.getName() + "\".", error);
}
}
private static IrisWorldInfo describe(World world, IrisWorldPhase phase) {
try {
return IrisWorldInfoFactory.forWorld(world);
} catch (Throwable error) {
IrisLogging.reportError("Iris world API could not describe world \"" + world.getName()
+ "\" for phase " + phase + "; the event is delivered without world info.", error);
return null;
}
}
private static void deliver(Event event) {
if (Bukkit.isPrimaryThread()) {
Bukkit.getPluginManager().callEvent(event);
return;
}
Iris.callEvent(event);
}
private static IrisPregenPhase toApi(PregenApiPhase phase) {
return switch (phase) {
case STARTED -> IrisPregenPhase.STARTED;
case TICK -> IrisPregenPhase.TICK;
case PAUSED -> IrisPregenPhase.PAUSED;
case RESUMED -> IrisPregenPhase.RESUMED;
case SAVING -> IrisPregenPhase.SAVING;
case COMPLETED -> IrisPregenPhase.COMPLETED;
case CANCELLED -> IrisPregenPhase.CANCELLED;
};
}
private static IrisPregenProgress toApi(PregeneratorJob.PregenProgress progress) {
return new IrisPregenProgress(
progress.worldName(),
progress.worldIdentity(),
progress.percent(),
progress.generated(),
progress.totalChunks(),
progress.chunksRemaining(),
progress.failed(),
progress.chunksPerSecond(),
progress.eta(),
progress.elapsed(),
progress.method(),
progress.paused()
);
}
@Override
public void onEnable() {
IrisServices.register(PregenApiSink.class, this);
}
@Override
public void onDisable() {
IrisServices.remove(PregenApiSink.class);
}
@Override
public void pregen(PregenApiPhase phase, PregeneratorJob.PregenProgress progress) {
if (phase == null || progress == null || progress.worldIdentity() == null) {
return;
}
Iris.callEvent(new IrisPregenerationEvent(toApi(phase), toApi(progress)));
}
}
@@ -61,6 +61,7 @@ public final class IrisEngineSVC implements IrisService {
new AtomicReference<>(IrisTelemetrySnapshot.EMPTY);
private final Map<World, CompletableFuture<Void>> pendingRegistrations = new HashMap<>();
private final Map<World, Registered> worlds = new ConcurrentHashMap<>();
private final IrisWorldPhaseLedger phases = new IrisWorldPhaseLedger();
private volatile ScheduledThreadPoolExecutor service;
private volatile ScheduledFuture<?> metricsTask;
@@ -105,26 +106,28 @@ public final class IrisEngineSVC implements IrisService {
activeMetricsTask.cancel(false);
}
List<Registered> registeredWorlds;
List<ClosingGenerator> reservedCloses = new ArrayList<>();
List<Teardown> teardowns = new ArrayList<>();
List<CompletableFuture<Void>> generatorCloses;
synchronized (registrationLock) {
registeredWorlds = List.copyOf(worlds.values());
for (Map.Entry<World, Registered> entry : worlds.entrySet()) {
Registered registered = entry.getValue();
registered.close();
teardowns.add(new Teardown(entry.getKey(), registered, reserveClose(registered)));
}
worlds.clear();
pendingRegistrations.clear();
for (Registered registered : registeredWorlds) {
registered.close();
reservedCloses.add(reserveClose(registered));
}
generatorCloses = new ArrayList<>(closingGenerators.size());
for (ClosingGenerator closing : closingGenerators) {
generatorCloses.add(closing.completion());
}
}
for (Teardown teardown : teardowns) {
phases.closing(teardown.world());
}
shutdownAndDrain(activeService);
for (int index = 0; index < registeredWorlds.size(); index++) {
startClose(registeredWorlds.get(index), reservedCloses.get(index));
for (Teardown teardown : teardowns) {
startClose(teardown.registered(), teardown.closing());
}
awaitGeneratorShutdown(generatorCloses);
resetMetrics();
@@ -177,6 +180,7 @@ public final class IrisEngineSVC implements IrisService {
Registered replaced = null;
ClosingGenerator replacementClose = null;
CompletableFuture<Void> retryAfter = null;
boolean registered = false;
try {
synchronized (registrationLock) {
if (service != activeService || activeService.isShutdown() || !isCurrentWorld(world)) {
@@ -202,6 +206,7 @@ public final class IrisEngineSVC implements IrisService {
Registration registration = new Registration(world.getName(), access, registrationIdentity);
worlds.put(world, new Registered(registration, activeService));
pendingRegistrations.remove(world);
registered = true;
}
}
}
@@ -211,7 +216,11 @@ public final class IrisEngineSVC implements IrisService {
}
}
if (registered) {
phases.ready(world);
}
if (replacementClose != null) {
phases.closing(world);
retryRegistrationAfterClose(world, retryAfter);
startClose(replaced, replacementClose);
return;
@@ -237,6 +246,7 @@ public final class IrisEngineSVC implements IrisService {
}
}
if (closing != null) {
phases.closing(world);
startClose(registered, closing);
}
}
@@ -697,4 +707,7 @@ public final class IrisEngineSVC implements IrisService {
private record ClosingGenerator(RegistrationIdentity registrationIdentity,
CompletableFuture<Void> completion) {
}
private record Teardown(World world, Registered registered, ClosingGenerator closing) {
}
}
@@ -0,0 +1,316 @@
package art.arcane.iris.core.service;
import art.arcane.iris.api.terrain.IrisColumnField;
import art.arcane.iris.api.terrain.IrisColumnQuery;
import art.arcane.iris.api.terrain.IrisColumnSink;
import art.arcane.iris.api.terrain.IrisSurfaceKind;
import art.arcane.iris.api.terrain.IrisTerrainService;
import art.arcane.iris.api.terrain.IrisWorldInfo;
import art.arcane.iris.core.IrisSettings;
import art.arcane.iris.core.service.terrain.IrisApiFaultGuard;
import art.arcane.iris.core.service.terrain.IrisColumnWalk;
import art.arcane.iris.core.service.terrain.IrisSampleLimits;
import art.arcane.iris.core.service.terrain.IrisSurfaceClassifier;
import art.arcane.iris.core.service.terrain.IrisWorldInfoFactory;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.object.InferredType;
import art.arcane.iris.engine.object.IrisBiome;
import art.arcane.iris.engine.object.IrisRegion;
import art.arcane.iris.engine.platform.PlatformChunkGenerator;
import art.arcane.iris.platform.bukkit.BukkitPlatform;
import art.arcane.iris.spi.IrisLogging;
import art.arcane.iris.spi.IrisServices;
import art.arcane.iris.util.common.plugin.IrisService;
import org.bukkit.Bukkit;
import org.bukkit.World;
import org.bukkit.generator.ChunkGenerator;
import org.bukkit.plugin.ServicePriority;
import java.util.EnumSet;
import java.util.Optional;
import java.util.OptionalInt;
import java.util.concurrent.atomic.AtomicBoolean;
public class IrisTerrainSVC implements IrisService, IrisTerrainService {
private static final long FAULT_REPORT_INTERVAL_MILLIS = 60_000L;
private final AtomicBoolean serviceEnabled = new AtomicBoolean();
private final IrisApiFaultGuard queryFaults = new IrisApiFaultGuard(FAULT_REPORT_INTERVAL_MILLIS);
private final IrisApiFaultGuard sinkFaults = new IrisApiFaultGuard(FAULT_REPORT_INTERVAL_MILLIS);
@Override
public void onEnable() {
serviceEnabled.set(true);
Bukkit.getServicesManager().register(
IrisTerrainService.class,
this,
BukkitPlatform.plugin(),
ServicePriority.Normal
);
IrisServices.register(IrisTerrainService.class, this);
}
@Override
public void onDisable() {
serviceEnabled.set(false);
Bukkit.getServicesManager().unregister(IrisTerrainService.class, this);
IrisServices.remove(IrisTerrainService.class);
}
@Override
public boolean isIrisWorld(World world) {
return generatorOf(world) != null;
}
@Override
public Optional<IrisWorldInfo> worldInfo(World world) {
PlatformChunkGenerator generator = liveGeneratorOf(world);
if (generator == null) {
return Optional.empty();
}
try {
return Optional.ofNullable(IrisWorldInfoFactory.from(generator));
} catch (Throwable error) {
reportQueryFault("worldInfo", world, error);
return Optional.empty();
}
}
@Override
public OptionalInt surfaceHeight(World world, int blockX, int blockZ) {
Engine engine = liveEngineOf(world);
if (engine == null) {
return OptionalInt.empty();
}
try {
return OptionalInt.of(engine.getHeight(blockX, blockZ) + engine.getMinHeight());
} catch (Throwable error) {
reportQueryFault("surfaceHeight", world, error);
return OptionalInt.empty();
}
}
@Override
public IrisSurfaceKind surfaceKind(World world, int blockX, int blockZ) {
Engine engine = liveEngineOf(world);
if (engine == null) {
return IrisSurfaceKind.UNKNOWN;
}
try {
int surface = engine.getHeight(blockX, blockZ);
int fluid = engine.getDimension().getFluidHeight();
InferredType inferredType = null;
if (IrisSurfaceClassifier.requiresSurfaceBiome(surface, fluid)) {
IrisBiome biome = engine.getSurfaceBiome(blockX, blockZ);
inferredType = biome == null ? null : biome.getInferredType();
}
return IrisSurfaceClassifier.classify(surface, fluid, inferredType);
} catch (Throwable error) {
reportQueryFault("surfaceKind", world, error);
return IrisSurfaceKind.UNKNOWN;
}
}
@Override
public Optional<String> surfaceBiomeKey(World world, int blockX, int blockZ) {
Engine engine = liveEngineOf(world);
if (engine == null) {
return Optional.empty();
}
try {
return key(engine.getSurfaceBiome(blockX, blockZ));
} catch (Throwable error) {
reportQueryFault("surfaceBiomeKey", world, error);
return Optional.empty();
}
}
@Override
public Optional<String> surfaceBiomeName(World world, int blockX, int blockZ) {
Engine engine = liveEngineOf(world);
if (engine == null) {
return Optional.empty();
}
try {
return name(engine.getSurfaceBiome(blockX, blockZ));
} catch (Throwable error) {
reportQueryFault("surfaceBiomeName", world, error);
return Optional.empty();
}
}
@Override
public Optional<String> biomeKey(World world, int blockX, int blockY, int blockZ) {
Engine engine = liveEngineOf(world);
if (engine == null) {
return Optional.empty();
}
try {
return key(engine.getBiome(blockX, blockY - engine.getMinHeight(), blockZ));
} catch (Throwable error) {
reportQueryFault("biomeKey", world, error);
return Optional.empty();
}
}
@Override
public Optional<String> regionKey(World world, int blockX, int blockZ) {
Engine engine = liveEngineOf(world);
if (engine == null) {
return Optional.empty();
}
try {
IrisRegion region = engine.getRegion(blockX, blockZ);
String loadKey = region == null ? null : region.getLoadKey();
return loadKey == null || loadKey.isEmpty() ? Optional.empty() : Optional.of(loadKey);
} catch (Throwable error) {
reportQueryFault("regionKey", world, error);
return Optional.empty();
}
}
@Override
public Optional<String> regionName(World world, int blockX, int blockZ) {
Engine engine = liveEngineOf(world);
if (engine == null) {
return Optional.empty();
}
try {
IrisRegion region = engine.getRegion(blockX, blockZ);
String name = region == null ? null : region.getName();
return name == null || name.isEmpty() ? Optional.empty() : Optional.of(name);
} catch (Throwable error) {
reportQueryFault("regionName", world, error);
return Optional.empty();
}
}
@Override
public int maxSampleColumns() {
return IrisSampleLimits.maxColumns(noiseCacheChunks());
}
@Override
public int maxSampleChunks() {
return IrisSampleLimits.maxChunks(noiseCacheChunks());
}
@Override
public boolean sampleColumns(World world, IrisColumnQuery query, IrisColumnSink sink) {
if (query == null || sink == null) {
return false;
}
Engine engine = liveEngineOf(world);
if (engine == null) {
return false;
}
int noiseCacheChunks = noiseCacheChunks();
if (!IrisSampleLimits.withinLimits(
query,
IrisSampleLimits.maxColumns(noiseCacheChunks),
IrisSampleLimits.maxChunks(noiseCacheChunks))) {
return false;
}
EnumSet<IrisColumnField> fields = query.fields();
boolean wantHeight = fields.contains(IrisColumnField.SURFACE_HEIGHT);
boolean wantKind = fields.contains(IrisColumnField.SURFACE_KIND);
boolean wantBiome = fields.contains(IrisColumnField.BIOME_KEY);
try {
int minHeight = engine.getMinHeight();
int fluid = engine.getDimension().getFluidHeight();
long visited = IrisColumnWalk.walk(query, (int blockX, int blockZ) -> {
if (engine.isClosed()) {
return false;
}
int surface = wantHeight || wantKind ? engine.getHeight(blockX, blockZ) : 0;
boolean needsBiome = wantBiome
|| (wantKind && IrisSurfaceClassifier.requiresSurfaceBiome(surface, fluid));
IrisBiome biome = needsBiome ? engine.getSurfaceBiome(blockX, blockZ) : null;
IrisSurfaceKind kind = wantKind
? IrisSurfaceClassifier.classify(surface, fluid, biome == null ? null : biome.getInferredType())
: IrisSurfaceKind.UNKNOWN;
String biomeKey = wantBiome && biome != null ? biome.getLoadKey() : null;
sink.accept(blockX, blockZ, wantHeight ? surface + minHeight : -1, kind, biomeKey);
return true;
});
return visited == query.columnCount();
} catch (Throwable error) {
reportSinkFault(world, error);
return false;
}
}
private static Optional<String> key(IrisBiome biome) {
String loadKey = biome == null ? null : biome.getLoadKey();
return loadKey == null || loadKey.isEmpty() ? Optional.empty() : Optional.of(loadKey);
}
private static Optional<String> name(IrisBiome biome) {
String name = biome == null ? null : biome.getName();
return name == null || name.isEmpty() ? Optional.empty() : Optional.of(name);
}
private static int noiseCacheChunks() {
return IrisSettings.get().getPerformance().getNoiseCacheSize();
}
static boolean answerable(boolean serviceEnabled, boolean worldPresent) {
return serviceEnabled && worldPresent;
}
private PlatformChunkGenerator generatorOf(World world) {
if (!answerable(serviceEnabled.get(), world != null)) {
return null;
}
ChunkGenerator generator = world.getGenerator();
return generator instanceof PlatformChunkGenerator platform ? platform : null;
}
private PlatformChunkGenerator liveGeneratorOf(World world) {
PlatformChunkGenerator generator = generatorOf(world);
return generator == null || generator.isClosing() ? null : generator;
}
private static Engine engineOf(PlatformChunkGenerator generator) {
if (generator == null) {
return null;
}
Engine engine = generator.getEngine();
return engine == null || engine.isClosed() ? null : engine;
}
private Engine liveEngineOf(World world) {
return engineOf(liveGeneratorOf(world));
}
private void reportQueryFault(String operation, World world, Throwable error) {
if (queryFaults.record(System.currentTimeMillis())) {
IrisLogging.reportError("Iris terrain API query \"" + operation + "\" failed for world \""
+ (world == null ? "null" : world.getName()) + "\" (" + queryFaults.faults()
+ " terrain API query faults so far).", error);
}
}
private void reportSinkFault(World world, Throwable error) {
if (sinkFaults.record(System.currentTimeMillis())) {
IrisLogging.reportError("Iris terrain API column sample failed for world \""
+ (world == null ? "null" : world.getName()) + "\" (" + sinkFaults.faults()
+ " terrain API sample faults so far). A third-party sink that throws is treated as a refusal.", error);
}
}
}
@@ -0,0 +1,49 @@
package art.arcane.iris.core.service;
import art.arcane.iris.api.world.IrisWorldPhase;
import org.bukkit.World;
import java.util.Objects;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
final class IrisWorldPhaseLedger {
@FunctionalInterface
interface Dispatch {
void fire(World world, IrisWorldPhase phase);
}
private final Set<UUID> announced = ConcurrentHashMap.newKeySet();
private final Dispatch dispatch;
IrisWorldPhaseLedger() {
this(IrisApiEventSVC::fireWorldPhase);
}
IrisWorldPhaseLedger(Dispatch dispatch) {
this.dispatch = Objects.requireNonNull(dispatch, "dispatch");
}
void ready(World world) {
UUID identity = identityOf(world);
if (identity == null || !announced.add(identity)) {
return;
}
dispatch.fire(world, IrisWorldPhase.ENGINE_READY);
}
void closing(World world) {
UUID identity = identityOf(world);
if (identity == null || !announced.remove(identity)) {
return;
}
dispatch.fire(world, IrisWorldPhase.ENGINE_CLOSING);
}
private static UUID identityOf(World world) {
return world == null ? null : world.getUID();
}
}
@@ -0,0 +1,31 @@
package art.arcane.iris.core.service.terrain;
import java.util.concurrent.atomic.AtomicLong;
public final class IrisApiFaultGuard {
private static final long NEVER = Long.MIN_VALUE;
private final long reportIntervalMillis;
private final AtomicLong faults = new AtomicLong();
private final AtomicLong lastReportedAt = new AtomicLong(NEVER);
public IrisApiFaultGuard(long reportIntervalMillis) {
if (reportIntervalMillis < 0L) {
throw new IllegalArgumentException("reportIntervalMillis must not be negative");
}
this.reportIntervalMillis = reportIntervalMillis;
}
public long faults() {
return faults.get();
}
public boolean record(long nowMillis) {
faults.incrementAndGet();
long last = lastReportedAt.get();
if (last != NEVER && nowMillis - last < reportIntervalMillis) {
return false;
}
return lastReportedAt.compareAndSet(last, nowMillis);
}
}
@@ -0,0 +1,49 @@
package art.arcane.iris.core.service.terrain;
import art.arcane.iris.api.terrain.IrisColumnQuery;
public final class IrisColumnWalk {
private IrisColumnWalk() {
}
public static long walk(IrisColumnQuery query, ColumnVisitor visitor) {
int stride = query.strideBlocks();
int minChunkX = query.minBlockX() >> 4;
int maxChunkX = query.maxBlockX() >> 4;
int minChunkZ = query.minBlockZ() >> 4;
int maxChunkZ = query.maxBlockZ() >> 4;
long visited = 0L;
for (int chunkZ = minChunkZ; chunkZ <= maxChunkZ; chunkZ++) {
int chunkMinBlockZ = Math.max(query.minBlockZ(), chunkZ << 4);
int chunkMaxBlockZ = Math.min(query.maxBlockZ(), (chunkZ << 4) + 15);
for (int chunkX = minChunkX; chunkX <= maxChunkX; chunkX++) {
int chunkMinBlockX = Math.max(query.minBlockX(), chunkX << 4);
int chunkMaxBlockX = Math.min(query.maxBlockX(), (chunkX << 4) + 15);
for (int blockZ = align(query.minBlockZ(), chunkMinBlockZ, stride); blockZ <= chunkMaxBlockZ; blockZ += stride) {
for (int blockX = align(query.minBlockX(), chunkMinBlockX, stride); blockX <= chunkMaxBlockX; blockX += stride) {
if (!visitor.visit(blockX, blockZ)) {
return visited;
}
visited++;
}
}
}
}
return visited;
}
private static int align(int origin, int lowerBound, int stride) {
long offset = (long) lowerBound - (long) origin;
long steps = (offset + stride - 1L) / stride;
return (int) (origin + steps * stride);
}
@FunctionalInterface
public interface ColumnVisitor {
boolean visit(int blockX, int blockZ);
}
}
@@ -0,0 +1,24 @@
package art.arcane.iris.core.service.terrain;
import art.arcane.iris.api.terrain.IrisColumnQuery;
public final class IrisSampleLimits {
public static final int MINIMUM_CHUNKS = 64;
public static final int CACHE_SHARE_DIVISOR = 4;
private IrisSampleLimits() {
}
public static int maxChunks(int noiseCacheChunks) {
return Math.max(MINIMUM_CHUNKS, noiseCacheChunks / CACHE_SHARE_DIVISOR);
}
public static int maxColumns(int noiseCacheChunks) {
long columns = (long) maxChunks(noiseCacheChunks) * 256L;
return (int) Math.min(columns, Integer.MAX_VALUE);
}
public static boolean withinLimits(IrisColumnQuery query, int maxColumns, int maxChunks) {
return query.columnCount() <= maxColumns && query.chunkCount() <= maxChunks;
}
}
@@ -0,0 +1,25 @@
package art.arcane.iris.core.service.terrain;
import art.arcane.iris.api.terrain.IrisSurfaceKind;
import art.arcane.iris.engine.object.InferredType;
public final class IrisSurfaceClassifier {
private IrisSurfaceClassifier() {
}
public static boolean requiresSurfaceBiome(int engineSurfaceHeight, int engineFluidHeight) {
return engineSurfaceHeight > 0 && engineSurfaceHeight > engineFluidHeight;
}
public static IrisSurfaceKind classify(int engineSurfaceHeight, int engineFluidHeight, InferredType inferredType) {
if (engineSurfaceHeight <= 0) {
return IrisSurfaceKind.VOID;
}
if (engineSurfaceHeight <= engineFluidHeight) {
return IrisSurfaceKind.OCEAN;
}
return inferredType == InferredType.SHORE ? IrisSurfaceKind.SHORE : IrisSurfaceKind.LAND;
}
}
@@ -0,0 +1,71 @@
package art.arcane.iris.core.service.terrain;
import art.arcane.iris.api.terrain.IrisWorldInfo;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.object.IrisDimension;
import art.arcane.iris.engine.object.IrisWorld;
import art.arcane.iris.engine.platform.PlatformChunkGenerator;
import org.bukkit.World;
import org.bukkit.generator.ChunkGenerator;
public final class IrisWorldInfoFactory {
private IrisWorldInfoFactory() {
}
public static IrisWorldInfo forWorld(World world) {
if (world == null) {
return null;
}
ChunkGenerator generator = world.getGenerator();
return generator instanceof PlatformChunkGenerator platform ? from(platform) : null;
}
public static IrisWorldInfo from(PlatformChunkGenerator generator) {
if (generator == null) {
return null;
}
Engine engine = generator.getEngine();
if (engine == null || engine.isClosed()) {
return null;
}
IrisWorld irisWorld = engine.getWorld();
IrisDimension dimension = engine.getDimension();
if (irisWorld == null || dimension == null) {
return null;
}
return build(
dimension.getLoadKey(),
irisWorld.identity(),
irisWorld.getRawWorldSeed(),
engine.getMinHeight(),
engine.getMaxHeight(),
dimension.getFluidHeight(),
generator.isStudio());
}
static IrisWorldInfo build(
String dimensionKey,
String worldIdentity,
long seed,
int minHeight,
int maxHeight,
int fluidHeightAboveMinimum,
boolean studio) {
if (dimensionKey == null || worldIdentity == null || maxHeight <= minHeight) {
return null;
}
return new IrisWorldInfo(
dimensionKey,
worldIdentity,
seed,
minHeight,
maxHeight,
fluidHeightAboveMinimum + minHeight,
studio);
}
}
@@ -0,0 +1,22 @@
package art.arcane.iris;
import art.arcane.iris.core.pregenerator.PregenApiSink;
import art.arcane.iris.core.service.IrisApiEventSVC;
import art.arcane.iris.core.service.IrisTerrainSVC;
import art.arcane.iris.util.common.plugin.IrisService;
import org.junit.Test;
import static org.junit.Assert.assertTrue;
public class IrisApiServiceDiscoveryTest {
@Test
public void bothApiServicesSatisfyTheServiceLoaderRule() {
assertTrue(Iris.isConcreteImplementation(IrisTerrainSVC.class, IrisService.class));
assertTrue(Iris.isConcreteImplementation(IrisApiEventSVC.class, IrisService.class));
}
@Test
public void theEventServiceIsTheSinkThePregeneratorLooksUp() {
assertTrue(PregenApiSink.class.isAssignableFrom(IrisApiEventSVC.class));
}
}
@@ -0,0 +1,154 @@
package art.arcane.iris.api;
import art.arcane.iris.api.terrain.IrisTerrainService;
import org.junit.Test;
import java.io.IOException;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.lang.reflect.Type;
import java.lang.reflect.TypeVariable;
import java.lang.reflect.WildcardType;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.GenericArrayType;
import java.net.URISyntaxException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;
import java.util.stream.Stream;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
public class IrisApiSurfacePurityTest {
private static final String API_PACKAGE = "art.arcane.iris.api";
private static final List<String> ALLOWED_PREFIXES = List.of(
"java.",
"javax.",
"org.bukkit.",
API_PACKAGE + "."
);
@Test
public void everyApiTypeIsReachableWithoutIris() throws IOException, URISyntaxException, ClassNotFoundException {
List<Class<?>> apiTypes = apiTypes();
assertTrue("the api package must contain types", apiTypes.size() >= 11);
Set<String> violations = new LinkedHashSet<>();
for (Class<?> apiType : apiTypes) {
collect(apiType.getGenericSuperclass(), violations, apiType);
for (Type implemented : apiType.getGenericInterfaces()) {
collect(implemented, violations, apiType);
}
for (Method method : apiType.getDeclaredMethods()) {
if (!isExported(method.getModifiers())) {
continue;
}
collect(method.getGenericReturnType(), violations, apiType);
for (Type parameter : method.getGenericParameterTypes()) {
collect(parameter, violations, apiType);
}
for (Type thrown : method.getGenericExceptionTypes()) {
collect(thrown, violations, apiType);
}
}
for (Constructor<?> constructor : apiType.getDeclaredConstructors()) {
if (!isExported(constructor.getModifiers())) {
continue;
}
for (Type parameter : constructor.getGenericParameterTypes()) {
collect(parameter, violations, apiType);
}
}
for (Field field : apiType.getDeclaredFields()) {
if (!isExported(field.getModifiers())) {
continue;
}
collect(field.getGenericType(), violations, apiType);
}
}
assertEquals("Iris API types must only expose java, bukkit and Iris API types: " + violations,
Set.of(), violations);
}
@Test
public void theTerrainServiceIsPartOfTheScannedSurface() throws IOException, URISyntaxException, ClassNotFoundException {
assertTrue(apiTypes().contains(IrisTerrainService.class));
}
private static boolean isExported(int modifiers) {
return Modifier.isPublic(modifiers) || Modifier.isProtected(modifiers);
}
private static void collect(Type type, Set<String> violations, Class<?> owner) {
if (type == null) {
return;
}
switch (type) {
case Class<?> raw -> {
Class<?> component = raw;
while (component.isArray()) {
component = component.getComponentType();
}
if (component.isPrimitive()) {
return;
}
String name = component.getName();
if (ALLOWED_PREFIXES.stream().noneMatch(name::startsWith)) {
violations.add(owner.getName() + " -> " + name);
}
}
case ParameterizedType parameterized -> {
collect(parameterized.getRawType(), violations, owner);
for (Type argument : parameterized.getActualTypeArguments()) {
collect(argument, violations, owner);
}
}
case GenericArrayType array -> collect(array.getGenericComponentType(), violations, owner);
case WildcardType wildcard -> {
for (Type bound : wildcard.getUpperBounds()) {
collect(bound, violations, owner);
}
for (Type bound : wildcard.getLowerBounds()) {
collect(bound, violations, owner);
}
}
case TypeVariable<?> variable -> {
for (Type bound : variable.getBounds()) {
collect(bound, violations, owner);
}
}
default -> violations.add(owner.getName() + " -> unresolvable type " + type);
}
}
private static List<Class<?>> apiTypes() throws IOException, URISyntaxException, ClassNotFoundException {
Path classesRoot = Path.of(IrisTerrainService.class.getProtectionDomain()
.getCodeSource().getLocation().toURI());
Path apiRoot = classesRoot.resolve(API_PACKAGE.replace('.', '/'));
assertTrue("compiled api package not found at " + apiRoot, Files.isDirectory(apiRoot));
List<String> names = new ArrayList<>();
try (Stream<Path> files = Files.walk(apiRoot)) {
files.filter(path -> path.getFileName().toString().endsWith(".class"))
.forEach(path -> names.add(classesRoot.relativize(path).toString()
.replace('/', '.')
.replace('\\', '.')
.replaceAll("\\.class$", "")));
}
names.sort(String::compareTo);
List<Class<?>> types = new ArrayList<>(names.size());
for (String name : names) {
types.add(Class.forName(name, false, IrisApiSurfacePurityTest.class.getClassLoader()));
}
return types;
}
}
@@ -0,0 +1,50 @@
package art.arcane.iris.api.pregen;
import org.junit.Test;
import static org.junit.Assert.assertEquals;
public class IrisPregenProgressTest {
@Test
public void anAbsentWorldNameFallsBackToTheIdentity() {
IrisPregenProgress progress = new IrisPregenProgress(
null, "minecraft:world", 12D, 1L, 2L, 1L, 0L, 3D, 4L, 5L, null, false);
assertEquals("minecraft:world", progress.worldName());
assertEquals("", progress.method());
}
@Test
public void hostileNumbersAreNormalisedRatherThanPropagated() {
IrisPregenProgress progress = new IrisPregenProgress(
"world", "minecraft:world", 400D, -1L, -2L, -3L, -4L, -5D, -6L, -7L, "hybrid", true);
assertEquals(100D, progress.percent(), 0D);
assertEquals(0L, progress.generatedChunks());
assertEquals(0L, progress.totalChunks());
assertEquals(0L, progress.remainingChunks());
assertEquals(0L, progress.failedChunks());
assertEquals(0D, progress.chunksPerSecond(), 0D);
assertEquals(0L, progress.etaMillis());
assertEquals(0L, progress.elapsedMillis());
}
@Test
public void nonFiniteRatesCollapseToZeroInsteadOfLeaking() {
IrisPregenProgress nan = new IrisPregenProgress(
"world", "minecraft:world", Double.NaN, 0L, 0L, 0L, 0L, Double.NaN, 0L, 0L, "hybrid", false);
IrisPregenProgress infinite = new IrisPregenProgress(
"world", "minecraft:world", Double.POSITIVE_INFINITY, 0L, 0L, 0L, 0L,
Double.POSITIVE_INFINITY, 0L, 0L, "hybrid", false);
assertEquals(0D, nan.percent(), 0D);
assertEquals(0D, nan.chunksPerSecond(), 0D);
assertEquals(0D, infinite.percent(), 0D);
assertEquals(0D, infinite.chunksPerSecond(), 0D);
}
@Test(expected = NullPointerException.class)
public void aProgressWithoutAWorldIdentityIsRejected() {
new IrisPregenProgress("world", null, 0D, 0L, 0L, 0L, 0L, 0D, 0L, 0L, "hybrid", false);
}
}
@@ -0,0 +1,77 @@
package art.arcane.iris.api.terrain;
import org.junit.Test;
import java.util.EnumSet;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
public class IrisColumnQueryTest {
@Test
public void fieldsAreCopiedOnBothSidesOfTheBoundary() {
EnumSet<IrisColumnField> supplied = EnumSet.of(IrisColumnField.SURFACE_KIND);
IrisColumnQuery query = IrisColumnQuery.rect(0, 0, 15, 15, 1, supplied);
supplied.add(IrisColumnField.BIOME_KEY);
assertEquals(EnumSet.of(IrisColumnField.SURFACE_KIND), query.fields());
EnumSet<IrisColumnField> returned = query.fields();
returned.add(IrisColumnField.SURFACE_HEIGHT);
assertEquals(EnumSet.of(IrisColumnField.SURFACE_KIND), query.fields());
}
@Test
public void columnCountAndChunkCountAreDecoupledByStride() {
IrisColumnQuery query = IrisColumnQuery.rect(
0, 0, 6399, 6399, 64, EnumSet.of(IrisColumnField.SURFACE_KIND));
assertEquals(10_000L, query.columnCount());
assertEquals(160_000L, query.chunkCount());
assertTrue("chunk span must dominate the column count for a strided query",
query.chunkCount() > query.columnCount());
}
@Test
public void countsAreExactForASingleChunk() {
IrisColumnQuery query = IrisColumnQuery.rect(
0, 0, 15, 15, 1, EnumSet.of(IrisColumnField.SURFACE_HEIGHT));
assertEquals(256L, query.columnCount());
assertEquals(1L, query.chunkCount());
}
@Test
public void countsSurviveNegativeCoordinates() {
IrisColumnQuery query = IrisColumnQuery.rect(
-32, -32, -1, -1, 8, EnumSet.of(IrisColumnField.SURFACE_HEIGHT));
assertEquals(16L, query.columnCount());
assertEquals(4L, query.chunkCount());
}
@Test
public void countsSaturateInsteadOfWrappingNegativePastTheCaps() {
IrisColumnQuery query = IrisColumnQuery.rect(
Integer.MIN_VALUE, Integer.MIN_VALUE, Integer.MAX_VALUE, Integer.MAX_VALUE, 1,
EnumSet.of(IrisColumnField.SURFACE_HEIGHT));
assertEquals(Long.MAX_VALUE, query.columnCount());
assertTrue(query.chunkCount() > 0L);
}
@Test(expected = IllegalArgumentException.class)
public void emptyFieldSetIsRejected() {
IrisColumnQuery.rect(0, 0, 15, 15, 1, EnumSet.noneOf(IrisColumnField.class));
}
@Test(expected = IllegalArgumentException.class)
public void invertedBoundsAreRejected() {
IrisColumnQuery.rect(16, 0, 0, 15, 1, EnumSet.of(IrisColumnField.SURFACE_KIND));
}
@Test(expected = IllegalArgumentException.class)
public void zeroStrideIsRejected() {
IrisColumnQuery.rect(0, 0, 15, 15, 0, EnumSet.of(IrisColumnField.SURFACE_KIND));
}
}
@@ -0,0 +1,30 @@
package art.arcane.iris.api.terrain;
import org.junit.Test;
import static org.junit.Assert.assertEquals;
public class IrisWorldInfoTest {
@Test
public void heightIsDerivedFromTheAbsoluteBounds() {
IrisWorldInfo info = new IrisWorldInfo("overworld", "minecraft:world", 42L, -64, 320, 63, false);
assertEquals(384, info.height());
assertEquals(63, info.fluidHeight());
}
@Test(expected = IllegalArgumentException.class)
public void collapsedHeightRangeIsRejected() {
new IrisWorldInfo("overworld", "minecraft:world", 42L, 0, 0, 63, false);
}
@Test(expected = NullPointerException.class)
public void nullDimensionKeyIsRejected() {
new IrisWorldInfo(null, "minecraft:world", 42L, -64, 320, 63, false);
}
@Test(expected = NullPointerException.class)
public void nullWorldIdentityIsRejected() {
new IrisWorldInfo("overworld", null, 42L, -64, 320, 63, false);
}
}
@@ -0,0 +1,64 @@
package art.arcane.iris.api.world;
import art.arcane.iris.api.terrain.IrisWorldInfo;
import org.bukkit.World;
import org.junit.Test;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.Optional;
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.assertThrows;
public class IrisWorldEngineEventTest {
private static final IrisWorldInfo INFO =
new IrisWorldInfo("overworld", "minecraft:world", 7L, -64, 320, -1, false);
@Test
public void aPhaseIsStillReportableWhenTheEngineCannotBeDescribed() {
IrisWorldEngineEvent event = new IrisWorldEngineEvent(world(), IrisWorldPhase.ENGINE_CLOSING, null);
assertEquals(IrisWorldPhase.ENGINE_CLOSING, event.getPhase());
assertFalse(event.getInfo().isPresent());
}
@Test
public void aDescribableEngineCarriesItsInfo() {
World world = world();
IrisWorldEngineEvent event = new IrisWorldEngineEvent(world, IrisWorldPhase.ENGINE_READY, INFO);
assertSame(world, event.getWorld());
assertEquals(Optional.of(INFO), event.getInfo());
}
@Test
public void theWorldAndPhaseAreAlwaysRequired() {
assertThrows(NullPointerException.class,
() -> new IrisWorldEngineEvent(null, IrisWorldPhase.ENGINE_READY, INFO));
assertThrows(NullPointerException.class,
() -> new IrisWorldEngineEvent(world(), null, INFO));
}
@Test
public void everyPhaseSharesOneHandlerList() {
assertNotNull(IrisWorldEngineEvent.getHandlerList());
assertSame(IrisWorldEngineEvent.getHandlerList(),
new IrisWorldEngineEvent(world(), IrisWorldPhase.ENGINE_HOTLOADED, null).getHandlers());
}
private static World world() {
return (World) Proxy.newProxyInstance(
IrisWorldEngineEventTest.class.getClassLoader(),
new Class<?>[]{World.class},
(Object proxy, Method method, Object[] arguments) -> switch (method.getName()) {
case "getName", "toString" -> "world";
case "hashCode" -> System.identityHashCode(proxy);
case "equals" -> proxy == arguments[0];
default -> throw new UnsupportedOperationException(method.getName());
});
}
}
@@ -0,0 +1,117 @@
package art.arcane.iris.core.link;
import art.arcane.iris.api.terrain.IrisColumnQuery;
import art.arcane.iris.api.terrain.IrisColumnSink;
import art.arcane.iris.api.terrain.IrisSurfaceKind;
import art.arcane.iris.api.terrain.IrisTerrainService;
import art.arcane.iris.api.terrain.IrisWorldInfo;
import org.bukkit.World;
import java.util.HashSet;
import java.util.Optional;
import java.util.OptionalInt;
import java.util.Set;
final class FakeIrisTerrainService implements IrisTerrainService {
private final Set<World> irisWorlds = new HashSet<>();
private String dimensionKey = "overworld";
private String biomeName = "Hot Desert Dunes";
private String biomeKey = "desert/hot-dunes";
private String regionName = "Scorched Expanse";
private String regionKey = "scorched";
private int builds;
private int lastBlockX;
private int lastBlockZ;
void addIrisWorld(World world) {
irisWorlds.add(world);
}
void describe(String dimensionKey, String biomeName, String biomeKey, String regionName, String regionKey) {
this.dimensionKey = dimensionKey;
this.biomeName = biomeName;
this.biomeKey = biomeKey;
this.regionName = regionName;
this.regionKey = regionKey;
}
int builds() {
return builds;
}
String sampledColumn() {
return lastBlockX + "," + lastBlockZ;
}
@Override
public boolean isIrisWorld(World world) {
return irisWorlds.contains(world);
}
@Override
public Optional<IrisWorldInfo> worldInfo(World world) {
if (!isIrisWorld(world)) {
return Optional.empty();
}
return Optional.of(new IrisWorldInfo(dimensionKey, "identity", 42L, -64, 320, 63, false));
}
@Override
public OptionalInt surfaceHeight(World world, int blockX, int blockZ) {
throw new AssertionError("a placeholder must never sample terrain height");
}
@Override
public IrisSurfaceKind surfaceKind(World world, int blockX, int blockZ) {
throw new AssertionError("a placeholder must never classify the surface");
}
@Override
public Optional<String> surfaceBiomeKey(World world, int blockX, int blockZ) {
return isIrisWorld(world) ? Optional.ofNullable(biomeKey) : Optional.empty();
}
@Override
public Optional<String> surfaceBiomeName(World world, int blockX, int blockZ) {
if (!isIrisWorld(world)) {
return Optional.empty();
}
builds++;
lastBlockX = blockX;
lastBlockZ = blockZ;
return Optional.ofNullable(biomeName);
}
@Override
public Optional<String> biomeKey(World world, int blockX, int blockY, int blockZ) {
throw new AssertionError("a placeholder must never resolve a three dimensional biome");
}
@Override
public Optional<String> regionKey(World world, int blockX, int blockZ) {
return isIrisWorld(world) ? Optional.ofNullable(regionKey) : Optional.empty();
}
@Override
public Optional<String> regionName(World world, int blockX, int blockZ) {
return isIrisWorld(world) ? Optional.ofNullable(regionName) : Optional.empty();
}
@Override
public int maxSampleColumns() {
return 0;
}
@Override
public int maxSampleChunks() {
return 0;
}
@Override
public boolean sampleColumns(World world, IrisColumnQuery query, IrisColumnSink sink) {
throw new AssertionError("a placeholder must never run a column sample");
}
}
@@ -0,0 +1,249 @@
package art.arcane.iris.core.link;
import art.arcane.iris.api.pregen.IrisPregenPhase;
import art.arcane.iris.api.pregen.IrisPregenProgress;
import art.arcane.iris.api.terrain.IrisTerrainService;
import org.bukkit.OfflinePlayer;
import org.bukkit.World;
import org.junit.Test;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
import java.util.Locale;
import java.util.UUID;
import java.util.logging.Level;
import java.util.logging.Logger;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
public class IrisPapiExpansionTest {
private static final UUID PLAYER_ID = UUID.fromString("00000000-0000-0000-0000-0000000000b2");
private static final String DASH = "---";
private static final List<String> PUBLISHED_KEYS = List.of(
"available",
"pregen.available",
"pregen.chunks",
"pregen.chunks-per-second",
"pregen.eta",
"pregen.eta-text",
"pregen.paused",
"pregen.percent",
"pregen.total",
"pregen.world",
"world.available",
"world.biome",
"world.biome-key",
"world.dimension",
"world.region",
"world.region-key");
private static final List<String> RETIRED_KEYS = List.of(
"biome_name",
"biome_id",
"biome_file",
"region_name",
"region_id",
"region_file",
"terrain_slope",
"terrain_height",
"world_mode",
"world_seed",
"world_speed");
private static Logger quietLogger() {
Logger logger = Logger.getLogger("IrisPapiExpansionTest");
logger.setUseParentHandlers(false);
logger.setLevel(Level.OFF);
return logger;
}
private static IrisPapiExpansion expansion(IrisPapiState state) {
return new IrisPapiExpansion(state, quietLogger());
}
private static IrisPapiState populatedState() {
FakeIrisTerrainService terrain = new FakeIrisTerrainService();
World world = IrisPapiTestSupport.world("sandbox");
terrain.addIrisWorld(world);
IrisPapiState state = new IrisPapiState(() -> terrain, new IrisPapiTestSupport.Clock());
state.trackPosition(PLAYER_ID, world, 64, 64);
state.publishPregen(IrisPregenPhase.TICK, new IrisPregenProgress(
"sandbox", "sandbox:identity", 42.5D, 1234L, 4096L, 2862L, 0L, 12.5D, 125_000L, 60_000L, "async", false));
return state;
}
@Test
public void theExpansionPublishesExactlyTheDocumentedKeySet() {
assertEquals(PUBLISHED_KEYS, expansion(populatedState()).getPlaceholders());
}
@Test
public void everyPublishedKeyResolvesToAValue() {
IrisPapiExpansion expansion = expansion(populatedState());
OfflinePlayer player = IrisPapiTestSupport.player(PLAYER_ID);
for (String key : PUBLISHED_KEYS) {
String value = expansion.onRequest(player, key);
assertNotNull("published key " + key + " must resolve", value);
assertFalse("published key " + key + " must not resolve to an empty string", value.isEmpty());
}
}
@Test
public void aFullyPopulatedBoardRendersRealValues() {
IrisPapiExpansion expansion = expansion(populatedState());
OfflinePlayer player = IrisPapiTestSupport.player(PLAYER_ID);
assertEquals("true", expansion.onRequest(player, "available"));
assertEquals("true", expansion.onRequest(player, "world.available"));
assertEquals("Hot Desert Dunes", expansion.onRequest(player, "world.biome"));
assertEquals("desert/hot-dunes", expansion.onRequest(player, "world.biome-key"));
assertEquals("Scorched Expanse", expansion.onRequest(player, "world.region"));
assertEquals("scorched", expansion.onRequest(player, "world.region-key"));
assertEquals("overworld", expansion.onRequest(player, "world.dimension"));
assertEquals("true", expansion.onRequest(player, "pregen.available"));
assertEquals("sandbox", expansion.onRequest(player, "pregen.world"));
assertEquals("42.50", expansion.onRequest(player, "pregen.percent"));
assertEquals("125", expansion.onRequest(player, "pregen.eta"));
assertEquals("2m 5s", expansion.onRequest(player, "pregen.eta-text"));
assertEquals("1234", expansion.onRequest(player, "pregen.chunks"));
assertEquals("4096", expansion.onRequest(player, "pregen.total"));
assertEquals("12.50", expansion.onRequest(player, "pregen.chunks-per-second"));
assertEquals("false", expansion.onRequest(player, "pregen.paused"));
}
@Test
public void everyPublishedKeyObeysTheSuiteGrammar() {
for (String key : expansion(populatedState()).getPlaceholders()) {
assertFalse("a placeholder path may never contain '_': " + key, key.indexOf('_') >= 0);
assertEquals("a placeholder path is lowercase ascii: " + key, key.toLowerCase(Locale.ROOT), key);
for (String segment : key.split("\\.", -1)) {
assertFalse("empty segment in " + key, segment.isEmpty());
assertTrue("segment must match [a-z0-9-] in " + key, segment.matches("[a-z0-9-]+"));
}
}
}
@Test
public void anUnknownPathReturnsNullSoTheTypoStaysVisible() {
IrisPapiExpansion expansion = expansion(populatedState());
OfflinePlayer player = IrisPapiTestSupport.player(PLAYER_ID);
assertNull(expansion.onRequest(player, "world.biom"));
assertNull(expansion.onRequest(player, "world"));
assertNull(expansion.onRequest(player, "world."));
assertNull(expansion.onRequest(player, ".biome"));
assertNull(expansion.onRequest(player, "definitely-not-a-key"));
}
@Test
public void theRetiredUnderscoreGrammarNoLongerResolves() {
IrisPapiExpansion expansion = expansion(populatedState());
OfflinePlayer player = IrisPapiTestSupport.player(PLAYER_ID);
for (String retired : RETIRED_KEYS) {
assertNull("the old key " + retired + " must render literally, not silently answer",
expansion.onRequest(player, retired));
}
}
@Test
public void blankParamsReturnNull() {
IrisPapiExpansion expansion = expansion(populatedState());
OfflinePlayer player = IrisPapiTestSupport.player(PLAYER_ID);
assertNull(expansion.onRequest(player, null));
assertNull(expansion.onRequest(player, ""));
assertNull(expansion.onRequest(player, " "));
}
@Test
public void pathsAreLowercasedBeforeDispatch() {
IrisPapiExpansion expansion = expansion(populatedState());
OfflinePlayer player = IrisPapiTestSupport.player(PLAYER_ID);
assertEquals("Hot Desert Dunes", expansion.onRequest(player, "WORLD.BIOME"));
assertEquals("Hot Desert Dunes", expansion.onRequest(player, "World.Biome"));
}
@Test
public void anUntrackedPlayerGetsTheUnavailableSentinelRatherThanALie() {
IrisPapiExpansion expansion = expansion(populatedState());
OfflinePlayer stranger = IrisPapiTestSupport.player(UUID.fromString("00000000-0000-0000-0000-0000000000c3"));
assertEquals("false", expansion.onRequest(stranger, "world.available"));
assertEquals(DASH, expansion.onRequest(stranger, "world.biome"));
assertEquals("true", expansion.onRequest(stranger, "available"));
}
@Test
public void aNullPlayerStillAnswersTheGlobalKeys() {
IrisPapiExpansion expansion = expansion(populatedState());
assertEquals("true", expansion.onRequest(null, "available"));
assertEquals("42.50", expansion.onRequest(null, "pregen.percent"));
assertEquals(DASH, expansion.onRequest(null, "world.biome"));
}
@Test
public void aResolverThatThrowsIsCaughtAndReportedAsUnavailable() {
IrisPapiState exploding = new IrisPapiState(() -> {
throw new IllegalStateException("terrain service exploded");
}, new IrisPapiTestSupport.Clock());
assertEquals(DASH, expansion(exploding).onRequest(IrisPapiTestSupport.player(PLAYER_ID), "available"));
}
@Test
public void metadataIsHardcodedAndTheOwningPluginIsDeclared() {
IrisPapiExpansion expansion = expansion(populatedState());
assertEquals("iris", expansion.getIdentifier());
assertEquals("Volmit Software", expansion.getAuthor());
assertEquals("2.0.0", expansion.getVersion());
assertEquals("Iris", expansion.getRequiredPlugin());
assertTrue(expansion.persist());
}
@Test
public void thePlaceholderPathNeverTouchesTheEngineOrAPluginStatic() throws Exception {
for (String file : List.of("IrisPapiExpansion.java", "IrisPapiState.java", "IrisPapiWorldView.java")) {
String source = Files.readString(Path.of("src/main/java/art/arcane/iris/core/link/" + file));
assertFalse(file + " must not import the engine", source.contains("art.arcane.iris.engine."));
assertFalse(file + " must not reach into the toolbelt", source.contains("IrisToolbelt"));
assertFalse(file + " must not read the plugin static", source.contains("Iris.instance"));
assertFalse(file + " must not derive metadata from the description", source.contains("getDescription()"));
assertFalse(file + " must not take a lock", source.contains("synchronized"));
}
}
@Test
public void theReadmeDocumentsEveryPublishedKeyAndEveryRetiredOne() throws Exception {
String readme = Files.readString(Path.of(System.getProperty("iris.readmeSource")));
for (String key : PUBLISHED_KEYS) {
assertTrue("README must document %iris_" + key + "%", readme.contains("%iris_" + key + "%"));
}
for (String retired : RETIRED_KEYS) {
assertTrue("README must carry the migration row for %iris_" + retired + "%",
readme.contains("%iris_" + retired + "%"));
}
}
@Test
public void theTerrainServiceIsTheOnlyDoorIntoIris() {
IrisTerrainService service = new FakeIrisTerrainService();
IrisPapiState state = new IrisPapiState(() -> service, new IrisPapiTestSupport.Clock());
assertEquals("true", expansion(state).onRequest(IrisPapiTestSupport.player(PLAYER_ID), "available"));
}
}
@@ -0,0 +1,132 @@
package art.arcane.iris.core.link;
import org.junit.Test;
import java.nio.file.Files;
import java.nio.file.Path;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
public class IrisPapiLifecycleTest {
private static final Path PLUGIN_SOURCE = Path.of("src/main/java/art/arcane/iris/Iris.java");
private static final String SETUP = "private void setupPapi() {";
private static final String TEARDOWN = "private void teardownPapi() {";
private static String source() throws Exception {
return Files.readString(PLUGIN_SOURCE);
}
private static String body(String declaration) throws Exception {
String source = source();
int start = source.indexOf(declaration);
assertTrue("Iris.java must declare " + declaration, start >= 0);
int open = source.indexOf('{', start);
int depth = 0;
for (int index = open; index < source.length(); index++) {
char character = source.charAt(index);
if (character == '{') {
depth++;
continue;
}
if (character != '}') {
continue;
}
depth--;
if (depth == 0) {
return source.substring(open + 1, index);
}
}
throw new AssertionError(declaration + " is not brace balanced");
}
@Test
public void registrationIsGatedOnPlaceholderApiBeingEnabled() throws Exception {
assertTrue("setupPapi must bail out when PlaceholderAPI is absent",
body(SETUP).contains("if (!PlaceholderRegistration.isPlaceholderApiEnabled()) {"));
}
@Test
public void theRegistrationTheListenerAndTheStateAreAllRetained() throws Exception {
String source = source();
assertTrue(source.contains("private volatile PlaceholderRegistration papiRegistration;"));
assertTrue(source.contains("private volatile IrisPapiListener papiListener;"));
assertTrue(source.contains("private volatile IrisPapiState papiState;"));
}
@Test
public void teardownUnregistersTheExpansionTheListenerAndClearsTheState() throws Exception {
String teardown = body(TEARDOWN);
assertTrue("the retained registration must be unregistered inside teardownPapi",
teardown.contains("registration.unregister();"));
assertTrue("the retained listener must be detached inside teardownPapi",
teardown.contains("HandlerList.unregisterAll(listener);"));
assertTrue("the retained state must drop every held world reference inside teardownPapi",
teardown.contains("state.clear();"));
assertTrue("teardownPapi must drop the retained listener", teardown.contains("papiListener = null;"));
assertTrue("teardownPapi must drop the retained registration", teardown.contains("papiRegistration = null;"));
assertTrue("teardownPapi must drop the retained state", teardown.contains("papiState = null;"));
}
@Test
public void bothDisablePathsTearThePlaceholderSurfaceDown() throws Exception {
String source = source();
assertTrue("onDisable must tear the expansion down",
source.contains("public void onDisable() {\n teardownPapi();"));
assertTrue("the BileTools pre-unload hook must tear the expansion down",
source.contains("public void onPreUnload(ReloadAware.PreUnloadReason reason) {\n teardownPapi();"));
}
@Test
public void aFailedListenerAttachDoesNotLeaveTheExpansionRegistered() throws Exception {
String setup = body(SETUP);
int attach = setup.indexOf("registerEvents(listener, this)");
assertTrue("the listener must be attached inside setupPapi", attach >= 0);
int rescue = setup.indexOf("} catch (Throwable failure) {", attach);
assertTrue("the attach must be guarded inside setupPapi", rescue > attach);
int rollback = setup.indexOf("registration.unregister();", rescue);
assertTrue("a failed attach must roll the registration back inside setupPapi", rollback > rescue);
int bail = setup.indexOf("return;", rollback);
assertTrue("a failed attach must leave setupPapi", bail > rollback);
int retained = setup.indexOf("papiRegistration = registration;");
assertTrue("setupPapi must retain the registration", retained > 0);
assertTrue("the registration may only be retained after a successful attach", retained > bail);
}
@Test
public void theExpansionIsNeverConstructedFromAPluginStatic() throws Exception {
String setup = body(SETUP);
assertFalse("the plugin class must not construct the expansion: that forces PlaceholderExpansion to load during enable and crashes a server without PlaceholderAPI",
setup.contains("new IrisPapiExpansion("));
assertTrue("the expansion takes its state as a constructor argument, built inside the installer",
installerSource().contains("new IrisPapiExpansion(state, logger)"));
assertFalse("the hand rolled PlaceholderAPI presence check must be gone",
source().contains("isPluginEnabled(\"PlaceholderAPI\")"));
}
private static String installerSource() throws Exception {
return java.nio.file.Files.readString(java.nio.file.Path.of(
"src/main/java/art/arcane/iris/core/link/IrisPapiInstaller.java"));
}
}
@@ -0,0 +1,316 @@
package art.arcane.iris.core.link;
import art.arcane.iris.api.pregen.IrisPregenPhase;
import art.arcane.iris.api.pregen.IrisPregenProgress;
import art.arcane.iris.api.pregen.IrisPregenerationEvent;
import net.kyori.adventure.text.Component;
import org.bukkit.Location;
import org.bukkit.World;
import org.bukkit.event.Event;
import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority;
import org.bukkit.event.HandlerList;
import org.bukkit.event.Listener;
import org.bukkit.event.player.PlayerChangedWorldEvent;
import org.bukkit.event.player.PlayerJoinEvent;
import org.bukkit.event.player.PlayerMoveEvent;
import org.bukkit.event.player.PlayerPortalEvent;
import org.bukkit.event.player.PlayerQuitEvent;
import org.bukkit.event.player.PlayerRespawnEvent;
import org.bukkit.event.player.PlayerTeleportEvent;
import org.junit.Test;
import java.lang.reflect.Method;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.UUID;
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 IrisPapiListenerTest {
private static final UUID PLAYER = UUID.fromString("00000000-0000-0000-0000-0000000000d4");
private static final String DASH = "---";
private static IrisPregenProgress progress(double percent) {
return new IrisPregenProgress(
"sandbox",
"sandbox:identity",
percent,
1234L,
4096L,
2862L,
0L,
12.5D,
125_000L,
60_000L,
"async",
false);
}
private static final class Harness {
private final FakeIrisTerrainService terrain = new FakeIrisTerrainService();
private final IrisPapiTestSupport.Clock clock = new IrisPapiTestSupport.Clock();
private final IrisPapiState state = new IrisPapiState(() -> terrain, clock);
private final IrisPapiListener listener = new IrisPapiListener(state);
private final World sandbox = IrisPapiTestSupport.world("sandbox");
private final World hub = IrisPapiTestSupport.world("hub");
private final IrisPapiTestSupport.StandingPlayer player;
private Harness() {
terrain.addIrisWorld(sandbox);
player = new IrisPapiTestSupport.StandingPlayer(PLAYER, at(sandbox, 10, 10));
}
private static Location at(World world, int blockX, int blockZ) {
return new Location(world, blockX + 0.5D, 71.0D, blockZ + 0.5D);
}
}
@Test
public void theListenerIsABukkitListener() {
assertTrue(Listener.class.isAssignableFrom(IrisPapiListener.class));
}
@Test
public void everyPositionSourceIsHandledAtMonitorPriority() {
Set<Class<?>> handled = new HashSet<>();
for (Method method : IrisPapiListener.class.getDeclaredMethods()) {
EventHandler handler = method.getAnnotation(EventHandler.class);
if (handler == null) {
continue;
}
assertEquals("placeholder bookkeeping must never influence an event: " + method.getName(),
EventPriority.MONITOR, handler.priority());
assertEquals("a handler takes exactly one event: " + method.getName(),
1, method.getParameterCount());
handled.add(method.getParameterTypes()[0]);
}
assertEquals(Set.of(
PlayerMoveEvent.class,
PlayerTeleportEvent.class,
PlayerPortalEvent.class,
PlayerRespawnEvent.class,
PlayerJoinEvent.class,
PlayerChangedWorldEvent.class,
PlayerQuitEvent.class,
IrisPregenerationEvent.class),
handled);
}
@Test
public void everyStepOfTheMoveHierarchyOwnsItsHandlerListSoNoneIsCoveredByItsParent() throws Exception {
List<Class<? extends Event>> hierarchy =
List.of(PlayerMoveEvent.class, PlayerTeleportEvent.class, PlayerPortalEvent.class);
Set<Class<?>> handled = handledEventTypes();
for (int index = 1; index < hierarchy.size(); index++) {
Class<? extends Event> child = hierarchy.get(index);
Class<? extends Event> parent = hierarchy.get(index - 1);
assertSame(child.getName() + " no longer extends " + parent.getName(),
parent, child.getSuperclass());
assertNotSame(child.getName() + " has its own HandlerList, so a fired " + child.getSimpleName()
+ " never reaches a " + parent.getSimpleName() + " handler",
handlerList(parent), handlerList(child));
assertTrue(child.getName() + " is a position source that no other handler can cover",
handled.contains(child));
}
assertTrue(PlayerMoveEvent.class.getName() + " must still be handled",
handled.contains(PlayerMoveEvent.class));
}
private static HandlerList handlerList(Class<? extends Event> type) throws Exception {
return (HandlerList) type.getDeclaredMethod("getHandlerList").invoke(null);
}
private static Set<Class<?>> handledEventTypes() {
Set<Class<?>> handled = new HashSet<>();
for (Method method : IrisPapiListener.class.getDeclaredMethods()) {
if (method.getAnnotation(EventHandler.class) != null) {
handled.add(method.getParameterTypes()[0]);
}
}
return handled;
}
@Test
public void joiningPublishesTheColumnThePlayerLandsOn() {
Harness harness = new Harness();
assertEquals("false", harness.state.worldAvailable(PLAYER));
harness.listener.onPlayerJoin(new PlayerJoinEvent(harness.player.handle(), Component.empty()));
assertEquals("true", harness.state.worldAvailable(PLAYER));
assertEquals("Hot Desert Dunes", harness.state.biome(PLAYER));
assertEquals("10,10", harness.terrain.sampledColumn());
}
@Test
public void movingPublishesTheDestinationColumn() {
Harness harness = new Harness();
Location from = harness.player.standing();
Location to = Harness.at(harness.sandbox, 40, -80);
harness.listener.onPlayerMove(new PlayerMoveEvent(harness.player.handle(), from, to));
assertEquals("true", harness.state.worldAvailable(PLAYER));
assertEquals("Hot Desert Dunes", harness.state.biome(PLAYER));
assertEquals("40,-80", harness.terrain.sampledColumn());
}
@Test
public void aSameWorldTeleportUpdatesTheBoardWithoutAnyFurtherMovement() {
Harness harness = new Harness();
harness.listener.onPlayerJoin(new PlayerJoinEvent(harness.player.handle(), Component.empty()));
assertEquals("Hot Desert Dunes", harness.state.biome(PLAYER));
assertEquals("10,10", harness.terrain.sampledColumn());
harness.terrain.describe("overworld", "Frozen Shelf", "cold/shelf", "Glacier", "glacier");
Location from = harness.player.standing();
Location to = Harness.at(harness.sandbox, 2000, -2000);
harness.player.standAt(to);
harness.listener.onPlayerTeleport(new PlayerTeleportEvent(
harness.player.handle(), from, to, PlayerTeleportEvent.TeleportCause.COMMAND));
assertEquals("a same world teleport must republish the position immediately,"
+ " with no move, no world change and no clock advance",
"Frozen Shelf", harness.state.biome(PLAYER));
assertEquals("2000,-2000", harness.terrain.sampledColumn());
assertEquals("cold/shelf", harness.state.biomeKey(PLAYER));
assertEquals("Glacier", harness.state.region(PLAYER));
}
@Test
public void aSameWorldPortalUpdatesTheBoardWithoutAnyFurtherMovement() {
Harness harness = new Harness();
harness.listener.onPlayerJoin(new PlayerJoinEvent(harness.player.handle(), Component.empty()));
assertEquals("Hot Desert Dunes", harness.state.biome(PLAYER));
harness.terrain.describe("overworld", "Frozen Shelf", "cold/shelf", "Glacier", "glacier");
Location from = harness.player.standing();
Location to = Harness.at(harness.sandbox, 512, 512);
harness.player.standAt(to);
harness.listener.onPlayerPortal(new PlayerPortalEvent(
harness.player.handle(), from, to, PlayerTeleportEvent.TeleportCause.NETHER_PORTAL));
assertEquals("Frozen Shelf", harness.state.biome(PLAYER));
assertEquals("512,512", harness.terrain.sampledColumn());
}
@Test
public void aSameWorldRespawnUpdatesTheBoardWithoutAnyFurtherMovement() {
Harness harness = new Harness();
harness.listener.onPlayerJoin(new PlayerJoinEvent(harness.player.handle(), Component.empty()));
assertEquals("Hot Desert Dunes", harness.state.biome(PLAYER));
harness.terrain.describe("overworld", "Frozen Shelf", "cold/shelf", "Glacier", "glacier");
Location bed = Harness.at(harness.sandbox, -333, 777);
harness.player.standAt(bed);
harness.listener.onPlayerRespawn(new PlayerRespawnEvent(
harness.player.handle(), bed, true, false, false, PlayerRespawnEvent.RespawnReason.DEATH));
assertEquals("Frozen Shelf", harness.state.biome(PLAYER));
assertEquals("-333,777", harness.terrain.sampledColumn());
}
@Test
public void changingWorldsRepublishesAgainstTheNewWorld() {
Harness harness = new Harness();
harness.listener.onPlayerJoin(new PlayerJoinEvent(harness.player.handle(), Component.empty()));
assertEquals("true", harness.state.worldAvailable(PLAYER));
harness.player.standAt(Harness.at(harness.hub, 0, 0));
harness.listener.onPlayerChangedWorld(new PlayerChangedWorldEvent(harness.player.handle(), harness.sandbox));
assertEquals("false", harness.state.worldAvailable(PLAYER));
assertEquals(DASH, harness.state.biome(PLAYER));
assertEquals(DASH, harness.state.dimension(PLAYER));
}
@Test
public void quittingEvictsThePositionAndTheViewSoNoWorldIsHeld() {
Harness harness = new Harness();
harness.listener.onPlayerJoin(new PlayerJoinEvent(harness.player.handle(), Component.empty()));
assertEquals("Hot Desert Dunes", harness.state.biome(PLAYER));
harness.listener.onPlayerQuit(new PlayerQuitEvent(
harness.player.handle(), Component.empty(), PlayerQuitEvent.QuitReason.DISCONNECTED));
assertEquals("false", harness.state.worldAvailable(PLAYER));
assertEquals(DASH, harness.state.biome(PLAYER));
assertEquals(DASH, harness.state.region(PLAYER));
assertEquals(DASH, harness.state.dimension(PLAYER));
}
@Test
public void thePregenHandlerLatchesProgressAndRetiresItOnATerminalPhase() {
Harness harness = new Harness();
assertEquals("false", harness.state.pregenAvailable(PLAYER));
harness.listener.onPregeneration(new IrisPregenerationEvent(IrisPregenPhase.TICK, progress(42.5D)));
assertEquals("true", harness.state.pregenAvailable(PLAYER));
assertEquals("sandbox", harness.state.pregenWorld(PLAYER));
assertEquals("42.50", harness.state.pregenPercent(PLAYER));
assertEquals("2m 5s", harness.state.pregenEtaText(PLAYER));
harness.listener.onPregeneration(new IrisPregenerationEvent(IrisPregenPhase.COMPLETED, progress(100.0D)));
assertEquals("false", harness.state.pregenAvailable(PLAYER));
assertEquals(DASH, harness.state.pregenPercent(PLAYER));
}
@Test
public void trackingPublishesTheBlockColumnOfTheLocation() {
FakeIrisTerrainService terrain = new FakeIrisTerrainService();
World world = IrisPapiTestSupport.world("sandbox");
terrain.addIrisWorld(world);
IrisPapiState state = new IrisPapiState(() -> terrain, new IrisPapiTestSupport.Clock());
IrisPapiListener.track(state, PLAYER, new Location(world, 128.7D, 71.0D, -512.2D));
assertEquals("true", state.worldAvailable(PLAYER));
assertEquals("Hot Desert Dunes", state.biome(PLAYER));
assertEquals("128,-513", terrain.sampledColumn());
}
@Test
public void trackingIgnoresMissingInputsInsteadOfThrowing() {
FakeIrisTerrainService terrain = new FakeIrisTerrainService();
World world = IrisPapiTestSupport.world("sandbox");
terrain.addIrisWorld(world);
IrisPapiState state = new IrisPapiState(() -> terrain, new IrisPapiTestSupport.Clock());
IrisPapiListener.track(null, PLAYER, new Location(world, 0.0D, 0.0D, 0.0D));
IrisPapiListener.track(state, null, new Location(world, 0.0D, 0.0D, 0.0D));
IrisPapiListener.track(state, PLAYER, null);
IrisPapiListener.track(state, PLAYER, new Location(null, 0.0D, 0.0D, 0.0D));
IrisPapiListener.trackNow(null, PLAYER, new Location(world, 0.0D, 0.0D, 0.0D));
IrisPapiListener.trackNow(state, null, new Location(world, 0.0D, 0.0D, 0.0D));
IrisPapiListener.trackNow(state, PLAYER, null);
IrisPapiListener.trackNow(state, PLAYER, new Location(null, 0.0D, 0.0D, 0.0D));
assertEquals("false", state.worldAvailable(PLAYER));
}
}
@@ -0,0 +1,394 @@
package art.arcane.iris.core.link;
import art.arcane.iris.api.pregen.IrisPregenPhase;
import art.arcane.iris.api.pregen.IrisPregenProgress;
import art.arcane.iris.api.terrain.IrisTerrainService;
import org.bukkit.World;
import org.junit.Test;
import java.util.List;
import java.util.UUID;
import java.util.function.Function;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotEquals;
public class IrisPapiStateTest {
private static final UUID PLAYER = UUID.fromString("00000000-0000-0000-0000-0000000000a1");
private static final String DASH = "---";
private static final long POSITION_INTERVAL = IrisPapiState.POSITION_INTERVAL_MS;
private static IrisPregenProgress progress(double percent, long etaMillis, boolean paused) {
return new IrisPregenProgress(
"sandbox",
"sandbox:identity",
percent,
1234L,
4096L,
2862L,
0L,
12.5D,
etaMillis,
60_000L,
"async",
paused);
}
@Test
public void availableReportsWhetherTheTerrainServiceIsRegistered() {
FakeIrisTerrainService terrain = new FakeIrisTerrainService();
IrisPapiState absent = new IrisPapiState(() -> null, new IrisPapiTestSupport.Clock());
IrisPapiState present = new IrisPapiState(() -> terrain, new IrisPapiTestSupport.Clock());
assertEquals("false", absent.available(PLAYER));
assertEquals("true", present.available(PLAYER));
}
@Test
public void worldKeysAreUnavailableUntilAPositionIsTracked() {
FakeIrisTerrainService terrain = new FakeIrisTerrainService();
IrisPapiState state = new IrisPapiState(() -> terrain, new IrisPapiTestSupport.Clock());
assertEquals("false", state.worldAvailable(PLAYER));
assertEquals(DASH, state.biome(PLAYER));
assertEquals(DASH, state.biomeKey(PLAYER));
assertEquals(DASH, state.region(PLAYER));
assertEquals(DASH, state.regionKey(PLAYER));
assertEquals(DASH, state.dimension(PLAYER));
assertEquals(0, terrain.builds());
}
@Test
public void worldKeysResolveTheBiomeRegionAndDimensionAtTheTrackedColumn() {
FakeIrisTerrainService terrain = new FakeIrisTerrainService();
World world = IrisPapiTestSupport.world("sandbox");
terrain.addIrisWorld(world);
IrisPapiState state = new IrisPapiState(() -> terrain, new IrisPapiTestSupport.Clock());
state.trackPosition(PLAYER, world, 128, -512);
assertEquals("true", state.worldAvailable(PLAYER));
assertEquals("Hot Desert Dunes", state.biome(PLAYER));
assertEquals("desert/hot-dunes", state.biomeKey(PLAYER));
assertEquals("Scorched Expanse", state.region(PLAYER));
assertEquals("scorched", state.regionKey(PLAYER));
assertEquals("overworld", state.dimension(PLAYER));
}
@Test
public void aNonIrisWorldAnswersFalseAndDashesWithoutQueryingTerrain() {
FakeIrisTerrainService terrain = new FakeIrisTerrainService();
World hub = IrisPapiTestSupport.world("hub");
IrisPapiState state = new IrisPapiState(() -> terrain, new IrisPapiTestSupport.Clock());
state.trackPosition(PLAYER, hub, 0, 0);
assertEquals("false", state.worldAvailable(PLAYER));
assertEquals(DASH, state.biome(PLAYER));
assertEquals(DASH, state.biomeKey(PLAYER));
assertEquals(DASH, state.region(PLAYER));
assertEquals(DASH, state.regionKey(PLAYER));
assertEquals(DASH, state.dimension(PLAYER));
assertEquals(0, terrain.builds());
}
@Test
public void aWholeBoardOfKeysBuildsTheViewExactlyOncePerSecond() {
FakeIrisTerrainService terrain = new FakeIrisTerrainService();
World world = IrisPapiTestSupport.world("sandbox");
terrain.addIrisWorld(world);
IrisPapiTestSupport.Clock clock = new IrisPapiTestSupport.Clock();
IrisPapiState state = new IrisPapiState(() -> terrain, clock);
state.trackPosition(PLAYER, world, 10, 10);
for (int pass = 0; pass < 6; pass++) {
state.worldAvailable(PLAYER);
state.biome(PLAYER);
state.biomeKey(PLAYER);
state.region(PLAYER);
state.regionKey(PLAYER);
state.dimension(PLAYER);
}
assertEquals(1, terrain.builds());
}
@Test
public void theViewIsRebuiltOnceTheOneSecondTtlElapses() {
FakeIrisTerrainService terrain = new FakeIrisTerrainService();
World world = IrisPapiTestSupport.world("sandbox");
terrain.addIrisWorld(world);
IrisPapiTestSupport.Clock clock = new IrisPapiTestSupport.Clock();
IrisPapiState state = new IrisPapiState(() -> terrain, clock);
state.trackPosition(PLAYER, world, 10, 10);
assertEquals("Hot Desert Dunes", state.biome(PLAYER));
terrain.describe("overworld", "Frozen Shelf", "cold/shelf", "Glacier", "glacier");
assertEquals("Hot Desert Dunes", state.biome(PLAYER));
clock.advance(IrisPapiState.VIEW_TTL_MS);
assertEquals("Frozen Shelf", state.biome(PLAYER));
assertEquals(2, terrain.builds());
}
@Test
public void asecondColumnWithinTheSameSecondDoesNotRepublishThePosition() {
FakeIrisTerrainService terrain = new FakeIrisTerrainService();
World world = IrisPapiTestSupport.world("sandbox");
terrain.addIrisWorld(world);
IrisPapiTestSupport.Clock clock = new IrisPapiTestSupport.Clock();
IrisPapiState state = new IrisPapiState(() -> terrain, clock);
state.trackPosition(PLAYER, world, 10, 10);
assertEquals("Hot Desert Dunes", state.biome(PLAYER));
terrain.describe("overworld", "Frozen Shelf", "cold/shelf", "Glacier", "glacier");
for (int step = 1; step <= 30; step++) {
clock.advance(40L);
state.trackPosition(PLAYER, world, 10 + step, 10);
state.biome(PLAYER);
}
assertEquals("a sprinting player must not force more than one view build per second",
2, terrain.builds());
}
@Test
public void aMoveInsideTheSameBlockColumnNeverRepublishesThePosition() {
FakeIrisTerrainService terrain = new FakeIrisTerrainService();
World world = IrisPapiTestSupport.world("sandbox");
terrain.addIrisWorld(world);
IrisPapiTestSupport.Clock clock = new IrisPapiTestSupport.Clock();
IrisPapiState state = new IrisPapiState(() -> terrain, clock);
state.trackPosition(PLAYER, world, 10, 10);
clock.advance(POSITION_INTERVAL / 2L);
assertEquals("Hot Desert Dunes", state.biome(PLAYER));
terrain.describe("overworld", "Frozen Shelf", "cold/shelf", "Glacier", "glacier");
clock.advance(POSITION_INTERVAL / 2L);
state.trackPosition(PLAYER, world, 10, 10);
clock.advance(POSITION_INTERVAL / 5L);
assertEquals("a look around inside one block column must not invalidate the memoised view",
"Hot Desert Dunes", state.biome(PLAYER));
assertEquals(1, terrain.builds());
}
@Test
public void anImmediatePublishIgnoresTheOneSecondPositionGate() {
FakeIrisTerrainService terrain = new FakeIrisTerrainService();
World world = IrisPapiTestSupport.world("sandbox");
terrain.addIrisWorld(world);
IrisPapiTestSupport.Clock clock = new IrisPapiTestSupport.Clock();
IrisPapiState state = new IrisPapiState(() -> terrain, clock);
state.trackPosition(PLAYER, world, 10, 10);
assertEquals("Hot Desert Dunes", state.biome(PLAYER));
assertEquals("10,10", terrain.sampledColumn());
terrain.describe("overworld", "Frozen Shelf", "cold/shelf", "Glacier", "glacier");
state.trackPositionNow(PLAYER, world, 2000, -2000);
assertEquals("a discrete jump must not be swallowed by the move rate limiter",
"Frozen Shelf", state.biome(PLAYER));
assertEquals("2000,-2000", terrain.sampledColumn());
}
@Test
public void anImmediatePublishInsideTheSameBlockColumnStillCostsNothing() {
FakeIrisTerrainService terrain = new FakeIrisTerrainService();
World world = IrisPapiTestSupport.world("sandbox");
terrain.addIrisWorld(world);
IrisPapiTestSupport.Clock clock = new IrisPapiTestSupport.Clock();
IrisPapiState state = new IrisPapiState(() -> terrain, clock);
state.trackPosition(PLAYER, world, 10, 10);
assertEquals("Hot Desert Dunes", state.biome(PLAYER));
terrain.describe("overworld", "Frozen Shelf", "cold/shelf", "Glacier", "glacier");
state.trackPositionNow(PLAYER, world, 10, 10);
assertEquals("Hot Desert Dunes", state.biome(PLAYER));
assertEquals(1, terrain.builds());
}
@Test
public void crossingIntoAnotherWorldRepublishesImmediately() {
FakeIrisTerrainService terrain = new FakeIrisTerrainService();
World sandbox = IrisPapiTestSupport.world("sandbox");
World hub = IrisPapiTestSupport.world("hub");
terrain.addIrisWorld(sandbox);
IrisPapiTestSupport.Clock clock = new IrisPapiTestSupport.Clock();
IrisPapiState state = new IrisPapiState(() -> terrain, clock);
state.trackPosition(PLAYER, sandbox, 10, 10);
assertEquals("true", state.worldAvailable(PLAYER));
state.trackPosition(PLAYER, hub, 0, 0);
assertEquals("false", state.worldAvailable(PLAYER));
assertEquals(DASH, state.biome(PLAYER));
}
@Test
public void releasingAPlayerClearsTheTrackedPositionAndView() {
FakeIrisTerrainService terrain = new FakeIrisTerrainService();
World world = IrisPapiTestSupport.world("sandbox");
terrain.addIrisWorld(world);
IrisPapiState state = new IrisPapiState(() -> terrain, new IrisPapiTestSupport.Clock());
state.trackPosition(PLAYER, world, 10, 10);
assertEquals("Hot Desert Dunes", state.biome(PLAYER));
state.release(PLAYER);
assertEquals("false", state.worldAvailable(PLAYER));
assertEquals(DASH, state.biome(PLAYER));
assertEquals(DASH, state.dimension(PLAYER));
}
@Test
public void clearingTheStateDropsEveryTrackedPlayerAndTheLatchedPregen() {
FakeIrisTerrainService terrain = new FakeIrisTerrainService();
World world = IrisPapiTestSupport.world("sandbox");
terrain.addIrisWorld(world);
IrisPapiState state = new IrisPapiState(() -> terrain, new IrisPapiTestSupport.Clock());
state.trackPosition(PLAYER, world, 10, 10);
state.publishPregen(IrisPregenPhase.TICK, progress(42.5D, 125_000L, false));
assertEquals("Hot Desert Dunes", state.biome(PLAYER));
assertEquals("true", state.pregenAvailable(PLAYER));
state.clear();
assertEquals(DASH, state.biome(PLAYER));
assertEquals("false", state.pregenAvailable(PLAYER));
}
@Test
public void pregenKeysAreUnavailableUntilAnEventArrives() {
IrisPapiState state = new IrisPapiState(FakeIrisTerrainService::new, new IrisPapiTestSupport.Clock());
assertEquals("false", state.pregenAvailable(PLAYER));
assertEquals(DASH, state.pregenWorld(PLAYER));
assertEquals(DASH, state.pregenPercent(PLAYER));
assertEquals(DASH, state.pregenEta(PLAYER));
assertEquals(DASH, state.pregenEtaText(PLAYER));
assertEquals(DASH, state.pregenChunks(PLAYER));
assertEquals(DASH, state.pregenTotal(PLAYER));
assertEquals(DASH, state.pregenChunksPerSecond(PLAYER));
assertEquals(DASH, state.pregenPaused(PLAYER));
}
@Test
public void pregenKeysRenderTheLatchedProgress() {
IrisPapiState state = new IrisPapiState(FakeIrisTerrainService::new, new IrisPapiTestSupport.Clock());
state.publishPregen(IrisPregenPhase.TICK, progress(42.5D, 125_000L, false));
assertEquals("true", state.pregenAvailable(PLAYER));
assertEquals("sandbox", state.pregenWorld(PLAYER));
assertEquals("42.50", state.pregenPercent(PLAYER));
assertEquals("125", state.pregenEta(PLAYER));
assertEquals("2m 5s", state.pregenEtaText(PLAYER));
assertEquals("1234", state.pregenChunks(PLAYER));
assertEquals("4096", state.pregenTotal(PLAYER));
assertEquals("12.50", state.pregenChunksPerSecond(PLAYER));
assertEquals("false", state.pregenPaused(PLAYER));
}
@Test
public void aPausedPregenReportsPaused() {
IrisPapiState state = new IrisPapiState(FakeIrisTerrainService::new, new IrisPapiTestSupport.Clock());
state.publishPregen(IrisPregenPhase.PAUSED, progress(42.5D, 125_000L, true));
assertEquals("true", state.pregenPaused(PLAYER));
}
@Test
public void pregenIsClearedWhenTheJobFinishesOrIsCancelled() {
for (IrisPregenPhase terminal : List.of(IrisPregenPhase.COMPLETED, IrisPregenPhase.CANCELLED)) {
IrisPapiState state = new IrisPapiState(FakeIrisTerrainService::new, new IrisPapiTestSupport.Clock());
state.publishPregen(IrisPregenPhase.TICK, progress(99.0D, 1_000L, false));
assertEquals("true", state.pregenAvailable(PLAYER));
state.publishPregen(terminal, progress(100.0D, 0L, false));
assertEquals(terminal + " must retire the pregen snapshot", "false", state.pregenAvailable(PLAYER));
assertEquals(DASH, state.pregenPercent(PLAYER));
}
}
@Test
public void noPublishedValueEverCarriesAPercentOrSectionCharacter() {
FakeIrisTerrainService terrain = new FakeIrisTerrainService();
World world = IrisPapiTestSupport.world("sandbox");
terrain.addIrisWorld(world);
terrain.describe("over%world", "Hot \u00A7cDesert", "hot%key", "Region\u00A7a", "reg%ion");
IrisPapiState state = new IrisPapiState(() -> terrain, new IrisPapiTestSupport.Clock());
state.trackPosition(PLAYER, world, 0, 0);
state.publishPregen(IrisPregenPhase.TICK, progress(100.0D, 3_600_000L, false));
List<Function<UUID, String>> resolvers = List.of(
state::available,
state::worldAvailable,
state::biome,
state::biomeKey,
state::region,
state::regionKey,
state::dimension,
state::pregenAvailable,
state::pregenWorld,
state::pregenPercent,
state::pregenEta,
state::pregenEtaText,
state::pregenChunks,
state::pregenTotal,
state::pregenChunksPerSecond,
state::pregenPaused);
for (Function<UUID, String> resolver : resolvers) {
String value = resolver.apply(PLAYER);
assertFalse("a placeholder value may never contain '%': " + value, value.indexOf('%') >= 0);
assertFalse("a placeholder value may never contain a legacy colour code: " + value,
value.indexOf('\u00A7') >= 0);
}
assertNotEquals("Hot \u00A7cDesert", state.biome(PLAYER));
}
@Test
public void etaTextCollapsesSecondsMinutesAndHours() {
assertEquals("0s", IrisPapiPregenView.duration(0L));
assertEquals("45s", IrisPapiPregenView.duration(45_000L));
assertEquals("2m 5s", IrisPapiPregenView.duration(125_000L));
assertEquals("1h 0m", IrisPapiPregenView.duration(3_600_000L));
assertEquals("2h 3m", IrisPapiPregenView.duration(7_380_000L));
}
@Test
public void aTerrainServiceThatDisappearsStopsAnsweringWithoutThrowing() {
FakeIrisTerrainService terrain = new FakeIrisTerrainService();
World world = IrisPapiTestSupport.world("sandbox");
terrain.addIrisWorld(world);
IrisTerrainService[] holder = new IrisTerrainService[]{terrain};
IrisPapiTestSupport.Clock clock = new IrisPapiTestSupport.Clock();
IrisPapiState state = new IrisPapiState(() -> holder[0], clock);
state.trackPosition(PLAYER, world, 10, 10);
assertEquals("Hot Desert Dunes", state.biome(PLAYER));
holder[0] = null;
clock.advance(IrisPapiState.VIEW_TTL_MS);
assertEquals("false", state.available(PLAYER));
assertEquals("false", state.worldAvailable(PLAYER));
assertEquals(DASH, state.biome(PLAYER));
}
}
@@ -0,0 +1,104 @@
package art.arcane.iris.core.link;
import org.bukkit.Location;
import org.bukkit.OfflinePlayer;
import org.bukkit.World;
import org.bukkit.entity.Player;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.UUID;
import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.LongSupplier;
final class IrisPapiTestSupport {
private IrisPapiTestSupport() {
}
static World world(String name) {
InvocationHandler handler = (Object proxy, Method method, Object[] args) -> switch (method.getName()) {
case "getName" -> name;
case "hashCode" -> System.identityHashCode(proxy);
case "equals" -> proxy == args[0];
case "toString" -> "World[" + name + "]";
default -> throw new AssertionError("a placeholder must not call World#" + method.getName());
};
return (World) Proxy.newProxyInstance(
World.class.getClassLoader(),
new Class<?>[]{World.class},
handler);
}
static OfflinePlayer player(UUID id) {
InvocationHandler handler = (Object proxy, Method method, Object[] args) -> switch (method.getName()) {
case "getUniqueId" -> id;
case "hashCode" -> System.identityHashCode(proxy);
case "equals" -> proxy == args[0];
case "toString" -> "OfflinePlayer[" + id + "]";
default -> throw new AssertionError("a placeholder must not call OfflinePlayer#" + method.getName());
};
return (OfflinePlayer) Proxy.newProxyInstance(
OfflinePlayer.class.getClassLoader(),
new Class<?>[]{OfflinePlayer.class},
handler);
}
static final class StandingPlayer {
private final UUID id;
private final AtomicReference<Location> standing = new AtomicReference<>();
private final Player handle;
StandingPlayer(UUID id, Location location) {
this.id = id;
this.standing.set(location);
InvocationHandler handler = (Object proxy, Method method, Object[] args) -> switch (method.getName()) {
case "getUniqueId" -> this.id;
case "getLocation" -> this.standing.get();
case "getWorld" -> this.standing.get().getWorld();
case "getName" -> "StandingPlayer";
case "hashCode" -> System.identityHashCode(proxy);
case "equals" -> proxy == args[0];
case "toString" -> "Player[" + this.id + "]";
default -> throw new AssertionError("a placeholder listener must not call Player#" + method.getName());
};
this.handle = (Player) Proxy.newProxyInstance(
Player.class.getClassLoader(),
new Class<?>[]{Player.class},
handler);
}
UUID id() {
return id;
}
Player handle() {
return handle;
}
Location standing() {
return standing.get();
}
void standAt(Location location) {
standing.set(location);
}
}
static final class Clock implements LongSupplier {
private final AtomicLong now = new AtomicLong(1_000_000L);
void advance(long millis) {
now.addAndGet(millis);
}
@Override
public long getAsLong() {
return now.get();
}
}
}
@@ -0,0 +1,82 @@
package art.arcane.iris.core.link;
import org.junit.Test;
import java.net.URL;
import java.net.URLClassLoader;
public class IrisPlaceholderAbsenceTest {
private static final String[] LOADS_WITHOUT_PLACEHOLDER_API = {
"art.arcane.iris.core.link.IrisPapiState",
"art.arcane.iris.core.link.IrisPapiListener",
"art.arcane.volmlib.util.bukkit.papi.PlaceholderRegistration"
};
private static final String[] REQUIRES_PLACEHOLDER_API = {
"art.arcane.iris.core.link.IrisPapiInstaller",
"art.arcane.iris.core.link.IrisPapiExpansion"
};
@Test
public void everyEnablePathClassLoadsWhenPlaceholderApiIsAbsent() {
ClassLoader hidden = new PlaceholderApiHidingLoader();
for (String name : LOADS_WITHOUT_PLACEHOLDER_API) {
try {
Class.forName(name, true, hidden);
} catch (Throwable failure) {
throw new AssertionError(name + " must load when PlaceholderAPI is not installed", failure);
}
}
}
@Test
public void theExpansionItselfStillDependsOnPlaceholderApi() {
ClassLoader hidden = new PlaceholderApiHidingLoader();
for (String name : REQUIRES_PLACEHOLDER_API) {
boolean threw = false;
try {
Class.forName(name, true, hidden);
} catch (Throwable failure) {
threw = true;
}
if (!threw) {
throw new AssertionError(name + " is expected to depend on PlaceholderAPI, so the split above is what keeps the plugin loadable");
}
}
}
private static final class PlaceholderApiHidingLoader extends URLClassLoader {
private PlaceholderApiHidingLoader() {
super(classpath(), ClassLoader.getPlatformClassLoader());
}
private static URL[] classpath() {
String[] entries = System.getProperty("java.class.path").split(java.io.File.pathSeparator);
URL[] resolved = new URL[entries.length];
for (int i = 0; i < entries.length; i++) {
try {
resolved[i] = new java.io.File(entries[i]).toURI().toURL();
} catch (Throwable failure) {
throw new IllegalStateException(entries[i], failure);
}
}
return resolved;
}
@Override
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
if (name.startsWith("me.clip.")) {
throw new ClassNotFoundException(name);
}
return super.loadClass(name, resolve);
}
}
}
@@ -0,0 +1,193 @@
package art.arcane.iris.core.service;
import org.junit.Test;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
public class IrisApiWiringContractTest {
@Test
public void theTerrainServiceRegistersOnBothRegistriesAndReleasesBoth() throws IOException {
String source = source("iris.terrainSvcSource");
String onEnable = method(source, "public void onEnable()");
String onDisable = method(source, "public void onDisable()");
assertTrue(onEnable.contains("Bukkit.getServicesManager().register("));
assertTrue(onEnable.contains("IrisTerrainService.class"));
assertTrue(onEnable.contains("IrisServices.register(IrisTerrainService.class, this)"));
assertTrue(onDisable.contains("Bukkit.getServicesManager().unregister(IrisTerrainService.class, this)"));
assertTrue(onDisable.contains("IrisServices.remove(IrisTerrainService.class)"));
assertBefore(onDisable, "serviceEnabled.set(false)", "Bukkit.getServicesManager().unregister(");
}
@Test
public void theTerrainServiceNeverForcesEngineInitialisationOrLoadsTheMantle() throws IOException {
String source = source("iris.terrainSvcSource");
assertFalse("isIrisWorld must not touch the generator", source.contains("IrisToolbelt"));
assertFalse("isIrisWorld must not touch the generator", source.contains(".touch("));
assertFalse("terrain queries must not load mantle chunks", source.contains("getMantle()"));
assertFalse("terrain queries must not load mantle chunks", source.contains("getObjectsAt("));
assertFalse("terrain queries must not load mantle chunks", source.contains("getPOIsAt("));
assertFalse("terrain queries must not load mantle chunks", source.contains("getCaveOrMantleBiome("));
assertFalse("terrain queries must never block", source.contains(".join()"));
assertFalse("terrain queries must never block", source.contains("synchronized"));
assertFalse("terrain queries must never block", source.contains("J.sfut("));
}
@Test
public void sampleColumnsBoundsTheQueryBeforeItWalksAndTreatsAThrowingSinkAsARefusal() throws IOException {
String sampleColumns = method(source("iris.terrainSvcSource"),
"public boolean sampleColumns(World world, IrisColumnQuery query, IrisColumnSink sink)");
assertBefore(sampleColumns, "IrisSampleLimits.withinLimits(", "IrisColumnWalk.walk(");
assertBefore(sampleColumns, "IrisColumnWalk.walk(", "catch (Throwable error)");
assertTrue(sampleColumns.contains("reportSinkFault(world, error)"));
assertTrue(sampleColumns.contains("return false;"));
assertTrue("the walk must abort when the engine closes underneath it",
sampleColumns.contains("engine.isClosed()"));
}
@Test
public void thePregeneratorDispatchesPhasesFromItsExistingTickAndNeverBlocksOnTheSink() throws IOException {
String source = source("iris.pregeneratorJobSource");
String dispatch = method(source, "private void dispatchApiPhases(List<PregenApiPhase> phases)");
assertTrue(dispatch.contains("IrisServices.getOrNull(PregenApiSink.class)"));
assertTrue(dispatch.contains("catch (Throwable error)"));
assertBefore(dispatch, "sink.pregen(phase, progress)", "catch (Throwable error)");
assertFalse(dispatch.contains(".join()"));
assertTrue(method(source, "public void onTick(").contains("dispatchApiPhases(apiPhases.onTick(paused()))"));
assertTrue(method(source, "public void onSaving()").contains("dispatchApiPhases(apiPhases.onSaving())"));
assertTrue(method(source, "public void onClose()").contains("dispatchApiPhases(apiPhases.onClose(reachedTotal()))"));
}
@Test
public void worldPhasesAreFiredOutsideTheRegistrationLock() throws IOException {
String source = source("iris.engineSvcSource");
String add = method(source, "private void add(World world)");
assertBefore(add, "catch (RejectedExecutionException exception)", "phases.ready(world)");
assertBefore(add, "registered = true;", "phases.ready(world)");
String remove = method(source, "private void remove(World world)");
assertBefore(remove, "registered = worlds.remove(world)", "phases.closing(world)");
assertBefore(remove, "phases.closing(world)", "startClose(registered, closing)");
}
@Test
public void everyRegisteredWorldIsAnnouncedClosingWhenTheServiceShutsDown() throws IOException {
String onDisable = method(source("iris.engineSvcSource"), "public void onDisable()");
assertTrue("service shutdown must announce ENGINE_CLOSING for every registered world",
onDisable.contains("phases.closing(teardown.world())"));
assertBefore(onDisable, "worlds.clear()", "phases.closing(teardown.world())");
assertBefore(onDisable, "phases.closing(teardown.world())", "shutdownAndDrain(activeService)");
assertBefore(onDisable, "phases.closing(teardown.world())",
"startClose(teardown.registered(), teardown.closing())");
}
@Test
public void aReplacedEngineIsAnnouncedClosingBeforeTheRetryReRegistersTheWorld() throws IOException {
String add = method(source("iris.engineSvcSource"), "private void add(World world)");
assertBefore(add, "catch (RejectedExecutionException exception)", "phases.closing(world)");
assertBefore(add, "phases.closing(world)", "retryRegistrationAfterClose(world, retryAfter)");
assertBefore(add, "phases.closing(world)", "startClose(replaced, replacementClose)");
}
@Test
public void aWorldPhaseIsNeverConditionalOnAnotherServicesRegistration() throws IOException {
String source = source("iris.apiEventSvcSource");
String fire = method(source, "public static void fireWorldPhase(World world, IrisWorldPhase phase)");
assertFalse("world lifecycle must not resolve a swappable service to build its payload",
fire.contains("IrisServices"));
assertFalse("world lifecycle must not resolve a swappable service to build its payload",
fire.contains("IrisTerrainService"));
assertTrue(fire.contains("deliver(new IrisWorldEngineEvent(world, phase, describe(world, phase)))"));
String describe = method(source, "private static IrisWorldInfo describe(World world, IrisWorldPhase phase)");
assertTrue(describe.contains("IrisWorldInfoFactory.forWorld(world)"));
assertTrue("an undescribable world must be reported, not swallowed",
describe.contains("IrisLogging.reportError("));
assertTrue("an undescribable world must still deliver the phase", describe.contains("return null;"));
}
@Test
public void aWorldPhaseRaisedFromAServerThreadIsDeliveredBeforeThatThreadMovesOn() throws IOException {
String deliver = method(source("iris.apiEventSvcSource"), "private static void deliver(Event event)");
assertBefore(deliver, "Bukkit.isPrimaryThread()", "Bukkit.getPluginManager().callEvent(event)");
assertBefore(deliver, "Bukkit.getPluginManager().callEvent(event)", "Iris.callEvent(event)");
}
@Test
public void theWorldInfoFactoryNeverForcesEngineInitialisationOrLoadsTheMantle() throws IOException {
String source = source("iris.worldInfoFactorySource");
assertFalse("the factory must not touch the generator", source.contains("IrisToolbelt"));
assertFalse("the factory must not touch the generator", source.contains(".touch("));
assertFalse("the factory must not load mantle chunks", source.contains("getMantle()"));
assertFalse("the factory must never block", source.contains(".join()"));
assertFalse("the factory must never block", source.contains("synchronized"));
assertTrue("the factory must refuse a closed engine", source.contains("engine.isClosed()"));
}
@Test
public void theHotloadHookStillFiresTheLegacyEventAlongsideTheApiEvent() throws IOException {
String hook = method(source("iris.bukkitEnginePlatformHooksSource"), "public void fireHotloadEvent(Engine engine)");
assertBefore(hook, "new IrisEngineHotloadEvent(engine)",
"IrisApiEventSVC.fireWorldPhase(BukkitWorldBinding.world(engine.getWorld()), IrisWorldPhase.ENGINE_HOTLOADED)");
}
@Test
public void theEventServiceNeverLetsAThirdPartyFailureEscapeALifecyclePath() throws IOException {
String fire = method(source("iris.apiEventSvcSource"),
"public static void fireWorldPhase(World world, IrisWorldPhase phase)");
assertTrue(fire.contains("catch (Throwable error)"));
assertTrue(fire.contains("IrisLogging.reportError("));
assertFalse(fire.contains("printStackTrace"));
}
private static String source(String property) throws IOException {
String path = System.getProperty(property);
assertTrue("missing source property " + property, path != null && !path.isBlank());
return Files.readString(Path.of(path));
}
private static void assertBefore(String source, String first, String second) {
int firstIndex = source.indexOf(first);
int secondIndex = source.indexOf(second);
assertTrue("Missing source contract token: " + first, firstIndex >= 0);
assertTrue("Missing source contract token: " + second, secondIndex >= 0);
assertTrue(first + " must occur before " + second, firstIndex < secondIndex);
}
private static String method(String source, String signature) {
int start = source.indexOf(signature);
assertTrue("Missing source contract signature: " + signature, start >= 0);
int openBrace = source.indexOf('{', start);
assertTrue("Missing source contract method body: " + signature, openBrace >= 0);
int depth = 0;
for (int index = openBrace; index < source.length(); index++) {
char current = source.charAt(index);
if (current == '{') {
depth++;
} else if (current == '}') {
depth--;
if (depth == 0) {
return source.substring(start, index + 1);
}
}
}
throw new IllegalArgumentException("Unclosed source contract method: " + signature);
}
}
@@ -0,0 +1,100 @@
package art.arcane.iris.core.service;
import art.arcane.iris.api.terrain.IrisColumnField;
import art.arcane.iris.api.terrain.IrisColumnQuery;
import art.arcane.iris.api.terrain.IrisSurfaceKind;
import art.arcane.iris.api.terrain.IrisTerrainService;
import art.arcane.iris.util.common.plugin.IrisService;
import org.bukkit.World;
import org.junit.Test;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.EnumSet;
import java.util.Optional;
import java.util.OptionalInt;
import java.util.concurrent.atomic.AtomicInteger;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
public class IrisTerrainSVCTest {
private static final IrisColumnQuery SMALL = IrisColumnQuery.rect(
0, 0, 15, 15, 4, EnumSet.of(IrisColumnField.SURFACE_HEIGHT, IrisColumnField.SURFACE_KIND));
@Test
public void theServiceImplementsBothContracts() throws NoSuchMethodException {
assertTrue(IrisService.class.isAssignableFrom(IrisTerrainSVC.class));
assertTrue(IrisTerrainService.class.isAssignableFrom(IrisTerrainSVC.class));
IrisTerrainSVC.class.getDeclaredConstructor();
}
@Test
public void aQueryIsOnlyAnswerableWhileEnabledAndAgainstARealWorld() {
assertTrue(IrisTerrainSVC.answerable(true, true));
assertFalse("a disabled service must never resolve a generator",
IrisTerrainSVC.answerable(false, true));
assertFalse(IrisTerrainSVC.answerable(true, false));
assertFalse(IrisTerrainSVC.answerable(false, false));
}
@Test
public void aServiceThatIsNotEnabledAnswersAbsenceInsteadOfThrowing() {
IrisTerrainSVC service = new IrisTerrainSVC();
assertFalse(service.isIrisWorld(null));
assertTrue(service.worldInfo(null).isEmpty());
assertEquals(OptionalInt.empty(), service.surfaceHeight(null, 0, 0));
assertEquals(IrisSurfaceKind.UNKNOWN, service.surfaceKind(null, 0, 0));
assertTrue(service.surfaceBiomeKey(null, 0, 0).isEmpty());
assertTrue(service.surfaceBiomeName(null, 0, 0).isEmpty());
assertTrue(service.biomeKey(null, 0, 64, 0).isEmpty());
assertTrue(service.regionKey(null, 0, 0).isEmpty());
assertTrue(service.regionName(null, 0, 0).isEmpty());
}
@Test
public void theDisplayNameAccessorsReadNamesRatherThanLoadKeys() throws IOException {
String source = Files.readString(Path.of(System.getProperty("iris.terrainSvcSource")));
assertTrue("surfaceBiomeName must read the biome display name",
source.contains("return name(engine.getSurfaceBiome(blockX, blockZ));"));
assertTrue("the biome name helper must read getName()",
source.contains("String name = biome == null ? null : biome.getName();"));
assertTrue("regionName must read the region display name",
source.contains("String name = region == null ? null : region.getName();"));
}
@Test
public void theServiceExposesDisplayNamesAlongsideLoadKeys() throws NoSuchMethodException {
assertEquals(Optional.class,
IrisTerrainService.class.getMethod("surfaceBiomeName", World.class, int.class, int.class)
.getReturnType());
assertEquals(Optional.class,
IrisTerrainService.class.getMethod("regionName", World.class, int.class, int.class)
.getReturnType());
}
@Test
public void anUnanswerableSampleNeverTouchesTheSink() {
IrisTerrainSVC service = new IrisTerrainSVC();
AtomicInteger sinkCalls = new AtomicInteger();
boolean answered = service.sampleColumns(null, SMALL,
(int blockX, int blockZ, int surfaceHeight, IrisSurfaceKind kind, String biomeKey)
-> sinkCalls.incrementAndGet());
assertFalse(answered);
assertEquals(0, sinkCalls.get());
}
@Test
public void nullArgumentsAreRefusedRatherThanDereferenced() {
IrisTerrainSVC service = new IrisTerrainSVC();
assertFalse(service.sampleColumns(null, null, null));
assertFalse(service.sampleColumns(null, SMALL, null));
}
}
@@ -0,0 +1,119 @@
package art.arcane.iris.core.service;
import art.arcane.iris.api.world.IrisWorldPhase;
import org.bukkit.World;
import org.junit.Test;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
import static org.junit.Assert.assertEquals;
public class IrisWorldPhaseLedgerTest {
@Test
public void aWorldIsAnnouncedReadyOnceNoMatterHowOftenRegistrationRuns() {
List<String> fired = new ArrayList<>();
IrisWorldPhaseLedger ledger = ledger(fired);
World world = world("alpha");
ledger.ready(world);
ledger.ready(world);
ledger.ready(world);
assertEquals(List.of("alpha:ENGINE_READY"), fired);
}
@Test
public void closingIsNeverAnnouncedForAWorldThatWasNeverAnnouncedReady() {
List<String> fired = new ArrayList<>();
IrisWorldPhaseLedger ledger = ledger(fired);
World world = world("alpha");
ledger.closing(world);
assertEquals(List.of(), fired);
}
@Test
public void everyReadyWorldIsAnnouncedClosingExactlyOnce() {
List<String> fired = new ArrayList<>();
IrisWorldPhaseLedger ledger = ledger(fired);
World world = world("alpha");
ledger.ready(world);
ledger.closing(world);
ledger.closing(world);
assertEquals(List.of("alpha:ENGINE_READY", "alpha:ENGINE_CLOSING"), fired);
}
@Test
public void anEngineReplacementClosesBeforeItIsAnnouncedReadyAgain() {
List<String> fired = new ArrayList<>();
IrisWorldPhaseLedger ledger = ledger(fired);
World world = world("alpha");
ledger.ready(world);
ledger.closing(world);
ledger.ready(world);
ledger.closing(world);
assertEquals(List.of(
"alpha:ENGINE_READY",
"alpha:ENGINE_CLOSING",
"alpha:ENGINE_READY",
"alpha:ENGINE_CLOSING"), fired);
}
@Test
public void worldsAreTrackedIndependently() {
List<String> fired = new ArrayList<>();
IrisWorldPhaseLedger ledger = ledger(fired);
World first = world("alpha");
World second = world("beta");
ledger.ready(first);
ledger.ready(second);
ledger.closing(first);
ledger.ready(first);
assertEquals(List.of(
"alpha:ENGINE_READY",
"beta:ENGINE_READY",
"alpha:ENGINE_CLOSING",
"alpha:ENGINE_READY"), fired);
}
@Test
public void aWorldThatIsNoLongerAddressableIsNeverAnnounced() {
List<String> fired = new ArrayList<>();
IrisWorldPhaseLedger ledger = ledger(fired);
ledger.ready(null);
ledger.closing(null);
assertEquals(List.of(), fired);
}
private static IrisWorldPhaseLedger ledger(List<String> fired) {
return new IrisWorldPhaseLedger((World world, IrisWorldPhase phase) ->
fired.add(world.getName() + ":" + phase.name()));
}
private static World world(String name) {
UUID identity = UUID.nameUUIDFromBytes(name.getBytes());
return (World) Proxy.newProxyInstance(
IrisWorldPhaseLedgerTest.class.getClassLoader(),
new Class<?>[]{World.class},
(Object proxy, Method method, Object[] arguments) -> switch (method.getName()) {
case "getUID" -> identity;
case "getName", "toString" -> name;
case "hashCode" -> System.identityHashCode(proxy);
case "equals" -> proxy == arguments[0];
default -> throw new UnsupportedOperationException(method.getName());
});
}
}
@@ -0,0 +1,52 @@
package art.arcane.iris.core.service.terrain;
import org.junit.Test;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
public class IrisApiFaultGuardTest {
@Test
public void theFirstFaultIsAlwaysReported() {
IrisApiFaultGuard guard = new IrisApiFaultGuard(60_000L);
assertTrue(guard.record(0L));
assertEquals(1L, guard.faults());
}
@Test
public void faultsInsideTheIntervalAreCountedButNotReported() {
IrisApiFaultGuard guard = new IrisApiFaultGuard(60_000L);
guard.record(1_000L);
assertFalse(guard.record(2_000L));
assertFalse(guard.record(60_999L));
assertEquals(3L, guard.faults());
}
@Test
public void reportingResumesOnceTheIntervalElapses() {
IrisApiFaultGuard guard = new IrisApiFaultGuard(60_000L);
guard.record(1_000L);
guard.record(2_000L);
assertTrue(guard.record(61_000L));
assertFalse(guard.record(61_001L));
assertEquals(4L, guard.faults());
}
@Test
public void aZeroIntervalReportsEveryFault() {
IrisApiFaultGuard guard = new IrisApiFaultGuard(0L);
assertTrue(guard.record(5L));
assertTrue(guard.record(5L));
assertEquals(2L, guard.faults());
}
@Test(expected = IllegalArgumentException.class)
public void aNegativeIntervalIsRejected() {
new IrisApiFaultGuard(-1L);
}
}
@@ -0,0 +1,87 @@
package art.arcane.iris.core.service.terrain;
import art.arcane.iris.api.terrain.IrisColumnField;
import art.arcane.iris.api.terrain.IrisColumnQuery;
import org.junit.Test;
import java.util.ArrayList;
import java.util.EnumSet;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
public class IrisColumnWalkTest {
private static final EnumSet<IrisColumnField> ANY = EnumSet.of(IrisColumnField.SURFACE_HEIGHT);
@Test
public void visitsExactlyTheAdvertisedColumnCount() {
IrisColumnQuery query = IrisColumnQuery.rect(-40, -40, 39, 39, 8, ANY);
List<long[]> visited = new ArrayList<>();
long count = IrisColumnWalk.walk(query, (int blockX, int blockZ) -> visited.add(new long[]{blockX, blockZ}));
assertEquals(query.columnCount(), count);
assertEquals(query.columnCount(), visited.size());
}
@Test
public void everyColumnIsStrideAlignedAndInsideTheRect() {
IrisColumnQuery query = IrisColumnQuery.rect(-37, -21, 60, 44, 7, ANY);
IrisColumnWalk.walk(query, (int blockX, int blockZ) -> {
assertTrue(blockX >= query.minBlockX() && blockX <= query.maxBlockX());
assertTrue(blockZ >= query.minBlockZ() && blockZ <= query.maxBlockZ());
assertEquals(0, (blockX - query.minBlockX()) % query.strideBlocks());
assertEquals(0, (blockZ - query.minBlockZ()) % query.strideBlocks());
return true;
});
}
@Test
public void everyChunkIsVisitedOnceAsAContiguousRun() {
IrisColumnQuery query = IrisColumnQuery.rect(-33, -33, 47, 47, 4, ANY);
List<Long> chunkRuns = new ArrayList<>();
IrisColumnWalk.walk(query, (int blockX, int blockZ) -> {
long chunkKey = (((long) (blockX >> 4)) << 32) ^ ((blockZ >> 4) & 0xFFFFFFFFL);
if (chunkRuns.isEmpty() || chunkRuns.get(chunkRuns.size() - 1) != chunkKey) {
chunkRuns.add(chunkKey);
}
return true;
});
Set<Long> distinct = new LinkedHashSet<>(chunkRuns);
assertEquals("a chunk must never be revisited after the walk leaves it",
distinct.size(), chunkRuns.size());
}
@Test
public void aRefusingVisitorStopsTheWalkAndReportsWhatItSaw() {
IrisColumnQuery query = IrisColumnQuery.rect(0, 0, 63, 63, 1, ANY);
int[] seen = new int[1];
long count = IrisColumnWalk.walk(query, (int blockX, int blockZ) -> {
seen[0]++;
return seen[0] < 10;
});
assertEquals(9L, count);
assertEquals(10, seen[0]);
assertTrue(count < query.columnCount());
}
@Test
public void aSingleColumnRectVisitsExactlyThatColumn() {
IrisColumnQuery query = IrisColumnQuery.rect(-1, -1, -1, -1, 16, ANY);
List<long[]> visited = new ArrayList<>();
long count = IrisColumnWalk.walk(query, (int blockX, int blockZ) -> visited.add(new long[]{blockX, blockZ}));
assertEquals(1L, count);
assertEquals(-1L, visited.get(0)[0]);
assertEquals(-1L, visited.get(0)[1]);
}
}
@@ -0,0 +1,58 @@
package art.arcane.iris.core.service.terrain;
import art.arcane.iris.api.terrain.IrisColumnField;
import art.arcane.iris.api.terrain.IrisColumnQuery;
import org.junit.Test;
import java.util.EnumSet;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
public class IrisSampleLimitsTest {
@Test
public void chunkCapIsAShareOfTheNoiseCacheWithAFloor() {
assertEquals(256, IrisSampleLimits.maxChunks(1_024));
assertEquals(IrisSampleLimits.MINIMUM_CHUNKS, IrisSampleLimits.maxChunks(0));
assertEquals(IrisSampleLimits.MINIMUM_CHUNKS, IrisSampleLimits.maxChunks(16));
}
@Test
public void columnCapIsDerivedFromTheChunkCapAndDoesNotOverflow() {
assertEquals(65_536, IrisSampleLimits.maxColumns(1_024));
assertTrue(IrisSampleLimits.maxColumns(Integer.MAX_VALUE) > 0);
}
@Test
public void strideCannotSmuggleAQueryPastTheChunkCap() {
IrisColumnQuery smuggled = IrisColumnQuery.rect(
0, 0, 6399, 6399, 64, EnumSet.of(IrisColumnField.SURFACE_KIND));
int maxColumns = IrisSampleLimits.maxColumns(1_024);
int maxChunks = IrisSampleLimits.maxChunks(1_024);
assertTrue("this query must pass a column-only cap", smuggled.columnCount() <= maxColumns);
assertFalse("but it must be refused on chunk span",
IrisSampleLimits.withinLimits(smuggled, maxColumns, maxChunks));
}
@Test
public void aQueryInsideBothCapsIsAccepted() {
IrisColumnQuery accepted = IrisColumnQuery.rect(
0, 0, 255, 255, 4, EnumSet.of(IrisColumnField.SURFACE_KIND));
assertTrue(IrisSampleLimits.withinLimits(
accepted, IrisSampleLimits.maxColumns(1_024), IrisSampleLimits.maxChunks(1_024)));
}
@Test
public void aDenseQueryOverManyColumnsIsRefused() {
IrisColumnQuery dense = IrisColumnQuery.rect(
0, 0, 1023, 1023, 1, EnumSet.of(IrisColumnField.SURFACE_HEIGHT));
assertTrue(dense.columnCount() > IrisSampleLimits.maxColumns(1_024));
assertFalse(IrisSampleLimits.withinLimits(
dense, IrisSampleLimits.maxColumns(1_024), IrisSampleLimits.maxChunks(1_024)));
}
}
@@ -0,0 +1,61 @@
package art.arcane.iris.core.service.terrain;
import art.arcane.iris.api.terrain.IrisSurfaceKind;
import art.arcane.iris.engine.object.InferredType;
import org.junit.Test;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
public class IrisSurfaceClassifierTest {
private static final int FLUID = 127;
@Test
public void columnAtOrBelowWorldMinimumIsVoid() {
assertEquals(IrisSurfaceKind.VOID, IrisSurfaceClassifier.classify(0, FLUID, InferredType.LAND));
assertEquals(IrisSurfaceKind.VOID, IrisSurfaceClassifier.classify(-8, FLUID, InferredType.SEA));
}
@Test
public void oceanIsExactlyTheEngineUnderwaterPredicate() {
assertEquals(IrisSurfaceKind.OCEAN, IrisSurfaceClassifier.classify(FLUID, FLUID, InferredType.LAND));
assertEquals(IrisSurfaceKind.OCEAN, IrisSurfaceClassifier.classify(FLUID - 1, FLUID, InferredType.LAND));
assertEquals(IrisSurfaceKind.LAND, IrisSurfaceClassifier.classify(FLUID + 1, FLUID, InferredType.LAND));
}
@Test
public void shoreOnlyAppliesAboveTheFluidLine() {
assertEquals(IrisSurfaceKind.SHORE, IrisSurfaceClassifier.classify(FLUID + 1, FLUID, InferredType.SHORE));
assertEquals(IrisSurfaceKind.OCEAN, IrisSurfaceClassifier.classify(FLUID, FLUID, InferredType.SHORE));
}
@Test
public void caveAndAbsentTypesFallBackToLandAboveWater() {
assertEquals(IrisSurfaceKind.LAND, IrisSurfaceClassifier.classify(FLUID + 10, FLUID, InferredType.CAVE));
assertEquals(IrisSurfaceKind.LAND, IrisSurfaceClassifier.classify(FLUID + 10, FLUID, InferredType.SEA));
assertEquals(IrisSurfaceKind.LAND, IrisSurfaceClassifier.classify(FLUID + 10, FLUID, null));
}
@Test
public void biomeIsOnlyRequiredWhenTheAnswerCanDependOnIt() {
assertFalse(IrisSurfaceClassifier.requiresSurfaceBiome(0, FLUID));
assertFalse(IrisSurfaceClassifier.requiresSurfaceBiome(FLUID, FLUID));
assertTrue(IrisSurfaceClassifier.requiresSurfaceBiome(FLUID + 1, FLUID));
}
@Test
public void whenBiomeIsNotRequiredEveryInferredTypeYieldsTheSameKind() {
for (int surface = -4; surface <= FLUID; surface++) {
if (IrisSurfaceClassifier.requiresSurfaceBiome(surface, FLUID)) {
continue;
}
IrisSurfaceKind expected = IrisSurfaceClassifier.classify(surface, FLUID, null);
for (InferredType inferredType : InferredType.values()) {
assertEquals("surface=" + surface + " type=" + inferredType,
expected, IrisSurfaceClassifier.classify(surface, FLUID, inferredType));
}
}
}
}
@@ -0,0 +1,52 @@
package art.arcane.iris.core.service.terrain;
import art.arcane.iris.api.terrain.IrisWorldInfo;
import org.junit.Test;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
public class IrisWorldInfoFactoryTest {
@Test
public void aDescribableWorldReportsFluidHeightInAbsoluteWorldY() {
IrisWorldInfo info = IrisWorldInfoFactory.build(
"overworld", "minecraft:world", 42L, -64, 320, 63, false);
assertNotNull(info);
assertEquals("overworld", info.dimensionKey());
assertEquals("minecraft:world", info.worldIdentity());
assertEquals(42L, info.seed());
assertEquals(-64, info.minHeight());
assertEquals(320, info.maxHeight());
assertEquals(-1, info.fluidHeight());
assertEquals(384, info.height());
assertFalse(info.studio());
}
@Test
public void aStudioWorldIsReportedAsStudio() {
IrisWorldInfo info = IrisWorldInfoFactory.build(
"overworld", "minecraft:studio", 1L, 0, 256, 63, true);
assertNotNull(info);
assertTrue(info.studio());
assertEquals(63, info.fluidHeight());
}
@Test
public void anIndescribableWorldIsAbsentRatherThanHalfBuilt() {
assertNull(IrisWorldInfoFactory.build(null, "minecraft:world", 1L, 0, 256, 63, false));
assertNull(IrisWorldInfoFactory.build("overworld", null, 1L, 0, 256, 63, false));
assertNull(IrisWorldInfoFactory.build("overworld", "minecraft:world", 1L, 256, 256, 63, false));
assertNull(IrisWorldInfoFactory.build("overworld", "minecraft:world", 1L, 320, 0, 63, false));
}
@Test
public void anAbsentGeneratorOrWorldIsDescribedAsNothing() {
assertNull(IrisWorldInfoFactory.from(null));
assertNull(IrisWorldInfoFactory.forWorld(null));
}
}