This commit is contained in:
Brian Neumann-Fopiano
2026-07-21 12:00:17 -04:00
parent d76dadec30
commit f6f06c1ab9
141 changed files with 10717 additions and 1725 deletions
@@ -55,6 +55,7 @@ import art.arcane.iris.engine.EnginePanic;
import art.arcane.iris.engine.framework.BlockEditAccess;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.framework.PreservationRegistry;
import art.arcane.iris.engine.framework.TreeBlockMaterial;
import art.arcane.iris.engine.object.IrisCompat;
import art.arcane.iris.engine.object.IrisDimension;
import art.arcane.iris.engine.object.IrisWorld;
@@ -634,11 +635,8 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
IrisServices.register(EngineComponentCleanup.class, (EngineComponentCleanup) BukkitPlatform::unregisterListener);
IrisServices.register(EngineEffectsProvider.class, (EngineEffectsProvider) IrisEngineEffects::new);
IrisServices.register(EnginePlatformHooks.class, new BukkitEnginePlatformHooks());
IrisServices.register(EngineWorldManagerProvider.class, (EngineWorldManagerProvider) (Engine engine) -> {
IrisWorldManager manager = new IrisWorldManager(engine);
manager.startManager();
return manager;
});
IrisServices.register(EngineWorldManagerProvider.class,
(EngineWorldManagerProvider) IrisWorldManager::new);
IrisServices.register(art.arcane.iris.core.runtime.WorldDeletionQueue.class, (art.arcane.iris.core.runtime.WorldDeletionQueue) Iris::queueWorldDeletionOnStartup);
settingsFile = getDataFile("settings.json");
configHotloadEngine = new ConfigHotloadEngine(
@@ -673,6 +671,7 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
}
IrisToolbelt.retainMantleDataForSlice(String.class.getCanonicalName());
IrisToolbelt.retainMantleDataForSlice(BlockData.class.getCanonicalName());
IrisToolbelt.retainMantleDataForSlice(TreeBlockMaterial.class.getCanonicalName());
});
}
@@ -0,0 +1,12 @@
package art.arcane.iris.api.tree;
import org.bukkit.block.Block;
import org.bukkit.event.block.BlockBreakEvent;
public interface IrisTreeFellerService {
boolean tryFell(BlockBreakEvent event, TreeFellerOptions options);
boolean isManagedBreak(BlockBreakEvent event);
boolean isTreeBlock(Block block);
}
@@ -0,0 +1,6 @@
package art.arcane.iris.api.tree;
public enum TreeFellerAccess {
STANDALONE,
INTEGRATION_OVERRIDE
}
@@ -0,0 +1,32 @@
package art.arcane.iris.api.tree;
import java.util.Objects;
public record TreeFellerOptions(
TreeFellerAccess access,
int durabilityPreservationChance,
TreeFellerRunHooks runHooks
) {
public TreeFellerOptions {
Objects.requireNonNull(access, "access");
Objects.requireNonNull(runHooks, "runHooks");
if (durabilityPreservationChance < 0 || durabilityPreservationChance > 100) {
throw new IllegalArgumentException("durabilityPreservationChance must be between 0 and 100");
}
}
public static TreeFellerOptions standalone() {
return new TreeFellerOptions(TreeFellerAccess.STANDALONE, 0, TreeFellerRunHooks.NONE);
}
public static TreeFellerOptions integrationOverride(
int durabilityPreservationChance,
TreeFellerRunHooks runHooks
) {
return new TreeFellerOptions(
TreeFellerAccess.INTEGRATION_OVERRIDE,
durabilityPreservationChance,
runHooks
);
}
}
@@ -0,0 +1,30 @@
package art.arcane.iris.api.tree;
public interface TreeFellerRunHooks {
TreeFellerRunHooks NONE = new TreeFellerRunHooks() {
@Override
public void onActivationAccepted() {
}
@Override
public boolean reserveLogCost() {
return true;
}
@Override
public void commitLogCost() {
}
@Override
public void refundLogCost() {
}
};
void onActivationAccepted();
boolean reserveLogCost();
void commitLogCost();
void refundLogCost();
}
@@ -80,7 +80,10 @@ public final class BukkitEnginePlatformHooks implements EnginePlatformHooks {
@Override
public void shutdownPregenerator(Engine engine) {
PregeneratorJob.shutdownInstance();
IrisWorld world = engine.getWorld();
if (world != null) {
PregeneratorJob.shutdownInstanceForWorld(world.identity());
}
}
@Override
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,91 @@
package art.arcane.iris.core.service;
import art.arcane.iris.core.loader.ResourceLoader;
import art.arcane.iris.engine.framework.MeteredCache;
import art.arcane.iris.spi.IrisServices;
import art.arcane.iris.util.common.format.C;
import art.arcane.iris.util.common.plugin.VolmitSender;
import art.arcane.iris.util.project.stream.utility.CachedDoubleStream2D;
import art.arcane.iris.util.project.stream.utility.CachedStream2D;
import art.arcane.iris.util.project.stream.utility.CachedStream3D;
import art.arcane.volmlib.util.format.Form;
import java.util.List;
final class IrisEngineStatus {
private IrisEngineStatus() {
}
static void send(VolmitSender sender, Snapshot snapshot) {
CacheSummary caches = summarizeCaches();
MaintenanceMetrics metrics = snapshot.metrics();
sender.sendMessage(C.DARK_PURPLE + "-------------------------");
sender.sendMessage(C.DARK_PURPLE + "Status:");
sender.sendMessage(C.DARK_PURPLE + "- Service: " + C.LIGHT_PURPLE + (snapshot.serviceRunning() ? "Running" : "Stopped"));
sender.sendMessage(C.DARK_PURPLE + "- Metrics: " + C.LIGHT_PURPLE + (snapshot.metricsRunning() ? "Running" : "Stopped"));
sender.sendMessage(C.DARK_PURPLE + "- Maintenance Period: " + C.LIGHT_PURPLE + Form.duration(snapshot.maintenancePeriodMillis()));
sender.sendMessage(C.DARK_PURPLE + "- Worker Parallelism: " + C.LIGHT_PURPLE + snapshot.workerParallelism());
sender.sendMessage(C.DARK_PURPLE + "- Active World Tasks: " + C.LIGHT_PURPLE + metrics.activeTasks());
sender.sendMessage(C.DARK_PURPLE + "Tectonic Plates:");
sender.sendMessage(C.DARK_PURPLE + "- Configured Retention: " + C.LIGHT_PURPLE + Form.duration(snapshot.retentionMillis()));
sender.sendMessage(C.DARK_PURPLE + "- Heap Usage: " + C.LIGHT_PURPLE + Form.pc(snapshot.heapUsage()));
sender.sendMessage(C.DARK_PURPLE + "- Resident: " + C.LIGHT_PURPLE + metrics.residentTectonicPlates());
sender.sendMessage(C.DARK_PURPLE + "- Queued: " + C.LIGHT_PURPLE + metrics.queuedTectonicPlates());
sender.sendMessage(C.DARK_PURPLE + "- Average Idle Duration: " + C.LIGHT_PURPLE + Form.duration(metrics.averageIdleDuration(), 2));
sender.sendMessage(C.DARK_PURPLE + "- Max Idle Duration: " + C.LIGHT_PURPLE + Form.duration(metrics.maxIdleDuration(), 2));
sender.sendMessage(C.DARK_PURPLE + "- Min Idle Duration: " + C.LIGHT_PURPLE + Form.duration(metrics.minIdleDuration(), 2));
sender.sendMessage(C.DARK_PURPLE + "Caches:");
sender.sendMessage(C.DARK_PURPLE + "- Resource: " + C.LIGHT_PURPLE + caches.sizes()[0] + " (" + caches.counts()[0] + ")");
sender.sendMessage(C.DARK_PURPLE + "- 2D Stream: " + C.LIGHT_PURPLE + caches.sizes()[1] + " (" + caches.counts()[1] + ")");
sender.sendMessage(C.DARK_PURPLE + "- 3D Stream: " + C.LIGHT_PURPLE + caches.sizes()[2] + " (" + caches.counts()[2] + ")");
sender.sendMessage(C.DARK_PURPLE + "- Other: " + C.LIGHT_PURPLE + caches.sizes()[3] + " (" + caches.counts()[3] + ")");
sender.sendMessage(C.DARK_PURPLE + "Other:");
sender.sendMessage(C.DARK_PURPLE + "- Iris Worlds: " + C.LIGHT_PURPLE + metrics.worlds());
sender.sendMessage(C.DARK_PURPLE + "- Loaded Chunks: " + C.LIGHT_PURPLE + metrics.loadedChunks());
sender.sendMessage(C.DARK_PURPLE + "-------------------------");
}
private static CacheSummary summarizeCaches() {
long[] sizes = new long[4];
long[] counts = new long[4];
PreservationSVC preservation = IrisServices.get(PreservationSVC.class);
List<MeteredCache> caches = preservation == null ? List.of() : preservation.getCaches();
for (MeteredCache cache : caches) {
int type = switch (cache) {
case ResourceLoader<?> ignored -> 0;
case CachedStream2D<?> ignored -> 1;
case CachedDoubleStream2D ignored -> 1;
case CachedStream3D<?> ignored -> 2;
default -> 3;
};
sizes[type] += cache.getSize();
counts[type]++;
}
return new CacheSummary(sizes, counts);
}
record Snapshot(boolean serviceRunning,
boolean metricsRunning,
long maintenancePeriodMillis,
int workerParallelism,
long retentionMillis,
double heapUsage,
MaintenanceMetrics metrics) {
}
record MaintenanceMetrics(long residentTectonicPlates,
long queuedTectonicPlates,
long loadedChunks,
int worlds,
int activeTasks,
double averageIdleDuration,
double maxIdleDuration,
double minIdleDuration) {
static final MaintenanceMetrics EMPTY = new MaintenanceMetrics(0L, 0L, 0L, 0, 0, 0D, 0D, 0D);
}
private record CacheSummary(long[] sizes, long[] counts) {
}
}
@@ -1,12 +1,14 @@
package art.arcane.iris.core.service;
import art.arcane.iris.Iris;
import art.arcane.iris.core.gui.PregeneratorJob;
import art.arcane.iris.engine.framework.EngineTelemetrySnapshot;
import art.arcane.iris.spi.IrisServices;
import art.arcane.iris.util.common.plugin.IrisService;
import art.arcane.volmlib.integration.IntegrationHandshakeRequest;
import art.arcane.volmlib.integration.IntegrationHandshakeResponse;
import art.arcane.volmlib.integration.IntegrationHeartbeat;
import art.arcane.volmlib.integration.IntegrationMetricDescriptor;
import art.arcane.volmlib.integration.IntegrationMetricGroup;
import art.arcane.volmlib.integration.IntegrationMetricSample;
import art.arcane.volmlib.integration.IntegrationMetricSchema;
import art.arcane.volmlib.integration.IntegrationProtocolNegotiator;
@@ -15,25 +17,59 @@ import art.arcane.volmlib.integration.IntegrationServiceContract;
import org.bukkit.Bukkit;
import org.bukkit.plugin.ServicePriority;
import java.util.HashMap;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.function.Supplier;
public class IrisIntegrationService implements IrisService, IntegrationServiceContract {
private static final IntegrationProtocolVersion CURRENT_PROTOCOL = new IntegrationProtocolVersion(1, 2);
private static final Set<IntegrationProtocolVersion> SUPPORTED_PROTOCOLS = Set.of(
new IntegrationProtocolVersion(1, 0),
new IntegrationProtocolVersion(1, 1)
new IntegrationProtocolVersion(1, 1),
CURRENT_PROTOCOL
);
private static final Set<String> CAPABILITIES = Set.of(
"handshake",
"heartbeat",
"metrics",
"iris-engine-metrics"
"metric-groups",
"iris-engine-metrics",
"iris-world-metrics"
);
private static final Map<String, String> TIMING_KEYS = Map.ofEntries(
Map.entry("total", IntegrationMetricSchema.IRIS_GENERATION_TOTAL_MS),
Map.entry("updates", IntegrationMetricSchema.IRIS_GENERATION_UPDATES_MS),
Map.entry("terrain", IntegrationMetricSchema.IRIS_GENERATION_TERRAIN_MS),
Map.entry("biome", IntegrationMetricSchema.IRIS_GENERATION_BIOME_MS),
Map.entry("post", IntegrationMetricSchema.IRIS_GENERATION_POST_MS),
Map.entry("perfection", IntegrationMetricSchema.IRIS_GENERATION_PERFECTION_MS),
Map.entry("decoration", IntegrationMetricSchema.IRIS_GENERATION_DECORATION_MS),
Map.entry("cave", IntegrationMetricSchema.IRIS_GENERATION_CAVE_MS),
Map.entry("deposit", IntegrationMetricSchema.IRIS_GENERATION_DEPOSIT_MS),
Map.entry("carve.resolve", IntegrationMetricSchema.IRIS_GENERATION_CARVE_RESOLVE_MS),
Map.entry("carve.apply", IntegrationMetricSchema.IRIS_GENERATION_CARVE_APPLY_MS),
Map.entry("context.prefill", IntegrationMetricSchema.IRIS_GENERATION_CONTEXT_PREFILL_MS),
Map.entry("pregen.wait.permit", IntegrationMetricSchema.IRIS_PREGEN_WAIT_PERMIT_MS),
Map.entry("pregen.wait.adaptive", IntegrationMetricSchema.IRIS_PREGEN_WAIT_ADAPTIVE_MS)
);
private volatile IntegrationProtocolVersion negotiatedProtocol = new IntegrationProtocolVersion(1, 1);
private final Supplier<IrisTelemetrySnapshot> telemetrySupplier;
public IrisIntegrationService() {
this(IrisIntegrationService::currentTelemetry);
}
IrisIntegrationService(Supplier<IrisTelemetrySnapshot> telemetrySupplier) {
this.telemetrySupplier = telemetrySupplier == null
? () -> IrisTelemetrySnapshot.EMPTY
: telemetrySupplier;
}
@Override
public void onEnable() {
@@ -68,9 +104,11 @@ public class IrisIntegrationService implements IrisService, IntegrationServiceCo
@Override
public Set<IntegrationMetricDescriptor> metricDescriptors() {
return IntegrationMetricSchema.descriptors().stream()
.filter(descriptor -> descriptor.key().startsWith("iris."))
.collect(java.util.stream.Collectors.toSet());
Set<IntegrationMetricDescriptor> descriptors = new LinkedHashSet<>();
for (String key : IntegrationMetricSchema.irisKeys()) {
descriptors.add(IntegrationMetricSchema.descriptor(key));
}
return Set.copyOf(descriptors);
}
@Override
@@ -106,12 +144,11 @@ public class IrisIntegrationService implements IrisService, IntegrationServiceCo
);
}
negotiatedProtocol = negotiated.get();
return new IntegrationHandshakeResponse(
pluginId(),
pluginVersion(),
true,
negotiatedProtocol,
negotiated.get(),
SUPPORTED_PROTOCOLS,
CAPABILITIES,
"ok",
@@ -121,90 +158,228 @@ public class IrisIntegrationService implements IrisService, IntegrationServiceCo
@Override
public IntegrationHeartbeat heartbeat() {
long now = System.currentTimeMillis();
return new IntegrationHeartbeat(negotiatedProtocol, true, now, "ok");
return new IntegrationHeartbeat(CURRENT_PROTOCOL, true, System.currentTimeMillis(), "ok");
}
@Override
public Map<String, IntegrationMetricSample> sampleMetrics(Set<String> metricKeys) {
Set<String> requested = metricKeys == null || metricKeys.isEmpty()
? IntegrationMetricSchema.irisKeys()
: metricKeys;
long now = System.currentTimeMillis();
Map<String, IntegrationMetricSample> out = new HashMap<>();
: Set.copyOf(metricKeys);
IrisTelemetrySnapshot snapshot = safeSnapshot();
long sampledAtMs = snapshot.sampledAtMs() > 0L
? snapshot.sampledAtMs()
: System.currentTimeMillis();
if (snapshot.sampledAtMs() <= 0L) {
return unavailableMetrics(requested, "telemetry-not-ready", sampledAtMs);
}
Map<String, IntegrationMetricSample> available = globalSamples(snapshot);
Map<String, IntegrationMetricSample> selected = new LinkedHashMap<>(requested.size());
for (String key : requested) {
switch (key) {
case IntegrationMetricSchema.IRIS_CHUNK_STREAM_MS -> out.put(key, sampleChunkStreamMetric(now));
case IntegrationMetricSchema.IRIS_PREGEN_QUEUE -> out.put(key, samplePregenQueueMetric(now));
case IntegrationMetricSchema.IRIS_BIOME_CACHE_HIT_RATE -> out.put(key, sampleBiomeCacheHitRateMetric(now));
default -> out.put(key, IntegrationMetricSample.unavailable(
IntegrationMetricSchema.descriptor(key),
"unsupported-key",
now
));
IntegrationMetricSample sample = available.get(key);
selected.put(key, sample == null
? unavailable(key, "unsupported-key", sampledAtMs)
: sample);
}
return Map.copyOf(selected);
}
@Override
public List<IntegrationMetricGroup> metricGroups() {
IrisTelemetrySnapshot snapshot = safeSnapshot();
if (snapshot.sampledAtMs() <= 0L || snapshot.worlds().isEmpty()) {
return List.of();
}
List<EngineTelemetrySnapshot> worlds = new ArrayList<>(snapshot.worlds());
worlds.sort(Comparator.comparing(EngineTelemetrySnapshot::worldIdentity));
List<IntegrationMetricGroup> groups = new ArrayList<>(worlds.size());
for (EngineTelemetrySnapshot world : worlds) {
groups.add(new IntegrationMetricGroup(
"world",
world.worldIdentity(),
world.worldName(),
Map.of(
"plugin", "iris",
"dimension", world.dimensionKey()
),
worldSamples(world, snapshot.pregenerator())
));
}
return List.copyOf(groups);
}
private Map<String, IntegrationMetricSample> globalSamples(IrisTelemetrySnapshot snapshot) {
long sampledAtMs = snapshot.sampledAtMs();
EngineTelemetrySnapshot.Aggregate aggregate = snapshot.aggregate();
Map<String, IntegrationMetricSample> samples = new LinkedHashMap<>();
put(samples, IntegrationMetricSchema.IRIS_WORLD_COUNT, aggregate.worldCount(), sampledAtMs);
put(samples, IntegrationMetricSchema.IRIS_ENGINE_ACTIVE, aggregate.active(), sampledAtMs);
put(samples, IntegrationMetricSchema.IRIS_ENGINE_CLOSING, aggregate.closing() + snapshot.closingEngines(), sampledAtMs);
put(samples, IntegrationMetricSchema.IRIS_ENGINE_FAILED, aggregate.failed(), sampledAtMs);
put(samples, IntegrationMetricSchema.IRIS_ENGINE_STUDIO, aggregate.studio(), sampledAtMs);
put(samples, IntegrationMetricSchema.IRIS_ENGINE_PENDING_REGISTRATIONS, snapshot.pendingRegistrations(), sampledAtMs);
put(samples, IntegrationMetricSchema.IRIS_LOADED_CHUNKS, aggregate.loadedChunks(), sampledAtMs);
put(samples, IntegrationMetricSchema.IRIS_LOADED_ENTITIES, aggregate.loadedEntities(), sampledAtMs);
put(samples, IntegrationMetricSchema.IRIS_ENTITY_SATURATION, aggregate.entitySaturationMax(), sampledAtMs);
put(samples, IntegrationMetricSchema.IRIS_CHUNKS_GENERATED_SESSION, aggregate.generatedSession(), sampledAtMs);
put(samples, IntegrationMetricSchema.IRIS_CHUNKS_GENERATED_TOTAL, aggregate.generatedTotal(), sampledAtMs);
put(samples, IntegrationMetricSchema.IRIS_CHUNKS_PER_SECOND, aggregate.chunksPerSecond(), sampledAtMs);
put(samples, IntegrationMetricSchema.IRIS_BLOCK_UPDATES_PER_SECOND, aggregate.blockUpdatesPerSecond(), sampledAtMs);
put(samples, IntegrationMetricSchema.IRIS_ENGINE_PARALLELISM, aggregate.parallelism(), sampledAtMs);
put(samples, IntegrationMetricSchema.IRIS_GENERATION_ACTIVE_LEASES, aggregate.activeGenerationLeases(), sampledAtMs);
put(samples, IntegrationMetricSchema.IRIS_HOTLOADS_TOTAL, aggregate.hotloadsTotal(), sampledAtMs);
put(samples, IntegrationMetricSchema.IRIS_MAINTENANCE_ACTIVE_TASKS, snapshot.maintenanceActiveTasks(), sampledAtMs);
put(samples, IntegrationMetricSchema.IRIS_MAINTENANCE_WORKERS, snapshot.maintenanceWorkers(), sampledAtMs);
put(samples, IntegrationMetricSchema.IRIS_MANTLE_RESIDENT_PLATES, aggregate.mantleResidentPlates(), sampledAtMs);
put(samples, IntegrationMetricSchema.IRIS_MANTLE_QUEUED_PLATES, aggregate.mantleQueuedPlates(), sampledAtMs);
put(samples, IntegrationMetricSchema.IRIS_MANTLE_IDLE_AVERAGE_MS, aggregate.mantleIdleAverageMs(), sampledAtMs);
put(samples, IntegrationMetricSchema.IRIS_MANTLE_IDLE_MAX_MS, aggregate.mantleIdleMaxMs(), sampledAtMs);
put(samples, IntegrationMetricSchema.IRIS_MANTLE_IDLE_MIN_MS, aggregate.mantleIdleMinMs(), sampledAtMs);
put(samples, IntegrationMetricSchema.IRIS_MANTLE_HEAP_USAGE, snapshot.heapUsage(), sampledAtMs);
put(samples, IntegrationMetricSchema.IRIS_MANTLE_RECLAIM_URGENCY, snapshot.reclaimUrgency(), sampledAtMs);
putCacheSamples(samples, snapshot.caches(), sampledAtMs);
putPregenSamples(samples, snapshot.pregenerator(), sampledAtMs);
putTimingSamples(samples, aggregate.generationTimingMaximaMs(), sampledAtMs);
return Map.copyOf(samples);
}
private Map<String, IntegrationMetricSample> worldSamples(
EngineTelemetrySnapshot world,
IrisTelemetrySnapshot.PregenSnapshot pregenerator
) {
long sampledAtMs = world.sampledAtMs();
Map<String, IntegrationMetricSample> samples = new LinkedHashMap<>();
put(samples, IntegrationMetricSchema.IRIS_ENGINE_ACTIVE, world.active() ? 1 : 0, sampledAtMs);
put(samples, IntegrationMetricSchema.IRIS_ENGINE_CLOSING, world.closing() ? 1 : 0, sampledAtMs);
put(samples, IntegrationMetricSchema.IRIS_ENGINE_FAILED, world.failed() ? 1 : 0, sampledAtMs);
put(samples, IntegrationMetricSchema.IRIS_ENGINE_STUDIO, world.studio() ? 1 : 0, sampledAtMs);
put(samples, IntegrationMetricSchema.IRIS_LOADED_CHUNKS, world.loadedChunks(), sampledAtMs);
put(samples, IntegrationMetricSchema.IRIS_LOADED_ENTITIES, world.loadedEntities(), sampledAtMs);
put(samples, IntegrationMetricSchema.IRIS_ENTITY_SATURATION, world.entitySaturation(), sampledAtMs);
put(samples, IntegrationMetricSchema.IRIS_CHUNKS_GENERATED_SESSION, world.generatedSession(), sampledAtMs);
put(samples, IntegrationMetricSchema.IRIS_CHUNKS_GENERATED_TOTAL, world.generatedTotal(), sampledAtMs);
put(samples, IntegrationMetricSchema.IRIS_CHUNKS_PER_SECOND, world.chunksPerSecond(), sampledAtMs);
put(samples, IntegrationMetricSchema.IRIS_BLOCK_UPDATES_PER_SECOND, world.blockUpdatesPerSecond(), sampledAtMs);
put(samples, IntegrationMetricSchema.IRIS_ENGINE_PARALLELISM, world.parallelism(), sampledAtMs);
put(samples, IntegrationMetricSchema.IRIS_GENERATION_ACTIVE_LEASES, world.activeGenerationLeases(), sampledAtMs);
put(samples, IntegrationMetricSchema.IRIS_HOTLOADS_TOTAL, world.hotloadsTotal(), sampledAtMs);
put(samples, IntegrationMetricSchema.IRIS_MANTLE_RESIDENT_PLATES, world.mantleResidentPlates(), sampledAtMs);
put(samples, IntegrationMetricSchema.IRIS_MANTLE_QUEUED_PLATES, world.mantleQueuedPlates(), sampledAtMs);
put(samples, IntegrationMetricSchema.IRIS_MANTLE_IDLE_AVERAGE_MS, world.mantleIdleMs(), sampledAtMs);
boolean targetsWorld = pregenerator.active()
&& world.worldIdentity().equals(pregenerator.worldIdentity());
putPregenSamples(samples, targetsWorld ? pregenerator : IrisTelemetrySnapshot.PregenSnapshot.INACTIVE, sampledAtMs);
putTimingSamples(samples, world.generationTimingsMs(), sampledAtMs);
samples.keySet().retainAll(IntegrationMetricSchema.irisWorldKeys());
return Map.copyOf(samples);
}
private void putCacheSamples(
Map<String, IntegrationMetricSample> samples,
IrisTelemetrySnapshot.CacheSnapshot caches,
long sampledAtMs
) {
putCacheBucket(samples, caches.total(), IntegrationMetricSchema.IRIS_CACHE_COUNT, IntegrationMetricSchema.IRIS_CACHE_ENTRIES, IntegrationMetricSchema.IRIS_CACHE_CAPACITY, IntegrationMetricSchema.IRIS_CACHE_USAGE, sampledAtMs);
putCacheBucket(samples, caches.resource(), IntegrationMetricSchema.IRIS_CACHE_RESOURCE_COUNT, IntegrationMetricSchema.IRIS_CACHE_RESOURCE_ENTRIES, IntegrationMetricSchema.IRIS_CACHE_RESOURCE_CAPACITY, IntegrationMetricSchema.IRIS_CACHE_RESOURCE_USAGE, sampledAtMs);
putCacheBucket(samples, caches.stream2d(), IntegrationMetricSchema.IRIS_CACHE_STREAM_2D_COUNT, IntegrationMetricSchema.IRIS_CACHE_STREAM_2D_ENTRIES, IntegrationMetricSchema.IRIS_CACHE_STREAM_2D_CAPACITY, IntegrationMetricSchema.IRIS_CACHE_STREAM_2D_USAGE, sampledAtMs);
putCacheBucket(samples, caches.stream3d(), IntegrationMetricSchema.IRIS_CACHE_STREAM_3D_COUNT, IntegrationMetricSchema.IRIS_CACHE_STREAM_3D_ENTRIES, IntegrationMetricSchema.IRIS_CACHE_STREAM_3D_CAPACITY, IntegrationMetricSchema.IRIS_CACHE_STREAM_3D_USAGE, sampledAtMs);
putCacheBucket(samples, caches.other(), IntegrationMetricSchema.IRIS_CACHE_OTHER_COUNT, IntegrationMetricSchema.IRIS_CACHE_OTHER_ENTRIES, IntegrationMetricSchema.IRIS_CACHE_OTHER_CAPACITY, IntegrationMetricSchema.IRIS_CACHE_OTHER_USAGE, sampledAtMs);
}
private void putCacheBucket(
Map<String, IntegrationMetricSample> samples,
IrisTelemetrySnapshot.CacheBucket bucket,
String countKey,
String entriesKey,
String capacityKey,
String usageKey,
long sampledAtMs
) {
put(samples, countKey, bucket.count(), sampledAtMs);
put(samples, entriesKey, bucket.entries(), sampledAtMs);
put(samples, capacityKey, bucket.capacity(), sampledAtMs);
put(samples, usageKey, bucket.usage(), sampledAtMs);
}
private void putPregenSamples(
Map<String, IntegrationMetricSample> samples,
IrisTelemetrySnapshot.PregenSnapshot pregenerator,
long sampledAtMs
) {
put(samples, IntegrationMetricSchema.IRIS_PREGEN_ACTIVE, pregenerator.active() ? 1 : 0, sampledAtMs);
if (!pregenerator.active()) {
for (String key : Set.of(
IntegrationMetricSchema.IRIS_PREGEN_PAUSED,
IntegrationMetricSchema.IRIS_PREGEN_PROGRESS,
IntegrationMetricSchema.IRIS_PREGEN_GENERATED,
IntegrationMetricSchema.IRIS_PREGEN_TOTAL,
IntegrationMetricSchema.IRIS_PREGEN_QUEUE,
IntegrationMetricSchema.IRIS_PREGEN_THROUGHPUT,
IntegrationMetricSchema.IRIS_PREGEN_ETA_MS,
IntegrationMetricSchema.IRIS_PREGEN_ELAPSED_MS,
IntegrationMetricSchema.IRIS_PREGEN_FAILED)) {
samples.put(key, unavailable(key, "pregen-inactive", sampledAtMs));
}
return;
}
return out;
put(samples, IntegrationMetricSchema.IRIS_PREGEN_PAUSED, pregenerator.paused() ? 1 : 0, sampledAtMs);
put(samples, IntegrationMetricSchema.IRIS_PREGEN_PROGRESS, pregenerator.progressPercent(), sampledAtMs);
put(samples, IntegrationMetricSchema.IRIS_PREGEN_GENERATED, pregenerator.generated(), sampledAtMs);
put(samples, IntegrationMetricSchema.IRIS_PREGEN_TOTAL, pregenerator.total(), sampledAtMs);
put(samples, IntegrationMetricSchema.IRIS_PREGEN_QUEUE, pregenerator.remaining(), sampledAtMs);
put(samples, IntegrationMetricSchema.IRIS_PREGEN_THROUGHPUT, pregenerator.chunksPerSecond(), sampledAtMs);
put(samples, IntegrationMetricSchema.IRIS_PREGEN_ETA_MS, pregenerator.etaMs(), sampledAtMs);
put(samples, IntegrationMetricSchema.IRIS_PREGEN_ELAPSED_MS, pregenerator.elapsedMs(), sampledAtMs);
put(samples, IntegrationMetricSchema.IRIS_PREGEN_FAILED, pregenerator.failed(), sampledAtMs);
}
private IntegrationMetricSample sampleChunkStreamMetric(long now) {
IntegrationMetricDescriptor descriptor = IntegrationMetricSchema.descriptor(IntegrationMetricSchema.IRIS_CHUNK_STREAM_MS);
double chunksPerSecond = PregeneratorJob.chunksPerSecond();
if (chunksPerSecond > 0D) {
return IntegrationMetricSample.available(descriptor, 1000D / chunksPerSecond, now);
private void putTimingSamples(
Map<String, IntegrationMetricSample> samples,
Map<String, Double> timings,
long sampledAtMs
) {
for (Map.Entry<String, String> entry : TIMING_KEYS.entrySet()) {
Double value = timings.get(entry.getKey());
samples.put(entry.getValue(), value == null
? unavailable(entry.getValue(), "timing-not-available", sampledAtMs)
: available(entry.getValue(), value, sampledAtMs));
}
IrisEngineSVC engineService = Iris.service(IrisEngineSVC.class);
if (engineService != null) {
double idle = engineService.getAverageIdleDuration();
if (idle > 0D && Double.isFinite(idle)) {
return IntegrationMetricSample.available(descriptor, idle, now);
}
}
return IntegrationMetricSample.available(descriptor, 0D, now);
}
private IntegrationMetricSample samplePregenQueueMetric(long now) {
IntegrationMetricDescriptor descriptor = IntegrationMetricSchema.descriptor(IntegrationMetricSchema.IRIS_PREGEN_QUEUE);
long totalQueue = 0L;
boolean hasAnySource = false;
long pregenRemaining = PregeneratorJob.chunksRemaining();
if (pregenRemaining >= 0L) {
totalQueue += pregenRemaining;
hasAnySource = true;
}
IrisEngineSVC engineService = Iris.service(IrisEngineSVC.class);
if (engineService != null) {
totalQueue += Math.max(0, engineService.getQueuedTectonicPlateCount());
hasAnySource = true;
}
if (!hasAnySource) {
return IntegrationMetricSample.unavailable(descriptor, "queue-not-available", now);
}
return IntegrationMetricSample.available(descriptor, totalQueue, now);
private void put(Map<String, IntegrationMetricSample> samples, String key, double value, long sampledAtMs) {
samples.put(key, available(key, value, sampledAtMs));
}
private IntegrationMetricSample sampleBiomeCacheHitRateMetric(long now) {
IntegrationMetricDescriptor descriptor = IntegrationMetricSchema.descriptor(IntegrationMetricSchema.IRIS_BIOME_CACHE_HIT_RATE);
IrisEngineSVC engineService = Iris.service(IrisEngineSVC.class);
if (engineService == null) {
return IntegrationMetricSample.unavailable(descriptor, "engine-service-unavailable", now);
}
private IntegrationMetricSample available(String key, double value, long sampledAtMs) {
return IntegrationMetricSample.available(IntegrationMetricSchema.descriptor(key), value, sampledAtMs);
}
double ratio = engineService.getBiomeCacheUsageRatio();
if (!Double.isFinite(ratio)) {
return IntegrationMetricSample.unavailable(descriptor, "biome-cache-ratio-invalid", now);
}
private IntegrationMetricSample unavailable(String key, String reason, long sampledAtMs) {
return IntegrationMetricSample.unavailable(IntegrationMetricSchema.descriptor(key), reason, sampledAtMs);
}
return IntegrationMetricSample.available(descriptor, Math.max(0D, Math.min(1D, ratio)), now);
private Map<String, IntegrationMetricSample> unavailableMetrics(
Set<String> keys,
String reason,
long sampledAtMs
) {
Map<String, IntegrationMetricSample> samples = new LinkedHashMap<>(keys.size());
for (String key : keys) {
samples.put(key, unavailable(key, reason, sampledAtMs));
}
return Map.copyOf(samples);
}
private IrisTelemetrySnapshot safeSnapshot() {
IrisTelemetrySnapshot snapshot = telemetrySupplier.get();
return snapshot == null ? IrisTelemetrySnapshot.EMPTY : snapshot;
}
private static IrisTelemetrySnapshot currentTelemetry() {
IrisEngineSVC service = IrisServices.get(IrisEngineSVC.class);
return service == null ? IrisTelemetrySnapshot.EMPTY : service.telemetrySnapshot();
}
}
@@ -0,0 +1,140 @@
package art.arcane.iris.core.service;
import art.arcane.iris.engine.framework.EngineTelemetrySnapshot;
import java.util.List;
record IrisTelemetrySnapshot(
long sampledAtMs,
List<EngineTelemetrySnapshot> worlds,
EngineTelemetrySnapshot.Aggregate aggregate,
int maintenanceActiveTasks,
int maintenanceWorkers,
int closingEngines,
int pendingRegistrations,
double heapUsage,
double reclaimUrgency,
CacheSnapshot caches,
PregenSnapshot pregenerator
) {
static final IrisTelemetrySnapshot EMPTY = new IrisTelemetrySnapshot(
0L,
List.of(),
EngineTelemetrySnapshot.aggregate(List.of()),
0,
0,
0,
0,
0D,
0D,
CacheSnapshot.EMPTY,
PregenSnapshot.INACTIVE
);
IrisTelemetrySnapshot {
worlds = worlds == null ? List.of() : List.copyOf(worlds);
aggregate = aggregate == null ? EngineTelemetrySnapshot.aggregate(worlds) : aggregate;
maintenanceActiveTasks = Math.max(0, maintenanceActiveTasks);
maintenanceWorkers = Math.max(0, maintenanceWorkers);
closingEngines = Math.max(0, closingEngines);
pendingRegistrations = Math.max(0, pendingRegistrations);
heapUsage = finiteRatio(heapUsage);
reclaimUrgency = finiteRatio(reclaimUrgency);
caches = caches == null ? CacheSnapshot.EMPTY : caches;
pregenerator = pregenerator == null ? PregenSnapshot.INACTIVE : pregenerator;
}
private static double finiteRatio(double value) {
return Double.isFinite(value) ? Math.max(0D, Math.min(1D, value)) : 0D;
}
record CacheSnapshot(
CacheBucket total,
CacheBucket resource,
CacheBucket stream2d,
CacheBucket stream3d,
CacheBucket other
) {
static final CacheSnapshot EMPTY = new CacheSnapshot(
CacheBucket.EMPTY,
CacheBucket.EMPTY,
CacheBucket.EMPTY,
CacheBucket.EMPTY,
CacheBucket.EMPTY
);
CacheSnapshot {
total = total == null ? CacheBucket.EMPTY : total;
resource = resource == null ? CacheBucket.EMPTY : resource;
stream2d = stream2d == null ? CacheBucket.EMPTY : stream2d;
stream3d = stream3d == null ? CacheBucket.EMPTY : stream3d;
other = other == null ? CacheBucket.EMPTY : other;
}
}
record CacheBucket(int count, long entries, long capacity) {
static final CacheBucket EMPTY = new CacheBucket(0, 0L, 0L);
CacheBucket {
count = Math.max(0, count);
entries = Math.max(0L, entries);
capacity = Math.max(0L, capacity);
}
double usage() {
return capacity <= 0L ? 0D : Math.min(1D, entries / (double) capacity);
}
CacheBucket plus(CacheBucket other) {
if (other == null) {
return this;
}
return new CacheBucket(count + other.count, entries + other.entries, capacity + other.capacity);
}
}
record PregenSnapshot(
boolean active,
String worldIdentity,
String worldName,
boolean paused,
double progressPercent,
long generated,
long total,
long remaining,
double chunksPerSecond,
long etaMs,
long elapsedMs,
long failed
) {
static final PregenSnapshot INACTIVE = new PregenSnapshot(
false,
"",
"",
false,
0D,
0L,
0L,
0L,
0D,
0L,
0L,
0L
);
PregenSnapshot {
worldIdentity = worldIdentity == null ? "" : worldIdentity.trim();
worldName = worldName == null ? "" : worldName.trim();
progressPercent = Double.isFinite(progressPercent)
? Math.max(0D, Math.min(100D, progressPercent))
: 0D;
generated = Math.max(0L, generated);
total = Math.max(0L, total);
remaining = Math.max(0L, remaining);
chunksPerSecond = Double.isFinite(chunksPerSecond) ? Math.max(0D, chunksPerSecond) : 0D;
etaMs = Math.max(0L, etaMs);
elapsedMs = Math.max(0L, elapsedMs);
failed = Math.max(0L, failed);
}
}
}
@@ -0,0 +1,308 @@
package art.arcane.iris.core.service;
import art.arcane.iris.spi.IrisLogging;
import art.arcane.iris.util.common.scheduling.J;
import org.bukkit.Location;
import org.bukkit.Material;
import org.bukkit.Particle;
import org.bukkit.Sound;
import org.bukkit.World;
import org.bukkit.block.Block;
import org.bukkit.block.data.BlockData;
import org.bukkit.entity.ExperienceOrb;
import org.bukkit.entity.Item;
import org.bukkit.entity.Player;
import org.bukkit.inventory.ItemStack;
import org.bukkit.util.Vector;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Queue;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
final class TreeFellerPresentation {
private static final int MIN_BLOCKS_PER_PULSE = 4;
private static final int MAX_BLOCKS_PER_PULSE = 64;
private static final int TARGET_EROSION_PULSES = 60;
private static final int MAX_EFFECT_ORIGINS_PER_PULSE = 16;
private final Player player;
private final World sourceWorld;
private final Queue<PendingDrop> pendingDrops = new ConcurrentLinkedQueue<>();
private final AtomicBoolean flushScheduled = new AtomicBoolean();
private final AtomicBoolean fallbackScheduled = new AtomicBoolean();
private final AtomicBoolean completed = new AtomicBoolean();
private final AtomicBoolean effectFailureReported = new AtomicBoolean();
private final AtomicBoolean deliveryFailureReported = new AtomicBoolean();
private final AtomicInteger deliveryPulses = new AtomicInteger();
private volatile Location fallbackLocation;
TreeFellerPresentation(Player player, World sourceWorld, Location fallbackLocation) {
this.player = player;
this.sourceWorld = sourceWorld;
this.fallbackLocation = fallbackLocation.clone();
}
static int blocksPerPulse(int blockCount) {
int requested = Math.max(1, (blockCount + TARGET_EROSION_PULSES - 1) / TARGET_EROSION_PULSES);
return Math.max(MIN_BLOCKS_PER_PULSE, Math.min(requested, MAX_BLOCKS_PER_PULSE));
}
static int effectStride(int blocksPerPulse) {
return Math.max(
1,
(blocksPerPulse + MAX_EFFECT_ORIGINS_PER_PULSE - 1) / MAX_EFFECT_ORIGINS_PER_PULSE
);
}
static List<ItemStack> consolidateDrops(Collection<ItemStack> drops) {
List<ItemStack> consolidated = new ArrayList<>();
for (ItemStack drop : drops) {
mergeDrop(consolidated, drop);
}
return List.copyOf(consolidated);
}
void activate(Block block) {
try {
Location center = block.getLocation().clone().add(0.5D, 0.5D, 0.5D);
World world = block.getWorld();
world.spawnParticle(Particle.ENCHANT, center, 24, 0.45D, 0.45D, 0.45D, 0.18D);
world.spawnParticle(Particle.END_ROD, center, 8, 0.25D, 0.25D, 0.25D, 0.035D);
world.playSound(center, Sound.BLOCK_ENCHANTMENT_TABLE_USE, 0.55F, 1.35F);
world.playSound(center, Sound.BLOCK_AMETHYST_BLOCK_CHIME, 0.4F, 0.8F);
} catch (Throwable error) {
reportEffectFailure(error);
}
}
void erode(
Location source,
BlockData visualData,
int erosionOrder,
int processed,
int blocksPerPulse,
int effectStride,
int totalBlocks
) {
if (processed % effectStride != 0 || source.getWorld() == null) {
return;
}
try {
World world = source.getWorld();
world.spawnParticle(Particle.BLOCK, source, 5, 0.3D, 0.3D, 0.3D, 0.04D, visualData);
world.spawnParticle(Particle.ENCHANT, source, 3, 0.28D, 0.28D, 0.28D, 0.12D);
if (processed % blocksPerPulse == 0) {
double progress = totalBlocks <= 1 ? 1D : (double) erosionOrder / (double) (totalBlocks - 1);
float pitch = (float) Math.min(1.95D, 0.65D + (progress * 1.25D));
world.playSound(source, Sound.BLOCK_AMETHYST_BLOCK_CHIME, 0.22F, pitch);
}
} catch (Throwable error) {
reportEffectFailure(error);
}
}
boolean routeDrop(Object drop) {
if (!(drop instanceof ItemStack item)) {
return false;
}
if (item.getType() == Material.AIR || item.getAmount() <= 0) {
return true;
}
return enqueue(new PendingDrop(item.clone(), 0));
}
boolean routeExperience(int experience) {
return experience <= 0 || enqueue(new PendingDrop(null, experience));
}
void finish() {
completed.set(true);
if (!pendingDrops.isEmpty()) {
scheduleFlush();
}
}
private boolean enqueue(PendingDrop drop) {
pendingDrops.add(drop);
if (scheduleFlush()) {
return true;
}
return !pendingDrops.remove(drop);
}
private boolean scheduleFlush() {
if (pendingDrops.isEmpty() || !flushScheduled.compareAndSet(false, true)) {
return true;
}
fallbackScheduled.set(false);
Runnable retired = () -> {
if (!scheduleFallbackFlush()) {
flushScheduled.set(false);
IrisLogging.error("Unable to deliver queued Iris tree-feller drops after the player retired.");
}
};
if (J.runEntity(player, this::flushAtPlayer, 1, retired)) {
return true;
}
return scheduleFallbackFlush();
}
private boolean scheduleFallbackFlush() {
if (!fallbackScheduled.compareAndSet(false, true)) {
return true;
}
Location fallback = fallbackLocation.clone();
if (J.runAt(fallback, () -> flushAtFallback(fallback), 1)) {
return true;
}
fallbackScheduled.set(false);
flushScheduled.set(false);
return false;
}
private void flushAtPlayer() {
if (!player.isOnline() || !player.getWorld().equals(sourceWorld)) {
if (!scheduleFallbackFlush()) {
IrisLogging.error("Unable to deliver queued Iris tree-feller drops at their fallback location.");
}
return;
}
Location target = player.getLocation().clone().add(0D, 0.15D, 0D);
fallbackLocation = target.clone();
deliverBatch(target, drainPendingDrops(), true);
completeFlush();
}
private void flushAtFallback(Location fallback) {
deliverBatch(fallback, drainPendingDrops(), false);
completeFlush();
}
private DropBatch drainPendingDrops() {
List<ItemStack> items = new ArrayList<>();
long experience = 0L;
PendingDrop pending;
while ((pending = pendingDrops.poll()) != null) {
if (pending.item() != null) {
items.add(pending.item());
}
experience = Math.min(Integer.MAX_VALUE, experience + pending.experience());
}
return new DropBatch(consolidateDrops(items), (int) experience);
}
private void deliverBatch(Location target, DropBatch batch, boolean atPlayer) {
World world = target.getWorld();
if (world == null) {
requeueBatch(batch);
return;
}
int deliveredItems = 0;
for (int index = 0; index < batch.items().size(); index++) {
ItemStack item = batch.items().get(index);
Item dropped;
try {
dropped = world.dropItem(target, item);
} catch (Throwable error) {
requeueUndelivered(batch, index);
reportDeliveryFailure(error);
return;
}
deliveredItems++;
try {
dropped.setVelocity(new Vector(0D, 0.08D, 0D));
} catch (Throwable error) {
reportEffectFailure(error);
}
}
if (batch.experience() > 0) {
try {
ExperienceOrb orb = world.spawn(target.clone().add(0D, 0.35D, 0D), ExperienceOrb.class);
orb.setExperience(batch.experience());
} catch (Throwable error) {
pendingDrops.add(new PendingDrop(null, batch.experience()));
reportDeliveryFailure(error);
}
}
try {
int enchantParticles = Math.min(32, 6 + (deliveredItems * 2));
Location effectLocation = target.clone().add(0D, 0.35D, 0D);
world.spawnParticle(Particle.ENCHANT, effectLocation, enchantParticles, 0.35D, 0.25D, 0.35D, 0.15D);
world.spawnParticle(Particle.END_ROD, effectLocation, Math.min(8, 2 + deliveredItems), 0.2D, 0.2D, 0.2D, 0.025D);
int deliveryPulse = deliveryPulses.incrementAndGet();
if (atPlayer && (deliveryPulse == 1 || deliveryPulse % 4 == 0 || completed.get())) {
world.playSound(target, Sound.BLOCK_AMETHYST_BLOCK_CHIME, 0.2F, 1.65F);
}
} catch (Throwable error) {
reportEffectFailure(error);
}
}
private void completeFlush() {
fallbackScheduled.set(false);
flushScheduled.set(false);
if (!pendingDrops.isEmpty() && !scheduleFlush()) {
IrisLogging.error("Unable to schedule the remaining Iris tree-feller drops.");
}
}
private void requeueBatch(DropBatch batch) {
requeueUndelivered(batch, 0);
}
private void requeueUndelivered(DropBatch batch, int firstUndeliveredItem) {
for (int index = firstUndeliveredItem; index < batch.items().size(); index++) {
pendingDrops.add(new PendingDrop(batch.items().get(index), 0));
}
if (batch.experience() > 0) {
pendingDrops.add(new PendingDrop(null, batch.experience()));
}
}
private void reportEffectFailure(Throwable error) {
if (effectFailureReported.compareAndSet(false, true)) {
IrisLogging.reportError("Failed to render an Iris tree-feller effect.", error);
}
}
private void reportDeliveryFailure(Throwable error) {
if (deliveryFailureReported.compareAndSet(false, true)) {
IrisLogging.reportError("Failed to deliver an Iris tree-feller drop; it remains queued for retry.", error);
}
}
private static void mergeDrop(List<ItemStack> consolidated, ItemStack source) {
if (source == null || source.getType() == Material.AIR || source.getAmount() <= 0) {
return;
}
int remaining = source.getAmount();
for (ItemStack existing : consolidated) {
if (!existing.isSimilar(source) || existing.getAmount() >= existing.getMaxStackSize()) {
continue;
}
int transferable = Math.min(remaining, existing.getMaxStackSize() - existing.getAmount());
existing.setAmount(existing.getAmount() + transferable);
remaining -= transferable;
if (remaining == 0) {
return;
}
}
int maximumStackSize = Math.max(1, source.getMaxStackSize());
while (remaining > 0) {
ItemStack stack = source.clone();
stack.setAmount(Math.min(remaining, maximumStackSize));
consolidated.add(stack);
remaining -= stack.getAmount();
}
}
private record PendingDrop(ItemStack item, int experience) {
}
private record DropBatch(List<ItemStack> items, int experience) {
}
}
@@ -8,6 +8,10 @@ load: STARTUP
authors: [ cyberpwn, NextdoorPsycho, Vatuu ]
website: volmit.com
description: More than a Dimension!
permissions:
iris.treefeller:
description: Allows survival players to fell Iris-managed trees with an axe.
default: op
dependencies:
server:
PlaceholderAPI:
@@ -22,7 +22,11 @@ loadbefore: [ Multiverse-Core ]
authors: [ cyberpwn, NextdoorPsycho, Vatuu ]
website: volmit.com
description: More than a Dimension!
commands:
iris:
aliases: [ ir, irs ]
commands:
iris:
aliases: [ ir, irs ]
permissions:
iris.treefeller:
description: Allows survival players to fell Iris-managed trees with an axe.
default: op
api-version: '${apiVersion}'
@@ -2,6 +2,8 @@ package art.arcane.iris;
import org.bukkit.plugin.PluginDescriptionFile;
import org.bukkit.plugin.PluginLoadOrder;
import org.bukkit.permissions.Permission;
import org.bukkit.permissions.PermissionDefault;
import org.junit.Test;
import java.io.InputStream;
@@ -54,6 +56,9 @@ public class PaperPluginMetadataTest {
assertTrue(metadata.contains("folia-supported: true"));
assertTrue(metadata.contains("load: STARTUP"));
assertFalse(metadata.contains("commands:"));
assertTrue(metadata.contains("permissions:\n iris.treefeller:\n"
+ " description: Allows survival players to fell Iris-managed trees with an axe.\n"
+ " default: op"));
for (String pluginId : JOINED_PLUGIN_IDS) {
assertTrue(metadata.contains(optionalDependencyBlock(pluginId, "BEFORE", true)));
}
@@ -76,6 +81,13 @@ public class PaperPluginMetadataTest {
assertEquals(List.of("ir", "irs"), commands.get("iris").get("aliases"));
assertEquals(BUKKIT_SOFT_DEPEND_IDS, metadata.getSoftDepend());
assertEquals(List.of("Multiverse-Core"), metadata.getLoadBeforePlugins());
Permission treeFeller = metadata.getPermissions().stream()
.filter(permission -> "iris.treefeller".equals(permission.getName()))
.findFirst()
.orElse(null);
assertNotNull(treeFeller);
assertEquals("Allows survival players to fell Iris-managed trees with an axe.", treeFeller.getDescription());
assertEquals(PermissionDefault.OP, treeFeller.getDefault());
}
@Test
@@ -0,0 +1,28 @@
package art.arcane.iris.api.tree;
import org.junit.Test;
import static org.junit.Assert.assertEquals;
public class TreeFellerOptionsTest {
@Test
public void factoriesExposeStableAccessModes() {
TreeFellerOptions standalone = TreeFellerOptions.standalone();
TreeFellerOptions override = TreeFellerOptions.integrationOverride(75, TreeFellerRunHooks.NONE);
assertEquals(TreeFellerAccess.STANDALONE, standalone.access());
assertEquals(0, standalone.durabilityPreservationChance());
assertEquals(TreeFellerAccess.INTEGRATION_OVERRIDE, override.access());
assertEquals(75, override.durabilityPreservationChance());
}
@Test(expected = IllegalArgumentException.class)
public void chanceBelowRangeIsRejected() {
TreeFellerOptions.integrationOverride(-1, TreeFellerRunHooks.NONE);
}
@Test(expected = IllegalArgumentException.class)
public void chanceAboveRangeIsRejected() {
TreeFellerOptions.integrationOverride(101, TreeFellerRunHooks.NONE);
}
}
@@ -0,0 +1,83 @@
package art.arcane.iris.core.runtime;
import org.junit.Test;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
public class BukkitEngineLifecycleContractTest {
@Test
public void closeFutureIsPublishedBeforeCloseWorkIsScheduled() throws IOException {
String source = Files.readString(Path.of(System.getProperty("iris.bukkitChunkGeneratorSource")));
String closeAsync = method(source, "public CompletableFuture<Void> closeAsync()");
assertBefore(closeAsync, "closeFuture.compareAndSet(null, future)", "withExclusiveControlFuture(");
assertTrue(closeAsync.contains("while (!closeFuture.compareAndSet(null, future))"));
assertTrue(closeAsync.contains("operation.whenComplete("));
assertFalse(closeAsync.contains("!existing.isDone()"));
String baseHeight = method(source, "public int getBaseHeight(");
assertTrue(baseHeight.contains("currentEngine.acquireGenerationLease(\"bukkit_base_height\")"));
assertTrue(baseHeight.contains("IrisContext.open(currentEngine, lease.sessionId(), null)"));
assertTrue(baseHeight.contains("catch (GenerationSessionException e)"));
String generation = method(source, "public void generateNoise(");
assertTrue(generation.contains("throw new IllegalStateException"));
assertTrue(generation.contains("engine.acquireGenerationLease(\"bukkit_terrain_stage\")"));
assertTrue(generation.contains("IrisContext.open(engine, lease.sessionId(), null)"));
assertBefore(generation, "engine.acquireGenerationLease(\"bukkit_terrain_stage\")", "blocks.apply()");
assertFalse(generation.contains("RED_GLAZED_TERRACOTTA"));
}
@Test
public void targetedPregeneratorShutdownChecksWorldIdentity() throws IOException {
String jobSource = Files.readString(Path.of(System.getProperty("iris.pregeneratorJobSource")));
String targetedShutdown = method(jobSource,
"public static boolean shutdownInstanceForWorld(String worldIdentity)");
assertBefore(targetedShutdown, "inst.targetsWorldIdentity(worldIdentity)",
"shutdownAndWait(inst, WORLD_SHUTDOWN_TIMEOUT_MILLIS)");
String waitedShutdown = method(jobSource,
"private static boolean shutdownAndWait(PregeneratorJob inst, long timeoutMs)");
assertBefore(waitedShutdown, "inst.worker.interrupt()", "inst.worker.join(");
assertTrue(waitedShutdown.contains("inst.worker.isAlive()"));
String hooksSource = Files.readString(Path.of(System.getProperty("iris.bukkitEnginePlatformHooksSource")));
String hookShutdown = method(hooksSource, "public void shutdownPregenerator(Engine engine)");
assertTrue(hookShutdown.contains("PregeneratorJob.shutdownInstanceForWorld(world.identity());"));
assertFalse(hookShutdown.contains("PregeneratorJob.shutdownInstance();"));
}
private static void assertBefore(String source, String first, String second) {
int firstIndex = source.indexOf(first);
int secondIndex = source.indexOf(second);
assertTrue("Missing source contract token: " + first, firstIndex >= 0);
assertTrue("Missing source contract token: " + second, secondIndex >= 0);
assertTrue(first + " must occur before " + second, firstIndex < secondIndex);
}
private static String method(String source, String signature) {
int start = source.indexOf(signature);
assertTrue("Missing source contract signature: " + signature, start >= 0);
int openBrace = source.indexOf('{', start);
assertTrue("Missing source contract method body: " + signature, openBrace >= 0);
int depth = 0;
for (int index = openBrace; index < source.length(); index++) {
char current = source.charAt(index);
if (current == '{') {
depth++;
} else if (current == '}') {
depth--;
if (depth == 0) {
return source.substring(start, index + 1);
}
}
}
throw new IllegalArgumentException("Unclosed source contract method: " + signature);
}
}
@@ -0,0 +1,105 @@
package art.arcane.iris.core.service;
import art.arcane.iris.engine.framework.EngineAssignedWorldManager;
import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority;
import org.bukkit.event.world.WorldUnloadEvent;
import org.junit.Test;
import java.io.IOException;
import java.lang.reflect.Method;
import java.nio.file.Files;
import java.nio.file.Path;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
public class IrisEngineLifecycleContractTest {
@Test
public void engineServiceExclusivelyOwnsWorldUnload() throws NoSuchMethodException {
assertMonitorUnloadHandler(IrisEngineSVC.class.getMethod("onWorldUnload", WorldUnloadEvent.class));
for (Method method : EngineAssignedWorldManager.class.getDeclaredMethods()) {
boolean handlesWorldUnload = method.getParameterCount() == 1
&& method.getParameterTypes()[0] == WorldUnloadEvent.class
&& method.isAnnotationPresent(EventHandler.class);
assertFalse("EngineAssignedWorldManager must defer world unload ownership to IrisEngineSVC.", handlesWorldUnload);
}
}
@Test
public void registrationIdentityConflictsByWorldIdentityOrFolder() {
Path sharedFolder = Path.of("build", "worlds", "shared");
IrisEngineSVC.RegistrationIdentity first =
new IrisEngineSVC.RegistrationIdentity("minecraft:first", sharedFolder);
IrisEngineSVC.RegistrationIdentity sameIdentity =
new IrisEngineSVC.RegistrationIdentity("minecraft:first", Path.of("build", "worlds", "other"));
IrisEngineSVC.RegistrationIdentity sameFolder =
new IrisEngineSVC.RegistrationIdentity("minecraft:other", sharedFolder);
IrisEngineSVC.RegistrationIdentity distinct =
new IrisEngineSVC.RegistrationIdentity("minecraft:distinct", Path.of("build", "worlds", "distinct"));
assertTrue(first.conflictsWith(sameIdentity));
assertTrue(first.conflictsWith(sameFolder));
assertFalse(first.conflictsWith(distinct));
}
@Test
public void registrationRetryWaitsAsynchronouslyForClose() throws IOException {
String source = Files.readString(Path.of(System.getProperty("iris.engineSvcSource")));
String add = method(source, "private void add(World world)");
String remove = method(source, "private void remove(World world)");
String completeClose = method(source, "private void completeClose(");
String retry = method(source, "private void retryRegistrationAfterClose(");
String invokeClose = method(source, "private CompletableFuture<Void> invokeGeneratorClose()");
assertBefore(add, "findClosingGenerator(registrationIdentity)", "new Registered(");
assertBefore(remove, "reserveClose(registered)", "startClose(registered, closing)");
assertBefore(completeClose, "if (failure == null)", "closingGenerators.remove(closing)");
assertBefore(completeClose, "closingGenerators.remove(closing)", "closing.completion().complete(null)");
assertBefore(completeClose, "} else {", "closing.completion().completeExceptionally(failure)");
assertTrue(retry.contains("completion.whenComplete("));
assertTrue(retry.contains("J.s(() -> add(world), 1);"));
assertBefore(retry, "if (failure != null)", "J.s(() -> add(world), 1);");
assertFalse(retry.contains("completion.get("));
assertFalse(retry.contains("completion.join("));
assertTrue(invokeClose.contains("CompletableFuture.failedFuture("));
assertTrue(invokeClose.contains("future.whenComplete("));
}
private static void assertMonitorUnloadHandler(Method method) {
EventHandler eventHandler = method.getAnnotation(EventHandler.class);
assertNotNull(eventHandler);
assertEquals(EventPriority.MONITOR, eventHandler.priority());
assertTrue(eventHandler.ignoreCancelled());
}
private static void assertBefore(String source, String first, String second) {
int firstIndex = source.indexOf(first);
int secondIndex = source.indexOf(second);
assertTrue("Missing source contract token: " + first, firstIndex >= 0);
assertTrue("Missing source contract token: " + second, secondIndex >= 0);
assertTrue(first + " must occur before " + second, firstIndex < secondIndex);
}
private static String method(String source, String signature) {
int start = source.indexOf(signature);
assertTrue("Missing source contract signature: " + signature, start >= 0);
int openBrace = source.indexOf('{', start);
assertTrue("Missing source contract method body: " + signature, openBrace >= 0);
int depth = 0;
for (int index = openBrace; index < source.length(); index++) {
char current = source.charAt(index);
if (current == '{') {
depth++;
} else if (current == '}') {
depth--;
if (depth == 0) {
return source.substring(start, index + 1);
}
}
}
throw new IllegalArgumentException("Unclosed source contract method: " + signature);
}
}
@@ -1,24 +0,0 @@
package art.arcane.iris.core.service;
import org.junit.Test;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
public class IrisEngineSVCTest {
@Test
public void maintenanceSkipsReductionWhenPregenDoesNotTargetWorld() {
assertTrue(IrisEngineSVC.shouldSkipMantleReductionForMaintenance(true, false));
}
@Test
public void maintenanceDoesNotSkipReductionForActivePregenWorld() {
assertFalse(IrisEngineSVC.shouldSkipMantleReductionForMaintenance(true, true));
}
@Test
public void noMaintenanceNeverSkipsReduction() {
assertFalse(IrisEngineSVC.shouldSkipMantleReductionForMaintenance(false, false));
assertFalse(IrisEngineSVC.shouldSkipMantleReductionForMaintenance(false, true));
}
}
@@ -0,0 +1,174 @@
package art.arcane.iris.core.service;
import art.arcane.iris.engine.framework.EngineTelemetrySnapshot;
import art.arcane.volmlib.integration.IntegrationMetricGroup;
import art.arcane.volmlib.integration.IntegrationMetricSample;
import art.arcane.volmlib.integration.IntegrationMetricSchema;
import org.junit.Test;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
public class IrisIntegrationServiceTest {
@Test
public void unavailableTelemetryDoesNotPublishFalseZeroes() {
IrisIntegrationService service = new IrisIntegrationService(() -> IrisTelemetrySnapshot.EMPTY);
Map<String, IntegrationMetricSample> samples = service.sampleMetrics(Set.of(
IntegrationMetricSchema.IRIS_WORLD_COUNT,
IntegrationMetricSchema.IRIS_PREGEN_QUEUE,
IntegrationMetricSchema.IRIS_GENERATION_TOTAL_MS));
for (IntegrationMetricSample sample : samples.values()) {
assertFalse(sample.available());
assertEquals("telemetry-not-ready", sample.message());
}
assertTrue(service.metricGroups().isEmpty());
}
@Test
public void publishesAggregateAndPerWorldMetricsFromOneImmutableSnapshot() {
long now = System.currentTimeMillis();
EngineTelemetrySnapshot first = world("minecraft:overworld", "world", 12L, 20L, 4D, now);
EngineTelemetrySnapshot second = world("minecraft:the_nether", "world_nether", 8L, 30L, 2D, now);
IrisTelemetrySnapshot snapshot = telemetry(
List.of(first, second),
new IrisTelemetrySnapshot.PregenSnapshot(
true,
first.worldIdentity(),
first.worldName(),
false,
50D,
100L,
200L,
100L,
8D,
2_000L,
5_000L,
1L
),
now
);
IrisIntegrationService service = new IrisIntegrationService(() -> snapshot);
Map<String, IntegrationMetricSample> samples = service.sampleMetrics(Set.of(
IntegrationMetricSchema.IRIS_WORLD_COUNT,
IntegrationMetricSchema.IRIS_ENGINE_PENDING_REGISTRATIONS,
IntegrationMetricSchema.IRIS_LOADED_CHUNKS,
IntegrationMetricSchema.IRIS_CHUNKS_GENERATED_TOTAL,
IntegrationMetricSchema.IRIS_PREGEN_THROUGHPUT,
IntegrationMetricSchema.IRIS_GENERATION_TOTAL_MS,
"iris.unsupported"
));
assertEquals(2D, value(samples, IntegrationMetricSchema.IRIS_WORLD_COUNT), 0D);
assertEquals(3D, value(samples, IntegrationMetricSchema.IRIS_ENGINE_PENDING_REGISTRATIONS), 0D);
assertEquals(20D, value(samples, IntegrationMetricSchema.IRIS_LOADED_CHUNKS), 0D);
assertEquals(50D, value(samples, IntegrationMetricSchema.IRIS_CHUNKS_GENERATED_TOTAL), 0D);
assertEquals(8D, value(samples, IntegrationMetricSchema.IRIS_PREGEN_THROUGHPUT), 0D);
assertEquals(4D, value(samples, IntegrationMetricSchema.IRIS_GENERATION_TOTAL_MS), 0D);
assertFalse(samples.get("iris.unsupported").available());
assertEquals("unsupported-key", samples.get("iris.unsupported").message());
List<IntegrationMetricGroup> groups = service.metricGroups();
assertEquals(2, groups.size());
IntegrationMetricGroup overworld = groups.get(0);
IntegrationMetricGroup nether = groups.get(1);
assertEquals("minecraft:overworld", overworld.scopeId());
assertEquals(1D, value(overworld.samples(), IntegrationMetricSchema.IRIS_PREGEN_ACTIVE), 0D);
assertEquals(8D, value(overworld.samples(), IntegrationMetricSchema.IRIS_PREGEN_THROUGHPUT), 0D);
assertEquals("minecraft:the_nether", nether.scopeId());
assertEquals(0D, value(nether.samples(), IntegrationMetricSchema.IRIS_PREGEN_ACTIVE), 0D);
assertFalse(nether.samples().get(IntegrationMetricSchema.IRIS_PREGEN_QUEUE).available());
}
@Test
public void publishesEveryManagedWorldWithoutAWorldCountCap() {
long now = System.currentTimeMillis();
List<EngineTelemetrySnapshot> worlds = new ArrayList<>();
for (int index = 0; index < 48; index++) {
worlds.add(world("iris:world_" + index, "world_" + index, index, index, index, now));
}
IrisIntegrationService service = new IrisIntegrationService(
() -> telemetry(worlds, IrisTelemetrySnapshot.PregenSnapshot.INACTIVE, now)
);
List<IntegrationMetricGroup> groups = service.metricGroups();
assertEquals(48, groups.size());
assertEquals(48, groups.stream().map(IntegrationMetricGroup::scopeId).distinct().count());
}
private static IrisTelemetrySnapshot telemetry(
List<EngineTelemetrySnapshot> worlds,
IrisTelemetrySnapshot.PregenSnapshot pregenerator,
long now
) {
IrisTelemetrySnapshot.CacheBucket resource = new IrisTelemetrySnapshot.CacheBucket(2, 10L, 100L);
IrisTelemetrySnapshot.CacheSnapshot caches = new IrisTelemetrySnapshot.CacheSnapshot(
resource,
resource,
IrisTelemetrySnapshot.CacheBucket.EMPTY,
IrisTelemetrySnapshot.CacheBucket.EMPTY,
IrisTelemetrySnapshot.CacheBucket.EMPTY
);
return new IrisTelemetrySnapshot(
now,
worlds,
EngineTelemetrySnapshot.aggregate(worlds),
1,
4,
0,
3,
0.5D,
0.1D,
caches,
pregenerator
);
}
private static EngineTelemetrySnapshot world(
String identity,
String name,
long loadedChunks,
long generatedTotal,
double generationMs,
long now
) {
return new EngineTelemetrySnapshot(
now,
identity,
name,
"dimension",
true,
false,
false,
false,
loadedChunks,
3L,
0.25D,
generatedTotal,
generatedTotal,
2D,
4L,
2,
1,
1L,
4L,
1L,
10D,
Map.of("total", generationMs)
);
}
private static double value(Map<String, IntegrationMetricSample> samples, String key) {
IntegrationMetricSample sample = samples.get(key);
assertTrue(sample.available());
return sample.valueOr(-1D);
}
}
@@ -0,0 +1,60 @@
package art.arcane.iris.core.service;
import art.arcane.iris.core.service.tree.TreeMarkerTraversal;
import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority;
import org.bukkit.event.player.PlayerItemHeldEvent;
import org.bukkit.event.player.PlayerSwapHandItemsEvent;
import org.bukkit.event.player.PlayerToggleSneakEvent;
import org.junit.Test;
import java.lang.reflect.Method;
import java.util.List;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
public class TreeFellerEventOrderTest {
@Test
public void standaloneIntentPrecedesMonitorFinalization() throws Exception {
Method request = TreeFellerSVC.class.getMethod("requestStandalone", org.bukkit.event.block.BlockBreakEvent.class);
Method finalize = TreeFellerSVC.class.getMethod("finalizeBreak", org.bukkit.event.block.BlockBreakEvent.class);
EventHandler requestHandler = request.getAnnotation(EventHandler.class);
EventHandler finalizeHandler = finalize.getAnnotation(EventHandler.class);
assertEquals(EventPriority.HIGHEST, requestHandler.priority());
assertTrue(requestHandler.ignoreCancelled());
assertEquals(EventPriority.MONITOR, finalizeHandler.priority());
assertFalse(finalizeHandler.ignoreCancelled());
}
@Test
public void incompleteDiscoveryFallsOnlyTheTrigger() {
TreeMarkerTraversal.Position trigger = new TreeMarkerTraversal.Position(4, 80, -2);
TreeMarkerTraversal.Discovery incomplete = new TreeMarkerTraversal.Discovery(
List.of(trigger, new TreeMarkerTraversal.Position(4, 81, -2)),
false
);
assertEquals(List.of(trigger), TreeFellerSVC.positionsForFelling(incomplete, trigger));
}
@Test
public void playerControlChangesHaltRunsAtMonitor() throws Exception {
Method sneak = TreeFellerSVC.class.getMethod("haltWhenSneakingStops", PlayerToggleSneakEvent.class);
Method held = TreeFellerSVC.class.getMethod("haltWhenHeldSlotChanges", PlayerItemHeldEvent.class);
Method hands = TreeFellerSVC.class.getMethod("haltWhenHandsSwap", PlayerSwapHandItemsEvent.class);
assertMonitorCancellationHandler(sneak);
assertMonitorCancellationHandler(held);
assertMonitorCancellationHandler(hands);
}
private void assertMonitorCancellationHandler(Method method) {
EventHandler handler = method.getAnnotation(EventHandler.class);
assertEquals(EventPriority.MONITOR, handler.priority());
assertTrue(handler.ignoreCancelled());
}
}
@@ -0,0 +1,27 @@
package art.arcane.iris.core.service;
import org.junit.Test;
import static org.junit.Assert.assertEquals;
public class TreeFellerPresentationTest {
@Test
public void pulseSizeKeepsSmallTreesReadableAndLargeTreesBounded() {
assertEquals(4, TreeFellerPresentation.blocksPerPulse(1));
assertEquals(4, TreeFellerPresentation.blocksPerPulse(240));
assertEquals(10, TreeFellerPresentation.blocksPerPulse(600));
assertEquals(64, TreeFellerPresentation.blocksPerPulse(100_000));
}
@Test
public void effectSamplingStaysBoundedPerPulse() {
assertEquals(1, TreeFellerPresentation.effectStride(4));
assertEquals(1, TreeFellerPresentation.effectStride(16));
assertEquals(2, TreeFellerPresentation.effectStride(17));
assertEquals(2, TreeFellerPresentation.effectStride(31));
assertEquals(2, TreeFellerPresentation.effectStride(32));
assertEquals(3, TreeFellerPresentation.effectStride(33));
assertEquals(4, TreeFellerPresentation.effectStride(64));
}
}