From b82472d5215b7130bf291261a81b452ca0f8993c Mon Sep 17 00:00:00 2001 From: Brian Neumann-Fopiano Date: Fri, 17 Apr 2026 16:14:49 -0400 Subject: [PATCH] Loads --- core/plugins/Iris/cache/instance | 2 +- core/src/main/java/art/arcane/iris/Iris.java | 2 - .../iris/core/ExternalDataPackPipeline.java | 133 +- .../art/arcane/iris/core/IrisSettings.java | 52 +- .../iris/core/commands/CommandDeveloper.java | 1318 ++++++++++++++++- .../iris/core/commands/CommandIris.java | 1293 ---------------- .../iris/core/commands/CommandLazyPregen.java | 127 -- .../iris/core/commands/CommandSmoke.java | 186 --- .../iris/core/commands/CommandStudio.java | 117 -- .../core/commands/CommandTurboPregen.java | 137 -- .../iris/core/commands/CommandUpdater.java | 102 -- .../arcane/iris/core/edit/DustRevealer.java | 7 +- .../iris/core/pregenerator/ChunkUpdater.java | 324 ---- .../core/pregenerator/LazyPregenerator.java | 295 ---- .../core/pregenerator/TurboPregenerator.java | 357 ----- .../methods/AsyncPregenMethod.java | 88 +- .../arcane/iris/core/project/IrisProject.java | 2 +- .../core/runtime/SmokeDiagnosticsService.java | 368 ----- .../iris/core/runtime/SmokeTestService.java | 418 ------ .../core/runtime/StudioOpenCoordinator.java | 144 +- .../art/arcane/iris/core/safeguard/Mode.java | 2 +- .../iris/core/service/IrisEngineSVC.java | 32 +- .../core/service/IrisIntegrationService.java | 20 - .../arcane/iris/core/service/StudioSVC.java | 2 +- .../engine/actuator/IrisDecorantActuator.java | 2 +- .../actuator/IrisTerrainNormalActuator.java | 34 +- .../decorator/IrisSurfaceDecorator.java | 2 +- .../arcane/iris/engine/framework/Engine.java | 13 +- .../iris/engine/mantle/EngineMantle.java | 28 + .../iris/engine/mantle/MantleComponent.java | 6 + .../iris/engine/mantle/MatterGenerator.java | 26 +- .../mantle/components/IrisCaveCarver3D.java | 79 +- .../components/MantleObjectComponent.java | 150 +- .../engine/modifier/IrisCarveModifier.java | 40 +- .../iris/engine/object/IrisCaveProfile.java | 3 + .../object/IrisDimensionCarvingResolver.java | 9 +- .../iris/engine/object/IrisObjectScale.java | 64 +- .../util/project/context/ChunkContext.java | 16 +- .../context/ChunkedDoubleDataCache.java | 24 +- .../util/project/profile/LoadBalancer.java | 70 - ...SmokeDiagnosticsServiceCloseStateTest.java | 28 - .../core/nms/v1_21_R7/IrisChunkGenerator.java | 43 +- 42 files changed, 2035 insertions(+), 4130 deletions(-) delete mode 100644 core/src/main/java/art/arcane/iris/core/commands/CommandLazyPregen.java delete mode 100644 core/src/main/java/art/arcane/iris/core/commands/CommandSmoke.java delete mode 100644 core/src/main/java/art/arcane/iris/core/commands/CommandTurboPregen.java delete mode 100644 core/src/main/java/art/arcane/iris/core/commands/CommandUpdater.java delete mode 100644 core/src/main/java/art/arcane/iris/core/pregenerator/ChunkUpdater.java delete mode 100644 core/src/main/java/art/arcane/iris/core/pregenerator/LazyPregenerator.java delete mode 100644 core/src/main/java/art/arcane/iris/core/pregenerator/TurboPregenerator.java delete mode 100644 core/src/main/java/art/arcane/iris/core/runtime/SmokeDiagnosticsService.java delete mode 100644 core/src/main/java/art/arcane/iris/core/runtime/SmokeTestService.java delete mode 100644 core/src/main/java/art/arcane/iris/util/project/profile/LoadBalancer.java delete mode 100644 core/src/test/java/art/arcane/iris/core/runtime/SmokeDiagnosticsServiceCloseStateTest.java diff --git a/core/plugins/Iris/cache/instance b/core/plugins/Iris/cache/instance index 1a937a41b..4b1cc96e1 100644 --- a/core/plugins/Iris/cache/instance +++ b/core/plugins/Iris/cache/instance @@ -1 +1 @@ -1982643195 \ No newline at end of file +699705819 \ No newline at end of file diff --git a/core/src/main/java/art/arcane/iris/Iris.java b/core/src/main/java/art/arcane/iris/Iris.java index 635e4907c..dd1f26aab 100644 --- a/core/src/main/java/art/arcane/iris/Iris.java +++ b/core/src/main/java/art/arcane/iris/Iris.java @@ -32,7 +32,6 @@ import art.arcane.iris.core.link.IrisPapiExpansion; import art.arcane.iris.core.link.MultiverseCoreLink; import art.arcane.iris.core.loader.IrisData; import art.arcane.iris.core.nms.INMS; -import art.arcane.iris.core.pregenerator.LazyPregenerator; import art.arcane.iris.core.service.StudioSVC; import art.arcane.iris.core.tools.IrisToolbelt; import art.arcane.iris.engine.EnginePanic; @@ -565,7 +564,6 @@ public class Iris extends VolmitPlugin implements Listener { J.s(() -> { J.a(() -> IO.delete(getTemp())); - J.a(LazyPregenerator::loadLazyGenerators, 100); J.a(this::bstats); J.ar(this::checkConfigHotload, 60); J.sr(this::tickQueue, 0); diff --git a/core/src/main/java/art/arcane/iris/core/ExternalDataPackPipeline.java b/core/src/main/java/art/arcane/iris/core/ExternalDataPackPipeline.java index 1bf6cc4c4..b4108a53c 100644 --- a/core/src/main/java/art/arcane/iris/core/ExternalDataPackPipeline.java +++ b/core/src/main/java/art/arcane/iris/core/ExternalDataPackPipeline.java @@ -979,6 +979,11 @@ public final class ExternalDataPackPipeline { packSourceFolder.mkdirs(); cacheFolder.mkdirs(); + RequestSyncResult metadataRestore = restoreRequestFromMetadata(packSourceFolder, request); + if (metadataRestore.success()) { + return metadataRestore; + } + try { ResolvedRemoteFile remoteFile = resolveRemoteFile(url); File output = new File(packSourceFolder, remoteFile.outputFileName()); @@ -1012,10 +1017,6 @@ public final class ExternalDataPackPipeline { writeRequestMetadata(packSourceFolder, request, output.getName(), remoteFile.sha1()); return RequestSyncResult.downloaded(output); } catch (Throwable e) { - RequestSyncResult restored = restoreRequestFromMetadata(packSourceFolder, request); - if (restored.success()) { - return restored; - } String message = e.getMessage() == null ? e.getClass().getSimpleName() : e.getMessage(); return RequestSyncResult.failure(message); } @@ -1194,6 +1195,18 @@ public final class ExternalDataPackPipeline { return ProjectionResult.success(managedName, 0, 0, Set.copyOf(request.resolvedLocateStructures()), 0, Set.of(), 0, 0, 0); } + String projectionCacheKey = buildProjectionCacheKey(sourceDescriptor.fingerprint(), request); + File projectionCacheDir = Iris.instance.getDataFolder("cache", "projected-datapacks"); + File cachedZip = new File(projectionCacheDir, projectionCacheKey + ".zip"); + File cachedMeta = new File(projectionCacheDir, projectionCacheKey + ".json"); + + if (cachedZip.exists() && cachedZip.length() > 0 && cachedMeta.exists()) { + ProjectionResult cachedResult = restoreCachedProjection(cachedZip, cachedMeta, managedName, worldDatapackFolders); + if (cachedResult != null) { + return cachedResult; + } + } + ProjectionAssetSummary projectionAssetSummary; try { projectionAssetSummary = buildProjectedAssets(source, sourceDescriptor, request); @@ -1219,6 +1232,7 @@ public final class ExternalDataPackPipeline { int installedDatapacks = 0; int installedAssets = 0; + boolean firstWrite = true; for (File worldDatapackFolder : worldDatapackFolders) { if (worldDatapackFolder == null) { continue; @@ -1242,6 +1256,11 @@ public final class ExternalDataPackPipeline { } installedDatapacks++; installedAssets += copiedAssets; + + if (firstWrite && managedZip.exists()) { + cacheProjection(managedZip, cachedZip, cachedMeta, projectionAssetSummary); + firstWrite = false; + } } catch (Throwable e) { Iris.warn("Failed to project external datapack source " + sourceDescriptor.sourceName() + " into " + worldDatapackFolder.getPath()); Iris.reportError(e); @@ -4091,6 +4110,112 @@ public final class ExternalDataPackPipeline { return builder.toString(); } + private static String buildProjectionCacheKey(String sourceFingerprint, DatapackRequest request) { + StringBuilder keyBuilder = new StringBuilder(); + keyBuilder.append(sourceFingerprint); + keyBuilder.append('|').append(request.getDedupeKey()); + keyBuilder.append('|').append(request.alongsideMode()); + keyBuilder.append('|').append(request.templateAliases()); + keyBuilder.append('|').append(request.structureStartHeights()); + keyBuilder.append('|').append(request.structureSetAliases()); + keyBuilder.append('|').append(request.structureAliases()); + keyBuilder.append('|').append(request.structures()); + keyBuilder.append('|').append(request.structureSets()); + keyBuilder.append('|').append(request.templatePools()); + keyBuilder.append('|').append(request.processorLists()); + keyBuilder.append('|').append(request.configuredFeatures()); + keyBuilder.append('|').append(request.placedFeatures()); + keyBuilder.append('|').append(request.biomeHasStructureTags()); + try { + MessageDigest digest = MessageDigest.getInstance("SHA-1"); + byte[] bytes = digest.digest(keyBuilder.toString().getBytes(StandardCharsets.UTF_8)); + StringBuilder hex = new StringBuilder(bytes.length * 2); + for (byte b : bytes) { + hex.append(String.format("%02x", b)); + } + return hex.toString(); + } catch (Throwable e) { + return shortHash(keyBuilder.toString()); + } + } + + private static ProjectionResult restoreCachedProjection(File cachedZip, File cachedMeta, String managedName, KList worldDatapackFolders) { + try { + JSONObject meta = new JSONObject(Files.readString(cachedMeta.toPath(), StandardCharsets.UTF_8)); + int installedDatapacks = 0; + int installedAssets = 0; + for (File worldDatapackFolder : worldDatapackFolders) { + if (worldDatapackFolder == null) { + continue; + } + worldDatapackFolder.mkdirs(); + String baseManagedName = managedName.endsWith(".zip") ? managedName.substring(0, managedName.length() - 4) : managedName; + deleteFolder(new File(worldDatapackFolder, baseManagedName)); + File managedZip = new File(worldDatapackFolder, managedName); + if (managedZip.exists()) { + managedZip.delete(); + } + Files.copy(cachedZip.toPath(), managedZip.toPath(), StandardCopyOption.REPLACE_EXISTING); + if (managedZip.exists() && managedZip.length() > 0) { + installedDatapacks++; + installedAssets += meta.optInt("installedAssets", 0); + } + } + Set resolvedLocateStructures = readJsonStringSet(meta, "resolvedLocateStructures"); + Set projectedStructureKeys = readJsonStringSet(meta, "projectedStructureKeys"); + return ProjectionResult.success( + managedName, + installedDatapacks, + installedAssets, + resolvedLocateStructures, + meta.optInt("syntheticStructureSets", 0), + projectedStructureKeys, + meta.optInt("templateAliasesApplied", 0), + meta.optInt("emptyElementConversions", 0), + meta.optInt("unresolvedTemplateRefs", 0) + ); + } catch (Throwable e) { + Iris.verbose("Projection cache miss: " + e.getMessage()); + return null; + } + } + + private static void cacheProjection(File sourceZip, File cachedZip, File cachedMeta, ProjectionAssetSummary summary) { + try { + File parent = cachedZip.getParentFile(); + if (parent != null) { + parent.mkdirs(); + } + Files.copy(sourceZip.toPath(), cachedZip.toPath(), StandardCopyOption.REPLACE_EXISTING); + JSONObject meta = new JSONObject(); + meta.put("installedAssets", summary.assets().size()); + meta.put("syntheticStructureSets", summary.syntheticStructureSets()); + meta.put("templateAliasesApplied", summary.templateAliasesApplied()); + meta.put("emptyElementConversions", summary.emptyElementConversions()); + meta.put("unresolvedTemplateRefs", summary.unresolvedTemplateRefs()); + meta.put("resolvedLocateStructures", new JSONArray(summary.resolvedLocateStructures())); + meta.put("projectedStructureKeys", new JSONArray(summary.projectedStructureKeys())); + Files.writeString(cachedMeta.toPath(), meta.toString(2), StandardCharsets.UTF_8); + } catch (Throwable e) { + Iris.verbose("Failed to cache projection: " + e.getMessage()); + } + } + + private static Set readJsonStringSet(JSONObject json, String key) { + JSONArray array = json.optJSONArray(key); + if (array == null) { + return Set.of(); + } + LinkedHashSet result = new LinkedHashSet<>(); + for (int i = 0; i < array.length(); i++) { + String value = array.optString(i, ""); + if (!value.isBlank()) { + result.add(value); + } + } + return Set.copyOf(result); + } + private static String shortHash(String value) { try { MessageDigest digest = MessageDigest.getInstance("SHA-1"); diff --git a/core/src/main/java/art/arcane/iris/core/IrisSettings.java b/core/src/main/java/art/arcane/iris/core/IrisSettings.java index 99b5430ba..601ebcf91 100644 --- a/core/src/main/java/art/arcane/iris/core/IrisSettings.java +++ b/core/src/main/java/art/arcane/iris/core/IrisSettings.java @@ -25,9 +25,7 @@ import art.arcane.volmlib.util.json.JSONException; import art.arcane.volmlib.util.json.JSONObject; import art.arcane.iris.util.common.misc.getHardware; import art.arcane.iris.util.common.plugin.VolmitSender; -import lombok.AllArgsConstructor; import lombok.Data; -import lombok.NoArgsConstructor; import java.io.File; import java.io.IOException; @@ -44,7 +42,6 @@ public class IrisSettings { private IrisSettingsConcurrency concurrency = new IrisSettingsConcurrency(); private IrisSettingsStudio studio = new IrisSettingsStudio(); private IrisSettingsPerformance performance = new IrisSettingsPerformance(); - private IrisSettingsUpdater updater = new IrisSettingsUpdater(); private IrisSettingsPregen pregen = new IrisSettingsPregen(); private IrisSettingsSentry sentry = new IrisSettingsSentry(); @@ -158,7 +155,7 @@ public class IrisSettings { public int foliaMaxConcurrency = 32; public int chunkLoadTimeoutSeconds = 15; public int timeoutWarnIntervalMs = 500; - public int saveIntervalMs = 120_000; + public int saveIntervalMs = 30_000; public boolean enablePregenPerformanceProfile = true; public int pregenProfileNoiseCacheSize = 4_096; public boolean pregenProfileEnableFastCache = true; @@ -212,37 +209,6 @@ public class IrisSettings { } } - @Data - public static class IrisSettingsUpdater { - public int maxConcurrency = 256; - public boolean nativeThreads = false; - public double threadMultiplier = 2; - - public double chunkLoadSensitivity = 0.7; - public MsRange emptyMsRange = new MsRange(80, 100); - public MsRange defaultMsRange = new MsRange(20, 40); - - public int getMaxConcurrency() { - return Math.max(Math.abs(maxConcurrency), 1); - } - - public double getThreadMultiplier() { - return Math.min(Math.abs(threadMultiplier), 0.1); - } - - public double getChunkLoadSensitivity() { - return Math.min(chunkLoadSensitivity, 0.9); - } - } - - @Data - @NoArgsConstructor - @AllArgsConstructor - public static class MsRange { - public int min = 20; - public int max = 40; - } - @Data public static class IrisSettingsGeneral { public boolean commandSounds = true; @@ -256,6 +222,22 @@ public class IrisSettings { public boolean adjustVanillaHeight = false; public boolean importExternalDatapacks = true; public boolean autoGenerateIntrinsicStructures = true; + public boolean intrinsicStructureFoundations = true; + public int intrinsicFoundationMaxDepth = 96; + public java.util.List intrinsicStructureAllowlist = new java.util.ArrayList<>(java.util.List.of( + "minecraft:village_plains", + "minecraft:village_desert", + "minecraft:village_savanna", + "minecraft:village_snowy", + "minecraft:village_taiga", + "minecraft:pillager_outpost", + "minecraft:desert_pyramid", + "minecraft:jungle_temple", + "minecraft:swamp_hut", + "minecraft:igloo", + "minecraft:mansion", + "minecraft:ruined_portal*" + )); public String forceMainWorld = ""; public int spinh = -20; public int spins = 7; diff --git a/core/src/main/java/art/arcane/iris/core/commands/CommandDeveloper.java b/core/src/main/java/art/arcane/iris/core/commands/CommandDeveloper.java index 86e4bc089..e48220841 100644 --- a/core/src/main/java/art/arcane/iris/core/commands/CommandDeveloper.java +++ b/core/src/main/java/art/arcane/iris/core/commands/CommandDeveloper.java @@ -34,16 +34,20 @@ import art.arcane.iris.core.tools.IrisToolbelt; import art.arcane.iris.engine.IrisEngineMantle; import art.arcane.iris.engine.framework.Engine; import art.arcane.iris.engine.object.IrisDimension; +import art.arcane.iris.engine.platform.ChunkReplacementListener; +import art.arcane.iris.engine.platform.ChunkReplacementOptions; import art.arcane.iris.engine.platform.PlatformChunkGenerator; import art.arcane.iris.engine.object.annotations.Snippet; import art.arcane.volmlib.util.collection.KSet; import art.arcane.iris.util.project.context.IrisContext; import art.arcane.volmlib.util.collection.KList; +import art.arcane.iris.util.common.director.DirectorContext; import art.arcane.iris.util.common.director.DirectorExecutor; import art.arcane.volmlib.util.director.DirectorOrigin; import art.arcane.volmlib.util.director.annotations.Director; import art.arcane.volmlib.util.director.annotations.Param; import art.arcane.iris.util.common.director.specialhandlers.NullableDimensionHandler; +import art.arcane.iris.util.common.misc.RegenRuntime; import art.arcane.iris.util.common.format.C; import art.arcane.volmlib.util.format.Form; import art.arcane.volmlib.util.io.CountingDataInputStream; @@ -55,6 +59,7 @@ import art.arcane.volmlib.util.matter.Matter; import art.arcane.iris.util.nbt.common.mca.MCAFile; import art.arcane.iris.util.nbt.common.mca.MCAUtil; import art.arcane.iris.util.common.parallel.MultiBurst; +import art.arcane.iris.util.common.parallel.SyncExecutor; import art.arcane.iris.util.common.plugin.VolmitSender; import art.arcane.iris.util.common.scheduling.J; import art.arcane.iris.util.common.scheduling.jobs.Job; @@ -67,6 +72,10 @@ import org.apache.commons.lang.RandomStringUtils; import org.bukkit.Bukkit; import org.bukkit.Chunk; import org.bukkit.World; +import org.bukkit.boss.BarColor; +import org.bukkit.boss.BarStyle; +import org.bukkit.boss.BossBar; +import org.bukkit.entity.Player; import java.io.*; import java.net.InetAddress; @@ -85,9 +94,6 @@ public class CommandDeveloper implements DirectorExecutor { private static final int DELETE_CHUNK_MAX_ATTEMPTS = 2; private static final int DELETE_CHUNK_STACK_LIMIT = 20; private static final Set ACTIVE_DELETE_CHUNK_WORLDS = ConcurrentHashMap.newKeySet(); - private CommandTurboPregen turboPregen; - private CommandLazyPregen lazyPregen; - private CommandSmoke smoke; @Director(description = "Get Loaded TectonicPlates Count", origin = DirectorOrigin.BOTH, sync = true) public void EngineStatus() { @@ -1059,4 +1065,1310 @@ public class CommandDeveloper implements DirectorExecutor { default -> throw new IllegalStateException("Unexpected value: " + algorithm); }); } + + // --- Regen --- + + private static final long REGEN_HEARTBEAT_MS = 5000L; + private static final int REGEN_MAX_ATTEMPTS = 2; + private static final int REGEN_STACK_LIMIT = 20; + private static final long REGEN_STALL_DUMP_IDLE_MS = 30000L; + private static final long REGEN_STALL_ABORT_IDLE_MS = 600000L; + private static final long REGEN_STACK_DUMP_INTERVAL_MS = 10000L; + private static final int REGEN_PROGRESS_BAR_WIDTH = 44; + private static final long REGEN_PROGRESS_UPDATE_MS = 200L; + private static final int REGEN_ACTION_PULSE_TICKS = 20; + private static final int REGEN_DISPLAY_FINAL_TICKS = 60; + + @Director(name = "regen", aliases = {"rg"}, description = "Regenerate nearby chunks using Iris generation", origin = DirectorOrigin.PLAYER, sync = true) + public void regen( + @Param(name = "radius", description = "The radius of nearby chunks", defaultValue = "5") + int radius, + @Param(name = "mode", aliases = {"scope", "profile"}, description = "Regen mode: terrain or full", defaultValue = "full") + String mode + ) { + if (radius < 0) { + sender().sendMessage(C.RED + "Radius must be 0 or greater."); + return; + } + + World world = player().getWorld(); + if (!IrisToolbelt.isIrisWorld(world)) { + sender().sendMessage(C.RED + "You must be in an Iris world to use regen."); + return; + } + + RegenMode regenMode = RegenMode.parse(mode); + if (regenMode == null) { + sender().sendMessage(C.RED + "Unknown regen mode \"" + mode + "\". Use mode=terrain or mode=full."); + return; + } + + VolmitSender sender = sender(); + int centerX = player().getLocation().getBlockX() >> 4; + int centerZ = player().getLocation().getBlockZ() >> 4; + int threadCount = resolveRegenThreadCount(regenMode); + List targets = buildRegenTargets(centerX, centerZ, radius); + int chunks = targets.size(); + String runId = world.getName() + "-" + System.currentTimeMillis(); + RegenDisplay display = createRegenDisplay(sender, regenMode); + + sender.sendMessage(C.GREEN + "Regen started (" + C.GOLD + regenMode.id() + C.GREEN + "): " + + C.GOLD + chunks + C.GREEN + " chunks, " + + C.GOLD + threadCount + C.GREEN + " worker(s). " + + C.GRAY + "Progress is shown on-screen."); + if (regenMode == RegenMode.TERRAIN) { + Iris.warn("Regen running in terrain mode; mantle overlay/object replay is skipped. Use mode=full to regenerate objects."); + } + + Iris.info("Regen run start: id=" + runId + + " world=" + world.getName() + + " center=" + centerX + "," + centerZ + + " radius=" + radius + + " mode=" + regenMode.id() + + " workers=" + threadCount + + " chunks=" + chunks); + if (IrisSettings.get().getGeneral().isDebug()) { + Iris.info("Regen mode config: id=" + runId + + " mode=" + regenMode.id() + + " maintenance=" + regenMode.usesMaintenance() + + " bypassMantle=" + regenMode.bypassMantleStages() + + " passes=" + regenMode.passCount() + + " fullMode=" + regenMode.isFullMode() + + " diagnostics=" + regenMode.logChunkDiagnostics()); + } + + String orchestratorName = "Iris-Regen-Orchestrator-" + runId; + Thread orchestrator = new Thread(() -> runRegenOrchestrator(sender, world, targets, threadCount, regenMode, runId, display), orchestratorName); + orchestrator.setDaemon(true); + try { + orchestrator.start(); + if (IrisSettings.get().getGeneral().isDebug()) { + Iris.info("Regen worker dispatched on dedicated thread=" + orchestratorName + " id=" + runId + "."); + } + } catch (Throwable e) { + sender.sendMessage(C.RED + "Failed to start regen worker thread. See console."); + closeRegenDisplay(display, 0); + Iris.reportError(e); + } + } + + private int resolveRegenThreadCount(RegenMode mode) { + int workers = resolveRegenWorkerThreads(); + boolean folia = J.isFolia(); + if (mode == RegenMode.TERRAIN) { + int cap = folia ? Math.min(workers * 4, 96) : Math.min(workers * 2, 64); + return Math.max(folia ? 16 : 8, cap); + } + int cap = folia ? Math.min(workers * 2, 64) : Math.min(workers, 32); + return Math.max(folia ? 8 : 4, cap); + } + + private int resolveRegenWorkerThreads() { + try { + Class moonriseCommonClass = Class.forName("ca.spottedleaf.moonrise.common.util.MoonriseCommon"); + java.lang.reflect.Field workerPoolField = moonriseCommonClass.getDeclaredField("WORKER_POOL"); + Object workerPool = workerPoolField.get(null); + Object coreThreads = workerPool.getClass().getDeclaredMethod("getCoreThreads").invoke(workerPool); + if (coreThreads instanceof Thread[] threadsArray && threadsArray.length > 0) { + return threadsArray.length; + } + } catch (Throwable ignored) { + } + int cpus = Math.max(1, Runtime.getRuntime().availableProcessors()); + int configured = Math.max(1, IrisSettings.get().getConcurrency().getWorldGenThreads()); + return Math.max(cpus, configured); + } + + private List buildRegenTargets(int centerX, int centerZ, int radius) { + int expected = (radius * 2 + 1) * (radius * 2 + 1); + List targets = new ArrayList<>(expected); + for (int ring = 0; ring <= radius; ring++) { + for (int x = -ring; x <= ring; x++) { + for (int z = -ring; z <= ring; z++) { + if (Math.max(Math.abs(x), Math.abs(z)) != ring) { + continue; + } + targets.add(new Position2(centerX + x, centerZ + z)); + } + } + } + return targets; + } + + private void runRegenOrchestrator( + VolmitSender sender, + World world, + List targets, + int threadCount, + RegenMode mode, + String runId, + RegenDisplay display + ) { + long runStart = System.currentTimeMillis(); + AtomicBoolean setupDone = new AtomicBoolean(false); + AtomicReference setupPhase = new AtomicReference<>("bootstrap"); + AtomicLong setupPhaseSince = new AtomicLong(runStart); + Thread setupWatchdog = createRegenSetupWatchdog(world, runId, setupDone, setupPhase, setupPhaseSince); + setupWatchdog.start(); + boolean displayTerminal = false; + + Set regenThreads = ConcurrentHashMap.newKeySet(); + AtomicInteger regenThreadCounter = new AtomicInteger(); + ThreadFactory threadFactory = runnable -> { + Thread thread = new Thread(runnable, "Iris-Regen-" + runId + "-" + regenThreadCounter.incrementAndGet()); + thread.setDaemon(true); + regenThreads.add(thread); + return thread; + }; + + try { + setRegenSetupPhase(setupPhase, setupPhaseSince, "touch-context", world, runId); + updateRegenSetupDisplay(display, mode, "Touching command context", 1, 6); + DirectorContext.touch(sender); + if (mode.usesMaintenance()) { + setRegenSetupPhase(setupPhase, setupPhaseSince, "enter-maintenance", world, runId); + updateRegenSetupDisplay(display, mode, "Entering maintenance", 2, 6); + IrisToolbelt.beginWorldMaintenance(world, "regen:" + mode.id(), mode.bypassMantleStages()); + } else { + setRegenSetupPhase(setupPhase, setupPhaseSince, "maintenance-skip", world, runId); + updateRegenSetupDisplay(display, mode, "Skipping maintenance", 2, 6); + } + + ThreadPoolExecutor pool = (ThreadPoolExecutor) Executors.newFixedThreadPool(threadCount, threadFactory); + try (SyncExecutor executor = new SyncExecutor(20)) { + setRegenSetupPhase(setupPhase, setupPhaseSince, "resolve-platform", world, runId); + updateRegenSetupDisplay(display, mode, "Resolving platform", 3, 6); + PlatformChunkGenerator platform = IrisToolbelt.access(world); + setRegenSetupPhase(setupPhase, setupPhaseSince, "validate-engine", world, runId); + updateRegenSetupDisplay(display, mode, "Validating engine", 4, 6); + if (platform == null || platform.getEngine() == null) { + Iris.warn("Regen aborted: engine access is null for world=" + world.getName() + " id=" + runId + "."); + completeRegenDisplay(display, mode, true, C.RED + "Engine access is null. Generate nearby chunks first."); + displayTerminal = true; + return; + } + + setRegenSetupPhase(setupPhase, setupPhaseSince, "prepare-options", world, runId); + updateRegenSetupDisplay(display, mode, "Preparing chunk replacement", 5, 6); + + setRegenSetupPhase(setupPhase, setupPhaseSince, "dispatch", world, runId); + updateRegenSetupDisplay(display, mode, "Dispatching chunk workers", 6, 6); + RegenSummary summary = executeRegenQueue(sender, world, platform, targets, executor, pool, regenThreads, mode, runId, 1, 1, runStart, display); + + if (summary == null) { + completeRegenDisplay(display, mode, true, C.RED + "Regen failed before pass execution."); + displayTerminal = true; + return; + } + + long totalRuntime = System.currentTimeMillis() - runStart; + if (summary.failedChunks() <= 0) { + completeRegenDisplay(display, mode, false, C.GREEN + "Complete " + C.GOLD + summary.successChunks() + + C.GREEN + "/" + C.GOLD + summary.totalChunks() + C.GREEN + " in " + C.GOLD + totalRuntime + "ms"); + displayTerminal = true; + return; + } + + String failureDetail = C.RED + "Failed chunks " + C.GOLD + summary.failedChunks() + C.RED + + ", retries " + C.GOLD + summary.retryCount() + + C.RED + ", runtime " + C.GOLD + totalRuntime + "ms"; + if (summary.failurePhaseSummary() != null && !summary.failurePhaseSummary().isBlank() && !"none".equals(summary.failurePhaseSummary())) { + failureDetail = failureDetail + C.DARK_GRAY + " [phase " + summary.failurePhaseSummary() + "]"; + } + if (!summary.failedPreview().isEmpty()) { + failureDetail = failureDetail + C.DARK_GRAY + " [" + summary.failedPreview() + "]"; + } + completeRegenDisplay(display, mode, true, failureDetail); + displayTerminal = true; + } finally { + pool.shutdownNow(); + } + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + completeRegenDisplay(display, mode, true, C.RED + "Regen interrupted."); + displayTerminal = true; + Iris.warn("Regen run interrupted: id=" + runId + " world=" + world.getName()); + } catch (Throwable e) { + String failureDetail = C.RED + "Regen failed. Check console."; + if (e.getMessage() != null && e.getMessage().contains("stalled")) { + failureDetail = C.RED + "Regen stalled. Try smaller radius or terrain mode."; + } + completeRegenDisplay(display, mode, true, failureDetail); + displayTerminal = true; + Iris.reportError(e); + e.printStackTrace(); + } finally { + setupDone.set(true); + setupWatchdog.interrupt(); + if (mode.usesMaintenance()) { + IrisToolbelt.endWorldMaintenance(world, "regen:" + mode.id()); + } + if (!displayTerminal) { + closeRegenDisplay(display, REGEN_DISPLAY_FINAL_TICKS); + } + DirectorContext.remove(); + Iris.info("Regen run closed: id=" + runId + " world=" + world.getName() + " totalMs=" + (System.currentTimeMillis() - runStart)); + } + } + + private RegenSummary executeRegenQueue( + VolmitSender sender, + World world, + PlatformChunkGenerator platform, + List targets, + SyncExecutor executor, + ThreadPoolExecutor pool, + Set regenThreads, + RegenMode mode, + String runId, + int passIndex, + int passCount, + long runStart, + RegenDisplay display + ) throws InterruptedException { + ArrayDeque pending = new ArrayDeque<>(targets.size()); + long queueTime = System.currentTimeMillis(); + for (Position2 target : targets) { + pending.addLast(new RegenChunkTask(target.getX(), target.getZ(), 1, queueTime)); + } + + ConcurrentMap activeTasks = new ConcurrentHashMap<>(); + ExecutorCompletionService completion = new ExecutorCompletionService<>(pool); + List failedChunks = new ArrayList<>(); + Map failurePhaseCounts = new HashMap<>(); + + int totalChunks = targets.size(); + int successChunks = 0; + int failedCount = 0; + int retryCount = 0; + int overlayChunks = 0; + int overlayObjectChunks = 0; + int overlayBlocks = 0; + long submittedTasks = 0L; + long finishedTasks = 0L; + int completedChunks = 0; + int inFlight = 0; + AtomicLong lastSignalMs = new AtomicLong(System.currentTimeMillis()); + long lastDump = 0L; + long lastProgressUiMs = 0L; + lastProgressUiMs = updateRegenProgressAction( + sender, + display, + mode, + passIndex, + passCount, + completedChunks, + totalChunks, + inFlight, + pending.size(), + false, + false, + false, + true, + "Queue initialized", + lastProgressUiMs + ); + + while (inFlight < pool.getMaximumPoolSize() && !pending.isEmpty()) { + RegenChunkTask task = pending.removeFirst(); + completion.submit(() -> runRegenChunk(task, world, platform, executor, activeTasks, mode, runId, lastSignalMs)); + inFlight++; + submittedTasks++; + } + + while (completedChunks < totalChunks) { + Future future = completion.poll(REGEN_HEARTBEAT_MS, TimeUnit.MILLISECONDS); + if (future == null) { + long now = System.currentTimeMillis(); + long idleMs = Math.max(0L, now - lastSignalMs.get()); + boolean stalled = idleMs >= REGEN_HEARTBEAT_MS; + String phaseSummary = summarizeActivePhases(activeTasks); + + Iris.warn("Regen heartbeat: id=" + runId + + " completed=" + completedChunks + "/" + totalChunks + + " remaining=" + (totalChunks - completedChunks) + + " queued=" + pending.size() + + " inFlight=" + inFlight + + " submitted=" + submittedTasks + + " finishedTasks=" + finishedTasks + + " retries=" + retryCount + + " failed=" + failedCount + + " poolActive=" + pool.getActiveCount() + + " poolQueue=" + pool.getQueue().size() + + " poolDone=" + pool.getCompletedTaskCount() + + " idleMs=" + idleMs + + " phases=" + phaseSummary + + " activeTasks=" + formatActiveTasks(activeTasks)); + lastProgressUiMs = updateRegenProgressAction( + sender, + display, + mode, + passIndex, + passCount, + completedChunks, + totalChunks, + inFlight, + pending.size(), + stalled, + false, + false, + true, + stalled ? "Waiting in phase " + phaseSummary : "Waiting for chunk result", + lastProgressUiMs + ); + + if (idleMs >= REGEN_STALL_DUMP_IDLE_MS && now - lastDump >= REGEN_STACK_DUMP_INTERVAL_MS) { + lastDump = now; + Iris.warn("Regen appears stalled; dumping worker stack traces for id=" + runId + "."); + dumpRegenWorkerStacks(regenThreads, world.getName()); + } + if (idleMs >= REGEN_STALL_ABORT_IDLE_MS) { + updateRegenProgressAction( + sender, + display, + mode, + passIndex, + passCount, + completedChunks, + totalChunks, + inFlight, + pending.size(), + true, + true, + true, + true, + "Stalled in phase " + phaseSummary, + lastProgressUiMs + ); + throw new IllegalStateException("Regen stalled with no chunk heartbeat or completion for " + + idleMs + + "ms (id=" + runId + + ", mode=" + mode.id() + + ", completed=" + completedChunks + + "/" + totalChunks + + ", inFlight=" + inFlight + + ", queued=" + pending.size() + + ", phase=" + phaseSummary + + ")."); + } + continue; + } + + RegenChunkResult result; + try { + result = future.get(); + } catch (ExecutionException e) { + Throwable cause = e.getCause() == null ? e : e.getCause(); + throw new IllegalStateException("Regen worker failed unexpectedly for run " + runId, cause); + } + + inFlight--; + finishedTasks++; + long duration = result.finishedAtMs() - result.startedAtMs(); + lastSignalMs.set(System.currentTimeMillis()); + + if (result.success()) { + completedChunks++; + successChunks++; + if (result.overlayAppliedBlocks() > 0) { + overlayChunks++; + } + if (result.overlayObjectKeys() > 0) { + overlayObjectChunks++; + } + overlayBlocks += result.overlayAppliedBlocks(); + if (result.task().attempt() > 1) { + Iris.warn("Regen chunk recovered after retry: id=" + runId + + " chunk=" + result.task().chunkX() + "," + result.task().chunkZ() + + " attempt=" + result.task().attempt() + + " durationMs=" + duration); + } else if (duration >= 5000L) { + Iris.warn("Regen chunk slow: id=" + runId + + " chunk=" + result.task().chunkX() + "," + result.task().chunkZ() + + " durationMs=" + duration + + " loadedAtStart=" + result.loadedAtStart()); + } + } else if (result.task().attempt() < REGEN_MAX_ATTEMPTS) { + retryCount++; + RegenChunkTask retryTask = result.task().retry(System.currentTimeMillis()); + pending.addLast(retryTask); + Iris.warn("Regen chunk retry scheduled: id=" + runId + + " chunk=" + result.task().chunkX() + "," + result.task().chunkZ() + + " failedAttempt=" + result.task().attempt() + + " nextAttempt=" + retryTask.attempt() + + " phase=" + result.failurePhase() + + " error=" + result.errorSummary()); + } else { + completedChunks++; + failedCount++; + Position2 failed = new Position2(result.task().chunkX(), result.task().chunkZ()); + failedChunks.add(failed); + String failurePhase = result.failurePhase() == null || result.failurePhase().isBlank() + ? "unknown" + : result.failurePhase(); + failurePhaseCounts.merge(failurePhase, 1, Integer::sum); + Iris.warn("Regen chunk failed terminally: id=" + runId + + " chunk=" + result.task().chunkX() + "," + result.task().chunkZ() + + " attempts=" + result.task().attempt() + + " phase=" + failurePhase + + " error=" + result.errorSummary()); + if (result.error() != null) { + Iris.reportError(result.error()); + } + } + + while (inFlight < pool.getMaximumPoolSize() && !pending.isEmpty()) { + RegenChunkTask task = pending.removeFirst(); + completion.submit(() -> runRegenChunk(task, world, platform, executor, activeTasks, mode, runId, lastSignalMs)); + inFlight++; + submittedTasks++; + } + + String phaseSummary = summarizeActivePhases(activeTasks); + lastProgressUiMs = updateRegenProgressAction( + sender, + display, + mode, + passIndex, + passCount, + completedChunks, + totalChunks, + inFlight, + pending.size(), + false, + false, + false, + false, + phaseSummary.equals("idle") ? "Generating chunks" : "Generating chunks in " + phaseSummary, + lastProgressUiMs + ); + } + + long runtimeMs = System.currentTimeMillis() - runStart; + String preview = formatFailedChunkPreview(failedChunks); + String failurePhaseSummary = formatFailurePhaseSummary(failurePhaseCounts); + Iris.info("Regen run complete: id=" + runId + + " world=" + world.getName() + + " total=" + totalChunks + + " success=" + successChunks + + " failed=" + failedCount + + " retries=" + retryCount + + " submittedTasks=" + submittedTasks + + " finishedTasks=" + finishedTasks + + " overlayChunks=" + overlayChunks + + " overlayObjectChunks=" + overlayObjectChunks + + " overlayBlocks=" + overlayBlocks + + " failurePhases=" + failurePhaseSummary + + " runtimeMs=" + runtimeMs + + " failedPreview=" + preview); + updateRegenProgressAction( + sender, + display, + mode, + passIndex, + passCount, + completedChunks, + totalChunks, + inFlight, + pending.size(), + false, + true, + failedCount > 0, + true, + failedCount > 0 ? "Completed with failures in " + failurePhaseSummary : "Pass complete", + lastProgressUiMs + ); + return new RegenSummary(totalChunks, successChunks, failedCount, retryCount, preview, failurePhaseSummary); + } + + private long updateRegenProgressAction( + VolmitSender sender, + RegenDisplay display, + RegenMode mode, + int passIndex, + int passCount, + int completed, + int total, + int inFlight, + int queued, + boolean stalled, + boolean terminal, + boolean failed, + boolean force, + String detail, + long lastUiMs + ) { + if (display == null && !sender.isPlayer()) { + return lastUiMs; + } + + long now = System.currentTimeMillis(); + if (!force && now - lastUiMs < REGEN_PROGRESS_UPDATE_MS) { + return lastUiMs; + } + + int safePassCount = Math.max(1, passCount); + int safePassIndex = Math.max(1, Math.min(passIndex, safePassCount)); + int safeTotal = Math.max(1, total); + int safeCompleted = Math.max(0, Math.min(completed, safeTotal)); + double passProgress = safeCompleted / (double) safeTotal; + double overallProgress = ((safePassIndex - 1) + passProgress) / safePassCount; + int percent = (int) Math.round(overallProgress * 100.0D); + String bar = buildRegenProgressBar(overallProgress); + C statusColor = failed ? C.RED : terminal ? C.GREEN : stalled ? C.RED : C.AQUA; + String statusLabel = failed ? "FAILED" : terminal ? "DONE" : stalled ? "STALLED" : "RUN"; + BarColor bossColor = failed ? BarColor.RED : terminal ? BarColor.GREEN : stalled ? BarColor.RED : BarColor.BLUE; + String title = C.GOLD + "Regen " + mode.id() + + C.GRAY + " " + statusColor + statusLabel + + C.GRAY + " " + C.YELLOW + percent + "%" + + C.DARK_GRAY + " P" + safePassIndex + "/" + safePassCount; + String action = bar + + C.GRAY + " " + C.YELLOW + percent + "%" + + C.DARK_GRAY + " P" + safePassIndex + "/" + safePassCount + + C.DARK_GRAY + " C" + safeCompleted + "/" + safeTotal + + C.DARK_GRAY + " Q" + queued + + C.DARK_GRAY + " F" + inFlight; + if (detail != null && !detail.isBlank()) { + action = action + C.GRAY + " | " + C.WHITE + detail; + } + + if (display != null) { + updateRegenDisplay(display, overallProgress, bossColor, title, action); + return now; + } + + if (sender.isPlayer()) { + String actionText = action; + J.runEntity(sender.player(), () -> sender.sendAction(actionText)); + } + return now; + } + + private static String buildRegenProgressBar(double progress) { + int width = REGEN_PROGRESS_BAR_WIDTH; + int filled = (int) Math.round(Math.max(0.0D, Math.min(1.0D, progress)) * width); + StringBuilder bar = new StringBuilder(width * 3 + 4); + bar.append(C.DARK_GRAY).append("["); + for (int i = 0; i < width; i++) { + bar.append(i < filled ? C.GREEN : C.DARK_GRAY).append("|"); + } + bar.append(C.DARK_GRAY).append("]"); + return bar.toString(); + } + + private RegenDisplay createRegenDisplay(VolmitSender sender, RegenMode mode) { + if (!sender.isPlayer()) { + return null; + } + + Player player = sender.player(); + if (player == null) { + return null; + } + + BossBar bossBar = Bukkit.createBossBar(C.GOLD + "Regen " + mode.id(), BarColor.BLUE, BarStyle.SEGMENTED_20); + bossBar.setProgress(0.0D); + bossBar.addPlayer(player); + bossBar.setVisible(true); + RegenDisplay display = new RegenDisplay(sender, bossBar); + String title = C.GOLD + "Regen " + mode.id() + C.GRAY + " " + C.AQUA + "RUN" + C.GRAY + " " + C.YELLOW + "0%"; + String action = buildRegenProgressBar(0.0D) + C.GRAY + " " + C.YELLOW + "0%" + C.GRAY + " | " + C.WHITE + "Preparing setup"; + updateRegenDisplay(display, 0.0D, BarColor.BLUE, title, action); + pulseRegenDisplay(display); + return display; + } + + private void updateRegenSetupDisplay(RegenDisplay display, RegenMode mode, String phase, int step, int totalSteps) { + if (display == null || display.closed.get()) { + return; + } + + int safeTotalSteps = Math.max(1, totalSteps); + int safeStep = Math.max(0, Math.min(step, safeTotalSteps)); + double setupProgress = Math.max(0.0D, Math.min(0.1D, (safeStep / (double) safeTotalSteps) * 0.1D)); + int percent = (int) Math.round(setupProgress * 100.0D); + String title = C.GOLD + "Regen " + mode.id() + C.GRAY + " " + C.AQUA + "SETUP" + C.GRAY + " " + C.YELLOW + percent + "%"; + String action = buildRegenProgressBar(setupProgress) + + C.GRAY + " " + C.YELLOW + percent + "%" + + C.GRAY + " | " + C.WHITE + phase; + updateRegenDisplay(display, setupProgress, BarColor.BLUE, title, action); + } + + private void updateRegenDisplay(RegenDisplay display, double progress, BarColor color, String title, String action) { + if (display == null || display.closed.get()) { + return; + } + + display.progress = Math.max(0.0D, Math.min(1.0D, progress)); + display.color = color == null ? BarColor.BLUE : color; + display.title = title == null ? "" : title; + display.actionLine = action == null ? "" : action; + + Player player = display.sender.player(); + if (player == null) { + closeRegenDisplay(display, 0); + return; + } + + boolean scheduled = J.runEntity(player, () -> { + if (display.closed.get()) { + return; + } + + display.bossBar.setProgress(display.progress); + display.bossBar.setColor(display.color); + display.bossBar.setTitle(display.title); + if (!display.actionLine.isBlank()) { + display.sender.sendAction(display.actionLine); + } + }); + if (!scheduled) { + closeRegenDisplay(display, 0); + } + } + + private void pulseRegenDisplay(RegenDisplay display) { + if (display == null || display.closed.get()) { + return; + } + + Player player = display.sender.player(); + if (player == null) { + closeRegenDisplay(display, 0); + return; + } + + boolean scheduled = J.runEntity(player, () -> { + if (display.closed.get()) { + return; + } + + Player activePlayer = display.sender.player(); + if (activePlayer == null || !activePlayer.isOnline()) { + closeRegenDisplay(display, 0); + return; + } + + if (!display.actionLine.isBlank()) { + display.sender.sendAction(display.actionLine); + } + pulseRegenDisplay(display); + }, REGEN_ACTION_PULSE_TICKS); + + if (!scheduled) { + closeRegenDisplay(display, 0); + } + } + + private void completeRegenDisplay(RegenDisplay display, RegenMode mode, boolean failed, String detail) { + if (display == null || display.closed.get()) { + return; + } + + double progress = failed ? Math.max(0.0D, Math.min(1.0D, display.progress)) : 1.0D; + int percent = (int) Math.round(progress * 100.0D); + BarColor color = failed ? BarColor.RED : BarColor.GREEN; + String status = failed ? C.RED + "FAILED" : C.GREEN + "DONE"; + String title = C.GOLD + "Regen " + mode.id() + C.GRAY + " " + status + C.GRAY + " " + C.YELLOW + percent + "%"; + String action = buildRegenProgressBar(progress) + C.GRAY + " " + C.YELLOW + percent + "%"; + if (detail != null && !detail.isBlank()) { + action = action + C.GRAY + " | " + C.WHITE + detail; + } + + updateRegenDisplay(display, progress, color, title, action); + closeRegenDisplay(display, REGEN_DISPLAY_FINAL_TICKS); + } + + private void closeRegenDisplay(RegenDisplay display, int delayTicks) { + if (display == null || display.closed.get()) { + return; + } + + Player player = display.sender.player(); + Runnable closeTask = () -> { + if (!display.closed.compareAndSet(false, true)) { + return; + } + + display.bossBar.removeAll(); + display.bossBar.setVisible(false); + display.sender.sendAction(" "); + }; + + if (player == null) { + display.closed.set(true); + return; + } + + boolean scheduled = delayTicks > 0 + ? J.runEntity(player, closeTask, delayTicks) + : J.runEntity(player, closeTask); + if (!scheduled) { + display.closed.set(true); + } + } + + private RegenChunkResult runRegenChunk( + RegenChunkTask task, + World world, + PlatformChunkGenerator platform, + SyncExecutor executor, + ConcurrentMap activeTasks, + RegenMode mode, + String runId, + AtomicLong lastSignalMs + ) { + String worker = Thread.currentThread().getName(); + long startedAt = System.currentTimeMillis(); + boolean loadedAtStart = false; + try { + loadedAtStart = world.isChunkLoaded(task.chunkX(), task.chunkZ()); + } catch (Throwable ignored) { + } + + RegenActiveTask activeTask = new RegenActiveTask(task.chunkX(), task.chunkZ(), task.attempt(), startedAt, loadedAtStart); + activeTasks.put(worker, activeTask); + AtomicReference failurePhase = new AtomicReference<>("unknown"); + AtomicInteger overlayAppliedBlocks = new AtomicInteger(); + AtomicInteger overlayObjectKeys = new AtomicInteger(); + ChunkReplacementListener listener = new ChunkReplacementListener() { + @Override + public void onPhase(String phase, int chunkX, int chunkZ, long timestampMs) { + activeTask.updatePhase(phase, timestampMs); + lastSignalMs.set(timestampMs); + } + + @Override + public void onOverlay(int chunkX, int chunkZ, int appliedBlocks, int objectKeys, long timestampMs) { + overlayAppliedBlocks.addAndGet(appliedBlocks); + overlayObjectKeys.addAndGet(objectKeys); + activeTask.updatePhase("overlay", timestampMs); + lastSignalMs.set(timestampMs); + } + + @Override + public void onFailurePhase(String phase, int chunkX, int chunkZ, Throwable error, long timestampMs) { + String classifiedPhase = classifyRegenFailurePhase(phase); + failurePhase.set(classifiedPhase); + activeTask.updatePhase(classifiedPhase, timestampMs); + lastSignalMs.set(timestampMs); + } + }; + try { + if (mode.logChunkDiagnostics()) { + Iris.info("Regen chunk start: id=" + runId + + " chunk=" + task.chunkX() + "," + task.chunkZ() + + " attempt=" + task.attempt() + + " loadedAtStart=" + loadedAtStart + + " worker=" + worker); + } + ChunkReplacementOptions options = mode == RegenMode.FULL + ? ChunkReplacementOptions.full(runId, mode.logChunkDiagnostics()) + : ChunkReplacementOptions.terrain(runId, mode.logChunkDiagnostics()); + RegenRuntime.setRunId(runId); + try { + platform.injectChunkReplacement(world, task.chunkX(), task.chunkZ(), executor, options, listener); + } finally { + RegenRuntime.clear(); + } + if (mode.logChunkDiagnostics()) { + Iris.info("Regen chunk end: id=" + runId + + " chunk=" + task.chunkX() + "," + task.chunkZ() + + " attempt=" + task.attempt() + + " worker=" + worker + + " durationMs=" + (System.currentTimeMillis() - startedAt)); + } + long finishedAt = System.currentTimeMillis(); + activeTask.updateHeartbeat(finishedAt); + lastSignalMs.set(finishedAt); + return RegenChunkResult.success(task, worker, startedAt, finishedAt, loadedAtStart, overlayAppliedBlocks.get(), overlayObjectKeys.get()); + } catch (Throwable e) { + if (e instanceof InterruptedException) { + Thread.currentThread().interrupt(); + } + long finishedAt = System.currentTimeMillis(); + String classifiedPhase = classifyRegenFailurePhase(failurePhase.get()); + if ("unknown".equals(classifiedPhase)) { + classifiedPhase = classifyRegenFailurePhase(activeTask.phase()); + } + activeTask.updatePhase(classifiedPhase, finishedAt); + activeTask.updateHeartbeat(finishedAt); + lastSignalMs.set(finishedAt); + return RegenChunkResult.failure( + task, + worker, + startedAt, + finishedAt, + loadedAtStart, + classifiedPhase, + overlayAppliedBlocks.get(), + overlayObjectKeys.get(), + e + ); + } finally { + activeTasks.remove(worker); + } + } + + private Thread createRegenSetupWatchdog( + World world, + String runId, + AtomicBoolean setupDone, + AtomicReference setupPhase, + AtomicLong setupPhaseSince + ) { + String setupWatchdogName = "Iris-Regen-SetupWatchdog-" + runId; + Thread setupWatchdog = new Thread(() -> { + while (!setupDone.get()) { + try { + Thread.sleep(REGEN_HEARTBEAT_MS); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + return; + } + + if (!setupDone.get()) { + long elapsed = System.currentTimeMillis() - setupPhaseSince.get(); + Iris.warn("Regen setup heartbeat: id=" + runId + + " phase=" + setupPhase.get() + + " elapsedMs=" + elapsed + + " world=" + world.getName()); + } + } + }, setupWatchdogName); + setupWatchdog.setDaemon(true); + return setupWatchdog; + } + + private void setRegenSetupPhase( + AtomicReference setupPhase, + AtomicLong setupPhaseSince, + String nextPhase, + World world, + String runId + ) { + setupPhase.set(nextPhase); + setupPhaseSince.set(System.currentTimeMillis()); + if (IrisSettings.get().getGeneral().isDebug()) { + Iris.info("Regen setup phase: id=" + runId + " phase=" + nextPhase + " world=" + world.getName()); + } + } + + private static String formatFailedChunkPreview(List failedChunks) { + if (failedChunks.isEmpty()) { + return "[]"; + } + + StringBuilder builder = new StringBuilder("["); + int index = 0; + for (Position2 chunk : failedChunks) { + if (index > 0) { + builder.append(", "); + } + if (index >= 10) { + builder.append("..."); + break; + } + builder.append(chunk.getX()).append(",").append(chunk.getZ()); + index++; + } + builder.append("]"); + return builder.toString(); + } + + private static String summarizeActivePhases(ConcurrentMap activeTasks) { + if (activeTasks.isEmpty()) { + return "idle"; + } + + Map counts = new HashMap<>(); + for (RegenActiveTask activeTask : activeTasks.values()) { + String phase = classifyRegenFailurePhase(activeTask.phase()); + counts.merge(phase, 1, Integer::sum); + } + if (counts.isEmpty()) { + return "idle"; + } + + List> entries = new ArrayList<>(counts.entrySet()); + entries.sort((a, b) -> { + int diff = Integer.compare(b.getValue(), a.getValue()); + if (diff != 0) { + return diff; + } + return a.getKey().compareTo(b.getKey()); + }); + + StringBuilder builder = new StringBuilder(); + int emitted = 0; + for (Map.Entry entry : entries) { + if (emitted > 0) { + builder.append(", "); + } + if (emitted >= 3) { + builder.append("..."); + break; + } + builder.append(entry.getKey()).append(" x").append(entry.getValue()); + emitted++; + } + return builder.toString(); + } + + private static String formatFailurePhaseSummary(Map failurePhaseCounts) { + if (failurePhaseCounts.isEmpty()) { + return "none"; + } + + List> entries = new ArrayList<>(failurePhaseCounts.entrySet()); + entries.sort((a, b) -> { + int diff = Integer.compare(b.getValue(), a.getValue()); + if (diff != 0) { + return diff; + } + return a.getKey().compareTo(b.getKey()); + }); + + StringBuilder builder = new StringBuilder(); + int emitted = 0; + for (Map.Entry entry : entries) { + if (emitted > 0) { + builder.append(", "); + } + if (emitted >= 5) { + builder.append("..."); + break; + } + builder.append(entry.getKey()).append("=").append(entry.getValue()); + emitted++; + } + return builder.toString(); + } + + private static String classifyRegenFailurePhase(String phase) { + if (phase == null || phase.isBlank()) { + return "unknown"; + } + + String normalized = phase.toLowerCase(Locale.ROOT); + if (normalized.contains("generate")) { + return "generate"; + } + if (normalized.contains("acquire-load-lock") || normalized.contains("reset-mantle")) { + return "generate"; + } + if (normalized.contains("apply-terrain") || normalized.contains("folia-region-run")) { + return "apply-terrain"; + } + if (normalized.contains("paperlib-async-load") || normalized.contains("folia-run-region")) { + return "apply-terrain"; + } + if (normalized.contains("overlay")) { + return "overlay"; + } + if (normalized.contains("structure")) { + return "structures"; + } + if (normalized.contains("chunk-load-callback")) { + return "chunk-load-callback"; + } + return "unknown"; + } + + private static String formatActiveTasks(ConcurrentMap activeTasks) { + if (activeTasks.isEmpty()) { + return "{}"; + } + + StringBuilder builder = new StringBuilder("{"); + int count = 0; + long now = System.currentTimeMillis(); + for (Map.Entry entry : activeTasks.entrySet()) { + if (count > 0) { + builder.append(", "); + } + if (count >= 8) { + builder.append("..."); + break; + } + RegenActiveTask activeTask = entry.getValue(); + builder.append(entry.getKey()) + .append("=") + .append(activeTask.chunkX()) + .append(",") + .append(activeTask.chunkZ()) + .append("@") + .append(activeTask.attempt()) + .append("/") + .append(now - activeTask.startedAtMs()) + .append("ms") + .append(":") + .append(classifyRegenFailurePhase(activeTask.phase())) + .append("/") + .append(now - activeTask.lastHeartbeatMs()) + .append("ms") + .append(activeTask.loadedAtStart() ? ":loaded" : ":cold"); + count++; + } + builder.append("}"); + return builder.toString(); + } + + private static void dumpRegenWorkerStacks(Set explicitThreads, String worldName) { + Set threads = new LinkedHashSet<>(); + threads.addAll(explicitThreads); + for (Thread thread : Thread.getAllStackTraces().keySet()) { + if (thread == null || !thread.isAlive()) { + continue; + } + + String name = thread.getName(); + if (name.startsWith("Iris-Regen-") + || name.startsWith("Iris EngineSVC-") + || name.startsWith("Iris World Manager") + || name.contains(worldName)) { + threads.add(thread); + } + } + + for (Thread thread : threads) { + if (thread == null || !thread.isAlive()) { + continue; + } + + Iris.warn("Regen worker thread=" + thread.getName() + " state=" + thread.getState()); + StackTraceElement[] trace = thread.getStackTrace(); + int limit = Math.min(trace.length, REGEN_STACK_LIMIT); + for (int i = 0; i < limit; i++) { + Iris.warn(" at " + trace[i]); + } + } + } + + private static final class RegenDisplay { + private final VolmitSender sender; + private final BossBar bossBar; + private final AtomicBoolean closed = new AtomicBoolean(false); + private volatile String title = ""; + private volatile String actionLine = ""; + private volatile double progress = 0.0D; + private volatile BarColor color = BarColor.BLUE; + + private RegenDisplay(VolmitSender sender, BossBar bossBar) { + this.sender = sender; + this.bossBar = bossBar; + } + } + + private record RegenChunkTask(int chunkX, int chunkZ, int attempt, long queuedAtMs) { + private RegenChunkTask retry(long now) { + return new RegenChunkTask(chunkX, chunkZ, attempt + 1, now); + } + } + + private enum RegenMode { + TERRAIN("terrain", true, false, false, false), + FULL("full", true, false, true, true); + + private final String id; + private final boolean usesMaintenance; + private final boolean bypassMantleStages; + private final boolean fullMode; + private final boolean logChunkDiagnostics; + + RegenMode( + String id, + boolean usesMaintenance, + boolean bypassMantleStages, + boolean fullMode, + boolean logChunkDiagnostics + ) { + this.id = id; + this.usesMaintenance = usesMaintenance; + this.bypassMantleStages = bypassMantleStages; + this.fullMode = fullMode; + this.logChunkDiagnostics = logChunkDiagnostics; + } + + private String id() { + return id; + } + + private boolean usesMaintenance() { + return usesMaintenance; + } + + private boolean bypassMantleStages() { + return bypassMantleStages; + } + + private boolean isFullMode() { + return fullMode; + } + + private int passCount() { + return 1; + } + + private boolean logChunkDiagnostics() { + return logChunkDiagnostics && IrisSettings.get().getGeneral().isDebug(); + } + + private static RegenMode parse(String raw) { + if (raw == null) { + return FULL; + } + + String normalized = raw.trim(); + if (normalized.isEmpty()) { + return FULL; + } + + for (RegenMode mode : values()) { + if (mode.id.equalsIgnoreCase(normalized)) { + return mode; + } + } + return null; + } + } + + private static final class RegenActiveTask { + private final int chunkX; + private final int chunkZ; + private final int attempt; + private final long startedAtMs; + private final boolean loadedAtStart; + private volatile String phase; + private volatile long lastHeartbeatMs; + + private RegenActiveTask(int chunkX, int chunkZ, int attempt, long startedAtMs, boolean loadedAtStart) { + this.chunkX = chunkX; + this.chunkZ = chunkZ; + this.attempt = attempt; + this.startedAtMs = startedAtMs; + this.loadedAtStart = loadedAtStart; + this.phase = "queued"; + this.lastHeartbeatMs = startedAtMs; + } + + private void updatePhase(String nextPhase, long timestampMs) { + this.phase = nextPhase == null || nextPhase.isBlank() ? "unknown" : nextPhase; + this.lastHeartbeatMs = timestampMs; + } + + private void updateHeartbeat(long timestampMs) { + this.lastHeartbeatMs = timestampMs; + } + + private int chunkX() { + return chunkX; + } + + private int chunkZ() { + return chunkZ; + } + + private int attempt() { + return attempt; + } + + private long startedAtMs() { + return startedAtMs; + } + + private boolean loadedAtStart() { + return loadedAtStart; + } + + private String phase() { + return phase; + } + + private long lastHeartbeatMs() { + return lastHeartbeatMs; + } + } + + private record RegenChunkResult( + RegenChunkTask task, + String worker, + long startedAtMs, + long finishedAtMs, + boolean loadedAtStart, + String failurePhase, + int overlayAppliedBlocks, + int overlayObjectKeys, + boolean success, + Throwable error + ) { + private static RegenChunkResult success( + RegenChunkTask task, + String worker, + long startedAtMs, + long finishedAtMs, + boolean loadedAtStart, + int overlayAppliedBlocks, + int overlayObjectKeys + ) { + return new RegenChunkResult( + task, + worker, + startedAtMs, + finishedAtMs, + loadedAtStart, + "none", + overlayAppliedBlocks, + overlayObjectKeys, + true, + null + ); + } + + private static RegenChunkResult failure( + RegenChunkTask task, + String worker, + long startedAtMs, + long finishedAtMs, + boolean loadedAtStart, + String failurePhase, + int overlayAppliedBlocks, + int overlayObjectKeys, + Throwable error + ) { + return new RegenChunkResult( + task, + worker, + startedAtMs, + finishedAtMs, + loadedAtStart, + failurePhase == null || failurePhase.isBlank() ? "unknown" : failurePhase, + overlayAppliedBlocks, + overlayObjectKeys, + false, + error + ); + } + + private String errorSummary() { + if (error == null) { + return "unknown"; + } + String message = error.getMessage(); + if (message == null || message.isEmpty()) { + return error.getClass().getSimpleName(); + } + return error.getClass().getSimpleName() + ": " + message; + } + } + + private record RegenSummary( + int totalChunks, + int successChunks, + int failedChunks, + int retryCount, + String failedPreview, + String failurePhaseSummary + ) { + } } diff --git a/core/src/main/java/art/arcane/iris/core/commands/CommandIris.java b/core/src/main/java/art/arcane/iris/core/commands/CommandIris.java index fb34df604..e9518d4c4 100644 --- a/core/src/main/java/art/arcane/iris/core/commands/CommandIris.java +++ b/core/src/main/java/art/arcane/iris/core/commands/CommandIris.java @@ -49,7 +49,6 @@ import art.arcane.volmlib.util.io.IO; import art.arcane.volmlib.util.math.Position2; import art.arcane.iris.util.common.parallel.SyncExecutor; import art.arcane.iris.util.common.misc.ServerProperties; -import art.arcane.iris.util.common.misc.RegenRuntime; import art.arcane.iris.util.common.plugin.VolmitSender; import art.arcane.iris.util.common.scheduling.J; import org.bukkit.Bukkit; @@ -92,17 +91,6 @@ import static org.bukkit.Bukkit.getServer; @Director(name = "iris", aliases = {"ir", "irs"}, description = "Basic Command") public class CommandIris implements DirectorExecutor { - private static final long REGEN_HEARTBEAT_MS = 5000L; - private static final int REGEN_MAX_ATTEMPTS = 2; - private static final int REGEN_STACK_LIMIT = 20; - private static final long REGEN_STALL_DUMP_IDLE_MS = 30000L; - private static final long REGEN_STALL_ABORT_IDLE_MS = 600000L; - private static final long REGEN_STACK_DUMP_INTERVAL_MS = 10000L; - private static final int REGEN_PROGRESS_BAR_WIDTH = 44; - private static final long REGEN_PROGRESS_UPDATE_MS = 200L; - private static final int REGEN_ACTION_PULSE_TICKS = 20; - private static final int REGEN_DISPLAY_FINAL_TICKS = 60; - private CommandUpdater updater; private CommandStudio studio; private CommandPregen pregen; private CommandSettings settings; @@ -849,1287 +837,6 @@ public class CommandIris implements DirectorExecutor { sender().sendMessage(C.GREEN + "Hotloaded settings"); } - @Director(name = "regen", aliases = {"rg"}, description = "Regenerate nearby chunks using Iris generation", origin = DirectorOrigin.PLAYER, sync = true) - public void regen( - @Param(name = "radius", description = "The radius of nearby chunks", defaultValue = "5") - int radius, - @Param(name = "parallelism", aliases = {"threads", "concurrency"}, description = "How many chunks to regenerate in parallel (0 = auto)", defaultValue = "0") - int parallelism, - @Param(name = "mode", aliases = {"scope", "profile"}, description = "Regen mode: terrain or full", defaultValue = "full") - String mode - ) { - if (radius < 0) { - sender().sendMessage(C.RED + "Radius must be 0 or greater."); - return; - } - - World world = player().getWorld(); - if (!IrisToolbelt.isIrisWorld(world)) { - sender().sendMessage(C.RED + "You must be in an Iris world to use regen."); - return; - } - - RegenMode regenMode = RegenMode.parse(mode); - if (regenMode == null) { - sender().sendMessage(C.RED + "Unknown regen mode \"" + mode + "\". Use mode=terrain or mode=full."); - return; - } - - VolmitSender sender = sender(); - int centerX = player().getLocation().getBlockX() >> 4; - int centerZ = player().getLocation().getBlockZ() >> 4; - int threadCount = resolveRegenThreadCount(parallelism, regenMode); - List targets = buildRegenTargets(centerX, centerZ, radius); - int chunks = targets.size(); - String runId = world.getName() + "-" + System.currentTimeMillis(); - RegenDisplay display = createRegenDisplay(sender, regenMode); - - sender.sendMessage(C.GREEN + "Regen started (" + C.GOLD + regenMode.id() + C.GREEN + "): " - + C.GOLD + chunks + C.GREEN + " chunks, " - + C.GOLD + threadCount + C.GREEN + " worker(s). " - + C.GRAY + "Progress is shown on-screen."); - if (regenMode == RegenMode.TERRAIN) { - Iris.warn("Regen running in terrain mode; mantle overlay/object replay is skipped. Use mode=full to regenerate objects."); - } - - Iris.info("Regen run start: id=" + runId - + " world=" + world.getName() - + " center=" + centerX + "," + centerZ - + " radius=" + radius - + " mode=" + regenMode.id() - + " workers=" + threadCount - + " chunks=" + chunks); - if (IrisSettings.get().getGeneral().isDebug()) { - Iris.info("Regen mode config: id=" + runId - + " mode=" + regenMode.id() - + " maintenance=" + regenMode.usesMaintenance() - + " bypassMantle=" + regenMode.bypassMantleStages() - + " passes=" + regenMode.passCount() - + " fullMode=" + regenMode.isFullMode() - + " diagnostics=" + regenMode.logChunkDiagnostics()); - } - - String orchestratorName = "Iris-Regen-Orchestrator-" + runId; - Thread orchestrator = new Thread(() -> runRegenOrchestrator(sender, world, targets, threadCount, regenMode, runId, display), orchestratorName); - orchestrator.setDaemon(true); - try { - orchestrator.start(); - if (IrisSettings.get().getGeneral().isDebug()) { - Iris.info("Regen worker dispatched on dedicated thread=" + orchestratorName + " id=" + runId + "."); - } - } catch (Throwable e) { - sender.sendMessage(C.RED + "Failed to start regen worker thread. See console."); - closeRegenDisplay(display, 0); - Iris.reportError(e); - } - } - - private int resolveRegenThreadCount(int parallelism, RegenMode mode) { - if (parallelism > 0) { - return Math.max(1, Math.min(8, parallelism)); - } - if (J.isFolia()) { - return 1; - } - int cpus = Runtime.getRuntime().availableProcessors(); - if (mode == RegenMode.TERRAIN) { - return Math.min(Math.max(1, cpus / 2), 4); - } - return Math.min(Math.max(1, cpus / 4), 2); - } - - private List buildRegenTargets(int centerX, int centerZ, int radius) { - int expected = (radius * 2 + 1) * (radius * 2 + 1); - List targets = new ArrayList<>(expected); - for (int ring = 0; ring <= radius; ring++) { - for (int x = -ring; x <= ring; x++) { - for (int z = -ring; z <= ring; z++) { - if (Math.max(Math.abs(x), Math.abs(z)) != ring) { - continue; - } - targets.add(new Position2(centerX + x, centerZ + z)); - } - } - } - return targets; - } - - private void runRegenOrchestrator( - VolmitSender sender, - World world, - List targets, - int threadCount, - RegenMode mode, - String runId, - RegenDisplay display - ) { - long runStart = System.currentTimeMillis(); - AtomicBoolean setupDone = new AtomicBoolean(false); - AtomicReference setupPhase = new AtomicReference<>("bootstrap"); - AtomicLong setupPhaseSince = new AtomicLong(runStart); - Thread setupWatchdog = createRegenSetupWatchdog(world, runId, setupDone, setupPhase, setupPhaseSince); - setupWatchdog.start(); - boolean displayTerminal = false; - - Set regenThreads = ConcurrentHashMap.newKeySet(); - AtomicInteger regenThreadCounter = new AtomicInteger(); - ThreadFactory threadFactory = runnable -> { - Thread thread = new Thread(runnable, "Iris-Regen-" + runId + "-" + regenThreadCounter.incrementAndGet()); - thread.setDaemon(true); - regenThreads.add(thread); - return thread; - }; - - try { - setRegenSetupPhase(setupPhase, setupPhaseSince, "touch-context", world, runId); - updateRegenSetupDisplay(display, mode, "Touching command context", 1, 6); - DirectorContext.touch(sender); - if (mode.usesMaintenance()) { - setRegenSetupPhase(setupPhase, setupPhaseSince, "enter-maintenance", world, runId); - updateRegenSetupDisplay(display, mode, "Entering maintenance", 2, 6); - IrisToolbelt.beginWorldMaintenance(world, "regen:" + mode.id(), mode.bypassMantleStages()); - } else { - setRegenSetupPhase(setupPhase, setupPhaseSince, "maintenance-skip", world, runId); - updateRegenSetupDisplay(display, mode, "Skipping maintenance", 2, 6); - } - - ThreadPoolExecutor pool = (ThreadPoolExecutor) Executors.newFixedThreadPool(threadCount, threadFactory); - try (SyncExecutor executor = new SyncExecutor(20)) { - setRegenSetupPhase(setupPhase, setupPhaseSince, "resolve-platform", world, runId); - updateRegenSetupDisplay(display, mode, "Resolving platform", 3, 6); - PlatformChunkGenerator platform = IrisToolbelt.access(world); - setRegenSetupPhase(setupPhase, setupPhaseSince, "validate-engine", world, runId); - updateRegenSetupDisplay(display, mode, "Validating engine", 4, 6); - if (platform == null || platform.getEngine() == null) { - Iris.warn("Regen aborted: engine access is null for world=" + world.getName() + " id=" + runId + "."); - completeRegenDisplay(display, mode, true, C.RED + "Engine access is null. Generate nearby chunks first."); - displayTerminal = true; - return; - } - - setRegenSetupPhase(setupPhase, setupPhaseSince, "prepare-options", world, runId); - updateRegenSetupDisplay(display, mode, "Preparing chunk replacement", 5, 6); - - setRegenSetupPhase(setupPhase, setupPhaseSince, "dispatch", world, runId); - updateRegenSetupDisplay(display, mode, "Dispatching chunk workers", 6, 6); - RegenSummary summary = executeRegenQueue(sender, world, platform, targets, executor, pool, regenThreads, mode, runId, 1, 1, runStart, display); - - if (summary == null) { - completeRegenDisplay(display, mode, true, C.RED + "Regen failed before pass execution."); - displayTerminal = true; - return; - } - - long totalRuntime = System.currentTimeMillis() - runStart; - if (summary.failedChunks() <= 0) { - completeRegenDisplay(display, mode, false, C.GREEN + "Complete " + C.GOLD + summary.successChunks() - + C.GREEN + "/" + C.GOLD + summary.totalChunks() + C.GREEN + " in " + C.GOLD + totalRuntime + "ms"); - displayTerminal = true; - return; - } - - String failureDetail = C.RED + "Failed chunks " + C.GOLD + summary.failedChunks() + C.RED - + ", retries " + C.GOLD + summary.retryCount() - + C.RED + ", runtime " + C.GOLD + totalRuntime + "ms"; - if (summary.failurePhaseSummary() != null && !summary.failurePhaseSummary().isBlank() && !"none".equals(summary.failurePhaseSummary())) { - failureDetail = failureDetail + C.DARK_GRAY + " [phase " + summary.failurePhaseSummary() + "]"; - } - if (!summary.failedPreview().isEmpty()) { - failureDetail = failureDetail + C.DARK_GRAY + " [" + summary.failedPreview() + "]"; - } - completeRegenDisplay(display, mode, true, failureDetail); - displayTerminal = true; - } finally { - pool.shutdownNow(); - } - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - completeRegenDisplay(display, mode, true, C.RED + "Regen interrupted."); - displayTerminal = true; - Iris.warn("Regen run interrupted: id=" + runId + " world=" + world.getName()); - } catch (Throwable e) { - String failureDetail = C.RED + "Regen failed. Check console."; - if (e.getMessage() != null && e.getMessage().contains("stalled")) { - failureDetail = C.RED + "Regen stalled. Try smaller radius or terrain mode."; - } - completeRegenDisplay(display, mode, true, failureDetail); - displayTerminal = true; - Iris.reportError(e); - e.printStackTrace(); - } finally { - setupDone.set(true); - setupWatchdog.interrupt(); - if (mode.usesMaintenance()) { - IrisToolbelt.endWorldMaintenance(world, "regen:" + mode.id()); - } - if (!displayTerminal) { - closeRegenDisplay(display, REGEN_DISPLAY_FINAL_TICKS); - } - DirectorContext.remove(); - Iris.info("Regen run closed: id=" + runId + " world=" + world.getName() + " totalMs=" + (System.currentTimeMillis() - runStart)); - } - } - - private RegenSummary executeRegenQueue( - VolmitSender sender, - World world, - PlatformChunkGenerator platform, - List targets, - SyncExecutor executor, - ThreadPoolExecutor pool, - Set regenThreads, - RegenMode mode, - String runId, - int passIndex, - int passCount, - long runStart, - RegenDisplay display - ) throws InterruptedException { - ArrayDeque pending = new ArrayDeque<>(targets.size()); - long queueTime = System.currentTimeMillis(); - for (Position2 target : targets) { - pending.addLast(new RegenChunkTask(target.getX(), target.getZ(), 1, queueTime)); - } - - ConcurrentMap activeTasks = new ConcurrentHashMap<>(); - ExecutorCompletionService completion = new ExecutorCompletionService<>(pool); - List failedChunks = new ArrayList<>(); - Map failurePhaseCounts = new HashMap<>(); - - int totalChunks = targets.size(); - int successChunks = 0; - int failedCount = 0; - int retryCount = 0; - int overlayChunks = 0; - int overlayObjectChunks = 0; - int overlayBlocks = 0; - long submittedTasks = 0L; - long finishedTasks = 0L; - int completedChunks = 0; - int inFlight = 0; - AtomicLong lastSignalMs = new AtomicLong(System.currentTimeMillis()); - long lastDump = 0L; - long lastProgressUiMs = 0L; - lastProgressUiMs = updateRegenProgressAction( - sender, - display, - mode, - passIndex, - passCount, - completedChunks, - totalChunks, - inFlight, - pending.size(), - false, - false, - false, - true, - "Queue initialized", - lastProgressUiMs - ); - - while (inFlight < pool.getMaximumPoolSize() && !pending.isEmpty()) { - RegenChunkTask task = pending.removeFirst(); - completion.submit(() -> runRegenChunk(task, world, platform, executor, activeTasks, mode, runId, lastSignalMs)); - inFlight++; - submittedTasks++; - } - - while (completedChunks < totalChunks) { - Future future = completion.poll(REGEN_HEARTBEAT_MS, TimeUnit.MILLISECONDS); - if (future == null) { - long now = System.currentTimeMillis(); - long idleMs = Math.max(0L, now - lastSignalMs.get()); - boolean stalled = idleMs >= REGEN_HEARTBEAT_MS; - String phaseSummary = summarizeActivePhases(activeTasks); - - Iris.warn("Regen heartbeat: id=" + runId - + " completed=" + completedChunks + "/" + totalChunks - + " remaining=" + (totalChunks - completedChunks) - + " queued=" + pending.size() - + " inFlight=" + inFlight - + " submitted=" + submittedTasks - + " finishedTasks=" + finishedTasks - + " retries=" + retryCount - + " failed=" + failedCount - + " poolActive=" + pool.getActiveCount() - + " poolQueue=" + pool.getQueue().size() - + " poolDone=" + pool.getCompletedTaskCount() - + " idleMs=" + idleMs - + " phases=" + phaseSummary - + " activeTasks=" + formatActiveTasks(activeTasks)); - lastProgressUiMs = updateRegenProgressAction( - sender, - display, - mode, - passIndex, - passCount, - completedChunks, - totalChunks, - inFlight, - pending.size(), - stalled, - false, - false, - true, - stalled ? "Waiting in phase " + phaseSummary : "Waiting for chunk result", - lastProgressUiMs - ); - - if (idleMs >= REGEN_STALL_DUMP_IDLE_MS && now - lastDump >= REGEN_STACK_DUMP_INTERVAL_MS) { - lastDump = now; - Iris.warn("Regen appears stalled; dumping worker stack traces for id=" + runId + "."); - dumpRegenWorkerStacks(regenThreads, world.getName()); - } - if (idleMs >= REGEN_STALL_ABORT_IDLE_MS) { - updateRegenProgressAction( - sender, - display, - mode, - passIndex, - passCount, - completedChunks, - totalChunks, - inFlight, - pending.size(), - true, - true, - true, - true, - "Stalled in phase " + phaseSummary, - lastProgressUiMs - ); - throw new IllegalStateException("Regen stalled with no chunk heartbeat or completion for " - + idleMs - + "ms (id=" + runId - + ", mode=" + mode.id() - + ", completed=" + completedChunks - + "/" + totalChunks - + ", inFlight=" + inFlight - + ", queued=" + pending.size() - + ", phase=" + phaseSummary - + ")."); - } - continue; - } - - RegenChunkResult result; - try { - result = future.get(); - } catch (ExecutionException e) { - Throwable cause = e.getCause() == null ? e : e.getCause(); - throw new IllegalStateException("Regen worker failed unexpectedly for run " + runId, cause); - } - - inFlight--; - finishedTasks++; - long duration = result.finishedAtMs() - result.startedAtMs(); - lastSignalMs.set(System.currentTimeMillis()); - - if (result.success()) { - completedChunks++; - successChunks++; - if (result.overlayAppliedBlocks() > 0) { - overlayChunks++; - } - if (result.overlayObjectKeys() > 0) { - overlayObjectChunks++; - } - overlayBlocks += result.overlayAppliedBlocks(); - if (result.task().attempt() > 1) { - Iris.warn("Regen chunk recovered after retry: id=" + runId - + " chunk=" + result.task().chunkX() + "," + result.task().chunkZ() - + " attempt=" + result.task().attempt() - + " durationMs=" + duration); - } else if (duration >= 5000L) { - Iris.warn("Regen chunk slow: id=" + runId - + " chunk=" + result.task().chunkX() + "," + result.task().chunkZ() - + " durationMs=" + duration - + " loadedAtStart=" + result.loadedAtStart()); - } - } else if (result.task().attempt() < REGEN_MAX_ATTEMPTS) { - retryCount++; - RegenChunkTask retryTask = result.task().retry(System.currentTimeMillis()); - pending.addLast(retryTask); - Iris.warn("Regen chunk retry scheduled: id=" + runId - + " chunk=" + result.task().chunkX() + "," + result.task().chunkZ() - + " failedAttempt=" + result.task().attempt() - + " nextAttempt=" + retryTask.attempt() - + " phase=" + result.failurePhase() - + " error=" + result.errorSummary()); - } else { - completedChunks++; - failedCount++; - Position2 failed = new Position2(result.task().chunkX(), result.task().chunkZ()); - failedChunks.add(failed); - String failurePhase = result.failurePhase() == null || result.failurePhase().isBlank() - ? "unknown" - : result.failurePhase(); - failurePhaseCounts.merge(failurePhase, 1, Integer::sum); - Iris.warn("Regen chunk failed terminally: id=" + runId - + " chunk=" + result.task().chunkX() + "," + result.task().chunkZ() - + " attempts=" + result.task().attempt() - + " phase=" + failurePhase - + " error=" + result.errorSummary()); - if (result.error() != null) { - Iris.reportError(result.error()); - } - } - - while (inFlight < pool.getMaximumPoolSize() && !pending.isEmpty()) { - RegenChunkTask task = pending.removeFirst(); - completion.submit(() -> runRegenChunk(task, world, platform, executor, activeTasks, mode, runId, lastSignalMs)); - inFlight++; - submittedTasks++; - } - - String phaseSummary = summarizeActivePhases(activeTasks); - lastProgressUiMs = updateRegenProgressAction( - sender, - display, - mode, - passIndex, - passCount, - completedChunks, - totalChunks, - inFlight, - pending.size(), - false, - false, - false, - false, - phaseSummary.equals("idle") ? "Generating chunks" : "Generating chunks in " + phaseSummary, - lastProgressUiMs - ); - } - - long runtimeMs = System.currentTimeMillis() - runStart; - String preview = formatFailedChunkPreview(failedChunks); - String failurePhaseSummary = formatFailurePhaseSummary(failurePhaseCounts); - Iris.info("Regen run complete: id=" + runId - + " world=" + world.getName() - + " total=" + totalChunks - + " success=" + successChunks - + " failed=" + failedCount - + " retries=" + retryCount - + " submittedTasks=" + submittedTasks - + " finishedTasks=" + finishedTasks - + " overlayChunks=" + overlayChunks - + " overlayObjectChunks=" + overlayObjectChunks - + " overlayBlocks=" + overlayBlocks - + " failurePhases=" + failurePhaseSummary - + " runtimeMs=" + runtimeMs - + " failedPreview=" + preview); - updateRegenProgressAction( - sender, - display, - mode, - passIndex, - passCount, - completedChunks, - totalChunks, - inFlight, - pending.size(), - false, - true, - failedCount > 0, - true, - failedCount > 0 ? "Completed with failures in " + failurePhaseSummary : "Pass complete", - lastProgressUiMs - ); - return new RegenSummary(totalChunks, successChunks, failedCount, retryCount, preview, failurePhaseSummary); - } - - private long updateRegenProgressAction( - VolmitSender sender, - RegenDisplay display, - RegenMode mode, - int passIndex, - int passCount, - int completed, - int total, - int inFlight, - int queued, - boolean stalled, - boolean terminal, - boolean failed, - boolean force, - String detail, - long lastUiMs - ) { - if (display == null && !sender.isPlayer()) { - return lastUiMs; - } - - long now = System.currentTimeMillis(); - if (!force && now - lastUiMs < REGEN_PROGRESS_UPDATE_MS) { - return lastUiMs; - } - - int safePassCount = Math.max(1, passCount); - int safePassIndex = Math.max(1, Math.min(passIndex, safePassCount)); - int safeTotal = Math.max(1, total); - int safeCompleted = Math.max(0, Math.min(completed, safeTotal)); - double passProgress = safeCompleted / (double) safeTotal; - double overallProgress = ((safePassIndex - 1) + passProgress) / safePassCount; - int percent = (int) Math.round(overallProgress * 100.0D); - String bar = buildRegenProgressBar(overallProgress); - C statusColor = failed ? C.RED : terminal ? C.GREEN : stalled ? C.RED : C.AQUA; - String statusLabel = failed ? "FAILED" : terminal ? "DONE" : stalled ? "STALLED" : "RUN"; - BarColor bossColor = failed ? BarColor.RED : terminal ? BarColor.GREEN : stalled ? BarColor.RED : BarColor.BLUE; - String title = C.GOLD + "Regen " + mode.id() - + C.GRAY + " " + statusColor + statusLabel - + C.GRAY + " " + C.YELLOW + percent + "%" - + C.DARK_GRAY + " P" + safePassIndex + "/" + safePassCount; - String action = bar - + C.GRAY + " " + C.YELLOW + percent + "%" - + C.DARK_GRAY + " P" + safePassIndex + "/" + safePassCount - + C.DARK_GRAY + " C" + safeCompleted + "/" + safeTotal - + C.DARK_GRAY + " Q" + queued - + C.DARK_GRAY + " F" + inFlight; - if (detail != null && !detail.isBlank()) { - action = action + C.GRAY + " | " + C.WHITE + detail; - } - - if (display != null) { - updateRegenDisplay(display, overallProgress, bossColor, title, action); - return now; - } - - if (sender.isPlayer()) { - String actionText = action; - J.runEntity(sender.player(), () -> sender.sendAction(actionText)); - } - return now; - } - - private static String buildRegenProgressBar(double progress) { - int width = REGEN_PROGRESS_BAR_WIDTH; - int filled = (int) Math.round(Math.max(0.0D, Math.min(1.0D, progress)) * width); - StringBuilder bar = new StringBuilder(width * 3 + 4); - bar.append(C.DARK_GRAY).append("["); - for (int i = 0; i < width; i++) { - bar.append(i < filled ? C.GREEN : C.DARK_GRAY).append("|"); - } - bar.append(C.DARK_GRAY).append("]"); - return bar.toString(); - } - - private RegenDisplay createRegenDisplay(VolmitSender sender, RegenMode mode) { - if (!sender.isPlayer()) { - return null; - } - - Player player = sender.player(); - if (player == null) { - return null; - } - - BossBar bossBar = Bukkit.createBossBar(C.GOLD + "Regen " + mode.id(), BarColor.BLUE, BarStyle.SEGMENTED_20); - bossBar.setProgress(0.0D); - bossBar.addPlayer(player); - bossBar.setVisible(true); - RegenDisplay display = new RegenDisplay(sender, bossBar); - String title = C.GOLD + "Regen " + mode.id() + C.GRAY + " " + C.AQUA + "RUN" + C.GRAY + " " + C.YELLOW + "0%"; - String action = buildRegenProgressBar(0.0D) + C.GRAY + " " + C.YELLOW + "0%" + C.GRAY + " | " + C.WHITE + "Preparing setup"; - updateRegenDisplay(display, 0.0D, BarColor.BLUE, title, action); - pulseRegenDisplay(display); - return display; - } - - private void updateRegenSetupDisplay(RegenDisplay display, RegenMode mode, String phase, int step, int totalSteps) { - if (display == null || display.closed.get()) { - return; - } - - int safeTotalSteps = Math.max(1, totalSteps); - int safeStep = Math.max(0, Math.min(step, safeTotalSteps)); - double setupProgress = Math.max(0.0D, Math.min(0.1D, (safeStep / (double) safeTotalSteps) * 0.1D)); - int percent = (int) Math.round(setupProgress * 100.0D); - String title = C.GOLD + "Regen " + mode.id() + C.GRAY + " " + C.AQUA + "SETUP" + C.GRAY + " " + C.YELLOW + percent + "%"; - String action = buildRegenProgressBar(setupProgress) - + C.GRAY + " " + C.YELLOW + percent + "%" - + C.GRAY + " | " + C.WHITE + phase; - updateRegenDisplay(display, setupProgress, BarColor.BLUE, title, action); - } - - private void updateRegenDisplay(RegenDisplay display, double progress, BarColor color, String title, String action) { - if (display == null || display.closed.get()) { - return; - } - - display.progress = Math.max(0.0D, Math.min(1.0D, progress)); - display.color = color == null ? BarColor.BLUE : color; - display.title = title == null ? "" : title; - display.actionLine = action == null ? "" : action; - - Player player = display.sender.player(); - if (player == null) { - closeRegenDisplay(display, 0); - return; - } - - boolean scheduled = J.runEntity(player, () -> { - if (display.closed.get()) { - return; - } - - display.bossBar.setProgress(display.progress); - display.bossBar.setColor(display.color); - display.bossBar.setTitle(display.title); - if (!display.actionLine.isBlank()) { - display.sender.sendAction(display.actionLine); - } - }); - if (!scheduled) { - closeRegenDisplay(display, 0); - } - } - - private void pulseRegenDisplay(RegenDisplay display) { - if (display == null || display.closed.get()) { - return; - } - - Player player = display.sender.player(); - if (player == null) { - closeRegenDisplay(display, 0); - return; - } - - boolean scheduled = J.runEntity(player, () -> { - if (display.closed.get()) { - return; - } - - Player activePlayer = display.sender.player(); - if (activePlayer == null || !activePlayer.isOnline()) { - closeRegenDisplay(display, 0); - return; - } - - if (!display.actionLine.isBlank()) { - display.sender.sendAction(display.actionLine); - } - pulseRegenDisplay(display); - }, REGEN_ACTION_PULSE_TICKS); - - if (!scheduled) { - closeRegenDisplay(display, 0); - } - } - - private void completeRegenDisplay(RegenDisplay display, RegenMode mode, boolean failed, String detail) { - if (display == null || display.closed.get()) { - return; - } - - double progress = failed ? Math.max(0.0D, Math.min(1.0D, display.progress)) : 1.0D; - int percent = (int) Math.round(progress * 100.0D); - BarColor color = failed ? BarColor.RED : BarColor.GREEN; - String status = failed ? C.RED + "FAILED" : C.GREEN + "DONE"; - String title = C.GOLD + "Regen " + mode.id() + C.GRAY + " " + status + C.GRAY + " " + C.YELLOW + percent + "%"; - String action = buildRegenProgressBar(progress) + C.GRAY + " " + C.YELLOW + percent + "%"; - if (detail != null && !detail.isBlank()) { - action = action + C.GRAY + " | " + C.WHITE + detail; - } - - updateRegenDisplay(display, progress, color, title, action); - closeRegenDisplay(display, REGEN_DISPLAY_FINAL_TICKS); - } - - private void closeRegenDisplay(RegenDisplay display, int delayTicks) { - if (display == null || display.closed.get()) { - return; - } - - Player player = display.sender.player(); - Runnable closeTask = () -> { - if (!display.closed.compareAndSet(false, true)) { - return; - } - - display.bossBar.removeAll(); - display.bossBar.setVisible(false); - display.sender.sendAction(" "); - }; - - if (player == null) { - display.closed.set(true); - return; - } - - boolean scheduled = delayTicks > 0 - ? J.runEntity(player, closeTask, delayTicks) - : J.runEntity(player, closeTask); - if (!scheduled) { - display.closed.set(true); - } - } - - private RegenChunkResult runRegenChunk( - RegenChunkTask task, - World world, - PlatformChunkGenerator platform, - SyncExecutor executor, - ConcurrentMap activeTasks, - RegenMode mode, - String runId, - AtomicLong lastSignalMs - ) { - String worker = Thread.currentThread().getName(); - long startedAt = System.currentTimeMillis(); - boolean loadedAtStart = false; - try { - loadedAtStart = world.isChunkLoaded(task.chunkX(), task.chunkZ()); - } catch (Throwable ignored) { - } - - RegenActiveTask activeTask = new RegenActiveTask(task.chunkX(), task.chunkZ(), task.attempt(), startedAt, loadedAtStart); - activeTasks.put(worker, activeTask); - AtomicReference failurePhase = new AtomicReference<>("unknown"); - AtomicInteger overlayAppliedBlocks = new AtomicInteger(); - AtomicInteger overlayObjectKeys = new AtomicInteger(); - ChunkReplacementListener listener = new ChunkReplacementListener() { - @Override - public void onPhase(String phase, int chunkX, int chunkZ, long timestampMs) { - activeTask.updatePhase(phase, timestampMs); - lastSignalMs.set(timestampMs); - } - - @Override - public void onOverlay(int chunkX, int chunkZ, int appliedBlocks, int objectKeys, long timestampMs) { - overlayAppliedBlocks.addAndGet(appliedBlocks); - overlayObjectKeys.addAndGet(objectKeys); - activeTask.updatePhase("overlay", timestampMs); - lastSignalMs.set(timestampMs); - } - - @Override - public void onFailurePhase(String phase, int chunkX, int chunkZ, Throwable error, long timestampMs) { - String classifiedPhase = classifyRegenFailurePhase(phase); - failurePhase.set(classifiedPhase); - activeTask.updatePhase(classifiedPhase, timestampMs); - lastSignalMs.set(timestampMs); - } - }; - try { - if (mode.logChunkDiagnostics()) { - Iris.info("Regen chunk start: id=" + runId - + " chunk=" + task.chunkX() + "," + task.chunkZ() - + " attempt=" + task.attempt() - + " loadedAtStart=" + loadedAtStart - + " worker=" + worker); - } - ChunkReplacementOptions options = mode == RegenMode.FULL - ? ChunkReplacementOptions.full(runId, mode.logChunkDiagnostics()) - : ChunkReplacementOptions.terrain(runId, mode.logChunkDiagnostics()); - RegenRuntime.setRunId(runId); - try { - platform.injectChunkReplacement(world, task.chunkX(), task.chunkZ(), executor, options, listener); - } finally { - RegenRuntime.clear(); - } - if (mode.logChunkDiagnostics()) { - Iris.info("Regen chunk end: id=" + runId - + " chunk=" + task.chunkX() + "," + task.chunkZ() - + " attempt=" + task.attempt() - + " worker=" + worker - + " durationMs=" + (System.currentTimeMillis() - startedAt)); - } - long finishedAt = System.currentTimeMillis(); - activeTask.updateHeartbeat(finishedAt); - lastSignalMs.set(finishedAt); - return RegenChunkResult.success(task, worker, startedAt, finishedAt, loadedAtStart, overlayAppliedBlocks.get(), overlayObjectKeys.get()); - } catch (Throwable e) { - if (e instanceof InterruptedException) { - Thread.currentThread().interrupt(); - } - long finishedAt = System.currentTimeMillis(); - String classifiedPhase = classifyRegenFailurePhase(failurePhase.get()); - if ("unknown".equals(classifiedPhase)) { - classifiedPhase = classifyRegenFailurePhase(activeTask.phase()); - } - activeTask.updatePhase(classifiedPhase, finishedAt); - activeTask.updateHeartbeat(finishedAt); - lastSignalMs.set(finishedAt); - return RegenChunkResult.failure( - task, - worker, - startedAt, - finishedAt, - loadedAtStart, - classifiedPhase, - overlayAppliedBlocks.get(), - overlayObjectKeys.get(), - e - ); - } finally { - activeTasks.remove(worker); - } - } - - private Thread createRegenSetupWatchdog( - World world, - String runId, - AtomicBoolean setupDone, - AtomicReference setupPhase, - AtomicLong setupPhaseSince - ) { - String setupWatchdogName = "Iris-Regen-SetupWatchdog-" + runId; - Thread setupWatchdog = new Thread(() -> { - while (!setupDone.get()) { - try { - Thread.sleep(REGEN_HEARTBEAT_MS); - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - return; - } - - if (!setupDone.get()) { - long elapsed = System.currentTimeMillis() - setupPhaseSince.get(); - Iris.warn("Regen setup heartbeat: id=" + runId - + " phase=" + setupPhase.get() - + " elapsedMs=" + elapsed - + " world=" + world.getName()); - } - } - }, setupWatchdogName); - setupWatchdog.setDaemon(true); - return setupWatchdog; - } - - private void setRegenSetupPhase( - AtomicReference setupPhase, - AtomicLong setupPhaseSince, - String nextPhase, - World world, - String runId - ) { - setupPhase.set(nextPhase); - setupPhaseSince.set(System.currentTimeMillis()); - if (IrisSettings.get().getGeneral().isDebug()) { - Iris.info("Regen setup phase: id=" + runId + " phase=" + nextPhase + " world=" + world.getName()); - } - } - - private static String formatFailedChunkPreview(List failedChunks) { - if (failedChunks.isEmpty()) { - return "[]"; - } - - StringBuilder builder = new StringBuilder("["); - int index = 0; - for (Position2 chunk : failedChunks) { - if (index > 0) { - builder.append(", "); - } - if (index >= 10) { - builder.append("..."); - break; - } - builder.append(chunk.getX()).append(",").append(chunk.getZ()); - index++; - } - builder.append("]"); - return builder.toString(); - } - - private static String summarizeActivePhases(ConcurrentMap activeTasks) { - if (activeTasks.isEmpty()) { - return "idle"; - } - - Map counts = new HashMap<>(); - for (RegenActiveTask activeTask : activeTasks.values()) { - String phase = classifyRegenFailurePhase(activeTask.phase()); - counts.merge(phase, 1, Integer::sum); - } - if (counts.isEmpty()) { - return "idle"; - } - - List> entries = new ArrayList<>(counts.entrySet()); - entries.sort((a, b) -> { - int diff = Integer.compare(b.getValue(), a.getValue()); - if (diff != 0) { - return diff; - } - return a.getKey().compareTo(b.getKey()); - }); - - StringBuilder builder = new StringBuilder(); - int emitted = 0; - for (Map.Entry entry : entries) { - if (emitted > 0) { - builder.append(", "); - } - if (emitted >= 3) { - builder.append("..."); - break; - } - builder.append(entry.getKey()).append(" x").append(entry.getValue()); - emitted++; - } - return builder.toString(); - } - - private static String formatFailurePhaseSummary(Map failurePhaseCounts) { - if (failurePhaseCounts.isEmpty()) { - return "none"; - } - - List> entries = new ArrayList<>(failurePhaseCounts.entrySet()); - entries.sort((a, b) -> { - int diff = Integer.compare(b.getValue(), a.getValue()); - if (diff != 0) { - return diff; - } - return a.getKey().compareTo(b.getKey()); - }); - - StringBuilder builder = new StringBuilder(); - int emitted = 0; - for (Map.Entry entry : entries) { - if (emitted > 0) { - builder.append(", "); - } - if (emitted >= 5) { - builder.append("..."); - break; - } - builder.append(entry.getKey()).append("=").append(entry.getValue()); - emitted++; - } - return builder.toString(); - } - - private static String classifyRegenFailurePhase(String phase) { - if (phase == null || phase.isBlank()) { - return "unknown"; - } - - String normalized = phase.toLowerCase(Locale.ROOT); - if (normalized.contains("generate")) { - return "generate"; - } - if (normalized.contains("acquire-load-lock") || normalized.contains("reset-mantle")) { - return "generate"; - } - if (normalized.contains("apply-terrain") || normalized.contains("folia-region-run")) { - return "apply-terrain"; - } - if (normalized.contains("paperlib-async-load") || normalized.contains("folia-run-region")) { - return "apply-terrain"; - } - if (normalized.contains("overlay")) { - return "overlay"; - } - if (normalized.contains("structure")) { - return "structures"; - } - if (normalized.contains("chunk-load-callback")) { - return "chunk-load-callback"; - } - return "unknown"; - } - - private static String formatActiveTasks(ConcurrentMap activeTasks) { - if (activeTasks.isEmpty()) { - return "{}"; - } - - StringBuilder builder = new StringBuilder("{"); - int count = 0; - long now = System.currentTimeMillis(); - for (Map.Entry entry : activeTasks.entrySet()) { - if (count > 0) { - builder.append(", "); - } - if (count >= 8) { - builder.append("..."); - break; - } - RegenActiveTask activeTask = entry.getValue(); - builder.append(entry.getKey()) - .append("=") - .append(activeTask.chunkX()) - .append(",") - .append(activeTask.chunkZ()) - .append("@") - .append(activeTask.attempt()) - .append("/") - .append(now - activeTask.startedAtMs()) - .append("ms") - .append(":") - .append(classifyRegenFailurePhase(activeTask.phase())) - .append("/") - .append(now - activeTask.lastHeartbeatMs()) - .append("ms") - .append(activeTask.loadedAtStart() ? ":loaded" : ":cold"); - count++; - } - builder.append("}"); - return builder.toString(); - } - - private static void dumpRegenWorkerStacks(Set explicitThreads, String worldName) { - Set threads = new LinkedHashSet<>(); - threads.addAll(explicitThreads); - for (Thread thread : Thread.getAllStackTraces().keySet()) { - if (thread == null || !thread.isAlive()) { - continue; - } - - String name = thread.getName(); - if (name.startsWith("Iris-Regen-") - || name.startsWith("Iris EngineSVC-") - || name.startsWith("Iris World Manager") - || name.contains(worldName)) { - threads.add(thread); - } - } - - for (Thread thread : threads) { - if (thread == null || !thread.isAlive()) { - continue; - } - - Iris.warn("Regen worker thread=" + thread.getName() + " state=" + thread.getState()); - StackTraceElement[] trace = thread.getStackTrace(); - int limit = Math.min(trace.length, REGEN_STACK_LIMIT); - for (int i = 0; i < limit; i++) { - Iris.warn(" at " + trace[i]); - } - } - } - - private static final class RegenDisplay { - private final VolmitSender sender; - private final BossBar bossBar; - private final AtomicBoolean closed = new AtomicBoolean(false); - private volatile String title = ""; - private volatile String actionLine = ""; - private volatile double progress = 0.0D; - private volatile BarColor color = BarColor.BLUE; - - private RegenDisplay(VolmitSender sender, BossBar bossBar) { - this.sender = sender; - this.bossBar = bossBar; - } - } - - private record RegenChunkTask(int chunkX, int chunkZ, int attempt, long queuedAtMs) { - private RegenChunkTask retry(long now) { - return new RegenChunkTask(chunkX, chunkZ, attempt + 1, now); - } - } - - private enum RegenMode { - TERRAIN("terrain", true, false, false, false), - FULL("full", true, false, true, true); - - private final String id; - private final boolean usesMaintenance; - private final boolean bypassMantleStages; - private final boolean fullMode; - private final boolean logChunkDiagnostics; - - RegenMode( - String id, - boolean usesMaintenance, - boolean bypassMantleStages, - boolean fullMode, - boolean logChunkDiagnostics - ) { - this.id = id; - this.usesMaintenance = usesMaintenance; - this.bypassMantleStages = bypassMantleStages; - this.fullMode = fullMode; - this.logChunkDiagnostics = logChunkDiagnostics; - } - - private String id() { - return id; - } - - private boolean usesMaintenance() { - return usesMaintenance; - } - - private boolean bypassMantleStages() { - return bypassMantleStages; - } - - private boolean isFullMode() { - return fullMode; - } - - private int passCount() { - return 1; - } - - private boolean logChunkDiagnostics() { - return logChunkDiagnostics && IrisSettings.get().getGeneral().isDebug(); - } - - private static RegenMode parse(String raw) { - if (raw == null) { - return FULL; - } - - String normalized = raw.trim(); - if (normalized.isEmpty()) { - return FULL; - } - - for (RegenMode mode : values()) { - if (mode.id.equalsIgnoreCase(normalized)) { - return mode; - } - } - return null; - } - } - - private static final class RegenActiveTask { - private final int chunkX; - private final int chunkZ; - private final int attempt; - private final long startedAtMs; - private final boolean loadedAtStart; - private volatile String phase; - private volatile long lastHeartbeatMs; - - private RegenActiveTask(int chunkX, int chunkZ, int attempt, long startedAtMs, boolean loadedAtStart) { - this.chunkX = chunkX; - this.chunkZ = chunkZ; - this.attempt = attempt; - this.startedAtMs = startedAtMs; - this.loadedAtStart = loadedAtStart; - this.phase = "queued"; - this.lastHeartbeatMs = startedAtMs; - } - - private void updatePhase(String nextPhase, long timestampMs) { - this.phase = nextPhase == null || nextPhase.isBlank() ? "unknown" : nextPhase; - this.lastHeartbeatMs = timestampMs; - } - - private void updateHeartbeat(long timestampMs) { - this.lastHeartbeatMs = timestampMs; - } - - private int chunkX() { - return chunkX; - } - - private int chunkZ() { - return chunkZ; - } - - private int attempt() { - return attempt; - } - - private long startedAtMs() { - return startedAtMs; - } - - private boolean loadedAtStart() { - return loadedAtStart; - } - - private String phase() { - return phase; - } - - private long lastHeartbeatMs() { - return lastHeartbeatMs; - } - } - - private record RegenChunkResult( - RegenChunkTask task, - String worker, - long startedAtMs, - long finishedAtMs, - boolean loadedAtStart, - String failurePhase, - int overlayAppliedBlocks, - int overlayObjectKeys, - boolean success, - Throwable error - ) { - private static RegenChunkResult success( - RegenChunkTask task, - String worker, - long startedAtMs, - long finishedAtMs, - boolean loadedAtStart, - int overlayAppliedBlocks, - int overlayObjectKeys - ) { - return new RegenChunkResult( - task, - worker, - startedAtMs, - finishedAtMs, - loadedAtStart, - "none", - overlayAppliedBlocks, - overlayObjectKeys, - true, - null - ); - } - - private static RegenChunkResult failure( - RegenChunkTask task, - String worker, - long startedAtMs, - long finishedAtMs, - boolean loadedAtStart, - String failurePhase, - int overlayAppliedBlocks, - int overlayObjectKeys, - Throwable error - ) { - return new RegenChunkResult( - task, - worker, - startedAtMs, - finishedAtMs, - loadedAtStart, - failurePhase == null || failurePhase.isBlank() ? "unknown" : failurePhase, - overlayAppliedBlocks, - overlayObjectKeys, - false, - error - ); - } - - private String errorSummary() { - if (error == null) { - return "unknown"; - } - String message = error.getMessage(); - if (message == null || message.isEmpty()) { - return error.getClass().getSimpleName(); - } - return error.getClass().getSimpleName() + ": " + message; - } - } - - private record RegenSummary( - int totalChunks, - int successChunks, - int failedChunks, - int retryCount, - String failedPreview, - String failurePhaseSummary - ) { - } @Director(description = "Unload an Iris World", origin = DirectorOrigin.PLAYER, sync = true) public void unloadWorld( diff --git a/core/src/main/java/art/arcane/iris/core/commands/CommandLazyPregen.java b/core/src/main/java/art/arcane/iris/core/commands/CommandLazyPregen.java deleted file mode 100644 index e2e392982..000000000 --- a/core/src/main/java/art/arcane/iris/core/commands/CommandLazyPregen.java +++ /dev/null @@ -1,127 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package art.arcane.iris.core.commands; - -import art.arcane.iris.Iris; -import art.arcane.iris.core.IrisSettings; -import art.arcane.iris.core.gui.PregeneratorJob; -import art.arcane.iris.core.pregenerator.LazyPregenerator; -import art.arcane.iris.core.pregenerator.PregenTask; -import art.arcane.iris.core.tools.IrisToolbelt; -import art.arcane.iris.engine.platform.PlatformChunkGenerator; -import art.arcane.iris.util.common.director.DirectorExecutor; -import art.arcane.volmlib.util.director.annotations.Director; -import art.arcane.volmlib.util.director.annotations.Param; -import art.arcane.iris.util.common.format.C; -import art.arcane.volmlib.util.math.Position2; -import org.bukkit.Bukkit; -import org.bukkit.World; -import org.bukkit.util.Vector; - -import java.io.File; -import java.io.IOException; - -@Director(name = "lazypregen", aliases = "lazy", description = "Pregenerate your Iris worlds!") -public class CommandLazyPregen implements DirectorExecutor { - public String worldName; - @Director(description = "Pregenerate a world") - public void start( - @Param(description = "The radius of the pregen in blocks", aliases = "size") - int radius, - @Param(description = "The world to pregen", contextual = true) - World world, - @Param(aliases = "middle", description = "The center location of the pregen. Use \"me\" for your current location", defaultValue = "0,0") - Vector center, - @Param(aliases = "maxcpm", description = "Limit the chunks per minute the pregen will generate", defaultValue = "999999999") - int cpm, - @Param(aliases = "silent", description = "Silent generation", defaultValue = "false") - boolean silent - ) { - - worldName = world.getName(); - File worldDirectory = new File(Bukkit.getWorldContainer(), world.getName()); - File lazyFile = new File(worldDirectory, "lazygen.json"); - if (lazyFile.exists()) { - sender().sendMessage(C.BLUE + "Lazy pregen is already in progress"); - Iris.info(C.YELLOW + "Lazy pregen is already in progress"); - return; - } - - try { - if (sender().isPlayer() && access() == null) { - sender().sendMessage(C.RED + "The engine access for this world is null!"); - sender().sendMessage(C.RED + "Please make sure the world is loaded & the engine is initialized. Generate a new chunk, for example."); - } - - PlatformChunkGenerator platform = IrisToolbelt.access(world); - if (platform != null) { - IrisToolbelt.applyPregenPerformanceProfile(platform.getEngine()); - } - - LazyPregenerator.LazyPregenJob pregenJob = LazyPregenerator.LazyPregenJob.builder() - .world(worldName) - .healingPosition(0) - .healing(false) - .chunksPerMinute(cpm) - .radiusBlocks(radius) - .position(0) - .silent(silent) - .build(); - - File lazyGenFile = new File(worldDirectory, "lazygen.json"); - LazyPregenerator pregenerator = new LazyPregenerator(pregenJob, lazyGenFile); - pregenerator.start(); - - String msg = C.GREEN + "LazyPregen started in " + C.GOLD + worldName + C.GREEN + " of " + C.GOLD + (radius * 2) + C.GREEN + " by " + C.GOLD + (radius * 2) + C.GREEN + " blocks from " + C.GOLD + center.getX() + "," + center.getZ(); - sender().sendMessage(msg); - Iris.info(msg); - } catch (Throwable e) { - sender().sendMessage(C.RED + "Epic fail. See console."); - Iris.reportError(e); - e.printStackTrace(); - } - } - - @Director(description = "Stop the active pregeneration task", aliases = "x") - public void stop( - @Param(aliases = "world", description = "The world to pause") - World world - ) throws IOException { - if (LazyPregenerator.getInstance() != null) { - LazyPregenerator.getInstance().shutdownInstance(world); - sender().sendMessage(C.LIGHT_PURPLE + "Closed lazygen instance for " + world.getName()); - } else { - sender().sendMessage(C.YELLOW + "No active pregeneration tasks to stop"); - } - } - - @Director(description = "Pause / continue the active pregeneration task", aliases = {"t", "resume", "unpause"}) - public void pause( - @Param(aliases = "world", description = "The world to pause") - World world - ) { - if (LazyPregenerator.getInstance() != null) { - LazyPregenerator.getInstance().setPausedLazy(world); - sender().sendMessage(C.GREEN + "Paused/unpaused Lazy Pregen, now: " + (LazyPregenerator.getInstance().isPausedLazy(world) ? "Paused" : "Running") + "."); - } else { - sender().sendMessage(C.YELLOW + "No active Lazy Pregen tasks to pause/unpause."); - - } - } -} diff --git a/core/src/main/java/art/arcane/iris/core/commands/CommandSmoke.java b/core/src/main/java/art/arcane/iris/core/commands/CommandSmoke.java deleted file mode 100644 index 180210e14..000000000 --- a/core/src/main/java/art/arcane/iris/core/commands/CommandSmoke.java +++ /dev/null @@ -1,186 +0,0 @@ -package art.arcane.iris.core.commands; - -import art.arcane.iris.Iris; -import art.arcane.iris.core.runtime.DatapackReadinessResult; -import art.arcane.iris.core.runtime.SmokeDiagnosticsService; -import art.arcane.iris.core.runtime.SmokeTestService; -import art.arcane.iris.util.common.director.DirectorExecutor; -import art.arcane.iris.util.common.format.C; -import art.arcane.volmlib.util.director.annotations.Director; -import art.arcane.volmlib.util.director.annotations.Param; -import art.arcane.volmlib.util.format.Form; - -import java.util.List; - -@Director(name = "smoke", description = "Run Iris developer smoke diagnostics") -public class CommandSmoke implements DirectorExecutor { - @Director(description = "Run the full smoke suite", sync = true) - public void full( - @Param(name = "dimension", aliases = {"pack"}, description = "The dimension/pack key to validate") - String dimension, - @Param(description = "The seed to use", defaultValue = "1337") - long seed, - @Param(description = "Optional player validation target or none", defaultValue = "none") - String player, - @Param(name = "retain-on-failure", aliases = {"retain"}, description = "Retain the temp world after failure", defaultValue = "false") - boolean retainOnFailure - ) { - String runId = SmokeTestService.get().startFullSmoke(sender(), dimension, seed, player, retainOnFailure); - announceRun(runId, "full"); - } - - @Director(description = "Run the studio smoke flow", sync = true) - public void studio( - @Param(name = "dimension", aliases = {"pack"}, description = "The dimension/pack key to validate") - String dimension, - @Param(description = "The seed to use", defaultValue = "1337") - long seed, - @Param(description = "Optional player validation target or none", defaultValue = "none") - String player, - @Param(name = "retain-on-failure", aliases = {"retain"}, description = "Retain the temp world after failure", defaultValue = "false") - boolean retainOnFailure - ) { - String runId = SmokeTestService.get().startStudioSmoke(sender(), dimension, seed, player, retainOnFailure); - announceRun(runId, "studio"); - } - - @Director(description = "Run the create/unload smoke flow", sync = true) - public void create( - @Param(name = "dimension", aliases = {"pack"}, description = "The dimension/pack key to validate") - String dimension, - @Param(description = "The seed to use", defaultValue = "1337") - long seed, - @Param(name = "retain-on-failure", aliases = {"retain"}, description = "Retain the temp world after failure", defaultValue = "false") - boolean retainOnFailure - ) { - String runId = SmokeTestService.get().startCreateSmoke(sender(), dimension, seed, retainOnFailure); - announceRun(runId, "create"); - } - - @Director(description = "Run the benchmark create/unload smoke flow", sync = true) - public void benchmark( - @Param(name = "dimension", aliases = {"pack"}, description = "The dimension/pack key to validate") - String dimension, - @Param(description = "The seed to use", defaultValue = "1337") - long seed, - @Param(name = "retain-on-failure", aliases = {"retain"}, description = "Retain the temp world after failure", defaultValue = "false") - boolean retainOnFailure - ) { - String runId = SmokeTestService.get().startBenchmarkSmoke(sender(), dimension, seed, retainOnFailure); - announceRun(runId, "benchmark"); - } - - @Director(description = "Show live or persisted smoke status", sync = true) - public void status( - @Param(description = "Use latest or a specific run id", defaultValue = "latest") - String run - ) { - SmokeDiagnosticsService.SmokeRunReport report = resolveReport(run); - if (report == null) { - sender().sendMessage(C.RED + "No smoke report found for \"" + run + "\"."); - return; - } - - sendReport(report); - } - - @Director(description = "Inspect a currently loaded smoke/studio world", sync = true) - public void inspect( - @Param(description = "The loaded world name to inspect") - String world - ) { - SmokeTestService.WorldInspection inspection = SmokeTestService.get().inspectWorld(world); - if (inspection == null) { - sender().sendMessage(C.RED + "World \"" + world + "\" is not currently loaded."); - return; - } - - sender().sendMessage(C.GREEN + "Smoke inspection for " + C.GOLD + inspection.worldName()); - sender().sendMessage(C.GRAY + "Lifecycle backend: " + C.WHITE + inspection.lifecycleBackend()); - sender().sendMessage(C.GRAY + "Runtime backend: " + C.WHITE + inspection.runtimeBackend()); - sender().sendMessage(C.GRAY + "Studio: " + C.WHITE + inspection.studio() + C.GRAY + " | Maintenance active: " + C.WHITE + inspection.maintenanceActive()); - sender().sendMessage(C.GRAY + "Engine closed: " + C.WHITE + inspection.engineClosed() + C.GRAY + " | Engine failing: " + C.WHITE + inspection.engineFailing()); - sender().sendMessage(C.GRAY + "Generation session: " + C.WHITE + inspection.generationSessionId() + C.GRAY + " | Active leases: " + C.WHITE + inspection.activeLeaseCount()); - sender().sendMessage(C.GRAY + "Datapack folders: " + C.WHITE + joinList(inspection.datapackFolders())); - } - - private void announceRun(String runId, String mode) { - sender().sendMessage(C.GREEN + "Started " + C.GOLD + mode + C.GREEN + " smoke run " + C.GOLD + runId + C.GREEN + "."); - sender().sendMessage(C.GREEN + "Use " + C.GOLD + "/iris developer smoke status run=" + runId + C.GREEN + " to monitor progress."); - sender().sendMessage(C.GREEN + "Latest report: " + C.GOLD + latestReportPath()); - } - - private SmokeDiagnosticsService.SmokeRunReport resolveReport(String run) { - if (run == null || run.isBlank() || run.equalsIgnoreCase("latest")) { - return SmokeTestService.get().latest(); - } - - return SmokeTestService.get().get(run); - } - - private void sendReport(SmokeDiagnosticsService.SmokeRunReport report) { - String elapsed = Form.duration(Math.max(0L, report.getElapsedMs()), 0); - sender().sendMessage(C.GREEN + "Smoke run " + C.GOLD + report.getRunId() + C.GREEN + " (" + C.GOLD + report.getMode() + C.GREEN + ")"); - sender().sendMessage(C.GRAY + "World: " + C.WHITE + fallback(report.getWorldName()) + C.GRAY + " | Outcome: " + C.WHITE + fallback(report.getOutcome())); - sender().sendMessage(C.GRAY + "Stage: " + C.WHITE + fallback(report.getStage()) + C.GRAY + " | Elapsed: " + C.WHITE + elapsed); - if (report.getStageDetail() != null && !report.getStageDetail().isBlank()) { - sender().sendMessage(C.GRAY + "Stage detail: " + C.WHITE + report.getStageDetail()); - } - sender().sendMessage(C.GRAY + "Lifecycle backend: " + C.WHITE + fallback(report.getLifecycleBackend())); - sender().sendMessage(C.GRAY + "Runtime backend: " + C.WHITE + fallback(report.getRuntimeBackend())); - sender().sendMessage(C.GRAY + "Generation session: " + C.WHITE + report.getGenerationSessionId() + C.GRAY + " | Active leases: " + C.WHITE + report.getGenerationActiveLeases()); - if (report.getEntryChunkX() != null && report.getEntryChunkZ() != null) { - sender().sendMessage(C.GRAY + "Entry chunk: " + C.WHITE + report.getEntryChunkX() + "," + report.getEntryChunkZ()); - } - sender().sendMessage(C.GRAY + "Headless: " + C.WHITE + report.isHeadless() + C.GRAY + " | Player: " + C.WHITE + fallback(report.getPlayerName())); - sender().sendMessage(C.GRAY + "Retain on failure: " + C.WHITE + report.isRetainOnFailure() + C.GRAY + " | Cleanup applied: " + C.WHITE + report.isCleanupApplied()); - sendDatapackReadiness(report.getDatapackReadiness()); - if (!report.getNotes().isEmpty()) { - sender().sendMessage(C.GRAY + "Notes: " + C.WHITE + joinList(report.getNotes())); - } - if (report.getFailureType() != null && !report.getFailureType().isBlank()) { - sender().sendMessage(C.RED + "Failure: " + report.getFailureType() + C.GRAY + " - " + C.WHITE + fallback(report.getFailureMessage())); - if (!report.getFailureChain().isEmpty()) { - sender().sendMessage(C.RED + "Failure chain: " + C.WHITE + joinList(report.getFailureChain())); - } - } - } - - private void sendDatapackReadiness(DatapackReadinessResult readiness) { - if (readiness == null) { - return; - } - - sender().sendMessage(C.GRAY + "Datapack pack key: " + C.WHITE + fallback(readiness.getRequestedPackKey())); - sender().sendMessage(C.GRAY + "Datapack folders: " + C.WHITE + joinList(readiness.getResolvedDatapackFolders())); - sender().sendMessage(C.GRAY + "External datapack result: " + C.WHITE + fallback(readiness.getExternalDatapackInstallResult())); - sender().sendMessage(C.GRAY + "Verification passed: " + C.WHITE + readiness.isVerificationPassed() + C.GRAY + " | Restart required: " + C.WHITE + readiness.isRestartRequired()); - if (!readiness.getMissingPaths().isEmpty()) { - sender().sendMessage(C.RED + "Missing datapack paths: " + C.WHITE + joinList(readiness.getMissingPaths())); - } - } - - private String latestReportPath() { - if (Iris.instance == null) { - return "plugins/Iris/diagnostics/smoke/latest.json"; - } - - return Iris.instance.getDataFile("diagnostics", "smoke", "latest.json").getAbsolutePath(); - } - - private String joinList(List values) { - if (values == null || values.isEmpty()) { - return "none"; - } - - return String.join(", ", values); - } - - private String fallback(String value) { - if (value == null || value.isBlank()) { - return "none"; - } - - return value; - } -} diff --git a/core/src/main/java/art/arcane/iris/core/commands/CommandStudio.java b/core/src/main/java/art/arcane/iris/core/commands/CommandStudio.java index 1b33af13c..806217d10 100644 --- a/core/src/main/java/art/arcane/iris/core/commands/CommandStudio.java +++ b/core/src/main/java/art/arcane/iris/core/commands/CommandStudio.java @@ -186,113 +186,6 @@ public class CommandStudio implements DirectorExecutor { sender().sendMessage(C.GREEN + "The \"" + dimension.getName() + "\" pack has version: " + dimension.getVersion()); } - @Director(name = "regen", description = "Regenerate nearby chunks.", aliases = "rg", sync = true, origin = DirectorOrigin.PLAYER) - public void regen( - @Param(name = "radius", description = "The radius of nearby cunks", defaultValue = "5") - int radius - ) { - World world = player().getWorld(); - if (!IrisToolbelt.isIrisWorld(world)) { - sender().sendMessage(C.RED + "You must be in an Iris World to use regen!"); - } - - VolmitSender sender = sender(); - var loc = player().getLocation().clone(); - final int threadCount = J.isFolia() ? 1 : Runtime.getRuntime().availableProcessors(); - - String orchestratorName = "Iris-Studio-Regen-Orchestrator-" + world.getName() + "-" + System.nanoTime(); - Thread orchestrator = new Thread(() -> { - PlatformChunkGenerator plat = IrisToolbelt.access(world); - Engine engine = plat.getEngine(); - DirectorContext.touch(sender); - IrisToolbelt.beginWorldMaintenance(world, "studio-regen"); - try (SyncExecutor executor = new SyncExecutor(20); - var service = Executors.newFixedThreadPool(threadCount) - ) { - int x = loc.getBlockX() >> 4; - int z = loc.getBlockZ() >> 4; - - int rad = 0; - var chunkMap = new KMap(); - boolean foliaFastRegen = J.isFolia(); - if (foliaFastRegen) { - sender.sendMessage(C.YELLOW + "Folia safe default: using 1 regen worker in studio."); - } - if (!foliaFastRegen) { - rad = engine.getMantle().getRadius(); - final var mantle = engine.getMantle().getMantle(); - ParallelRadiusJob prep = new ParallelRadiusJob(threadCount, service) { - @Override - protected void execute(int rX, int rZ) { - if (Math.abs(rX) <= radius && Math.abs(rZ) <= radius) { - mantle.deleteChunk(rX + x, rZ + z); - return; - } - rX += x; - rZ += z; - chunkMap.put(new Position2(rX, rZ), mantle.getChunk(rX, rZ)); - mantle.deleteChunk(rX, rZ); - } - - @Override - public String getName() { - return "Preparing Mantle"; - } - }.retarget(radius + rad, 0, 0); - sender.sendMessage(C.YELLOW + "Preparing mantle data for studio regen..."); - prep.execute(); - } else { - sender.sendMessage(C.YELLOW + "Folia fast regen: skipping outer mantle preservation stage."); - } - - final String runId = "studio-regen-" + world.getName() + "-" + System.currentTimeMillis(); - - ParallelRadiusJob job = new ParallelRadiusJob(threadCount, service) { - @Override - protected void execute(int x, int z) { - if (foliaFastRegen) { - Iris.verbose("Folia fast studio regen skipping mantle delete for " + x + "," + z + "."); - } - plat.injectChunkReplacement( - world, - x, - z, - executor, - ChunkReplacementOptions.terrain(runId, IrisSettings.get().getGeneral().isDebug()), - ChunkReplacementListener.NO_OP - ); - } - - @Override - public String getName() { - return "Regenerating"; - } - }.retarget(radius, x, z); - job.execute(); - - if (!foliaFastRegen) { - var mantle = engine.getMantle().getMantle(); - chunkMap.forEach((pos, chunk) -> - mantle.getChunk(pos.getX(), pos.getZ()).copyFrom(chunk)); - } - } catch (Throwable e) { - sender().sendMessage("Error while regenerating chunks"); - e.printStackTrace(); - } finally { - IrisToolbelt.endWorldMaintenance(world, "studio-regen"); - DirectorContext.remove(); - } - }, orchestratorName); - orchestrator.setDaemon(true); - try { - orchestrator.start(); - Iris.info("Studio regen worker dispatched on dedicated thread=" + orchestratorName + "."); - } catch (Throwable e) { - sender.sendMessage(C.RED + "Failed to start studio regen worker thread. See console."); - Iris.reportError(e); - } - } - @Director(description = "Open the noise explorer (External GUI)", aliases = {"nmap", "n"}) public void noise() { if (noGUI()) return; @@ -331,16 +224,6 @@ public class CommandStudio implements DirectorExecutor { NoiseExplorerGUI.launch(l, "Custom Generator"); } - @Director(description = "Hotload a studio", aliases = {"reload", "h"}) - public void hotload() { - if (!Iris.service(StudioSVC.class).isProjectOpen()) { - sender().sendMessage(C.RED + "No studio world open!"); - return; - } - Iris.service(StudioSVC.class).getActiveProject().getActiveProvider().getEngine().hotload(); - sender().sendMessage(C.GREEN + "Hotloaded"); - } - @Director(description = "Show loot if a chest were right here", origin = DirectorOrigin.PLAYER, sync = true) public void loot( @Param(description = "Fast insertion of items in virtual inventory (may cause performance drop)", defaultValue = "false") diff --git a/core/src/main/java/art/arcane/iris/core/commands/CommandTurboPregen.java b/core/src/main/java/art/arcane/iris/core/commands/CommandTurboPregen.java deleted file mode 100644 index 69b11c1e8..000000000 --- a/core/src/main/java/art/arcane/iris/core/commands/CommandTurboPregen.java +++ /dev/null @@ -1,137 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package art.arcane.iris.core.commands; - -import art.arcane.iris.Iris; -import art.arcane.iris.core.pregenerator.LazyPregenerator; -import art.arcane.iris.core.pregenerator.TurboPregenerator; -import art.arcane.iris.core.tools.IrisToolbelt; -import art.arcane.iris.engine.platform.PlatformChunkGenerator; -import art.arcane.iris.util.common.director.DirectorExecutor; -import art.arcane.volmlib.util.director.annotations.Director; -import art.arcane.volmlib.util.director.annotations.Param; -import art.arcane.iris.util.common.format.C; -import org.bukkit.Bukkit; -import org.bukkit.World; -import org.bukkit.util.Vector; - -import java.io.File; -import java.io.IOException; - -@Director(name = "turbopregen", aliases = "turbo", description = "Pregenerate your Iris worlds!") -public class CommandTurboPregen implements DirectorExecutor { - public String worldName; - @Director(description = "Pregenerate a world") - public void start( - @Param(description = "The radius of the pregen in blocks", aliases = "size") - int radius, - @Param(description = "The world to pregen", contextual = true) - World world, - @Param(aliases = "middle", description = "The center location of the pregen. Use \"me\" for your current location", defaultValue = "0,0") - Vector center - ) { - - worldName = world.getName(); - File worldDirectory = new File(Bukkit.getWorldContainer(), world.getName()); - File TurboFile = new File(worldDirectory, "turbogen.json"); - if (TurboFile.exists()) { - if (TurboPregenerator.getInstance() != null) { - sender().sendMessage(C.BLUE + "Turbo pregen is already in progress"); - Iris.info(C.YELLOW + "Turbo pregen is already in progress"); - return; - } else { - try { - TurboFile.delete(); - } catch (Exception e){ - Iris.error("Failed to delete the old instance file of Turbo Pregen!"); - return; - } - } - } - - try { - if (sender().isPlayer() && access() == null) { - sender().sendMessage(C.RED + "The engine access for this world is null!"); - sender().sendMessage(C.RED + "Please make sure the world is loaded & the engine is initialized. Generate a new chunk, for example."); - } - - PlatformChunkGenerator platform = IrisToolbelt.access(world); - if (platform != null) { - IrisToolbelt.applyPregenPerformanceProfile(platform.getEngine()); - } - - TurboPregenerator.TurboPregenJob pregenJob = TurboPregenerator.TurboPregenJob.builder() - .world(worldName) - .radiusBlocks(radius) - .position(0) - .build(); - - File TurboGenFile = new File(worldDirectory, "turbogen.json"); - TurboPregenerator pregenerator = new TurboPregenerator(pregenJob, TurboGenFile); - pregenerator.start(); - - String msg = C.GREEN + "TurboPregen started in " + C.GOLD + worldName + C.GREEN + " of " + C.GOLD + (radius * 2) + C.GREEN + " by " + C.GOLD + (radius * 2) + C.GREEN + " blocks from " + C.GOLD + center.getX() + "," + center.getZ(); - sender().sendMessage(msg); - Iris.info(msg); - } catch (Throwable e) { - sender().sendMessage(C.RED + "Epic fail. See console."); - Iris.reportError(e); - e.printStackTrace(); - } - } - - @Director(description = "Stop the active pregeneration task", aliases = "x") - public void stop(@Param(aliases = "world", description = "The world to pause") World world) throws IOException { - TurboPregenerator turboPregenInstance = TurboPregenerator.getInstance(); - File worldDirectory = new File(Bukkit.getWorldContainer(), world.getName()); - File turboFile = new File(worldDirectory, "turbogen.json"); - - if (turboPregenInstance != null) { - turboPregenInstance.shutdownInstance(world); - sender().sendMessage(C.LIGHT_PURPLE + "Closed Turbogen instance for " + world.getName()); - } else if (turboFile.exists() && turboFile.delete()) { - sender().sendMessage(C.LIGHT_PURPLE + "Closed Turbogen instance for " + world.getName()); - } else if (turboFile.exists()) { - Iris.error("Failed to delete the old instance file of Turbo Pregen!"); - } else { - sender().sendMessage(C.YELLOW + "No active pregeneration tasks to stop"); - } - } - - @Director(description = "Pause / continue the active pregeneration task", aliases = {"t", "resume", "unpause"}) - public void pause( - @Param(aliases = "world", description = "The world to pause") - World world - ) { - if (TurboPregenerator.getInstance() != null) { - TurboPregenerator.setPausedTurbo(world); - sender().sendMessage(C.GREEN + "Paused/unpaused Turbo Pregen, now: " + (TurboPregenerator.isPausedTurbo(world) ? "Paused" : "Running") + "."); - } else { - File worldDirectory = new File(Bukkit.getWorldContainer(), world.getName()); - File TurboFile = new File(worldDirectory, "turbogen.json"); - if (TurboFile.exists()){ - TurboPregenerator.loadTurboGenerator(world.getName()); - sender().sendMessage(C.YELLOW + "Started Turbo Pregen back up!"); - } else { - sender().sendMessage(C.YELLOW + "No active Turbo Pregen tasks to pause/unpause."); - } - - } - } -} diff --git a/core/src/main/java/art/arcane/iris/core/commands/CommandUpdater.java b/core/src/main/java/art/arcane/iris/core/commands/CommandUpdater.java deleted file mode 100644 index fc0c90ad0..000000000 --- a/core/src/main/java/art/arcane/iris/core/commands/CommandUpdater.java +++ /dev/null @@ -1,102 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package art.arcane.iris.core.commands; - -import lombok.Synchronized; -import org.bukkit.World; - -import art.arcane.iris.Iris; -import art.arcane.iris.core.pregenerator.ChunkUpdater; -import art.arcane.iris.core.tools.IrisToolbelt; -import art.arcane.iris.util.common.director.DirectorExecutor; -import art.arcane.volmlib.util.director.DirectorOrigin; -import art.arcane.volmlib.util.director.annotations.Director; -import art.arcane.volmlib.util.director.annotations.Param; -import art.arcane.iris.util.common.format.C; -import art.arcane.volmlib.util.format.Form; - -@Director(name = "updater", origin = DirectorOrigin.BOTH, description = "Iris World Updater") -public class CommandUpdater implements DirectorExecutor { - private final Object lock = new Object(); - private transient ChunkUpdater chunkUpdater; - - @Director(description = "Updates all chunk in the specified world") - public void start( - @Param(description = "World to update chunks at", contextual = true) - World world - ) { - if (!IrisToolbelt.isIrisWorld(world)) { - sender().sendMessage(C.GOLD + "This is not an Iris world"); - return; - } - synchronized (lock) { - if (chunkUpdater != null) { - chunkUpdater.stop(); - } - - chunkUpdater = new ChunkUpdater(world); - if (sender().isPlayer()) { - sender().sendMessage(C.GREEN + "Updating " + world.getName() + C.GRAY + " Total chunks: " + Form.f(chunkUpdater.getChunks())); - } else { - Iris.info(C.GREEN + "Updating " + world.getName() + C.GRAY + " Total chunks: " + Form.f(chunkUpdater.getChunks())); - } - chunkUpdater.start(); - } - } - - @Synchronized("lock") - @Director(description = "Pause the updater") - public void pause( ) { - if (chunkUpdater == null) { - sender().sendMessage(C.GOLD + "You cant pause something that doesnt exist?"); - return; - } - boolean status = chunkUpdater.pause(); - if (sender().isPlayer()) { - if (status) { - sender().sendMessage(C.IRIS + "Paused task for: " + C.GRAY + chunkUpdater.getName()); - } else { - sender().sendMessage(C.IRIS + "Unpause task for: " + C.GRAY + chunkUpdater.getName()); - } - } else { - if (status) { - Iris.info(C.IRIS + "Paused task for: " + C.GRAY + chunkUpdater.getName()); - } else { - Iris.info(C.IRIS + "Unpause task for: " + C.GRAY + chunkUpdater.getName()); - } - } - } - - @Synchronized("lock") - @Director(description = "Stops the updater") - public void stop() { - if (chunkUpdater == null) { - sender().sendMessage(C.GOLD + "You cant stop something that doesnt exist?"); - return; - } - if (sender().isPlayer()) { - sender().sendMessage("Stopping Updater for: " + C.GRAY + chunkUpdater.getName()); - } else { - Iris.info("Stopping Updater for: " + C.GRAY + chunkUpdater.getName()); - } - chunkUpdater.stop(); - } -} - - diff --git a/core/src/main/java/art/arcane/iris/core/edit/DustRevealer.java b/core/src/main/java/art/arcane/iris/core/edit/DustRevealer.java index b559b5b95..6ac5014cd 100644 --- a/core/src/main/java/art/arcane/iris/core/edit/DustRevealer.java +++ b/core/src/main/java/art/arcane/iris/core/edit/DustRevealer.java @@ -21,6 +21,7 @@ package art.arcane.iris.core.edit; import art.arcane.iris.Iris; import art.arcane.iris.core.tools.IrisToolbelt; import art.arcane.iris.engine.framework.Engine; +import art.arcane.iris.engine.platform.PlatformChunkGenerator; import art.arcane.volmlib.util.collection.KList; import art.arcane.volmlib.util.math.BlockPosition; import art.arcane.volmlib.util.math.M; @@ -103,7 +104,11 @@ public class DustRevealer { public static void spawn(Block block, VolmitSender sender) { World world = block.getWorld(); - Engine access = IrisToolbelt.access(world).getEngine(); + PlatformChunkGenerator generator = IrisToolbelt.access(world); + if (generator == null) { + return; + } + Engine access = generator.getEngine(); if (access != null) { String a = access.getObjectPlacementKey(block.getX(), block.getY() - block.getWorld().getMinHeight(), block.getZ()); diff --git a/core/src/main/java/art/arcane/iris/core/pregenerator/ChunkUpdater.java b/core/src/main/java/art/arcane/iris/core/pregenerator/ChunkUpdater.java deleted file mode 100644 index 03d78dea5..000000000 --- a/core/src/main/java/art/arcane/iris/core/pregenerator/ChunkUpdater.java +++ /dev/null @@ -1,324 +0,0 @@ -package art.arcane.iris.core.pregenerator; - -import art.arcane.iris.Iris; -import art.arcane.iris.core.IrisSettings; -import art.arcane.iris.core.service.PreservationSVC; -import art.arcane.iris.core.tools.IrisToolbelt; -import art.arcane.iris.engine.framework.Engine; -import art.arcane.iris.util.project.profile.LoadBalancer; -import art.arcane.volmlib.util.format.Form; -import art.arcane.volmlib.util.mantle.flag.MantleFlag; -import art.arcane.volmlib.util.math.M; -import art.arcane.volmlib.util.math.Position2; -import art.arcane.volmlib.util.math.RollingSequence; -import art.arcane.iris.util.common.plugin.chunk.TicketHolder; -import art.arcane.iris.util.common.scheduling.J; -import io.papermc.lib.PaperLib; -import org.bukkit.Bukkit; -import org.bukkit.Chunk; -import org.bukkit.World; - -import java.io.File; - -import java.util.concurrent.*; -import java.util.concurrent.atomic.AtomicBoolean; -import java.util.concurrent.atomic.AtomicInteger; -import java.util.concurrent.atomic.AtomicLong; - -public class ChunkUpdater { - private static final String REGION_PATH = "region" + File.separator + "r."; - private final AtomicBoolean paused = new AtomicBoolean(); - private final AtomicBoolean cancelled = new AtomicBoolean(); - private final TicketHolder holder; - private final RollingSequence chunksPerSecond = new RollingSequence(5); - private final AtomicInteger totalMaxChunks = new AtomicInteger(); - private final AtomicInteger chunksProcessed = new AtomicInteger(); - private final AtomicInteger chunksProcessedLast = new AtomicInteger(); - private final AtomicInteger chunksUpdated = new AtomicInteger(); - private final AtomicBoolean serverEmpty = new AtomicBoolean(true); - private final AtomicLong lastCpsTime = new AtomicLong(M.ms()); - private final int maxConcurrency = IrisSettings.get().getUpdater().getMaxConcurrency(); - private final int coreLimit = (int) Math.max(Runtime.getRuntime().availableProcessors() * IrisSettings.get().getUpdater().getThreadMultiplier(), 1); - private final Semaphore semaphore = new Semaphore(maxConcurrency); - private final LoadBalancer loadBalancer = new LoadBalancer(semaphore, maxConcurrency, IrisSettings.get().getUpdater().emptyMsRange); - private final AtomicLong startTime = new AtomicLong(); - private final Dimensions dimensions; - private final PregenTask task; - private final ExecutorService chunkExecutor = IrisSettings.get().getUpdater().isNativeThreads() ? Executors.newFixedThreadPool(coreLimit) : Executors.newVirtualThreadPerTaskExecutor(); - private final ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1); - private final CountDownLatch latch; - private final Engine engine; - private final World world; - - public ChunkUpdater(World world) { - this.engine = IrisToolbelt.access(world).getEngine(); - this.world = world; - this.holder = Iris.tickets.getHolder(world); - this.dimensions = calculateWorldDimensions(new File(world.getWorldFolder(), "region")); - this.task = dimensions.task(); - this.totalMaxChunks.set(dimensions.count * 1024); - this.latch = new CountDownLatch(totalMaxChunks.get()); - } - - public String getName() { - return world.getName(); - } - - public int getChunks() { - return totalMaxChunks.get(); - } - - public void start() { - unloadAndSaveAllChunks(); - update(); - } - - public boolean pause() { - unloadAndSaveAllChunks(); - if (paused.get()) { - paused.set(false); - return false; - } else { - paused.set(true); - return true; - } - } - - public void stop() { - unloadAndSaveAllChunks(); - cancelled.set(true); - } - - private void update() { - Iris.info("Updating.."); - try { - startTime.set(System.currentTimeMillis()); - scheduler.scheduleAtFixedRate(() -> { - try { - if (!paused.get()) { - long eta = computeETA(); - int processed = chunksProcessed.get(); - double last = processed - chunksProcessedLast.getAndSet(processed); - double cps = last / ((M.ms() - lastCpsTime.getAndSet(M.ms())) / 1000d); - chunksPerSecond.put(cps); - double percentage = ((double) processed / (double) totalMaxChunks.get()) * 100; - if (!cancelled.get()) { - Iris.info("Updated: " + Form.f(processed) + " of " + Form.f(totalMaxChunks.get()) + " (%.0f%%) " + Form.f(chunksPerSecond.getAverage()) + "/s, ETA: " + Form.duration(eta, - 2), percentage); - } - } - } catch (Exception e) { - Iris.reportError(e); - e.printStackTrace(); - } - }, 0, 3, TimeUnit.SECONDS); - scheduler.scheduleAtFixedRate(() -> { - boolean empty = Bukkit.getOnlinePlayers().isEmpty(); - if (serverEmpty.getAndSet(empty) == empty) - return; - loadBalancer.setRange(empty ? IrisSettings.get().getUpdater().emptyMsRange : IrisSettings.get().getUpdater().defaultMsRange); - }, 0, 10, TimeUnit.SECONDS); - - var t = new Thread(() -> { - run(); - close(); - }, "Iris Chunk Updater - " + world.getName()); - t.setPriority(Thread.MAX_PRIORITY); - t.start(); - - Iris.service(PreservationSVC.class).register(t); - } catch (Exception e) { - e.printStackTrace(); - } - } - - public void close() { - try { - loadBalancer.close(); - semaphore.acquire(256); - - chunkExecutor.shutdown(); - chunkExecutor.awaitTermination(5, TimeUnit.SECONDS); - scheduler.shutdownNow(); - unloadAndSaveAllChunks(); - } catch (Exception ignored) {} - if (cancelled.get()) { - Iris.info("Updated: " + Form.f(chunksUpdated.get()) + " Chunks"); - Iris.info("Irritated: " + Form.f(chunksProcessed.get()) + " of " + Form.f(totalMaxChunks.get())); - Iris.info("Stopped updater."); - } else { - Iris.info("Processed: " + Form.f(chunksProcessed.get()) + " Chunks"); - Iris.info("Finished Updating: " + Form.f(chunksUpdated.get()) + " Chunks"); - } - } - - private void run() { - task.iterateRegions((rX, rZ) -> { - if (cancelled.get()) - return; - - while (paused.get()) { - J.sleep(50); - } - - if (rX < dimensions.min.getX() || - rX > dimensions.max.getX() || - rZ < dimensions.min.getZ() || - rZ > dimensions.max.getZ() || - !new File(world.getWorldFolder(), REGION_PATH + rX + "." + rZ + ".mca").exists() - ) return; - - task.iterateChunks(rX, rZ, (x, z) -> { - while (paused.get() && !cancelled.get()) { - J.sleep(50); - } - - try { - semaphore.acquire(); - } catch (InterruptedException ignored) { - return; - } - chunkExecutor.submit(() -> { - try { - if (!cancelled.get()) - processChunk(x, z); - } finally { - latch.countDown(); - semaphore.release(); - } - }); - }); - }); - } - - private void processChunk(int x, int z) { - if (!loadChunksIfGenerated(x, z)) { - chunksProcessed.getAndIncrement(); - return; - } - - var mc = engine.getMantle().getMantle().getChunk(x, z).use(); - try { - Chunk c = world.getChunkAt(x, z); - engine.updateChunk(c); - - removeTickets(x, z); - } finally { - chunksUpdated.incrementAndGet(); - chunksProcessed.getAndIncrement(); - mc.release(); - } - } - - private boolean loadChunksIfGenerated(int x, int z) { - if (engine.getMantle().getMantle().hasFlag(x, z, MantleFlag.ETCHED)) - return false; - - for (int dx = -1; dx <= 1; dx++) { - for (int dz = -1; dz <= 1; dz++) { - if (!PaperLib.isChunkGenerated(world, x + dx, z + dz)) { - return false; - } - } - } - - AtomicBoolean generated = new AtomicBoolean(true); - CountDownLatch latch = new CountDownLatch(9); - for (int dx = -1; dx <= 1; dx++) { - for (int dz = -1; dz <= 1; dz++) { - int xx = x + dx; - int zz = z + dz; - PaperLib.getChunkAtAsync(world, xx, zz, false, true) - .thenAccept(chunk -> { - if (chunk == null || !chunk.isGenerated()) { - latch.countDown(); - generated.set(false); - return; - } - holder.addTicket(chunk); - latch.countDown(); - }); - } - } - - try { - latch.await(); - } catch (InterruptedException e) { - Iris.info("Interrupted while waiting for chunks to load"); - } - - if (generated.get()) return true; - removeTickets(x, z); - return false; - } - - private void removeTickets(int x, int z) { - for (int xx = -1; xx <= 1; xx++) { - for (int zz = -1; zz <= 1; zz++) { - holder.removeTicket(x + xx, z + zz); - } - } - } - - private void unloadAndSaveAllChunks() { - if (J.isFolia()) { - return; - } - - try { - J.sfut(() -> { - if (world == null) { - Iris.warn("World was null somehow..."); - return; - } - - world.save(); - }).get(); - } catch (Throwable e) { - Iris.reportError(e); - e.printStackTrace(); - } - } - - private long computeETA() { - return (long) (totalMaxChunks.get() > 1024 ? // Generated chunks exceed 1/8th of total? - // If yes, use smooth function (which gets more accurate over time since its less sensitive to outliers) - ((totalMaxChunks.get() - chunksProcessed.get()) * ((double) (M.ms() - startTime.get()) / (double) chunksProcessed.get())) : - // If no, use quick function (which is less accurate over time but responds better to the initial delay) - ((totalMaxChunks.get() - chunksProcessed.get()) / chunksPerSecond.getAverage()) * 1000 - ); - } - - private Dimensions calculateWorldDimensions(File regionDir) { - File[] files = regionDir.listFiles((dir, name) -> name.endsWith(".mca")); - - int minX = Integer.MAX_VALUE; - int maxX = Integer.MIN_VALUE; - int minZ = Integer.MAX_VALUE; - int maxZ = Integer.MIN_VALUE; - - for (File file : files) { - String[] parts = file.getName().split("\\."); - int x = Integer.parseInt(parts[1]); - int z = Integer.parseInt(parts[2]); - - minX = Math.min(minX, x); - maxX = Math.max(maxX, x); - minZ = Math.min(minZ, z); - maxZ = Math.max(maxZ, z); - } - int oX = minX + ((maxX - minX) / 2); - int oZ = minZ + ((maxZ - minZ) / 2); - - int height = maxX - minX + 1; - int width = maxZ - minZ + 1; - - return new Dimensions(new Position2(minX, minZ), new Position2(maxX, maxZ), height * width, PregenTask.builder() - .radiusZ((int) Math.ceil(width / 2d * 512)) - .radiusX((int) Math.ceil(height / 2d * 512)) - .center(new Position2(oX, oZ)) - .build()); - } - - private record Dimensions(Position2 min, Position2 max, int count, PregenTask task) { } -} diff --git a/core/src/main/java/art/arcane/iris/core/pregenerator/LazyPregenerator.java b/core/src/main/java/art/arcane/iris/core/pregenerator/LazyPregenerator.java deleted file mode 100644 index 57b8ac822..000000000 --- a/core/src/main/java/art/arcane/iris/core/pregenerator/LazyPregenerator.java +++ /dev/null @@ -1,295 +0,0 @@ -package art.arcane.iris.core.pregenerator; - -import com.google.gson.Gson; -import art.arcane.iris.Iris; -import art.arcane.iris.util.common.format.C; -import art.arcane.volmlib.util.format.Form; -import art.arcane.volmlib.util.io.IO; -import art.arcane.volmlib.util.math.M; -import art.arcane.volmlib.util.math.Position2; -import art.arcane.volmlib.util.math.RollingSequence; -import art.arcane.volmlib.util.math.Spiraler; -import art.arcane.volmlib.util.scheduling.ChronoLatch; -import art.arcane.iris.util.common.scheduling.J; -import io.papermc.lib.PaperLib; -import lombok.Data; -import lombok.Getter; -import org.bukkit.Bukkit; -import org.bukkit.World; -import org.bukkit.event.EventHandler; -import org.bukkit.event.Listener; -import org.bukkit.event.world.WorldUnloadEvent; - -import java.io.File; -import java.io.IOException; -import java.util.concurrent.CountDownLatch; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; -import java.util.concurrent.atomic.AtomicInteger; -import java.util.concurrent.atomic.AtomicLong; - -import java.util.HashMap; -import java.util.Map; - -public class LazyPregenerator extends Thread implements Listener { - @Getter - private static LazyPregenerator instance; - private final LazyPregenJob job; - private final File destination; - private final int maxPosition; - private World world; - private final long rate; - private final ChronoLatch latch; - private static AtomicInteger lazyGeneratedChunks; - private final AtomicInteger generatedLast; - private final AtomicInteger lazyTotalChunks; - private final AtomicLong startTime; - private final RollingSequence chunksPerSecond; - private final RollingSequence chunksPerMinute; - - private static final Map jobs = new HashMap<>(); - - public LazyPregenerator(LazyPregenJob job, File destination) { - this.job = job; - this.destination = destination; - this.maxPosition = new Spiraler(job.getRadiusBlocks() * 2, job.getRadiusBlocks() * 2, (x, z) -> { - }).count(); - this.world = Bukkit.getWorld(job.getWorld()); - this.rate = Math.round((1D / (job.getChunksPerMinute() / 60D)) * 1000D); - this.latch = new ChronoLatch(15000); - this.startTime = new AtomicLong(M.ms()); - this.chunksPerSecond = new RollingSequence(10); - this.chunksPerMinute = new RollingSequence(10); - lazyGeneratedChunks = new AtomicInteger(0); - this.generatedLast = new AtomicInteger(0); - this.lazyTotalChunks = new AtomicInteger((int) Math.ceil(Math.pow((2.0 * job.getRadiusBlocks()) / 16, 2))); - jobs.put(job.getWorld(), job); - LazyPregenerator.instance = this; - } - - public LazyPregenerator(File file) throws IOException { - this(new Gson().fromJson(IO.readAll(file), LazyPregenJob.class), file); - } - - public static void loadLazyGenerators() { - for (World i : Bukkit.getWorlds()) { - File lazygen = new File(i.getWorldFolder(), "lazygen.json"); - if (lazygen.exists()) { - try { - LazyPregenerator p = new LazyPregenerator(lazygen); - p.start(); - Iris.info("Started Lazy Pregenerator: " + p.job); - } catch (IOException e) { - throw new RuntimeException(e); - } - } - } - } - - @EventHandler - public void on(WorldUnloadEvent e) { - if (e.getWorld().equals(world)) { - interrupt(); - } - } - - public void run() { - while (!interrupted()) { - J.sleep(rate); - tick(); - } - - try { - saveNow(); - } catch (IOException e) { - throw new RuntimeException(e); - } - } - - public void tick() { - LazyPregenJob job = jobs.get(world.getName()); - if (latch.flip() && !job.paused) { - long eta = computeETA(); - save(); - int secondGenerated = lazyGeneratedChunks.get() - generatedLast.get(); - generatedLast.set(lazyGeneratedChunks.get()); - secondGenerated = secondGenerated / 15; - chunksPerSecond.put(secondGenerated); - chunksPerMinute.put(secondGenerated * 60); - if (!job.isSilent()) { - Iris.info("LazyGen: " + C.IRIS + world.getName() + C.RESET + " RTT: " + Form.f(lazyGeneratedChunks.get()) + " of " + Form.f(lazyTotalChunks.get()) + " " + Form.f((int) chunksPerMinute.getAverage()) + "/m ETA: " + Form.duration((double) eta, 2)); - } - } - - if (lazyGeneratedChunks.get() >= lazyTotalChunks.get()) { - if (job.isHealing()) { - Iris.warn("LazyGen healing mode is not supported on 1.21.11; ending lazy generation for " + world.getName() + "."); - job.setHealing(false); - } - Iris.info("Completed Lazy Gen!"); - interrupt(); - } else { - int pos = job.getPosition() + 1; - job.setPosition(pos); - if (!job.paused) { - tickGenerate(getChunk(pos)); - } - } - } - - private long computeETA() { - return (long) ((lazyTotalChunks.get() - lazyGeneratedChunks.get()) / chunksPerMinute.getAverage()) * 1000; - // todo broken - } - - private final ExecutorService executorService = Executors.newSingleThreadExecutor(); - - private void tickGenerate(Position2 chunk) { - executorService.submit(() -> { - CountDownLatch latch = new CountDownLatch(1); - if (PaperLib.isPaper()) { - PaperLib.getChunkAtAsync(world, chunk.getX(), chunk.getZ(), true) - .thenAccept((i) -> { - Iris.verbose("Generated Async " + chunk); - latch.countDown(); - }); - } else { - J.s(() -> { - world.getChunkAt(chunk.getX(), chunk.getZ()); - Iris.verbose("Generated " + chunk); - latch.countDown(); - }); - } - try { - latch.await(); - } catch (InterruptedException ex) { - Thread.currentThread().interrupt(); - Iris.verbose("Lazy pregenerator worker interrupted while waiting for chunk " + chunk + "."); - } - lazyGeneratedChunks.addAndGet(1); - }); - } - - public Position2 getChunk(int position) { - int p = -1; - AtomicInteger xx = new AtomicInteger(); - AtomicInteger zz = new AtomicInteger(); - Spiraler s = new Spiraler(job.getRadiusBlocks() * 2, job.getRadiusBlocks() * 2, (x, z) -> { - xx.set(x); - zz.set(z); - }); - - while (s.hasNext() && p++ < position) { - s.next(); - } - - return new Position2(xx.get(), zz.get()); - } - - public void save() { - J.a(() -> { - try { - saveNow(); - } catch (Throwable e) { - e.printStackTrace(); - } - }); - } - - public static void setPausedLazy(World world) { - LazyPregenJob job = jobs.get(world.getName()); - if (isPausedLazy(world)){ - job.paused = false; - } else { - job.paused = true; - } - - if ( job.paused) { - Iris.info(C.BLUE + "LazyGen: " + C.IRIS + world.getName() + C.BLUE + " Paused"); - } else { - Iris.info(C.BLUE + "LazyGen: " + C.IRIS + world.getName() + C.BLUE + " Resumed"); - } - } - - public static boolean isPausedLazy(World world) { - LazyPregenJob job = jobs.get(world.getName()); - return job != null && job.isPaused(); - } - - public static long remainingChunks() { - LazyPregenerator local = instance; - AtomicInteger generated = lazyGeneratedChunks; - if (local == null || generated == null) { - return -1L; - } - - return Math.max(0L, local.lazyTotalChunks.get() - generated.get()); - } - - public static double chunksPerSecond() { - LazyPregenerator local = instance; - if (local == null) { - return 0D; - } - - return Math.max(0D, local.chunksPerMinute.getAverage() / 60D); - } - - public void shutdownInstance(World world) throws IOException { - Iris.info("LazyGen: " + C.IRIS + world.getName() + C.BLUE + " Shutting down.."); - LazyPregenJob job = jobs.get(world.getName()); - File worldDirectory = new File(Bukkit.getWorldContainer(), world.getName()); - File lazyFile = new File(worldDirectory, "lazygen.json"); - - if (job == null) { - Iris.error("No Lazygen job found for world: " + world.getName()); - return; - } - - try { - if (!job.isPaused()) { - job.setPaused(true); - } - save(); - jobs.remove(world.getName()); - J.a(() -> { - while (lazyFile.exists()) { - lazyFile.delete(); - J.sleep(1000); - } - Iris.info("LazyGen: " + C.IRIS + world.getName() + C.BLUE + " File deleted and instance closed."); - }, 20); - } catch (Exception e) { - Iris.error("Failed to shutdown Lazygen for " + world.getName()); - e.printStackTrace(); - } finally { - saveNow(); - interrupt(); - } - } - - - public void saveNow() throws IOException { - IO.writeAll(this.destination, new Gson().toJson(job)); - } - - @Data - @lombok.Builder - public static class LazyPregenJob { - private String world; - @lombok.Builder.Default - private int healingPosition = 0; - @lombok.Builder.Default - private boolean healing = false; - @lombok.Builder.Default - private int chunksPerMinute = 32; - @lombok.Builder.Default - private int radiusBlocks = 5000; - @lombok.Builder.Default - private int position = 0; - @lombok.Builder.Default - boolean silent = false; - @lombok.Builder.Default - boolean paused = false; - } -} diff --git a/core/src/main/java/art/arcane/iris/core/pregenerator/TurboPregenerator.java b/core/src/main/java/art/arcane/iris/core/pregenerator/TurboPregenerator.java deleted file mode 100644 index 1436ee289..000000000 --- a/core/src/main/java/art/arcane/iris/core/pregenerator/TurboPregenerator.java +++ /dev/null @@ -1,357 +0,0 @@ -package art.arcane.iris.core.pregenerator; - -import com.google.gson.Gson; -import art.arcane.iris.Iris; -import art.arcane.iris.core.IrisSettings; -import art.arcane.volmlib.util.collection.KList; -import art.arcane.iris.util.common.format.C; -import art.arcane.volmlib.util.format.Form; -import art.arcane.volmlib.util.io.IO; -import art.arcane.volmlib.util.math.M; -import art.arcane.volmlib.util.math.Position2; -import art.arcane.volmlib.util.math.RollingSequence; -import art.arcane.volmlib.util.math.Spiraler; -import art.arcane.iris.util.common.parallel.BurstExecutor; -import art.arcane.iris.util.common.parallel.HyperLock; -import art.arcane.iris.util.common.parallel.MultiBurst; -import art.arcane.volmlib.util.scheduling.ChronoLatch; -import art.arcane.iris.util.common.scheduling.J; -import art.arcane.volmlib.util.scheduling.PrecisionStopwatch; -import io.papermc.lib.PaperLib; -import lombok.Data; -import lombok.Getter; -import org.apache.logging.log4j.core.util.ExecutorServices; -import org.bukkit.Bukkit; -import org.bukkit.World; -import org.bukkit.event.EventHandler; -import org.bukkit.event.Listener; -import org.bukkit.event.world.WorldUnloadEvent; -import org.checkerframework.checker.units.qual.N; - -import java.io.File; -import java.io.IOException; -import java.lang.reflect.Array; -import java.util.*; -import java.util.concurrent.*; -import java.util.concurrent.atomic.AtomicBoolean; -import java.util.concurrent.atomic.AtomicInteger; -import java.util.concurrent.atomic.AtomicLong; -import java.util.concurrent.locks.ReentrantLock; -import java.util.stream.IntStream; - -public class TurboPregenerator extends Thread implements Listener { - @Getter - private static TurboPregenerator instance; - private final TurboPregenJob job; - private final File destination; - private final int maxPosition; - private World world; - private final ChronoLatch latch; - private static AtomicInteger turboGeneratedChunks; - private final AtomicInteger generatedLast; - private final AtomicLong cachedLast; - private final RollingSequence cachePerSecond; - private final AtomicInteger turboTotalChunks; - private final AtomicLong startTime; - private final RollingSequence chunksPerSecond; - private final RollingSequence chunksPerMinute; - private KList queue; - private ConcurrentHashMap cache; - private AtomicInteger maxWaiting; - private ReentrantLock cachinglock; - private AtomicBoolean caching; - private final HyperLock hyperLock; - private MultiBurst burst; - private static final Map jobs = new HashMap<>(); - - public TurboPregenerator(TurboPregenJob job, File destination) { - this.job = job; - queue = new KList<>(512); - this.maxWaiting = new AtomicInteger(128); - this.destination = destination; - this.maxPosition = new Spiraler(job.getRadiusBlocks() * 2, job.getRadiusBlocks() * 2, (x, z) -> { - }).count(); - this.world = Bukkit.getWorld(job.getWorld()); - this.latch = new ChronoLatch(3000); - this.burst = MultiBurst.burst; - this.hyperLock = new HyperLock(); - this.startTime = new AtomicLong(M.ms()); - this.cachePerSecond = new RollingSequence(10); - this.chunksPerSecond = new RollingSequence(10); - this.chunksPerMinute = new RollingSequence(10); - turboGeneratedChunks = new AtomicInteger(0); - this.generatedLast = new AtomicInteger(0); - this.cachedLast = new AtomicLong(0); - this.caching = new AtomicBoolean(false); - this.turboTotalChunks = new AtomicInteger((int) Math.ceil(Math.pow((2.0 * job.getRadiusBlocks()) / 16, 2))); - cache = new ConcurrentHashMap<>(turboTotalChunks.get()); - this.cachinglock = new ReentrantLock(); - jobs.put(job.getWorld(), job); - TurboPregenerator.instance = this; - } - - public TurboPregenerator(File file) throws IOException { - this(new Gson().fromJson(IO.readAll(file), TurboPregenerator.TurboPregenJob.class), file); - } - - public static void loadTurboGenerator(String i) { - World x = Bukkit.getWorld(i); - File turbogen = new File(x.getWorldFolder(), "turbogen.json"); - if (turbogen.exists()) { - try { - TurboPregenerator p = new TurboPregenerator(turbogen); - p.start(); - Iris.info("Started Turbo Pregenerator: " + p.job); - } catch (IOException e) { - throw new RuntimeException(e); - } - } - - } - - @EventHandler - public void on(WorldUnloadEvent e) { - if (e.getWorld().equals(world)) { - interrupt(); - } - } - - public void run() { - while (!interrupted()) { - tick(); - } - - try { - saveNow(); - } catch (IOException e) { - throw new RuntimeException(e); - } - } - - public void tick() { - TurboPregenJob job = jobs.get(world.getName()); - if (!cachinglock.isLocked() && cache.isEmpty() && !caching.get()) { - ExecutorService cache = Executors.newFixedThreadPool(1); - cache.submit(this::cache); - } - - if (latch.flip() && caching.get()) { - long secondCached = cache.mappingCount() - cachedLast.get(); - cachedLast.set(cache.mappingCount()); - secondCached = secondCached / 3; - cachePerSecond.put(secondCached); - Iris.info("TurboGen: " + C.IRIS + world.getName() + C.RESET + C.BLUE + " Caching: " + Form.f(cache.mappingCount()) + " of " + Form.f(turboTotalChunks.get()) + " " + Form.f((int) cachePerSecond.getAverage()) + "/s"); - } - - if (latch.flip() && !job.paused && !cachinglock.isLocked()) { - long eta = computeETA(); - save(); - int secondGenerated = turboGeneratedChunks.get() - generatedLast.get(); - generatedLast.set(turboGeneratedChunks.get()); - secondGenerated = secondGenerated / 3; - chunksPerSecond.put(secondGenerated); - chunksPerMinute.put(secondGenerated * 60); - Iris.info("TurboGen: " + C.IRIS + world.getName() + C.RESET + " RTT: " + Form.f(turboGeneratedChunks.get()) + " of " + Form.f(turboTotalChunks.get()) + " " + Form.f((int) chunksPerSecond.getAverage()) + "/s ETA: " + Form.duration((double) eta, 2)); - - } - if (turboGeneratedChunks.get() >= turboTotalChunks.get()) { - Iris.info("Completed Turbo Gen!"); - interrupt(); - } else { - if (!cachinglock.isLocked()) { - int pos = job.getPosition() + 1; - job.setPosition(pos); - if (!job.paused) { - if (queue.size() < maxWaiting.get()) { - Position2 chunk = cache.get(pos); - queue.add(chunk); - } - waitForChunksPartial(); - } - } - } - } - - private void cache() { - if (!cachinglock.isLocked()) { - cachinglock.lock(); - caching.set(true); - PrecisionStopwatch p = PrecisionStopwatch.start(); - BurstExecutor b = MultiBurst.burst.burst(turboTotalChunks.get()); - b.setMulticore(true); - int[] list = IntStream.rangeClosed(0, turboTotalChunks.get()).toArray(); - AtomicInteger order = new AtomicInteger(turboTotalChunks.get()); - - int threads = Runtime.getRuntime().availableProcessors(); - if (threads > 1) threads--; - ExecutorService process = Executors.newFixedThreadPool(threads); - - for (int id : list) { - b.queue(() -> { - cache.put(id, getChunk(id)); - order.addAndGet(-1); - }); - } - b.complete(); - - if (order.get() < 0) { - cachinglock.unlock(); - caching.set(false); - Iris.info("Completed Caching in: " + Form.duration(p.getMilliseconds(), 2)); - } - } else { - Iris.error("TurboCache is locked!"); - } - } - - private void waitForChunksPartial() { - while (!queue.isEmpty() && maxWaiting.get() > queue.size()) { - try { - for (Position2 c : new KList<>(queue)) { - tickGenerate(c); - queue.remove(c); - } - } catch (Exception e) { - e.printStackTrace(); - } - } - } - - private long computeETA() { - return (long) ((turboTotalChunks.get() - turboGeneratedChunks.get()) / chunksPerMinute.getAverage()) * 1000; - // todo broken - } - - private final ExecutorService executorService = Executors.newFixedThreadPool(10); - private void tickGenerate(Position2 chunk) { - executorService.submit(() -> { - CountDownLatch latch = new CountDownLatch(1); - PaperLib.getChunkAtAsync(world, chunk.getX(), chunk.getZ(), true) - .thenAccept((i) -> { - latch.countDown(); - }); - try { - latch.await(); - } catch (InterruptedException ex) { - Thread.currentThread().interrupt(); - Iris.verbose("Turbo pregenerator worker interrupted while waiting for chunk " + chunk + "."); - } - turboGeneratedChunks.addAndGet(1); - }); - } - - public Position2 getChunk(int position) { - int p = -1; - AtomicInteger xx = new AtomicInteger(); - AtomicInteger zz = new AtomicInteger(); - Spiraler s = new Spiraler(job.getRadiusBlocks() * 2, job.getRadiusBlocks() * 2, (x, z) -> { - xx.set(x); - zz.set(z); - }); - - while (s.hasNext() && p++ < position) { - s.next(); - } - - return new Position2(xx.get(), zz.get()); - } - - public void save() { - J.a(() -> { - try { - saveNow(); - } catch (Throwable e) { - e.printStackTrace(); - } - }); - } - - public static void setPausedTurbo(World world) { - TurboPregenJob job = jobs.get(world.getName()); - if (isPausedTurbo(world)) { - job.paused = false; - } else { - job.paused = true; - } - - if (job.paused) { - Iris.info(C.BLUE + "TurboGen: " + C.IRIS + world.getName() + C.BLUE + " Paused"); - } else { - Iris.info(C.BLUE + "TurboGen: " + C.IRIS + world.getName() + C.BLUE + " Resumed"); - } - } - - public static boolean isPausedTurbo(World world) { - TurboPregenJob job = jobs.get(world.getName()); - return job != null && job.isPaused(); - } - - public static long remainingChunks() { - TurboPregenerator local = instance; - AtomicInteger generated = turboGeneratedChunks; - if (local == null || generated == null) { - return -1L; - } - - return Math.max(0L, local.turboTotalChunks.get() - generated.get()); - } - - public static double chunksPerSecond() { - TurboPregenerator local = instance; - if (local == null) { - return 0D; - } - - return Math.max(0D, local.chunksPerSecond.getAverage()); - } - - public void shutdownInstance(World world) throws IOException { - Iris.info("turboGen: " + C.IRIS + world.getName() + C.BLUE + " Shutting down.."); - TurboPregenJob job = jobs.get(world.getName()); - File worldDirectory = new File(Bukkit.getWorldContainer(), world.getName()); - File turboFile = new File(worldDirectory, "turbogen.json"); - - if (job == null) { - Iris.error("No turbogen job found for world: " + world.getName()); - return; - } - - try { - if (!job.isPaused()) { - job.setPaused(true); - } - save(); - jobs.remove(world.getName()); - J.a(() -> { - while (turboFile.exists()) { - turboFile.delete(); - J.sleep(1000); - } - Iris.info("turboGen: " + C.IRIS + world.getName() + C.BLUE + " File deleted and instance closed."); - }, 20); - } catch (Exception e) { - Iris.error("Failed to shutdown turbogen for " + world.getName()); - e.printStackTrace(); - } finally { - saveNow(); - interrupt(); - } - } - - - public void saveNow() throws IOException { - IO.writeAll(this.destination, new Gson().toJson(job)); - } - - @Data - @lombok.Builder - public static class TurboPregenJob { - private String world; - @lombok.Builder.Default - private int radiusBlocks = 5000; - @lombok.Builder.Default - private int position = 0; - @lombok.Builder.Default - boolean paused = false; - } -} diff --git a/core/src/main/java/art/arcane/iris/core/pregenerator/methods/AsyncPregenMethod.java b/core/src/main/java/art/arcane/iris/core/pregenerator/methods/AsyncPregenMethod.java index 14543cf1a..052274132 100644 --- a/core/src/main/java/art/arcane/iris/core/pregenerator/methods/AsyncPregenMethod.java +++ b/core/src/main/java/art/arcane/iris/core/pregenerator/methods/AsyncPregenMethod.java @@ -52,6 +52,9 @@ import java.util.concurrent.atomic.AtomicLong; public class AsyncPregenMethod implements PregeneratorMethod { private static final AtomicInteger THREAD_COUNT = new AtomicInteger(); private static final int ADAPTIVE_TIMEOUT_STEP = 3; + private static final int ADAPTIVE_RECOVERY_INTERVAL = 8; + private static final long CHUNK_CLEANUP_INTERVAL_MS = 15_000L; + private static final long CHUNK_CLEANUP_MIN_AGE_MS = 5_000L; private final World world; private final IrisRuntimeSchedulerMode runtimeSchedulerMode; private final IrisPaperLikeBackendMode paperLikeBackendMode; @@ -84,6 +87,7 @@ public class AsyncPregenMethod implements PregeneratorMethod { private final AtomicLong failed = new AtomicLong(); private final AtomicLong lastProgressAt = new AtomicLong(M.ms()); private final AtomicLong lastPermitWaitLog = new AtomicLong(0L); + private final AtomicLong lastChunkCleanup = new AtomicLong(M.ms()); private final Object permitMonitor = new Object(); private volatile Engine metricsEngine; @@ -120,8 +124,11 @@ public class AsyncPregenMethod implements PregeneratorMethod { this.backendMode = "paper-ticket"; } } + int runtimeMaxConcurrency = foliaRuntime + ? pregen.getFoliaMaxConcurrency() + : pregen.getPaperLikeMaxConcurrency(); int configuredThreads = applyRuntimeConcurrencyCap( - pregen.getMaxConcurrency(), + runtimeMaxConcurrency, foliaRuntime, workerThreadsForCap ); @@ -206,6 +213,48 @@ public class AsyncPregenMethod implements PregeneratorMethod { } } + private void periodicChunkCleanup() { + long now = M.ms(); + long lastCleanup = lastChunkCleanup.get(); + if (now - lastCleanup < CHUNK_CLEANUP_INTERVAL_MS) { + return; + } + + if (!lastChunkCleanup.compareAndSet(lastCleanup, now)) { + return; + } + + if (foliaRuntime) { + int sizeBefore = lastUse.size(); + if (sizeBefore > 0) { + lastUse.clear(); + Iris.info("Periodic chunk cleanup: cleared " + sizeBefore + " Folia chunk references"); + } + return; + } + + int sizeBefore = lastUse.size(); + if (sizeBefore == 0) { + return; + } + + long minTime = now - CHUNK_CLEANUP_MIN_AGE_MS; + AtomicInteger removed = new AtomicInteger(); + lastUse.entrySet().removeIf(entry -> { + Long lastUseTime = entry.getValue(); + if (lastUseTime == null || lastUseTime < minTime) { + removed.incrementAndGet(); + return true; + } + return false; + }); + + int removedCount = removed.get(); + if (removedCount > 0) { + Iris.info("Periodic chunk cleanup: removed " + removedCount + "/" + sizeBefore + " stale chunk references"); + } + } + private Chunk onChunkFutureFailure(int x, int z, Throwable throwable) { Throwable root = throwable; while (root.getCause() != null) { @@ -246,11 +295,14 @@ public class AsyncPregenMethod implements PregeneratorMethod { private void onSuccess() { int streak = timeoutStreak.get(); if (streak > 0) { - timeoutStreak.compareAndSet(streak, streak - 1); - return; + int newStreak = Math.max(0, streak - 2); + timeoutStreak.compareAndSet(streak, newStreak); + if (newStreak > 0) { + return; + } } - if ((completed.get() & 31L) == 0L) { + if ((completed.get() & (ADAPTIVE_RECOVERY_INTERVAL - 1)) == 0L) { raiseAdaptiveInFlightLimit(); } } @@ -278,7 +330,9 @@ public class AsyncPregenMethod implements PregeneratorMethod { return; } - int next = Math.min(threads, current + 1); + int deficit = threads - current; + int step = deficit > (threads / 2) ? Math.max(2, threads / 8) : 1; + int next = Math.min(threads, current + step); if (adaptiveInFlightLimit.compareAndSet(current, next)) { logAdaptiveLimit("increase", next); notifyPermitWaiters(); @@ -301,13 +355,13 @@ public class AsyncPregenMethod implements PregeneratorMethod { static int computePaperLikeRecommendedCap(int workerThreads) { int normalizedWorkers = Math.max(1, workerThreads); - int recommendedCap = normalizedWorkers * 2; + int recommendedCap = normalizedWorkers * 4; if (recommendedCap < 8) { return 8; } - if (recommendedCap > 96) { - return 96; + if (recommendedCap > 128) { + return 128; } return recommendedCap; @@ -400,6 +454,16 @@ public class AsyncPregenMethod implements PregeneratorMethod { } } + private void cleanupMantleChunk(int x, int z) { + Engine engine = resolveMetricsEngine(); + if (engine != null) { + try { + engine.getMantle().forceCleanupChunk(x, z); + } catch (Throwable ignored) { + } + } + } + private Engine resolveMetricsEngine() { Engine cachedEngine = metricsEngine; if (cachedEngine != null) { @@ -488,13 +552,14 @@ public class AsyncPregenMethod implements PregeneratorMethod { @Override public void generateChunk(int x, int z, PregenListener listener) { listener.onChunkGenerating(x, z); + periodicChunkCleanup(); try { long waitStart = M.ms(); synchronized (permitMonitor) { while (inFlight.get() >= adaptiveInFlightLimit.get()) { long waited = Math.max(0L, M.ms() - waitStart); logPermitWaitIfNeeded(x, z, waited); - permitMonitor.wait(5000L); + permitMonitor.wait(500L); } } long adaptiveWait = Math.max(0L, M.ms() - waitStart); @@ -503,7 +568,7 @@ public class AsyncPregenMethod implements PregeneratorMethod { } long permitWaitStart = M.ms(); - while (!semaphore.tryAcquire(5, TimeUnit.SECONDS)) { + while (!semaphore.tryAcquire(1, TimeUnit.SECONDS)) { logPermitWaitIfNeeded(x, z, Math.max(0L, M.ms() - waitStart)); } long permitWait = Math.max(0L, M.ms() - permitWaitStart); @@ -698,6 +763,7 @@ public class AsyncPregenMethod implements PregeneratorMethod { } listener.onChunkGenerated(x, z); + cleanupMantleChunk(x, z); listener.onChunkCleaned(x, z); lastUse.put(chunk, M.ms()); success = true; @@ -730,6 +796,7 @@ public class AsyncPregenMethod implements PregeneratorMethod { } listener.onChunkGenerated(x, z); + cleanupMantleChunk(x, z); listener.onChunkCleaned(x, z); lastUse.put(i, M.ms()); success = true; @@ -765,6 +832,7 @@ public class AsyncPregenMethod implements PregeneratorMethod { } listener.onChunkGenerated(x, z); + cleanupMantleChunk(x, z); listener.onChunkCleaned(x, z); lastUse.put(i, M.ms()); success = true; diff --git a/core/src/main/java/art/arcane/iris/core/project/IrisProject.java b/core/src/main/java/art/arcane/iris/core/project/IrisProject.java index 3a6af8cab..53c422ef8 100644 --- a/core/src/main/java/art/arcane/iris/core/project/IrisProject.java +++ b/core/src/main/java/art/arcane/iris/core/project/IrisProject.java @@ -338,7 +338,7 @@ public class IrisProject { public CompletableFuture close() { if (activeProvider == null) { - return CompletableFuture.completedFuture(new StudioOpenCoordinator.StudioCloseResult(null, true, true, false, null, null)); + return CompletableFuture.completedFuture(new StudioOpenCoordinator.StudioCloseResult(null, true, true, false, null)); } return StudioOpenCoordinator.get().closeProject(this); diff --git a/core/src/main/java/art/arcane/iris/core/runtime/SmokeDiagnosticsService.java b/core/src/main/java/art/arcane/iris/core/runtime/SmokeDiagnosticsService.java deleted file mode 100644 index 418d2e26a..000000000 --- a/core/src/main/java/art/arcane/iris/core/runtime/SmokeDiagnosticsService.java +++ /dev/null @@ -1,368 +0,0 @@ -package art.arcane.iris.core.runtime; - -import com.google.gson.Gson; -import com.google.gson.GsonBuilder; -import art.arcane.iris.Iris; -import art.arcane.volmlib.util.io.IO; -import lombok.Data; - -import java.io.File; -import java.io.PrintWriter; -import java.io.StringWriter; -import java.util.ArrayList; -import java.util.List; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.atomic.AtomicLong; -import java.util.concurrent.atomic.AtomicReference; - -public final class SmokeDiagnosticsService { - private static volatile SmokeDiagnosticsService instance; - - private final ConcurrentHashMap reports; - private final AtomicReference latestRunId; - private final AtomicLong runCounter; - private final Gson gson; - - private SmokeDiagnosticsService() { - this.reports = new ConcurrentHashMap<>(); - this.latestRunId = new AtomicReference<>(); - this.runCounter = new AtomicLong(1L); - this.gson = new GsonBuilder().setPrettyPrinting().create(); - } - - public static SmokeDiagnosticsService get() { - SmokeDiagnosticsService current = instance; - if (current != null) { - return current; - } - - synchronized (SmokeDiagnosticsService.class) { - if (instance != null) { - return instance; - } - - instance = new SmokeDiagnosticsService(); - return instance; - } - } - - public SmokeRunHandle beginRun(SmokeRunMode mode, String worldName, boolean studio, boolean headless, String playerName, boolean retainOnFailure) { - long ordinal = runCounter.getAndIncrement(); - String runId = String.format("%s-%05d", mode.id(), ordinal); - SmokeRunReport report = new SmokeRunReport(); - report.setRunId(runId); - report.setMode(mode.id()); - report.setWorldName(worldName); - report.setStudio(studio); - report.setHeadless(headless); - report.setPlayerName(playerName); - report.setRetainOnFailure(retainOnFailure); - report.setStartedAt(System.currentTimeMillis()); - report.setOutcome("running"); - report.setStage("queued"); - report.setLifecycleBackend(art.arcane.iris.core.lifecycle.WorldLifecycleService.get().capabilities().serverFamily().id()); - report.setRuntimeBackend(WorldRuntimeControlService.get().backendName()); - reports.put(runId, report); - latestRunId.set(runId); - persist(report); - return new SmokeRunHandle(report); - } - - public SmokeRunReport latest() { - String runId = latestRunId.get(); - if (runId == null) { - return null; - } - - return get(runId); - } - - public SmokeRunReport get(String runId) { - if (runId == null || runId.isBlank()) { - return null; - } - - SmokeRunReport report = reports.get(runId); - if (report != null) { - return snapshot(report); - } - - return load(runId); - } - - public SmokeRunReport latestPersisted() { - File latestFile = latestFile(); - if (!latestFile.exists()) { - return null; - } - - try { - return gson.fromJson(IO.readAll(latestFile), SmokeRunReport.class); - } catch (Throwable e) { - return null; - } - } - - private SmokeRunReport load(String runId) { - File file = reportFile(runId); - if (!file.exists()) { - return null; - } - - try { - return gson.fromJson(IO.readAll(file), SmokeRunReport.class); - } catch (Throwable e) { - return null; - } - } - - private void persist(SmokeRunReport report) { - if (report == null || !SmokeRunMode.shouldPersist(report.getMode())) { - return; - } - - try { - String json = gson.toJson(report); - File file = reportFile(report.getRunId()); - IO.writeAll(file, json); - IO.writeAll(latestFile(), json); - } catch (Throwable e) { - Iris.reportError("Failed to persist smoke report \"" + report.getRunId() + "\".", e); - } - } - - private SmokeRunReport snapshot(SmokeRunReport report) { - String json = gson.toJson(report); - return gson.fromJson(json, SmokeRunReport.class); - } - - private File reportFile(String runId) { - if (Iris.instance == null) { - File root = new File("plugins/Iris/diagnostics/smoke"); - root.mkdirs(); - return new File(root, runId + ".json"); - } - - return Iris.instance.getDataFile("diagnostics", "smoke", runId + ".json"); - } - - private File latestFile() { - if (Iris.instance == null) { - File root = new File("plugins/Iris/diagnostics/smoke"); - root.mkdirs(); - return new File(root, "latest.json"); - } - - return Iris.instance.getDataFile("diagnostics", "smoke", "latest.json"); - } - - public enum SmokeRunMode { - FULL("full", true), - STUDIO("studio", true), - CREATE("create", true), - BENCHMARK("benchmark", true), - STUDIO_OPEN("studio_open", false), - STUDIO_CLOSE("studio_close", false); - - private final String id; - private final boolean persisted; - - SmokeRunMode(String id, boolean persisted) { - this.id = id; - this.persisted = persisted; - } - - public String id() { - return id; - } - - static boolean shouldPersist(String id) { - for (SmokeRunMode mode : values()) { - if (mode.id.equals(id)) { - return mode.persisted; - } - } - - return false; - } - } - - public final class SmokeRunHandle { - private final SmokeRunReport report; - - private SmokeRunHandle(SmokeRunReport report) { - this.report = report; - } - - public String runId() { - return report.getRunId(); - } - - public SmokeRunReport snapshot() { - synchronized (report) { - report.setElapsedMs(System.currentTimeMillis() - report.getStartedAt()); - return SmokeDiagnosticsService.this.snapshot(report); - } - } - - public void setWorldName(String worldName) { - synchronized (report) { - report.setWorldName(worldName); - report.setElapsedMs(System.currentTimeMillis() - report.getStartedAt()); - persist(report); - } - } - - public void setLifecycleBackend(String backend) { - synchronized (report) { - report.setLifecycleBackend(backend); - persist(report); - } - } - - public void setRuntimeBackend(String backend) { - synchronized (report) { - report.setRuntimeBackend(backend); - persist(report); - } - } - - public void setEntryChunk(int chunkX, int chunkZ) { - synchronized (report) { - report.setEntryChunkX(chunkX); - report.setEntryChunkZ(chunkZ); - report.setElapsedMs(System.currentTimeMillis() - report.getStartedAt()); - persist(report); - } - } - - public void setGenerationSession(long sessionId, int activeLeases) { - synchronized (report) { - report.setGenerationSessionId(sessionId); - report.setGenerationActiveLeases(activeLeases); - report.setElapsedMs(System.currentTimeMillis() - report.getStartedAt()); - persist(report); - } - } - - public void setDatapackReadiness(DatapackReadinessResult readiness) { - synchronized (report) { - report.setDatapackReadiness(readiness); - report.setElapsedMs(System.currentTimeMillis() - report.getStartedAt()); - persist(report); - } - } - - public void setCloseState(boolean unloadCompletedLive, boolean folderDeletionCompletedLive, boolean startupCleanupQueued) { - synchronized (report) { - report.setCloseUnloadCompletedLive(unloadCompletedLive); - report.setCloseFolderDeletionCompletedLive(folderDeletionCompletedLive); - report.setCloseStartupCleanupQueued(startupCleanupQueued); - report.setElapsedMs(System.currentTimeMillis() - report.getStartedAt()); - persist(report); - } - } - - public void note(String text) { - synchronized (report) { - ArrayList notes = new ArrayList<>(report.getNotes()); - notes.add(text); - report.setNotes(List.copyOf(notes)); - report.setElapsedMs(System.currentTimeMillis() - report.getStartedAt()); - persist(report); - } - } - - public void stage(String stage) { - stage(stage, null); - } - - public void stage(String stage, String detail) { - synchronized (report) { - report.setStage(stage); - report.setStageDetail(detail); - report.setElapsedMs(System.currentTimeMillis() - report.getStartedAt()); - persist(report); - } - } - - public void completeSuccess(String finalStage, boolean cleanupApplied) { - synchronized (report) { - report.setStage(finalStage); - report.setOutcome("success"); - report.setCleanupApplied(cleanupApplied); - report.setCompletedAt(System.currentTimeMillis()); - report.setElapsedMs(report.getCompletedAt() - report.getStartedAt()); - persist(report); - } - } - - public void completeFailure(String finalStage, Throwable throwable, boolean cleanupApplied) { - synchronized (report) { - report.setStage(finalStage); - report.setOutcome("failed"); - report.setCleanupApplied(cleanupApplied); - report.setCompletedAt(System.currentTimeMillis()); - report.setElapsedMs(report.getCompletedAt() - report.getStartedAt()); - if (throwable != null) { - report.setFailureType(throwable.getClass().getName()); - report.setFailureMessage(String.valueOf(throwable.getMessage())); - report.setFailureChain(failureChain(throwable)); - report.setFailureStacktrace(stacktrace(throwable)); - } - persist(report); - } - } - - private List failureChain(Throwable throwable) { - ArrayList chain = new ArrayList<>(); - Throwable cursor = throwable; - while (cursor != null) { - chain.add(cursor.getClass().getName() + ": " + String.valueOf(cursor.getMessage())); - cursor = cursor.getCause(); - } - return List.copyOf(chain); - } - - private String stacktrace(Throwable throwable) { - StringWriter writer = new StringWriter(); - PrintWriter printWriter = new PrintWriter(writer); - throwable.printStackTrace(printWriter); - printWriter.flush(); - return writer.toString(); - } - } - - @Data - public static final class SmokeRunReport { - private String runId; - private String mode; - private String worldName; - private String stage; - private String stageDetail; - private long startedAt; - private long completedAt; - private long elapsedMs; - private String outcome; - private String lifecycleBackend; - private String runtimeBackend; - private long generationSessionId; - private int generationActiveLeases; - private Integer entryChunkX; - private Integer entryChunkZ; - private boolean studio; - private boolean headless; - private String playerName; - private boolean retainOnFailure; - private boolean cleanupApplied; - private boolean closeUnloadCompletedLive; - private boolean closeFolderDeletionCompletedLive; - private boolean closeStartupCleanupQueued; - private DatapackReadinessResult datapackReadiness; - private String failureType; - private String failureMessage; - private List failureChain = List.of(); - private String failureStacktrace; - private List notes = List.of(); - } -} diff --git a/core/src/main/java/art/arcane/iris/core/runtime/SmokeTestService.java b/core/src/main/java/art/arcane/iris/core/runtime/SmokeTestService.java deleted file mode 100644 index 1095a95e7..000000000 --- a/core/src/main/java/art/arcane/iris/core/runtime/SmokeTestService.java +++ /dev/null @@ -1,418 +0,0 @@ -package art.arcane.iris.core.runtime; - -import art.arcane.iris.Iris; -import art.arcane.iris.core.ServerConfigurator; -import art.arcane.iris.core.lifecycle.WorldLifecycleService; -import art.arcane.iris.core.tools.IrisCreator; -import art.arcane.iris.core.tools.IrisToolbelt; -import art.arcane.iris.engine.IrisEngine; -import art.arcane.iris.engine.platform.PlatformChunkGenerator; -import art.arcane.iris.util.common.plugin.VolmitSender; -import art.arcane.iris.util.common.scheduling.J; -import art.arcane.volmlib.util.exceptions.IrisException; -import art.arcane.volmlib.util.io.IO; -import org.bukkit.Bukkit; -import org.bukkit.World; - -import java.io.File; -import java.io.IOException; -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; -import java.util.UUID; -import java.util.concurrent.CompletableFuture; - -public final class SmokeTestService { - private static volatile SmokeTestService instance; - - private final SmokeDiagnosticsService diagnostics; - - private SmokeTestService() { - this.diagnostics = SmokeDiagnosticsService.get(); - } - - public static SmokeTestService get() { - SmokeTestService current = instance; - if (current != null) { - return current; - } - - synchronized (SmokeTestService.class) { - if (instance != null) { - return instance; - } - - instance = new SmokeTestService(); - return instance; - } - } - - public String startCreateSmoke(VolmitSender sender, String dimensionKey, long seed, boolean retainOnFailure) { - SmokeDiagnosticsService.SmokeRunHandle handle = diagnostics.beginRun( - SmokeDiagnosticsService.SmokeRunMode.CREATE, - nextWorldName("create"), - false, - true, - null, - retainOnFailure - ); - J.a(() -> executeCreateSmoke(handle, sender, dimensionKey, seed, false, true)); - return handle.runId(); - } - - public String startBenchmarkSmoke(VolmitSender sender, String dimensionKey, long seed, boolean retainOnFailure) { - SmokeDiagnosticsService.SmokeRunHandle handle = diagnostics.beginRun( - SmokeDiagnosticsService.SmokeRunMode.BENCHMARK, - nextWorldName("benchmark"), - false, - true, - null, - retainOnFailure - ); - J.a(() -> executeCreateSmoke(handle, sender, dimensionKey, seed, true, true)); - return handle.runId(); - } - - public String startStudioSmoke(VolmitSender sender, String dimensionKey, long seed, String playerName, boolean retainOnFailure) { - String normalizedPlayer = normalizePlayerName(playerName); - SmokeDiagnosticsService.SmokeRunHandle handle = diagnostics.beginRun( - SmokeDiagnosticsService.SmokeRunMode.STUDIO, - nextWorldName("studio"), - true, - normalizedPlayer == null, - normalizedPlayer, - retainOnFailure - ); - J.a(() -> executeStudioSmoke(handle, sender, dimensionKey, seed, normalizedPlayer, retainOnFailure, true)); - return handle.runId(); - } - - public String startFullSmoke(VolmitSender sender, String dimensionKey, long seed, String playerName, boolean retainOnFailure) { - String normalizedPlayer = normalizePlayerName(playerName); - SmokeDiagnosticsService.SmokeRunHandle handle = diagnostics.beginRun( - SmokeDiagnosticsService.SmokeRunMode.FULL, - nextWorldName("full"), - false, - normalizedPlayer == null, - normalizedPlayer, - retainOnFailure - ); - J.a(() -> executeFullSmoke(handle, sender, dimensionKey, seed, normalizedPlayer, retainOnFailure)); - return handle.runId(); - } - - public SmokeDiagnosticsService.SmokeRunReport latest() { - SmokeDiagnosticsService.SmokeRunReport latest = diagnostics.latest(); - if (latest != null) { - return latest; - } - - return diagnostics.latestPersisted(); - } - - public SmokeDiagnosticsService.SmokeRunReport get(String runId) { - return diagnostics.get(runId); - } - - public WorldInspection inspectWorld(String worldName) { - World world = Bukkit.getWorld(worldName); - if (world == null) { - return null; - } - - PlatformChunkGenerator provider = IrisToolbelt.access(world); - boolean studio = provider != null && provider.isStudio(); - boolean engineClosed = false; - boolean engineFailing = false; - long generationSessionId = 0L; - int activeLeases = 0; - if (provider != null && provider.getEngine() instanceof IrisEngine irisEngine) { - engineClosed = irisEngine.isClosed(); - engineFailing = irisEngine.isFailing(); - generationSessionId = irisEngine.getGenerationSessionId(); - activeLeases = irisEngine.getGenerationSessions().activeLeases(); - } - - ArrayList datapackFolders = new ArrayList<>(); - File datapacksFolder = ServerConfigurator.resolveDatapacksFolder(world.getWorldFolder()); - datapackFolders.add(datapacksFolder.getAbsolutePath()); - return new WorldInspection( - world.getName(), - WorldLifecycleService.get().backendNameForWorld(world.getName()), - WorldRuntimeControlService.get().backendName(), - studio, - engineClosed, - engineFailing, - generationSessionId, - activeLeases, - List.copyOf(datapackFolders), - IrisToolbelt.isWorldMaintenanceActive(world) - ); - } - - private void executeFullSmoke( - SmokeDiagnosticsService.SmokeRunHandle handle, - VolmitSender sender, - String dimensionKey, - long seed, - String playerName, - boolean retainOnFailure - ) { - try { - handle.stage("create"); - executeCreateSmoke(handle, sender, dimensionKey, seed, false, false); - handle.note("create smoke complete"); - - handle.stage("benchmark"); - executeCreateSmoke(handle, sender, dimensionKey, seed, true, false); - handle.note("benchmark smoke complete"); - - handle.stage("studio"); - executeStudioSmoke(handle, sender, dimensionKey, seed, playerName, retainOnFailure, false); - handle.note("studio smoke complete"); - - handle.completeSuccess("cleanup", true); - } catch (Throwable e) { - handle.completeFailure("cleanup", e, !retainOnFailure); - } - } - - private void executeCreateSmoke( - SmokeDiagnosticsService.SmokeRunHandle handle, - VolmitSender sender, - String dimensionKey, - long seed, - boolean benchmark, - boolean completeHandle - ) { - String worldName = nextWorldName(benchmark ? "benchmark" : "create"); - handle.setWorldName(worldName); - cleanupTransientPrefix("iris-smoke-"); - World world = null; - PlatformChunkGenerator provider = null; - boolean cleanupApplied = false; - try { - IrisCreator creator = IrisToolbelt.createWorld() - .dimension(dimensionKey) - .name(worldName) - .seed(seed) - .sender(sender) - .studio(false) - .benchmark(benchmark) - .studioProgressConsumer((progress, stage) -> handle.stage(mapCreateStage(stage))); - world = creator.create(); - provider = IrisToolbelt.access(world); - handle.setLifecycleBackend(WorldLifecycleService.get().backendNameForWorld(world.getName())); - handle.setRuntimeBackend(WorldRuntimeControlService.get().backendName()); - handle.setDatapackReadiness(creator.getLastDatapackReadinessResult()); - captureGenerationSession(provider, handle); - - if (benchmark) { - handle.stage("apply_world_rules"); - WorldRuntimeControlService.get().applyStudioWorldRules(world); - } - - handle.stage("cleanup"); - cleanupWorld(world, worldName); - cleanupApplied = true; - if (completeHandle) { - handle.completeSuccess("cleanup", true); - } - } catch (Throwable e) { - Iris.reportError("Smoke create failed for world \"" + worldName + "\".", e); - if (!handle.snapshot().isRetainOnFailure()) { - try { - cleanupWorld(world, worldName); - cleanupApplied = true; - } catch (Throwable cleanupError) { - Iris.reportError("Smoke cleanup failed for world \"" + worldName + "\".", cleanupError); - } - } - if (completeHandle) { - handle.completeFailure("cleanup", e, cleanupApplied); - } else { - if (e instanceof RuntimeException runtimeException) { - throw runtimeException; - } - - throw new RuntimeException(e); - } - } - } - - private void executeStudioSmoke( - SmokeDiagnosticsService.SmokeRunHandle handle, - VolmitSender sender, - String dimensionKey, - long seed, - String playerName, - boolean retainOnFailure, - boolean completeHandle - ) { - String worldName = nextWorldName("studio"); - handle.setWorldName(worldName); - cleanupTransientPrefix("iris-smoke-"); - World world = null; - boolean cleanupApplied = false; - CompletableFuture future = StudioOpenCoordinator.get().open( - new StudioOpenCoordinator.StudioOpenRequest( - dimensionKey, - null, - sender, - seed, - worldName, - playerName, - false, - retainOnFailure, - SmokeDiagnosticsService.SmokeRunMode.STUDIO, - handle, - completeHandle, - update -> handle.stage(update.stage()), - openedWorld -> { - } - ) - ); - try { - StudioOpenCoordinator.StudioOpenResult result = future.join(); - world = result == null ? null : result.world(); - handle.stage("cleanup"); - cleanupWorld(world, worldName); - cleanupApplied = true; - if (completeHandle) { - handle.completeSuccess("cleanup", true); - } - } catch (Throwable e) { - if (world != null && !cleanupApplied) { - try { - cleanupWorld(world, worldName); - cleanupApplied = true; - } catch (Throwable cleanupError) { - Iris.reportError("Smoke cleanup failed for world \"" + worldName + "\".", cleanupError); - } - } - if (completeHandle && !"failed".equalsIgnoreCase(handle.snapshot().getOutcome())) { - handle.completeFailure("cleanup", e, cleanupApplied); - } - if (!completeHandle) { - if (e instanceof RuntimeException runtimeException) { - throw runtimeException; - } - - throw new RuntimeException(e); - } - } - } - - private void captureGenerationSession(PlatformChunkGenerator provider, SmokeDiagnosticsService.SmokeRunHandle handle) { - if (provider == null || provider.getEngine() == null) { - return; - } - - if (provider.getEngine() instanceof IrisEngine irisEngine) { - handle.setGenerationSession(irisEngine.getGenerationSessionId(), irisEngine.getGenerationSessions().activeLeases()); - } - } - - private void cleanupWorld(World world, String worldName) { - if (world != null) { - PlatformChunkGenerator provider = IrisToolbelt.access(world); - if (provider != null) { - provider.close(); - } - WorldLifecycleService.get().unload(world, false); - } - - File container = Bukkit.getWorldContainer(); - deleteFolder(new File(container, worldName), worldName); - deleteFolder(new File(container, worldName + "_nether"), null); - deleteFolder(new File(container, worldName + "_the_end"), null); - } - - private void deleteFolder(File folder, String worldName) { - if (folder == null) { - return; - } - - IO.delete(folder); - if (!folder.exists()) { - return; - } - - if (worldName == null) { - return; - } - - try { - Iris.queueWorldDeletionOnStartup(Collections.singleton(worldName)); - } catch (IOException e) { - Iris.reportError("Failed to queue smoke world deletion for \"" + worldName + "\".", e); - } - } - - private void cleanupTransientPrefix(String prefix) { - File container = Bukkit.getWorldContainer(); - File[] children = container.listFiles(); - if (children == null) { - return; - } - - for (File child : children) { - if (!child.isDirectory()) { - continue; - } - if (!child.getName().startsWith(prefix)) { - continue; - } - if (Bukkit.getWorld(child.getName()) != null) { - continue; - } - IO.delete(child); - } - } - - private String nextWorldName(String mode) { - return "iris-smoke-" + mode + "-" + UUID.randomUUID().toString().substring(0, 8); - } - - private String normalizePlayerName(String playerName) { - if (playerName == null) { - return null; - } - - String trimmed = playerName.trim(); - if (trimmed.isEmpty() || trimmed.equalsIgnoreCase("none")) { - return null; - } - - return trimmed; - } - - private String mapCreateStage(String stage) { - if (stage == null || stage.isBlank()) { - return "create_world"; - } - - String normalized = stage.trim().toLowerCase(); - return switch (normalized) { - case "resolve_dimension", "resolving dimension" -> "resolve_dimension"; - case "prepare_world_pack", "preparing world pack" -> "prepare_world_pack"; - case "install_datapacks", "installing datapacks" -> "install_datapacks"; - case "create_world", "creating world", "world created" -> "create_world"; - default -> normalized.replace(' ', '_'); - }; - } - - public record WorldInspection( - String worldName, - String lifecycleBackend, - String runtimeBackend, - boolean studio, - boolean engineClosed, - boolean engineFailing, - long generationSessionId, - int activeLeaseCount, - List datapackFolders, - boolean maintenanceActive - ) { - } -} diff --git a/core/src/main/java/art/arcane/iris/core/runtime/StudioOpenCoordinator.java b/core/src/main/java/art/arcane/iris/core/runtime/StudioOpenCoordinator.java index 79ae63b49..6b9eed8fa 100644 --- a/core/src/main/java/art/arcane/iris/core/runtime/StudioOpenCoordinator.java +++ b/core/src/main/java/art/arcane/iris/core/runtime/StudioOpenCoordinator.java @@ -5,7 +5,6 @@ import art.arcane.iris.core.lifecycle.WorldLifecycleService; import art.arcane.iris.core.project.IrisProject; import art.arcane.iris.core.tools.IrisCreator; import art.arcane.iris.core.tools.IrisToolbelt; -import art.arcane.iris.engine.IrisEngine; import art.arcane.iris.engine.platform.PlatformChunkGenerator; import art.arcane.iris.util.common.plugin.VolmitSender; import art.arcane.iris.util.common.scheduling.J; @@ -31,10 +30,7 @@ import java.util.function.Consumer; public final class StudioOpenCoordinator { private static volatile StudioOpenCoordinator instance; - private final SmokeDiagnosticsService diagnostics; - private StudioOpenCoordinator() { - this.diagnostics = SmokeDiagnosticsService.get(); } public static StudioOpenCoordinator get() { @@ -67,94 +63,54 @@ public final class StudioOpenCoordinator { private StudioCloseResult executeClose(IrisProject project) { if (project == null) { - return new StudioCloseResult(null, true, true, false, null, null); + return new StudioCloseResult(null, true, true, false, null); } PlatformChunkGenerator provider = project.getActiveProvider(); if (provider == null) { - return new StudioCloseResult(null, true, true, false, null, null); + return new StudioCloseResult(null, true, true, false, null); } World world = provider.getTarget().getWorld().realWorld(); String worldName = world == null ? provider.getTarget().getWorld().name() : world.getName(); - SmokeDiagnosticsService.SmokeRunHandle handle = diagnostics.beginRun( - SmokeDiagnosticsService.SmokeRunMode.STUDIO_CLOSE, - worldName, - true, - true, - null, - false - ); - StudioCloseResult result; try { - handle.setRuntimeBackend(WorldRuntimeControlService.get().backendName()); - if (world != null) { - handle.setLifecycleBackend(WorldLifecycleService.get().backendNameForWorld(world.getName())); - captureGenerationSession(provider, handle); - } - result = closeWorld(provider, worldName, world, true, handle, project); - handle.setCloseState(result.unloadCompletedLive(), result.folderDeletionCompletedLive(), result.startupCleanupQueued()); - if (result.failureCause() != null) { - handle.completeFailure("finalize_close", result.failureCause(), result.folderDeletionCompletedLive() || result.startupCleanupQueued()); - } else { - handle.completeSuccess("finalize_close", result.folderDeletionCompletedLive() || result.startupCleanupQueued()); - } + return closeWorld(provider, worldName, world, true, project); } catch (Throwable e) { project.setActiveProvider(null); - handle.completeFailure("finalize_close", e, false); - result = new StudioCloseResult(worldName, false, false, false, e, handle.runId()); + return new StudioCloseResult(worldName, false, false, false, e); } - - return result; } private void executeOpen(StudioOpenRequest request, CompletableFuture future) { - boolean ownsHandle = request.runHandle() == null; - SmokeDiagnosticsService.SmokeRunHandle handle = ownsHandle - ? diagnostics.beginRun( - request.mode(), - request.worldName(), - true, - request.playerName() == null || request.playerName().isBlank(), - request.playerName(), - request.retainOnFailure() - ) - : request.runHandle(); World world = null; PlatformChunkGenerator provider = null; - boolean cleanupApplied = false; try { - updateStage(handle, request, "resolve_dimension", 0.04D); + updateStage(request, "resolve_dimension", 0.04D); if (IrisToolbelt.getDimension(request.dimensionKey()) == null) { throw new IrisException("Dimension cannot be found for id " + request.dimensionKey() + "."); } - updateStage(handle, request, "prepare_world_pack", 0.10D); + updateStage(request, "prepare_world_pack", 0.10D); cleanupStaleTransientWorlds(request.worldName()); - updateStage(handle, request, "install_datapacks", 0.18D); + updateStage(request, "install_datapacks", 0.18D); IrisCreator creator = IrisToolbelt.createWorld() .seed(request.seed()) .sender(request.sender()) .studio(true) .name(request.worldName()) .dimension(request.dimensionKey()) - .studioProgressConsumer((progress, stage) -> updateStage(handle, request, mapCreatorStage(stage), progress)); + .studioProgressConsumer((progress, stage) -> updateStage(request, mapCreatorStage(stage), progress)); world = creator.create(); provider = IrisToolbelt.access(world); if (provider == null) { throw new IllegalStateException("Studio runtime provider is unavailable for world \"" + request.worldName() + "\"."); } - handle.setLifecycleBackend(WorldLifecycleService.get().backendNameForWorld(world.getName())); - handle.setRuntimeBackend(WorldRuntimeControlService.get().backendName()); - handle.setDatapackReadiness(creator.getLastDatapackReadinessResult()); - captureGenerationSession(provider, handle); - - updateStage(handle, request, "apply_world_rules", 0.72D); + updateStage(request, "apply_world_rules", 0.72D); WorldRuntimeControlService.get().applyStudioWorldRules(world); - updateStage(handle, request, "prepare_generator", 0.78D); + updateStage(request, "prepare_generator", 0.78D); WorldRuntimeControlService.get().prepareGenerator(world); Location entryAnchor = WorldRuntimeControlService.get().resolveEntryAnchor(world); @@ -163,17 +119,17 @@ public final class StudioOpenCoordinator { } long deadline = System.currentTimeMillis() + TimeUnit.SECONDS.toMillis(120L); - updateStage(handle, request, "request_entry_chunk", 0.84D); - requestEntryChunk(world, entryAnchor, deadline, handle); + updateStage(request, "request_entry_chunk", 0.84D); + requestEntryChunk(world, entryAnchor, deadline); - updateStage(handle, request, "resolve_safe_entry", 0.90D); + updateStage(request, "resolve_safe_entry", 0.90D); Location safeEntry = resolveSafeEntry(world, entryAnchor, deadline); if (safeEntry == null) { throw new IllegalStateException("Studio safe entry resolution timed out."); } if (request.playerName() != null && !request.playerName().isBlank()) { - updateStage(handle, request, "teleport_player", 0.96D); + updateStage(request, "teleport_player", 0.96D); Player player = resolvePlayer(request.playerName()); if (player == null) { throw new IllegalStateException("Player \"" + request.playerName() + "\" is not online."); @@ -186,7 +142,7 @@ public final class StudioOpenCoordinator { } } - updateStage(handle, request, "finalize_open", 1.00D); + updateStage(request, "finalize_open", 1.00D); if (request.project() != null) { request.project().setActiveProvider(provider); } @@ -197,36 +153,24 @@ public final class StudioOpenCoordinator { request.onDone().accept(world); } - if (request.completeHandle()) { - handle.completeSuccess("finalize_open", false); - } else { - handle.stage("finalize_open"); - } - future.complete(new StudioOpenResult(world, handle.runId(), safeEntry, creator.getLastDatapackReadinessResult())); + future.complete(new StudioOpenResult(world, safeEntry, creator.getLastDatapackReadinessResult())); } catch (Throwable e) { Iris.reportError("Studio open failed for world \"" + request.worldName() + "\".", e); if (!request.retainOnFailure()) { try { - updateStage(handle, request, "cleanup", 1.00D); - StudioCloseResult cleanupResult = closeWorld(provider, request.worldName(), world, true, handle, request.project()); - cleanupApplied = cleanupResult.folderDeletionCompletedLive() || cleanupResult.startupCleanupQueued(); + updateStage(request, "cleanup", 1.00D); + closeWorld(provider, request.worldName(), world, true, request.project()); } catch (Throwable cleanupError) { Iris.reportError("Studio cleanup failed for world \"" + request.worldName() + "\".", cleanupError); } } - if (request.completeHandle()) { - handle.completeFailure("cleanup", e, cleanupApplied); - } else { - handle.stage("cleanup", String.valueOf(e.getMessage())); - } future.completeExceptionally(e); } } - private void requestEntryChunk(World world, Location entryAnchor, long deadline, SmokeDiagnosticsService.SmokeRunHandle handle) throws Exception { + private void requestEntryChunk(World world, Location entryAnchor, long deadline) throws Exception { int chunkX = entryAnchor.getBlockX() >> 4; int chunkZ = entryAnchor.getBlockZ() >> 4; - handle.setEntryChunk(chunkX, chunkZ); long remaining = Math.max(1000L, deadline - System.currentTimeMillis()); waitForEntryChunk(world, chunkX, chunkZ, deadline, null).get(remaining, TimeUnit.MILLISECONDS); } @@ -241,25 +185,15 @@ public final class StudioOpenCoordinator { String worldName, World world, boolean deleteFolder, - SmokeDiagnosticsService.SmokeRunHandle handle, IrisProject project ) { Throwable failure = null; boolean unloadCompletedLive = world == null || !isWorldFamilyLoaded(worldName); boolean folderDeletionCompletedLive = !deleteFolder; boolean startupCleanupQueued = false; - CompletableFuture closeFuture = provider == null ? CompletableFuture.completedFuture(null) : CompletableFuture.completedFuture(null); - - updateCloseStage(handle, "prepare_close"); - if (world != null) { - handle.setWorldName(world.getName()); - handle.setLifecycleBackend(WorldLifecycleService.get().backendNameForWorld(world.getName())); - handle.setRuntimeBackend(WorldRuntimeControlService.get().backendName()); - captureGenerationSession(provider, handle); - } + CompletableFuture closeFuture = CompletableFuture.completedFuture(null); if (world != null) { - updateCloseStage(handle, "evacuate_players"); try { evacuatePlayers(world); } catch (Throwable e) { @@ -272,21 +206,17 @@ public final class StudioOpenCoordinator { } try { - updateCloseStage(handle, "seal_runtime"); if (project != null) { project.setActiveProvider(null); } if (provider != null) { - captureGenerationSession(provider, handle); closeFuture = provider.closeAsync(); } - updateCloseStage(handle, "request_unload"); if (worldName != null && !worldName.isBlank()) { requestWorldFamilyUnload(worldName); } - updateCloseStage(handle, "await_unload"); if (worldName != null && !worldName.isBlank()) { long unloadDeadline = System.currentTimeMillis() + TimeUnit.SECONDS.toMillis(20L); CompletableFuture unloadFuture = waitForWorldFamilyUnload(worldName, unloadDeadline); @@ -310,21 +240,17 @@ public final class StudioOpenCoordinator { } if (deleteFolder && worldName != null && !worldName.isBlank()) { - updateCloseStage(handle, "delete_world_family"); WorldFamilyDeleteResult deleteResult = deleteWorldFamily(worldName, unloadCompletedLive); folderDeletionCompletedLive = deleteResult.liveDeleted(); startupCleanupQueued = deleteResult.startupCleanupQueued(); } - - updateCloseStage(handle, "finalize_close"); } finally { if (world != null) { IrisToolbelt.endWorldMaintenance(world, "studio-close"); } } - handle.setCloseState(unloadCompletedLive, folderDeletionCompletedLive, startupCleanupQueued); - return new StudioCloseResult(worldName, unloadCompletedLive, folderDeletionCompletedLive, startupCleanupQueued, failure, handle.runId()); + return new StudioCloseResult(worldName, unloadCompletedLive, folderDeletionCompletedLive, startupCleanupQueued, failure); } private void evacuatePlayers(World world) throws Exception { @@ -414,18 +340,7 @@ public final class StudioOpenCoordinator { } } - private void captureGenerationSession(PlatformChunkGenerator provider, SmokeDiagnosticsService.SmokeRunHandle handle) { - if (provider == null || provider.getEngine() == null) { - return; - } - - if (provider.getEngine() instanceof IrisEngine irisEngine) { - handle.setGenerationSession(irisEngine.getGenerationSessionId(), irisEngine.getGenerationSessions().activeLeases()); - } - } - - private void updateStage(SmokeDiagnosticsService.SmokeRunHandle handle, StudioOpenRequest request, String stage, double progress) { - handle.stage(stage); + private void updateStage(StudioOpenRequest request, String stage, double progress) { if (request.progressConsumer() != null) { request.progressConsumer().accept(new StudioOpenProgress(progress, stage)); } @@ -583,10 +498,6 @@ public final class StudioOpenCoordinator { return null; } - private void updateCloseStage(SmokeDiagnosticsService.SmokeRunHandle handle, String stage) { - handle.stage(stage); - } - private boolean isWorldFamilyLoaded(String worldName) { if (worldName == null || worldName.isBlank()) { return false; @@ -610,9 +521,6 @@ public final class StudioOpenCoordinator { String playerName, boolean openWorkspace, boolean retainOnFailure, - SmokeDiagnosticsService.SmokeRunMode mode, - SmokeDiagnosticsService.SmokeRunHandle runHandle, - boolean completeHandle, Consumer progressConsumer, Consumer onDone ) { @@ -627,9 +535,6 @@ public final class StudioOpenCoordinator { playerName, true, false, - SmokeDiagnosticsService.SmokeRunMode.STUDIO_OPEN, - null, - true, progressConsumer, onDone ); @@ -639,7 +544,7 @@ public final class StudioOpenCoordinator { public record StudioOpenProgress(double progress, String stage) { } - public record StudioOpenResult(World world, String runId, Location entryLocation, DatapackReadinessResult datapackReadiness) { + public record StudioOpenResult(World world, Location entryLocation, DatapackReadinessResult datapackReadiness) { } public record StudioCloseResult( @@ -647,8 +552,7 @@ public final class StudioOpenCoordinator { boolean unloadCompletedLive, boolean folderDeletionCompletedLive, boolean startupCleanupQueued, - Throwable failureCause, - String runId + Throwable failureCause ) { public boolean successful() { return failureCause == null; diff --git a/core/src/main/java/art/arcane/iris/core/safeguard/Mode.java b/core/src/main/java/art/arcane/iris/core/safeguard/Mode.java index ddddb9e6a..6ef3e216f 100644 --- a/core/src/main/java/art/arcane/iris/core/safeguard/Mode.java +++ b/core/src/main/java/art/arcane/iris/core/safeguard/Mode.java @@ -74,7 +74,7 @@ public enum Mode { String[] info = new String[]{ "", - padd2 + color + " Iris, " + C.AQUA + "Dimension Engine " + C.RED + "[" + releaseTrain + " RC.1]", + padd2 + color + " Iris, " + C.AQUA + "Dimension Engine " + C.RED + "[" + releaseTrain + " RC.1.1.6]", padd2 + C.GRAY + " Version: " + color + version, padd2 + C.GRAY + " By: " + color + "Volmit Software (Arcane Arts)", padd2 + C.GRAY + " Server: " + color + serverVersion, diff --git a/core/src/main/java/art/arcane/iris/core/service/IrisEngineSVC.java b/core/src/main/java/art/arcane/iris/core/service/IrisEngineSVC.java index b360743a3..86e21bfb5 100644 --- a/core/src/main/java/art/arcane/iris/core/service/IrisEngineSVC.java +++ b/core/src/main/java/art/arcane/iris/core/service/IrisEngineSVC.java @@ -33,6 +33,7 @@ import java.util.concurrent.atomic.AtomicLong; public class IrisEngineSVC implements IrisService { private static final int TRIM_PERIOD = 2_000; + private static final long ACTIVE_PREGEN_IDLE_MILLIS = 500L; private final AtomicInteger tectonicLimit = new AtomicInteger(30); private final AtomicInteger tectonicPlates = new AtomicInteger(); private final AtomicInteger queuedTectonicPlates = new AtomicInteger(); @@ -298,7 +299,7 @@ public class IrisEngineSVC implements IrisService { } try { - engine.getMantle().trim(tectonicLimit()); + engine.getMantle().trim(activeIdleDuration(engineWorld), activeTectonicLimit(engineWorld)); } catch (Throwable e) { if (isMantleClosed(e)) { close(); @@ -326,7 +327,7 @@ public class IrisEngineSVC implements IrisService { try { long unloadStart = System.currentTimeMillis(); - int count = engine.getMantle().unloadTectonicPlate(IrisSettings.get().getPerformance().getEngineSVC().forceMulticoreWrite ? 0 : tectonicLimit()); + int count = engine.getMantle().unloadTectonicPlate(IrisSettings.get().getPerformance().getEngineSVC().forceMulticoreWrite ? 0 : activeTectonicLimit(engineWorld)); if (count > 0) { Iris.debug(C.GOLD + "Unloaded " + C.YELLOW + count + " TectonicPlates in " + C.RED + Form.duration(System.currentTimeMillis() - unloadStart, 2)); } @@ -347,6 +348,33 @@ public class IrisEngineSVC implements IrisService { return tectonicLimit.get() / Math.max(worlds.size(), 1); } + private int activeTectonicLimit(@Nullable World world) { + int limit = tectonicLimit(); + if (world == null) { + return limit; + } + + PregeneratorJob pregeneratorJob = PregeneratorJob.getInstance(); + if (pregeneratorJob == null || !pregeneratorJob.targetsWorld(world)) { + return limit; + } + + return Math.max(1, Math.min(limit, Math.max(2, limit / 8))); + } + + private long activeIdleDuration(@Nullable World world) { + if (world == null) { + return TimeUnit.SECONDS.toMillis(IrisSettings.get().getPerformance().getMantleKeepAlive()); + } + + PregeneratorJob pregeneratorJob = PregeneratorJob.getInstance(); + if (pregeneratorJob == null || !pregeneratorJob.targetsWorld(world)) { + return TimeUnit.SECONDS.toMillis(IrisSettings.get().getPerformance().getMantleKeepAlive()); + } + + return ACTIVE_PREGEN_IDLE_MILLIS; + } + @Synchronized private void close() { if (closed) return; diff --git a/core/src/main/java/art/arcane/iris/core/service/IrisIntegrationService.java b/core/src/main/java/art/arcane/iris/core/service/IrisIntegrationService.java index 6361c9355..13cbd478c 100644 --- a/core/src/main/java/art/arcane/iris/core/service/IrisIntegrationService.java +++ b/core/src/main/java/art/arcane/iris/core/service/IrisIntegrationService.java @@ -2,8 +2,6 @@ package art.arcane.iris.core.service; import art.arcane.iris.Iris; import art.arcane.iris.core.gui.PregeneratorJob; -import art.arcane.iris.core.pregenerator.LazyPregenerator; -import art.arcane.iris.core.pregenerator.TurboPregenerator; import art.arcane.iris.util.common.plugin.IrisService; import art.arcane.volmlib.integration.IntegrationHandshakeRequest; import art.arcane.volmlib.integration.IntegrationHandshakeResponse; @@ -155,12 +153,6 @@ public class IrisIntegrationService implements IrisService, IntegrationServiceCo IntegrationMetricDescriptor descriptor = IntegrationMetricSchema.descriptor(IntegrationMetricSchema.IRIS_CHUNK_STREAM_MS); double chunksPerSecond = PregeneratorJob.chunksPerSecond(); - if (chunksPerSecond <= 0D) { - chunksPerSecond = TurboPregenerator.chunksPerSecond(); - } - if (chunksPerSecond <= 0D) { - chunksPerSecond = LazyPregenerator.chunksPerSecond(); - } if (chunksPerSecond > 0D) { return IntegrationMetricSample.available(descriptor, 1000D / chunksPerSecond, now); @@ -188,18 +180,6 @@ public class IrisIntegrationService implements IrisService, IntegrationServiceCo hasAnySource = true; } - long turboRemaining = TurboPregenerator.remainingChunks(); - if (turboRemaining >= 0L) { - totalQueue += turboRemaining; - hasAnySource = true; - } - - long lazyRemaining = LazyPregenerator.remainingChunks(); - if (lazyRemaining >= 0L) { - totalQueue += lazyRemaining; - hasAnySource = true; - } - IrisEngineSVC engineService = Iris.service(IrisEngineSVC.class); if (engineService != null) { totalQueue += Math.max(0, engineService.getQueuedTectonicPlateCount()); diff --git a/core/src/main/java/art/arcane/iris/core/service/StudioSVC.java b/core/src/main/java/art/arcane/iris/core/service/StudioSVC.java index 3ca61b4a2..2edecc780 100644 --- a/core/src/main/java/art/arcane/iris/core/service/StudioSVC.java +++ b/core/src/main/java/art/arcane/iris/core/service/StudioSVC.java @@ -434,7 +434,7 @@ public class StudioSVC implements IrisService { } if (activeProject == null) { - return CompletableFuture.completedFuture(new art.arcane.iris.core.runtime.StudioOpenCoordinator.StudioCloseResult(null, true, true, false, null, null)); + return CompletableFuture.completedFuture(new art.arcane.iris.core.runtime.StudioOpenCoordinator.StudioCloseResult(null, true, true, false, null)); } Iris.debug("Closing Active Project"); diff --git a/core/src/main/java/art/arcane/iris/engine/actuator/IrisDecorantActuator.java b/core/src/main/java/art/arcane/iris/engine/actuator/IrisDecorantActuator.java index 9c09687e1..4c0250cb8 100644 --- a/core/src/main/java/art/arcane/iris/engine/actuator/IrisDecorantActuator.java +++ b/core/src/main/java/art/arcane/iris/engine/actuator/IrisDecorantActuator.java @@ -87,7 +87,7 @@ public class IrisDecorantActuator extends EngineAssignedActuator { continue; } - if (height < getDimension().getFluidHeight()) { + if (height < getDimension().getFluidHeight() && PREDICATE_SOLID.test(output.get(i, height, j))) { getSeaSurfaceDecorator().decorate(i, j, realX, Math.round(i + 1), Math.round(x + i - 1), realZ, Math.round(z + j + 1), Math.round(z + j - 1), diff --git a/core/src/main/java/art/arcane/iris/engine/actuator/IrisTerrainNormalActuator.java b/core/src/main/java/art/arcane/iris/engine/actuator/IrisTerrainNormalActuator.java index 06284289c..80372bc98 100644 --- a/core/src/main/java/art/arcane/iris/engine/actuator/IrisTerrainNormalActuator.java +++ b/core/src/main/java/art/arcane/iris/engine/actuator/IrisTerrainNormalActuator.java @@ -86,6 +86,7 @@ public class IrisTerrainNormalActuator extends EngineAssignedActuator int zf, realX, realZ, hf, he; IrisBiome biome; IrisRegion region; + int clampedFluidHeight = Math.min(h.getHeight(), getDimension().getFluidHeight()); for (zf = 0; zf < h.getDepth(); zf++) { realX = xf + x; @@ -93,7 +94,7 @@ public class IrisTerrainNormalActuator extends EngineAssignedActuator biome = context.getBiome().get(xf, zf); region = context.getRegion().get(xf, zf); he = Math.min(h.getHeight(), context.getRoundedHeight(xf, zf)); - hf = Math.round(Math.max(Math.min(h.getHeight(), getDimension().getFluidHeight()), he)); + hf = Math.max(clampedFluidHeight, he); if (hf < 0) { continue; @@ -109,7 +110,7 @@ public class IrisTerrainNormalActuator extends EngineAssignedActuator if (i == 0) { if (getDimension().isBedrock()) { - h.set(xf, i, zf, BEDROCK); + h.setRaw(xf, i, zf, BEDROCK); lastBedrock = i; continue; } @@ -119,7 +120,7 @@ public class IrisTerrainNormalActuator extends EngineAssignedActuator ore = ore == null ? region.generateOres(realX, i, realZ, rng, getData(), true) : ore; ore = ore == null ? getDimension().generateOres(realX, i, realZ, rng, getData(), true) : ore; if (ore != null) { - h.set(xf, i, zf, ore); + h.setRaw(xf, i, zf, ore); continue; } @@ -131,11 +132,11 @@ public class IrisTerrainNormalActuator extends EngineAssignedActuator } if (fblocks.hasIndex(fdepth)) { - h.set(xf, i, zf, fblocks.get(fdepth)); + h.setRaw(xf, i, zf, fblocks.get(fdepth)); continue; } - h.set(xf, i, zf, context.getFluid().get(xf, zf)); + h.setRaw(xf, i, zf, context.getFluid().get(xf, zf)); continue; } @@ -151,7 +152,7 @@ public class IrisTerrainNormalActuator extends EngineAssignedActuator if (blocks.hasIndex(depth)) { - h.set(xf, i, zf, blocks.get(depth)); + h.setRaw(xf, i, zf, blocks.get(depth)); continue; } @@ -160,9 +161,9 @@ public class IrisTerrainNormalActuator extends EngineAssignedActuator ore = ore == null ? getDimension().generateOres(realX, i, realZ, rng, getData(), false) : ore; if (ore != null) { - h.set(xf, i, zf, ore); + h.setRaw(xf, i, zf, ore); } else { - h.set(xf, i, zf, context.getRock().get(xf, zf)); + h.setRaw(xf, i, zf, context.getRock().get(xf, zf)); } } } @@ -178,6 +179,7 @@ public class IrisTerrainNormalActuator extends EngineAssignedActuator IrisComplex complex = getComplex(); RNG localRng = rng; int fluidHeight = dimension.getFluidHeight(); + int clampedFluidHeight = Math.min(chunkHeight, fluidHeight); boolean bedrockEnabled = dimension.isBedrock(); ChunkedDataCache biomeCache = context.getBiome(); ChunkedDataCache regionCache = context.getRegion(); @@ -190,7 +192,7 @@ public class IrisTerrainNormalActuator extends EngineAssignedActuator IrisBiome biome = biomeCache.get(xf, zf); IrisRegion region = regionCache.get(xf, zf); int he = Math.min(chunkHeight, context.getRoundedHeight(xf, zf)); - int hf = Math.round(Math.max(Math.min(chunkHeight, fluidHeight), he)); + int hf = Math.max(clampedFluidHeight, he); if (hf < 0) { continue; } @@ -205,7 +207,7 @@ public class IrisTerrainNormalActuator extends EngineAssignedActuator for (int i = topY; i >= 0; i--) { if (i == 0 && bedrockEnabled) { - h.set(xf, i, zf, BEDROCK); + h.setRaw(xf, i, zf, BEDROCK); lastBedrock = i; continue; } @@ -217,7 +219,7 @@ public class IrisTerrainNormalActuator extends EngineAssignedActuator ore = ore == null ? dimension.generateSurfaceOres(realX, i, realZ, localRng, data) : ore; } if (ore != null) { - h.set(xf, i, zf, ore); + h.setRaw(xf, i, zf, ore); continue; } @@ -228,9 +230,9 @@ public class IrisTerrainNormalActuator extends EngineAssignedActuator } if (fblocks.hasIndex(fdepth)) { - h.set(xf, i, zf, fblocks.get(fdepth)); + h.setRaw(xf, i, zf, fblocks.get(fdepth)); } else { - h.set(xf, i, zf, fluid); + h.setRaw(xf, i, zf, fluid); } continue; } @@ -242,7 +244,7 @@ public class IrisTerrainNormalActuator extends EngineAssignedActuator } if (blocks.hasIndex(depth)) { - h.set(xf, i, zf, blocks.get(depth)); + h.setRaw(xf, i, zf, blocks.get(depth)); continue; } @@ -253,9 +255,9 @@ public class IrisTerrainNormalActuator extends EngineAssignedActuator } if (ore != null) { - h.set(xf, i, zf, ore); + h.setRaw(xf, i, zf, ore); } else { - h.set(xf, i, zf, rock); + h.setRaw(xf, i, zf, rock); } } } diff --git a/core/src/main/java/art/arcane/iris/engine/decorator/IrisSurfaceDecorator.java b/core/src/main/java/art/arcane/iris/engine/decorator/IrisSurfaceDecorator.java index 16ef3bbe2..8fdcf9b13 100644 --- a/core/src/main/java/art/arcane/iris/engine/decorator/IrisSurfaceDecorator.java +++ b/core/src/main/java/art/arcane/iris/engine/decorator/IrisSurfaceDecorator.java @@ -50,7 +50,7 @@ public class IrisSurfaceDecorator extends IrisEngineDecorator { RNG rng = getRNG(realX, realZ); IrisDecorator decorator = getDecorator(rng, biome, realX, realZ); bdx = data.get(x, height, z); - boolean underwater = height < getDimension().getFluidHeight(); + boolean underwater = height < getDimension().getFluidHeight() && biome.getInferredType() != InferredType.CAVE; if (decorator != null) { if (!decorator.isForcePlace() && !decorator.getSlopeCondition().isDefault() diff --git a/core/src/main/java/art/arcane/iris/engine/framework/Engine.java b/core/src/main/java/art/arcane/iris/engine/framework/Engine.java index fe68f1b87..225057891 100644 --- a/core/src/main/java/art/arcane/iris/engine/framework/Engine.java +++ b/core/src/main/java/art/arcane/iris/engine/framework/Engine.java @@ -23,12 +23,12 @@ import art.arcane.iris.core.IrisSettings; import art.arcane.iris.core.events.IrisLootEvent; import art.arcane.iris.core.gui.components.RenderType; import art.arcane.iris.core.gui.components.Renderer; +import art.arcane.iris.core.gui.PregeneratorJob; import art.arcane.iris.core.link.Identifier; import art.arcane.iris.core.loader.IrisData; import art.arcane.iris.core.loader.IrisRegistrant; import art.arcane.iris.core.nms.container.BlockPos; import art.arcane.iris.core.nms.container.Pair; -import art.arcane.iris.core.pregenerator.ChunkUpdater; import art.arcane.iris.core.service.ExternalDataSVC; import art.arcane.iris.core.tools.IrisToolbelt; import art.arcane.iris.engine.IrisComplex; @@ -333,16 +333,14 @@ public interface Engine extends DataProvider, Fallible, LootProvider, BlockUpdat if (c.getWorld().isChunkLoaded(c.getX() + x, c.getZ() + z)) continue; var msg = "Chunk %s, %s [%s, %s] is not loaded".formatted(c.getX() + x, c.getZ() + z, x, z); - if (W.getStack().getCallerClass().equals(ChunkUpdater.class)) Iris.warn(msg); - else Iris.debug(msg); + Iris.debug(msg); return; } } var mantle = getMantle().getMantle(); if (!mantle.isLoaded(c)) { var msg = "Mantle Chunk " + c.getX() + "," + c.getZ() + " is not loaded"; - if (W.getStack().getCallerClass().equals(ChunkUpdater.class)) Iris.warn(msg); - else Iris.debug(msg); + Iris.debug(msg); return; } @@ -1081,7 +1079,10 @@ public interface Engine extends DataProvider, Fallible, LootProvider, BlockUpdat default void cleanupMantleChunk(int x, int z) { World world = getWorld().realWorld(); if (world != null && IrisToolbelt.isWorldMaintenanceActive(world)) { - return; + PregeneratorJob pregeneratorJob = PregeneratorJob.getInstance(); + if (pregeneratorJob == null || !pregeneratorJob.targetsWorld(world)) { + return; + } } if (IrisSettings.get().getPerformance().isTrimMantleInStudio() || !isStudio()) { getMantle().cleanupChunk(x, z); diff --git a/core/src/main/java/art/arcane/iris/engine/mantle/EngineMantle.java b/core/src/main/java/art/arcane/iris/engine/mantle/EngineMantle.java index 82a89915b..2fe193c41 100644 --- a/core/src/main/java/art/arcane/iris/engine/mantle/EngineMantle.java +++ b/core/src/main/java/art/arcane/iris/engine/mantle/EngineMantle.java @@ -21,6 +21,7 @@ package art.arcane.iris.engine.mantle; import art.arcane.iris.core.IrisSettings; import art.arcane.iris.core.loader.IrisData; import art.arcane.iris.core.nms.container.Pair; +import art.arcane.iris.core.link.Identifier; import art.arcane.iris.engine.IrisComplex; import art.arcane.iris.engine.framework.Engine; import art.arcane.iris.engine.framework.EngineTarget; @@ -32,6 +33,7 @@ import art.arcane.iris.util.common.data.B; import art.arcane.volmlib.util.documentation.BlockCoordinates; import art.arcane.volmlib.util.documentation.ChunkCoordinates; import art.arcane.iris.util.project.hunk.Hunk; +import art.arcane.iris.util.project.matter.TileWrapper; import art.arcane.volmlib.util.mantle.runtime.Mantle; import art.arcane.volmlib.util.mantle.runtime.MantleChunk; import art.arcane.volmlib.util.mantle.flag.MantleFlag; @@ -243,13 +245,39 @@ public interface EngineMantle extends MatterGenerator { default void cleanupChunk(int x, int z) { if (!isCovered(x, z)) return; + doCleanupChunk(x, z); + } + + default void forceCleanupChunk(int x, int z) { MantleChunk chunk = getMantle().getChunk(x, z).use(); try { chunk.raiseFlagUnchecked(MantleFlag.CLEANED, () -> { chunk.deleteSlices(BlockData.class); chunk.deleteSlices(String.class); + chunk.deleteSlices(TileWrapper.class); + chunk.deleteSlices(Identifier.class); + chunk.deleteSlices(UpdateMatter.class); chunk.deleteSlices(MatterCavern.class); chunk.deleteSlices(MatterFluidBody.class); + chunk.deleteSlices(MatterMarker.class); + chunk.trimSlices(); + }); + } finally { + chunk.release(); + } + } + + private void doCleanupChunk(int x, int z) { + MantleChunk chunk = getMantle().getChunk(x, z).use(); + try { + chunk.raiseFlagUnchecked(MantleFlag.CLEANED, () -> { + chunk.deleteSlices(BlockData.class); + chunk.deleteSlices(TileWrapper.class); + chunk.deleteSlices(Identifier.class); + chunk.deleteSlices(UpdateMatter.class); + chunk.deleteSlices(MatterCavern.class); + chunk.deleteSlices(MatterFluidBody.class); + chunk.trimSlices(); }); } finally { chunk.release(); diff --git a/core/src/main/java/art/arcane/iris/engine/mantle/MantleComponent.java b/core/src/main/java/art/arcane/iris/engine/mantle/MantleComponent.java index 85058a0f4..cf60c9fba 100644 --- a/core/src/main/java/art/arcane/iris/engine/mantle/MantleComponent.java +++ b/core/src/main/java/art/arcane/iris/engine/mantle/MantleComponent.java @@ -62,6 +62,12 @@ public interface MantleComponent extends Comparable { MantleFlag getFlag(); + default MantleFlag[] getPrerequisiteFlags() { + return EMPTY_PREREQUISITES; + } + + MantleFlag[] EMPTY_PREREQUISITES = new MantleFlag[0]; + boolean isEnabled(); void setEnabled(boolean b); diff --git a/core/src/main/java/art/arcane/iris/engine/mantle/MatterGenerator.java b/core/src/main/java/art/arcane/iris/engine/mantle/MatterGenerator.java index e19c752c4..b53c40e4d 100644 --- a/core/src/main/java/art/arcane/iris/engine/mantle/MatterGenerator.java +++ b/core/src/main/java/art/arcane/iris/engine/mantle/MatterGenerator.java @@ -142,10 +142,28 @@ public interface MatterGenerator { continue; } - int componentPassRadius = Math.ceilDiv(component.getRadius(), 16); - if (Math.abs(i) > componentPassRadius || Math.abs(j) > componentPassRadius) { - partialChunks.add(passKey); - continue; + int componentRadius = component.getRadius(); + if (componentRadius > 0) { + int componentPassRadius = Math.ceilDiv(componentRadius, 16); + if (Math.abs(i) > componentPassRadius || Math.abs(j) > componentPassRadius) { + partialChunks.add(passKey); + continue; + } + } + + MantleFlag[] prerequisites = component.getPrerequisiteFlags(); + if (prerequisites.length > 0) { + boolean prerequisitesMet = true; + for (MantleFlag prereq : prerequisites) { + if (!chunk.isFlagged(prereq)) { + prerequisitesMet = false; + break; + } + } + if (!prerequisitesMet) { + partialChunks.add(passKey); + continue; + } } if (forceRegen && chunk.isFlagged(component.getFlag())) { diff --git a/core/src/main/java/art/arcane/iris/engine/mantle/components/IrisCaveCarver3D.java b/core/src/main/java/art/arcane/iris/engine/mantle/components/IrisCaveCarver3D.java index e55c670ad..6eb3bcb63 100644 --- a/core/src/main/java/art/arcane/iris/engine/mantle/components/IrisCaveCarver3D.java +++ b/core/src/main/java/art/arcane/iris/engine/mantle/components/IrisCaveCarver3D.java @@ -41,8 +41,13 @@ public class IrisCaveCarver3D { private static final byte LIQUID_AIR = 0; private static final byte LIQUID_LAVA = 2; private static final byte LIQUID_FORCED_AIR = 3; - private static final int ADAPTIVE_MIN_PLANE_COLUMNS = 48; + private static final int ADAPTIVE_MIN_PLANE_COLUMNS = 32; + private static final int ADAPTIVE_DEEP_MIN_PLANE_COLUMNS = 64; + private static final int ADAPTIVE_DEEP_SAMPLE_STEP = 8; + private static final int ADAPTIVE_DEEP_SURFACE_MARGIN = 12; + private static final int ADAPTIVE_DEEP_NEAR_SURFACE_DIVISOR = 4; private static final double ADAPTIVE_LOCAL_RANGE_SCALE = 0.25D; + private static final double ADAPTIVE_DEEP_MARGIN_BOOST = 0.015D; private static final ThreadLocal SCRATCH = ThreadLocal.withInitial(Scratch::new); private final Engine engine; @@ -609,6 +614,19 @@ public class IrisCaveCarver3D { continue; } + int effectiveAdaptiveSampleStep = resolveAdaptivePlaneSampleStep( + y, + planeColumnIndices, + planeCount, + adaptiveSampleStep, + surfaceBreakColumn, + surfaceBreakFloorY + ); + double effectiveAdaptiveThresholdMargin = resolveAdaptivePlaneThresholdMargin( + adaptiveThresholdMargin, + adaptiveSampleStep, + effectiveAdaptiveSampleStep + ); classifyDensityPlaneAdaptive( x0, z0, @@ -617,8 +635,8 @@ public class IrisCaveCarver3D { planeThresholdLimit, planeCount, planeCarve, - adaptiveSampleStep, - adaptiveThresholdMargin + effectiveAdaptiveSampleStep, + effectiveAdaptiveThresholdMargin ); int fadeIndex = y - minY; int localY = y & 15; @@ -660,6 +678,45 @@ public class IrisCaveCarver3D { return carved; } + private int resolveAdaptivePlaneSampleStep( + int y, + int[] planeColumnIndices, + int planeCount, + int adaptiveSampleStep, + boolean[] surfaceBreakColumn, + int[] surfaceBreakFloorY + ) { + if (adaptiveSampleStep >= ADAPTIVE_DEEP_SAMPLE_STEP || planeCount < ADAPTIVE_DEEP_MIN_PLANE_COLUMNS) { + return adaptiveSampleStep; + } + + int nearSurfaceColumns = 0; + int allowedNearSurfaceColumns = Math.max(8, planeCount / ADAPTIVE_DEEP_NEAR_SURFACE_DIVISOR); + for (int planeIndex = 0; planeIndex < planeCount; planeIndex++) { + int columnIndex = planeColumnIndices[planeIndex]; + if (surfaceBreakColumn[columnIndex] || y > (surfaceBreakFloorY[columnIndex] - ADAPTIVE_DEEP_SURFACE_MARGIN)) { + nearSurfaceColumns++; + if (nearSurfaceColumns > allowedNearSurfaceColumns) { + return adaptiveSampleStep; + } + } + } + + return ADAPTIVE_DEEP_SAMPLE_STEP; + } + + private double resolveAdaptivePlaneThresholdMargin( + double adaptiveThresholdMargin, + int adaptiveSampleStep, + int effectiveAdaptiveSampleStep + ) { + if (effectiveAdaptiveSampleStep <= adaptiveSampleStep) { + return adaptiveThresholdMargin; + } + + return adaptiveThresholdMargin + ((effectiveAdaptiveSampleStep - adaptiveSampleStep) * ADAPTIVE_DEEP_MARGIN_BOOST); + } + private int carvePassLattice( MantleChunk chunk, int x0, @@ -1285,6 +1342,10 @@ public class IrisCaveCarver3D { planeCarve[planeIndex] = false; continue; } + if (adaptiveSampleStep >= ADAPTIVE_DEEP_SAMPLE_STEP) { + planeCarve[planeIndex] = predictedDensity <= threshold; + continue; + } planeCarve[planeIndex] = classifyDensityPointNoWarpNoModules(x0 + localX, y, z0 + localZ, planeThresholdLimit[planeIndex]); } @@ -1353,6 +1414,10 @@ public class IrisCaveCarver3D { planeCarve[planeIndex] = false; continue; } + if (adaptiveSampleStep >= ADAPTIVE_DEEP_SAMPLE_STEP) { + planeCarve[planeIndex] = predictedDensity <= threshold; + continue; + } planeCarve[planeIndex] = classifyDensityPointNoWarpModules( x0 + localX, @@ -1414,6 +1479,10 @@ public class IrisCaveCarver3D { planeCarve[planeIndex] = false; continue; } + if (adaptiveSampleStep >= ADAPTIVE_DEEP_SAMPLE_STEP) { + planeCarve[planeIndex] = predictedDensity <= threshold; + continue; + } planeCarve[planeIndex] = classifyDensityPointWarpOnly(x0 + localX, y, z0 + localZ, planeThresholdLimit[planeIndex]); } @@ -1547,6 +1616,10 @@ public class IrisCaveCarver3D { planeCarve[planeIndex] = false; continue; } + if (adaptiveSampleStep >= ADAPTIVE_DEEP_SAMPLE_STEP) { + planeCarve[planeIndex] = predictedDensity <= threshold; + continue; + } planeCarve[planeIndex] = classifyDensityPointWarpModules( x0 + localX, diff --git a/core/src/main/java/art/arcane/iris/engine/mantle/components/MantleObjectComponent.java b/core/src/main/java/art/arcane/iris/engine/mantle/components/MantleObjectComponent.java index eee35fd25..4d7db4103 100644 --- a/core/src/main/java/art/arcane/iris/engine/mantle/components/MantleObjectComponent.java +++ b/core/src/main/java/art/arcane/iris/engine/mantle/components/MantleObjectComponent.java @@ -30,17 +30,22 @@ import art.arcane.iris.engine.object.*; import art.arcane.volmlib.util.collection.KList; import art.arcane.volmlib.util.collection.KMap; import art.arcane.volmlib.util.collection.KSet; +import art.arcane.iris.util.project.context.ChunkedDoubleDataCache; import art.arcane.iris.util.project.context.ChunkContext; import art.arcane.iris.util.common.data.B; import art.arcane.iris.util.project.stream.ProceduralStream; import art.arcane.volmlib.util.documentation.BlockCoordinates; import art.arcane.volmlib.util.documentation.ChunkCoordinates; import art.arcane.volmlib.util.format.Form; +import art.arcane.volmlib.util.mantle.runtime.MantleChunk; import art.arcane.volmlib.util.mantle.flag.ReservedFlag; import art.arcane.volmlib.util.math.RNG; +import art.arcane.volmlib.util.matter.Matter; import art.arcane.volmlib.util.matter.MatterStructurePOI; import art.arcane.iris.util.project.noise.CNG; import art.arcane.iris.util.project.noise.NoiseType; +import it.unimi.dsi.fastutil.longs.Long2IntOpenHashMap; +import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap; import org.bukkit.util.BlockVector; import java.io.IOException; @@ -55,8 +60,8 @@ import java.util.concurrent.atomic.AtomicLong; @ComponentFlag(ReservedFlag.OBJECT) public class MantleObjectComponent extends IrisMantleComponent { private static final long CAVE_REJECT_LOG_THROTTLE_MS = 5000L; + private static final int SURFACE_HEIGHT_CHUNK_FILL_THRESHOLD = 128; private static final Map CAVE_REJECT_LOG_STATE = new ConcurrentHashMap<>(); - public MantleObjectComponent(EngineMantle engineMantle) { super(engineMantle, ReservedFlag.OBJECT, 1); } @@ -73,6 +78,25 @@ public class MantleObjectComponent extends IrisMantleComponent { int surfaceY = getEngineMantle().getEngine().getHeight(xxx, zzz, true); IrisBiome caveBiome = resolveCaveObjectBiome(xxx, zzz, surfaceY, surfaceBiome); SurfaceHeightLookup surfaceHeightLookup = new SurfaceHeightLookup(context); + if (IrisSettings.get().getGeneral().isDebug() && (x & 31) == 0 && (z & 31) == 0) { + int carvedBlocks = 0; + int minY = 1; + int maxY = Math.min(getEngineMantle().getEngine().getHeight() - 1, surfaceY - 14); + for (int sy = minY; sy < maxY; sy++) { + if (writer.isCarved(8 + (x << 4), sy, 8 + (z << 4))) { + carvedBlocks++; + } + } + Iris.info("Cave object diag: chunk=" + x + "," + z + + " surfaceBiome=" + surfaceBiome.getLoadKey() + + " caveBiome=" + caveBiome.getLoadKey() + + " surfaceY=" + surfaceY + + " maxAnchorY=" + maxY + + " carvedAtCenter=" + carvedBlocks + + " biomeCarvingObjects=" + caveBiome.getCarvingObjects().size() + + " regionCarvingObjects=" + region.getCarvingObjects().size() + + " sameBiome=" + (caveBiome == surfaceBiome || caveBiome.getLoadKey().equals(surfaceBiome.getLoadKey()))); + } if (traceRegen) { Iris.info("Regen object layer start: chunk=" + x + "," + z + " surfaceBiome=" + surfaceBiome.getLoadKey() @@ -194,7 +218,7 @@ public class MantleObjectComponent extends IrisMantleComponent { } for (IrisObjectPlacement i : caveBiome.getCarvingObjects()) { - if (!i.getCarvingSupport().equals(CarvingMode.CARVING_ONLY)) { + if (!i.getCarvingSupport().supportsCarving()) { continue; } biomeCaveChecked++; @@ -259,7 +283,7 @@ public class MantleObjectComponent extends IrisMantleComponent { } for (IrisObjectPlacement i : region.getCarvingObjects()) { - if (!i.getCarvingSupport().equals(CarvingMode.CARVING_ONLY)) { + if (!i.getCarvingSupport().supportsCarving()) { continue; } regionCaveChecked++; @@ -316,7 +340,7 @@ public class MantleObjectComponent extends IrisMantleComponent { int x, int z, IrisObjectPlacement objectPlacement, - int surfaceObjectExclusionDepth, + int surfaceObjectExclusionBaseDepth, IrisComplex complex, boolean traceRegen, int chunkX, @@ -347,6 +371,7 @@ public class MantleObjectComponent extends IrisMantleComponent { } int xx = rng.i(x, x + 15); int zz = rng.i(z, z + 15); + int surfaceObjectExclusionDepth = resolveSurfaceObjectExclusionDepth(surfaceObjectExclusionBaseDepth, v); int surfaceObjectExclusionRadius = resolveSurfaceObjectExclusionRadius(v); if (surfaceObjectExclusionDepth > 0 && hasSurfaceCarveExposure(writer, surfaceHeightLookup, xx, zz, surfaceObjectExclusionDepth, surfaceObjectExclusionRadius)) { rejected++; @@ -491,7 +516,15 @@ public class MantleObjectComponent extends IrisMantleComponent { } int id = rng.i(0, Integer.MAX_VALUE); - IrisObjectPlacement effectivePlacement = resolveEffectivePlacement(objectPlacement, object); + IrisObjectPlacement resolvedPlacement = resolveEffectivePlacement(objectPlacement, object); + if (resolvedPlacement.getMode() == ObjectPlaceMode.CENTER_HEIGHT && caveProfile != null) { + ObjectPlaceMode profileMode = caveProfile.getDefaultObjectPlaceMode(); + if (profileMode != null) { + resolvedPlacement = resolvedPlacement.toPlacement(object.getLoadKey()); + resolvedPlacement.setMode(profileMode); + } + } + IrisObjectPlacement effectivePlacement = resolvedPlacement; AtomicBoolean wrotePlacementData = new AtomicBoolean(false); try { @@ -717,14 +750,17 @@ public class MantleObjectComponent extends IrisMantleComponent { int surfaceY = getEngineMantle().getEngine().getHeight(x, z); int maxAnchorY = Math.min(height - 1, surfaceY - Math.max(0, objectMinDepthBelowSurface)); if (maxAnchorY <= 1) { + logCaveAnchorDiag(writer, x, z, surfaceY, maxAnchorY, height, objectMinDepthBelowSurface, 0, 0); return anchors; } + int carvedCount = 0; for (int y = 1; y < maxAnchorY; y += step) { if (!writer.isCarved(x, y, z)) { continue; } + carvedCount++; boolean solidBelow = y <= 0 || !writer.isCarved(x, y - 1, z); boolean solidAbove = y >= (height - 1) || !writer.isCarved(x, y + 1, z); if (matchesCaveAnchor(anchorMode, solidBelow, solidAbove)) { @@ -732,9 +768,33 @@ public class MantleObjectComponent extends IrisMantleComponent { } } + if (anchors.isEmpty()) { + logCaveAnchorDiag(writer, x, z, surfaceY, maxAnchorY, height, objectMinDepthBelowSurface, carvedCount, 0); + } + return anchors; } + private void logCaveAnchorDiag(MantleWriter writer, int x, int z, int surfaceY, int maxAnchorY, int height, int minDepth, int carvedCount, int anchorCount) { + long now = System.currentTimeMillis(); + CaveRejectLogState state = CAVE_REJECT_LOG_STATE.computeIfAbsent("anchor-diag-" + (x >> 4) + "," + (z >> 4), k -> new CaveRejectLogState()); + if (now - state.lastLogMs.get() < CAVE_REJECT_LOG_THROTTLE_MS) { + return; + } + state.lastLogMs.set(now); + MantleChunk chunk = writer.acquireChunk(x >> 4, z >> 4); + Iris.info("Cave anchor diag: block=" + x + "," + z + + " chunk=" + (x >> 4) + "," + (z >> 4) + + " surfaceY=" + surfaceY + + " maxAnchorY=" + maxAnchorY + + " worldHeight=" + height + + " minDepth=" + minDepth + + " carvedInColumn=" + carvedCount + + " anchorsFound=" + anchorCount + + " chunkRef=" + (chunk == null ? "null" : System.identityHashCode(chunk)) + + " writerRef=" + System.identityHashCode(writer)); + } + private boolean matchesCaveAnchor(IrisCaveAnchorMode anchorMode, boolean solidBelow, boolean solidAbove) { return switch (anchorMode) { case PROFILE_DEFAULT, FLOOR -> solidBelow; @@ -803,6 +863,16 @@ public class MantleObjectComponent extends IrisMantleComponent { return Math.max(0, caveProfile.getSurfaceObjectExclusionDepth()); } + private int resolveSurfaceObjectExclusionDepth(int baseDepth, IrisObject object) { + if (object == null) { + return baseDepth; + } + + int horizontalReach = resolveSurfaceObjectExclusionRadius(object) + 2; + int verticalReach = Math.max(4, Math.min(16, Math.floorDiv(Math.max(1, object.getH()), 2))); + return Math.max(baseDepth, Math.max(horizontalReach, verticalReach)); + } + private int resolveSurfaceObjectExclusionRadius(IrisObject object) { if (object == null) { return 1; @@ -940,7 +1010,7 @@ public class MantleObjectComponent extends IrisMantleComponent { } for (Map.Entry> entry : scalars.entrySet()) { - double ms = entry.getKey().getMaximumScale(); + double ms = entry.getKey().getMaxScale(); for (String j : entry.getValue()) { updateRadiusBounds(sizeCache, xg, zg, j, ms); } @@ -993,12 +1063,12 @@ public class MantleObjectComponent extends IrisMantleComponent { private static final class SurfaceHeightLookup { private final ChunkContext context; private final ProceduralStream heightStream; - private final KMap columnHeights; + private final Long2ObjectOpenHashMap foreignChunkHeights; private SurfaceHeightLookup(ChunkContext context) { this.context = context; this.heightStream = context.getComplex().getHeightStream(); - this.columnHeights = new KMap<>(); + this.foreignChunkHeights = new Long2ObjectOpenHashMap<>(); } private int getRoundedHeight(int worldX, int worldZ) { @@ -1008,14 +1078,66 @@ public class MantleObjectComponent extends IrisMantleComponent { return context.getRoundedHeight(worldX & 15, worldZ & 15); } - long columnKey = Cache.key(worldX, worldZ); - Integer columnHeight = columnHeights.get(columnKey); - if (columnHeight == null) { - columnHeight = (int) Math.round(heightStream.getDouble(worldX, worldZ)); - columnHeights.put(columnKey, columnHeight); + long chunkKey = Cache.key(chunkBlockX, chunkBlockZ); + ForeignChunkHeights chunkHeights = foreignChunkHeights.get(chunkKey); + if (chunkHeights == null) { + chunkHeights = new ForeignChunkHeights(heightStream, chunkBlockX, chunkBlockZ); + foreignChunkHeights.put(chunkKey, chunkHeights); + } + return chunkHeights.getRoundedHeight(worldX, worldZ); + } + } + + private static final class ForeignChunkHeights { + private final ProceduralStream heightStream; + private final int chunkBlockX; + private final int chunkBlockZ; + private final Long2IntOpenHashMap sparseColumnHeights; + private int uniqueColumnCount; + private int[] roundedHeights; + + private ForeignChunkHeights(ProceduralStream heightStream, int chunkBlockX, int chunkBlockZ) { + this.heightStream = heightStream; + this.chunkBlockX = chunkBlockX; + this.chunkBlockZ = chunkBlockZ; + this.sparseColumnHeights = new Long2IntOpenHashMap(); + this.sparseColumnHeights.defaultReturnValue(Integer.MIN_VALUE); + this.uniqueColumnCount = 0; + } + + private int getRoundedHeight(int worldX, int worldZ) { + int[] localRoundedHeights = roundedHeights; + if (localRoundedHeights != null) { + int localX = worldX - chunkBlockX; + int localZ = worldZ - chunkBlockZ; + return localRoundedHeights[(localZ << 4) + localX]; } - return columnHeight; + long columnKey = Cache.key(worldX, worldZ); + int cachedHeight = sparseColumnHeights.get(columnKey); + if (cachedHeight != Integer.MIN_VALUE) { + return cachedHeight; + } + + int roundedHeight = (int) Math.round(heightStream.getDouble(worldX, worldZ)); + sparseColumnHeights.put(columnKey, roundedHeight); + uniqueColumnCount++; + if (uniqueColumnCount >= SURFACE_HEIGHT_CHUNK_FILL_THRESHOLD) { + promoteToChunkCache(); + } + + return roundedHeight; + } + + private void promoteToChunkCache() { + if (roundedHeights != null) { + return; + } + + int[] filledHeights = new int[256]; + new ChunkedDoubleDataCache(heightStream, chunkBlockX, chunkBlockZ, true).fillRounded(filledHeights); + roundedHeights = filledHeights; + sparseColumnHeights.clear(); } } } diff --git a/core/src/main/java/art/arcane/iris/engine/modifier/IrisCarveModifier.java b/core/src/main/java/art/arcane/iris/engine/modifier/IrisCarveModifier.java index 40f00be82..fa0002fe9 100644 --- a/core/src/main/java/art/arcane/iris/engine/modifier/IrisCarveModifier.java +++ b/core/src/main/java/art/arcane/iris/engine/modifier/IrisCarveModifier.java @@ -97,7 +97,7 @@ public class IrisCarveModifier extends EngineAssignedModifier { int rx = xx & 15; int rz = zz & 15; int columnIndex = PowerOfTwoCoordinates.packLocal16(rx, rz); - BlockData current = output.get(rx, yy, rz); + BlockData current = output.getRaw(rx, yy, rz); if (B.isFluid(current)) { return; @@ -126,15 +126,15 @@ public class IrisCarveModifier extends EngineAssignedModifier { } if (c.isWater()) { - output.set(rx, yy, rz, context.getFluid().get(rx, rz)); + output.setRaw(rx, yy, rz, context.getFluid().get(rx, rz)); } else if (c.isLava()) { - output.set(rx, yy, rz, LAVA); + output.setRaw(rx, yy, rz, LAVA); } else if (c.getLiquid() == 3) { - output.set(rx, yy, rz, AIR); + output.setRaw(rx, yy, rz, AIR); } else if (getEngine().getDimension().getCaveLavaHeight() > yy) { - output.set(rx, yy, rz, LAVA); + output.setRaw(rx, yy, rz, LAVA); } else { - output.set(rx, yy, rz, AIR); + output.setRaw(rx, yy, rz, AIR); } }); getEngine().getMetrics().getCarveResolve().put(resolveStopwatch.getMilliseconds()); @@ -154,8 +154,8 @@ public class IrisCarveModifier extends EngineAssignedModifier { BlockData data = biome.getWall().get(rng, worldX, yy, worldZ, getData()); int columnIndex = PowerOfTwoCoordinates.packLocal16(rx, rz); - if (data != null && B.isSolid(output.get(rx, yy, rz)) && yy <= surfaceHeights[columnIndex]) { - output.set(rx, yy, rz, data); + if (data != null && B.isSolid(output.getRaw(rx, yy, rz)) && yy <= surfaceHeights[columnIndex]) { + output.setRaw(rx, yy, rz, data); } } }); @@ -231,11 +231,11 @@ public class IrisCarveModifier extends EngineAssignedModifier { String customBiome = ""; if (B.isDecorant(output.getClosest(rx, zone.ceiling + 1, rz))) { - output.set(rx, zone.ceiling + 1, rz, AIR); + output.setRaw(rx, zone.ceiling + 1, rz, AIR); } - if (B.isDecorant(output.get(rx, zone.ceiling, rz))) { - output.set(rx, zone.ceiling, rz, AIR); + if (B.isDecorant(output.getRaw(rx, zone.ceiling, rz))) { + output.setRaw(rx, zone.ceiling, rz, AIR); } if (M.r(1D / 16D)) { @@ -275,18 +275,18 @@ public class IrisCarveModifier extends EngineAssignedModifier { int y = zone.floor - i - 1; BlockData b = blocks.get(i); - BlockData down = output.get(rx, y, rz); + BlockData down = output.getRaw(rx, y, rz); if (!B.isSolid(down)) { continue; } if (B.isOre(down)) { - output.set(rx, y, rz, B.toDeepSlateOre(down, b)); + output.setRaw(rx, y, rz, B.toDeepSlateOre(down, b)); continue; } - output.set(rx, y, rz, blocks.get(i)); + output.setRaw(rx, y, rz, blocks.get(i)); } blocks = biome.generateCeilingLayers(getDimension(), xx, zz, rng, 3, zone.ceiling, getData(), getComplex()); @@ -298,25 +298,25 @@ public class IrisCarveModifier extends EngineAssignedModifier { } BlockData b = blocks.get(i); - BlockData up = output.get(rx, zone.ceiling + i + 1, rz); + BlockData up = output.getRaw(rx, zone.ceiling + i + 1, rz); if (!B.isSolid(up)) { continue; } if (B.isOre(up)) { - output.set(rx, zone.ceiling + i + 1, rz, B.toDeepSlateOre(up, b)); + output.setRaw(rx, zone.ceiling + i + 1, rz, B.toDeepSlateOre(up, b)); continue; } - output.set(rx, zone.ceiling + i + 1, rz, b); + output.setRaw(rx, zone.ceiling + i + 1, rz, b); } } - for (IrisDecorator i : biome.getDecorators()) { - if (i.getPartOf().equals(IrisDecorationPart.NONE) && B.isSolid(output.get(rx, zone.getFloor() - 1, rz))) { + for (IrisDecorator decorator : biome.getDecorators()) { + if (decorator.getPartOf().equals(IrisDecorationPart.NONE) && B.isSolid(output.getRaw(rx, zone.getFloor() - 1, rz))) { decorant.getSurfaceDecorator().decorate(rx, rz, xx, xx, xx, zz, zz, zz, output, biome, zone.getFloor() - 1, zone.airThickness()); - } else if (i.getPartOf().equals(IrisDecorationPart.CEILING) && B.isSolid(output.get(rx, zone.getCeiling() + 1, rz))) { + } else if (decorator.getPartOf().equals(IrisDecorationPart.CEILING) && B.isSolid(output.getRaw(rx, zone.getCeiling() + 1, rz))) { decorant.getCeilingDecorator().decorate(rx, rz, xx, xx, xx, zz, zz, zz, output, biome, zone.getCeiling(), zone.airThickness()); } } diff --git a/core/src/main/java/art/arcane/iris/engine/object/IrisCaveProfile.java b/core/src/main/java/art/arcane/iris/engine/object/IrisCaveProfile.java index e2baa2b5d..67398cb58 100644 --- a/core/src/main/java/art/arcane/iris/engine/object/IrisCaveProfile.java +++ b/core/src/main/java/art/arcane/iris/engine/object/IrisCaveProfile.java @@ -134,6 +134,9 @@ public class IrisCaveProfile { @Desc("Default cave anchor mode for cave-only object placement.") private IrisCaveAnchorMode defaultObjectAnchor = IrisCaveAnchorMode.FLOOR; + @Desc("Default placement mode for cave objects. Stilt modes tile the object base block down to the cave floor surface. FAST_MIN_STILT is recommended for cave objects to prevent floating.") + private ObjectPlaceMode defaultObjectPlaceMode = null; + @MinNumber(1) @MaxNumber(8) @Desc("Vertical scan step used while searching cave anchors.") diff --git a/core/src/main/java/art/arcane/iris/engine/object/IrisDimensionCarvingResolver.java b/core/src/main/java/art/arcane/iris/engine/object/IrisDimensionCarvingResolver.java index 0e65e2566..4a94bfeac 100644 --- a/core/src/main/java/art/arcane/iris/engine/object/IrisDimensionCarvingResolver.java +++ b/core/src/main/java/art/arcane/iris/engine/object/IrisDimensionCarvingResolver.java @@ -14,17 +14,18 @@ import java.util.Map; public final class IrisDimensionCarvingResolver { private static final int MAX_CHILD_DEPTH = 32; private static final long CHILD_SEED_SALT = 0x9E3779B97F4A7C15L; + private static final ThreadLocal THREAD_STATE = ThreadLocal.withInitial(State::new); private IrisDimensionCarvingResolver() { } public static IrisDimensionCarvingEntry resolveRootEntry(Engine engine, int worldY) { - return resolveRootEntry(engine, worldY, new State()); + return resolveRootEntry(engine, worldY, THREAD_STATE.get()); } public static IrisDimensionCarvingEntry resolveRootEntry(Engine engine, int worldY, State state) { - State resolvedState = state == null ? new State() : state; + State resolvedState = state == null ? THREAD_STATE.get() : state; if (resolvedState.rootEntriesByWorldY.containsKey(worldY)) { return resolvedState.rootEntriesByWorldY.get(worldY); } @@ -50,11 +51,11 @@ public final class IrisDimensionCarvingResolver { } public static IrisDimensionCarvingEntry resolveFromRoot(Engine engine, IrisDimensionCarvingEntry rootEntry, int worldX, int worldZ) { - return resolveFromRoot(engine, rootEntry, worldX, worldZ, new State()); + return resolveFromRoot(engine, rootEntry, worldX, worldZ, THREAD_STATE.get()); } public static IrisDimensionCarvingEntry resolveFromRoot(Engine engine, IrisDimensionCarvingEntry rootEntry, int worldX, int worldZ, State state) { - State resolvedState = state == null ? new State() : state; + State resolvedState = state == null ? THREAD_STATE.get() : state; if (rootEntry == null) { return null; } diff --git a/core/src/main/java/art/arcane/iris/engine/object/IrisObjectScale.java b/core/src/main/java/art/arcane/iris/engine/object/IrisObjectScale.java index d7ae262e3..f3ab0bedc 100644 --- a/core/src/main/java/art/arcane/iris/engine/object/IrisObjectScale.java +++ b/core/src/main/java/art/arcane/iris/engine/object/IrisObjectScale.java @@ -37,61 +37,89 @@ import lombok.experimental.Accessors; @Desc("Scale objects") @Data public class IrisObjectScale { - private static ConcurrentLinkedHashMap> cache - = new ConcurrentLinkedHashMap.Builder>() + private static final ConcurrentLinkedHashMap> cache + = new ConcurrentLinkedHashMap.Builder>() .initialCapacity(64) .maximumWeightedCapacity(1024) .concurrencyLevel(32) .build(); + + @MinNumber(0.01) + @MaxNumber(50) + @Desc("Fixed scale multiplier for this object. 0.5 shrinks to half size, 2.0 doubles the size. When set to anything other than 1, this overrides minimumScale and maximumScale. Leave at 1 to use the minimumScale/maximumScale range.") + private double size = 1; + @MinNumber(1) @MaxNumber(32) @Desc("Iris Objects are scaled and cached to speed up placements. Because of this extra memory is used, so we evenly distribute variations across the defined scale range, then pick one randomly. If the differences is small, use a lower number. For more possibilities on the scale spectrum, increase this at the cost of memory.") private int variations = 7; + @MinNumber(0.01) @MaxNumber(50) - @Desc("The minimum scale") + @Desc("The minimum scale. Used when size is 1 to pick a random scale per placement.") private double minimumScale = 1; + @MinNumber(0.01) @MaxNumber(50) - @Desc("The maximum height for placement (top of object)") + @Desc("The maximum scale. Used when size is 1 to pick a random scale per placement.") private double maximumScale = 1; - @Desc("If this object is scaled up beyond its origin size, specify a 3D interpolator") + + @Desc("If this object is scaled up beyond its origin size, specify a 3D interpolator. NONE keeps blocky scaled output, TRILINEAR (LERP) smooths with linear interpolation, TRICUBIC and TRIHERMITE produce smoother but slower output.") private IrisObjectPlacementScaleInterpolator interpolation = IrisObjectPlacementScaleInterpolator.NONE; public boolean shouldScale() { - return ((minimumScale == maximumScale) && maximumScale == 1) || variations <= 0; + if (size != 1) { + return true; + } + if (variations <= 0) { + return false; + } + return minimumScale != 1 || maximumScale != 1; } public int getMaxSizeFor(int indim) { - return (int) (getMaxScale() * indim); + return (int) Math.ceil(getMaxScale() * indim); } public double getMaxScale() { - double mx = 0; - - for (double i = minimumScale; i < maximumScale; i += (maximumScale - minimumScale) / (double) (Math.min(variations, 32))) { - mx = i; + if (size != 1) { + return size; } - - return mx; + return Math.max(minimumScale, maximumScale); } public IrisObject get(RNG rng, IrisObject origin) { - if (shouldScale()) { + if (!shouldScale()) { return origin; } - return cache.computeIfAbsent(origin, (k) -> { + CacheKey key = new CacheKey(origin, size, minimumScale, maximumScale, variations, interpolation); + return cache.computeIfAbsent(key, (k) -> { KList c = new KList<>(); - for (double i = minimumScale; i < maximumScale; i += (maximumScale - minimumScale) / (double) (Math.min(variations, 32))) { - c.add(origin.scaled(i, getInterpolation())); + + if (size != 1) { + c.add(origin.scaled(size, interpolation)); + return c; } + if (minimumScale == maximumScale) { + c.add(origin.scaled(minimumScale, interpolation)); + return c; + } + + int vs = Math.max(1, Math.min(variations, 32)); + double step = (maximumScale - minimumScale) / (double) vs; + for (int v = 0; v < vs; v++) { + c.add(origin.scaled(minimumScale + step * v, interpolation)); + } return c; }).getRandom(rng); } public boolean canScaleBeyond() { - return shouldScale() && maximumScale > 1; + return shouldScale() && getMaxScale() > 1; + } + + private record CacheKey(IrisObject origin, double size, double minimumScale, double maximumScale, int variations, IrisObjectPlacementScaleInterpolator interpolation) { } } diff --git a/core/src/main/java/art/arcane/iris/util/project/context/ChunkContext.java b/core/src/main/java/art/arcane/iris/util/project/context/ChunkContext.java index 6f5768033..5facff86b 100644 --- a/core/src/main/java/art/arcane/iris/util/project/context/ChunkContext.java +++ b/core/src/main/java/art/arcane/iris/util/project/context/ChunkContext.java @@ -60,7 +60,7 @@ public class ChunkContext { long totalStartNanos = capturePrefillMetric ? System.nanoTime() : 0L; List fillTasks = new ArrayList<>(6); if (resolvedPlan.height) { - fillTasks.add(height::fill); + fillTasks.add(() -> height.fillRounded(roundedHeight)); } if (resolvedPlan.biome) { fillTasks.add(new PrefillFillTask(biome)); @@ -91,14 +91,9 @@ public class ChunkContext { future.join(); } } - if (capturePrefillMetric) { metrics.getContextPrefill().put((System.nanoTime() - totalStartNanos) / 1_000_000D); } - - if (resolvedPlan.height) { - fillRoundedHeight(); - } } } @@ -189,13 +184,4 @@ public class ChunkContext { dataCache.fill(); } } - - private void fillRoundedHeight() { - for (int z = 0; z < 16; z++) { - int rowOffset = z << 4; - for (int x = 0; x < 16; x++) { - roundedHeight[rowOffset + x] = (int) Math.round(height.getDouble(x, z)); - } - } - } } diff --git a/core/src/main/java/art/arcane/iris/util/project/context/ChunkedDoubleDataCache.java b/core/src/main/java/art/arcane/iris/util/project/context/ChunkedDoubleDataCache.java index ea3b7f494..2826704f6 100644 --- a/core/src/main/java/art/arcane/iris/util/project/context/ChunkedDoubleDataCache.java +++ b/core/src/main/java/art/arcane/iris/util/project/context/ChunkedDoubleDataCache.java @@ -36,12 +36,30 @@ public class ChunkedDoubleDataCache { } public void fill(Executor executor) { + fillRounded(null); + } + + public void fillRounded(int[] roundedTarget) { if (!cache) { + if (roundedTarget != null) { + for (int row = 0; row < 16; row++) { + int rowOffset = row << 4; + int worldZ = z + row; + for (int column = 0; column < 16; column++) { + roundedTarget[rowOffset + column] = (int) Math.round(stream.getDouble(x + column, worldZ)); + } + } + } return; } if (stream instanceof ChunkFillableDoubleStream2D cachedStream) { cachedStream.fillChunkDoubles(x, z, data); + if (roundedTarget != null) { + for (int index = 0; index < 256; index++) { + roundedTarget[index] = (int) Math.round(data[index]); + } + } return; } @@ -49,7 +67,11 @@ public class ChunkedDoubleDataCache { int rowOffset = row << 4; int worldZ = z + row; for (int column = 0; column < 16; column++) { - data[rowOffset + column] = stream.getDouble(x + column, worldZ); + double sampled = stream.getDouble(x + column, worldZ); + data[rowOffset + column] = sampled; + if (roundedTarget != null) { + roundedTarget[rowOffset + column] = (int) Math.round(sampled); + } } } } diff --git a/core/src/main/java/art/arcane/iris/util/project/profile/LoadBalancer.java b/core/src/main/java/art/arcane/iris/util/project/profile/LoadBalancer.java deleted file mode 100644 index 0a893473a..000000000 --- a/core/src/main/java/art/arcane/iris/util/project/profile/LoadBalancer.java +++ /dev/null @@ -1,70 +0,0 @@ -package art.arcane.iris.util.project.profile; - - -import art.arcane.iris.Iris; -import art.arcane.iris.core.IrisSettings; -import art.arcane.volmlib.util.math.M; -import lombok.Getter; -import lombok.Setter; - -import java.util.concurrent.Semaphore; - -@Getter -public class LoadBalancer extends MsptTimings { - private final Semaphore semaphore; - private final int maxPermits; - private final double range; - @Setter - private int minMspt, maxMspt; - private int permits, lastMspt; - private long lastTime = M.ms(); - - public LoadBalancer(Semaphore semaphore, int maxPermits, IrisSettings.MsRange range) { - this(semaphore, maxPermits, range.getMin(), range.getMax()); - } - - public LoadBalancer(Semaphore semaphore, int maxPermits, int minMspt, int maxMspt) { - this.semaphore = semaphore; - this.maxPermits = maxPermits; - this.minMspt = minMspt; - this.maxMspt = maxMspt; - this.range = maxMspt - minMspt; - setName("LoadBalancer"); - start(); - } - - @Override - protected void update(int raw) { - lastTime = M.ms(); - int mspt = raw; - if (mspt < lastMspt) { - int min = (int) Math.max(lastMspt * IrisSettings.get().getUpdater().getChunkLoadSensitivity(), 1); - mspt = Math.max(mspt, min); - } - lastMspt = mspt; - mspt = Math.max(mspt - minMspt, 0); - double percent = mspt / range; - - int target = (int) (maxPermits * percent); - target = Math.min(target, maxPermits - 20); - - int diff = target - permits; - permits = target; - - if (diff == 0) return; - Iris.debug("Adjusting load to %s (%s) permits (%s mspt, %.2f)".formatted(target, diff, raw, percent)); - - if (diff > 0) semaphore.acquireUninterruptibly(diff); - else semaphore.release(Math.abs(diff)); - } - - public void close() { - interrupt(); - semaphore.release(permits); - } - - public void setRange(IrisSettings.MsRange range) { - minMspt = range.getMin(); - maxMspt = range.getMax(); - } -} diff --git a/core/src/test/java/art/arcane/iris/core/runtime/SmokeDiagnosticsServiceCloseStateTest.java b/core/src/test/java/art/arcane/iris/core/runtime/SmokeDiagnosticsServiceCloseStateTest.java deleted file mode 100644 index 9fbf011d6..000000000 --- a/core/src/test/java/art/arcane/iris/core/runtime/SmokeDiagnosticsServiceCloseStateTest.java +++ /dev/null @@ -1,28 +0,0 @@ -package art.arcane.iris.core.runtime; - -import org.junit.Test; - -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertTrue; - -public class SmokeDiagnosticsServiceCloseStateTest { - @Test - public void closeStateIsPersistedIntoRunSnapshot() { - SmokeDiagnosticsService service = SmokeDiagnosticsService.get(); - SmokeDiagnosticsService.SmokeRunHandle handle = service.beginRun( - SmokeDiagnosticsService.SmokeRunMode.STUDIO_CLOSE, - "iris-test-world", - true, - true, - null, - false - ); - - handle.setCloseState(true, false, true); - - SmokeDiagnosticsService.SmokeRunReport report = handle.snapshot(); - assertTrue(report.isCloseUnloadCompletedLive()); - assertFalse(report.isCloseFolderDeletionCompletedLive()); - assertTrue(report.isCloseStartupCleanupQueued()); - } -} diff --git a/nms/v1_21_R7/src/main/java/art/arcane/iris/core/nms/v1_21_R7/IrisChunkGenerator.java b/nms/v1_21_R7/src/main/java/art/arcane/iris/core/nms/v1_21_R7/IrisChunkGenerator.java index a34d2deee..732c7af3d 100644 --- a/nms/v1_21_R7/src/main/java/art/arcane/iris/core/nms/v1_21_R7/IrisChunkGenerator.java +++ b/nms/v1_21_R7/src/main/java/art/arcane/iris/core/nms/v1_21_R7/IrisChunkGenerator.java @@ -204,6 +204,10 @@ public class IrisChunkGenerator extends CustomChunkGenerator { List starts = new ArrayList<>(structureManager.startsForStructure(chunkAccess.getPos(), structure -> true)); starts.sort(Comparator.comparingInt(start -> structureOrder.getOrDefault(start.getStructure(), Integer.MAX_VALUE))); Set externalSmartBoreStructures = ExternalDataPackPipeline.snapshotSmartBoreStructureKeys(); + IrisSettings.IrisSettingsGeneral general = IrisSettings.get().getGeneral(); + boolean intrinsicFoundationsEnabled = general.isIntrinsicStructureFoundations(); + int intrinsicFoundationDepth = Math.max(0, general.getIntrinsicFoundationMaxDepth()); + List intrinsicAllowlist = general.getIntrinsicStructureAllowlist(); int seededStructureIndex = Integer.MIN_VALUE; for (int j = 0; j < starts.size(); j++) { @@ -217,16 +221,23 @@ public class IrisChunkGenerator extends CustomChunkGenerator { Supplier supplier = () -> structureRegistry.getResourceKey(structure).map(Object::toString).orElseGet(structure::toString); String structureKey = resolveStructureKey(structureRegistry, structure); boolean isExternalSmartBoreStructure = externalSmartBoreStructures.contains(structureKey); + boolean isIntrinsicFoundationStructure = !isExternalSmartBoreStructure + && intrinsicFoundationsEnabled + && intrinsicFoundationDepth > 0 + && matchesIntrinsicAllowlist(structureKey, intrinsicAllowlist); + int foundationDepth = isExternalSmartBoreStructure + ? EXTERNAL_FOUNDATION_MAX_DEPTH + : (isIntrinsicFoundationStructure ? intrinsicFoundationDepth : 0); BitSet[] beforeSolidColumns = null; - if (isExternalSmartBoreStructure) { + if (foundationDepth > 0) { beforeSolidColumns = snapshotChunkSolidColumns(level, chunkAccess); } try { level.setCurrentlyGenerating(supplier); start.placeInChunk(level, structureManager, this, random, getWritableArea(chunkAccess), chunkAccess.getPos()); - if (isExternalSmartBoreStructure && beforeSolidColumns != null) { - applyExternalStructureFoundations(level, chunkAccess, beforeSolidColumns, EXTERNAL_FOUNDATION_MAX_DEPTH); + if (beforeSolidColumns != null) { + applyStructureFoundations(level, chunkAccess, beforeSolidColumns, foundationDepth); } if (shouldLogExternalStructureFingerprint(structureKey)) { logExternalStructureFingerprint(structureKey, start); @@ -300,7 +311,31 @@ public class IrisChunkGenerator extends CustomChunkGenerator { return columns; } - private static void applyExternalStructureFoundations( + private static boolean matchesIntrinsicAllowlist(String structureKey, List allowlist) { + if (structureKey == null || structureKey.isBlank() || allowlist == null || allowlist.isEmpty()) { + return false; + } + String key = structureKey.toLowerCase(Locale.ROOT); + for (String raw : allowlist) { + if (raw == null) { + continue; + } + String pattern = raw.trim().toLowerCase(Locale.ROOT); + if (pattern.isEmpty()) { + continue; + } + if (pattern.endsWith("*")) { + if (key.startsWith(pattern.substring(0, pattern.length() - 1))) { + return true; + } + } else if (key.equals(pattern)) { + return true; + } + } + return false; + } + + private static void applyStructureFoundations( WorldGenLevel level, ChunkAccess chunkAccess, BitSet[] beforeSolidColumns,