canvas checks

This commit is contained in:
Brian Neumann-Fopiano
2026-02-17 22:44:01 -05:00
parent 05d79b6d40
commit ba819e4899
9 changed files with 211 additions and 29 deletions
@@ -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();
@@ -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();
@@ -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<Boolean> 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<Boolean> 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<Boolean> 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<Boolean> callbackFuture = new CompletableFuture<>();
Runnable invokeTask = () -> {
Consumer<Boolean> 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<Void> 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;
}
@@ -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");
}
@@ -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<Integer> list) {
return Collections.max(list);
}
}
}
@@ -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())
}
}
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
}
}
@@ -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 <T> MutableList<T>.addAll(vararg values: T) = values.forEach(this::add)
fun task(action: () -> ValueWithDiagnostics<Mode>) = PropertyDelegateProvider<Any?, ReadOnlyProperty<Any?, Task>> { _, _ ->