From ba819e48998831c5070afecec9d37836259ad8c4 Mon Sep 17 00:00:00 2001 From: Brian Neumann-Fopiano Date: Tue, 17 Feb 2026 22:44:01 -0500 Subject: [PATCH] canvas checks --- build.gradle.kts | 3 +- core/build.gradle.kts | 1 + core/src/main/java/art/arcane/iris/Iris.java | 1 - .../iris/core/commands/CommandIris.java | 11 +- .../iris/core/link/FoliaWorldsLink.java | 101 +++++++++++++++--- .../arcane/iris/core/project/IrisProject.java | 37 ++++++- .../iris/core/tools/IrisPackBenchmarking.java | 5 +- .../art/arcane/iris/core/safeguard/Mode.kt | 59 +++++++++- .../arcane/iris/core/safeguard/task/Tasks.kt | 22 +++- 9 files changed, 211 insertions(+), 29 deletions(-) diff --git a/build.gradle.kts b/build.gradle.kts index f05121a60..0d6e26f4d 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -54,7 +54,7 @@ registerCustomOutputTask("PixelFury", "C://Users/repix/workplace/Iris/1.21.3 - D registerCustomOutputTask("PixelFuryDev", "C://Users/repix/workplace/Iris/1.21 - Development-v3/plugins") // ========================== UNIX ============================== registerCustomOutputTaskUnix("CyberpwnLT", "/Users/danielmills/development/server/plugins") -registerCustomOutputTaskUnix("PsychoLT", "/Users/brianfopiano/Developer/RemoteGit/[Minecraft Server]/plugin-jars") +registerCustomOutputTaskUnix("PsychoLT", "/Users/brianfopiano/Developer/RemoteGit/[Minecraft Server]/consumers/plugin-consumers/dropins/plugins") registerCustomOutputTaskUnix("PixelMac", "/Users/test/Desktop/mcserver/plugins") registerCustomOutputTaskUnix("CrazyDev22LT", "/home/julian/Desktop/server/plugins") // ============================================================== @@ -228,6 +228,7 @@ allprojects { compileJava { options.compilerArgs.add("-parameters") options.encoding = "UTF-8" + options.debugOptions.debugLevel = "none" } javadoc { diff --git a/core/build.gradle.kts b/core/build.gradle.kts index 765a46cb4..fcc2c1f30 100644 --- a/core/build.gradle.kts +++ b/core/build.gradle.kts @@ -174,6 +174,7 @@ tasks { compileJava { options.compilerArgs.add("-parameters") options.encoding = "UTF-8" + options.debugOptions.debugLevel = "none" } /** diff --git a/core/src/main/java/art/arcane/iris/Iris.java b/core/src/main/java/art/arcane/iris/Iris.java index b417999fd..c671082dd 100644 --- a/core/src/main/java/art/arcane/iris/Iris.java +++ b/core/src/main/java/art/arcane/iris/Iris.java @@ -1004,7 +1004,6 @@ public class Iris extends VolmitPlugin implements Listener { } public void splash() { - Iris.info("Server type & version: " + Bukkit.getName() + " v" + Bukkit.getVersion()); Iris.info("Custom Biomes: " + INMS.get().countCustomBiomes()); printPacks(); 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 6410893b6..c89607746 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 @@ -21,6 +21,7 @@ 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.link.FoliaWorldsLink; import art.arcane.iris.core.loader.IrisData; import art.arcane.iris.core.nms.INMS; import art.arcane.iris.core.service.StudioSVC; @@ -545,7 +546,7 @@ public class CommandIris implements DirectorExecutor { return; } - if (!Bukkit.unloadWorld(world, false)) { + if (!FoliaWorldsLink.get().unloadWorld(world, false)) { sender().sendMessage(C.RED + "Failed to unload world: " + world.getName()); return; } @@ -1998,8 +1999,12 @@ public class CommandIris implements DirectorExecutor { sender().sendMessage(C.GREEN + "Unloading world: " + world.getName()); try { IrisToolbelt.evacuate(world); - Bukkit.unloadWorld(world, false); - sender().sendMessage(C.GREEN + "World unloaded successfully."); + boolean unloaded = FoliaWorldsLink.get().unloadWorld(world, false); + if (unloaded) { + sender().sendMessage(C.GREEN + "World unloaded successfully."); + } else { + sender().sendMessage(C.RED + "Failed to unload the world."); + } } catch (Exception e) { sender().sendMessage(C.RED + "Failed to unload the world: " + e.getMessage()); e.printStackTrace(); diff --git a/core/src/main/java/art/arcane/iris/core/link/FoliaWorldsLink.java b/core/src/main/java/art/arcane/iris/core/link/FoliaWorldsLink.java index e6e8312e3..0e601819c 100644 --- a/core/src/main/java/art/arcane/iris/core/link/FoliaWorldsLink.java +++ b/core/src/main/java/art/arcane/iris/core/link/FoliaWorldsLink.java @@ -24,6 +24,7 @@ import java.util.Locale; import java.util.Map; import java.util.concurrent.CompletableFuture; import java.util.concurrent.TimeUnit; +import java.util.function.Consumer; public class FoliaWorldsLink { private static volatile FoliaWorldsLink instance; @@ -136,6 +137,11 @@ public class FoliaWorldsLink { return false; } + CompletableFuture asyncWorldUnload = unloadWorldViaAsyncApi(world, save); + if (asyncWorldUnload != null) { + return resolveAsyncUnload(asyncWorldUnload); + } + try { return Bukkit.unloadWorld(world, save); } catch (UnsupportedOperationException unsupported) { @@ -158,6 +164,67 @@ public class FoliaWorldsLink { } } + private boolean resolveAsyncUnload(CompletableFuture asyncWorldUnload) { + if (J.isPrimaryThread()) { + if (!asyncWorldUnload.isDone()) { + return true; + } + + try { + return Boolean.TRUE.equals(asyncWorldUnload.join()); + } catch (Throwable e) { + throw new IllegalStateException("Failed to consume async world unload result.", unwrap(e)); + } + } + + try { + return Boolean.TRUE.equals(asyncWorldUnload.get(120, TimeUnit.SECONDS)); + } catch (Throwable e) { + throw new IllegalStateException("Failed while waiting for async world unload result.", unwrap(e)); + } + } + + private CompletableFuture unloadWorldViaAsyncApi(World world, boolean save) { + Object bukkitServer = Bukkit.getServer(); + if (bukkitServer == null) { + return null; + } + + Method unloadWorldAsyncMethod; + try { + unloadWorldAsyncMethod = bukkitServer.getClass().getMethod("unloadWorldAsync", World.class, boolean.class, Consumer.class); + } catch (Throwable ignored) { + return null; + } + + CompletableFuture callbackFuture = new CompletableFuture<>(); + Runnable invokeTask = () -> { + Consumer callback = result -> callbackFuture.complete(Boolean.TRUE.equals(result)); + try { + unloadWorldAsyncMethod.invoke(bukkitServer, world, save, callback); + } catch (Throwable e) { + callbackFuture.completeExceptionally(unwrap(e)); + } + }; + + if (J.isFolia() && !isGlobalTickThread()) { + CompletableFuture scheduled = J.sfut(invokeTask); + if (scheduled == null) { + callbackFuture.completeExceptionally(new IllegalStateException("Failed to schedule global world-unload task.")); + return callbackFuture; + } + scheduled.whenComplete((unused, throwable) -> { + if (throwable != null) { + callbackFuture.completeExceptionally(unwrap(throwable)); + } + }); + } else { + invokeTask.run(); + } + + return callbackFuture; + } + private boolean isWorldsProviderActive() { return provider != null && levelStemClass != null && generatorTypeClass != null; } @@ -457,17 +524,7 @@ public class FoliaWorldsLink { return; } - Server server = Bukkit.getServer(); - boolean globalThread = false; - if (server != null) { - try { - Method isGlobalTickThreadMethod = server.getClass().getMethod("isGlobalTickThread"); - globalThread = (boolean) isGlobalTickThreadMethod.invoke(server); - } catch (Throwable ignored) { - } - } - - if (globalThread) { + if (isGlobalTickThread()) { detachTask.run(); return; } @@ -479,9 +536,29 @@ public class FoliaWorldsLink { detachFuture.get(15, TimeUnit.SECONDS); } + private static boolean isGlobalTickThread() { + Server server = Bukkit.getServer(); + if (server == null) { + return false; + } + + try { + Method isGlobalTickThreadMethod = server.getClass().getMethod("isGlobalTickThread"); + return Boolean.TRUE.equals(isGlobalTickThreadMethod.invoke(server)); + } catch (Throwable ignored) { + return false; + } + } + private static Throwable unwrap(Throwable throwable) { if (throwable instanceof InvocationTargetException invocationTargetException && invocationTargetException.getCause() != null) { - return invocationTargetException.getCause(); + return unwrap(invocationTargetException.getCause()); + } + if (throwable instanceof java.util.concurrent.CompletionException completionException && completionException.getCause() != null) { + return unwrap(completionException.getCause()); + } + if (throwable instanceof java.util.concurrent.ExecutionException executionException && executionException.getCause() != null) { + return unwrap(executionException.getCause()); } return throwable; } diff --git a/core/src/main/java/art/arcane/iris/core/project/IrisProject.java b/core/src/main/java/art/arcane/iris/core/project/IrisProject.java index a45108b09..e0f1c9b42 100644 --- a/core/src/main/java/art/arcane/iris/core/project/IrisProject.java +++ b/core/src/main/java/art/arcane/iris/core/project/IrisProject.java @@ -60,6 +60,7 @@ import java.io.IOException; import java.util.Objects; import java.util.UUID; import java.util.concurrent.CompletableFuture; +import java.util.concurrent.TimeUnit; import java.util.function.Consumer; @SuppressWarnings("ALL") @@ -291,11 +292,45 @@ public class IrisProject { } } - J.attemptAsync(() -> IO.delete(folder)); + J.attemptAsync(() -> deleteStudioFolderWithRetry(folder, worldName)); Iris.debug("Closed Active Provider " + worldName); activeProvider = null; } + private static void deleteStudioFolderWithRetry(File folder, String worldName) { + if (folder == null) { + return; + } + + long unloadWaitDeadlineMs = System.currentTimeMillis() + TimeUnit.SECONDS.toMillis(20); + while (Bukkit.getWorld(worldName) != null && System.currentTimeMillis() < unloadWaitDeadlineMs) { + J.sleep(100); + } + + int attempts = 0; + while (folder.exists() && attempts < 40) { + IO.delete(folder); + if (!folder.exists()) { + return; + } + + attempts++; + J.sleep(250); + } + + if (!folder.exists()) { + return; + } + + try { + Iris.queueWorldDeletionOnStartup(java.util.Collections.singleton(worldName)); + Iris.warn("Queued deferred deletion for studio world folder \"" + worldName + "\"."); + } catch (IOException e) { + Iris.warn("Failed to queue deferred deletion for studio world folder \"" + worldName + "\"."); + Iris.reportError(e); + } + } + public File getCodeWorkspaceFile() { return new File(path, getName() + ".code-workspace"); } diff --git a/core/src/main/java/art/arcane/iris/core/tools/IrisPackBenchmarking.java b/core/src/main/java/art/arcane/iris/core/tools/IrisPackBenchmarking.java index 5cbbfbaa1..e5bebf9fe 100644 --- a/core/src/main/java/art/arcane/iris/core/tools/IrisPackBenchmarking.java +++ b/core/src/main/java/art/arcane/iris/core/tools/IrisPackBenchmarking.java @@ -2,6 +2,7 @@ package art.arcane.iris.core.tools; import art.arcane.iris.Iris; +import art.arcane.iris.core.link.FoliaWorldsLink; import art.arcane.iris.core.pregenerator.PregenTask; import art.arcane.iris.engine.framework.Engine; import art.arcane.iris.engine.object.IrisDimension; @@ -102,7 +103,7 @@ public class IrisPackBenchmarking { var world = Bukkit.getWorld("benchmark"); if (world == null) return; IrisToolbelt.evacuate(world); - Bukkit.unloadWorld(world, true); + FoliaWorldsLink.get().unloadWorld(world, true); }); stopwatch.end(); @@ -167,4 +168,4 @@ public class IrisPackBenchmarking { private int findHighest(KList list) { return Collections.max(list); } -} \ No newline at end of file +} diff --git a/core/src/main/kotlin/art/arcane/iris/core/safeguard/Mode.kt b/core/src/main/kotlin/art/arcane/iris/core/safeguard/Mode.kt index d2c4f5ed1..ef59807d9 100644 --- a/core/src/main/kotlin/art/arcane/iris/core/safeguard/Mode.kt +++ b/core/src/main/kotlin/art/arcane/iris/core/safeguard/Mode.kt @@ -5,6 +5,9 @@ import art.arcane.iris.Iris import art.arcane.iris.core.IrisSettings import art.arcane.iris.util.format.C import art.arcane.volmlib.util.format.Form +import org.bukkit.Bukkit +import java.time.LocalDate +import java.time.format.DateTimeFormatter enum class Mode(private val color: C) { STABLE(C.IRIS), @@ -34,6 +37,11 @@ enum class Mode(private val color: C) { fun splash() { val padd = Form.repeat(" ", 8) val padd2 = Form.repeat(" ", 4) + val version = Iris.instance.description.version + val releaseTrain = getReleaseTrain(version) + val serverVersion = getServerVersion() + val startupDate = getStartupDate() + val javaVersion = getJavaVersion() val splash = arrayOf( padd + C.GRAY + " @@@@@@@@@@@@@@" + C.DARK_GRAY + "@@@", @@ -51,14 +59,16 @@ enum class Mode(private val color: C) { val info = arrayOf( "", + padd2 + color + " Iris, " + C.AQUA + "Iris, Dimension Engine " + C.RED + "[" + releaseTrain + " RELEASE]", + padd2 + C.GRAY + " Version: " + color + version, + padd2 + C.GRAY + " By: " + color + "Arcane Arts (Volmit Software)", + padd2 + C.GRAY + " Server: " + color + serverVersion, + padd2 + C.GRAY + " Java: " + color + javaVersion + C.GRAY + " | Date: " + color + startupDate, + padd2 + C.GRAY + " Commit: " + color + BuildConstants.COMMIT + C.GRAY + "/" + color + BuildConstants.ENVIRONMENT, "", "", "", "", - padd2 + color + " Iris", - padd2 + C.GRAY + " by " + color + "Volmit Software", - padd2 + C.GRAY + " v" + color + Iris.instance.description.version, - padd2 + C.GRAY + " c" + color + BuildConstants.COMMIT + C.GRAY + "/" + color + BuildConstants.ENVIRONMENT, ) @@ -73,4 +83,43 @@ enum class Mode(private val color: C) { Iris.info(builder.toString()) } -} \ No newline at end of file + + private fun getServerVersion(): String { + var version = Bukkit.getVersion() + val mcMarkerIndex = version.indexOf(" (MC:") + if (mcMarkerIndex != -1) { + version = version.substring(0, mcMarkerIndex) + } + return version + } + + private fun getJavaVersion(): Int { + var version = System.getProperty("java.version") + if (version.startsWith("1.")) { + version = version.substring(2, 3) + } else { + val dot = version.indexOf(".") + if (dot != -1) { + version = version.substring(0, dot) + } + } + return version.toInt() + } + + private fun getStartupDate(): String { + return LocalDate.now().format(DateTimeFormatter.ISO_LOCAL_DATE) + } + + private fun getReleaseTrain(version: String): String { + var value = version + val suffixIndex = value.indexOf("-") + if (suffixIndex >= 0) { + value = value.substring(0, suffixIndex) + } + val split = value.split('.') + if (split.size >= 2) { + return split[0] + "." + split[1] + } + return value + } +} diff --git a/core/src/main/kotlin/art/arcane/iris/core/safeguard/task/Tasks.kt b/core/src/main/kotlin/art/arcane/iris/core/safeguard/task/Tasks.kt index 7707b157a..d0e27ca7a 100644 --- a/core/src/main/kotlin/art/arcane/iris/core/safeguard/task/Tasks.kt +++ b/core/src/main/kotlin/art/arcane/iris/core/safeguard/task/Tasks.kt @@ -56,6 +56,7 @@ private val incompatibilities by task { private val software by task { val supported = setOf( + "canvas", "folia", "purpur", "pufferfish", @@ -64,10 +65,10 @@ private val software by task { "bukkit" ) - if (supported.any { server.name.contains(it, true) }) STABLE.withDiagnostics() + if (isCanvasServer() || supported.any { server.name.contains(it, true) }) STABLE.withDiagnostics() else WARNING.withDiagnostics( WARN.create("Unsupported Server Software"), - WARN.create("- Please consider using Folia, Paper, or Purpur instead.") + WARN.create("- Please consider using Canvas, Folia, Paper, or Purpur instead.") ) } @@ -88,7 +89,7 @@ private val injection by task { WARNING.withDiagnostics( WARN.create("Java Agent"), WARN.create("- Skipping dynamic Java agent attach on Spigot/Bukkit to avoid runtime agent warnings."), - WARN.create("- For full runtime injection support, run with -javaagent:" + Agent.AGENT_JAR.path + " or use Paper/Purpur.") + WARN.create("- For full runtime injection support, run with -javaagent:" + Agent.AGENT_JAR.path + " or use Canvas/Folia/Paper/Purpur.") ) } else if (!Agent.install()) UNSTABLE.withDiagnostics( ERROR.create("Java Agent"), @@ -154,7 +155,20 @@ val tasks = listOf( private val server get() = Bukkit.getServer() private fun isPaperPreferredServer(): Boolean { val name = server.name.lowercase(Locale.ROOT) - return name.contains("folia") || name.contains("paper") || name.contains("purpur") || name.contains("pufferfish") + return isCanvasServer() + || name.contains("folia") + || name.contains("paper") + || name.contains("purpur") + || name.contains("pufferfish") +} +private fun isCanvasServer(): Boolean { + val loader: ClassLoader? = server.javaClass.classLoader + return try { + Class.forName("io.canvasmc.canvas.region.WorldRegionizer", false, loader) + true + } catch (_: Throwable) { + server.name.contains("canvas", true) + } } private fun MutableList.addAll(vararg values: T) = values.forEach(this::add) fun task(action: () -> ValueWithDiagnostics) = PropertyDelegateProvider> { _, _ ->