mirror of
https://github.com/VolmitSoftware/Iris.git
synced 2026-08-28 13:00: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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user