This commit is contained in:
Brian Neumann-Fopiano
2026-07-26 12:12:49 -05:00
parent de25b7d7ab
commit d5a55ccfcf
66 changed files with 6300 additions and 424 deletions
@@ -37,7 +37,10 @@ import art.arcane.iris.core.runtime.BukkitEnginePlatformHooks;
import art.arcane.iris.core.runtime.TransientWorldCleanupSupport;
import art.arcane.iris.core.runtime.WorldRuntimeControlService;
import art.arcane.iris.core.lifecycle.WorldLifecycleStaging;
import art.arcane.iris.core.link.IrisPapiExpansion;
import art.arcane.iris.api.terrain.IrisTerrainService;
import art.arcane.iris.core.link.IrisPapiInstaller;
import art.arcane.iris.core.link.IrisPapiListener;
import art.arcane.iris.core.link.IrisPapiState;
import art.arcane.iris.core.localization.IrisLanguage;
import art.arcane.iris.core.link.MultiverseCoreLink;
import art.arcane.iris.core.loader.IrisData;
@@ -71,6 +74,7 @@ import art.arcane.iris.spi.IrisServices;
import art.arcane.iris.spi.LogLevel;
import art.arcane.volmlib.integration.ReloadAware;
import art.arcane.volmlib.util.bukkit.WorldIdentity;
import art.arcane.volmlib.util.bukkit.papi.PlaceholderRegistration;
import art.arcane.volmlib.util.collection.KList;
import art.arcane.volmlib.util.collection.KMap;
import art.arcane.volmlib.util.exceptions.IrisException;
@@ -173,6 +177,9 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
}
private final AtomicBoolean alreadyDrained = new AtomicBoolean(false);
private volatile PlaceholderRegistration papiRegistration;
private volatile IrisPapiListener papiListener;
private volatile IrisPapiState papiState;
private KMap<Class<? extends IrisService>, IrisService> services;
public static VolmitSender getSender() {
@@ -1000,6 +1007,7 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
}
public void onDisable() {
teardownPapi();
if (IrisSafeguard.isForceShutdown()) return;
if (alreadyDrained.compareAndSet(false, true)) {
drainWorldGenerators("onDisable", 30L);
@@ -1023,6 +1031,7 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
@Override
public void onPreUnload(ReloadAware.PreUnloadReason reason) {
teardownPapi();
if (!alreadyDrained.compareAndSet(false, true)) {
Iris.info("Pre-unload hook skipped; Iris already drained.");
return;
@@ -1083,8 +1092,53 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
}
private void setupPapi() {
if (Bukkit.getPluginManager().isPluginEnabled("PlaceholderAPI")) {
new IrisPapiExpansion().register();
if (!PlaceholderRegistration.isPlaceholderApiEnabled()) {
return;
}
IrisPapiState state = new IrisPapiState(() -> IrisServices.getOrNull(IrisTerrainService.class));
PlaceholderRegistration registration = new PlaceholderRegistration(getLogger());
if (!IrisPapiInstaller.install(registration, state, getLogger())) {
return;
}
IrisPapiListener listener = new IrisPapiListener(state);
try {
Bukkit.getPluginManager().registerEvents(listener, this);
} catch (Throwable failure) {
registration.unregister();
Iris.warn("Failed to attach the Iris PlaceholderAPI listener: "
+ failure.getClass().getName() + ": " + failure.getMessage());
return;
}
papiState = state;
papiListener = listener;
papiRegistration = registration;
}
private void teardownPapi() {
IrisPapiListener listener = papiListener;
papiListener = null;
if (listener != null) {
HandlerList.unregisterAll(listener);
}
PlaceholderRegistration registration = papiRegistration;
papiRegistration = null;
if (registration != null) {
registration.unregister();
}
IrisPapiState state = papiState;
papiState = null;
if (state != null) {
state.clear();
}
}
@@ -0,0 +1,11 @@
package art.arcane.iris.api.pregen;
public enum IrisPregenPhase {
STARTED,
TICK,
PAUSED,
RESUMED,
SAVING,
COMPLETED,
CANCELLED
}
@@ -0,0 +1,31 @@
package art.arcane.iris.api.pregen;
import java.util.Objects;
public record IrisPregenProgress(
String worldName,
String worldIdentity,
double percent,
long generatedChunks,
long totalChunks,
long remainingChunks,
long failedChunks,
double chunksPerSecond,
long etaMillis,
long elapsedMillis,
String method,
boolean paused) {
public IrisPregenProgress {
Objects.requireNonNull(worldIdentity, "worldIdentity");
worldName = worldName == null ? worldIdentity : worldName;
method = method == null ? "" : method;
percent = Double.isFinite(percent) ? Math.clamp(percent, 0D, 100D) : 0D;
generatedChunks = Math.max(0L, generatedChunks);
totalChunks = Math.max(0L, totalChunks);
remainingChunks = Math.max(0L, remainingChunks);
failedChunks = Math.max(0L, failedChunks);
chunksPerSecond = Double.isFinite(chunksPerSecond) ? Math.max(0D, chunksPerSecond) : 0D;
etaMillis = Math.max(0L, etaMillis);
elapsedMillis = Math.max(0L, elapsedMillis);
}
}
@@ -0,0 +1,35 @@
package art.arcane.iris.api.pregen;
import org.bukkit.event.Event;
import org.bukkit.event.HandlerList;
import java.util.Objects;
public class IrisPregenerationEvent extends Event {
private static final HandlerList HANDLERS = new HandlerList();
private final IrisPregenPhase phase;
private final IrisPregenProgress progress;
public IrisPregenerationEvent(IrisPregenPhase phase, IrisPregenProgress progress) {
this.phase = Objects.requireNonNull(phase, "phase");
this.progress = Objects.requireNonNull(progress, "progress");
}
public static HandlerList getHandlerList() {
return HANDLERS;
}
public IrisPregenPhase getPhase() {
return phase;
}
public IrisPregenProgress getProgress() {
return progress;
}
@Override
public HandlerList getHandlers() {
return HANDLERS;
}
}
@@ -0,0 +1,7 @@
package art.arcane.iris.api.terrain;
public enum IrisColumnField {
SURFACE_HEIGHT,
SURFACE_KIND,
BIOME_KEY
}
@@ -0,0 +1,61 @@
package art.arcane.iris.api.terrain;
import java.util.EnumSet;
import java.util.Objects;
public record IrisColumnQuery(
int minBlockX,
int minBlockZ,
int maxBlockX,
int maxBlockZ,
int strideBlocks,
EnumSet<IrisColumnField> fields) {
public IrisColumnQuery {
Objects.requireNonNull(fields, "fields");
if (fields.isEmpty()) {
throw new IllegalArgumentException("at least one field is required");
}
if (maxBlockX < minBlockX || maxBlockZ < minBlockZ) {
throw new IllegalArgumentException("query bounds are inverted");
}
if (strideBlocks < 1) {
throw new IllegalArgumentException("strideBlocks must be at least 1");
}
fields = EnumSet.copyOf(fields);
}
public static IrisColumnQuery rect(
int minBlockX,
int minBlockZ,
int maxBlockX,
int maxBlockZ,
int strideBlocks,
EnumSet<IrisColumnField> fields) {
return new IrisColumnQuery(minBlockX, minBlockZ, maxBlockX, maxBlockZ, strideBlocks, fields);
}
public long columnCount() {
long columnsX = (((long) maxBlockX - (long) minBlockX) / strideBlocks) + 1L;
long columnsZ = (((long) maxBlockZ - (long) minBlockZ) / strideBlocks) + 1L;
return saturatedProduct(columnsX, columnsZ);
}
public long chunkCount() {
long chunksX = ((long) (maxBlockX >> 4) - (long) (minBlockX >> 4)) + 1L;
long chunksZ = ((long) (maxBlockZ >> 4) - (long) (minBlockZ >> 4)) + 1L;
return saturatedProduct(chunksX, chunksZ);
}
private static long saturatedProduct(long left, long right) {
try {
return Math.multiplyExact(left, right);
} catch (ArithmeticException overflow) {
return Long.MAX_VALUE;
}
}
@Override
public EnumSet<IrisColumnField> fields() {
return EnumSet.copyOf(fields);
}
}
@@ -0,0 +1,6 @@
package art.arcane.iris.api.terrain;
@FunctionalInterface
public interface IrisColumnSink {
void accept(int blockX, int blockZ, int surfaceHeight, IrisSurfaceKind kind, String biomeKey);
}
@@ -0,0 +1,9 @@
package art.arcane.iris.api.terrain;
public enum IrisSurfaceKind {
UNKNOWN,
LAND,
SHORE,
OCEAN,
VOID
}
@@ -0,0 +1,32 @@
package art.arcane.iris.api.terrain;
import org.bukkit.World;
import java.util.Optional;
import java.util.OptionalInt;
public interface IrisTerrainService {
boolean isIrisWorld(World world);
Optional<IrisWorldInfo> worldInfo(World world);
OptionalInt surfaceHeight(World world, int blockX, int blockZ);
IrisSurfaceKind surfaceKind(World world, int blockX, int blockZ);
Optional<String> surfaceBiomeKey(World world, int blockX, int blockZ);
Optional<String> surfaceBiomeName(World world, int blockX, int blockZ);
Optional<String> biomeKey(World world, int blockX, int blockY, int blockZ);
Optional<String> regionKey(World world, int blockX, int blockZ);
Optional<String> regionName(World world, int blockX, int blockZ);
int maxSampleColumns();
int maxSampleChunks();
boolean sampleColumns(World world, IrisColumnQuery query, IrisColumnSink sink);
}
@@ -0,0 +1,24 @@
package art.arcane.iris.api.terrain;
import java.util.Objects;
public record IrisWorldInfo(
String dimensionKey,
String worldIdentity,
long seed,
int minHeight,
int maxHeight,
int fluidHeight,
boolean studio) {
public IrisWorldInfo {
Objects.requireNonNull(dimensionKey, "dimensionKey");
Objects.requireNonNull(worldIdentity, "worldIdentity");
if (maxHeight <= minHeight) {
throw new IllegalArgumentException("maxHeight must exceed minHeight");
}
}
public int height() {
return maxHeight - minHeight;
}
}
@@ -0,0 +1,44 @@
package art.arcane.iris.api.world;
import art.arcane.iris.api.terrain.IrisWorldInfo;
import org.bukkit.World;
import org.bukkit.event.Event;
import org.bukkit.event.HandlerList;
import java.util.Objects;
import java.util.Optional;
public class IrisWorldEngineEvent extends Event {
private static final HandlerList HANDLERS = new HandlerList();
private final World world;
private final IrisWorldPhase phase;
private final IrisWorldInfo info;
public IrisWorldEngineEvent(World world, IrisWorldPhase phase, IrisWorldInfo info) {
this.world = Objects.requireNonNull(world, "world");
this.phase = Objects.requireNonNull(phase, "phase");
this.info = info;
}
public static HandlerList getHandlerList() {
return HANDLERS;
}
public World getWorld() {
return world;
}
public IrisWorldPhase getPhase() {
return phase;
}
public Optional<IrisWorldInfo> getInfo() {
return Optional.ofNullable(info);
}
@Override
public HandlerList getHandlers() {
return HANDLERS;
}
}
@@ -0,0 +1,7 @@
package art.arcane.iris.api.world;
public enum IrisWorldPhase {
ENGINE_READY,
ENGINE_HOTLOADED,
ENGINE_CLOSING
}
@@ -18,100 +18,42 @@
package art.arcane.iris.core.link;
import art.arcane.iris.Iris;
import art.arcane.iris.core.tools.IrisToolbelt;
import art.arcane.iris.engine.object.IrisBiome;
import art.arcane.iris.engine.platform.EngineBukkitOps;
import art.arcane.iris.engine.platform.PlatformChunkGenerator;
import me.clip.placeholderapi.expansion.PlaceholderExpansion;
import org.bukkit.Location;
import org.bukkit.OfflinePlayer;
import org.jetbrains.annotations.NotNull;
import art.arcane.volmlib.util.bukkit.papi.PlaceholderKeyRegistry;
import art.arcane.volmlib.util.bukkit.papi.VolmitPlaceholderExpansion;
// See/update https://app.gitbook.com/@volmitsoftware/s/iris/compatability/papi/
public class IrisPapiExpansion extends PlaceholderExpansion {
@Override
public @NotNull String getIdentifier() {
return "iris";
import java.util.Objects;
import java.util.logging.Logger;
public final class IrisPapiExpansion extends VolmitPlaceholderExpansion {
public static final String IDENTIFIER = "iris";
public static final String AUTHOR = "Volmit Software";
public static final String VERSION = "2.0.0";
public static final String REQUIRED_PLUGIN = "Iris";
public IrisPapiExpansion(IrisPapiState state, Logger logger) {
super(IDENTIFIER, AUTHOR, VERSION, REQUIRED_PLUGIN, registry(state), logger);
}
@Override
public @NotNull String getAuthor() {
return "Volmit Software";
}
public static PlaceholderKeyRegistry registry(IrisPapiState state) {
Objects.requireNonNull(state, "state");
@Override
public @NotNull String getVersion() {
return Iris.instance.getDescription().getVersion();
}
@Override
public boolean persist() {
return true;
}
@Override
public String onRequest(OfflinePlayer player, String p) {
Location l = null;
PlatformChunkGenerator a = null;
if (player.isOnline() && player.getPlayer() != null) {
l = player.getPlayer().getLocation().add(0, 2, 0);
a = IrisToolbelt.access(l.getWorld());
}
if (p.equalsIgnoreCase("biome_name")) {
if (a != null) {
return getBiome(a, l).getName();
}
} else if (p.equalsIgnoreCase("biome_id")) {
if (a != null) {
return getBiome(a, l).getLoadKey();
}
} else if (p.equalsIgnoreCase("biome_file")) {
if (a != null) {
return getBiome(a, l).getLoadFile().getPath();
}
} else if (p.equalsIgnoreCase("region_name")) {
if (a != null) {
return EngineBukkitOps.getRegion(a.getEngine(), l).getName();
}
} else if (p.equalsIgnoreCase("region_id")) {
if (a != null) {
return EngineBukkitOps.getRegion(a.getEngine(), l).getLoadKey();
}
} else if (p.equalsIgnoreCase("region_file")) {
if (a != null) {
return EngineBukkitOps.getRegion(a.getEngine(), l).getLoadFile().getPath();
}
} else if (p.equalsIgnoreCase("terrain_slope")) {
if (a != null) {
return (a.getEngine())
.getComplex().getSlopeStream()
.get(l.getX(), l.getZ()) + "";
}
} else if (p.equalsIgnoreCase("terrain_height")) {
if (a != null) {
return Math.round(a.getEngine().getHeight(l.getBlockX(), l.getBlockZ())) + "";
}
} else if (p.equalsIgnoreCase("world_mode")) {
if (a != null) {
return a.isStudio() ? "Studio" : "Production";
}
} else if (p.equalsIgnoreCase("world_seed")) {
if (a != null) {
return a.getEngine().getSeedManager().getSeed() + "";
}
} else if (p.equalsIgnoreCase("world_speed")) {
if (a != null) {
return a.getEngine().getGeneratedPerSecond() + "/s";
}
}
return null;
}
private IrisBiome getBiome(PlatformChunkGenerator a, Location l) {
return a.getEngine().getBiome(l.getBlockX(), l.getBlockY() - l.getWorld().getMinHeight(), l.getBlockZ());
return PlaceholderKeyRegistry.builder()
.key("available", state::available)
.key("world.available", state::worldAvailable)
.key("world.biome", state::biome)
.key("world.biome-key", state::biomeKey)
.key("world.region", state::region)
.key("world.region-key", state::regionKey)
.key("world.dimension", state::dimension)
.key("pregen.available", state::pregenAvailable)
.key("pregen.world", state::pregenWorld)
.key("pregen.percent", state::pregenPercent)
.key("pregen.eta", state::pregenEta)
.key("pregen.eta-text", state::pregenEtaText)
.key("pregen.chunks", state::pregenChunks)
.key("pregen.total", state::pregenTotal)
.key("pregen.chunks-per-second", state::pregenChunksPerSecond)
.key("pregen.paused", state::pregenPaused)
.build();
}
}
@@ -0,0 +1,14 @@
package art.arcane.iris.core.link;
import art.arcane.volmlib.util.bukkit.papi.PlaceholderRegistration;
import java.util.logging.Logger;
public final class IrisPapiInstaller {
private IrisPapiInstaller() {
}
public static boolean install(PlaceholderRegistration registration, IrisPapiState state, Logger logger) {
return registration.register(() -> new IrisPapiExpansion(state, logger));
}
}
@@ -0,0 +1,104 @@
package art.arcane.iris.core.link;
import art.arcane.iris.api.pregen.IrisPregenerationEvent;
import org.bukkit.Location;
import org.bukkit.World;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority;
import org.bukkit.event.Listener;
import org.bukkit.event.player.PlayerChangedWorldEvent;
import org.bukkit.event.player.PlayerJoinEvent;
import org.bukkit.event.player.PlayerMoveEvent;
import org.bukkit.event.player.PlayerPortalEvent;
import org.bukkit.event.player.PlayerQuitEvent;
import org.bukkit.event.player.PlayerRespawnEvent;
import org.bukkit.event.player.PlayerTeleportEvent;
import java.util.Objects;
import java.util.UUID;
public final class IrisPapiListener implements Listener {
private final IrisPapiState state;
public IrisPapiListener(IrisPapiState state) {
this.state = Objects.requireNonNull(state, "state");
}
static void track(IrisPapiState state, UUID playerId, Location location) {
publish(state, playerId, location, false);
}
static void trackNow(IrisPapiState state, UUID playerId, Location location) {
publish(state, playerId, location, true);
}
private static void publish(IrisPapiState state, UUID playerId, Location location, boolean immediate) {
if (state == null || playerId == null || location == null) {
return;
}
World world = location.getWorld();
if (world == null) {
return;
}
if (immediate) {
state.trackPositionNow(playerId, world, location.getBlockX(), location.getBlockZ());
return;
}
state.trackPosition(playerId, world, location.getBlockX(), location.getBlockZ());
}
@EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
public void onPlayerMove(PlayerMoveEvent event) {
Player player = event.getPlayer();
track(state, player == null ? null : player.getUniqueId(), event.getTo());
}
@EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
public void onPlayerTeleport(PlayerTeleportEvent event) {
Player player = event.getPlayer();
trackNow(state, player == null ? null : player.getUniqueId(), event.getTo());
}
@EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
public void onPlayerPortal(PlayerPortalEvent event) {
Player player = event.getPlayer();
trackNow(state, player == null ? null : player.getUniqueId(), event.getTo());
}
@EventHandler(priority = EventPriority.MONITOR)
public void onPlayerRespawn(PlayerRespawnEvent event) {
Player player = event.getPlayer();
trackNow(state, player == null ? null : player.getUniqueId(), event.getRespawnLocation());
}
@EventHandler(priority = EventPriority.MONITOR)
public void onPlayerJoin(PlayerJoinEvent event) {
Player player = event.getPlayer();
trackNow(state, player == null ? null : player.getUniqueId(), player == null ? null : player.getLocation());
}
@EventHandler(priority = EventPriority.MONITOR)
public void onPlayerChangedWorld(PlayerChangedWorldEvent event) {
Player player = event.getPlayer();
trackNow(state, player == null ? null : player.getUniqueId(), player == null ? null : player.getLocation());
}
@EventHandler(priority = EventPriority.MONITOR)
public void onPlayerQuit(PlayerQuitEvent event) {
Player player = event.getPlayer();
if (player != null) {
state.release(player.getUniqueId());
}
}
@EventHandler(priority = EventPriority.MONITOR)
public void onPregeneration(IrisPregenerationEvent event) {
state.publishPregen(event.getPhase(), event.getProgress());
}
}
@@ -0,0 +1,9 @@
package art.arcane.iris.core.link;
import org.bukkit.World;
public record IrisPapiPosition(World world, int blockX, int blockZ, long publishedAtMs) {
public boolean sameColumn(World other, int otherBlockX, int otherBlockZ) {
return world == other && blockX == otherBlockX && blockZ == otherBlockZ;
}
}
@@ -0,0 +1,51 @@
package art.arcane.iris.core.link;
import art.arcane.iris.api.pregen.IrisPregenProgress;
import art.arcane.volmlib.util.bukkit.papi.PlaceholderValues;
public record IrisPapiPregenView(
String world,
String percent,
String eta,
String etaText,
String chunks,
String total,
String chunksPerSecond,
String paused) {
private static final long MILLIS_PER_SECOND = 1_000L;
private static final long SECONDS_PER_MINUTE = 60L;
private static final long SECONDS_PER_HOUR = 3_600L;
public static IrisPapiPregenView of(IrisPregenProgress progress) {
if (progress == null) {
return null;
}
return new IrisPapiPregenView(
PlaceholderValues.text(progress.worldName()),
PlaceholderValues.num(progress.percent()),
PlaceholderValues.count(progress.etaMillis() / MILLIS_PER_SECOND),
duration(progress.etaMillis()),
PlaceholderValues.count(progress.generatedChunks()),
PlaceholderValues.count(progress.totalChunks()),
PlaceholderValues.num(progress.chunksPerSecond()),
PlaceholderValues.bool(progress.paused()));
}
static String duration(long millis) {
long totalSeconds = millis / MILLIS_PER_SECOND;
long hours = totalSeconds / SECONDS_PER_HOUR;
long minutes = (totalSeconds % SECONDS_PER_HOUR) / SECONDS_PER_MINUTE;
long seconds = totalSeconds % SECONDS_PER_MINUTE;
if (hours > 0L) {
return hours + "h " + minutes + "m";
}
if (minutes > 0L) {
return minutes + "m " + seconds + "s";
}
return seconds + "s";
}
}
@@ -0,0 +1,204 @@
package art.arcane.iris.core.link;
import art.arcane.iris.api.pregen.IrisPregenPhase;
import art.arcane.iris.api.pregen.IrisPregenProgress;
import art.arcane.iris.api.terrain.IrisTerrainService;
import art.arcane.iris.api.terrain.IrisWorldInfo;
import art.arcane.volmlib.util.bukkit.papi.PlaceholderSnapshot;
import art.arcane.volmlib.util.bukkit.papi.PlaceholderValues;
import art.arcane.volmlib.util.bukkit.papi.PlayerSnapshotStore;
import org.bukkit.World;
import java.util.Objects;
import java.util.UUID;
import java.util.function.LongSupplier;
import java.util.function.Supplier;
public final class IrisPapiState {
static final long VIEW_TTL_MS = 1_000L;
static final long POSITION_INTERVAL_MS = 1_000L;
private final Supplier<IrisTerrainService> terrain;
private final LongSupplier clock;
private final PlayerSnapshotStore<IrisPapiPosition> positions = new PlayerSnapshotStore<>();
private final PlayerSnapshotStore<IrisPapiWorldView> views = new PlayerSnapshotStore<>();
private final PlaceholderSnapshot<IrisPapiPregenView> pregen = new PlaceholderSnapshot<>();
public IrisPapiState(Supplier<IrisTerrainService> terrain) {
this(terrain, System::currentTimeMillis);
}
public IrisPapiState(Supplier<IrisTerrainService> terrain, LongSupplier clock) {
this.terrain = Objects.requireNonNull(terrain, "terrain");
this.clock = Objects.requireNonNull(clock, "clock");
}
public void trackPosition(UUID playerId, World world, int blockX, int blockZ) {
publishPosition(playerId, world, blockX, blockZ, false);
}
public void trackPositionNow(UUID playerId, World world, int blockX, int blockZ) {
publishPosition(playerId, world, blockX, blockZ, true);
}
private void publishPosition(UUID playerId, World world, int blockX, int blockZ, boolean immediate) {
if (playerId == null || world == null) {
return;
}
IrisPapiPosition current = positions.get(playerId);
if (current != null && current.sameColumn(world, blockX, blockZ)) {
return;
}
long now = clock.getAsLong();
if (!immediate && current != null && current.world() == world
&& now - current.publishedAtMs() < POSITION_INTERVAL_MS) {
return;
}
positions.publish(playerId, new IrisPapiPosition(world, blockX, blockZ, now));
}
public void release(UUID playerId) {
positions.publish(playerId, null);
views.publish(playerId, null);
}
public void publishPregen(IrisPregenPhase phase, IrisPregenProgress progress) {
if (phase == null || progress == null) {
return;
}
pregen.publish(switch (phase) {
case STARTED, TICK, PAUSED, RESUMED, SAVING -> IrisPapiPregenView.of(progress);
case COMPLETED, CANCELLED -> null;
});
}
public void clear() {
positions.clear();
views.clear();
pregen.publish(null);
}
public String available(UUID playerId) {
return PlaceholderValues.bool(terrain.get() != null);
}
public String worldAvailable(UUID playerId) {
IrisPapiWorldView view = viewOf(playerId);
return view == null ? PlaceholderValues.FALSE : view.available();
}
public String biome(UUID playerId) {
IrisPapiWorldView view = viewOf(playerId);
return view == null ? PlaceholderValues.UNAVAILABLE : view.biome();
}
public String biomeKey(UUID playerId) {
IrisPapiWorldView view = viewOf(playerId);
return view == null ? PlaceholderValues.UNAVAILABLE : view.biomeKey();
}
public String region(UUID playerId) {
IrisPapiWorldView view = viewOf(playerId);
return view == null ? PlaceholderValues.UNAVAILABLE : view.region();
}
public String regionKey(UUID playerId) {
IrisPapiWorldView view = viewOf(playerId);
return view == null ? PlaceholderValues.UNAVAILABLE : view.regionKey();
}
public String dimension(UUID playerId) {
IrisPapiWorldView view = viewOf(playerId);
return view == null ? PlaceholderValues.UNAVAILABLE : view.dimension();
}
public String pregenAvailable(UUID playerId) {
return pregen.available();
}
public String pregenWorld(UUID playerId) {
IrisPapiPregenView view = pregen.get();
return view == null ? PlaceholderValues.UNAVAILABLE : view.world();
}
public String pregenPercent(UUID playerId) {
IrisPapiPregenView view = pregen.get();
return view == null ? PlaceholderValues.UNAVAILABLE : view.percent();
}
public String pregenEta(UUID playerId) {
IrisPapiPregenView view = pregen.get();
return view == null ? PlaceholderValues.UNAVAILABLE : view.eta();
}
public String pregenEtaText(UUID playerId) {
IrisPapiPregenView view = pregen.get();
return view == null ? PlaceholderValues.UNAVAILABLE : view.etaText();
}
public String pregenChunks(UUID playerId) {
IrisPapiPregenView view = pregen.get();
return view == null ? PlaceholderValues.UNAVAILABLE : view.chunks();
}
public String pregenTotal(UUID playerId) {
IrisPapiPregenView view = pregen.get();
return view == null ? PlaceholderValues.UNAVAILABLE : view.total();
}
public String pregenChunksPerSecond(UUID playerId) {
IrisPapiPregenView view = pregen.get();
return view == null ? PlaceholderValues.UNAVAILABLE : view.chunksPerSecond();
}
public String pregenPaused(UUID playerId) {
IrisPapiPregenView view = pregen.get();
return view == null ? PlaceholderValues.UNAVAILABLE : view.paused();
}
private IrisPapiWorldView viewOf(UUID playerId) {
IrisPapiPosition position = positions.get(playerId);
if (position == null) {
return null;
}
IrisPapiWorldView cached = views.get(playerId);
long now = clock.getAsLong();
if (cached != null && cached.position() == position && now - cached.builtAtMs() < VIEW_TTL_MS) {
return cached;
}
IrisPapiWorldView built = build(position, now);
views.publish(playerId, built);
return built;
}
private IrisPapiWorldView build(IrisPapiPosition position, long now) {
IrisTerrainService service = terrain.get();
World world = position.world();
if (service == null || !service.isIrisWorld(world)) {
return IrisPapiWorldView.absent(position, now);
}
int blockX = position.blockX();
int blockZ = position.blockZ();
return IrisPapiWorldView.present(
position,
now,
service.surfaceBiomeName(world, blockX, blockZ),
service.surfaceBiomeKey(world, blockX, blockZ),
service.regionName(world, blockX, blockZ),
service.regionKey(world, blockX, blockZ),
service.worldInfo(world).map(IrisWorldInfo::dimensionKey));
}
}
@@ -0,0 +1,50 @@
package art.arcane.iris.core.link;
import art.arcane.volmlib.util.bukkit.papi.PlaceholderValues;
import java.util.Optional;
public record IrisPapiWorldView(
IrisPapiPosition position,
long builtAtMs,
String available,
String biome,
String biomeKey,
String region,
String regionKey,
String dimension) {
public static IrisPapiWorldView absent(IrisPapiPosition position, long builtAtMs) {
return new IrisPapiWorldView(
position,
builtAtMs,
PlaceholderValues.FALSE,
PlaceholderValues.UNAVAILABLE,
PlaceholderValues.UNAVAILABLE,
PlaceholderValues.UNAVAILABLE,
PlaceholderValues.UNAVAILABLE,
PlaceholderValues.UNAVAILABLE);
}
public static IrisPapiWorldView present(
IrisPapiPosition position,
long builtAtMs,
Optional<String> biome,
Optional<String> biomeKey,
Optional<String> region,
Optional<String> regionKey,
Optional<String> dimension) {
return new IrisPapiWorldView(
position,
builtAtMs,
PlaceholderValues.TRUE,
text(biome),
text(biomeKey),
text(region),
text(regionKey),
text(dimension));
}
static String text(Optional<String> value) {
return value == null || value.isEmpty() ? PlaceholderValues.UNAVAILABLE : PlaceholderValues.text(value.get());
}
}
@@ -18,11 +18,13 @@
package art.arcane.iris.core.runtime;
import art.arcane.iris.api.world.IrisWorldPhase;
import art.arcane.iris.core.ServerConfigurator;
import art.arcane.iris.core.datapack.DatapackIngestService;
import art.arcane.iris.core.events.IrisEngineHotloadEvent;
import art.arcane.iris.core.gui.PregeneratorJob;
import art.arcane.iris.core.project.IrisProject;
import art.arcane.iris.core.service.IrisApiEventSVC;
import art.arcane.iris.core.tools.IrisToolbelt;
import art.arcane.iris.core.tools.WorldMaintenance;
import art.arcane.iris.engine.framework.Engine;
@@ -57,6 +59,7 @@ public final class BukkitEnginePlatformHooks implements EnginePlatformHooks {
@Override
public void fireHotloadEvent(Engine engine) {
IrisPlatforms.get().callEvent(new IrisEngineHotloadEvent(engine));
IrisApiEventSVC.fireWorldPhase(BukkitWorldBinding.world(engine.getWorld()), IrisWorldPhase.ENGINE_HOTLOADED);
}
@Override
@@ -0,0 +1,106 @@
package art.arcane.iris.core.service;
import art.arcane.iris.Iris;
import art.arcane.iris.api.pregen.IrisPregenPhase;
import art.arcane.iris.api.pregen.IrisPregenProgress;
import art.arcane.iris.api.pregen.IrisPregenerationEvent;
import art.arcane.iris.api.terrain.IrisWorldInfo;
import art.arcane.iris.api.world.IrisWorldEngineEvent;
import art.arcane.iris.api.world.IrisWorldPhase;
import art.arcane.iris.core.gui.PregeneratorJob;
import art.arcane.iris.core.pregenerator.PregenApiPhase;
import art.arcane.iris.core.pregenerator.PregenApiSink;
import art.arcane.iris.core.service.terrain.IrisWorldInfoFactory;
import art.arcane.iris.spi.IrisLogging;
import art.arcane.iris.spi.IrisServices;
import art.arcane.iris.util.common.plugin.IrisService;
import org.bukkit.Bukkit;
import org.bukkit.World;
import org.bukkit.event.Event;
public class IrisApiEventSVC implements IrisService, PregenApiSink {
public static void fireWorldPhase(World world, IrisWorldPhase phase) {
if (phase == null) {
return;
}
if (world == null) {
IrisLogging.debug("Iris world API skipped phase " + phase
+ " because the engine has no bound platform world.");
return;
}
try {
deliver(new IrisWorldEngineEvent(world, phase, describe(world, phase)));
} catch (Throwable error) {
IrisLogging.reportError("Iris world API dispatch failed for phase " + phase
+ " on world \"" + world.getName() + "\".", error);
}
}
private static IrisWorldInfo describe(World world, IrisWorldPhase phase) {
try {
return IrisWorldInfoFactory.forWorld(world);
} catch (Throwable error) {
IrisLogging.reportError("Iris world API could not describe world \"" + world.getName()
+ "\" for phase " + phase + "; the event is delivered without world info.", error);
return null;
}
}
private static void deliver(Event event) {
if (Bukkit.isPrimaryThread()) {
Bukkit.getPluginManager().callEvent(event);
return;
}
Iris.callEvent(event);
}
private static IrisPregenPhase toApi(PregenApiPhase phase) {
return switch (phase) {
case STARTED -> IrisPregenPhase.STARTED;
case TICK -> IrisPregenPhase.TICK;
case PAUSED -> IrisPregenPhase.PAUSED;
case RESUMED -> IrisPregenPhase.RESUMED;
case SAVING -> IrisPregenPhase.SAVING;
case COMPLETED -> IrisPregenPhase.COMPLETED;
case CANCELLED -> IrisPregenPhase.CANCELLED;
};
}
private static IrisPregenProgress toApi(PregeneratorJob.PregenProgress progress) {
return new IrisPregenProgress(
progress.worldName(),
progress.worldIdentity(),
progress.percent(),
progress.generated(),
progress.totalChunks(),
progress.chunksRemaining(),
progress.failed(),
progress.chunksPerSecond(),
progress.eta(),
progress.elapsed(),
progress.method(),
progress.paused()
);
}
@Override
public void onEnable() {
IrisServices.register(PregenApiSink.class, this);
}
@Override
public void onDisable() {
IrisServices.remove(PregenApiSink.class);
}
@Override
public void pregen(PregenApiPhase phase, PregeneratorJob.PregenProgress progress) {
if (phase == null || progress == null || progress.worldIdentity() == null) {
return;
}
Iris.callEvent(new IrisPregenerationEvent(toApi(phase), toApi(progress)));
}
}
@@ -61,6 +61,7 @@ public final class IrisEngineSVC implements IrisService {
new AtomicReference<>(IrisTelemetrySnapshot.EMPTY);
private final Map<World, CompletableFuture<Void>> pendingRegistrations = new HashMap<>();
private final Map<World, Registered> worlds = new ConcurrentHashMap<>();
private final IrisWorldPhaseLedger phases = new IrisWorldPhaseLedger();
private volatile ScheduledThreadPoolExecutor service;
private volatile ScheduledFuture<?> metricsTask;
@@ -105,26 +106,28 @@ public final class IrisEngineSVC implements IrisService {
activeMetricsTask.cancel(false);
}
List<Registered> registeredWorlds;
List<ClosingGenerator> reservedCloses = new ArrayList<>();
List<Teardown> teardowns = new ArrayList<>();
List<CompletableFuture<Void>> generatorCloses;
synchronized (registrationLock) {
registeredWorlds = List.copyOf(worlds.values());
for (Map.Entry<World, Registered> entry : worlds.entrySet()) {
Registered registered = entry.getValue();
registered.close();
teardowns.add(new Teardown(entry.getKey(), registered, reserveClose(registered)));
}
worlds.clear();
pendingRegistrations.clear();
for (Registered registered : registeredWorlds) {
registered.close();
reservedCloses.add(reserveClose(registered));
}
generatorCloses = new ArrayList<>(closingGenerators.size());
for (ClosingGenerator closing : closingGenerators) {
generatorCloses.add(closing.completion());
}
}
for (Teardown teardown : teardowns) {
phases.closing(teardown.world());
}
shutdownAndDrain(activeService);
for (int index = 0; index < registeredWorlds.size(); index++) {
startClose(registeredWorlds.get(index), reservedCloses.get(index));
for (Teardown teardown : teardowns) {
startClose(teardown.registered(), teardown.closing());
}
awaitGeneratorShutdown(generatorCloses);
resetMetrics();
@@ -177,6 +180,7 @@ public final class IrisEngineSVC implements IrisService {
Registered replaced = null;
ClosingGenerator replacementClose = null;
CompletableFuture<Void> retryAfter = null;
boolean registered = false;
try {
synchronized (registrationLock) {
if (service != activeService || activeService.isShutdown() || !isCurrentWorld(world)) {
@@ -202,6 +206,7 @@ public final class IrisEngineSVC implements IrisService {
Registration registration = new Registration(world.getName(), access, registrationIdentity);
worlds.put(world, new Registered(registration, activeService));
pendingRegistrations.remove(world);
registered = true;
}
}
}
@@ -211,7 +216,11 @@ public final class IrisEngineSVC implements IrisService {
}
}
if (registered) {
phases.ready(world);
}
if (replacementClose != null) {
phases.closing(world);
retryRegistrationAfterClose(world, retryAfter);
startClose(replaced, replacementClose);
return;
@@ -237,6 +246,7 @@ public final class IrisEngineSVC implements IrisService {
}
}
if (closing != null) {
phases.closing(world);
startClose(registered, closing);
}
}
@@ -697,4 +707,7 @@ public final class IrisEngineSVC implements IrisService {
private record ClosingGenerator(RegistrationIdentity registrationIdentity,
CompletableFuture<Void> completion) {
}
private record Teardown(World world, Registered registered, ClosingGenerator closing) {
}
}
@@ -0,0 +1,316 @@
package art.arcane.iris.core.service;
import art.arcane.iris.api.terrain.IrisColumnField;
import art.arcane.iris.api.terrain.IrisColumnQuery;
import art.arcane.iris.api.terrain.IrisColumnSink;
import art.arcane.iris.api.terrain.IrisSurfaceKind;
import art.arcane.iris.api.terrain.IrisTerrainService;
import art.arcane.iris.api.terrain.IrisWorldInfo;
import art.arcane.iris.core.IrisSettings;
import art.arcane.iris.core.service.terrain.IrisApiFaultGuard;
import art.arcane.iris.core.service.terrain.IrisColumnWalk;
import art.arcane.iris.core.service.terrain.IrisSampleLimits;
import art.arcane.iris.core.service.terrain.IrisSurfaceClassifier;
import art.arcane.iris.core.service.terrain.IrisWorldInfoFactory;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.object.InferredType;
import art.arcane.iris.engine.object.IrisBiome;
import art.arcane.iris.engine.object.IrisRegion;
import art.arcane.iris.engine.platform.PlatformChunkGenerator;
import art.arcane.iris.platform.bukkit.BukkitPlatform;
import art.arcane.iris.spi.IrisLogging;
import art.arcane.iris.spi.IrisServices;
import art.arcane.iris.util.common.plugin.IrisService;
import org.bukkit.Bukkit;
import org.bukkit.World;
import org.bukkit.generator.ChunkGenerator;
import org.bukkit.plugin.ServicePriority;
import java.util.EnumSet;
import java.util.Optional;
import java.util.OptionalInt;
import java.util.concurrent.atomic.AtomicBoolean;
public class IrisTerrainSVC implements IrisService, IrisTerrainService {
private static final long FAULT_REPORT_INTERVAL_MILLIS = 60_000L;
private final AtomicBoolean serviceEnabled = new AtomicBoolean();
private final IrisApiFaultGuard queryFaults = new IrisApiFaultGuard(FAULT_REPORT_INTERVAL_MILLIS);
private final IrisApiFaultGuard sinkFaults = new IrisApiFaultGuard(FAULT_REPORT_INTERVAL_MILLIS);
@Override
public void onEnable() {
serviceEnabled.set(true);
Bukkit.getServicesManager().register(
IrisTerrainService.class,
this,
BukkitPlatform.plugin(),
ServicePriority.Normal
);
IrisServices.register(IrisTerrainService.class, this);
}
@Override
public void onDisable() {
serviceEnabled.set(false);
Bukkit.getServicesManager().unregister(IrisTerrainService.class, this);
IrisServices.remove(IrisTerrainService.class);
}
@Override
public boolean isIrisWorld(World world) {
return generatorOf(world) != null;
}
@Override
public Optional<IrisWorldInfo> worldInfo(World world) {
PlatformChunkGenerator generator = liveGeneratorOf(world);
if (generator == null) {
return Optional.empty();
}
try {
return Optional.ofNullable(IrisWorldInfoFactory.from(generator));
} catch (Throwable error) {
reportQueryFault("worldInfo", world, error);
return Optional.empty();
}
}
@Override
public OptionalInt surfaceHeight(World world, int blockX, int blockZ) {
Engine engine = liveEngineOf(world);
if (engine == null) {
return OptionalInt.empty();
}
try {
return OptionalInt.of(engine.getHeight(blockX, blockZ) + engine.getMinHeight());
} catch (Throwable error) {
reportQueryFault("surfaceHeight", world, error);
return OptionalInt.empty();
}
}
@Override
public IrisSurfaceKind surfaceKind(World world, int blockX, int blockZ) {
Engine engine = liveEngineOf(world);
if (engine == null) {
return IrisSurfaceKind.UNKNOWN;
}
try {
int surface = engine.getHeight(blockX, blockZ);
int fluid = engine.getDimension().getFluidHeight();
InferredType inferredType = null;
if (IrisSurfaceClassifier.requiresSurfaceBiome(surface, fluid)) {
IrisBiome biome = engine.getSurfaceBiome(blockX, blockZ);
inferredType = biome == null ? null : biome.getInferredType();
}
return IrisSurfaceClassifier.classify(surface, fluid, inferredType);
} catch (Throwable error) {
reportQueryFault("surfaceKind", world, error);
return IrisSurfaceKind.UNKNOWN;
}
}
@Override
public Optional<String> surfaceBiomeKey(World world, int blockX, int blockZ) {
Engine engine = liveEngineOf(world);
if (engine == null) {
return Optional.empty();
}
try {
return key(engine.getSurfaceBiome(blockX, blockZ));
} catch (Throwable error) {
reportQueryFault("surfaceBiomeKey", world, error);
return Optional.empty();
}
}
@Override
public Optional<String> surfaceBiomeName(World world, int blockX, int blockZ) {
Engine engine = liveEngineOf(world);
if (engine == null) {
return Optional.empty();
}
try {
return name(engine.getSurfaceBiome(blockX, blockZ));
} catch (Throwable error) {
reportQueryFault("surfaceBiomeName", world, error);
return Optional.empty();
}
}
@Override
public Optional<String> biomeKey(World world, int blockX, int blockY, int blockZ) {
Engine engine = liveEngineOf(world);
if (engine == null) {
return Optional.empty();
}
try {
return key(engine.getBiome(blockX, blockY - engine.getMinHeight(), blockZ));
} catch (Throwable error) {
reportQueryFault("biomeKey", world, error);
return Optional.empty();
}
}
@Override
public Optional<String> regionKey(World world, int blockX, int blockZ) {
Engine engine = liveEngineOf(world);
if (engine == null) {
return Optional.empty();
}
try {
IrisRegion region = engine.getRegion(blockX, blockZ);
String loadKey = region == null ? null : region.getLoadKey();
return loadKey == null || loadKey.isEmpty() ? Optional.empty() : Optional.of(loadKey);
} catch (Throwable error) {
reportQueryFault("regionKey", world, error);
return Optional.empty();
}
}
@Override
public Optional<String> regionName(World world, int blockX, int blockZ) {
Engine engine = liveEngineOf(world);
if (engine == null) {
return Optional.empty();
}
try {
IrisRegion region = engine.getRegion(blockX, blockZ);
String name = region == null ? null : region.getName();
return name == null || name.isEmpty() ? Optional.empty() : Optional.of(name);
} catch (Throwable error) {
reportQueryFault("regionName", world, error);
return Optional.empty();
}
}
@Override
public int maxSampleColumns() {
return IrisSampleLimits.maxColumns(noiseCacheChunks());
}
@Override
public int maxSampleChunks() {
return IrisSampleLimits.maxChunks(noiseCacheChunks());
}
@Override
public boolean sampleColumns(World world, IrisColumnQuery query, IrisColumnSink sink) {
if (query == null || sink == null) {
return false;
}
Engine engine = liveEngineOf(world);
if (engine == null) {
return false;
}
int noiseCacheChunks = noiseCacheChunks();
if (!IrisSampleLimits.withinLimits(
query,
IrisSampleLimits.maxColumns(noiseCacheChunks),
IrisSampleLimits.maxChunks(noiseCacheChunks))) {
return false;
}
EnumSet<IrisColumnField> fields = query.fields();
boolean wantHeight = fields.contains(IrisColumnField.SURFACE_HEIGHT);
boolean wantKind = fields.contains(IrisColumnField.SURFACE_KIND);
boolean wantBiome = fields.contains(IrisColumnField.BIOME_KEY);
try {
int minHeight = engine.getMinHeight();
int fluid = engine.getDimension().getFluidHeight();
long visited = IrisColumnWalk.walk(query, (int blockX, int blockZ) -> {
if (engine.isClosed()) {
return false;
}
int surface = wantHeight || wantKind ? engine.getHeight(blockX, blockZ) : 0;
boolean needsBiome = wantBiome
|| (wantKind && IrisSurfaceClassifier.requiresSurfaceBiome(surface, fluid));
IrisBiome biome = needsBiome ? engine.getSurfaceBiome(blockX, blockZ) : null;
IrisSurfaceKind kind = wantKind
? IrisSurfaceClassifier.classify(surface, fluid, biome == null ? null : biome.getInferredType())
: IrisSurfaceKind.UNKNOWN;
String biomeKey = wantBiome && biome != null ? biome.getLoadKey() : null;
sink.accept(blockX, blockZ, wantHeight ? surface + minHeight : -1, kind, biomeKey);
return true;
});
return visited == query.columnCount();
} catch (Throwable error) {
reportSinkFault(world, error);
return false;
}
}
private static Optional<String> key(IrisBiome biome) {
String loadKey = biome == null ? null : biome.getLoadKey();
return loadKey == null || loadKey.isEmpty() ? Optional.empty() : Optional.of(loadKey);
}
private static Optional<String> name(IrisBiome biome) {
String name = biome == null ? null : biome.getName();
return name == null || name.isEmpty() ? Optional.empty() : Optional.of(name);
}
private static int noiseCacheChunks() {
return IrisSettings.get().getPerformance().getNoiseCacheSize();
}
static boolean answerable(boolean serviceEnabled, boolean worldPresent) {
return serviceEnabled && worldPresent;
}
private PlatformChunkGenerator generatorOf(World world) {
if (!answerable(serviceEnabled.get(), world != null)) {
return null;
}
ChunkGenerator generator = world.getGenerator();
return generator instanceof PlatformChunkGenerator platform ? platform : null;
}
private PlatformChunkGenerator liveGeneratorOf(World world) {
PlatformChunkGenerator generator = generatorOf(world);
return generator == null || generator.isClosing() ? null : generator;
}
private static Engine engineOf(PlatformChunkGenerator generator) {
if (generator == null) {
return null;
}
Engine engine = generator.getEngine();
return engine == null || engine.isClosed() ? null : engine;
}
private Engine liveEngineOf(World world) {
return engineOf(liveGeneratorOf(world));
}
private void reportQueryFault(String operation, World world, Throwable error) {
if (queryFaults.record(System.currentTimeMillis())) {
IrisLogging.reportError("Iris terrain API query \"" + operation + "\" failed for world \""
+ (world == null ? "null" : world.getName()) + "\" (" + queryFaults.faults()
+ " terrain API query faults so far).", error);
}
}
private void reportSinkFault(World world, Throwable error) {
if (sinkFaults.record(System.currentTimeMillis())) {
IrisLogging.reportError("Iris terrain API column sample failed for world \""
+ (world == null ? "null" : world.getName()) + "\" (" + sinkFaults.faults()
+ " terrain API sample faults so far). A third-party sink that throws is treated as a refusal.", error);
}
}
}
@@ -0,0 +1,49 @@
package art.arcane.iris.core.service;
import art.arcane.iris.api.world.IrisWorldPhase;
import org.bukkit.World;
import java.util.Objects;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
final class IrisWorldPhaseLedger {
@FunctionalInterface
interface Dispatch {
void fire(World world, IrisWorldPhase phase);
}
private final Set<UUID> announced = ConcurrentHashMap.newKeySet();
private final Dispatch dispatch;
IrisWorldPhaseLedger() {
this(IrisApiEventSVC::fireWorldPhase);
}
IrisWorldPhaseLedger(Dispatch dispatch) {
this.dispatch = Objects.requireNonNull(dispatch, "dispatch");
}
void ready(World world) {
UUID identity = identityOf(world);
if (identity == null || !announced.add(identity)) {
return;
}
dispatch.fire(world, IrisWorldPhase.ENGINE_READY);
}
void closing(World world) {
UUID identity = identityOf(world);
if (identity == null || !announced.remove(identity)) {
return;
}
dispatch.fire(world, IrisWorldPhase.ENGINE_CLOSING);
}
private static UUID identityOf(World world) {
return world == null ? null : world.getUID();
}
}
@@ -0,0 +1,31 @@
package art.arcane.iris.core.service.terrain;
import java.util.concurrent.atomic.AtomicLong;
public final class IrisApiFaultGuard {
private static final long NEVER = Long.MIN_VALUE;
private final long reportIntervalMillis;
private final AtomicLong faults = new AtomicLong();
private final AtomicLong lastReportedAt = new AtomicLong(NEVER);
public IrisApiFaultGuard(long reportIntervalMillis) {
if (reportIntervalMillis < 0L) {
throw new IllegalArgumentException("reportIntervalMillis must not be negative");
}
this.reportIntervalMillis = reportIntervalMillis;
}
public long faults() {
return faults.get();
}
public boolean record(long nowMillis) {
faults.incrementAndGet();
long last = lastReportedAt.get();
if (last != NEVER && nowMillis - last < reportIntervalMillis) {
return false;
}
return lastReportedAt.compareAndSet(last, nowMillis);
}
}
@@ -0,0 +1,49 @@
package art.arcane.iris.core.service.terrain;
import art.arcane.iris.api.terrain.IrisColumnQuery;
public final class IrisColumnWalk {
private IrisColumnWalk() {
}
public static long walk(IrisColumnQuery query, ColumnVisitor visitor) {
int stride = query.strideBlocks();
int minChunkX = query.minBlockX() >> 4;
int maxChunkX = query.maxBlockX() >> 4;
int minChunkZ = query.minBlockZ() >> 4;
int maxChunkZ = query.maxBlockZ() >> 4;
long visited = 0L;
for (int chunkZ = minChunkZ; chunkZ <= maxChunkZ; chunkZ++) {
int chunkMinBlockZ = Math.max(query.minBlockZ(), chunkZ << 4);
int chunkMaxBlockZ = Math.min(query.maxBlockZ(), (chunkZ << 4) + 15);
for (int chunkX = minChunkX; chunkX <= maxChunkX; chunkX++) {
int chunkMinBlockX = Math.max(query.minBlockX(), chunkX << 4);
int chunkMaxBlockX = Math.min(query.maxBlockX(), (chunkX << 4) + 15);
for (int blockZ = align(query.minBlockZ(), chunkMinBlockZ, stride); blockZ <= chunkMaxBlockZ; blockZ += stride) {
for (int blockX = align(query.minBlockX(), chunkMinBlockX, stride); blockX <= chunkMaxBlockX; blockX += stride) {
if (!visitor.visit(blockX, blockZ)) {
return visited;
}
visited++;
}
}
}
}
return visited;
}
private static int align(int origin, int lowerBound, int stride) {
long offset = (long) lowerBound - (long) origin;
long steps = (offset + stride - 1L) / stride;
return (int) (origin + steps * stride);
}
@FunctionalInterface
public interface ColumnVisitor {
boolean visit(int blockX, int blockZ);
}
}
@@ -0,0 +1,24 @@
package art.arcane.iris.core.service.terrain;
import art.arcane.iris.api.terrain.IrisColumnQuery;
public final class IrisSampleLimits {
public static final int MINIMUM_CHUNKS = 64;
public static final int CACHE_SHARE_DIVISOR = 4;
private IrisSampleLimits() {
}
public static int maxChunks(int noiseCacheChunks) {
return Math.max(MINIMUM_CHUNKS, noiseCacheChunks / CACHE_SHARE_DIVISOR);
}
public static int maxColumns(int noiseCacheChunks) {
long columns = (long) maxChunks(noiseCacheChunks) * 256L;
return (int) Math.min(columns, Integer.MAX_VALUE);
}
public static boolean withinLimits(IrisColumnQuery query, int maxColumns, int maxChunks) {
return query.columnCount() <= maxColumns && query.chunkCount() <= maxChunks;
}
}
@@ -0,0 +1,25 @@
package art.arcane.iris.core.service.terrain;
import art.arcane.iris.api.terrain.IrisSurfaceKind;
import art.arcane.iris.engine.object.InferredType;
public final class IrisSurfaceClassifier {
private IrisSurfaceClassifier() {
}
public static boolean requiresSurfaceBiome(int engineSurfaceHeight, int engineFluidHeight) {
return engineSurfaceHeight > 0 && engineSurfaceHeight > engineFluidHeight;
}
public static IrisSurfaceKind classify(int engineSurfaceHeight, int engineFluidHeight, InferredType inferredType) {
if (engineSurfaceHeight <= 0) {
return IrisSurfaceKind.VOID;
}
if (engineSurfaceHeight <= engineFluidHeight) {
return IrisSurfaceKind.OCEAN;
}
return inferredType == InferredType.SHORE ? IrisSurfaceKind.SHORE : IrisSurfaceKind.LAND;
}
}
@@ -0,0 +1,71 @@
package art.arcane.iris.core.service.terrain;
import art.arcane.iris.api.terrain.IrisWorldInfo;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.object.IrisDimension;
import art.arcane.iris.engine.object.IrisWorld;
import art.arcane.iris.engine.platform.PlatformChunkGenerator;
import org.bukkit.World;
import org.bukkit.generator.ChunkGenerator;
public final class IrisWorldInfoFactory {
private IrisWorldInfoFactory() {
}
public static IrisWorldInfo forWorld(World world) {
if (world == null) {
return null;
}
ChunkGenerator generator = world.getGenerator();
return generator instanceof PlatformChunkGenerator platform ? from(platform) : null;
}
public static IrisWorldInfo from(PlatformChunkGenerator generator) {
if (generator == null) {
return null;
}
Engine engine = generator.getEngine();
if (engine == null || engine.isClosed()) {
return null;
}
IrisWorld irisWorld = engine.getWorld();
IrisDimension dimension = engine.getDimension();
if (irisWorld == null || dimension == null) {
return null;
}
return build(
dimension.getLoadKey(),
irisWorld.identity(),
irisWorld.getRawWorldSeed(),
engine.getMinHeight(),
engine.getMaxHeight(),
dimension.getFluidHeight(),
generator.isStudio());
}
static IrisWorldInfo build(
String dimensionKey,
String worldIdentity,
long seed,
int minHeight,
int maxHeight,
int fluidHeightAboveMinimum,
boolean studio) {
if (dimensionKey == null || worldIdentity == null || maxHeight <= minHeight) {
return null;
}
return new IrisWorldInfo(
dimensionKey,
worldIdentity,
seed,
minHeight,
maxHeight,
fluidHeightAboveMinimum + minHeight,
studio);
}
}