mirror of
https://github.com/VolmitSoftware/Iris.git
synced 2026-08-27 12:41:43 +00:00
d
This commit is contained in:
@@ -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());
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
+12
@@ -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();
|
||||
}
|
||||
+4
-1
@@ -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
|
||||
|
||||
+610
-365
File diff suppressed because it is too large
Load Diff
+91
@@ -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) {
|
||||
}
|
||||
}
|
||||
+251
-76
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
+140
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
+308
@@ -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) {
|
||||
}
|
||||
}
|
||||
+1110
File diff suppressed because it is too large
Load Diff
@@ -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
|
||||
|
||||
+28
@@ -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);
|
||||
}
|
||||
}
|
||||
+83
@@ -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);
|
||||
}
|
||||
}
|
||||
+105
@@ -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);
|
||||
}
|
||||
}
|
||||
-24
@@ -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));
|
||||
}
|
||||
}
|
||||
+174
@@ -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);
|
||||
}
|
||||
}
|
||||
+60
@@ -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());
|
||||
}
|
||||
}
|
||||
+27
@@ -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));
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user