diff --git a/build.gradle.kts b/build.gradle.kts index ed55393f2..f05121a60 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -35,6 +35,9 @@ plugins { group = "art.arcane" version = "4.0.0-1.20.1-1.21.11-Dev1" +val volmLibCoordinate: String = providers.gradleProperty("volmLibCoordinate") + .orElse("com.github.VolmitSoftware:VolmLib:master-SNAPSHOT") + .get() apply() @@ -89,6 +92,10 @@ nmsBindings.forEach { (key, value) -> dependencies { compileOnly(project(":core")) + compileOnly(volmLibCoordinate) { + isChanging = true + isTransitive = false + } compileOnly(rootProject.libs.annotations) compileOnly(rootProject.libs.byteBuddy.core) } @@ -181,8 +188,8 @@ fun exec(vararg command: Any) { } configurations.configureEach { - resolutionStrategy.cacheChangingModulesFor(60, "minutes") - resolutionStrategy.cacheDynamicVersionsFor(60, "minutes") + resolutionStrategy.cacheChangingModulesFor(0, "seconds") + resolutionStrategy.cacheDynamicVersionsFor(0, "seconds") } allprojects { diff --git a/core/build.gradle.kts b/core/build.gradle.kts index 92c4a7fe4..765a46cb4 100644 --- a/core/build.gradle.kts +++ b/core/build.gradle.kts @@ -38,6 +38,9 @@ plugins { val apiVersion = "1.19" val main = "art.arcane.iris.Iris" val lib = "art.arcane.iris.util" +val volmLibCoordinate: String = providers.gradleProperty("volmLibCoordinate") + .orElse("com.github.VolmitSoftware:VolmLib:master-SNAPSHOT") + .get() /** * Dependencies. @@ -73,6 +76,10 @@ dependencies { // Shaded implementation(slimjarHelper("spigot")) + implementation(volmLibCoordinate) { + isChanging = true + isTransitive = false + } // Dynamically Loaded slim(libs.paralithic) @@ -234,10 +241,5 @@ rootProject.tasks.named("prepareKotlinBuildScriptModel") { } sourceSets.main { - java.srcDir("../../VolmLib/shared/src/main/java") java.srcDir(generateTemplates.map { it.outputs }) } - -kotlin.sourceSets.named("main") { - kotlin.srcDir("../../VolmLib/shared/src/main/kotlin") -} diff --git a/core/src/main/java/art/arcane/iris/Iris.java b/core/src/main/java/art/arcane/iris/Iris.java index e7766e820..0bb899af3 100644 --- a/core/src/main/java/art/arcane/iris/Iris.java +++ b/core/src/main/java/art/arcane/iris/Iris.java @@ -57,6 +57,7 @@ import art.arcane.iris.util.plugin.VolmitPlugin; import art.arcane.iris.util.plugin.VolmitSender; import art.arcane.iris.util.plugin.chunk.ChunkTickets; import art.arcane.iris.util.scheduling.J; +import art.arcane.iris.util.misc.ServerProperties; import art.arcane.volmlib.util.scheduling.Queue; import art.arcane.volmlib.util.scheduling.ShurikenQueue; import lombok.NonNull; @@ -94,6 +95,7 @@ public class Iris extends VolmitPlugin implements Listener { private static VolmitSender sender; private static Thread shutdownHook; private static File settingsFile; + private static final String PENDING_WORLD_DELETE_FILE = "pending-world-deletes.txt"; static { try { @@ -470,6 +472,12 @@ public class Iris extends VolmitPlugin implements Listener { services.values().forEach(IrisService::onEnable); services.values().forEach(this::registerListener); addShutdownHook(); + processPendingStartupWorldDeletes(); + + if (J.isFolia()) { + checkForBukkitWorlds(s -> true); + } + J.s(() -> { J.a(() -> IO.delete(getTemp())); J.a(LazyPregenerator::loadLazyGenerators, 100); @@ -480,7 +488,9 @@ public class Iris extends VolmitPlugin implements Listener { J.a(ServerConfigurator::configure, 20); autoStartStudio(); - checkForBukkitWorlds(s -> true); + if (!J.isFolia()) { + checkForBukkitWorlds(s -> true); + } IrisToolbelt.retainMantleDataForSlice(String.class.getCanonicalName()); IrisToolbelt.retainMantleDataForSlice(BlockData.class.getCanonicalName()); }); @@ -534,6 +544,12 @@ public class Iris extends VolmitPlugin implements Listener { Iris.info(C.LIGHT_PURPLE + "Loaded " + s + "!"); } catch (Throwable e) { Iris.error("Failed to load world " + s + "!"); + if (containsCreateWorldUnsupportedOperation(e)) { + Iris.error("This server denied Bukkit.createWorld for \"" + s + "\" at the current startup phase."); + Iris.error("Ensure Iris is loaded at STARTUP and restart after staging worlds in bukkit.yml."); + reportError(e); + return; + } e.printStackTrace(); } }); @@ -543,6 +559,153 @@ public class Iris extends VolmitPlugin implements Listener { } } + private static boolean containsCreateWorldUnsupportedOperation(Throwable throwable) { + Throwable cursor = throwable; + while (cursor != null) { + if (cursor instanceof UnsupportedOperationException || cursor instanceof IllegalStateException) { + for (StackTraceElement element : cursor.getStackTrace()) { + if ("org.bukkit.craftbukkit.CraftServer".equals(element.getClassName()) + && "createWorld".equals(element.getMethodName())) { + return true; + } + } + } + cursor = cursor.getCause(); + } + return false; + } + + public static synchronized int queueWorldDeletionOnStartup(Collection worldNames) throws IOException { + if (instance == null || worldNames == null || worldNames.isEmpty()) { + return 0; + } + + LinkedHashMap queue = loadPendingWorldDeleteMap(); + int before = queue.size(); + + for (String worldName : worldNames) { + String normalized = normalizeWorldName(worldName); + if (normalized == null) { + continue; + } + queue.putIfAbsent(normalized.toLowerCase(Locale.ROOT), normalized); + } + + if (queue.size() != before) { + writePendingWorldDeleteMap(queue); + } + + return queue.size() - before; + } + + private void processPendingStartupWorldDeletes() { + try { + LinkedHashMap queue = loadPendingWorldDeleteMap(); + if (queue.isEmpty()) { + return; + } + + LinkedHashMap remaining = new LinkedHashMap<>(); + for (String worldName : queue.values()) { + if (worldName.equalsIgnoreCase(ServerProperties.LEVEL_NAME)) { + Iris.warn("Skipping queued deletion for \"" + worldName + "\" because it is configured as level-name."); + continue; + } + + if (Bukkit.getWorld(worldName) != null) { + Iris.warn("Skipping queued deletion for \"" + worldName + "\" because it is currently loaded."); + remaining.put(worldName.toLowerCase(Locale.ROOT), worldName); + continue; + } + + File worldFolder = new File(Bukkit.getWorldContainer(), worldName); + if (!worldFolder.exists()) { + Iris.info("Queued world deletion skipped for \"" + worldName + "\" (folder missing)."); + continue; + } + + IO.delete(worldFolder); + if (worldFolder.exists()) { + Iris.warn("Failed to delete queued world folder \"" + worldName + "\". Retrying on next startup."); + remaining.put(worldName.toLowerCase(Locale.ROOT), worldName); + continue; + } + + Iris.info("Deleted queued world folder \"" + worldName + "\"."); + } + + writePendingWorldDeleteMap(remaining); + } catch (Throwable e) { + Iris.error("Failed to process queued startup world deletions."); + reportError(e); + e.printStackTrace(); + } + } + + private static LinkedHashMap loadPendingWorldDeleteMap() throws IOException { + LinkedHashMap queue = new LinkedHashMap<>(); + if (instance == null) { + return queue; + } + + File queueFile = instance.getDataFile(PENDING_WORLD_DELETE_FILE); + if (!queueFile.exists()) { + return queue; + } + + try (BufferedReader reader = new BufferedReader(new FileReader(queueFile))) { + String line; + while ((line = reader.readLine()) != null) { + String normalized = normalizeWorldName(line); + if (normalized == null) { + continue; + } + queue.putIfAbsent(normalized.toLowerCase(Locale.ROOT), normalized); + } + } + + return queue; + } + + private static void writePendingWorldDeleteMap(Map queue) throws IOException { + if (instance == null) { + return; + } + + File queueFile = instance.getDataFile(PENDING_WORLD_DELETE_FILE); + if (queue.isEmpty()) { + if (queueFile.exists()) { + IO.delete(queueFile); + } + return; + } + + File parent = queueFile.getParentFile(); + if (parent != null && !parent.exists() && !parent.mkdirs()) { + throw new IOException("Failed to create queue directory: " + parent.getAbsolutePath()); + } + + try (PrintWriter writer = new PrintWriter(new FileWriter(queueFile))) { + for (String worldName : queue.values()) { + writer.println(worldName); + } + } + } + + @Nullable + private static String normalizeWorldName(String worldName) { + if (worldName == null) { + return null; + } + + String trimmed = worldName.trim(); + if (trimmed.isEmpty()) { + return null; + } + + return trimmed; + } + private void autoStartStudio() { if (IrisSettings.get().getStudio().isAutoStartDefaultStudio()) { Iris.info("Starting up auto Studio!"); 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 da50ebb76..9175432f9 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,6 +34,7 @@ import art.arcane.iris.core.tools.IrisToolbelt; import art.arcane.iris.engine.framework.Engine; import art.arcane.iris.engine.object.IrisDimension; import art.arcane.iris.engine.object.IrisPosition; +import art.arcane.iris.engine.platform.PlatformChunkGenerator; import art.arcane.iris.engine.object.annotations.Snippet; import art.arcane.volmlib.util.collection.KMap; import art.arcane.volmlib.util.collection.KSet; @@ -41,21 +42,23 @@ import art.arcane.iris.util.context.IrisContext; import art.arcane.iris.engine.object.IrisJigsawStructurePlacement; import art.arcane.volmlib.util.collection.KList; import art.arcane.iris.util.decree.DecreeExecutor; -import art.arcane.volmlib.util.decree.DecreeOrigin; -import art.arcane.volmlib.util.decree.annotations.Decree; -import art.arcane.volmlib.util.decree.annotations.Param; +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.decree.specialhandlers.NullableDimensionHandler; import art.arcane.iris.util.format.C; import art.arcane.volmlib.util.format.Form; import art.arcane.volmlib.util.io.CountingDataInputStream; import art.arcane.volmlib.util.io.IO; import art.arcane.iris.util.mantle.TectonicPlate; +import art.arcane.iris.util.math.Position2; import art.arcane.volmlib.util.math.M; import art.arcane.iris.util.matter.Matter; import art.arcane.iris.util.nbt.mca.MCAFile; import art.arcane.iris.util.nbt.mca.MCAUtil; import art.arcane.iris.util.parallel.MultiBurst; import art.arcane.iris.util.plugin.VolmitSender; +import art.arcane.iris.util.scheduling.J; import art.arcane.iris.util.scheduling.jobs.Job; import lombok.SneakyThrows; import net.jpountz.lz4.LZ4BlockInputStream; @@ -72,38 +75,41 @@ import java.net.InetAddress; import java.net.NetworkInterface; import java.nio.file.Files; import java.util.*; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; -import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.*; +import java.util.concurrent.atomic.*; import java.util.stream.Collectors; import java.util.zip.GZIPInputStream; import java.util.zip.GZIPOutputStream; -@Decree(name = "Developer", origin = DecreeOrigin.BOTH, description = "Iris World Manager", aliases = {"dev"}) +@Director(name = "Developer", origin = DirectorOrigin.BOTH, description = "Iris World Manager", aliases = {"dev"}) public class CommandDeveloper implements DecreeExecutor { + private static final long DELETE_CHUNK_HEARTBEAT_MS = 5000L; + 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; - @Decree(description = "Get Loaded TectonicPlates Count", origin = DecreeOrigin.BOTH, sync = true) + @Director(description = "Get Loaded TectonicPlates Count", origin = DirectorOrigin.BOTH, sync = true) public void EngineStatus() { Iris.service(IrisEngineSVC.class) .engineStatus(sender()); } - @Decree(description = "Send a test exception to sentry") + @Director(description = "Send a test exception to sentry") public void Sentry() { Engine engine = engine(); if (engine != null) IrisContext.getOr(engine); Iris.reportError(new Exception("This is a test")); } - @Decree(description = "QOL command to open an overworld studio world", sync = true) + @Director(description = "QOL command to open an overworld studio world", sync = true) public void so() { sender().sendMessage(C.GREEN + "Opening studio for the \"Overworld\" pack (seed: 1337)"); Iris.service(StudioSVC.class).open(sender(), 1337, "overworld"); } - @Decree(description = "Set aura spins") + @Director(description = "Set aura spins") public void aura( @Param(description = "The h color value", defaultValue = "-20") int h, @@ -119,7 +125,7 @@ public class CommandDeveloper implements DecreeExecutor { sender().sendMessage("Aura Spins updated to " + h + " " + s + " " + b); } - @Decree(description = "Bitwise calculations") + @Director(description = "Bitwise calculations") public void bitwise( @Param(description = "The first value to run calculations on") int value1, @@ -144,7 +150,7 @@ public class CommandDeveloper implements DecreeExecutor { sender().sendMessage(C.GREEN + "" + value1 + " " + C.GREEN + operator.replaceAll("<", "≺").replaceAll(">", "≻").replaceAll("%", "%") + " " + C.GREEN + value2 + C.GREEN + " returns " + C.GREEN + v); } - @Decree(description = "Update the pack of a world (UNSAFE!)", name = "update-world", aliases = "^world") + @Director(description = "Update the pack of a world (UNSAFE!)", name = "update-world", aliases = "^world") public void updateWorld( @Param(description = "The world to update", contextual = true) World world, @@ -180,7 +186,7 @@ public class CommandDeveloper implements DecreeExecutor { Iris.service(StudioSVC.class).installIntoWorld(sender(), pack.getLoadKey(), folder); } - @Decree(description = "Dev cmd to fix all the broken objects caused by faulty shrinkwarp") + @Director(description = "Dev cmd to fix all the broken objects caused by faulty shrinkwarp") public void fixObjects( @Param(aliases = "dimension", description = "The dimension type to create the world with") IrisDimension type @@ -298,7 +304,7 @@ public class CommandDeveloper implements DecreeExecutor { }); } - @Decree(description = "Test") + @Director(description = "Test") public void mantle(@Param(defaultValue = "false") boolean plate, @Param(defaultValue = "21474836474") String name) throws Throwable { var base = Iris.instance.getDataFile("dump", "pv." + name + ".ttp.lz4b.bin"); var section = Iris.instance.getDataFile("dump", "pv." + name + ".section.bin"); @@ -325,7 +331,7 @@ public class CommandDeveloper implements DecreeExecutor { Files.write(target.toPath(), bytes); } - @Decree(description = "Test") + @Director(description = "Test") public void dumpThreads() { try { File fi = Iris.instance.getDataFile("dump", "td-" + new java.sql.Date(M.ms()) + ".txt"); @@ -362,7 +368,7 @@ public class CommandDeveloper implements DecreeExecutor { } @SneakyThrows - @Decree(description = "Generate Iris structures for all loaded datapack structures") + @Director(description = "Generate Iris structures for all loaded datapack structures") public void generateStructures( @Param(description = "The pack to add the generated structures to", aliases = "pack", defaultValue = "null", customHandler = NullableDimensionHandler.class) IrisDimension dimension, @@ -485,7 +491,7 @@ public class CommandDeveloper implements DecreeExecutor { data.hotloaded(); } - @Decree(description = "Test") + @Director(description = "Test") public void packBenchmark( @Param(description = "The pack to bench", aliases = {"pack"}, defaultValue = "overworld") IrisDimension dimension, @@ -497,7 +503,7 @@ public class CommandDeveloper implements DecreeExecutor { new IrisPackBenchmarking(dimension, radius, gui); } - @Decree(description = "Upgrade to another Minecraft version") + @Director(description = "Upgrade to another Minecraft version") public void upgrade( @Param(description = "The version to upgrade to", defaultValue = "latest") DataVersion version) { sender().sendMessage(C.GREEN + "Upgrading to " + version.getVersion() + "..."); @@ -505,7 +511,7 @@ public class CommandDeveloper implements DecreeExecutor { sender().sendMessage(C.GREEN + "Done upgrading! You can now update your server version to " + version.getVersion()); } - @Decree(description = "test") + @Director(description = "test") public void mca ( @Param(description = "String") String world) { try { @@ -519,7 +525,550 @@ public class CommandDeveloper implements DecreeExecutor { } - @Decree(description = "UnloadChunks for good reasons.") + @Director(description = "Delete nearby chunk blocks for regen testing", name = "delete-chunk", aliases = {"delchunk", "dc"}, origin = DirectorOrigin.PLAYER, sync = true) + public void deleteChunk( + @Param(description = "Radius in chunks around your current chunk", defaultValue = "0") + int radius, + @Param(description = "How many chunks to process in parallel (0 = auto)", aliases = {"threads", "concurrency"}, defaultValue = "0") + int parallelism + ) { + 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 + "This is not an Iris world."); + return; + } + String worldKey = world.getName().toLowerCase(Locale.ROOT); + if (!ACTIVE_DELETE_CHUNK_WORLDS.add(worldKey)) { + sender().sendMessage(C.RED + "A delete-chunk run is already active for this world."); + return; + } + + int threads = resolveDeleteChunkThreadCount(parallelism); + int centerX = player().getLocation().getBlockX() >> 4; + int centerZ = player().getLocation().getBlockZ() >> 4; + List targets = buildDeleteChunkTargets(centerX, centerZ, radius); + int totalChunks = targets.size(); + String runId = world.getName() + "-" + System.currentTimeMillis(); + PlatformChunkGenerator access = IrisToolbelt.access(world); + if (access == null || access.getEngine() == null) { + ACTIVE_DELETE_CHUNK_WORLDS.remove(worldKey); + sender().sendMessage(C.RED + "The engine access for this world is null."); + return; + } + + art.arcane.iris.util.mantle.Mantle mantle = access.getEngine().getMantle().getMantle(); + VolmitSender sender = sender(); + + sender.sendMessage(C.GREEN + "Deleting blocks in " + C.GOLD + totalChunks + C.GREEN + " chunk(s) with " + C.GOLD + threads + C.GREEN + " worker(s)."); + if (J.isFolia()) { + sender.sendMessage(C.YELLOW + "Folia maintenance mode enabled for lock-safe chunk wipe + mantle purge."); + } + sender.sendMessage(C.YELLOW + "Delete-chunk run id: " + C.GOLD + runId + C.YELLOW + "."); + Iris.info("Delete-chunk run start: id=" + runId + + " world=" + world.getName() + + " center=" + centerX + "," + centerZ + + " radius=" + radius + + " workers=" + threads + + " chunks=" + totalChunks); + + Set workerThreads = ConcurrentHashMap.newKeySet(); + AtomicInteger workerCounter = new AtomicInteger(); + ThreadFactory threadFactory = runnable -> { + Thread thread = new Thread(runnable, "Iris-DeleteChunk-" + runId + "-" + workerCounter.incrementAndGet()); + thread.setDaemon(true); + workerThreads.add(thread); + return thread; + }; + + Thread orchestrator = new Thread(() -> runDeleteChunkOrchestrator( + sender, + world, + mantle, + targets, + threads, + runId, + worldKey, + workerThreads, + threadFactory + ), "Iris-DeleteChunk-Orchestrator-" + runId); + orchestrator.setDaemon(true); + try { + orchestrator.start(); + Iris.info("Delete-chunk worker dispatched on dedicated thread=" + orchestrator.getName() + " id=" + runId + "."); + } catch (Throwable e) { + ACTIVE_DELETE_CHUNK_WORLDS.remove(worldKey); + sender.sendMessage(C.RED + "Failed to start delete-chunk worker thread. See console."); + Iris.reportError(e); + } + } + + private int resolveDeleteChunkThreadCount(int parallelism) { + int threads = parallelism <= 0 ? Runtime.getRuntime().availableProcessors() : parallelism; + if (J.isFolia() && parallelism <= 0) { + threads = 1; + } + return Math.max(1, threads); + } + + private List buildDeleteChunkTargets(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 runDeleteChunkOrchestrator( + VolmitSender sender, + World world, + art.arcane.iris.util.mantle.Mantle mantle, + List targets, + int threadCount, + String runId, + String worldKey, + Set workerThreads, + ThreadFactory threadFactory + ) { + long runStart = System.currentTimeMillis(); + AtomicReference phase = new AtomicReference<>("bootstrap"); + AtomicLong phaseSince = new AtomicLong(runStart); + AtomicBoolean runDone = new AtomicBoolean(false); + Thread watchdog = createDeleteChunkSetupWatchdog(world, runId, runDone, phase, phaseSince); + watchdog.start(); + + IrisToolbelt.beginWorldMaintenance(world, "delete-chunk"); + try (ThreadPoolExecutor pool = (ThreadPoolExecutor) Executors.newFixedThreadPool(threadCount, threadFactory)) { + setDeleteChunkPhase(phase, phaseSince, "dispatch", world, runId); + DeleteChunkSummary summary = executeDeleteChunkQueue(world, mantle, targets, pool, workerThreads, runId); + if (summary.failedChunks() <= 0) { + sender.sendMessage(C.GREEN + "Deleted blocks in " + C.GOLD + summary.successChunks() + C.GREEN + "/" + C.GOLD + summary.totalChunks() + C.GREEN + " chunk(s)."); + return; + } + + sender.sendMessage(C.RED + "Delete-chunk completed with " + C.GOLD + summary.failedChunks() + C.RED + " failed chunk(s)."); + sender.sendMessage(C.YELLOW + "Successful chunks: " + C.GOLD + summary.successChunks() + C.YELLOW + "/" + C.GOLD + summary.totalChunks() + C.YELLOW + "."); + sender.sendMessage(C.YELLOW + "Retry attempts used: " + C.GOLD + summary.retryCount() + C.YELLOW + "."); + if (!summary.failedPreview().isEmpty()) { + sender.sendMessage(C.YELLOW + "Failed chunks sample: " + C.GOLD + summary.failedPreview()); + } + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + sender.sendMessage(C.RED + "Delete-chunk run was interrupted."); + Iris.warn("Delete-chunk run interrupted: id=" + runId + " world=" + world.getName()); + } catch (Throwable e) { + sender.sendMessage(C.RED + "Delete-chunk run failed. See console."); + Iris.reportError(e); + } finally { + runDone.set(true); + watchdog.interrupt(); + IrisToolbelt.endWorldMaintenance(world, "delete-chunk"); + ACTIVE_DELETE_CHUNK_WORLDS.remove(worldKey); + Iris.info("Delete-chunk run closed: id=" + runId + " world=" + world.getName() + " totalMs=" + (System.currentTimeMillis() - runStart)); + } + } + + private DeleteChunkSummary executeDeleteChunkQueue( + World world, + art.arcane.iris.util.mantle.Mantle mantle, + List targets, + ThreadPoolExecutor pool, + Set workerThreads, + String runId + ) throws InterruptedException { + ArrayDeque pending = new ArrayDeque<>(targets.size()); + long queuedAt = System.currentTimeMillis(); + for (Position2 target : targets) { + pending.addLast(new DeleteChunkTask(target.getX(), target.getZ(), 1, queuedAt)); + } + + ConcurrentMap activeTasks = new ConcurrentHashMap<>(); + ExecutorCompletionService completion = new ExecutorCompletionService<>(pool); + List failedChunks = new ArrayList<>(); + + int totalChunks = targets.size(); + int successChunks = 0; + int failedCount = 0; + int retryCount = 0; + long submittedTasks = 0L; + long finishedTasks = 0L; + int completedChunks = 0; + int inFlight = 0; + int unchangedHeartbeats = 0; + int lastCompleted = -1; + long lastDump = 0L; + + while (inFlight < pool.getMaximumPoolSize() && !pending.isEmpty()) { + DeleteChunkTask task = pending.removeFirst(); + completion.submit(() -> runDeleteChunkTask(task, world, mantle, activeTasks)); + inFlight++; + submittedTasks++; + } + + while (completedChunks < totalChunks) { + Future future = completion.poll(DELETE_CHUNK_HEARTBEAT_MS, TimeUnit.MILLISECONDS); + if (future == null) { + if (completedChunks == lastCompleted) { + unchangedHeartbeats++; + } else { + unchangedHeartbeats = 0; + lastCompleted = completedChunks; + } + + Iris.warn("Delete-chunk 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() + + " activeTasks=" + formatDeleteChunkActiveTasks(activeTasks)); + + if (unchangedHeartbeats >= 3 && System.currentTimeMillis() - lastDump >= 10000L) { + lastDump = System.currentTimeMillis(); + Iris.warn("Delete-chunk appears stalled; dumping worker stack traces for id=" + runId + "."); + dumpDeleteChunkWorkerStacks(workerThreads, world.getName()); + } + continue; + } + + DeleteChunkResult result; + try { + result = future.get(); + } catch (ExecutionException e) { + Throwable cause = e.getCause() == null ? e : e.getCause(); + throw new IllegalStateException("Delete-chunk worker failed unexpectedly for run " + runId, cause); + } + + inFlight--; + finishedTasks++; + long duration = result.finishedAtMs() - result.startedAtMs(); + + if (result.success()) { + completedChunks++; + successChunks++; + if (result.task().attempt() > 1) { + Iris.warn("Delete-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("Delete-chunk slow: id=" + runId + + " chunk=" + result.task().chunkX() + "," + result.task().chunkZ() + + " durationMs=" + duration + + " loadedAtStart=" + result.loadedAtStart()); + } + } else if (result.task().attempt() < DELETE_CHUNK_MAX_ATTEMPTS) { + retryCount++; + DeleteChunkTask retryTask = result.task().retry(System.currentTimeMillis()); + pending.addLast(retryTask); + Iris.warn("Delete-chunk retry scheduled: id=" + runId + + " chunk=" + result.task().chunkX() + "," + result.task().chunkZ() + + " failedAttempt=" + result.task().attempt() + + " nextAttempt=" + retryTask.attempt() + + " error=" + result.errorSummary()); + } else { + completedChunks++; + failedCount++; + Position2 failed = new Position2(result.task().chunkX(), result.task().chunkZ()); + failedChunks.add(failed); + Iris.warn("Delete-chunk terminal failure: id=" + runId + + " chunk=" + result.task().chunkX() + "," + result.task().chunkZ() + + " attempts=" + result.task().attempt() + + " error=" + result.errorSummary()); + if (result.error() != null) { + Iris.reportError(result.error()); + } + } + + while (inFlight < pool.getMaximumPoolSize() && !pending.isEmpty()) { + DeleteChunkTask task = pending.removeFirst(); + completion.submit(() -> runDeleteChunkTask(task, world, mantle, activeTasks)); + inFlight++; + submittedTasks++; + } + } + + String preview = formatDeleteChunkFailedPreview(failedChunks); + Iris.info("Delete-chunk run complete: id=" + runId + + " world=" + world.getName() + + " total=" + totalChunks + + " success=" + successChunks + + " failed=" + failedCount + + " retries=" + retryCount + + " submittedTasks=" + submittedTasks + + " finishedTasks=" + finishedTasks + + " failedPreview=" + preview); + return new DeleteChunkSummary(totalChunks, successChunks, failedCount, retryCount, preview); + } + + private DeleteChunkResult runDeleteChunkTask( + DeleteChunkTask task, + World world, + art.arcane.iris.util.mantle.Mantle mantle, + ConcurrentMap activeTasks + ) { + String worker = Thread.currentThread().getName(); + long startedAt = System.currentTimeMillis(); + boolean loadedAtStart = false; + try { + loadedAtStart = world.isChunkLoaded(task.chunkX(), task.chunkZ()); + } catch (Throwable ignored) { + } + + activeTasks.put(worker, new DeleteChunkActiveTask(task.chunkX(), task.chunkZ(), task.attempt(), startedAt, loadedAtStart)); + try { + DeleteChunkRegionResult regionResult = wipeChunkRegion(world, task.chunkX(), task.chunkZ()); + if (!regionResult.success()) { + return DeleteChunkResult.failure(task, worker, startedAt, System.currentTimeMillis(), loadedAtStart, regionResult.error()); + } + mantle.deleteChunk(task.chunkX(), task.chunkZ()); + return DeleteChunkResult.success(task, worker, startedAt, System.currentTimeMillis(), loadedAtStart); + } catch (Throwable e) { + if (e instanceof InterruptedException) { + Thread.currentThread().interrupt(); + } + return DeleteChunkResult.failure(task, worker, startedAt, System.currentTimeMillis(), loadedAtStart, e); + } finally { + activeTasks.remove(worker); + } + } + + private DeleteChunkRegionResult wipeChunkRegion(World world, int chunkX, int chunkZ) throws InterruptedException { + CountDownLatch latch = new CountDownLatch(1); + AtomicReference failure = new AtomicReference<>(); + if (!J.runRegion(world, chunkX, chunkZ, () -> { + try { + Chunk chunk = world.getChunkAt(chunkX, chunkZ); + for (org.bukkit.entity.Entity entity : chunk.getEntities()) { + if (!(entity instanceof org.bukkit.entity.Player)) { + entity.remove(); + } + } + + int minY = world.getMinHeight(); + int maxY = world.getMaxHeight(); + for (int xx = 0; xx < 16; xx++) { + for (int zz = 0; zz < 16; zz++) { + for (int yy = minY; yy < maxY; yy++) { + chunk.getBlock(xx, yy, zz).setType(org.bukkit.Material.AIR, false); + } + } + } + } catch (Throwable e) { + failure.set(e); + } finally { + latch.countDown(); + } + })) { + return DeleteChunkRegionResult.fail(new IllegalStateException("Failed to schedule region task for chunk " + chunkX + "," + chunkZ)); + } + + if (!latch.await(30, TimeUnit.SECONDS)) { + return DeleteChunkRegionResult.fail(new TimeoutException("Timed out waiting for region task at chunk " + chunkX + "," + chunkZ)); + } + + Throwable thrown = failure.get(); + if (thrown != null) { + return DeleteChunkRegionResult.fail(thrown); + } + return DeleteChunkRegionResult.ok(); + } + + private Thread createDeleteChunkSetupWatchdog( + World world, + String runId, + AtomicBoolean runDone, + AtomicReference phase, + AtomicLong phaseSince + ) { + Thread watchdog = new Thread(() -> { + while (!runDone.get()) { + try { + Thread.sleep(DELETE_CHUNK_HEARTBEAT_MS); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + return; + } + + if (!runDone.get()) { + long elapsed = System.currentTimeMillis() - phaseSince.get(); + Iris.warn("Delete-chunk setup heartbeat: id=" + runId + + " phase=" + phase.get() + + " elapsedMs=" + elapsed + + " world=" + world.getName()); + } + } + }, "Iris-DeleteChunk-SetupWatchdog-" + runId); + watchdog.setDaemon(true); + return watchdog; + } + + private void setDeleteChunkPhase( + AtomicReference phase, + AtomicLong phaseSince, + String next, + World world, + String runId + ) { + phase.set(next); + phaseSince.set(System.currentTimeMillis()); + Iris.info("Delete-chunk phase: id=" + runId + " phase=" + next + " world=" + world.getName()); + } + + private String formatDeleteChunkFailedPreview(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 String formatDeleteChunkActiveTasks(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; + } + DeleteChunkActiveTask 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(activeTask.loadedAtStart() ? ":loaded" : ":cold"); + count++; + } + builder.append("}"); + return builder.toString(); + } + + private void dumpDeleteChunkWorkerStacks(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-DeleteChunk-") + || 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("Delete-chunk worker thread=" + thread.getName() + " state=" + thread.getState()); + StackTraceElement[] trace = thread.getStackTrace(); + int limit = Math.min(trace.length, DELETE_CHUNK_STACK_LIMIT); + for (int i = 0; i < limit; i++) { + Iris.warn(" at " + trace[i]); + } + } + } + + private record DeleteChunkTask(int chunkX, int chunkZ, int attempt, long queuedAtMs) { + private DeleteChunkTask retry(long now) { + return new DeleteChunkTask(chunkX, chunkZ, attempt + 1, now); + } + } + + private record DeleteChunkActiveTask(int chunkX, int chunkZ, int attempt, long startedAtMs, boolean loadedAtStart) { + } + + private record DeleteChunkResult( + DeleteChunkTask task, + String worker, + long startedAtMs, + long finishedAtMs, + boolean loadedAtStart, + boolean success, + Throwable error + ) { + private static DeleteChunkResult success(DeleteChunkTask task, String worker, long startedAtMs, long finishedAtMs, boolean loadedAtStart) { + return new DeleteChunkResult(task, worker, startedAtMs, finishedAtMs, loadedAtStart, true, null); + } + + private static DeleteChunkResult failure(DeleteChunkTask task, String worker, long startedAtMs, long finishedAtMs, boolean loadedAtStart, Throwable error) { + return new DeleteChunkResult(task, worker, startedAtMs, finishedAtMs, loadedAtStart, 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 DeleteChunkRegionResult(boolean success, Throwable error) { + private static DeleteChunkRegionResult ok() { + return new DeleteChunkRegionResult(true, null); + } + + private static DeleteChunkRegionResult fail(Throwable error) { + return new DeleteChunkRegionResult(false, error); + } + } + + private record DeleteChunkSummary(int totalChunks, int successChunks, int failedChunks, int retryCount, String failedPreview) { + } + + @Director(description = "UnloadChunks for good reasons.") public void unloadchunks() { List IrisWorlds = new ArrayList<>(); int chunksUnloaded = 0; @@ -545,7 +1094,7 @@ public class CommandDeveloper implements DecreeExecutor { } - @Decree + @Director public void objects(@Param(defaultValue = "overworld") IrisDimension dimension) { var loader = dimension.getLoader().getObjectLoader(); var sender = sender(); @@ -562,7 +1111,7 @@ public class CommandDeveloper implements DecreeExecutor { sender.sendMessage(C.RED + "Failed to load " + failed.get() + " of " + keys.length + " objects"); } - @Decree(description = "Test", aliases = {"ip"}) + @Director(description = "Test", aliases = {"ip"}) public void network() { try { Enumeration networkInterfaces = NetworkInterface.getNetworkInterfaces(); @@ -578,7 +1127,7 @@ public class CommandDeveloper implements DecreeExecutor { } } - @Decree(description = "Test the compression algorithms") + @Director(description = "Test the compression algorithms") public void compression( @Param(description = "base IrisWorld") World world, @Param(description = "raw TectonicPlate File") String path, @@ -661,4 +1210,3 @@ public class CommandDeveloper implements DecreeExecutor { }); } } - diff --git a/core/src/main/java/art/arcane/iris/core/commands/CommandEdit.java b/core/src/main/java/art/arcane/iris/core/commands/CommandEdit.java index d2cdd0100..58b761dcc 100644 --- a/core/src/main/java/art/arcane/iris/core/commands/CommandEdit.java +++ b/core/src/main/java/art/arcane/iris/core/commands/CommandEdit.java @@ -22,15 +22,15 @@ import art.arcane.iris.Iris; import art.arcane.iris.core.service.StudioSVC; import art.arcane.iris.engine.object.*; import art.arcane.iris.util.decree.DecreeExecutor; -import art.arcane.volmlib.util.decree.DecreeOrigin; -import art.arcane.volmlib.util.decree.annotations.Decree; -import art.arcane.volmlib.util.decree.annotations.Param; +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.format.C; import java.awt.*; -@Decree(name = "edit", origin = DecreeOrigin.PLAYER, studio = true, description = "Edit something") +@Director(name = "edit", origin = DirectorOrigin.PLAYER, studio = true, description = "Edit something") public class CommandEdit implements DecreeExecutor { private boolean noStudio() { @@ -60,7 +60,7 @@ public class CommandEdit implements DecreeExecutor { } - @Decree(description = "Edit the biome you specified", aliases = {"b"}, origin = DecreeOrigin.PLAYER) + @Director(description = "Edit the biome you specified", aliases = {"b"}, origin = DirectorOrigin.PLAYER) public void biome(@Param(contextual = false, description = "The biome to edit") IrisBiome biome) { if (noStudio()) { return; @@ -78,7 +78,7 @@ public class CommandEdit implements DecreeExecutor { } } - @Decree(description = "Edit the region you specified", aliases = {"r"}, origin = DecreeOrigin.PLAYER) + @Director(description = "Edit the region you specified", aliases = {"r"}, origin = DirectorOrigin.PLAYER) public void region(@Param(contextual = false, description = "The region to edit") IrisRegion region) { if (noStudio()) { return; @@ -96,7 +96,7 @@ public class CommandEdit implements DecreeExecutor { } } - @Decree(description = "Edit the dimension you specified", aliases = {"d"}, origin = DecreeOrigin.PLAYER) + @Director(description = "Edit the dimension you specified", aliases = {"d"}, origin = DirectorOrigin.PLAYER) public void dimension(@Param(contextual = false, description = "The dimension to edit") IrisDimension dimension) { if (noStudio()) { return; @@ -114,7 +114,7 @@ public class CommandEdit implements DecreeExecutor { } } - @Decree(description = "Edit the cave file you specified", aliases = {"c"}, origin = DecreeOrigin.PLAYER) + @Director(description = "Edit the cave file you specified", aliases = {"c"}, origin = DirectorOrigin.PLAYER) public void cave(@Param(contextual = false, description = "The cave to edit") IrisCave cave) { if (noStudio()) { return; @@ -132,7 +132,7 @@ public class CommandEdit implements DecreeExecutor { } } - @Decree(description = "Edit the structure file you specified", aliases = {"jigsawstructure", "structure"}, origin = DecreeOrigin.PLAYER) + @Director(description = "Edit the structure file you specified", aliases = {"jigsawstructure", "structure"}, origin = DirectorOrigin.PLAYER) public void jigsaw(@Param(contextual = false, description = "The jigsaw structure to edit") IrisJigsawStructure jigsaw) { if (noStudio()) { return; @@ -150,7 +150,7 @@ public class CommandEdit implements DecreeExecutor { } } - @Decree(description = "Edit the pool file you specified", aliases = {"jigsawpool", "pool"}, origin = DecreeOrigin.PLAYER) + @Director(description = "Edit the pool file you specified", aliases = {"jigsawpool", "pool"}, origin = DirectorOrigin.PLAYER) public void jigsawPool(@Param(contextual = false, description = "The jigsaw pool to edit") IrisJigsawPool pool) { if (noStudio()) { return; @@ -168,7 +168,7 @@ public class CommandEdit implements DecreeExecutor { } } - @Decree(description = "Edit the jigsaw piece file you specified", aliases = {"jigsawpiece", "piece"}, origin = DecreeOrigin.PLAYER) + @Director(description = "Edit the jigsaw piece file you specified", aliases = {"jigsawpiece", "piece"}, origin = DirectorOrigin.PLAYER) public void jigsawPiece(@Param(contextual = false, description = "The jigsaw piece to edit") IrisJigsawPiece piece) { if (noStudio()) { return; diff --git a/core/src/main/java/art/arcane/iris/core/commands/CommandFind.java b/core/src/main/java/art/arcane/iris/core/commands/CommandFind.java index 51d3e2c55..3066ca2e0 100644 --- a/core/src/main/java/art/arcane/iris/core/commands/CommandFind.java +++ b/core/src/main/java/art/arcane/iris/core/commands/CommandFind.java @@ -23,15 +23,15 @@ import art.arcane.iris.engine.object.IrisBiome; import art.arcane.iris.engine.object.IrisJigsawStructure; import art.arcane.iris.engine.object.IrisRegion; import art.arcane.iris.util.decree.DecreeExecutor; -import art.arcane.volmlib.util.decree.DecreeOrigin; -import art.arcane.volmlib.util.decree.annotations.Decree; -import art.arcane.volmlib.util.decree.annotations.Param; +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.decree.specialhandlers.ObjectHandler; import art.arcane.iris.util.format.C; -@Decree(name = "find", origin = DecreeOrigin.PLAYER, description = "Iris Find commands", aliases = "goto") +@Director(name = "find", origin = DirectorOrigin.PLAYER, description = "Iris Find commands", aliases = "goto") public class CommandFind implements DecreeExecutor { - @Decree(description = "Find a biome") + @Director(description = "Find a biome") public void biome( @Param(description = "The biome to look for") IrisBiome biome, @@ -48,7 +48,7 @@ public class CommandFind implements DecreeExecutor { e.gotoBiome(biome, player(), teleport); } - @Decree(description = "Find a region") + @Director(description = "Find a region") public void region( @Param(description = "The region to look for") IrisRegion region, @@ -65,7 +65,7 @@ public class CommandFind implements DecreeExecutor { e.gotoRegion(region, player(), teleport); } - @Decree(description = "Find a structure") + @Director(description = "Find a structure") public void structure( @Param(description = "The structure to look for") IrisJigsawStructure structure, @@ -82,7 +82,7 @@ public class CommandFind implements DecreeExecutor { e.gotoJigsaw(structure, player(), teleport); } - @Decree(description = "Find a point of interest.") + @Director(description = "Find a point of interest.") public void poi( @Param(description = "The type of PoI to look for.") String type, @@ -98,7 +98,7 @@ public class CommandFind implements DecreeExecutor { e.gotoPOI(type, player(), teleport); } - @Decree(description = "Find an object") + @Director(description = "Find an object") public void object( @Param(description = "The object to look for", customHandler = ObjectHandler.class) String object, 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 f5d4bb6a0..58e48313d 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 @@ -20,44 +20,88 @@ package art.arcane.iris.core.commands; import art.arcane.iris.Iris; import art.arcane.iris.core.IrisSettings; +import art.arcane.iris.core.IrisWorlds; import art.arcane.iris.core.loader.IrisData; import art.arcane.iris.core.nms.INMS; import art.arcane.iris.core.service.StudioSVC; import art.arcane.iris.core.tools.IrisToolbelt; import art.arcane.iris.engine.framework.Engine; import art.arcane.iris.engine.object.IrisDimension; +import art.arcane.iris.engine.platform.PlatformChunkGenerator; import art.arcane.volmlib.util.collection.KList; -import art.arcane.volmlib.util.decree.DecreeParameterHandler; +import art.arcane.iris.util.decree.DecreeContext; +import art.arcane.volmlib.util.director.DirectorParameterHandler; import art.arcane.iris.util.decree.DecreeExecutor; -import art.arcane.volmlib.util.decree.DecreeOrigin; -import art.arcane.volmlib.util.decree.annotations.Decree; -import art.arcane.volmlib.util.decree.annotations.Param; -import art.arcane.volmlib.util.decree.exceptions.DecreeParsingException; +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.volmlib.util.director.exceptions.DirectorParsingException; import art.arcane.iris.util.decree.specialhandlers.NullablePlayerHandler; import art.arcane.iris.util.format.C; import art.arcane.volmlib.util.io.IO; +import art.arcane.iris.util.math.Position2; +import art.arcane.iris.util.parallel.SyncExecutor; import art.arcane.iris.util.misc.ServerProperties; +import art.arcane.iris.util.misc.RegenRuntime; +import art.arcane.iris.util.mantle.MantleChunk; +import art.arcane.iris.util.matter.TileWrapper; import art.arcane.iris.util.plugin.VolmitSender; import art.arcane.iris.util.scheduling.J; import org.bukkit.Bukkit; +import org.bukkit.Chunk; import org.bukkit.World; +import org.bukkit.block.data.BlockData; +import org.bukkit.boss.BarColor; +import org.bukkit.boss.BarStyle; +import org.bukkit.boss.BossBar; import org.bukkit.configuration.ConfigurationSection; import org.bukkit.configuration.file.YamlConfiguration; import org.bukkit.entity.Player; import org.bukkit.scheduler.BukkitRunnable; +import art.arcane.volmlib.util.mantle.flag.MantleFlag; import java.io.*; +import java.util.ArrayDeque; +import java.util.ArrayList; import java.util.LinkedHashSet; import java.util.List; +import java.util.Map; import java.util.Set; +import java.util.concurrent.ConcurrentMap; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ExecutorCompletionService; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.ThreadFactory; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeoutException; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicLong; import java.util.concurrent.atomic.AtomicReference; import static art.arcane.iris.core.service.EditSVC.deletingWorld; import static art.arcane.iris.util.misc.ServerProperties.BUKKIT_YML; import static org.bukkit.Bukkit.getServer; -@Decree(name = "iris", aliases = {"ir", "irs"}, description = "Basic Command") +@Director(name = "iris", aliases = {"ir", "irs"}, description = "Basic Command") public class CommandIris implements DecreeExecutor { + 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 int REGEN_STALL_DUMP_HEARTBEATS = 3; + private static final int REGEN_STALL_ABORT_HEARTBEATS = 24; + private static final long REGEN_MAX_RESET_CHUNKS = 65536L; + private static final int REGEN_RESET_PROGRESS_STEP = 128; + private static final long REGEN_RESET_DELETE_ABORT_MS = 60000L; + 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; @@ -74,7 +118,7 @@ public class CommandIris implements DecreeExecutor { String worldNameToCheck = "YourWorldName"; VolmitSender sender = Iris.getSender(); - @Decree(description = "Create a new world", aliases = {"+", "c"}) + @Director(description = "Create a new world", aliases = {"+", "c"}) public void create( @Param(aliases = "world-name", description = "The name of the world to create") String name, @@ -88,7 +132,11 @@ public class CommandIris implements DecreeExecutor { @Param(description = "The seed to generate the world with", defaultValue = "1337") long seed, @Param(aliases = "main-world", description = "Whether or not to automatically use this world as the main world", defaultValue = "false") - boolean main + boolean main, + @Param(aliases = {"remove-others", "removeothers"}, description = "When main-world is true, remove other Iris worlds from bukkit.yml and queue deletion on startup", defaultValue = "false") + boolean removeOthers, + @Param(aliases = {"remove-worlds", "removeworlds"}, description = "Comma-separated world names to remove from Iris control and delete on next startup (main-world only)", defaultValue = "none") + String removeWorlds ) { if (name.equalsIgnoreCase("iris")) { sender().sendMessage(C.RED + "You cannot use the world name \"iris\" for creating worlds as Iris uses this directory for studio worlds."); @@ -119,8 +167,14 @@ public class CommandIris implements DecreeExecutor { return; } + if (!main && (removeOthers || hasExplicitCleanupWorlds(removeWorlds))) { + sender().sendMessage(C.YELLOW + "remove-others/remove-worlds only apply when main-world=true. Ignoring cleanup options."); + removeOthers = false; + removeWorlds = "none"; + } + if (J.isFolia()) { - if (stageFoliaWorldCreation(name, dimension, seed, main)) { + if (stageFoliaWorldCreation(name, dimension, seed, main, removeOthers, removeWorlds)) { sender().sendMessage(C.GREEN + "World staging completed. Restart the server to generate/load \"" + name + "\"."); } return; @@ -148,6 +202,12 @@ public class CommandIris implements DecreeExecutor { worldCreation = false; return; } + + if (main && !applyMainWorldCleanup(name, removeOthers, removeWorlds)) { + worldCreation = false; + return; + } + worldCreation = false; sender().sendMessage(C.GREEN + "Successfully created your world!"); if (main) sender().sendMessage(C.GREEN + "Your world will automatically be set as the main world when the server restarts."); @@ -188,7 +248,7 @@ public class CommandIris implements DecreeExecutor { } } - private boolean stageFoliaWorldCreation(String name, IrisDimension dimension, long seed, boolean main) { + private boolean stageFoliaWorldCreation(String name, IrisDimension dimension, long seed, boolean main, boolean removeOthers, String removeWorlds) { sender().sendMessage(C.YELLOW + "Runtime world creation is disabled on Folia."); sender().sendMessage(C.YELLOW + "Preparing world files and bukkit.yml for next startup..."); @@ -210,6 +270,11 @@ public class CommandIris implements DecreeExecutor { sender().sendMessage(C.RED + "World was staged, but failed to update server.properties main world."); return false; } + + if (!applyMainWorldCleanup(name, removeOthers, removeWorlds)) { + sender().sendMessage(C.RED + "World was staged, but failed to apply main-world cleanup options."); + return false; + } } sender().sendMessage(C.GREEN + "Staged Iris world \"" + name + "\" with generator Iris:" + dimension.getLoadKey() + " and seed " + seed + "."); @@ -248,7 +313,127 @@ public class CommandIris implements DecreeExecutor { } } - @Decree(description = "Teleport to another world", aliases = {"tp"}, sync = true) + private boolean applyMainWorldCleanup(String mainWorld, boolean removeOthers, String removeWorlds) { + Set targets = resolveCleanupTargets(mainWorld, removeOthers, removeWorlds); + if (targets.isEmpty()) { + return true; + } + + sender().sendMessage(C.YELLOW + "Applying main-world cleanup for " + targets.size() + " world(s)."); + + YamlConfiguration yml = YamlConfiguration.loadConfiguration(BUKKIT_YML); + ConfigurationSection worlds = yml.getConfigurationSection("worlds"); + + Set removedFromBukkit = new LinkedHashSet<>(); + Set notRemoved = new LinkedHashSet<>(); + for (String target : targets) { + String key = findWorldKeyIgnoreCase(worlds, target); + if (key == null) { + notRemoved.add(target); + continue; + } + + String generator = worlds.getString(key + ".generator"); + if (generator == null || !(generator.equalsIgnoreCase("iris") || generator.startsWith("Iris:"))) { + notRemoved.add(key); + continue; + } + + worlds.set(key, null); + removedFromBukkit.add(key); + } + + try { + if (worlds != null && worlds.getKeys(false).isEmpty()) { + yml.set("worlds", null); + } + + if (!removedFromBukkit.isEmpty()) { + yml.save(BUKKIT_YML); + } + } catch (IOException e) { + sender().sendMessage(C.RED + "Failed to update bukkit.yml while applying cleanup: " + e.getMessage()); + Iris.reportError(e); + return false; + } + + try { + int queued = Iris.queueWorldDeletionOnStartup(targets); + if (queued > 0) { + sender().sendMessage(C.GREEN + "Queued " + queued + " world folder(s) for deletion on next startup."); + } else { + sender().sendMessage(C.YELLOW + "Cleanup queue already contained the requested world folder(s)."); + } + } catch (IOException e) { + sender().sendMessage(C.RED + "Failed to queue startup world deletions: " + e.getMessage()); + Iris.reportError(e); + return false; + } + + if (!removedFromBukkit.isEmpty()) { + sender().sendMessage(C.GREEN + "Removed from Iris control in bukkit.yml: " + String.join(", ", removedFromBukkit)); + } + + if (!notRemoved.isEmpty()) { + sender().sendMessage(C.YELLOW + "Skipped from bukkit.yml removal (not found or non-Iris generator): " + String.join(", ", notRemoved)); + } + + return true; + } + + private Set resolveCleanupTargets(String mainWorld, boolean removeOthers, String removeWorlds) { + Set targets = new LinkedHashSet<>(); + if (removeOthers) { + IrisWorlds.readBukkitWorlds().keySet().stream() + .filter(world -> !world.equalsIgnoreCase(mainWorld)) + .forEach(targets::add); + } + + if (hasExplicitCleanupWorlds(removeWorlds)) { + for (String raw : removeWorlds.split("[,;\\s]+")) { + if (raw == null || raw.isBlank()) { + continue; + } + + if (raw.equalsIgnoreCase(mainWorld)) { + continue; + } + + targets.add(raw.trim()); + } + } + + return targets; + } + + private static boolean hasExplicitCleanupWorlds(String removeWorlds) { + if (removeWorlds == null) { + return false; + } + + String trimmed = removeWorlds.trim(); + return !trimmed.isEmpty() && !trimmed.equalsIgnoreCase("none"); + } + + private static String findWorldKeyIgnoreCase(ConfigurationSection worlds, String requested) { + if (worlds == null || requested == null) { + return null; + } + + if (worlds.contains(requested)) { + return requested; + } + + for (String key : worlds.getKeys(false)) { + if (key.equalsIgnoreCase(requested)) { + return key; + } + } + + return null; + } + + @Director(description = "Teleport to another world", aliases = {"tp"}, sync = true) public void teleport( @Param(description = "World to teleport to") World world, @@ -273,14 +458,14 @@ public class CommandIris implements DecreeExecutor { }.runTask(Iris.instance); } - @Decree(description = "Print version information") + @Director(description = "Print version information") public void version() { sender().sendMessage(C.GREEN + "Iris v" + Iris.instance.getDescription().getVersion() + " by Volmit Software"); } /* /todo - @Decree(description = "Benchmark a pack", origin = DecreeOrigin.CONSOLE) + @Director(description = "Benchmark a pack", origin = DirectorOrigin.CONSOLE) public void packbenchmark( @Param(description = "Dimension to benchmark") IrisDimension type @@ -291,7 +476,7 @@ public class CommandIris implements DecreeExecutor { IrisPackBenchmarking.runBenchmark(); } */ - @Decree(description = "Print world height information", origin = DecreeOrigin.PLAYER) + @Director(description = "Print world height information", origin = DirectorOrigin.PLAYER) public void height() { if (sender().isPlayer()) { sender().sendMessage(C.GREEN + "" + sender().player().getWorld().getMinHeight() + " to " + sender().player().getWorld().getMaxHeight()); @@ -303,7 +488,7 @@ public class CommandIris implements DecreeExecutor { } } - @Decree(description = "Check access of all worlds.", aliases = {"accesslist"}) + @Director(description = "Check access of all worlds.", aliases = {"accesslist"}) public void worlds() { KList IrisWorlds = new KList<>(); KList BukkitWorlds = new KList<>(); @@ -341,7 +526,7 @@ public class CommandIris implements DecreeExecutor { } } - @Decree(description = "Remove an Iris world", aliases = {"del", "rm", "delete"}, sync = true) + @Director(description = "Remove an Iris world", aliases = {"del", "rm", "delete"}, sync = true) public void remove( @Param(description = "The world to remove") World world, @@ -417,7 +602,7 @@ public class CommandIris implements DecreeExecutor { return dir.delete(); } - @Decree(description = "Toggle debug") + @Director(description = "Toggle debug") public void debug( @Param(name = "on", description = "Whether or not debug should be on", defaultValue = "other") Boolean on @@ -429,7 +614,7 @@ public class CommandIris implements DecreeExecutor { } //TODO fix pack trimming - @Decree(description = "Download a project.", aliases = "dl") + @Director(description = "Download a project.", aliases = "dl") public void download( @Param(name = "pack", description = "The pack to download", defaultValue = "overworld", aliases = "project") String pack, @@ -450,7 +635,7 @@ public class CommandIris implements DecreeExecutor { } } - @Decree(description = "Get metrics for your world", aliases = "measure", origin = DecreeOrigin.PLAYER) + @Director(description = "Get metrics for your world", aliases = "measure", origin = DirectorOrigin.PLAYER) public void metrics() { if (!IrisToolbelt.isIrisWorld(world())) { sender().sendMessage(C.RED + "You must be in an Iris world"); @@ -460,14 +645,1346 @@ public class CommandIris implements DecreeExecutor { engine().printMetrics(sender()); } - @Decree(description = "Reload configuration file (this is also done automatically)") + @Director(description = "Reload configuration file (this is also done automatically)") public void reload() { IrisSettings.invalidate(); IrisSettings.get(); sender().sendMessage(C.GREEN + "Hotloaded settings"); } - @Decree(description = "Unload an Iris World", origin = DecreeOrigin.PLAYER, sync = true) + @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); + 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 object/jigsaw stages are bypassed. 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); + Iris.info("Regen mode config: id=" + runId + + " mode=" + regenMode.id() + + " maintenance=" + regenMode.usesMaintenance() + + " bypassMantle=" + regenMode.bypassMantleStages() + + " resetMantleChunks=" + regenMode.resetMantleChunks() + + " passes=" + regenMode.passCount() + + " overlay=" + regenMode.applyMantleOverlay() + + " 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(); + 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) { + int threads = parallelism <= 0 ? Runtime.getRuntime().availableProcessors() : parallelism; + if (J.isFolia() && parallelism <= 0) { + threads = 1; + } + return Math.max(1, threads); + } + + 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); + DecreeContext.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; + } + + if (mode.resetMantleChunks()) { + setRegenSetupPhase(setupPhase, setupPhaseSince, "prepare-mantle", world, runId); + updateRegenSetupDisplay(display, mode, "Preparing mantle reset", 5, 6); + int writeRadius = Math.max(0, platform.getEngine().getMantle().getRadius()); + int plannedRadius = Math.max(0, platform.getEngine().getMantle().getRealRadius()); + int resetPadding = mode.usesMaintenance() ? plannedRadius : 0; + long estimatedResetChunks = estimateRegenMantleResetChunks(targets, resetPadding); + if (estimatedResetChunks > REGEN_MAX_RESET_CHUNKS) { + int cappedPadding = capRegenMantleResetPadding(targets, resetPadding, REGEN_MAX_RESET_CHUNKS); + Iris.warn("Regen mantle reset cap applied: id=" + runId + + " desiredPadding=" + resetPadding + + " cappedPadding=" + cappedPadding + + " estimatedChunks=" + estimatedResetChunks + + " maxChunks=" + REGEN_MAX_RESET_CHUNKS); + resetPadding = cappedPadding; + } + Iris.info("Regen mantle reset planning: id=" + runId + + " writeRadius=" + writeRadius + + " plannedRadius=" + plannedRadius + + " resetPadding=" + resetPadding); + int resetChunks = resetRegenMantleChunks(platform, targets, resetPadding, runId); + Iris.info("Regen mantle reset complete: id=" + runId + + " resetChunks=" + resetChunks + + " resetPadding=" + resetPadding); + } + + setRegenSetupPhase(setupPhase, setupPhaseSince, "dispatch", world, runId); + updateRegenSetupDisplay(display, mode, "Dispatching chunk workers", 6, 6); + RegenSummary summary = null; + for (int pass = 1; pass <= mode.passCount(); pass++) { + String passId = mode.passCount() > 1 ? runId + "-p" + pass : runId; + summary = executeRegenQueue(sender, world, platform, targets, executor, pool, regenThreads, mode, passId, pass, mode.passCount(), runStart, display); + if (summary.failedChunks() > 0) { + break; + } + } + + 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.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); + } + DecreeContext.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<>(); + + int totalChunks = targets.size(); + int successChunks = 0; + int failedCount = 0; + int retryCount = 0; + long submittedTasks = 0L; + long finishedTasks = 0L; + int completedChunks = 0; + int inFlight = 0; + int unchangedHeartbeats = 0; + int lastCompleted = -1; + 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)); + inFlight++; + submittedTasks++; + } + + while (completedChunks < totalChunks) { + Future future = completion.poll(REGEN_HEARTBEAT_MS, TimeUnit.MILLISECONDS); + if (future == null) { + if (completedChunks == lastCompleted) { + unchangedHeartbeats++; + } else { + unchangedHeartbeats = 0; + lastCompleted = completedChunks; + } + + 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() + + " activeTasks=" + formatActiveTasks(activeTasks)); + lastProgressUiMs = updateRegenProgressAction( + sender, + display, + mode, + passIndex, + passCount, + completedChunks, + totalChunks, + inFlight, + pending.size(), + unchangedHeartbeats > 0, + false, + false, + true, + unchangedHeartbeats > 0 ? "Waiting for active chunk to finish" : "Waiting for chunk result", + lastProgressUiMs + ); + + if (unchangedHeartbeats >= REGEN_STALL_DUMP_HEARTBEATS && System.currentTimeMillis() - lastDump >= 10000L) { + lastDump = System.currentTimeMillis(); + Iris.warn("Regen appears stalled; dumping worker stack traces for id=" + runId + "."); + dumpRegenWorkerStacks(regenThreads, world.getName()); + } + if (unchangedHeartbeats >= REGEN_STALL_ABORT_HEARTBEATS) { + updateRegenProgressAction( + sender, + display, + mode, + passIndex, + passCount, + completedChunks, + totalChunks, + inFlight, + pending.size(), + true, + true, + true, + true, + "Stalled with no chunk completion", + lastProgressUiMs + ); + throw new IllegalStateException("Regen stalled with no progress for " + + (REGEN_STALL_ABORT_HEARTBEATS * REGEN_HEARTBEAT_MS) + + "ms (id=" + runId + + ", mode=" + mode.id() + + ", completed=" + completedChunks + + "/" + totalChunks + + ", inFlight=" + inFlight + + ", queued=" + pending.size() + + ")."); + } + 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(); + + if (result.success()) { + completedChunks++; + successChunks++; + 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() + + " error=" + result.errorSummary()); + } else { + completedChunks++; + failedCount++; + Position2 failed = new Position2(result.task().chunkX(), result.task().chunkZ()); + failedChunks.add(failed); + Iris.warn("Regen chunk failed terminally: id=" + runId + + " chunk=" + result.task().chunkX() + "," + result.task().chunkZ() + + " attempts=" + result.task().attempt() + + " 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)); + inFlight++; + submittedTasks++; + } + + lastProgressUiMs = updateRegenProgressAction( + sender, + display, + mode, + passIndex, + passCount, + completedChunks, + totalChunks, + inFlight, + pending.size(), + unchangedHeartbeats > 0, + false, + false, + false, + "Generating chunks", + lastProgressUiMs + ); + } + + MantleOverlaySummary overlaySummary = MantleOverlaySummary.empty(); + if (failedCount <= 0 && mode.applyMantleOverlay()) { + overlaySummary = applyRegenMantleOverlay(world, platform, targets, runId, mode.logChunkDiagnostics()); + } + + long runtimeMs = System.currentTimeMillis() - runStart; + String preview = formatFailedChunkPreview(failedChunks); + Iris.info("Regen run complete: id=" + runId + + " world=" + world.getName() + + " total=" + totalChunks + + " success=" + successChunks + + " failed=" + failedCount + + " retries=" + retryCount + + " submittedTasks=" + submittedTasks + + " finishedTasks=" + finishedTasks + + " overlayChunks=" + overlaySummary.chunksProcessed() + + " overlayObjectChunks=" + overlaySummary.chunksWithObjectKeys() + + " overlayBlocks=" + overlaySummary.blocksApplied() + + " 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" : "Pass complete", + lastProgressUiMs + ); + return new RegenSummary(totalChunks, successChunks, failedCount, retryCount, preview); + } + + 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); + String 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()) { + J.runEntity(sender.player(), () -> sender.sendAction(action)); + } + 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 + ) { + String worker = Thread.currentThread().getName(); + long startedAt = System.currentTimeMillis(); + boolean loadedAtStart = false; + try { + loadedAtStart = world.isChunkLoaded(task.chunkX(), task.chunkZ()); + } catch (Throwable ignored) { + } + + activeTasks.put(worker, new RegenActiveTask(task.chunkX(), task.chunkZ(), task.attempt(), startedAt, loadedAtStart)); + try { + if (mode.logChunkDiagnostics()) { + Iris.info("Regen chunk start: id=" + runId + + " chunk=" + task.chunkX() + "," + task.chunkZ() + + " attempt=" + task.attempt() + + " loadedAtStart=" + loadedAtStart + + " worker=" + worker); + } + RegenRuntime.setRunId(runId); + try { + platform.injectChunkReplacement(world, task.chunkX(), task.chunkZ(), executor); + } 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)); + } + return RegenChunkResult.success(task, worker, startedAt, System.currentTimeMillis(), loadedAtStart); + } catch (Throwable e) { + if (e instanceof InterruptedException) { + Thread.currentThread().interrupt(); + } + return RegenChunkResult.failure(task, worker, startedAt, System.currentTimeMillis(), loadedAtStart, 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()); + Iris.info("Regen setup phase: id=" + runId + " phase=" + nextPhase + " world=" + world.getName()); + } + + private RegenMantleChunkState inspectRegenMantleChunk(PlatformChunkGenerator platform, int chunkX, int chunkZ) { + MantleChunk chunk = platform.getEngine().getMantle().getMantle().getChunk(chunkX, chunkZ).use(); + try { + AtomicInteger blockDataEntries = new AtomicInteger(); + AtomicInteger stringEntries = new AtomicInteger(); + AtomicInteger objectKeyEntries = new AtomicInteger(); + AtomicInteger tileEntries = new AtomicInteger(); + + chunk.iterate(BlockData.class, (x, y, z, data) -> { + if (data != null) { + blockDataEntries.incrementAndGet(); + } + }); + chunk.iterate(String.class, (x, y, z, key) -> { + if (key == null || key.isEmpty()) { + return; + } + stringEntries.incrementAndGet(); + if (key.indexOf('@') > 0) { + objectKeyEntries.incrementAndGet(); + } + }); + chunk.iterate(TileWrapper.class, (x, y, z, tile) -> { + if (tile != null) { + tileEntries.incrementAndGet(); + } + }); + + return new RegenMantleChunkState( + chunk.isFlagged(MantleFlag.PLANNED), + chunk.isFlagged(MantleFlag.OBJECT), + chunk.isFlagged(MantleFlag.JIGSAW), + chunk.isFlagged(MantleFlag.REAL), + blockDataEntries.get(), + stringEntries.get(), + objectKeyEntries.get(), + tileEntries.get() + ); + } finally { + chunk.release(); + } + } + + private MantleOverlaySummary applyRegenMantleOverlay( + World world, + PlatformChunkGenerator platform, + List targets, + String runId, + boolean diagnostics + ) throws InterruptedException { + int processed = 0; + int chunksWithObjectKeys = 0; + int totalAppliedBlocks = 0; + + for (Position2 target : targets) { + int chunkX = target.getX(); + int chunkZ = target.getZ(); + CountDownLatch latch = new CountDownLatch(1); + AtomicInteger chunkApplied = new AtomicInteger(); + AtomicInteger chunkObjectKeys = new AtomicInteger(); + AtomicReference failure = new AtomicReference<>(); + + boolean scheduled = J.runRegion(world, chunkX, chunkZ, () -> { + try { + Chunk chunk = world.getChunkAt(chunkX, chunkZ); + MantleChunk mantleChunk = platform.getEngine().getMantle().getMantle().getChunk(chunkX, chunkZ).use(); + try { + mantleChunk.iterate(String.class, (x, y, z, value) -> { + if (value != null && !value.isEmpty() && value.indexOf('@') > 0) { + chunkObjectKeys.incrementAndGet(); + } + }); + + int minWorldY = world.getMinHeight(); + int maxWorldY = world.getMaxHeight(); + mantleChunk.iterate(BlockData.class, (x, y, z, blockData) -> { + if (blockData == null) { + return; + } + int worldY = y + minWorldY; + if (worldY < minWorldY || worldY >= maxWorldY) { + return; + } + chunk.getBlock(x & 15, worldY, z & 15).setBlockData(blockData, false); + chunkApplied.incrementAndGet(); + }); + } finally { + mantleChunk.release(); + } + } catch (Throwable e) { + failure.set(e); + } finally { + latch.countDown(); + } + }); + + if (!scheduled) { + throw new IllegalStateException("Failed to schedule regen mantle overlay for chunk " + chunkX + "," + chunkZ + " id=" + runId); + } + + while (!latch.await(REGEN_HEARTBEAT_MS, TimeUnit.MILLISECONDS)) { + Iris.warn("Regen overlay heartbeat: id=" + runId + + " chunk=" + chunkX + "," + chunkZ + + " appliedBlocks=" + chunkApplied.get()); + } + + Throwable error = failure.get(); + if (error != null) { + throw new IllegalStateException("Failed to apply regen mantle overlay at chunk " + chunkX + "," + chunkZ + " id=" + runId, error); + } + + processed++; + totalAppliedBlocks += chunkApplied.get(); + if (chunkObjectKeys.get() > 0) { + chunksWithObjectKeys++; + } + + if (diagnostics) { + Iris.info("Regen overlay chunk: id=" + runId + + " chunk=" + chunkX + "," + chunkZ + + " objectKeys=" + chunkObjectKeys.get() + + " appliedBlocks=" + chunkApplied.get()); + } + } + return new MantleOverlaySummary(processed, chunksWithObjectKeys, totalAppliedBlocks); + } + + 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 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(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 int resetRegenMantleChunks( + PlatformChunkGenerator platform, + List targets, + int padding, + String runId + ) { + if (targets.isEmpty()) { + return 0; + } + + int minX = Integer.MAX_VALUE; + int maxX = Integer.MIN_VALUE; + int minZ = Integer.MAX_VALUE; + int maxZ = Integer.MIN_VALUE; + for (Position2 target : targets) { + minX = Math.min(minX, target.getX()); + maxX = Math.max(maxX, target.getX()); + minZ = Math.min(minZ, target.getZ()); + maxZ = Math.max(maxZ, target.getZ()); + } + + int fromX = minX - padding; + int toX = maxX + padding; + int fromZ = minZ - padding; + int toZ = maxZ + padding; + long total = (long) (toX - fromX + 1) * (long) (toZ - fromZ + 1); + long started = System.currentTimeMillis(); + int resetCount = 0; + art.arcane.iris.util.mantle.Mantle mantle = platform.getEngine().getMantle().getMantle(); + AtomicReference deleteThread = new AtomicReference<>(); + ThreadFactory deleteFactory = runnable -> { + Thread thread = new Thread(runnable, "Iris-Regen-Reset-" + runId); + thread.setDaemon(true); + deleteThread.set(thread); + return thread; + }; + ExecutorService deleteExecutor = Executors.newSingleThreadExecutor(deleteFactory); + + Iris.info("Regen mantle reset begin: id=" + runId + + " targets=" + targets.size() + + " padding=" + padding + + " bounds=" + fromX + "," + fromZ + "->" + toX + "," + toZ + + " totalChunks=" + total); + + try { + for (int x = fromX; x <= toX; x++) { + for (int z = fromZ; z <= toZ; z++) { + final int chunkX = x; + final int chunkZ = z; + Future deleteFuture = deleteExecutor.submit(() -> mantle.deleteChunk(chunkX, chunkZ)); + long waitStart = System.currentTimeMillis(); + while (true) { + try { + deleteFuture.get(REGEN_HEARTBEAT_MS, TimeUnit.MILLISECONDS); + break; + } catch (TimeoutException timeout) { + long waited = System.currentTimeMillis() - waitStart; + Iris.warn("Regen mantle reset waiting: id=" + runId + + " chunk=" + chunkX + "," + chunkZ + + " waitedMs=" + waited + + " reset=" + resetCount + "/" + total); + Thread worker = deleteThread.get(); + if (worker != null && worker.isAlive()) { + Iris.warn("Regen mantle reset worker thread=" + worker.getName() + " state=" + worker.getState()); + StackTraceElement[] trace = worker.getStackTrace(); + int limit = Math.min(trace.length, REGEN_STACK_LIMIT); + for (int i = 0; i < limit; i++) { + Iris.warn(" at " + trace[i]); + } + } + if (waited >= REGEN_RESET_DELETE_ABORT_MS) { + deleteFuture.cancel(true); + throw new IllegalStateException("Timed out deleting mantle chunk " + chunkX + "," + chunkZ + + " during regen reset id=" + runId + + " waitedMs=" + waited); + } + } catch (ExecutionException e) { + Throwable cause = e.getCause() == null ? e : e.getCause(); + throw new IllegalStateException("Failed deleting mantle chunk " + chunkX + "," + chunkZ + " during regen reset id=" + runId, cause); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw new IllegalStateException("Interrupted deleting mantle chunk " + chunkX + "," + chunkZ + " during regen reset id=" + runId, e); + } + } + + resetCount++; + if (resetCount % REGEN_RESET_PROGRESS_STEP == 0 || resetCount == total) { + long elapsed = System.currentTimeMillis() - started; + Iris.info("Regen mantle reset progress: id=" + runId + + " reset=" + resetCount + "/" + total + + " elapsedMs=" + elapsed + + " chunk=" + chunkX + "," + chunkZ); + } + } + } + } finally { + deleteExecutor.shutdownNow(); + } + + Iris.info("Regen mantle reset done: id=" + runId + + " targets=" + targets.size() + + " padding=" + padding + + " resetChunks=" + resetCount + + " elapsedMs=" + (System.currentTimeMillis() - started)); + return resetCount; + } + + private long estimateRegenMantleResetChunks(List targets, int padding) { + if (targets.isEmpty()) { + return 0L; + } + + int minX = Integer.MAX_VALUE; + int maxX = Integer.MIN_VALUE; + int minZ = Integer.MAX_VALUE; + int maxZ = Integer.MIN_VALUE; + for (Position2 target : targets) { + minX = Math.min(minX, target.getX()); + maxX = Math.max(maxX, target.getX()); + minZ = Math.min(minZ, target.getZ()); + maxZ = Math.max(maxZ, target.getZ()); + } + + long width = (long) (maxX - minX + 1) + (padding * 2L); + long depth = (long) (maxZ - minZ + 1) + (padding * 2L); + return Math.max(0L, width) * Math.max(0L, depth); + } + + private int capRegenMantleResetPadding(List targets, int desiredPadding, long maxChunks) { + int low = 0; + int high = Math.max(0, desiredPadding); + int best = 0; + while (low <= high) { + int mid = low + ((high - low) >>> 1); + long estimate = estimateRegenMantleResetChunks(targets, mid); + if (estimate <= maxChunks) { + best = mid; + low = mid + 1; + } else { + high = mid - 1; + } + } + return best; + } + + 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 record MantleOverlaySummary(int chunksProcessed, int chunksWithObjectKeys, int blocksApplied) { + private static MantleOverlaySummary empty() { + return new MantleOverlaySummary(0, 0, 0); + } + } + + private record RegenMantleChunkState( + boolean planned, + boolean objectFlag, + boolean jigsawFlag, + boolean realFlag, + int blockDataEntries, + int stringEntries, + int objectKeyEntries, + int tileEntries + ) { + private String describe() { + return "flags[planned=" + planned + + ",object=" + objectFlag + + ",jigsaw=" + jigsawFlag + + ",real=" + realFlag + + "] slices[blockData=" + blockDataEntries + + ",strings=" + stringEntries + + ",objectKeys=" + objectKeyEntries + + ",tiles=" + tileEntries + + "]"; + } + } + + private enum RegenMode { + TERRAIN("terrain", true, true, false, 1, false, false), + FULL("full", true, false, false, 2, true, true); + + private final String id; + private final boolean usesMaintenance; + private final boolean bypassMantleStages; + private final boolean resetMantleChunks; + private final int passCount; + private final boolean applyMantleOverlay; + private final boolean logChunkDiagnostics; + + RegenMode( + String id, + boolean usesMaintenance, + boolean bypassMantleStages, + boolean resetMantleChunks, + int passCount, + boolean applyMantleOverlay, + boolean logChunkDiagnostics + ) { + this.id = id; + this.usesMaintenance = usesMaintenance; + this.bypassMantleStages = bypassMantleStages; + this.resetMantleChunks = resetMantleChunks; + this.passCount = passCount; + this.applyMantleOverlay = applyMantleOverlay; + this.logChunkDiagnostics = logChunkDiagnostics; + } + + private String id() { + return id; + } + + private boolean usesMaintenance() { + return usesMaintenance; + } + + private boolean bypassMantleStages() { + return bypassMantleStages; + } + + private boolean resetMantleChunks() { + return resetMantleChunks; + } + + private int passCount() { + return passCount; + } + + private boolean applyMantleOverlay() { + return applyMantleOverlay; + } + + 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 record RegenActiveTask(int chunkX, int chunkZ, int attempt, long startedAtMs, boolean loadedAtStart) { + } + + private record RegenChunkResult( + RegenChunkTask task, + String worker, + long startedAtMs, + long finishedAtMs, + boolean loadedAtStart, + boolean success, + Throwable error + ) { + private static RegenChunkResult success(RegenChunkTask task, String worker, long startedAtMs, long finishedAtMs, boolean loadedAtStart) { + return new RegenChunkResult(task, worker, startedAtMs, finishedAtMs, loadedAtStart, true, null); + } + + private static RegenChunkResult failure(RegenChunkTask task, String worker, long startedAtMs, long finishedAtMs, boolean loadedAtStart, Throwable error) { + return new RegenChunkResult(task, worker, startedAtMs, finishedAtMs, loadedAtStart, 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) { + } + + @Director(description = "Unload an Iris World", origin = DirectorOrigin.PLAYER, sync = true) public void unloadWorld( @Param(description = "The world to unload") World world @@ -487,7 +2004,7 @@ public class CommandIris implements DecreeExecutor { } } - @Decree(description = "Load an Iris World", origin = DecreeOrigin.PLAYER, sync = true, aliases = {"import"}) + @Director(description = "Load an Iris World", origin = DirectorOrigin.PLAYER, sync = true, aliases = {"import"}) public void loadWorld( @Param(description = "The name of the world to load") String world @@ -543,7 +2060,7 @@ public class CommandIris implements DecreeExecutor { Iris.instance.checkForBukkitWorlds(world::equals); sender().sendMessage(C.GREEN + world + " loaded successfully."); } - @Decree(description = "Evacuate an iris world", origin = DecreeOrigin.PLAYER, sync = true) + @Director(description = "Evacuate an iris world", origin = DirectorOrigin.PLAYER, sync = true) public void evacuate( @Param(description = "Evacuate the world") World world @@ -562,7 +2079,7 @@ public class CommandIris implements DecreeExecutor { return worldDirectory.exists() && worldDirectory.isDirectory(); } - public static class PackDimensionTypeHandler implements DecreeParameterHandler { + public static class PackDimensionTypeHandler implements DirectorParameterHandler { @Override public KList getPossibilities() { Set options = new LinkedHashSet<>(); @@ -602,9 +2119,9 @@ public class CommandIris implements DecreeExecutor { } @Override - public String parse(String in, boolean force) throws DecreeParsingException { + public String parse(String in, boolean force) throws DirectorParsingException { if (in == null || in.trim().isEmpty()) { - throw new DecreeParsingException("World type cannot be empty"); + throw new DirectorParsingException("World type cannot be empty"); } return in.trim(); diff --git a/core/src/main/java/art/arcane/iris/core/commands/CommandJigsaw.java b/core/src/main/java/art/arcane/iris/core/commands/CommandJigsaw.java index ecf0b18fb..e9a972887 100644 --- a/core/src/main/java/art/arcane/iris/core/commands/CommandJigsaw.java +++ b/core/src/main/java/art/arcane/iris/core/commands/CommandJigsaw.java @@ -28,9 +28,9 @@ import art.arcane.iris.engine.object.IrisJigsawStructure; import art.arcane.iris.engine.object.IrisObject; import art.arcane.iris.engine.object.IrisPosition; import art.arcane.iris.util.decree.DecreeExecutor; -import art.arcane.volmlib.util.decree.DecreeOrigin; -import art.arcane.volmlib.util.decree.annotations.Decree; -import art.arcane.volmlib.util.decree.annotations.Param; +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.decree.specialhandlers.ObjectHandler; import art.arcane.iris.util.format.C; import art.arcane.volmlib.util.format.Form; @@ -40,9 +40,9 @@ import art.arcane.volmlib.util.scheduling.PrecisionStopwatch; import java.io.File; -@Decree(name = "jigsaw", origin = DecreeOrigin.PLAYER, studio = true, description = "Iris jigsaw commands") +@Director(name = "jigsaw", origin = DirectorOrigin.PLAYER, studio = true, description = "Iris jigsaw commands") public class CommandJigsaw implements DecreeExecutor { - @Decree(description = "Edit a jigsaw piece") + @Director(description = "Edit a jigsaw piece") public void edit( @Param(description = "The jigsaw piece to edit") IrisJigsawPiece piece @@ -51,7 +51,7 @@ public class CommandJigsaw implements DecreeExecutor { new JigsawEditor(player(), piece, IrisData.loadAnyObject(piece.getObject(), data()), dest); } - @Decree(description = "Place a jigsaw structure") + @Director(description = "Place a jigsaw structure") public void place( @Param(description = "The jigsaw structure to place") IrisJigsawStructure structure @@ -69,7 +69,7 @@ public class CommandJigsaw implements DecreeExecutor { } } - @Decree(description = "Create a jigsaw piece") + @Director(description = "Create a jigsaw piece") public void create( @Param(description = "The name of the jigsaw piece") String piece, @@ -93,7 +93,7 @@ public class CommandJigsaw implements DecreeExecutor { sender().sendMessage(C.GREEN + "Remember to use /iris jigsaw save"); } - @Decree(description = "Exit the current jigsaw editor") + @Director(description = "Exit the current jigsaw editor") public void exit() { JigsawEditor editor = JigsawEditor.editors.get(player()); @@ -106,7 +106,7 @@ public class CommandJigsaw implements DecreeExecutor { sender().sendMessage(C.GREEN + "Exited Jigsaw Editor"); } - @Decree(description = "Save & Exit the current jigsaw editor") + @Director(description = "Save & Exit the current jigsaw editor") public void save() { JigsawEditor editor = JigsawEditor.editors.get(player()); 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 index 68e111a4e..e799fb6fa 100644 --- a/core/src/main/java/art/arcane/iris/core/commands/CommandLazyPregen.java +++ b/core/src/main/java/art/arcane/iris/core/commands/CommandLazyPregen.java @@ -25,8 +25,8 @@ 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.util.decree.DecreeExecutor; -import art.arcane.volmlib.util.decree.annotations.Decree; -import art.arcane.volmlib.util.decree.annotations.Param; +import art.arcane.volmlib.util.director.annotations.Director; +import art.arcane.volmlib.util.director.annotations.Param; import art.arcane.iris.util.format.C; import art.arcane.iris.util.math.Position2; import org.bukkit.Bukkit; @@ -36,10 +36,10 @@ import org.bukkit.util.Vector; import java.io.File; import java.io.IOException; -@Decree(name = "lazypregen", aliases = "lazy", description = "Pregenerate your Iris worlds!") +@Director(name = "lazypregen", aliases = "lazy", description = "Pregenerate your Iris worlds!") public class CommandLazyPregen implements DecreeExecutor { public String worldName; - @Decree(description = "Pregenerate a world") + @Director(description = "Pregenerate a world") public void start( @Param(description = "The radius of the pregen in blocks", aliases = "size") int radius, @@ -92,7 +92,7 @@ public class CommandLazyPregen implements DecreeExecutor { } } - @Decree(description = "Stop the active pregeneration task", aliases = "x") + @Director(description = "Stop the active pregeneration task", aliases = "x") public void stop( @Param(aliases = "world", description = "The world to pause") World world @@ -105,7 +105,7 @@ public class CommandLazyPregen implements DecreeExecutor { } } - @Decree(description = "Pause / continue the active pregeneration task", aliases = {"t", "resume", "unpause"}) + @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 diff --git a/core/src/main/java/art/arcane/iris/core/commands/CommandObject.java b/core/src/main/java/art/arcane/iris/core/commands/CommandObject.java index e9a76e74b..10e1d3395 100644 --- a/core/src/main/java/art/arcane/iris/core/commands/CommandObject.java +++ b/core/src/main/java/art/arcane/iris/core/commands/CommandObject.java @@ -31,9 +31,9 @@ import art.arcane.volmlib.util.data.Cuboid; import art.arcane.iris.util.data.IrisCustomData; import art.arcane.iris.util.data.registry.Materials; import art.arcane.iris.util.decree.DecreeExecutor; -import art.arcane.volmlib.util.decree.DecreeOrigin; -import art.arcane.volmlib.util.decree.annotations.Decree; -import art.arcane.volmlib.util.decree.annotations.Param; +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.decree.specialhandlers.ObjectHandler; import art.arcane.iris.util.format.C; import art.arcane.iris.util.math.Direction; @@ -49,7 +49,7 @@ import java.io.IOException; import java.text.NumberFormat; import java.util.*; -@Decree(name = "object", aliases = "o", origin = DecreeOrigin.PLAYER, studio = true, description = "Iris object manipulation") +@Director(name = "object", aliases = "o", origin = DirectorOrigin.PLAYER, studio = true, description = "Iris object manipulation") public class CommandObject implements DecreeExecutor { private static final Set skipBlocks = Set.of(Materials.GRASS, Material.SNOW, Material.VINE, Material.TORCH, Material.DEAD_BUSH, @@ -140,7 +140,7 @@ public class CommandObject implements DecreeExecutor { }; } - @Decree(description = "Check the composition of an object") + @Director(description = "Check the composition of an object") public void analyze( @Param(description = "The object to analyze", customHandler = ObjectHandler.class) String object @@ -208,7 +208,7 @@ public class CommandObject implements DecreeExecutor { } } - @Decree(description = "Shrink an object to its minimum size") + @Director(description = "Shrink an object to its minimum size") public void shrink(@Param(description = "The object to shrink", customHandler = ObjectHandler.class) String object) { IrisObject o = IrisData.loadAnyObject(object, data()); sender().sendMessage("Current Object Size: " + o.getW() + " * " + o.getH() + " * " + o.getD()); @@ -222,7 +222,7 @@ public class CommandObject implements DecreeExecutor { } } - @Decree(description = "Convert .schem files in the 'convert' folder to .iob files.") + @Director(description = "Convert .schem files in the 'convert' folder to .iob files.") public void convert () { try { IrisConverter.convertSchematics(sender()); @@ -232,13 +232,13 @@ public class CommandObject implements DecreeExecutor { } - @Decree(description = "Get a powder that reveals objects", studio = true, aliases = "d") + @Director(description = "Get a powder that reveals objects", studio = true, aliases = "d") public void dust() { player().getInventory().addItem(WandSVC.createDust()); sender().playSound(Sound.AMBIENT_SOUL_SAND_VALLEY_ADDITIONS, 1f, 1.5f); } - @Decree(description = "Contract a selection based on your looking direction", aliases = "-") + @Director(description = "Contract a selection based on your looking direction", aliases = "-") public void contract( @Param(description = "The amount to inset by", defaultValue = "1") int amount @@ -267,7 +267,7 @@ public class CommandObject implements DecreeExecutor { sender().playSound(Sound.ENTITY_ITEM_FRAME_ROTATE_ITEM, 1f, 0.55f); } - @Decree(description = "Set point 1 to look", aliases = "p1") + @Director(description = "Set point 1 to look", aliases = "p1") public void position1( @Param(description = "Whether to use your current position, or where you look", defaultValue = "true") boolean here @@ -293,7 +293,7 @@ public class CommandObject implements DecreeExecutor { } } - @Decree(description = "Set point 2 to look", aliases = "p2") + @Director(description = "Set point 2 to look", aliases = "p2") public void position2( @Param(description = "Whether to use your current position, or where you look", defaultValue = "true") boolean here @@ -320,7 +320,7 @@ public class CommandObject implements DecreeExecutor { } } - @Decree(description = "Paste an object", sync = true) + @Director(description = "Paste an object", sync = true) public void paste( @Param(description = "The object to paste", customHandler = ObjectHandler.class) String object, @@ -381,7 +381,7 @@ public class CommandObject implements DecreeExecutor { } } - @Decree(description = "Save an object") + @Director(description = "Save an object") public void save( @Param(description = "The dimension to store the object in", contextual = true) IrisDimension dimension, @@ -416,7 +416,7 @@ public class CommandObject implements DecreeExecutor { sender().sendMessage(C.GREEN + "Successfully object to saved: " + dimension.getLoadKey() + "/objects/" + name); } - @Decree(description = "Shift a selection in your looking direction", aliases = "-") + @Director(description = "Shift a selection in your looking direction", aliases = "-") public void shift( @Param(description = "The amount to shift by", defaultValue = "1") int amount @@ -447,7 +447,7 @@ public class CommandObject implements DecreeExecutor { sender().playSound(Sound.ENTITY_ITEM_FRAME_ROTATE_ITEM, 1f, 0.55f); } - @Decree(description = "Undo a number of pastes", aliases = "-") + @Director(description = "Undo a number of pastes", aliases = "-") public void undo( @Param(description = "The amount of pastes to undo", defaultValue = "1") int amount @@ -458,7 +458,7 @@ public class CommandObject implements DecreeExecutor { sender().sendMessage(C.BLUE + "Reverted " + actualReverts + C.BLUE +" pastes!"); } - @Decree(description = "Gets an object wand and grabs the current WorldEdit selection.", aliases = "we", origin = DecreeOrigin.PLAYER, studio = true) + @Director(description = "Gets an object wand and grabs the current WorldEdit selection.", aliases = "we", origin = DirectorOrigin.PLAYER, studio = true) public void we() { if (!Bukkit.getPluginManager().isPluginEnabled("WorldEdit")) { sender().sendMessage(C.RED + "You can't get a WorldEdit selection without WorldEdit, you know."); @@ -476,14 +476,14 @@ public class CommandObject implements DecreeExecutor { sender().sendMessage(C.GREEN + "A fresh wand with your current WorldEdit selection on it!"); } - @Decree(description = "Get an object wand", sync = true) + @Director(description = "Get an object wand", sync = true) public void wand() { player().getInventory().addItem(WandSVC.createWand()); sender().playSound(Sound.ITEM_ARMOR_EQUIP_NETHERITE, 1f, 1.5f); sender().sendMessage(C.GREEN + "Poof! Good luck building!"); } - @Decree(name = "x&y", description = "Autoselect up, down & out", sync = true) + @Director(name = "x&y", description = "Autoselect up, down & out", sync = true) public void xay() { if (!WandSVC.isHoldingWand(player())) { sender().sendMessage(C.YELLOW + "Hold your wand!"); @@ -534,7 +534,7 @@ public class CommandObject implements DecreeExecutor { sender().sendMessage(C.GREEN + "Auto-select complete!"); } - @Decree(name = "x+y", description = "Autoselect up & out", sync = true) + @Director(name = "x+y", description = "Autoselect up & out", sync = true) public void xpy() { if (!WandSVC.isHoldingWand(player())) { sender().sendMessage(C.YELLOW + "Hold your wand!"); diff --git a/core/src/main/java/art/arcane/iris/core/commands/CommandPregen.java b/core/src/main/java/art/arcane/iris/core/commands/CommandPregen.java index 0e7ba00ab..57a19f4b4 100644 --- a/core/src/main/java/art/arcane/iris/core/commands/CommandPregen.java +++ b/core/src/main/java/art/arcane/iris/core/commands/CommandPregen.java @@ -23,16 +23,16 @@ import art.arcane.iris.core.gui.PregeneratorJob; import art.arcane.iris.core.pregenerator.PregenTask; import art.arcane.iris.core.tools.IrisToolbelt; import art.arcane.iris.util.decree.DecreeExecutor; -import art.arcane.volmlib.util.decree.annotations.Decree; -import art.arcane.volmlib.util.decree.annotations.Param; +import art.arcane.volmlib.util.director.annotations.Director; +import art.arcane.volmlib.util.director.annotations.Param; import art.arcane.iris.util.format.C; import art.arcane.iris.util.math.Position2; import org.bukkit.World; import org.bukkit.util.Vector; -@Decree(name = "pregen", aliases = "pregenerate", description = "Pregenerate your Iris worlds!") +@Director(name = "pregen", aliases = "pregenerate", description = "Pregenerate your Iris worlds!") public class CommandPregen implements DecreeExecutor { - @Decree(description = "Pregenerate a world") + @Director(description = "Pregenerate a world") public void start( @Param(description = "The radius of the pregen in blocks", aliases = "size") int radius, @@ -66,7 +66,7 @@ public class CommandPregen implements DecreeExecutor { } } - @Decree(description = "Stop the active pregeneration task", aliases = "x") + @Director(description = "Stop the active pregeneration task", aliases = "x") public void stop() { if (PregeneratorJob.shutdownInstance()) { Iris.info( C.BLUE + "Finishing up mca region..."); @@ -75,7 +75,7 @@ public class CommandPregen implements DecreeExecutor { } } - @Decree(description = "Pause / continue the active pregeneration task", aliases = {"t", "resume", "unpause"}) + @Director(description = "Pause / continue the active pregeneration task", aliases = {"t", "resume", "unpause"}) public void pause() { if (PregeneratorJob.pauseResume()) { sender().sendMessage(C.GREEN + "Paused/unpaused pregeneration task, now: " + (PregeneratorJob.isPaused() ? "Paused" : "Running") + "."); 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 0c719b69a..aa4cb95e9 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 @@ -37,9 +37,9 @@ import art.arcane.iris.util.decree.DecreeContext; import art.arcane.iris.util.decree.DecreeExecutor; import art.arcane.iris.util.decree.handlers.DimensionHandler; import art.arcane.iris.util.decree.specialhandlers.NullableDimensionHandler; -import art.arcane.volmlib.util.decree.DecreeOrigin; -import art.arcane.volmlib.util.decree.annotations.Decree; -import art.arcane.volmlib.util.decree.annotations.Param; +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.format.C; import art.arcane.volmlib.util.format.Form; import art.arcane.volmlib.util.function.Function2; @@ -79,12 +79,11 @@ import java.time.temporal.ChronoUnit; import java.util.Arrays; import java.util.Date; import java.util.Objects; -import java.util.concurrent.CountDownLatch; import java.util.concurrent.Executors; import java.util.concurrent.atomic.AtomicInteger; import java.util.function.Supplier; -@Decree(name = "studio", aliases = {"std", "s"}, description = "Studio Commands", studio = true) +@Director(name = "studio", aliases = {"std", "s"}, description = "Studio Commands", studio = true) public class CommandStudio implements DecreeExecutor { private CommandFind find; private CommandEdit edit; @@ -95,7 +94,7 @@ public class CommandStudio implements DecreeExecutor { } //TODO fix pack trimming - @Decree(description = "Download a project.", aliases = "dl") + @Director(description = "Download a project.", aliases = "dl") public void download( @Param(name = "pack", description = "The pack to download", defaultValue = "overworld", aliases = "project") String pack, @@ -109,7 +108,7 @@ public class CommandStudio implements DecreeExecutor { new CommandIris().download(pack, branch, overwrite); } - @Decree(description = "Open a new studio world", aliases = "o", sync = true) + @Director(description = "Open a new studio world", aliases = "o", sync = true) public void open( @Param(defaultValue = "default", description = "The dimension to open a studio for", aliases = "dim", customHandler = DimensionHandler.class) IrisDimension dimension, @@ -126,7 +125,7 @@ public class CommandStudio implements DecreeExecutor { Iris.service(StudioSVC.class).open(sender(), seed, dimension.getLoadKey()); } - @Decree(description = "Open VSCode for a dimension", aliases = {"vsc", "edit"}) + @Director(description = "Open VSCode for a dimension", aliases = {"vsc", "edit"}) public void vscode( @Param(defaultValue = "default", description = "The dimension to open VSCode for", aliases = "dim", customHandler = DimensionHandler.class) IrisDimension dimension @@ -135,7 +134,7 @@ public class CommandStudio implements DecreeExecutor { Iris.service(StudioSVC.class).openVSCode(sender(), dimension.getLoadKey()); } - @Decree(description = "Close an open studio project", aliases = {"x", "c"}, sync = true) + @Director(description = "Close an open studio project", aliases = {"x", "c"}, sync = true) public void close() { if (!Iris.service(StudioSVC.class).isProjectOpen()) { sender().sendMessage(C.RED + "No open studio projects."); @@ -146,7 +145,7 @@ public class CommandStudio implements DecreeExecutor { sender().sendMessage(C.GREEN + "Project Closed."); } - @Decree(description = "Create a new studio project", aliases = "+", sync = true) + @Director(description = "Create a new studio project", aliases = "+", sync = true) public void create( @Param(description = "The name of this new Iris Project.") String name, @@ -163,7 +162,7 @@ public class CommandStudio implements DecreeExecutor { } } - @Decree(description = "Get the version of a pack") + @Director(description = "Get the version of a pack") public void version( @Param(defaultValue = "default", description = "The dimension get the version of", aliases = "dim", contextual = true, customHandler = DimensionHandler.class) IrisDimension dimension @@ -171,7 +170,7 @@ public class CommandStudio implements DecreeExecutor { sender().sendMessage(C.GREEN + "The \"" + dimension.getName() + "\" pack has version: " + dimension.getVersion()); } - @Decree(name = "regen", description = "Regenerate nearby chunks.", aliases = "rg", sync = true, origin = DecreeOrigin.PLAYER) + @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 @@ -183,46 +182,60 @@ public class CommandStudio implements DecreeExecutor { VolmitSender sender = sender(); var loc = player().getLocation().clone(); + final int threadCount = J.isFolia() ? 1 : Runtime.getRuntime().availableProcessors(); - J.a(() -> { + String orchestratorName = "Iris-Studio-Regen-Orchestrator-" + world.getName() + "-" + System.nanoTime(); + Thread orchestrator = new Thread(() -> { PlatformChunkGenerator plat = IrisToolbelt.access(world); Engine engine = plat.getEngine(); DecreeContext.touch(sender); + IrisToolbelt.beginWorldMaintenance(world, "studio-regen"); try (SyncExecutor executor = new SyncExecutor(20); - var service = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors()) + var service = Executors.newFixedThreadPool(threadCount) ) { int x = loc.getBlockX() >> 4; int z = loc.getBlockZ() >> 4; - int rad = engine.getMantle().getRadius(); - var mantle = engine.getMantle().getMantle(); + int rad = 0; var chunkMap = new KMap(); - ParallelRadiusJob prep = new ParallelRadiusJob(Integer.MAX_VALUE, 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; + 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); } - 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); - CountDownLatch pLatch = new CountDownLatch(1); - prep.execute(sender(), pLatch::countDown); - pLatch.await(); + @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."); + } - ParallelRadiusJob job = new ParallelRadiusJob(Integer.MAX_VALUE, service) { + 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); } @@ -231,28 +244,38 @@ public class CommandStudio implements DecreeExecutor { return "Regenerating"; } }.retarget(radius, x, z); - CountDownLatch latch = new CountDownLatch(1); - job.execute(sender(), latch::countDown); - latch.await(); + job.execute(); - chunkMap.forEach((pos, chunk) -> - mantle.getChunk(pos.getX(), pos.getZ()).copyFrom(chunk)); + 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"); DecreeContext.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); + } } - @Decree(description = "Convert objects in the \"convert\" folder") + @Director(description = "Convert objects in the \"convert\" folder") public void convert() { Iris.service(ConversionSVC.class).check(sender()); //IrisConverter.convertSchematics(sender()); } - @Decree(description = "Execute a script", aliases = "run", origin = DecreeOrigin.PLAYER) + @Director(description = "Execute a script", aliases = "run", origin = DirectorOrigin.PLAYER) public void execute( @Param(description = "The script to run") IrisScript script @@ -260,14 +283,14 @@ public class CommandStudio implements DecreeExecutor { engine().getExecution().execute(script.getLoadKey()); } - @Decree(description = "Open the noise explorer (External GUI)", aliases = {"nmap", "n"}) + @Director(description = "Open the noise explorer (External GUI)", aliases = {"nmap", "n"}) public void noise() { if (noGUI()) return; sender().sendMessage(C.GREEN + "Opening Noise Explorer!"); NoiseExplorerGUI.launch(); } - @Decree(description = "Charges all spawners in the area", aliases = "zzt", origin = DecreeOrigin.PLAYER) + @Director(description = "Charges all spawners in the area", aliases = "zzt", origin = DirectorOrigin.PLAYER) public void charge() { if (!IrisToolbelt.isIrisWorld(world())) { sender().sendMessage(C.RED + "You must be in an Iris world to charge spawners!"); @@ -277,7 +300,7 @@ public class CommandStudio implements DecreeExecutor { engine().getWorldManager().chargeEnergy(); } - @Decree(description = "Preview noise gens (External GUI)", aliases = {"generator", "gen"}) + @Director(description = "Preview noise gens (External GUI)", aliases = {"generator", "gen"}) public void explore( @Param(description = "The generator to explore", contextual = true) IrisGenerator generator, @@ -298,7 +321,7 @@ public class CommandStudio implements DecreeExecutor { NoiseExplorerGUI.launch(l, "Custom Generator"); } - @Decree(description = "Hotload a studio", aliases = {"reload", "h"}) + @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!"); @@ -308,7 +331,7 @@ public class CommandStudio implements DecreeExecutor { sender().sendMessage(C.GREEN + "Hotloaded"); } - @Decree(description = "Show loot if a chest were right here", origin = DecreeOrigin.PLAYER, sync = true) + @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") boolean fast, @@ -355,7 +378,7 @@ public class CommandStudio implements DecreeExecutor { player().openInventory(inv); } - @Decree(description = "Calculate the chance for each region to generate", origin = DecreeOrigin.PLAYER) + @Director(description = "Calculate the chance for each region to generate", origin = DirectorOrigin.PLAYER) public void regions(@Param(description = "The radius in chunks", defaultValue = "500") int radius) { var engine = engine(); if (engine == null) { @@ -392,7 +415,7 @@ public class CommandStudio implements DecreeExecutor { }); } - @Decree(description = "Get all structures in a radius of chunks", aliases = "dist", origin = DecreeOrigin.PLAYER) + @Director(description = "Get all structures in a radius of chunks", aliases = "dist", origin = DirectorOrigin.PLAYER) public void distances(@Param(description = "The radius in chunks") int radius) { var engine = engine(); if (engine == null) { @@ -458,7 +481,7 @@ public class CommandStudio implements DecreeExecutor { } - @Decree(description = "Render a world map (External GUI)", aliases = "render") + @Director(description = "Render a world map (External GUI)", aliases = "render") public void map( @Param(name = "world", description = "The world to open the generator for", contextual = true) World world @@ -474,7 +497,7 @@ public class CommandStudio implements DecreeExecutor { sender().sendMessage(C.GREEN + "Opening map!"); } - @Decree(description = "Package a dimension into a compressed format", aliases = "package") + @Director(description = "Package a dimension into a compressed format", aliases = "package") public void pkg( @Param(name = "dimension", description = "The dimension pack to compress", contextual = true, defaultValue = "default", customHandler = DimensionHandler.class) IrisDimension dimension, @@ -486,7 +509,7 @@ public class CommandStudio implements DecreeExecutor { Iris.service(StudioSVC.class).compilePackage(sender(), dimension.getLoadKey(), obfuscate, minify); } - @Decree(description = "Profiles the performance of a dimension", origin = DecreeOrigin.PLAYER) + @Director(description = "Profiles the performance of a dimension", origin = DirectorOrigin.PLAYER) public void profile( @Param(description = "The dimension to profile", contextual = true, defaultValue = "default", customHandler = DimensionHandler.class) IrisDimension dimension @@ -675,7 +698,7 @@ public class CommandStudio implements DecreeExecutor { sender().sendMessage(C.GREEN + "Done! " + report.getPath()); } - @Decree(description = "Spawn an Iris entity", aliases = "summon", origin = DecreeOrigin.PLAYER) + @Director(description = "Spawn an Iris entity", aliases = "summon", origin = DirectorOrigin.PLAYER) public void spawn( @Param(description = "The entity to spawn") IrisEntity entity, @@ -688,7 +711,7 @@ public class CommandStudio implements DecreeExecutor { entity.spawn(engine(), new Location(world(), location.getX(), location.getY(), location.getZ())); } - @Decree(description = "Teleport to the active studio world", aliases = "stp", origin = DecreeOrigin.PLAYER, sync = true) + @Director(description = "Teleport to the active studio world", aliases = "stp", origin = DirectorOrigin.PLAYER, sync = true) public void tpstudio() { if (!Iris.service(StudioSVC.class).isProjectOpen()) { sender().sendMessage(C.RED + "No studio world is open!"); @@ -711,7 +734,7 @@ public class CommandStudio implements DecreeExecutor { ).thenRun(() -> player.setGameMode(GameMode.SPECTATOR)); } - @Decree(description = "Update your dimension projects VSCode workspace") + @Director(description = "Update your dimension projects VSCode workspace") public void update( @Param(description = "The dimension to update the workspace of", contextual = true, defaultValue = "default", customHandler = DimensionHandler.class) IrisDimension dimension @@ -724,7 +747,7 @@ public class CommandStudio implements DecreeExecutor { } } - @Decree(aliases = "find-objects", description = "Get information about nearby structures") + @Director(aliases = "find-objects", description = "Get information about nearby structures") public void objects() { if (!IrisToolbelt.isIrisWorld(player().getWorld())) { sender().sendMessage(C.RED + "You must be in an Iris world"); 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 index df3eeb8b0..9dbfd1e7c 100644 --- a/core/src/main/java/art/arcane/iris/core/commands/CommandTurboPregen.java +++ b/core/src/main/java/art/arcane/iris/core/commands/CommandTurboPregen.java @@ -23,8 +23,8 @@ import art.arcane.iris.core.pregenerator.LazyPregenerator; import art.arcane.iris.core.pregenerator.TurboPregenerator; import art.arcane.iris.core.pregenerator.TurboPregenerator; import art.arcane.iris.util.decree.DecreeExecutor; -import art.arcane.volmlib.util.decree.annotations.Decree; -import art.arcane.volmlib.util.decree.annotations.Param; +import art.arcane.volmlib.util.director.annotations.Director; +import art.arcane.volmlib.util.director.annotations.Param; import art.arcane.iris.util.format.C; import org.bukkit.Bukkit; import org.bukkit.World; @@ -33,10 +33,10 @@ import org.bukkit.util.Vector; import java.io.File; import java.io.IOException; -@Decree(name = "turbopregen", aliases = "turbo", description = "Pregenerate your Iris worlds!") +@Director(name = "turbopregen", aliases = "turbo", description = "Pregenerate your Iris worlds!") public class CommandTurboPregen implements DecreeExecutor { public String worldName; - @Decree(description = "Pregenerate a world") + @Director(description = "Pregenerate a world") public void start( @Param(description = "The radius of the pregen in blocks", aliases = "size") int radius, @@ -90,7 +90,7 @@ public class CommandTurboPregen implements DecreeExecutor { } } - @Decree(description = "Stop the active pregeneration task", aliases = "x") + @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()); @@ -108,7 +108,7 @@ public class CommandTurboPregen implements DecreeExecutor { } } - @Decree(description = "Pause / continue the active pregeneration task", aliases = {"t", "resume", "unpause"}) + @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 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 index 68a67a98f..d48fa274c 100644 --- a/core/src/main/java/art/arcane/iris/core/commands/CommandUpdater.java +++ b/core/src/main/java/art/arcane/iris/core/commands/CommandUpdater.java @@ -25,18 +25,18 @@ import art.arcane.iris.Iris; import art.arcane.iris.core.pregenerator.ChunkUpdater; import art.arcane.iris.core.tools.IrisToolbelt; import art.arcane.iris.util.decree.DecreeExecutor; -import art.arcane.volmlib.util.decree.DecreeOrigin; -import art.arcane.volmlib.util.decree.annotations.Decree; -import art.arcane.volmlib.util.decree.annotations.Param; +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.format.C; import art.arcane.volmlib.util.format.Form; -@Decree(name = "updater", origin = DecreeOrigin.BOTH, description = "Iris World Updater") +@Director(name = "updater", origin = DirectorOrigin.BOTH, description = "Iris World Updater") public class CommandUpdater implements DecreeExecutor { private final Object lock = new Object(); private transient ChunkUpdater chunkUpdater; - @Decree(description = "Updates all chunk in the specified world") + @Director(description = "Updates all chunk in the specified world") public void start( @Param(description = "World to update chunks at", contextual = true) World world @@ -61,7 +61,7 @@ public class CommandUpdater implements DecreeExecutor { } @Synchronized("lock") - @Decree(description = "Pause the updater") + @Director(description = "Pause the updater") public void pause( ) { if (chunkUpdater == null) { sender().sendMessage(C.GOLD + "You cant pause something that doesnt exist?"); @@ -84,7 +84,7 @@ public class CommandUpdater implements DecreeExecutor { } @Synchronized("lock") - @Decree(description = "Stops the updater") + @Director(description = "Stops the updater") public void stop() { if (chunkUpdater == null) { sender().sendMessage(C.GOLD + "You cant stop something that doesnt exist?"); diff --git a/core/src/main/java/art/arcane/iris/core/commands/CommandWhat.java b/core/src/main/java/art/arcane/iris/core/commands/CommandWhat.java index da3e83b85..77f7fda97 100644 --- a/core/src/main/java/art/arcane/iris/core/commands/CommandWhat.java +++ b/core/src/main/java/art/arcane/iris/core/commands/CommandWhat.java @@ -27,9 +27,9 @@ import art.arcane.iris.engine.object.IrisBiome; import art.arcane.iris.engine.object.IrisRegion; import art.arcane.iris.util.data.B; import art.arcane.iris.util.decree.DecreeExecutor; -import art.arcane.volmlib.util.decree.DecreeOrigin; -import art.arcane.volmlib.util.decree.annotations.Decree; -import art.arcane.volmlib.util.decree.annotations.Param; +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.format.C; import art.arcane.volmlib.util.matter.MatterMarker; import art.arcane.iris.util.scheduling.J; @@ -42,9 +42,9 @@ import org.bukkit.block.data.BlockData; import java.util.Objects; import java.util.concurrent.atomic.AtomicInteger; -@Decree(name = "what", origin = DecreeOrigin.PLAYER, studio = true, description = "Iris What?") +@Director(name = "what", origin = DirectorOrigin.PLAYER, studio = true, description = "Iris What?") public class CommandWhat implements DecreeExecutor { - @Decree(description = "What is in my hand?", origin = DecreeOrigin.PLAYER) + @Director(description = "What is in my hand?", origin = DirectorOrigin.PLAYER) public void hand() { try { BlockData bd = player().getInventory().getItemInMainHand().getType().createBlockData(); @@ -65,7 +65,7 @@ public class CommandWhat implements DecreeExecutor { } } - @Decree(description = "What biome am i in?", origin = DecreeOrigin.PLAYER) + @Director(description = "What biome am i in?", origin = DirectorOrigin.PLAYER) public void biome() { try { IrisBiome b = engine().getBiome(player().getLocation().getBlockX(), player().getLocation().getBlockY() - player().getWorld().getMinHeight(), player().getLocation().getBlockZ()); @@ -85,7 +85,7 @@ public class CommandWhat implements DecreeExecutor { } } - @Decree(description = "What region am i in?", origin = DecreeOrigin.PLAYER) + @Director(description = "What region am i in?", origin = DirectorOrigin.PLAYER) public void region() { try { Chunk chunk = world().getChunkAt(player().getLocation().getBlockZ() / 16, player().getLocation().getBlockZ() / 16); @@ -98,7 +98,7 @@ public class CommandWhat implements DecreeExecutor { } } - @Decree(description = "What block am i looking at?", origin = DecreeOrigin.PLAYER) + @Director(description = "What block am i looking at?", origin = DirectorOrigin.PLAYER) public void block() { BlockData bd; try { @@ -143,7 +143,7 @@ public class CommandWhat implements DecreeExecutor { } } - @Decree(description = "Show markers in chunk", origin = DecreeOrigin.PLAYER) + @Director(description = "Show markers in chunk", origin = DirectorOrigin.PLAYER) public void markers(@Param(description = "Marker name such as cave_floor or cave_ceiling") String marker) { Chunk c = player().getLocation().getChunk(); diff --git a/core/src/main/java/art/arcane/iris/core/gui/PregeneratorJob.java b/core/src/main/java/art/arcane/iris/core/gui/PregeneratorJob.java index c08c1ee63..fd106e383 100644 --- a/core/src/main/java/art/arcane/iris/core/gui/PregeneratorJob.java +++ b/core/src/main/java/art/arcane/iris/core/gui/PregeneratorJob.java @@ -34,6 +34,7 @@ import art.arcane.volmlib.util.math.M; import art.arcane.iris.util.math.Position2; import art.arcane.volmlib.util.scheduling.ChronoLatch; import art.arcane.iris.util.scheduling.J; +import org.bukkit.World; import javax.swing.*; import java.awt.*; @@ -158,6 +159,15 @@ public class PregeneratorJob implements PregenListener { return inst == null ? -1L : Math.max(0L, inst.lastChunksRemaining); } + public boolean targetsWorld(World world) { + if (world == null || engine == null || engine.getWorld() == null) { + return false; + } + + String targetName = engine.getWorld().name(); + return targetName != null && targetName.equalsIgnoreCase(world.getName()); + } + private static Color parseColor(String c) { String v = (c.startsWith("#") ? c : "#" + c).trim(); try { 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 index de54a9b42..0c4ae9684 100644 --- a/core/src/main/java/art/arcane/iris/core/pregenerator/ChunkUpdater.java +++ b/core/src/main/java/art/arcane/iris/core/pregenerator/ChunkUpdater.java @@ -261,6 +261,10 @@ public class ChunkUpdater { } private void unloadAndSaveAllChunks() { + if (J.isFolia()) { + return; + } + try { J.sfut(() -> { if (world == null) { 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 2ac581650..f1387debe 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 @@ -23,7 +23,6 @@ import art.arcane.iris.core.IrisSettings; import art.arcane.iris.core.pregenerator.PregenListener; import art.arcane.iris.core.pregenerator.PregeneratorMethod; import art.arcane.iris.core.tools.IrisToolbelt; -import art.arcane.volmlib.util.collection.KMap; import art.arcane.iris.util.mantle.Mantle; import art.arcane.volmlib.util.math.M; import art.arcane.iris.util.parallel.MultiBurst; @@ -34,19 +33,30 @@ import org.bukkit.World; import java.lang.reflect.InvocationTargetException; import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Semaphore; +import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicLong; public class AsyncPregenMethod implements PregeneratorMethod { private static final AtomicInteger THREAD_COUNT = new AtomicInteger(); + private static final int FOLIA_MAX_CONCURRENCY = 32; + private static final long CHUNK_LOAD_TIMEOUT_SECONDS = 15L; private final World world; private final Executor executor; private final Semaphore semaphore; private final int threads; private final boolean urgent; private final Map lastUse; + private final AtomicInteger inFlight = new AtomicInteger(); + private final AtomicLong submitted = new AtomicLong(); + private final AtomicLong completed = new AtomicLong(); + private final AtomicLong failed = new AtomicLong(); + private final AtomicLong lastProgressAt = new AtomicLong(M.ms()); + private final AtomicLong lastPermitWaitLog = new AtomicLong(0L); public AsyncPregenMethod(World world, int unusedThreads) { if (!PaperLib.isPaper()) { @@ -54,14 +64,29 @@ public class AsyncPregenMethod implements PregeneratorMethod { } this.world = world; - this.executor = IrisSettings.get().getPregen().isUseTicketQueue() ? new TicketExecutor() : new ServiceExecutor(); - this.threads = IrisSettings.get().getPregen().getMaxConcurrency(); + if (J.isFolia()) { + this.executor = new FoliaRegionExecutor(); + } else { + boolean useTicketQueue = IrisSettings.get().getPregen().isUseTicketQueue(); + this.executor = useTicketQueue ? new TicketExecutor() : new ServiceExecutor(); + } + int configuredThreads = IrisSettings.get().getPregen().getMaxConcurrency(); + if (J.isFolia()) { + configuredThreads = Math.min(configuredThreads, FOLIA_MAX_CONCURRENCY); + } + this.threads = Math.max(1, configuredThreads); this.semaphore = new Semaphore(this.threads, true); this.urgent = IrisSettings.get().getPregen().useHighPriority; - this.lastUse = new KMap<>(); + this.lastUse = new ConcurrentHashMap<>(); } private void unloadAndSaveAllChunks() { + if (J.isFolia()) { + // Folia requires world/chunk mutations to be region-owned; periodic global unload/save is unsafe. + lastUse.clear(); + return; + } + try { J.sfut(() -> { if (world == null) { @@ -88,8 +113,71 @@ public class AsyncPregenMethod implements PregeneratorMethod { } } + private Chunk onChunkFutureFailure(int x, int z, Throwable throwable) { + Throwable root = throwable; + while (root.getCause() != null) { + root = root.getCause(); + } + + if (root instanceof java.util.concurrent.TimeoutException) { + Iris.warn("Timed out async pregen chunk load at " + x + "," + z + " after " + CHUNK_LOAD_TIMEOUT_SECONDS + "s. " + metricsSnapshot()); + } else { + Iris.warn("Failed async pregen chunk load at " + x + "," + z + ". " + metricsSnapshot()); + } + + Iris.reportError(throwable); + return null; + } + + private String metricsSnapshot() { + long stalledFor = Math.max(0L, M.ms() - lastProgressAt.get()); + return "world=" + world.getName() + + " permits=" + semaphore.availablePermits() + "/" + threads + + " inFlight=" + inFlight.get() + + " submitted=" + submitted.get() + + " completed=" + completed.get() + + " failed=" + failed.get() + + " stalledForMs=" + stalledFor; + } + + private void markSubmitted() { + submitted.incrementAndGet(); + inFlight.incrementAndGet(); + } + + private void markFinished(boolean success) { + if (success) { + completed.incrementAndGet(); + } else { + failed.incrementAndGet(); + } + + lastProgressAt.set(M.ms()); + int after = inFlight.decrementAndGet(); + if (after < 0) { + inFlight.compareAndSet(after, 0); + } + } + + private void logPermitWaitIfNeeded(int x, int z, long waitedMs) { + long now = M.ms(); + long last = lastPermitWaitLog.get(); + if (now - last < 5000L) { + return; + } + + if (lastPermitWaitLog.compareAndSet(last, now)) { + Iris.warn("Async pregen waiting for permit at chunk " + x + "," + z + " waitedMs=" + waitedMs + " " + metricsSnapshot()); + } + } + @Override public void init() { + Iris.info("Async pregen init: world=" + world.getName() + + ", mode=" + (J.isFolia() ? "folia" : "paper") + + ", threads=" + threads + + ", urgent=" + urgent + + ", timeout=" + CHUNK_LOAD_TIMEOUT_SECONDS + "s"); unloadAndSaveAllChunks(); increaseWorkerThreads(); } @@ -126,10 +214,16 @@ public class AsyncPregenMethod implements PregeneratorMethod { public void generateChunk(int x, int z, PregenListener listener) { listener.onChunkGenerating(x, z); try { - semaphore.acquire(); + long waitStart = M.ms(); + while (!semaphore.tryAcquire(5, TimeUnit.SECONDS)) { + logPermitWaitIfNeeded(x, z, Math.max(0L, M.ms() - waitStart)); + } } catch (InterruptedException e) { + Thread.currentThread().interrupt(); return; } + + markSubmitted(); executor.generate(x, z, listener); } @@ -189,6 +283,40 @@ public class AsyncPregenMethod implements PregeneratorMethod { default void shutdown() {} } + private class FoliaRegionExecutor implements Executor { + @Override + public void generate(int x, int z, PregenListener listener) { + if (!J.runRegion(world, x, z, () -> PaperLib.getChunkAtAsync(world, x, z, true, urgent) + .orTimeout(CHUNK_LOAD_TIMEOUT_SECONDS, TimeUnit.SECONDS) + .whenComplete((chunk, throwable) -> { + boolean success = false; + try { + if (throwable != null) { + onChunkFutureFailure(x, z, throwable); + return; + } + + listener.onChunkGenerated(x, z); + listener.onChunkCleaned(x, z); + if (chunk != null) { + lastUse.put(chunk, M.ms()); + } + success = true; + } catch (Throwable e) { + Iris.reportError(e); + e.printStackTrace(); + } finally { + markFinished(success); + semaphore.release(); + } + }))) { + markFinished(false); + semaphore.release(); + Iris.warn("Failed to schedule Folia region pregen task at " + x + "," + z + ". " + metricsSnapshot()); + } + } + } + private class ServiceExecutor implements Executor { private final ExecutorService service = IrisSettings.get().getPregen().isUseVirtualThreads() ? Executors.newVirtualThreadPerTaskExecutor() : @@ -196,18 +324,27 @@ public class AsyncPregenMethod implements PregeneratorMethod { public void generate(int x, int z, PregenListener listener) { service.submit(() -> { + boolean success = false; try { - PaperLib.getChunkAtAsync(world, x, z, true, urgent).thenAccept((i) -> { - listener.onChunkGenerated(x, z); - listener.onChunkCleaned(x, z); - if (i == null) return; - lastUse.put(i, M.ms()); - }).get(); + Chunk i = PaperLib.getChunkAtAsync(world, x, z, true, urgent) + .orTimeout(CHUNK_LOAD_TIMEOUT_SECONDS, TimeUnit.SECONDS) + .exceptionally(e -> onChunkFutureFailure(x, z, e)) + .get(); + + listener.onChunkGenerated(x, z); + listener.onChunkCleaned(x, z); + if (i == null) { + return; + } + lastUse.put(i, M.ms()); + success = true; } catch (InterruptedException ignored) { + Thread.currentThread().interrupt(); } catch (Throwable e) { Iris.reportError(e); e.printStackTrace(); } finally { + markFinished(success); semaphore.release(); } }); @@ -223,17 +360,21 @@ public class AsyncPregenMethod implements PregeneratorMethod { @Override public void generate(int x, int z, PregenListener listener) { PaperLib.getChunkAtAsync(world, x, z, true, urgent) - .exceptionally(e -> { - Iris.reportError(e); - e.printStackTrace(); - return null; - }) + .orTimeout(CHUNK_LOAD_TIMEOUT_SECONDS, TimeUnit.SECONDS) + .exceptionally(e -> onChunkFutureFailure(x, z, e)) .thenAccept(i -> { - semaphore.release(); - listener.onChunkGenerated(x, z); - listener.onChunkCleaned(x, z); - if (i == null) return; - lastUse.put(i, M.ms()); + boolean success = false; + try { + listener.onChunkGenerated(x, z); + listener.onChunkCleaned(x, z); + if (i != null) { + lastUse.put(i, M.ms()); + } + success = true; + } finally { + markFinished(success); + semaphore.release(); + } }); } } diff --git a/core/src/main/java/art/arcane/iris/core/pregenerator/methods/MedievalPregenMethod.java b/core/src/main/java/art/arcane/iris/core/pregenerator/methods/MedievalPregenMethod.java index ee4cc6c6b..4d904bcb8 100644 --- a/core/src/main/java/art/arcane/iris/core/pregenerator/methods/MedievalPregenMethod.java +++ b/core/src/main/java/art/arcane/iris/core/pregenerator/methods/MedievalPregenMethod.java @@ -24,7 +24,6 @@ import art.arcane.iris.core.pregenerator.PregenListener; import art.arcane.iris.core.pregenerator.PregeneratorMethod; import art.arcane.iris.core.tools.IrisToolbelt; import art.arcane.volmlib.util.collection.KList; -import art.arcane.volmlib.util.collection.KMap; import art.arcane.iris.util.mantle.Mantle; import art.arcane.volmlib.util.math.M; import art.arcane.iris.util.scheduling.J; @@ -35,6 +34,7 @@ import org.bukkit.World; import java.util.ArrayList; import java.util.Map; import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ConcurrentHashMap; public class MedievalPregenMethod implements PregeneratorMethod { private final World world; @@ -44,7 +44,7 @@ public class MedievalPregenMethod implements PregeneratorMethod { public MedievalPregenMethod(World world) { this.world = world; futures = new KList<>(); - this.lastUse = new KMap<>(); + this.lastUse = new ConcurrentHashMap<>(); } private void waitForChunks() { @@ -60,6 +60,11 @@ public class MedievalPregenMethod implements PregeneratorMethod { } private void unloadAndSaveAllChunks() { + if (J.isFolia()) { + lastUse.clear(); + return; + } + try { J.sfut(() -> { if (world == null) { diff --git a/core/src/main/java/art/arcane/iris/core/service/CommandSVC.java b/core/src/main/java/art/arcane/iris/core/service/CommandSVC.java index 84a8c7200..4a0815077 100644 --- a/core/src/main/java/art/arcane/iris/core/service/CommandSVC.java +++ b/core/src/main/java/art/arcane/iris/core/service/CommandSVC.java @@ -30,7 +30,7 @@ import art.arcane.iris.util.format.C; import art.arcane.iris.util.plugin.IrisService; import art.arcane.iris.util.plugin.VolmitSender; import art.arcane.iris.util.scheduling.J; -import art.arcane.volmlib.util.director.compat.DirectorDecreeEngineFactory; +import art.arcane.volmlib.util.director.compat.DirectorEngineFactory; import art.arcane.volmlib.util.director.context.DirectorContextRegistry; import art.arcane.volmlib.util.director.runtime.DirectorExecutionMode; import art.arcane.volmlib.util.director.runtime.DirectorExecutionResult; @@ -94,7 +94,7 @@ public class CommandSVC implements IrisService, CommandExecutor, TabCompleter, D } public DirectorRuntimeEngine getDirector() { - return directorCache.aquireNastyPrint(() -> DirectorDecreeEngineFactory.create( + return directorCache.aquireNastyPrint(() -> DirectorEngineFactory.create( new CommandIris(), null, buildDirectorContexts(), 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 c8f3e7791..978768c11 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 @@ -281,6 +281,10 @@ public class IrisEngineSVC implements IrisService { || engine.getMantle().getMantle().isClosed() || !engine.getMantle().getMantle().shouldReduce(engine)) return; + World engineWorld = engine.getWorld().realWorld(); + if (engineWorld != null && IrisToolbelt.isWorldMaintenanceActive(engineWorld)) { + return; + } try { engine.getMantle().trim(tectonicLimit()); @@ -304,6 +308,10 @@ public class IrisEngineSVC implements IrisService { || engine.getMantle().getMantle().isClosed() || !engine.getMantle().getMantle().shouldReduce(engine)) return; + World engineWorld = engine.getWorld().realWorld(); + if (engineWorld != null && IrisToolbelt.isWorldMaintenanceActive(engineWorld)) { + return; + } try { long unloadStart = System.currentTimeMillis(); 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 48841e365..8c7b03228 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 @@ -57,7 +57,7 @@ public class StudioSVC implements IrisService { @Override public void onEnable() { - J.s(() -> { + J.a(() -> { String pack = IrisSettings.get().getGenerator().getDefaultWorldType(); File f = IrisPack.packsPack(pack); diff --git a/core/src/main/java/art/arcane/iris/core/tools/IrisToolbelt.java b/core/src/main/java/art/arcane/iris/core/tools/IrisToolbelt.java index d0a81f0f1..893f95ff9 100644 --- a/core/src/main/java/art/arcane/iris/core/tools/IrisToolbelt.java +++ b/core/src/main/java/art/arcane/iris/core/tools/IrisToolbelt.java @@ -30,6 +30,7 @@ import art.arcane.iris.core.service.StudioSVC; import art.arcane.iris.engine.framework.Engine; import art.arcane.iris.engine.object.IrisDimension; import art.arcane.iris.engine.platform.PlatformChunkGenerator; +import art.arcane.iris.util.scheduling.J; import art.arcane.iris.util.plugin.VolmitSender; import org.bukkit.Bukkit; import org.bukkit.World; @@ -40,6 +41,8 @@ import java.io.File; import java.io.IOException; import java.util.HashMap; import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicInteger; /** * Something you really want to wear if working on Iris. Shit gets pretty hectic down there. @@ -48,6 +51,8 @@ import java.util.Map; public class IrisToolbelt { @ApiStatus.Internal public static Map toolbeltConfiguration = new HashMap<>(); + private static final Map worldMaintenanceDepth = new ConcurrentHashMap<>(); + private static final Map worldMaintenanceMantleBypassDepth = new ConcurrentHashMap<>(); /** * Will find / download / search for the dimension or return null @@ -215,7 +220,8 @@ public class IrisToolbelt { * @return the pregenerator job (already started) */ public static PregeneratorJob pregenerate(PregenTask task, PregeneratorMethod method, Engine engine, boolean cached) { - return new PregeneratorJob(task, cached && engine != null ? new CachedPregenMethod(method, engine.getWorld().name()) : method, engine); + boolean useCachedWrapper = cached && engine != null && !J.isFolia(); + return new PregeneratorJob(task, useCachedWrapper ? new CachedPregenMethod(method, engine.getWorld().name()) : method, engine); } /** @@ -293,6 +299,71 @@ public class IrisToolbelt { return isIrisWorld(i) && access(i).isStudio(); } + public static void beginWorldMaintenance(World world, String reason) { + beginWorldMaintenance(world, reason, false); + } + + public static void beginWorldMaintenance(World world, String reason, boolean bypassMantleStages) { + if (world == null) { + return; + } + + String name = world.getName(); + int depth = worldMaintenanceDepth.computeIfAbsent(name, k -> new AtomicInteger()).incrementAndGet(); + if (bypassMantleStages) { + worldMaintenanceMantleBypassDepth.computeIfAbsent(name, k -> new AtomicInteger()).incrementAndGet(); + } + Iris.info("World maintenance enter: " + name + " reason=" + reason + " depth=" + depth + " bypassMantle=" + bypassMantleStages); + } + + public static void endWorldMaintenance(World world, String reason) { + if (world == null) { + return; + } + + String name = world.getName(); + AtomicInteger depthCounter = worldMaintenanceDepth.get(name); + if (depthCounter == null) { + return; + } + + int depth = depthCounter.decrementAndGet(); + if (depth <= 0) { + worldMaintenanceDepth.remove(name, depthCounter); + depth = 0; + } + + AtomicInteger bypassCounter = worldMaintenanceMantleBypassDepth.get(name); + int bypassDepth = 0; + if (bypassCounter != null) { + bypassDepth = bypassCounter.decrementAndGet(); + if (bypassDepth <= 0) { + worldMaintenanceMantleBypassDepth.remove(name, bypassCounter); + bypassDepth = 0; + } + } + + Iris.info("World maintenance exit: " + name + " reason=" + reason + " depth=" + depth + " bypassMantleDepth=" + bypassDepth); + } + + public static boolean isWorldMaintenanceActive(World world) { + if (world == null) { + return false; + } + + AtomicInteger counter = worldMaintenanceDepth.get(world.getName()); + return counter != null && counter.get() > 0; + } + + public static boolean isWorldMaintenanceBypassingMantleStages(World world) { + if (world == null) { + return false; + } + + AtomicInteger counter = worldMaintenanceMantleBypassDepth.get(world.getName()); + return counter != null && counter.get() > 0; + } + public static void retainMantleDataForSlice(String className) { toolbeltConfiguration.put("retain.mantle." + className, Boolean.TRUE); } diff --git a/core/src/main/java/art/arcane/iris/engine/IrisEngine.java b/core/src/main/java/art/arcane/iris/engine/IrisEngine.java index 8c080ad48..35c506e38 100644 --- a/core/src/main/java/art/arcane/iris/engine/IrisEngine.java +++ b/core/src/main/java/art/arcane/iris/engine/IrisEngine.java @@ -30,6 +30,7 @@ import art.arcane.iris.core.nms.container.Pair; import art.arcane.iris.core.project.IrisProject; import art.arcane.iris.core.scripting.environment.EngineEnvironment; import art.arcane.iris.core.service.PreservationSVC; +import art.arcane.iris.core.tools.IrisToolbelt; import art.arcane.iris.engine.data.cache.AtomicCache; import art.arcane.iris.engine.framework.*; import art.arcane.iris.engine.mantle.EngineMantle; @@ -55,6 +56,7 @@ import lombok.Data; import lombok.EqualsAndHashCode; import lombok.ToString; import org.bukkit.Material; +import org.bukkit.World; import org.bukkit.block.Biome; import org.bukkit.block.data.BlockData; import org.bukkit.command.CommandSender; @@ -499,7 +501,11 @@ public class IrisEngine implements Engine { mode.generate(x, z, blocks, vbiomes, multicore); } - getMantle().getMantle().flag(x >> 4, z >> 4, MantleFlag.REAL, true); + World realWorld = getWorld().realWorld(); + boolean skipRealFlag = J.isFolia() && realWorld != null && IrisToolbelt.isWorldMaintenanceBypassingMantleStages(realWorld); + if (!skipRealFlag) { + getMantle().getMantle().flag(x >> 4, z >> 4, MantleFlag.REAL, true); + } getMetrics().getTotal().put(p.getMilliseconds()); generated.incrementAndGet(); diff --git a/core/src/main/java/art/arcane/iris/engine/IrisWorldManager.java b/core/src/main/java/art/arcane/iris/engine/IrisWorldManager.java index 792b99b61..09412fd6d 100644 --- a/core/src/main/java/art/arcane/iris/engine/IrisWorldManager.java +++ b/core/src/main/java/art/arcane/iris/engine/IrisWorldManager.java @@ -20,9 +20,11 @@ package art.arcane.iris.engine; import art.arcane.iris.Iris; import art.arcane.iris.core.IrisSettings; +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.service.ExternalDataSVC; +import art.arcane.iris.core.tools.IrisToolbelt; import art.arcane.iris.engine.data.cache.Cache; import art.arcane.iris.engine.framework.Engine; import art.arcane.iris.engine.framework.EngineAssignedWorldManager; @@ -62,6 +64,7 @@ import java.util.List; import java.util.Map; import java.util.Set; import java.util.concurrent.*; +import java.util.function.BiConsumer; import java.util.function.Predicate; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -80,6 +83,10 @@ public class IrisWorldManager extends EngineAssignedWorldManager { private final ChronoLatch chunkDiscovery; private final KMap> cleanup = new KMap<>(); private final ScheduledExecutorService cleanupService; + private final Set mantleWarmupQueue = ConcurrentHashMap.newKeySet(); + private final Set markerFlagQueue = ConcurrentHashMap.newKeySet(); + private final Set discoveredFlagQueue = ConcurrentHashMap.newKeySet(); + private final Set markerScanQueue = ConcurrentHashMap.newKeySet(); private double energy = 25; private int entityCount = 0; private long charge = 0; @@ -190,12 +197,15 @@ public class IrisWorldManager extends EngineAssignedWorldManager { } private void discoverChunks() { - var mantle = getEngine().getMantle().getMantle(); World world = getEngine().getWorld().realWorld(); if (world == null) { return; } + if (isPregenActiveForThisWorld()) { + return; + } + J.s(() -> { for (Player player : world.getPlayers()) { if (player == null || !player.isOnline()) { @@ -208,7 +218,9 @@ public class IrisWorldManager extends EngineAssignedWorldManager { int radius = 1; for (int x = -radius; x <= radius; x++) { for (int z = -radius; z <= radius; z++) { - mantle.getChunk(centerX + x, centerZ + z).flag(MantleFlag.DISCOVERED, true); + int chunkX = centerX + x; + int chunkZ = centerZ + z; + raiseDiscoveredChunkFlag(world, chunkX, chunkZ); } } }); @@ -216,12 +228,45 @@ public class IrisWorldManager extends EngineAssignedWorldManager { }); } + private void raiseDiscoveredChunkFlag(World world, int chunkX, int chunkZ) { + if (world == null) { + return; + } + + if (!J.isFolia()) { + getMantle().getChunk(chunkX, chunkZ).flag(MantleFlag.DISCOVERED, true); + return; + } + + long key = Cache.key(chunkX, chunkZ); + if (!discoveredFlagQueue.add(key)) { + return; + } + + J.a(() -> { + try { + Mantle mantle = getMantle(); + if (!mantle.hasFlag(chunkX, chunkZ, MantleFlag.DISCOVERED)) { + mantle.flag(chunkX, chunkZ, MantleFlag.DISCOVERED, true); + } + } catch (Throwable e) { + Iris.reportError(e); + } finally { + discoveredFlagQueue.remove(key); + } + }); + } + private void updateChunks() { World world = getEngine().getWorld().realWorld(); if (world == null) { return; } + if (isPregenActiveForThisWorld()) { + return; + } + J.s(() -> { for (Player player : world.getPlayers()) { if (player == null || !player.isOnline()) { @@ -253,6 +298,10 @@ public class IrisWorldManager extends EngineAssignedWorldManager { Chunk chunk = world.getChunkAt(chunkX, chunkZ); if (IrisSettings.get().getWorld().isPostLoadBlockUpdates()) { + if (J.isFolia() && !getMantle().isChunkLoaded(chunkX, chunkZ)) { + warmupMantleChunkAsync(chunkX, chunkZ); + return; + } getEngine().updateChunk(chunk); } @@ -260,7 +309,12 @@ public class IrisWorldManager extends EngineAssignedWorldManager { return; } - getMantle().raiseFlag(chunkX, chunkZ, MantleFlag.INITIAL_SPAWNED_MARKER, () -> { + if (!J.isFolia() && !getMantle().isChunkLoaded(chunkX, chunkZ)) { + warmupMantleChunkAsync(chunkX, chunkZ); + return; + } + + raiseInitialSpawnMarkerFlag(world, chunkX, chunkZ, () -> { int delay = RNG.r.i(5, 200); J.runRegion(world, chunkX, chunkZ, () -> { if (!world.isChunkLoaded(chunkX, chunkZ)) { @@ -269,23 +323,86 @@ public class IrisWorldManager extends EngineAssignedWorldManager { spawnIn(world.getChunkAt(chunkX, chunkZ), true); }, delay); - getSpawnersFromMarkers(chunk).forEach((blockf, spawners) -> { - if (spawners.isEmpty()) { + Chunk markerChunk = world.getChunkAt(chunkX, chunkZ); + forEachMarkerSpawner(markerChunk, (block, spawners) -> { + IrisSpawner s = new KList<>(spawners).getRandom(); + if (s == null) { return; } - - IrisPosition block = new IrisPosition(blockf.getX(), blockf.getY() + getEngine().getWorld().minHeight(), blockf.getZ()); - IrisSpawner s = new KList<>(spawners).getRandom(); spawn(block, s, true); }); }); } + private void raiseInitialSpawnMarkerFlag(World world, int chunkX, int chunkZ, Runnable onFirstRaise) { + if (world == null || onFirstRaise == null) { + return; + } + + if (!J.isFolia()) { + getMantle().raiseFlag(chunkX, chunkZ, MantleFlag.INITIAL_SPAWNED_MARKER, onFirstRaise); + return; + } + + long key = Cache.key(chunkX, chunkZ); + if (!markerFlagQueue.add(key)) { + return; + } + + J.a(() -> { + boolean raised = false; + try { + Mantle mantle = getMantle(); + if (!mantle.hasFlag(chunkX, chunkZ, MantleFlag.INITIAL_SPAWNED_MARKER)) { + mantle.flag(chunkX, chunkZ, MantleFlag.INITIAL_SPAWNED_MARKER, true); + raised = true; + } + } catch (Throwable e) { + Iris.reportError(e); + } finally { + markerFlagQueue.remove(key); + } + + if (!raised) { + return; + } + + J.runRegion(world, chunkX, chunkZ, () -> { + if (!world.isChunkLoaded(chunkX, chunkZ) || !Chunks.isSafe(world, chunkX, chunkZ)) { + return; + } + onFirstRaise.run(); + }); + }); + } + + private void warmupMantleChunkAsync(int chunkX, int chunkZ) { + long key = Cache.key(chunkX, chunkZ); + if (!mantleWarmupQueue.add(key)) { + return; + } + + J.a(() -> { + try { + getMantle().getChunk(chunkX, chunkZ); + } catch (Throwable e) { + Iris.reportError(e); + } finally { + mantleWarmupQueue.remove(key); + } + }); + } + private boolean onAsyncTick() { if (getEngine().isClosed()) { return false; } + if (isPregenActiveForThisWorld()) { + J.sleep(500); + return false; + } + actuallySpawned = 0; if (energy < 100) { @@ -335,6 +452,24 @@ public class IrisWorldManager extends EngineAssignedWorldManager { return actuallySpawned > 0; } + private boolean isPregenActiveForThisWorld() { + World world = getEngine().getWorld().realWorld(); + if (world == null) { + return false; + } + + if (IrisToolbelt.isWorldMaintenanceActive(world)) { + return true; + } + + PregeneratorJob job = PregeneratorJob.getInstance(); + if (job == null) { + return false; + } + + return job.targetsWorld(world); + } + private Chunk[] getLoadedChunksSnapshot(World world) { if (world == null) { return new Chunk[0]; @@ -396,15 +531,14 @@ public class IrisWorldManager extends EngineAssignedWorldManager { } if (IrisSettings.get().getWorld().isMarkerEntitySpawningSystem()) { - getSpawnersFromMarkers(c).forEach((blockf, spawners) -> { - if (spawners.isEmpty()) { + forEachMarkerSpawner(c, (block, spawners) -> { + IrisSpawner s = new KList<>(spawners).getRandom(); + if (s == null) { return; } - IrisPosition block = new IrisPosition(blockf.getX(), blockf.getY() + getEngine().getWorld().minHeight(), blockf.getZ()); - IrisSpawner s = new KList<>(spawners).getRandom(); spawn(block, s, false); - J.runRegion(c.getWorld(), c.getX(), c.getZ(), () -> getMantle().raiseFlag(c.getX(), c.getZ(), MantleFlag.INITIAL_SPAWNED_MARKER, + J.runRegion(c.getWorld(), c.getX(), c.getZ(), () -> raiseInitialSpawnMarkerFlag(c.getWorld(), c.getX(), c.getZ(), () -> spawn(block, s, true))); }); } @@ -530,21 +664,42 @@ public class IrisWorldManager extends EngineAssignedWorldManager { //INMS.get().injectBiomesFromMantle(e, getMantle()); if (!IrisSettings.get().getGenerator().earlyCustomBlocks) return; + if (isPregenActiveForThisWorld()) return; + + World world = e.getWorld(); + int chunkX = e.getX(); + int chunkZ = e.getZ(); + int minY = getTarget().getWorld().minHeight(); + int delay = RNG.r.i(20, 60); Iris.tickets.addTicket(e); - J.s(() -> { - var chunk = getMantle().getChunk(e).use(); - int minY = getTarget().getWorld().minHeight(); + + Runnable applyCustomBlocks = () -> { + if (J.isFolia() && (!world.isChunkLoaded(chunkX, chunkZ) || !Chunks.isSafe(world, chunkX, chunkZ))) { + Iris.tickets.removeTicket(e); + return; + } + + Chunk chunkRef = world.getChunkAt(chunkX, chunkZ); + var mantleChunk = getMantle().getChunk(chunkRef).use(); try { - chunk.raiseFlagUnchecked(MantleFlag.CUSTOM, () -> { - chunk.iterate(Identifier.class, (x, y, z, v) -> { - Iris.service(ExternalDataSVC.class).processUpdate(getEngine(), e.getBlock(x & 15, y + minY, z & 15), v); + mantleChunk.raiseFlagUnchecked(MantleFlag.CUSTOM, () -> { + mantleChunk.iterate(Identifier.class, (x, y, z, v) -> { + Iris.service(ExternalDataSVC.class).processUpdate(getEngine(), chunkRef.getBlock(x & 15, y + minY, z & 15), v); }); }); } finally { - chunk.release(); + mantleChunk.release(); Iris.tickets.removeTicket(e); } - }, RNG.r.i(20, 60)); + }; + + if (J.isFolia()) { + if (!J.runRegion(world, chunkX, chunkZ, applyCustomBlocks, delay)) { + Iris.tickets.removeTicket(e); + } + } else { + J.s(applyCustomBlocks, delay); + } } } @@ -643,6 +798,14 @@ public class IrisWorldManager extends EngineAssignedWorldManager { public Map> getSpawnersFromMarkers(Chunk c) { Map> p = new KMap<>(); Set b = new KSet<>(); + + if (J.isFolia()) { + if (!getMantle().isChunkLoaded(c.getX(), c.getZ())) { + warmupMantleChunkAsync(c.getX(), c.getZ()); + } + return p; + } + getMantle().iterateChunk(c.getX(), c.getZ(), MatterMarker.class, (x, y, z, t) -> { if (t.getTag().equals("cave_floor") || t.getTag().equals("cave_ceiling")) { return; @@ -684,6 +847,128 @@ public class IrisWorldManager extends EngineAssignedWorldManager { return p; } + private void forEachMarkerSpawner(Chunk c, BiConsumer> consumer) { + if (c == null || consumer == null) { + return; + } + + if (!J.isFolia()) { + int minY = getEngine().getWorld().minHeight(); + getSpawnersFromMarkers(c).forEach((relative, spawners) -> { + if (spawners.isEmpty()) { + return; + } + + consumer.accept(new IrisPosition(relative.getX(), relative.getY() + minY, relative.getZ()), spawners); + }); + return; + } + + int chunkX = c.getX(); + int chunkZ = c.getZ(); + World world = c.getWorld(); + long key = Cache.key(chunkX, chunkZ); + if (!markerScanQueue.add(key)) { + return; + } + + J.a(() -> { + try { + Map markerData = collectMarkerSpawnData(chunkX, chunkZ); + if (markerData.isEmpty()) { + return; + } + + J.runRegion(world, chunkX, chunkZ, () -> { + if (!world.isChunkLoaded(chunkX, chunkZ) || !Chunks.isSafe(world, chunkX, chunkZ)) { + return; + } + + Chunk chunk = world.getChunkAt(chunkX, chunkZ); + int minY = getEngine().getWorld().minHeight(); + markerData.forEach((relative, data) -> { + if (data.spawners.isEmpty()) { + return; + } + + if (isMarkerObstructed(chunk, relative, data.requiresEmptyAbove)) { + removeMarkerAsync(relative); + return; + } + + consumer.accept(new IrisPosition(relative.getX(), relative.getY() + minY, relative.getZ()), data.spawners); + }); + }); + } catch (Throwable e) { + Iris.reportError(e); + } finally { + markerScanQueue.remove(key); + } + }); + } + + private Map collectMarkerSpawnData(int chunkX, int chunkZ) { + Map markerData = new KMap<>(); + getMantle().iterateChunk(chunkX, chunkZ, MatterMarker.class, (x, y, z, t) -> { + if (t.getTag().equals("cave_floor") || t.getTag().equals("cave_ceiling")) { + return; + } + + IrisMarker mark = getData().getMarkerLoader().load(t.getTag()); + if (mark == null) { + return; + } + + IrisPosition position = new IrisPosition((chunkX << 4) + x, y, (chunkZ << 4) + z); + MarkerSpawnData data = markerData.computeIfAbsent(position, k -> new MarkerSpawnData()); + data.requiresEmptyAbove = data.requiresEmptyAbove || mark.isEmptyAbove(); + + for (String i : mark.getSpawners()) { + IrisSpawner spawner = getData().getSpawnerLoader().load(i); + if (spawner == null) { + Iris.error("Cannot load spawner: " + i + " for marker on " + getName()); + continue; + } + spawner.setReferenceMarker(mark); + data.spawners.add(spawner); + } + }); + + return markerData; + } + + private boolean isMarkerObstructed(Chunk chunk, IrisPosition relative, boolean requiresEmptyAbove) { + if (!requiresEmptyAbove) { + return false; + } + + int minY = getEngine().getWorld().minHeight(); + int markerY = relative.getY() + minY; + if (markerY + 2 >= chunk.getWorld().getMaxHeight()) { + return true; + } + + int localX = relative.getX() & 15; + int localZ = relative.getZ() & 15; + return chunk.getBlock(localX, markerY + 1, localZ).getBlockData().getMaterial().isSolid() + || chunk.getBlock(localX, markerY + 2, localZ).getBlockData().getMaterial().isSolid(); + } + + private void removeMarkerAsync(IrisPosition marker) { + J.a(() -> { + try { + getMantle().remove(marker.getX(), marker.getY(), marker.getZ(), MatterMarker.class); + } catch (Throwable e) { + Iris.reportError(e); + } + }); + } + + private static final class MarkerSpawnData { + private final KSet spawners = new KSet<>(); + private boolean requiresEmptyAbove; + } + @Override public void onBlockBreak(BlockBreakEvent e) { if (e.getBlock().getWorld().equals(getTarget().getWorld().realWorld())) { 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 5a0f2e1e9..90a894655 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 @@ -31,6 +31,7 @@ import art.arcane.iris.core.nms.container.Pair; import art.arcane.iris.core.pregenerator.ChunkUpdater; import art.arcane.iris.core.scripting.environment.EngineEnvironment; import art.arcane.iris.core.service.ExternalDataSVC; +import art.arcane.iris.core.tools.IrisToolbelt; import art.arcane.iris.engine.IrisComplex; import art.arcane.iris.engine.data.cache.Cache; import art.arcane.iris.engine.data.chunk.TerrainChunk; @@ -1003,6 +1004,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; + } if (IrisSettings.get().getPerformance().isTrimMantleInStudio() || !isStudio()) { getMantle().cleanupChunk(x, z); } diff --git a/core/src/main/java/art/arcane/iris/engine/framework/EngineMode.java b/core/src/main/java/art/arcane/iris/engine/framework/EngineMode.java index f1f2fee8b..08309047f 100644 --- a/core/src/main/java/art/arcane/iris/engine/framework/EngineMode.java +++ b/core/src/main/java/art/arcane/iris/engine/framework/EngineMode.java @@ -18,6 +18,7 @@ package art.arcane.iris.engine.framework; +import art.arcane.iris.core.tools.IrisToolbelt; import art.arcane.iris.engine.IrisComplex; import art.arcane.iris.engine.mantle.EngineMantle; import art.arcane.iris.util.context.ChunkContext; @@ -27,6 +28,7 @@ import art.arcane.iris.util.hunk.Hunk; import art.arcane.volmlib.util.math.RollingSequence; import art.arcane.iris.util.parallel.BurstExecutor; import art.arcane.iris.util.parallel.MultiBurst; +import art.arcane.iris.util.scheduling.J; import org.bukkit.block.Biome; import org.bukkit.block.data.BlockData; @@ -69,7 +71,14 @@ public interface EngineMode extends Staged { @BlockCoordinates default void generate(int x, int z, Hunk blocks, Hunk biomes, boolean multicore) { - ChunkContext ctx = new ChunkContext(x, z, getComplex()); + boolean cacheContext = true; + if (J.isFolia()) { + var world = getEngine().getWorld().realWorld(); + if (world != null && IrisToolbelt.isWorldMaintenanceActive(world)) { + cacheContext = false; + } + } + ChunkContext ctx = new ChunkContext(x, z, getComplex(), cacheContext); IrisContext.getOr(getEngine()).setChunkContext(ctx); for (EngineStage i : getStages()) { 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 b1ddbcbab..df858094d 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 @@ -42,6 +42,8 @@ import art.arcane.volmlib.util.matter.MatterMarker; import art.arcane.iris.util.matter.*; import art.arcane.iris.util.matter.slices.UpdateMatter; import art.arcane.iris.util.parallel.MultiBurst; +import art.arcane.iris.util.scheduling.J; +import org.bukkit.World; import org.bukkit.block.data.BlockData; import org.jetbrains.annotations.UnmodifiableView; @@ -80,6 +82,13 @@ public interface EngineMantle extends MatterGenerator { @ChunkCoordinates default KList findMarkers(int x, int z, MatterMarker marker) { KList p = new KList<>(); + if (J.isFolia()) { + World world = getEngine().getWorld().realWorld(); + if (world != null && J.isOwnedByCurrentRegion(world, x, z)) { + return p; + } + } + getMantle().iterateChunk(x, z, MatterMarker.class, (xx, yy, zz, mm) -> { if (marker.equals(mm)) { p.add(new IrisPosition(xx + (x << 4), yy, zz + (z << 4))); diff --git a/core/src/main/java/art/arcane/iris/engine/mantle/MantleWriter.java b/core/src/main/java/art/arcane/iris/engine/mantle/MantleWriter.java index 1ab905887..5baa04e15 100644 --- a/core/src/main/java/art/arcane/iris/engine/mantle/MantleWriter.java +++ b/core/src/main/java/art/arcane/iris/engine/mantle/MantleWriter.java @@ -20,6 +20,8 @@ package art.arcane.iris.engine.mantle; import com.google.common.collect.ImmutableList; import art.arcane.iris.Iris; +import art.arcane.iris.core.IrisSettings; +import art.arcane.iris.core.tools.IrisToolbelt; import art.arcane.iris.core.loader.IrisData; import art.arcane.iris.engine.data.cache.Cache; import art.arcane.iris.engine.framework.Engine; @@ -40,6 +42,7 @@ import art.arcane.iris.util.matter.Matter; import art.arcane.volmlib.util.matter.MatterCavern; import art.arcane.iris.util.matter.TileWrapper; import art.arcane.iris.util.noise.CNG; +import art.arcane.iris.util.scheduling.J; import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap; import lombok.Data; import org.bukkit.block.data.BlockData; @@ -67,7 +70,12 @@ public class MantleWriter implements IObjectPlacer, AutoCloseable { this.x = x; this.z = z; - final int parallelism = multicore ? Runtime.getRuntime().availableProcessors() / 2 : 4; + final boolean foliaMaintenance = J.isFolia() + && IrisToolbelt.isWorldMaintenanceActive(engineMantle.getEngine().getWorld().realWorld()); + final int parallelism = foliaMaintenance ? 1 : (multicore ? Runtime.getRuntime().availableProcessors() / 2 : 4); + if (foliaMaintenance && IrisSettings.get().getGeneral().isDebug()) { + Iris.info("MantleWriter using sequential chunk prefetch for maintenance regen at " + x + "," + z + "."); + } final var map = multicore ? cachedChunks : new KMap(d * d, 1f, parallelism); mantle.getChunks( x - radius, 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 e2150c059..471e6bf25 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 @@ -19,6 +19,8 @@ package art.arcane.iris.engine.mantle.components; import art.arcane.iris.Iris; +import art.arcane.iris.core.IrisSettings; +import art.arcane.iris.core.tools.IrisToolbelt; import art.arcane.iris.engine.data.cache.Cache; import art.arcane.iris.engine.mantle.ComponentFlag; import art.arcane.iris.engine.mantle.EngineMantle; @@ -39,6 +41,7 @@ import art.arcane.iris.util.matter.MatterStructurePOI; import art.arcane.iris.util.noise.CNG; import art.arcane.iris.util.noise.NoiseType; import art.arcane.iris.util.parallel.BurstExecutor; +import art.arcane.iris.util.scheduling.J; import org.bukkit.util.BlockVector; import java.io.IOException; @@ -55,12 +58,32 @@ public class MantleObjectComponent extends IrisMantleComponent { @Override public void generateLayer(MantleWriter writer, int x, int z, ChunkContext context) { + boolean traceRegen = isRegenTraceThread(); RNG rng = applyNoise(x, z, Cache.key(x, z) + seed()); int xxx = 8 + (x << 4); int zzz = 8 + (z << 4); IrisRegion region = getComplex().getRegionStream().get(xxx, zzz); IrisBiome biome = getComplex().getTrueBiomeStream().get(xxx, zzz); - placeObjects(writer, rng, x, z, biome, region); + if (traceRegen) { + Iris.info("Regen object layer start: chunk=" + x + "," + z + + " biome=" + biome.getLoadKey() + + " region=" + region.getLoadKey() + + " biomePlacers=" + biome.getSurfaceObjects().size() + + " regionPlacers=" + region.getSurfaceObjects().size()); + } + ObjectPlacementSummary summary = placeObjects(writer, rng, x, z, biome, region, traceRegen); + if (traceRegen) { + Iris.info("Regen object layer done: chunk=" + x + "," + z + + " biomePlacersChecked=" + summary.biomePlacersChecked() + + " biomePlacersTriggered=" + summary.biomePlacersTriggered() + + " regionPlacersChecked=" + summary.regionPlacersChecked() + + " regionPlacersTriggered=" + summary.regionPlacersTriggered() + + " objectAttempts=" + summary.objectAttempts() + + " objectPlaced=" + summary.objectPlaced() + + " objectRejected=" + summary.objectRejected() + + " objectNull=" + summary.objectNull() + + " objectErrors=" + summary.objectErrors()); + } } private RNG applyNoise(int x, int z, long seed) { @@ -69,12 +92,39 @@ public class MantleObjectComponent extends IrisMantleComponent { } @ChunkCoordinates - private void placeObjects(MantleWriter writer, RNG rng, int x, int z, IrisBiome biome, IrisRegion region) { + private ObjectPlacementSummary placeObjects(MantleWriter writer, RNG rng, int x, int z, IrisBiome biome, IrisRegion region, boolean traceRegen) { + int biomeChecked = 0; + int biomeTriggered = 0; + int regionChecked = 0; + int regionTriggered = 0; + int attempts = 0; + int placed = 0; + int rejected = 0; + int nullObjects = 0; + int errors = 0; + for (IrisObjectPlacement i : biome.getSurfaceObjects()) { - if (rng.chance(i.getChance() + rng.d(-0.005, 0.005))) { + biomeChecked++; + boolean chance = rng.chance(i.getChance() + rng.d(-0.005, 0.005)); + if (traceRegen) { + Iris.info("Regen object placer chance: chunk=" + x + "," + z + + " scope=biome" + + " chanceResult=" + chance + + " chanceBase=" + i.getChance() + + " densityMid=" + i.getDensity() + + " objects=" + i.getPlace().size()); + } + if (chance) { + biomeTriggered++; try { - placeObject(writer, rng, x << 4, z << 4, i); + ObjectPlacementResult result = placeObject(writer, rng, x << 4, z << 4, i, traceRegen, x, z, "biome"); + attempts += result.attempts(); + placed += result.placed(); + rejected += result.rejected(); + nullObjects += result.nullObjects(); + errors += result.errors(); } catch (Throwable e) { + errors++; Iris.reportError(e); Iris.error("Failed to place objects in the following biome: " + biome.getName()); Iris.error("Object(s) " + i.getPlace().toString(", ") + " (" + e.getClass().getSimpleName() + ")."); @@ -85,10 +135,27 @@ public class MantleObjectComponent extends IrisMantleComponent { } for (IrisObjectPlacement i : region.getSurfaceObjects()) { - if (rng.chance(i.getChance() + rng.d(-0.005, 0.005))) { + regionChecked++; + boolean chance = rng.chance(i.getChance() + rng.d(-0.005, 0.005)); + if (traceRegen) { + Iris.info("Regen object placer chance: chunk=" + x + "," + z + + " scope=region" + + " chanceResult=" + chance + + " chanceBase=" + i.getChance() + + " densityMid=" + i.getDensity() + + " objects=" + i.getPlace().size()); + } + if (chance) { + regionTriggered++; try { - placeObject(writer, rng, x << 4, z << 4, i); + ObjectPlacementResult result = placeObject(writer, rng, x << 4, z << 4, i, traceRegen, x, z, "region"); + attempts += result.attempts(); + placed += result.placed(); + rejected += result.rejected(); + nullObjects += result.nullObjects(); + errors += result.errors(); } catch (Throwable e) { + errors++; Iris.reportError(e); Iris.error("Failed to place objects in the following region: " + region.getName()); Iris.error("Object(s) " + i.getPlace().toString(", ") + " (" + e.getClass().getSimpleName() + ")."); @@ -97,25 +164,114 @@ public class MantleObjectComponent extends IrisMantleComponent { } } } + + return new ObjectPlacementSummary( + biomeChecked, + biomeTriggered, + regionChecked, + regionTriggered, + attempts, + placed, + rejected, + nullObjects, + errors + ); } @BlockCoordinates - private void placeObject(MantleWriter writer, RNG rng, int x, int z, IrisObjectPlacement objectPlacement) { - for (int i = 0; i < objectPlacement.getDensity(rng, x, z, getData()); i++) { + private ObjectPlacementResult placeObject( + MantleWriter writer, + RNG rng, + int x, + int z, + IrisObjectPlacement objectPlacement, + boolean traceRegen, + int chunkX, + int chunkZ, + String scope + ) { + int attempts = 0; + int placed = 0; + int rejected = 0; + int nullObjects = 0; + int errors = 0; + int density = objectPlacement.getDensity(rng, x, z, getData()); + + for (int i = 0; i < density; i++) { + attempts++; IrisObject v = objectPlacement.getScale().get(rng, objectPlacement.getObject(getComplex(), rng)); if (v == null) { - return; + nullObjects++; + if (traceRegen) { + Iris.warn("Regen object placement null object: chunk=" + chunkX + "," + chunkZ + + " scope=" + scope + + " densityIndex=" + i + + " density=" + density + + " placementKeys=" + objectPlacement.getPlace().toString(",")); + } + continue; } int xx = rng.i(x, x + 15); int zz = rng.i(z, z + 15); int id = rng.i(0, Integer.MAX_VALUE); - v.place(xx, -1, zz, writer, objectPlacement, rng, (b, data) -> { - writer.setData(b.getX(), b.getY(), b.getZ(), v.getLoadKey() + "@" + id); - if (objectPlacement.isDolphinTarget() && objectPlacement.isUnderwater() && B.isStorageChest(data)) { - writer.setData(b.getX(), b.getY(), b.getZ(), MatterStructurePOI.BURIED_TREASURE); + try { + int result = v.place(xx, -1, zz, writer, objectPlacement, rng, (b, data) -> { + writer.setData(b.getX(), b.getY(), b.getZ(), v.getLoadKey() + "@" + id); + if (objectPlacement.isDolphinTarget() && objectPlacement.isUnderwater() && B.isStorageChest(data)) { + writer.setData(b.getX(), b.getY(), b.getZ(), MatterStructurePOI.BURIED_TREASURE); + } + }, null, getData()); + + if (result >= 0) { + placed++; + } else { + rejected++; } - }, null, getData()); + + if (traceRegen) { + Iris.info("Regen object placement result: chunk=" + chunkX + "," + chunkZ + + " scope=" + scope + + " object=" + v.getLoadKey() + + " resultY=" + result + + " px=" + xx + + " pz=" + zz + + " densityIndex=" + i + + " density=" + density); + } + } catch (Throwable e) { + errors++; + Iris.reportError(e); + Iris.error("Regen object placement exception: chunk=" + chunkX + "," + chunkZ + + " scope=" + scope + + " object=" + v.getLoadKey() + + " densityIndex=" + i + + " density=" + density + + " error=" + e.getClass().getSimpleName() + ":" + e.getMessage()); + } } + + return new ObjectPlacementResult(attempts, placed, rejected, nullObjects, errors); + } + + private boolean isRegenTraceThread() { + return Thread.currentThread().getName().startsWith("Iris-Regen-") + && IrisSettings.get().getGeneral().isDebug(); + } + + private record ObjectPlacementSummary( + int biomePlacersChecked, + int biomePlacersTriggered, + int regionPlacersChecked, + int regionPlacersTriggered, + int objectAttempts, + int objectPlaced, + int objectRejected, + int objectNull, + int objectErrors + ) { + } + + private record ObjectPlacementResult(int attempts, int placed, int rejected, int nullObjects, int errors) { } @BlockCoordinates @@ -182,6 +338,15 @@ public class MantleObjectComponent extends IrisMantleComponent { } BurstExecutor e = getEngineMantle().getTarget().getBurster().burst(objects.size()); + boolean maintenanceFolia = false; + if (J.isFolia()) { + var world = getEngineMantle().getEngine().getWorld().realWorld(); + maintenanceFolia = world != null && IrisToolbelt.isWorldMaintenanceActive(world); + } + if (maintenanceFolia) { + Iris.info("MantleObjectComponent radius scan using single-threaded mode during maintenance regen."); + e.setMulticore(false); + } KMap sizeCache = new KMap<>(); for (String i : objects) { e.queue(() -> { diff --git a/core/src/main/java/art/arcane/iris/engine/mode/ModeOverworld.java b/core/src/main/java/art/arcane/iris/engine/mode/ModeOverworld.java index 3e8b201a3..7b6a95c61 100644 --- a/core/src/main/java/art/arcane/iris/engine/mode/ModeOverworld.java +++ b/core/src/main/java/art/arcane/iris/engine/mode/ModeOverworld.java @@ -18,6 +18,7 @@ package art.arcane.iris.engine.mode; +import art.arcane.iris.core.tools.IrisToolbelt; import art.arcane.iris.engine.actuator.IrisBiomeActuator; import art.arcane.iris.engine.actuator.IrisDecorantActuator; import art.arcane.iris.engine.actuator.IrisTerrainNormalActuator; @@ -26,9 +27,14 @@ import art.arcane.iris.engine.framework.EngineMode; import art.arcane.iris.engine.framework.EngineStage; import art.arcane.iris.engine.framework.IrisEngineMode; import art.arcane.iris.engine.modifier.*; +import art.arcane.iris.util.scheduling.J; import org.bukkit.block.data.BlockData; +import java.util.concurrent.atomic.AtomicLong; + public class ModeOverworld extends IrisEngineMode implements EngineMode { + private static final AtomicLong lastMaintenanceBypassLog = new AtomicLong(0L); + public ModeOverworld(Engine engine) { super(engine); var terrain = new IrisTerrainNormalActuator(getEngine()); @@ -40,15 +46,45 @@ public class ModeOverworld extends IrisEngineMode implements EngineMode { var perfection = new IrisPerfectionModifier(getEngine()); var custom = new IrisCustomModifier(getEngine()); EngineStage sBiome = (x, z, k, p, m, c) -> biome.actuate(x, z, p, m, c); - EngineStage sGenMatter = (x, z, k, p, m, c) -> generateMatter(x >> 4, z >> 4, m, c); + EngineStage sGenMatter = (x, z, k, p, m, c) -> { + if (shouldBypassMantleStages(getEngine())) { + return; + } + generateMatter(x >> 4, z >> 4, m, c); + }; EngineStage sTerrain = (x, z, k, p, m, c) -> terrain.actuate(x, z, k, m, c); EngineStage sDecorant = (x, z, k, p, m, c) -> decorant.actuate(x, z, k, m, c); - EngineStage sCave = (x, z, k, p, m, c) -> cave.modify(x >> 4, z >> 4, k, m, c); - EngineStage sDeposit = (x, z, k, p, m, c) -> deposit.modify(x, z, k, m, c); - EngineStage sPost = (x, z, k, p, m, c) -> post.modify(x, z, k, m, c); - EngineStage sInsertMatter = (x, z, K, p, m, c) -> getMantle().insertMatter(x >> 4, z >> 4, BlockData.class, K, m); + EngineStage sCave = (x, z, k, p, m, c) -> { + if (shouldBypassMantleStages(getEngine())) { + return; + } + cave.modify(x >> 4, z >> 4, k, m, c); + }; + EngineStage sDeposit = (x, z, k, p, m, c) -> { + if (shouldBypassMantleStages(getEngine())) { + return; + } + deposit.modify(x, z, k, m, c); + }; + EngineStage sPost = (x, z, k, p, m, c) -> { + if (shouldBypassMantleStages(getEngine())) { + return; + } + post.modify(x, z, k, m, c); + }; + EngineStage sInsertMatter = (x, z, K, p, m, c) -> { + if (shouldBypassMantleStages(getEngine())) { + return; + } + getMantle().insertMatter(x >> 4, z >> 4, BlockData.class, K, m); + }; EngineStage sPerfection = (x, z, k, p, m, c) -> perfection.modify(x, z, k, m, c); - EngineStage sCustom = (x, z, k, p, m, c) -> custom.modify(x, z, k, m, c); + EngineStage sCustom = (x, z, k, p, m, c) -> { + if (shouldBypassMantleStages(getEngine())) { + return; + } + custom.modify(x, z, k, m, c); + }; registerStage(burst( sGenMatter, @@ -66,4 +102,21 @@ public class ModeOverworld extends IrisEngineMode implements EngineMode { registerStage(sPerfection); registerStage(sCustom); } + + private static boolean shouldBypassMantleStages(Engine engine) { + if (!J.isFolia()) { + return false; + } + + var world = engine.getWorld().realWorld(); + boolean active = world != null && IrisToolbelt.isWorldMaintenanceBypassingMantleStages(world); + if (active) { + long now = System.currentTimeMillis(); + long last = lastMaintenanceBypassLog.get(); + if (now - last >= 5000L && lastMaintenanceBypassLog.compareAndSet(last, now)) { + art.arcane.iris.Iris.info("Maintenance regen bypass: skipping mantle-backed overworld stages for Folia safety."); + } + } + return active; + } } diff --git a/core/src/main/java/art/arcane/iris/engine/object/IrisEntitySpawn.java b/core/src/main/java/art/arcane/iris/engine/object/IrisEntitySpawn.java index dd964ad4a..84459a2d6 100644 --- a/core/src/main/java/art/arcane/iris/engine/object/IrisEntitySpawn.java +++ b/core/src/main/java/art/arcane/iris/engine/object/IrisEntitySpawn.java @@ -28,6 +28,7 @@ import art.arcane.volmlib.util.math.RNG; import art.arcane.volmlib.util.math.Vector3d; import art.arcane.volmlib.util.matter.MatterMarker; import art.arcane.iris.util.matter.slices.MarkerMatter; +import art.arcane.iris.util.scheduling.J; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; @@ -77,8 +78,14 @@ public class IrisEntitySpawn implements IRare { int hf = gen.getHeight(x, z, false) + (gen.getWorld().tryGetRealWorld() ? gen.getWorld().realWorld().getMinHeight() : -64); Location l = switch (getReferenceSpawner().getGroup()) { case NORMAL -> new Location(c.getWorld(), x, hf + 1, z); - case CAVE -> gen.getMantle().findMarkers(c.getX(), c.getZ(), MarkerMatter.CAVE_FLOOR) - .convert((i) -> i.toLocation(c.getWorld()).add(0, 1, 0)).getRandom(rng); + case CAVE -> { + if (J.isFolia()) { + // Avoid mantle region IO lookups on Folia tick threads. + yield new Location(c.getWorld(), x, h + 1, z); + } + yield gen.getMantle().findMarkers(c.getX(), c.getZ(), MarkerMatter.CAVE_FLOOR) + .convert((i) -> i.toLocation(c.getWorld()).add(0, 1, 0)).getRandom(rng); + } case UNDERWATER, BEACH -> new Location(c.getWorld(), x, rng.i(h + 1, hf), z); }; @@ -113,7 +120,11 @@ public class IrisEntitySpawn implements IRare { if (spawns > 0) { if (referenceMarker != null && referenceMarker.shouldExhaust()) { - gen.getMantle().getMantle().remove(c.getX(), c.getY() - gen.getWorld().minHeight(), c.getZ(), MatterMarker.class); + if (J.isFolia()) { + J.a(() -> gen.getMantle().getMantle().remove(c.getX(), c.getY() - gen.getWorld().minHeight(), c.getZ(), MatterMarker.class)); + } else { + gen.getMantle().getMantle().remove(c.getX(), c.getY() - gen.getWorld().minHeight(), c.getZ(), MatterMarker.class); + } } for (int id = 0; id < spawns; id++) { diff --git a/core/src/main/java/art/arcane/iris/engine/platform/BukkitChunkGenerator.java b/core/src/main/java/art/arcane/iris/engine/platform/BukkitChunkGenerator.java index 756896c89..6dcbfe673 100644 --- a/core/src/main/java/art/arcane/iris/engine/platform/BukkitChunkGenerator.java +++ b/core/src/main/java/art/arcane/iris/engine/platform/BukkitChunkGenerator.java @@ -65,6 +65,7 @@ import java.util.Random; 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; @EqualsAndHashCode(callSuper = true) @@ -198,74 +199,186 @@ public class BukkitChunkGenerator extends ChunkGenerator implements PlatformChun @Override public void injectChunkReplacement(World world, int x, int z, Executor syncExecutor) { + boolean acquired = false; + String phase = "start"; try { - loadLock.acquire(); + phase = "acquire-load-lock"; + long acquireStart = System.currentTimeMillis(); + while (!loadLock.tryAcquire(5, TimeUnit.SECONDS)) { + Iris.warn("Chunk replacement waiting for load lock at " + x + "," + z + + " for " + (System.currentTimeMillis() - acquireStart) + "ms."); + } + acquired = true; + long acquireWait = System.currentTimeMillis() - acquireStart; + if (acquireWait >= 5000L) { + Iris.warn("Chunk replacement waited " + acquireWait + "ms for load lock at " + x + "," + z + "."); + } IrisBiomeStorage st = new IrisBiomeStorage(); TerrainChunk tc = TerrainChunk.createUnsafe(world, st); this.world.bind(world); - getEngine().generate(x << 4, z << 4, tc, IrisSettings.get().getGenerator().useMulticore); - Chunk c = PaperLib.getChunkAtAsync(world, x, z) - .thenApply(d -> { - Iris.tickets.addTicket(d); + phase = "engine-generate"; + long generateStart = System.currentTimeMillis(); + boolean useMulticore = IrisSettings.get().getGenerator().useMulticore && !J.isFolia(); + AtomicBoolean generateDone = new AtomicBoolean(false); + AtomicLong generationWatchdogStart = new AtomicLong(System.currentTimeMillis()); + Thread generateThread = Thread.currentThread(); + J.a(() -> { + while (!generateDone.get()) { + if (!J.sleep(5000)) { + return; + } + if (generateDone.get()) { + return; + } - for (Entity ee : d.getEntities()) { - if (ee instanceof Player) { - continue; - } + Iris.warn("Chunk replacement still generating at " + x + "," + z + + " for " + (System.currentTimeMillis() - generationWatchdogStart.get()) + "ms" + + " thread=" + generateThread.getName() + + " state=" + generateThread.getState()); + } + }); + try { + getEngine().generate(x << 4, z << 4, tc, useMulticore); + } finally { + generateDone.set(true); + } + long generateTook = System.currentTimeMillis() - generateStart; + if (generateTook >= 5000L) { + Iris.warn("Chunk replacement terrain generation took " + generateTook + "ms at " + x + "," + z + "."); + } - ee.remove(); - } - - return d; - }).get(); - - - KList> futures = new KList<>(1 + getEngine().getHeight() >> 4); - for (int i = getEngine().getHeight() >> 4; i >= 0; i--) { - int finalI = i << 4; - futures.add(CompletableFuture.runAsync(() -> { - for (int xx = 0; xx < 16; xx++) { - for (int yy = 0; yy < 16; yy++) { - for (int zz = 0; zz < 16; zz++) { - if (yy + finalI >= engine.getHeight() || yy + finalI < 0) { + if (J.isFolia()) { + phase = "folia-run-region"; + CountDownLatch latch = new CountDownLatch(1); + Throwable[] failure = new Throwable[1]; + long regionScheduleStart = System.currentTimeMillis(); + if (!J.runRegion(world, x, z, () -> { + try { + phaseUnsafeSet("folia-region-run", x, z); + Chunk c = world.getChunkAt(x, z); + Iris.tickets.addTicket(c); + try { + for (Entity ee : c.getEntities()) { + if (ee instanceof Player) { continue; } - int y = yy + finalI + world.getMinHeight(); - c.getBlock(xx, y, zz).setBlockData(tc.getBlockData(xx, y, zz), false); + + ee.remove(); + } + + for (int i = getEngine().getHeight() >> 4; i >= 0; i--) { + int finalI = i << 4; + for (int xx = 0; xx < 16; xx++) { + for (int yy = 0; yy < 16; yy++) { + for (int zz = 0; zz < 16; zz++) { + if (yy + finalI >= engine.getHeight() || yy + finalI < 0) { + continue; + } + int y = yy + finalI + world.getMinHeight(); + c.getBlock(xx, y, zz).setBlockData(tc.getBlockData(xx, y, zz), false); + } + } + } + } + + INMS.get().placeStructures(c); + engine.getWorldManager().onChunkLoad(c, true); + } finally { + Iris.tickets.removeTicket(c); + } + } catch (Throwable e) { + failure[0] = e; + } finally { + latch.countDown(); + } + })) { + throw new IllegalStateException("Failed to schedule region task for chunk replacement at " + x + "," + z); + } + long regionScheduleTook = System.currentTimeMillis() - regionScheduleStart; + if (regionScheduleTook >= 1000L) { + Iris.verbose("Chunk replacement region task scheduling took " + regionScheduleTook + "ms at " + x + "," + z + "."); + } + + long regionWaitStart = System.currentTimeMillis(); + while (!latch.await(5, TimeUnit.SECONDS)) { + Iris.warn("Chunk replacement waiting on region task at " + x + "," + z + + " for " + (System.currentTimeMillis() - regionWaitStart) + "ms."); + } + long regionWaitTook = System.currentTimeMillis() - regionWaitStart; + if (regionWaitTook >= 5000L) { + Iris.warn("Chunk replacement region task completed after " + regionWaitTook + "ms at " + x + "," + z + "."); + } + if (failure[0] != null) { + throw failure[0]; + } + } else { + phase = "paperlib-async-load"; + long loadChunkStart = System.currentTimeMillis(); + Chunk c = PaperLib.getChunkAtAsync(world, x, z).get(); + long loadChunkTook = System.currentTimeMillis() - loadChunkStart; + if (loadChunkTook >= 5000L) { + Iris.warn("Chunk replacement chunk load took " + loadChunkTook + "ms at " + x + "," + z + "."); + } + + phase = "non-folia-apply"; + Iris.tickets.addTicket(c); + CompletableFuture.runAsync(() -> { + for (Entity ee : c.getEntities()) { + if (ee instanceof Player) { + continue; + } + + ee.remove(); + } + }, syncExecutor).get(); + + KList> futures = new KList<>(1 + getEngine().getHeight() >> 4); + for (int i = getEngine().getHeight() >> 4; i >= 0; i--) { + int finalI = i << 4; + futures.add(CompletableFuture.runAsync(() -> { + for (int xx = 0; xx < 16; xx++) { + for (int yy = 0; yy < 16; yy++) { + for (int zz = 0; zz < 16; zz++) { + if (yy + finalI >= engine.getHeight() || yy + finalI < 0) { + continue; + } + int y = yy + finalI + world.getMinHeight(); + c.getBlock(xx, y, zz).setBlockData(tc.getBlockData(xx, y, zz), false); + } } } - } - }, syncExecutor)); + }, syncExecutor)); + } + futures.add(CompletableFuture.runAsync(() -> INMS.get().placeStructures(c), syncExecutor)); + + CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])) + .thenRunAsync(() -> { + Iris.tickets.removeTicket(c); + engine.getWorldManager().onChunkLoad(c, true); + }, syncExecutor) + .get(); } - futures.add(CompletableFuture.runAsync(() -> INMS.get().placeStructures(c), syncExecutor)); - CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])) - .thenRunAsync(() -> { - Iris.tickets.removeTicket(c); - engine.getWorldManager().onChunkLoad(c, true); - }, syncExecutor) - .get(); Iris.debug("Regenerated " + x + " " + z); - - loadLock.release(); } catch (Throwable e) { - loadLock.release(); Iris.error("======================================"); + Iris.error("Chunk replacement failed at phase=" + phase + " chunk=" + x + "," + z); e.printStackTrace(); Iris.reportErrorChunk(x, z, e, "CHUNK"); Iris.error("======================================"); - - ChunkData d = Bukkit.createChunkData(world); - - for (int i = 0; i < 16; i++) { - for (int j = 0; j < 16; j++) { - d.setBlock(i, 0, j, Material.RED_GLAZED_TERRACOTTA.createBlockData()); - } + throw new IllegalStateException("Chunk replacement failed at phase=" + phase + " chunk=" + x + "," + z, e); + } finally { + if (acquired) { + loadLock.release(); } } } + private static void phaseUnsafeSet(String phase, int x, int z) { + Iris.verbose("Chunk replacement phase=" + phase + " chunk=" + x + "," + z); + } + private Engine getEngine(WorldInfo world) { if (setup.get()) { return getEngine(); diff --git a/core/src/main/java/art/arcane/iris/util/common/decree/DecreeContext.java b/core/src/main/java/art/arcane/iris/util/common/decree/DecreeContext.java index bd225181e..f5653ccf2 100644 --- a/core/src/main/java/art/arcane/iris/util/common/decree/DecreeContext.java +++ b/core/src/main/java/art/arcane/iris/util/common/decree/DecreeContext.java @@ -1,10 +1,10 @@ package art.arcane.iris.util.decree; -import art.arcane.volmlib.util.decree.context.DecreeContextBase; +import art.arcane.volmlib.util.director.context.DirectorContextBase; import art.arcane.iris.util.plugin.VolmitSender; public class DecreeContext { - private static final DecreeContextBase context = new DecreeContextBase<>(); + private static final DirectorContextBase context = new DirectorContextBase<>(); public static VolmitSender get() { return context.get(); diff --git a/core/src/main/java/art/arcane/iris/util/common/decree/DecreeContextHandler.java b/core/src/main/java/art/arcane/iris/util/common/decree/DecreeContextHandler.java index f452d2504..0b56bf0a2 100644 --- a/core/src/main/java/art/arcane/iris/util/common/decree/DecreeContextHandler.java +++ b/core/src/main/java/art/arcane/iris/util/common/decree/DecreeContextHandler.java @@ -1,17 +1,17 @@ package art.arcane.iris.util.decree; -import art.arcane.volmlib.util.decree.context.DecreeContextHandlers; -import art.arcane.volmlib.util.decree.context.DecreeContextHandlerType; +import art.arcane.volmlib.util.director.context.DirectorContextHandlers; +import art.arcane.volmlib.util.director.context.DirectorContextHandlerType; import art.arcane.iris.Iris; import art.arcane.iris.util.plugin.VolmitSender; import java.util.Map; -public interface DecreeContextHandler extends DecreeContextHandlerType { +public interface DecreeContextHandler extends DirectorContextHandlerType { Map, DecreeContextHandler> contextHandlers = buildContextHandlers(); static Map, DecreeContextHandler> buildContextHandlers() { - return DecreeContextHandlers.buildOrEmpty( + return DirectorContextHandlers.buildOrEmpty( Iris.initialize("art.arcane.iris.util.decree.context"), DecreeContextHandler.class, h -> ((DecreeContextHandler) h).getType(), diff --git a/core/src/main/java/art/arcane/iris/util/common/decree/DecreeExecutor.java b/core/src/main/java/art/arcane/iris/util/common/decree/DecreeExecutor.java index b672b8e9b..17269dfc7 100644 --- a/core/src/main/java/art/arcane/iris/util/common/decree/DecreeExecutor.java +++ b/core/src/main/java/art/arcane/iris/util/common/decree/DecreeExecutor.java @@ -18,7 +18,7 @@ package art.arcane.iris.util.decree; -import art.arcane.volmlib.util.decree.DecreeExecutorBase; +import art.arcane.volmlib.util.director.DirectorExecutorBase; import art.arcane.iris.core.loader.IrisData; import art.arcane.iris.core.tools.IrisToolbelt; import art.arcane.iris.engine.framework.Engine; @@ -26,7 +26,7 @@ import art.arcane.iris.engine.platform.PlatformChunkGenerator; import art.arcane.iris.util.plugin.VolmitSender; import org.bukkit.entity.Player; -public interface DecreeExecutor extends DecreeExecutorBase { +public interface DecreeExecutor extends DirectorExecutorBase { default VolmitSender sender() { return DecreeContext.get(); } diff --git a/core/src/main/java/art/arcane/iris/util/common/decree/DecreeParameterHandler.java b/core/src/main/java/art/arcane/iris/util/common/decree/DecreeParameterHandler.java index d86b0a54a..833216f19 100644 --- a/core/src/main/java/art/arcane/iris/util/common/decree/DecreeParameterHandler.java +++ b/core/src/main/java/art/arcane/iris/util/common/decree/DecreeParameterHandler.java @@ -1,5 +1,5 @@ package art.arcane.iris.util.decree; -public interface DecreeParameterHandler - extends DecreeExecutor, art.arcane.volmlib.util.decree.DecreeParameterHandler { +public interface DirectorParameterHandler + extends DecreeExecutor, art.arcane.volmlib.util.director.DirectorParameterHandler { } diff --git a/core/src/main/java/art/arcane/iris/util/common/decree/DecreeSystem.java b/core/src/main/java/art/arcane/iris/util/common/decree/DecreeSystem.java index 7f95eb956..4b499433a 100644 --- a/core/src/main/java/art/arcane/iris/util/common/decree/DecreeSystem.java +++ b/core/src/main/java/art/arcane/iris/util/common/decree/DecreeSystem.java @@ -18,11 +18,11 @@ package art.arcane.iris.util.decree; -import art.arcane.volmlib.util.decree.DecreeSystemSupport; +import art.arcane.volmlib.util.director.DirectorSystemSupport; import art.arcane.iris.Iris; import art.arcane.volmlib.util.collection.KList; public final class DecreeSystem { - public static final KList> handlers = Iris.initialize("art.arcane.iris.util.decree.handlers", null).convert((i) -> (DecreeParameterHandler) i); + public static final KList> handlers = Iris.initialize("art.arcane.iris.util.decree.handlers", null).convert((i) -> (DirectorParameterHandler) i); private DecreeSystem() { } @@ -31,15 +31,15 @@ public final class DecreeSystem { * Get the handler for the specified type * * @param type The type to handle - * @return The corresponding {@link DecreeParameterHandler}, or null + * @return The corresponding {@link DirectorParameterHandler}, or null */ - public static DecreeParameterHandler getHandler(Class type) { - DecreeParameterHandler handler = DecreeSystemSupport.getHandler(handlers, type, (h, t) -> h.supports(t)); + public static DirectorParameterHandler getHandler(Class type) { + DirectorParameterHandler handler = DirectorSystemSupport.getHandler(handlers, type, (h, t) -> h.supports(t)); if (handler != null) { return handler; } - Iris.error("Unhandled type in Decree Parameter: " + type.getName() + ". This is bad!"); + Iris.error("Unhandled type in Director Parameter: " + type.getName() + ". This is bad!"); return null; } } diff --git a/core/src/main/java/art/arcane/iris/util/common/decree/context/WorldContextHandler.java b/core/src/main/java/art/arcane/iris/util/common/decree/context/WorldContextHandler.java index 41ef553c9..b2cfc9c3b 100644 --- a/core/src/main/java/art/arcane/iris/util/common/decree/context/WorldContextHandler.java +++ b/core/src/main/java/art/arcane/iris/util/common/decree/context/WorldContextHandler.java @@ -1,6 +1,6 @@ package art.arcane.iris.util.decree.context; -import art.arcane.volmlib.util.decree.context.WorldContextHandlerBase; +import art.arcane.volmlib.util.director.context.WorldContextHandlerBase; import art.arcane.iris.util.decree.DecreeContextHandler; import art.arcane.iris.util.plugin.VolmitSender; import org.bukkit.World; diff --git a/core/src/main/java/art/arcane/iris/util/common/decree/handlers/BlockVectorHandler.java b/core/src/main/java/art/arcane/iris/util/common/decree/handlers/BlockVectorHandler.java index fc3cb1213..cd4c8c78a 100644 --- a/core/src/main/java/art/arcane/iris/util/common/decree/handlers/BlockVectorHandler.java +++ b/core/src/main/java/art/arcane/iris/util/common/decree/handlers/BlockVectorHandler.java @@ -1,9 +1,9 @@ package art.arcane.iris.util.decree.handlers; import art.arcane.iris.util.decree.DecreeContext; -import art.arcane.iris.util.decree.DecreeParameterHandler; +import art.arcane.iris.util.decree.DirectorParameterHandler; import art.arcane.iris.util.decree.DecreeSystem; -import art.arcane.volmlib.util.decree.handlers.base.BlockVectorHandlerBase; +import art.arcane.volmlib.util.director.handlers.base.BlockVectorHandlerBase; import art.arcane.volmlib.util.format.Form; import org.bukkit.FluidCollisionMode; import org.bukkit.entity.Player; @@ -11,7 +11,7 @@ import org.bukkit.util.BlockVector; import java.util.List; -public class BlockVectorHandler extends BlockVectorHandlerBase implements DecreeParameterHandler { +public class BlockVectorHandler extends BlockVectorHandlerBase implements DirectorParameterHandler { @Override protected boolean isSenderPlayer() { return DecreeContext.get().isPlayer(); diff --git a/core/src/main/java/art/arcane/iris/util/common/decree/handlers/BooleanHandler.java b/core/src/main/java/art/arcane/iris/util/common/decree/handlers/BooleanHandler.java index db47fe4ea..ef6631a69 100644 --- a/core/src/main/java/art/arcane/iris/util/common/decree/handlers/BooleanHandler.java +++ b/core/src/main/java/art/arcane/iris/util/common/decree/handlers/BooleanHandler.java @@ -1,7 +1,7 @@ package art.arcane.iris.util.decree.handlers; -import art.arcane.volmlib.util.decree.handlers.base.BooleanHandlerBase; -import art.arcane.iris.util.decree.DecreeParameterHandler; +import art.arcane.volmlib.util.director.handlers.base.BooleanHandlerBase; +import art.arcane.iris.util.decree.DirectorParameterHandler; -public class BooleanHandler extends BooleanHandlerBase implements DecreeParameterHandler { +public class BooleanHandler extends BooleanHandlerBase implements DirectorParameterHandler { } diff --git a/core/src/main/java/art/arcane/iris/util/common/decree/handlers/ByteHandler.java b/core/src/main/java/art/arcane/iris/util/common/decree/handlers/ByteHandler.java index b396a2617..c61ba3ec7 100644 --- a/core/src/main/java/art/arcane/iris/util/common/decree/handlers/ByteHandler.java +++ b/core/src/main/java/art/arcane/iris/util/common/decree/handlers/ByteHandler.java @@ -1,7 +1,7 @@ package art.arcane.iris.util.decree.handlers; -import art.arcane.volmlib.util.decree.handlers.base.ByteHandlerBase; -import art.arcane.iris.util.decree.DecreeParameterHandler; +import art.arcane.volmlib.util.director.handlers.base.ByteHandlerBase; +import art.arcane.iris.util.decree.DirectorParameterHandler; -public class ByteHandler extends ByteHandlerBase implements DecreeParameterHandler { +public class ByteHandler extends ByteHandlerBase implements DirectorParameterHandler { } diff --git a/core/src/main/java/art/arcane/iris/util/common/decree/handlers/DataVersionHandler.java b/core/src/main/java/art/arcane/iris/util/common/decree/handlers/DataVersionHandler.java index 3fd03a9f8..909a2d3bd 100644 --- a/core/src/main/java/art/arcane/iris/util/common/decree/handlers/DataVersionHandler.java +++ b/core/src/main/java/art/arcane/iris/util/common/decree/handlers/DataVersionHandler.java @@ -2,10 +2,10 @@ package art.arcane.iris.util.decree.handlers; import art.arcane.iris.core.nms.datapack.DataVersion; import art.arcane.volmlib.util.collection.KList; -import art.arcane.iris.util.decree.DecreeParameterHandler; -import art.arcane.volmlib.util.decree.exceptions.DecreeParsingException; +import art.arcane.iris.util.decree.DirectorParameterHandler; +import art.arcane.volmlib.util.director.exceptions.DirectorParsingException; -public class DataVersionHandler implements DecreeParameterHandler { +public class DataVersionHandler implements DirectorParameterHandler { @Override public KList getPossibilities() { return new KList<>(DataVersion.values()).qdel(DataVersion.UNSUPPORTED); @@ -17,7 +17,7 @@ public class DataVersionHandler implements DecreeParameterHandler { } @Override - public DataVersion parse(String in, boolean force) throws DecreeParsingException { + public DataVersion parse(String in, boolean force) throws DirectorParsingException { if (in.equalsIgnoreCase("latest")) { return DataVersion.getLatest(); } @@ -26,7 +26,7 @@ public class DataVersionHandler implements DecreeParameterHandler { return v; } } - throw new DecreeParsingException("Unable to parse data version \"" + in + "\""); + throw new DirectorParsingException("Unable to parse data version \"" + in + "\""); } @Override diff --git a/core/src/main/java/art/arcane/iris/util/common/decree/handlers/DimensionHandler.java b/core/src/main/java/art/arcane/iris/util/common/decree/handlers/DimensionHandler.java index 8c81e1a52..efd307782 100644 --- a/core/src/main/java/art/arcane/iris/util/common/decree/handlers/DimensionHandler.java +++ b/core/src/main/java/art/arcane/iris/util/common/decree/handlers/DimensionHandler.java @@ -22,7 +22,7 @@ import art.arcane.iris.core.IrisSettings; import art.arcane.iris.core.tools.IrisToolbelt; import art.arcane.iris.engine.object.IrisDimension; import art.arcane.volmlib.util.collection.KList; -import art.arcane.volmlib.util.decree.exceptions.DecreeParsingException; +import art.arcane.volmlib.util.director.exceptions.DirectorParsingException; import art.arcane.iris.util.decree.specialhandlers.RegistrantHandler; import java.util.Locale; @@ -33,7 +33,7 @@ public class DimensionHandler extends RegistrantHandler { } @Override - public IrisDimension parse(String in, boolean force) throws DecreeParsingException { + public IrisDimension parse(String in, boolean force) throws DirectorParsingException { String key = in.trim(); if (key.equalsIgnoreCase("default")) { key = IrisSettings.get().getGenerator().getDefaultWorldType(); @@ -41,7 +41,7 @@ public class DimensionHandler extends RegistrantHandler { try { return super.parse(key, force); - } catch (DecreeParsingException ignored) { + } catch (DirectorParsingException ignored) { String normalized = key.toLowerCase(Locale.ROOT); IrisDimension resolved = IrisToolbelt.getDimension(normalized); if (resolved != null) { diff --git a/core/src/main/java/art/arcane/iris/util/common/decree/handlers/DoubleHandler.java b/core/src/main/java/art/arcane/iris/util/common/decree/handlers/DoubleHandler.java index c3581172a..6f1eff983 100644 --- a/core/src/main/java/art/arcane/iris/util/common/decree/handlers/DoubleHandler.java +++ b/core/src/main/java/art/arcane/iris/util/common/decree/handlers/DoubleHandler.java @@ -1,7 +1,7 @@ package art.arcane.iris.util.decree.handlers; -import art.arcane.volmlib.util.decree.handlers.base.DoubleHandlerBase; -import art.arcane.iris.util.decree.DecreeParameterHandler; +import art.arcane.volmlib.util.director.handlers.base.DoubleHandlerBase; +import art.arcane.iris.util.decree.DirectorParameterHandler; -public class DoubleHandler extends DoubleHandlerBase implements DecreeParameterHandler { +public class DoubleHandler extends DoubleHandlerBase implements DirectorParameterHandler { } diff --git a/core/src/main/java/art/arcane/iris/util/common/decree/handlers/FloatHandler.java b/core/src/main/java/art/arcane/iris/util/common/decree/handlers/FloatHandler.java index 23b57eb2c..87e214308 100644 --- a/core/src/main/java/art/arcane/iris/util/common/decree/handlers/FloatHandler.java +++ b/core/src/main/java/art/arcane/iris/util/common/decree/handlers/FloatHandler.java @@ -1,7 +1,7 @@ package art.arcane.iris.util.decree.handlers; -import art.arcane.volmlib.util.decree.handlers.base.FloatHandlerBase; -import art.arcane.iris.util.decree.DecreeParameterHandler; +import art.arcane.volmlib.util.director.handlers.base.FloatHandlerBase; +import art.arcane.iris.util.decree.DirectorParameterHandler; -public class FloatHandler extends FloatHandlerBase implements DecreeParameterHandler { +public class FloatHandler extends FloatHandlerBase implements DirectorParameterHandler { } diff --git a/core/src/main/java/art/arcane/iris/util/common/decree/handlers/IntegerHandler.java b/core/src/main/java/art/arcane/iris/util/common/decree/handlers/IntegerHandler.java index 91e8bc92b..2fa88b88a 100644 --- a/core/src/main/java/art/arcane/iris/util/common/decree/handlers/IntegerHandler.java +++ b/core/src/main/java/art/arcane/iris/util/common/decree/handlers/IntegerHandler.java @@ -1,7 +1,7 @@ package art.arcane.iris.util.decree.handlers; -import art.arcane.volmlib.util.decree.handlers.base.IntegerHandlerBase; -import art.arcane.iris.util.decree.DecreeParameterHandler; +import art.arcane.volmlib.util.director.handlers.base.IntegerHandlerBase; +import art.arcane.iris.util.decree.DirectorParameterHandler; -public class IntegerHandler extends IntegerHandlerBase implements DecreeParameterHandler { +public class IntegerHandler extends IntegerHandlerBase implements DirectorParameterHandler { } diff --git a/core/src/main/java/art/arcane/iris/util/common/decree/handlers/LongHandler.java b/core/src/main/java/art/arcane/iris/util/common/decree/handlers/LongHandler.java index e980204fb..cf175ce23 100644 --- a/core/src/main/java/art/arcane/iris/util/common/decree/handlers/LongHandler.java +++ b/core/src/main/java/art/arcane/iris/util/common/decree/handlers/LongHandler.java @@ -1,7 +1,7 @@ package art.arcane.iris.util.decree.handlers; -import art.arcane.volmlib.util.decree.handlers.base.LongHandlerBase; -import art.arcane.iris.util.decree.DecreeParameterHandler; +import art.arcane.volmlib.util.director.handlers.base.LongHandlerBase; +import art.arcane.iris.util.decree.DirectorParameterHandler; -public class LongHandler extends LongHandlerBase implements DecreeParameterHandler { +public class LongHandler extends LongHandlerBase implements DirectorParameterHandler { } diff --git a/core/src/main/java/art/arcane/iris/util/common/decree/handlers/PlayerHandler.java b/core/src/main/java/art/arcane/iris/util/common/decree/handlers/PlayerHandler.java index 4cb3efef4..7bded1c1f 100644 --- a/core/src/main/java/art/arcane/iris/util/common/decree/handlers/PlayerHandler.java +++ b/core/src/main/java/art/arcane/iris/util/common/decree/handlers/PlayerHandler.java @@ -18,9 +18,9 @@ package art.arcane.iris.util.decree.handlers; -import art.arcane.volmlib.util.decree.handlers.base.BukkitPlayerHandlerBase; -import art.arcane.iris.util.decree.DecreeParameterHandler; +import art.arcane.volmlib.util.director.handlers.base.BukkitPlayerHandlerBase; +import art.arcane.iris.util.decree.DirectorParameterHandler; import org.bukkit.entity.Player; -public class PlayerHandler extends BukkitPlayerHandlerBase implements DecreeParameterHandler { +public class PlayerHandler extends BukkitPlayerHandlerBase implements DirectorParameterHandler { } diff --git a/core/src/main/java/art/arcane/iris/util/common/decree/handlers/ShortHandler.java b/core/src/main/java/art/arcane/iris/util/common/decree/handlers/ShortHandler.java index 508120542..517529882 100644 --- a/core/src/main/java/art/arcane/iris/util/common/decree/handlers/ShortHandler.java +++ b/core/src/main/java/art/arcane/iris/util/common/decree/handlers/ShortHandler.java @@ -1,7 +1,7 @@ package art.arcane.iris.util.decree.handlers; -import art.arcane.volmlib.util.decree.handlers.base.ShortHandlerBase; -import art.arcane.iris.util.decree.DecreeParameterHandler; +import art.arcane.volmlib.util.director.handlers.base.ShortHandlerBase; +import art.arcane.iris.util.decree.DirectorParameterHandler; -public class ShortHandler extends ShortHandlerBase implements DecreeParameterHandler { +public class ShortHandler extends ShortHandlerBase implements DirectorParameterHandler { } diff --git a/core/src/main/java/art/arcane/iris/util/common/decree/handlers/StringHandler.java b/core/src/main/java/art/arcane/iris/util/common/decree/handlers/StringHandler.java index cd3a9f441..5564c8728 100644 --- a/core/src/main/java/art/arcane/iris/util/common/decree/handlers/StringHandler.java +++ b/core/src/main/java/art/arcane/iris/util/common/decree/handlers/StringHandler.java @@ -1,7 +1,7 @@ package art.arcane.iris.util.decree.handlers; -import art.arcane.volmlib.util.decree.handlers.StringHandlerBase; -import art.arcane.iris.util.decree.DecreeParameterHandler; +import art.arcane.volmlib.util.director.handlers.StringHandlerBase; +import art.arcane.iris.util.decree.DirectorParameterHandler; -public class StringHandler extends StringHandlerBase implements DecreeParameterHandler { +public class StringHandler extends StringHandlerBase implements DirectorParameterHandler { } diff --git a/core/src/main/java/art/arcane/iris/util/common/decree/handlers/VectorHandler.java b/core/src/main/java/art/arcane/iris/util/common/decree/handlers/VectorHandler.java index c70b3fa15..906e5910f 100644 --- a/core/src/main/java/art/arcane/iris/util/common/decree/handlers/VectorHandler.java +++ b/core/src/main/java/art/arcane/iris/util/common/decree/handlers/VectorHandler.java @@ -1,9 +1,9 @@ package art.arcane.iris.util.decree.handlers; import art.arcane.iris.util.decree.DecreeContext; -import art.arcane.iris.util.decree.DecreeParameterHandler; +import art.arcane.iris.util.decree.DirectorParameterHandler; import art.arcane.iris.util.decree.DecreeSystem; -import art.arcane.volmlib.util.decree.handlers.base.VectorHandlerBase; +import art.arcane.volmlib.util.director.handlers.base.VectorHandlerBase; import art.arcane.volmlib.util.format.Form; import org.bukkit.FluidCollisionMode; import org.bukkit.entity.Player; @@ -11,7 +11,7 @@ import org.bukkit.util.Vector; import java.util.List; -public class VectorHandler extends VectorHandlerBase implements DecreeParameterHandler { +public class VectorHandler extends VectorHandlerBase implements DirectorParameterHandler { @Override protected boolean isSenderPlayer() { return DecreeContext.get().isPlayer(); diff --git a/core/src/main/java/art/arcane/iris/util/common/decree/handlers/WorldHandler.java b/core/src/main/java/art/arcane/iris/util/common/decree/handlers/WorldHandler.java index 9eabf70cb..932109dc6 100644 --- a/core/src/main/java/art/arcane/iris/util/common/decree/handlers/WorldHandler.java +++ b/core/src/main/java/art/arcane/iris/util/common/decree/handlers/WorldHandler.java @@ -1,10 +1,10 @@ package art.arcane.iris.util.decree.handlers; -import art.arcane.volmlib.util.decree.handlers.base.WorldHandlerBase; -import art.arcane.iris.util.decree.DecreeParameterHandler; +import art.arcane.volmlib.util.director.handlers.base.WorldHandlerBase; +import art.arcane.iris.util.decree.DirectorParameterHandler; import org.bukkit.World; -public class WorldHandler extends WorldHandlerBase implements DecreeParameterHandler { +public class WorldHandler extends WorldHandlerBase implements DirectorParameterHandler { @Override protected String excludedPrefix() { return "iris/"; diff --git a/core/src/main/java/art/arcane/iris/util/common/decree/specialhandlers/DummyHandler.java b/core/src/main/java/art/arcane/iris/util/common/decree/specialhandlers/DummyHandler.java index 297f32295..9a14a3909 100644 --- a/core/src/main/java/art/arcane/iris/util/common/decree/specialhandlers/DummyHandler.java +++ b/core/src/main/java/art/arcane/iris/util/common/decree/specialhandlers/DummyHandler.java @@ -1,7 +1,7 @@ package art.arcane.iris.util.decree.specialhandlers; -import art.arcane.volmlib.util.decree.handlers.base.DummyHandlerBase; -import art.arcane.iris.util.decree.DecreeParameterHandler; +import art.arcane.volmlib.util.director.handlers.base.DummyHandlerBase; +import art.arcane.iris.util.decree.DirectorParameterHandler; -public class DummyHandler extends DummyHandlerBase implements DecreeParameterHandler { +public class DummyHandler extends DummyHandlerBase implements DirectorParameterHandler { } diff --git a/core/src/main/java/art/arcane/iris/util/common/decree/specialhandlers/NullableDimensionHandler.java b/core/src/main/java/art/arcane/iris/util/common/decree/specialhandlers/NullableDimensionHandler.java index 460ba3c37..889ba7374 100644 --- a/core/src/main/java/art/arcane/iris/util/common/decree/specialhandlers/NullableDimensionHandler.java +++ b/core/src/main/java/art/arcane/iris/util/common/decree/specialhandlers/NullableDimensionHandler.java @@ -22,7 +22,7 @@ import art.arcane.iris.core.IrisSettings; import art.arcane.iris.core.tools.IrisToolbelt; import art.arcane.iris.engine.object.IrisDimension; import art.arcane.volmlib.util.collection.KList; -import art.arcane.volmlib.util.decree.exceptions.DecreeParsingException; +import art.arcane.volmlib.util.director.exceptions.DirectorParsingException; import java.util.Locale; @@ -32,7 +32,7 @@ public class NullableDimensionHandler extends RegistrantHandler { } @Override - public IrisDimension parse(String in, boolean force) throws DecreeParsingException { + public IrisDimension parse(String in, boolean force) throws DirectorParsingException { String key = in.trim(); if (key.equalsIgnoreCase("default")) { key = IrisSettings.get().getGenerator().getDefaultWorldType(); @@ -40,7 +40,7 @@ public class NullableDimensionHandler extends RegistrantHandler { try { return super.parse(key, force); - } catch (DecreeParsingException ignored) { + } catch (DirectorParsingException ignored) { String normalized = key.toLowerCase(Locale.ROOT); IrisDimension resolved = IrisToolbelt.getDimension(normalized); if (resolved != null) { diff --git a/core/src/main/java/art/arcane/iris/util/common/decree/specialhandlers/NullablePlayerHandler.java b/core/src/main/java/art/arcane/iris/util/common/decree/specialhandlers/NullablePlayerHandler.java index 06fc36abd..d0a4191a1 100644 --- a/core/src/main/java/art/arcane/iris/util/common/decree/specialhandlers/NullablePlayerHandler.java +++ b/core/src/main/java/art/arcane/iris/util/common/decree/specialhandlers/NullablePlayerHandler.java @@ -1,14 +1,14 @@ package art.arcane.iris.util.decree.specialhandlers; -import art.arcane.volmlib.util.decree.handlers.base.NullablePlayerHandlerBase; -import art.arcane.volmlib.util.decree.exceptions.DecreeParsingException; -import art.arcane.iris.util.decree.DecreeParameterHandler; +import art.arcane.volmlib.util.director.handlers.base.NullablePlayerHandlerBase; +import art.arcane.volmlib.util.director.exceptions.DirectorParsingException; +import art.arcane.iris.util.decree.DirectorParameterHandler; import art.arcane.iris.util.decree.handlers.PlayerHandler; import org.bukkit.entity.Player; -public class NullablePlayerHandler extends PlayerHandler implements DecreeParameterHandler { +public class NullablePlayerHandler extends PlayerHandler implements DirectorParameterHandler { @Override - public Player parse(String in, boolean force) throws DecreeParsingException { + public Player parse(String in, boolean force) throws DirectorParsingException { return NullablePlayerHandlerBase.parseNullable(this, in); } } diff --git a/core/src/main/java/art/arcane/iris/util/common/decree/specialhandlers/ObjectHandler.java b/core/src/main/java/art/arcane/iris/util/common/decree/specialhandlers/ObjectHandler.java index 00026cc0a..461d375a3 100644 --- a/core/src/main/java/art/arcane/iris/util/common/decree/specialhandlers/ObjectHandler.java +++ b/core/src/main/java/art/arcane/iris/util/common/decree/specialhandlers/ObjectHandler.java @@ -21,13 +21,13 @@ package art.arcane.iris.util.decree.specialhandlers; import art.arcane.iris.Iris; import art.arcane.iris.core.loader.IrisData; import art.arcane.volmlib.util.collection.KList; -import art.arcane.iris.util.decree.DecreeParameterHandler; -import art.arcane.volmlib.util.decree.exceptions.DecreeParsingException; +import art.arcane.iris.util.decree.DirectorParameterHandler; +import art.arcane.volmlib.util.director.exceptions.DirectorParsingException; import java.io.File; import java.util.stream.Collectors; -public class ObjectHandler implements DecreeParameterHandler { +public class ObjectHandler implements DirectorParameterHandler { @Override public KList getPossibilities() { KList p = new KList<>(); @@ -53,16 +53,16 @@ public class ObjectHandler implements DecreeParameterHandler { } @Override - public String parse(String in, boolean force) throws DecreeParsingException { + public String parse(String in, boolean force) throws DirectorParsingException { KList options = getPossibilities(in); if (options.isEmpty()) { - throw new DecreeParsingException("Unable to find Object \"" + in + "\""); + throw new DirectorParsingException("Unable to find Object \"" + in + "\""); } try { return options.stream().filter((i) -> toString(i).equalsIgnoreCase(in)).collect(Collectors.toList()).get(0); } catch (Throwable e) { - throw new DecreeParsingException("Unable to filter which Object \"" + in + "\""); + throw new DirectorParsingException("Unable to filter which Object \"" + in + "\""); } } diff --git a/core/src/main/java/art/arcane/iris/util/common/decree/specialhandlers/RegistrantHandler.java b/core/src/main/java/art/arcane/iris/util/common/decree/specialhandlers/RegistrantHandler.java index 80bbe48ba..a18aa5b50 100644 --- a/core/src/main/java/art/arcane/iris/util/common/decree/specialhandlers/RegistrantHandler.java +++ b/core/src/main/java/art/arcane/iris/util/common/decree/specialhandlers/RegistrantHandler.java @@ -4,14 +4,14 @@ import art.arcane.iris.Iris; import art.arcane.iris.core.loader.IrisData; import art.arcane.iris.core.loader.IrisRegistrant; import art.arcane.volmlib.util.collection.KList; -import art.arcane.iris.util.decree.DecreeParameterHandler; -import art.arcane.volmlib.util.decree.exceptions.DecreeParsingException; +import art.arcane.iris.util.decree.DirectorParameterHandler; +import art.arcane.volmlib.util.director.exceptions.DirectorParsingException; import java.io.File; import java.util.HashSet; import java.util.Set; -public abstract class RegistrantHandler implements DecreeParameterHandler { +public abstract class RegistrantHandler implements DirectorParameterHandler { private final Class type; private final String name; private final boolean nullable; @@ -54,19 +54,19 @@ public abstract class RegistrantHandler implements Dec } @Override - public T parse(String in, boolean force) throws DecreeParsingException { + public T parse(String in, boolean force) throws DirectorParsingException { if (in.equals("null") && nullable) { return null; } KList options = getPossibilities(in); if (options.isEmpty()) { - throw new DecreeParsingException("Unable to find " + name + " \"" + in + "\""); + throw new DirectorParsingException("Unable to find " + name + " \"" + in + "\""); } return options.stream() .filter((i) -> toString(i).equalsIgnoreCase(in)) .findFirst() - .orElseThrow(() -> new DecreeParsingException("Unable to filter which " + name + " \"" + in + "\"")); + .orElseThrow(() -> new DirectorParsingException("Unable to filter which " + name + " \"" + in + "\"")); } @Override diff --git a/core/src/main/java/art/arcane/iris/util/common/mantle/io/IOWorker.java b/core/src/main/java/art/arcane/iris/util/common/mantle/io/IOWorker.java index 83316505c..bebbb1572 100644 --- a/core/src/main/java/art/arcane/iris/util/common/mantle/io/IOWorker.java +++ b/core/src/main/java/art/arcane/iris/util/common/mantle/io/IOWorker.java @@ -54,9 +54,16 @@ public class IOWorker { public IOWorker(File root, int worldHeight) { this.worldHeight = worldHeight; - this.support = new IOWorkerSupport(root, 128, (name, millis) -> - Iris.debug("Acquired Channel for " + C.DARK_GREEN + name + C.RED + " in " + Form.duration(millis, 2)) - ); + this.support = new IOWorkerSupport(root, 128, (name, millis) -> { + String threadName = Thread.currentThread().getName(); + String msg = "Acquired Channel for " + C.DARK_GREEN + name + C.RED + " in " + Form.duration(millis, 2) + + C.GRAY + " thread=" + threadName; + if (millis >= 1000L) { + Iris.warn(msg); + } else { + Iris.debug(msg); + } + }); this.runtime = new IOWorkerRuntimeSupport(support, LZ4_CODEC); } diff --git a/core/src/main/java/art/arcane/iris/util/common/misc/RegenRuntime.java b/core/src/main/java/art/arcane/iris/util/common/misc/RegenRuntime.java new file mode 100644 index 000000000..8f51f4a36 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/util/common/misc/RegenRuntime.java @@ -0,0 +1,20 @@ +package art.arcane.iris.util.misc; + +public final class RegenRuntime { + private static final ThreadLocal RUN_ID = new ThreadLocal<>(); + + private RegenRuntime() { + } + + public static void setRunId(String runId) { + RUN_ID.set(runId); + } + + public static String getRunId() { + return RUN_ID.get(); + } + + public static void clear() { + RUN_ID.remove(); + } +} diff --git a/core/src/main/kotlin/art/arcane/iris/core/scripting/kotlin/runner/Utils.kt b/core/src/main/kotlin/art/arcane/iris/core/scripting/kotlin/runner/Utils.kt index 779d7a25c..9efee1fde 100644 --- a/core/src/main/kotlin/art/arcane/iris/core/scripting/kotlin/runner/Utils.kt +++ b/core/src/main/kotlin/art/arcane/iris/core/scripting/kotlin/runner/Utils.kt @@ -11,7 +11,6 @@ import kotlin.script.experimental.dependencies.Repository import kotlin.script.experimental.dependencies.addRepository import kotlin.script.experimental.dependencies.impl.SimpleExternalDependenciesResolverOptionsParser import kotlin.script.experimental.jvm.JvmDependency -import kotlin.script.experimental.jvm.JvmDependencyFromClassLoader import kotlin.script.experimental.jvm.updateClasspath import kotlin.script.experimental.jvm.util.classpathFromClassloader import kotlin.script.experimental.util.PropertiesCollection @@ -193,7 +192,7 @@ private fun ResultWithDiagnostics.appendReports(reports : Collection) { files.forEach { addURL(it.toURI().toURL()) } @@ -205,7 +204,10 @@ internal fun ScriptCompilationConfiguration.Builder.configure() { beforeParsing { context -> try { context.compilationConfiguration.with { if (context.compilationConfiguration[ScriptCompilationConfiguration.server] ?: false) { - ScriptCompilationConfiguration.dependencies.append(this[ScriptCompilationConfiguration.sharedClassloader]!!.dependency) + val sharedClasspath = this[ScriptCompilationConfiguration.sharedClassloader]!!.classpath + if (sharedClasspath.isNotEmpty()) { + ScriptCompilationConfiguration.dependencies.append(JvmDependency(sharedClasspath)) + } } }.asSuccess() } catch (e: Throwable) { @@ -214,4 +216,4 @@ internal fun ScriptCompilationConfiguration.Builder.configure() { onAnnotations(DependsOn::class, Repository::class, handler = ::configureMavenDepsOnAnnotations) } -} \ No newline at end of file +} diff --git a/core/src/main/kotlin/art/arcane/iris/engine/mantle/MatterGenerator.kt b/core/src/main/kotlin/art/arcane/iris/engine/mantle/MatterGenerator.kt index 284b7083a..f165d35f3 100644 --- a/core/src/main/kotlin/art/arcane/iris/engine/mantle/MatterGenerator.kt +++ b/core/src/main/kotlin/art/arcane/iris/engine/mantle/MatterGenerator.kt @@ -1,9 +1,12 @@ package art.arcane.iris.engine.mantle +import art.arcane.iris.Iris import art.arcane.iris.core.IrisSettings import art.arcane.iris.core.nms.container.Pair import art.arcane.iris.engine.framework.Engine import art.arcane.iris.util.context.ChunkContext +import art.arcane.iris.util.misc.RegenRuntime +import art.arcane.iris.util.matter.TileWrapper import art.arcane.volmlib.util.documentation.ChunkCoordinates import art.arcane.iris.util.mantle.Mantle import art.arcane.volmlib.util.mantle.flag.MantleFlag @@ -11,7 +14,10 @@ import art.arcane.iris.util.parallel.MultiBurst import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking +import org.bukkit.block.data.BlockData +import java.util.concurrent.ConcurrentHashMap import kotlin.coroutines.EmptyCoroutineContext +import kotlin.math.min interface MatterGenerator { val engine: Engine @@ -24,34 +30,110 @@ interface MatterGenerator { fun generateMatter(x: Int, z: Int, multicore: Boolean, context: ChunkContext) { if (!engine.dimension.isUseMantle) return val multicore = multicore || IrisSettings.get().generator.isUseMulticoreMantle + val threadName = Thread.currentThread().name + val regenThread = threadName.startsWith("Iris-Regen-") + val traceRegen = regenThread && IrisSettings.get().general.isDebug + val forceRegen = regenThread + val regenPassKey = if (forceRegen) resolveRegenPassKey(threadName) else null + val optimizedRegen = forceRegen && !IrisSettings.get().general.isDebug && regenPassKey != null + val writeRadius = if (optimizedRegen) min(radius, realRadius) else radius + val clearedChunks = if (optimizedRegen) getRegenPassSet(regenClearedChunksByPass, regenPassKey!!) else HashSet() + val plannedChunks = if (optimizedRegen) getRegenPassSet(regenPlannedChunksByPass, regenPassKey!!) else null - mantle.write(engine.mantle, x, z, radius, multicore).use { writer -> + if (optimizedRegen) { + touchRegenPass(regenPassKey!!) + } + + if (traceRegen) { + Iris.info("Regen matter start: center=$x,$z radius=$radius realRadius=$realRadius writeRadius=$writeRadius multicore=$multicore components=${components.size} optimized=$optimizedRegen passKey=${regenPassKey ?: "none"} thread=$threadName") + } + + mantle.write(engine.mantle, x, z, writeRadius, multicore).use { writer -> for (pair in components) { + val rawPassRadius = pair.b + val passRadius = if (optimizedRegen) min(rawPassRadius, writeRadius) else rawPassRadius + val passFlags = pair.a.joinToString(",") { it.flag.toString() } + val passFlagKey = if (optimizedRegen) "$regenPassKey|$passFlags" else null + val generatedChunks = if (passFlagKey != null) getRegenPassSet(regenGeneratedChunksByPass, passFlagKey) else null + var visitedChunks = 0 + var clearedCount = 0 + var plannedSkipped = 0 + var componentSkipped = 0 + var componentForcedReset = 0 + var launchedLayers = 0 + var dedupSkipped = 0 + + if (passFlagKey != null) { + touchRegenPass(passFlagKey) + } + if (traceRegen) { + Iris.info("Regen matter pass start: center=$x,$z passRadius=$passRadius rawPassRadius=$rawPassRadius flags=[$passFlags]") + } + runBlocking { - radius(x, z, pair.b) { x, z -> - val mc = writer.acquireChunk(x, z) - if (mc.isFlagged(MantleFlag.PLANNED)) + radius(x, z, passRadius) { passX, passZ -> + visitedChunks++ + val passKey = chunkKey(passX, passZ) + if (generatedChunks != null && !generatedChunks.add(passKey)) { + dedupSkipped++ return@radius + } + + val mc = writer.acquireChunk(passX, passZ) + if (forceRegen) { + if (clearedChunks.add(passKey)) { + mc.deleteSlices(BlockData::class.java) + mc.deleteSlices(String::class.java) + mc.deleteSlices(TileWrapper::class.java) + mc.flag(MantleFlag.PLANNED, false) + clearedCount++ + } + } + + if (!forceRegen && mc.isFlagged(MantleFlag.PLANNED)) { + plannedSkipped++ + return@radius + } for (c in pair.a) { - if (mc.isFlagged(c.flag)) + if (!forceRegen && mc.isFlagged(c.flag)) { + componentSkipped++ continue + } + if (forceRegen && mc.isFlagged(c.flag)) { + mc.flag(c.flag, false) + componentForcedReset++ + } + + launchedLayers++ launch(multicore) { mc.raiseFlagSuspend(c.flag) { - c.generateLayer(writer, x, z, context) + c.generateLayer(writer, passX, passZ, context) } } } } } + + if (traceRegen) { + Iris.info("Regen matter pass done: center=$x,$z passRadius=$passRadius rawPassRadius=$rawPassRadius visited=$visitedChunks cleared=$clearedCount dedupSkipped=$dedupSkipped plannedSkipped=$plannedSkipped componentSkipped=$componentSkipped componentForcedReset=$componentForcedReset launchedLayers=$launchedLayers flags=[$passFlags]") + } } - radius(x, z, realRadius) { x, z -> - writer.acquireChunk(x, z) + radius(x, z, realRadius) { realX, realZ -> + val realKey = chunkKey(realX, realZ) + if (plannedChunks != null && !plannedChunks.add(realKey)) { + return@radius + } + writer.acquireChunk(realX, realZ) .flag(MantleFlag.PLANNED, true) } } + + if (traceRegen) { + Iris.info("Regen matter done: center=$x,$z markedRealRadius=$realRadius forceRegen=$forceRegen") + } } private inline fun radius(x: Int, z: Int, radius: Int, crossinline task: (Int, Int) -> Unit) { @@ -64,7 +146,59 @@ interface MatterGenerator { companion object { private val dispatcher = MultiBurst.burst.dispatcher//.limitedParallelism(128, "Mantle") + private const val regenPassCacheTtlMs = 600000L + private val regenGeneratedChunksByPass = ConcurrentHashMap>() + private val regenClearedChunksByPass = ConcurrentHashMap>() + private val regenPlannedChunksByPass = ConcurrentHashMap>() + private val regenPassTouchedMs = ConcurrentHashMap() + private fun CoroutineScope.launch(multicore: Boolean, block: suspend CoroutineScope.() -> Unit) = launch(if (multicore) dispatcher else EmptyCoroutineContext, block = block) + + private fun chunkKey(x: Int, z: Int): Long { + return (x.toLong() shl 32) xor (z.toLong() and 0xffffffffL) + } + + private fun getRegenPassSet(store: ConcurrentHashMap>, passKey: String): MutableSet { + return store.computeIfAbsent(passKey) { ConcurrentHashMap.newKeySet() } + } + + private fun resolveRegenPassKey(threadName: String): String? { + val runtimeKey = RegenRuntime.getRunId() + if (!runtimeKey.isNullOrBlank()) { + return runtimeKey + } + if (!threadName.startsWith("Iris-Regen-")) { + return null + } + + val suffix = threadName.substring("Iris-Regen-".length) + val lastDash = suffix.lastIndexOf('-') + if (lastDash <= 0) { + return suffix + } + return suffix.substring(0, lastDash) + } + + private fun touchRegenPass(passKey: String) { + val now = System.currentTimeMillis() + regenPassTouchedMs[passKey] = now + if (regenPassTouchedMs.size <= 64) { + return + } + + val iterator = regenPassTouchedMs.entries.iterator() + while (iterator.hasNext()) { + val entry = iterator.next() + if (now - entry.value <= regenPassCacheTtlMs) { + continue + } + val key = entry.key + iterator.remove() + regenGeneratedChunksByPass.remove(key) + regenClearedChunksByPass.remove(key) + regenPlannedChunksByPass.remove(key) + } + } } -} \ No newline at end of file +} diff --git a/settings.gradle.kts b/settings.gradle.kts index 77a1d78eb..b0b1c0b54 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -15,12 +15,30 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ +import java.io.File + plugins { id ("org.gradle.toolchains.foojay-resolver-convention") version "1.0.0" } rootProject.name = "Iris" +val useLocalVolmLib: Boolean = providers.gradleProperty("useLocalVolmLib") + .orElse("true") + .map { value: String -> value.equals("true", ignoreCase = true) } + .get() +val localVolmLibDirectory: File = file("../VolmLib") + +if (useLocalVolmLib && localVolmLibDirectory.resolve("settings.gradle.kts").exists()) { + includeBuild(localVolmLibDirectory) { + dependencySubstitution { + substitute(module("com.github.VolmitSoftware:VolmLib")).using(project(":shared")) + substitute(module("com.github.VolmitSoftware.VolmLib:shared")).using(project(":shared")) + substitute(module("com.github.VolmitSoftware.VolmLib:volmlib-shared")).using(project(":shared")) + } + } +} + include(":core", ":core:agent") include( ":nms:v1_21_R7", @@ -34,4 +52,4 @@ include( ":nms:v1_20_R3", ":nms:v1_20_R2", ":nms:v1_20_R1", -) \ No newline at end of file +)