mirror of
https://github.com/VolmitSoftware/Iris.git
synced 2026-08-27 12:41:43 +00:00
API
This commit is contained in:
@@ -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
|
||||
}
|
||||
+31
@@ -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);
|
||||
}
|
||||
}
|
||||
+35
@@ -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
|
||||
}
|
||||
+32
@@ -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;
|
||||
}
|
||||
}
|
||||
+44
@@ -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
|
||||
}
|
||||
+33
-91
@@ -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;
|
||||
}
|
||||
}
|
||||
+51
@@ -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());
|
||||
}
|
||||
}
|
||||
+3
@@ -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
|
||||
|
||||
+106
@@ -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)));
|
||||
}
|
||||
}
|
||||
+22
-9
@@ -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) {
|
||||
}
|
||||
}
|
||||
|
||||
+316
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
+49
@@ -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();
|
||||
}
|
||||
}
|
||||
+31
@@ -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);
|
||||
}
|
||||
}
|
||||
+49
@@ -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);
|
||||
}
|
||||
}
|
||||
+24
@@ -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;
|
||||
}
|
||||
}
|
||||
+25
@@ -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;
|
||||
}
|
||||
}
|
||||
+71
@@ -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));
|
||||
}
|
||||
}
|
||||
+154
@@ -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;
|
||||
}
|
||||
}
|
||||
+50
@@ -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);
|
||||
}
|
||||
}
|
||||
+77
@@ -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));
|
||||
}
|
||||
}
|
||||
+30
@@ -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);
|
||||
}
|
||||
}
|
||||
+64
@@ -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());
|
||||
});
|
||||
}
|
||||
}
|
||||
+117
@@ -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");
|
||||
}
|
||||
}
|
||||
+249
@@ -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"));
|
||||
}
|
||||
}
|
||||
+132
@@ -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"));
|
||||
}
|
||||
}
|
||||
+316
@@ -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));
|
||||
}
|
||||
}
|
||||
+394
@@ -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));
|
||||
}
|
||||
}
|
||||
+104
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
+82
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
+193
@@ -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);
|
||||
}
|
||||
}
|
||||
+100
@@ -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));
|
||||
}
|
||||
}
|
||||
+119
@@ -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());
|
||||
});
|
||||
}
|
||||
}
|
||||
+52
@@ -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);
|
||||
}
|
||||
}
|
||||
+87
@@ -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]);
|
||||
}
|
||||
}
|
||||
+58
@@ -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)));
|
||||
}
|
||||
}
|
||||
+61
@@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+52
@@ -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));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user