diff --git a/core/build.gradle.kts b/core/build.gradle.kts index d72d05f83..92c4a7fe4 100644 --- a/core/build.gradle.kts +++ b/core/build.gradle.kts @@ -2,6 +2,7 @@ import io.github.slimjar.func.slimjarHelper import io.github.slimjar.resolver.data.Mirror import org.ajoberstar.grgit.Grgit import org.gradle.jvm.toolchain.JavaLanguageVersion +import org.gradle.jvm.tasks.Jar import org.jetbrains.kotlin.gradle.dsl.JvmTarget import java.net.URI @@ -157,6 +158,8 @@ slimJar { relocate("com.github.benmanes.caffeine", "$lib.caffeine") } +val embeddedAgentJar = project(":core:agent").tasks.named("jar") + tasks { /** * We need parameter meta for the decree command system @@ -182,10 +185,14 @@ tasks { } shadowJar { + dependsOn(embeddedAgentJar) mergeServiceFiles() //minimize() relocate("io.github.slimjar", "$lib.slimjar") exclude("modules/loader-agent.isolated-jar") + from(embeddedAgentJar.map { it.archiveFile }) { + rename { "agent.jar" } + } } sentryCollectSourcesJava { diff --git a/core/src/main/java/art/arcane/iris/Iris.java b/core/src/main/java/art/arcane/iris/Iris.java index 1044c6f69..e7766e820 100644 --- a/core/src/main/java/art/arcane/iris/Iris.java +++ b/core/src/main/java/art/arcane/iris/Iris.java @@ -98,8 +98,10 @@ public class Iris extends VolmitPlugin implements Listener { static { try { InstanceState.updateInstanceId(); - } catch (Throwable ignored) { - + } catch (Throwable ex) { + System.err.println("[Iris] Failed to update instance id: " + ex.getClass().getSimpleName() + + (ex.getMessage() == null ? "" : " - " + ex.getMessage())); + ex.printStackTrace(); } } @@ -135,8 +137,12 @@ public class Iris extends VolmitPlugin implements Listener { if (slicedClass == null || i.isAnnotationPresent(slicedClass)) { try { v.add(i.getDeclaredConstructor().newInstance()); - } catch (Throwable ignored) { - + } catch (Throwable ex) { + Iris.warn("Skipped class initialization for %s: %s%s", + i.getName(), + ex.getClass().getSimpleName(), + ex.getMessage() == null ? "" : " - " + ex.getMessage()); + Iris.reportError(ex); } } } @@ -152,8 +158,12 @@ public class Iris extends VolmitPlugin implements Listener { if (slicedClass == null || i.isAnnotationPresent(slicedClass)) { try { v.add(i); - } catch (Throwable ignored) { - + } catch (Throwable ex) { + Iris.warn("Skipped class discovery entry for %s: %s%s", + i.getName(), + ex.getClass().getSimpleName(), + ex.getMessage() == null ? "" : " - " + ex.getMessage()); + Iris.reportError(ex); } } } @@ -181,8 +191,9 @@ public class Iris extends VolmitPlugin implements Listener { } catch (Throwable e) { try { instance.getLogger().info(instance.getTag() + string.replaceAll("(<([^>]+)>)", "")); - } catch (Throwable ignored1) { - + } catch (Throwable inner) { + System.err.println("[Iris] Failed to emit log message: " + inner.getMessage()); + inner.printStackTrace(System.err); } } } @@ -307,8 +318,7 @@ public class Iris extends VolmitPlugin implements Listener { @SuppressWarnings("deprecation") public static void later(NastyRunnable object) { try { - Bukkit.getScheduler().scheduleAsyncDelayedTask(instance, () -> - { + J.a(() -> { try { object.run(); } catch (Throwable e) { @@ -316,8 +326,10 @@ public class Iris extends VolmitPlugin implements Listener { Iris.reportError(e); } }, RNG.r.i(100, 1200)); - } catch (IllegalPluginAccessException ignored) { - + } catch (IllegalPluginAccessException ex) { + Iris.verbose("Skipping deferred task registration because plugin access is unavailable: " + + ex.getClass().getSimpleName() + + (ex.getMessage() == null ? "" : " - " + ex.getMessage())); } } @@ -476,7 +488,12 @@ public class Iris extends VolmitPlugin implements Listener { public void addShutdownHook() { if (shutdownHook != null) { - Runtime.getRuntime().removeShutdownHook(shutdownHook); + try { + Runtime.getRuntime().removeShutdownHook(shutdownHook); + } catch (IllegalStateException ex) { + Iris.debug("Skipping shutdown hook replacement because JVM shutdown is already in progress."); + return; + } } shutdownHook = new Thread(() -> { Bukkit.getWorlds() @@ -487,9 +504,15 @@ public class Iris extends VolmitPlugin implements Listener { MultiBurst.burst.close(); MultiBurst.ioBurst.close(); - services.clear(); - }); - Runtime.getRuntime().addShutdownHook(shutdownHook); + if (services != null) { + services.clear(); + } + }, "Iris-ShutdownHook"); + try { + Runtime.getRuntime().addShutdownHook(shutdownHook); + } catch (IllegalStateException ex) { + Iris.debug("Skipping shutdown hook registration because JVM shutdown is already in progress."); + } } public void checkForBukkitWorlds(Predicate filter) { @@ -563,12 +586,14 @@ public class Iris extends VolmitPlugin implements Listener { public void onDisable() { if (IrisSafeguard.isForceShutdown()) return; - services.values().forEach(IrisService::onDisable); + if (services != null) { + services.values().forEach(IrisService::onDisable); + } if (configHotloadEngine != null) { configHotloadEngine.clear(); configHotloadEngine = null; } - Bukkit.getScheduler().cancelTasks(this); + J.cancelPluginTasks(); HandlerList.unregisterAll((Plugin) this); postShutdown.forEach(Runnable::run); super.onDisable(); @@ -632,7 +657,12 @@ public class Iris extends VolmitPlugin implements Listener { try { return IO.readAll(file); - } catch (Throwable ignored) { + } catch (Throwable ex) { + Iris.warn("Failed to read settings file %s: %s%s", + file.getAbsolutePath(), + ex.getClass().getSimpleName(), + ex.getMessage() == null ? "" : " - " + ex.getMessage()); + Iris.reportError(ex); return null; } } @@ -765,7 +795,10 @@ public class Iris extends VolmitPlugin implements Listener { JsonObject json = JsonParser.parseReader(r).getAsJsonObject(); if (json.has("version")) version = json.get("version").getAsString(); - } catch (IOException | JsonParseException ignored) { + } catch (IOException | JsonParseException ex) { + Iris.verbose("Failed to read dimension version metadata for " + dimName + ": " + + ex.getClass().getSimpleName() + + (ex.getMessage() == null ? "" : " - " + ex.getMessage())); } Iris.info(" " + dimName + " v" + version); } diff --git a/core/src/main/java/art/arcane/iris/core/IrisSettings.java b/core/src/main/java/art/arcane/iris/core/IrisSettings.java index 84ac5ae14..6c6f81f27 100644 --- a/core/src/main/java/art/arcane/iris/core/IrisSettings.java +++ b/core/src/main/java/art/arcane/iris/core/IrisSettings.java @@ -207,7 +207,6 @@ public class IrisSettings { @Data public static class IrisSettingsGeneral { - public boolean DoomsdayAnnihilationSelfDestructMode = false; public boolean commandSounds = true; public boolean debug = false; public boolean dumpMantleOnError = false; diff --git a/core/src/main/java/art/arcane/iris/core/ServerConfigurator.java b/core/src/main/java/art/arcane/iris/core/ServerConfigurator.java index 8294ffb3e..d6ed95bd9 100644 --- a/core/src/main/java/art/arcane/iris/core/ServerConfigurator.java +++ b/core/src/main/java/art/arcane/iris/core/ServerConfigurator.java @@ -104,7 +104,13 @@ public class ServerConfigurator { } public static boolean installDataPacks(boolean fullInstall) { - return installDataPacks(DataVersion.getDefault(), fullInstall); + IDataFixer fixer = DataVersion.getDefault(); + if (fixer == null) { + DataVersion fallback = DataVersion.getLatest(); + Iris.warn("Primary datapack fixer was null, forcing latest fixer: " + fallback.getVersion()); + fixer = fallback.get(); + } + return installDataPacks(fixer, fullInstall); } public static boolean installDataPacks(IDataFixer fixer, boolean fullInstall) { 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 5fc8b3aae..f5d4bb6a0 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 @@ -39,7 +39,6 @@ import art.arcane.volmlib.util.io.IO; import art.arcane.iris.util.misc.ServerProperties; import art.arcane.iris.util.plugin.VolmitSender; import art.arcane.iris.util.scheduling.J; -import lombok.SneakyThrows; import org.bukkit.Bukkit; import org.bukkit.World; import org.bukkit.configuration.ConfigurationSection; @@ -120,6 +119,13 @@ public class CommandIris implements DecreeExecutor { return; } + if (J.isFolia()) { + if (stageFoliaWorldCreation(name, dimension, seed, main)) { + sender().sendMessage(C.GREEN + "World staging completed. Restart the server to generate/load \"" + name + "\"."); + } + return; + } + try { worldCreation = true; IrisToolbelt.createWorld() @@ -147,20 +153,98 @@ public class CommandIris implements DecreeExecutor { if (main) sender().sendMessage(C.GREEN + "Your world will automatically be set as the main world when the server restarts."); } - @SneakyThrows - private void updateMainWorld(String newName) { - File worlds = Bukkit.getWorldContainer(); - var data = ServerProperties.DATA; - try (var in = new FileInputStream(ServerProperties.SERVER_PROPERTIES)) { - data.load(in); + private boolean updateMainWorld(String newName) { + try { + File worlds = Bukkit.getWorldContainer(); + var data = ServerProperties.DATA; + try (var in = new FileInputStream(ServerProperties.SERVER_PROPERTIES)) { + data.load(in); + } + + File oldWorldFolder = new File(worlds, ServerProperties.LEVEL_NAME); + File newWorldFolder = new File(worlds, newName); + if (!newWorldFolder.exists() && !newWorldFolder.mkdirs()) { + Iris.warn("Could not create target main world folder: " + newWorldFolder.getAbsolutePath()); + } + + for (String sub : List.of("datapacks", "playerdata", "advancements", "stats")) { + File source = new File(oldWorldFolder, sub); + if (!source.exists()) { + continue; + } + + IO.copyDirectory(source.toPath(), new File(newWorldFolder, sub).toPath()); + } + + data.setProperty("level-name", newName); + try (var out = new FileOutputStream(ServerProperties.SERVER_PROPERTIES)) { + data.store(out, null); + } + return true; + } catch (Throwable e) { + Iris.error("Failed to update server.properties main world to \"" + newName + "\""); + Iris.reportError(e); + return false; } - for (String sub : List.of("datapacks", "playerdata", "advancements", "stats")) { - IO.copyDirectory(new File(worlds, ServerProperties.LEVEL_NAME + "/" + sub).toPath(), new File(worlds, newName + "/" + sub).toPath()); + } + + private boolean stageFoliaWorldCreation(String name, IrisDimension dimension, long seed, boolean main) { + sender().sendMessage(C.YELLOW + "Runtime world creation is disabled on Folia."); + sender().sendMessage(C.YELLOW + "Preparing world files and bukkit.yml for next startup..."); + + File worldFolder = new File(Bukkit.getWorldContainer(), name); + IrisDimension installed = Iris.service(StudioSVC.class).installIntoWorld(sender(), dimension.getLoadKey(), worldFolder); + if (installed == null) { + sender().sendMessage(C.RED + "Failed to stage world files for dimension \"" + dimension.getLoadKey() + "\"."); + return false; } - data.setProperty("level-name", newName); - try (var out = new FileOutputStream(ServerProperties.SERVER_PROPERTIES)) { - data.store(out, null); + if (!registerWorldInBukkitYml(name, dimension.getLoadKey(), seed)) { + return false; + } + + if (main) { + if (updateMainWorld(name)) { + sender().sendMessage(C.GREEN + "Updated server.properties level-name to \"" + name + "\"."); + } else { + sender().sendMessage(C.RED + "World was staged, but failed to update server.properties main world."); + return false; + } + } + + sender().sendMessage(C.GREEN + "Staged Iris world \"" + name + "\" with generator Iris:" + dimension.getLoadKey() + " and seed " + seed + "."); + if (main) { + sender().sendMessage(C.GREEN + "This world is now configured as main for next restart."); + } + return true; + } + + private boolean registerWorldInBukkitYml(String worldName, String dimension, Long seed) { + YamlConfiguration yml = YamlConfiguration.loadConfiguration(BUKKIT_YML); + ConfigurationSection worlds = yml.getConfigurationSection("worlds"); + if (worlds == null) { + worlds = yml.createSection("worlds"); + } + ConfigurationSection worldSection = worlds.getConfigurationSection(worldName); + if (worldSection == null) { + worldSection = worlds.createSection(worldName); + } + + String generator = "Iris:" + dimension; + worldSection.set("generator", generator); + if (seed != null) { + worldSection.set("seed", seed); + } + + try { + yml.save(BUKKIT_YML); + Iris.info("Registered \"" + worldName + "\" in bukkit.yml"); + return true; + } catch (IOException e) { + sender().sendMessage(C.RED + "Failed to update bukkit.yml: " + e.getMessage()); + Iris.error("Failed to update bukkit.yml!"); + Iris.reportError(e); + return false; } } @@ -439,22 +523,23 @@ public class CommandIris implements DecreeExecutor { sender().sendMessage(C.GOLD + world + " is not an iris world."); return; } + + if (dimension == null) { + sender().sendMessage(C.RED + "Could not determine Iris dimension for " + world + "."); + return; + } + sender().sendMessage(C.GREEN + "Loading world: " + world); - YamlConfiguration yml = YamlConfiguration.loadConfiguration(BUKKIT_YML); - String gen = "Iris:" + dimension; - ConfigurationSection section = yml.contains("worlds") ? yml.getConfigurationSection("worlds") : yml.createSection("worlds"); - if (!section.contains(world)) { - section.createSection(world).set("generator", gen); - try { - yml.save(BUKKIT_YML); - Iris.info("Registered \"" + world + "\" in bukkit.yml"); - } catch (IOException e) { - Iris.error("Failed to update bukkit.yml!"); - e.printStackTrace(); - return; - } + if (!registerWorldInBukkitYml(world, dimension, null)) { + return; } + + if (J.isFolia()) { + sender().sendMessage(C.YELLOW + "Folia cannot load new worlds at runtime. Restart the server to load \"" + world + "\"."); + return; + } + Iris.instance.checkForBukkitWorlds(world::equals); sender().sendMessage(C.GREEN + world + " loaded successfully."); } @@ -498,7 +583,12 @@ public class CommandIris implements DecreeExecutor { for (String key : data.getDimensionLoader().getPossibleKeys()) { options.add(key); } - } catch (Throwable ignored) { + } catch (Throwable ex) { + Iris.warn("Failed to read dimension keys from pack %s: %s%s", + pack.getName(), + ex.getClass().getSimpleName(), + ex.getMessage() == null ? "" : " - " + ex.getMessage()); + Iris.reportError(ex); } } } 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 587935847..0c719b69a 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 @@ -115,6 +115,13 @@ public class CommandStudio implements DecreeExecutor { IrisDimension dimension, @Param(defaultValue = "1337", description = "The seed to generate the studio with", aliases = "s") long seed) { + if (J.isFolia()) { + sender().sendMessage(C.RED + "Studio world opening is disabled on Folia."); + sender().sendMessage(C.YELLOW + "Folia does not currently support runtime world creation via Bukkit.createWorld()."); + sender().sendMessage(C.YELLOW + "Use Paper/Purpur for Studio mode, or preconfigure worlds and restart."); + return; + } + sender().sendMessage(C.GREEN + "Opening studio for the \"" + dimension.getName() + "\" pack (seed: " + seed + ")"); Iris.service(StudioSVC.class).open(sender(), seed, dimension.getLoadKey()); } @@ -329,10 +336,10 @@ public class CommandStudio implements DecreeExecutor { var player = player(); var engine = engine(); - ta.set(Bukkit.getScheduler().scheduleSyncRepeatingTask(Iris.instance, () -> + ta.set(J.sr(() -> { if (!player.getOpenInventory().getType().equals(InventoryType.CHEST)) { - Bukkit.getScheduler().cancelTask(ta.get()); + J.csr(ta.get()); sender.sendMessage(C.GREEN + "Opened inventory!"); return; } @@ -342,7 +349,7 @@ public class CommandStudio implements DecreeExecutor { } engine.addItems(true, inv, new RNG(RNG.r.imax()), tables, InventorySlotType.STORAGE, player.getWorld(), player.getLocation().getBlockX(), player.getLocation().getBlockY(), player.getLocation().getBlockZ(), 1); - }, 0, fast ? 5 : 35)); + }, fast ? 5 : 35)); sender().sendMessage(C.GREEN + "Opening inventory now!"); player().openInventory(inv); diff --git a/core/src/main/java/art/arcane/iris/core/nms/INMS.java b/core/src/main/java/art/arcane/iris/core/nms/INMS.java index 11f61bea8..ba6520a35 100644 --- a/core/src/main/java/art/arcane/iris/core/nms/INMS.java +++ b/core/src/main/java/art/arcane/iris/core/nms/INMS.java @@ -93,7 +93,9 @@ public class INMS { Iris.reportError(e); e.printStackTrace(); } - } catch (ClassNotFoundException|NoClassDefFoundError classNotFoundException) {} + } catch (ClassNotFoundException|NoClassDefFoundError classNotFoundException) { + Iris.warn("Failed to load NMS binding class for " + code + ": " + classNotFoundException.getMessage()); + } Iris.info("Craftbukkit " + code + " <-> " + NMSBinding1X.class.getSimpleName() + " Successfully Bound"); Iris.warn("Note: Some features of Iris may not work the same since you are on an unsupported version of Minecraft."); diff --git a/core/src/main/java/art/arcane/iris/core/nms/datapack/DataVersion.java b/core/src/main/java/art/arcane/iris/core/nms/datapack/DataVersion.java index ab81e9796..8ad75c0f5 100644 --- a/core/src/main/java/art/arcane/iris/core/nms/datapack/DataVersion.java +++ b/core/src/main/java/art/arcane/iris/core/nms/datapack/DataVersion.java @@ -1,5 +1,6 @@ package art.arcane.iris.core.nms.datapack; +import art.arcane.iris.Iris; import art.arcane.iris.core.nms.INMS; import art.arcane.iris.core.nms.datapack.v1192.DataFixerV1192; import art.arcane.iris.core.nms.datapack.v1206.DataFixerV1206; @@ -36,7 +37,21 @@ public enum DataVersion { } public static IDataFixer getDefault() { - return INMS.get().getDataVersion().get(); + DataVersion version = INMS.get().getDataVersion(); + if (version == null || version == UNSUPPORTED) { + DataVersion fallback = getLatest(); + Iris.warn("Unsupported datapack version mapping detected, falling back to latest fixer: " + fallback.getVersion()); + return fallback.get(); + } + + IDataFixer fixer = version.get(); + if (fixer == null) { + DataVersion fallback = getLatest(); + Iris.warn("Null datapack fixer for " + version.getVersion() + ", falling back to latest fixer: " + fallback.getVersion()); + return fallback.get(); + } + + return fixer; } public static DataVersion getLatest() { diff --git a/core/src/main/java/art/arcane/iris/core/pregenerator/DeepSearchPregenerator.java b/core/src/main/java/art/arcane/iris/core/pregenerator/DeepSearchPregenerator.java index 95a39527c..00f920fd0 100644 --- a/core/src/main/java/art/arcane/iris/core/pregenerator/DeepSearchPregenerator.java +++ b/core/src/main/java/art/arcane/iris/core/pregenerator/DeepSearchPregenerator.java @@ -22,7 +22,6 @@ import org.bukkit.World; import org.bukkit.event.EventHandler; import org.bukkit.event.Listener; import org.bukkit.event.world.WorldUnloadEvent; -import org.bukkit.scheduler.BukkitRunnable; import java.io.File; import java.io.FileWriter; @@ -235,16 +234,13 @@ public class DeepSearchPregenerator extends Thread implements Listener { } save(); jobs.remove(world.getName()); - new BukkitRunnable() { - @Override - public void run() { - while (deepFile.exists()){ - deepFile.delete(); - J.sleep(1000); - } - Iris.info("DeepSearch: " + C.IRIS + world.getName() + C.BLUE + " File deleted and instance closed."); + J.a(() -> { + while (deepFile.exists()) { + deepFile.delete(); + J.sleep(1000); } - }.runTaskLater(Iris.instance, 20L); + Iris.info("DeepSearch: " + C.IRIS + world.getName() + C.BLUE + " File deleted and instance closed."); + }, 20); } catch (Exception e) { Iris.error("Failed to shutdown DeepSearch for " + world.getName()); e.printStackTrace(); @@ -271,4 +267,3 @@ public class DeepSearchPregenerator extends Thread implements Listener { boolean paused = false; } } - diff --git a/core/src/main/java/art/arcane/iris/core/pregenerator/LazyPregenerator.java b/core/src/main/java/art/arcane/iris/core/pregenerator/LazyPregenerator.java index 4ea197150..788c097e3 100644 --- a/core/src/main/java/art/arcane/iris/core/pregenerator/LazyPregenerator.java +++ b/core/src/main/java/art/arcane/iris/core/pregenerator/LazyPregenerator.java @@ -19,7 +19,6 @@ import org.bukkit.World; import org.bukkit.event.EventHandler; import org.bukkit.event.Listener; import org.bukkit.event.world.WorldUnloadEvent; -import org.bukkit.scheduler.BukkitRunnable; import java.io.File; import java.io.IOException; @@ -165,7 +164,10 @@ public class LazyPregenerator extends Thread implements Listener { } try { latch.await(); - } catch (InterruptedException ignored) {} + } catch (InterruptedException ex) { + Thread.currentThread().interrupt(); + Iris.verbose("Lazy pregenerator worker interrupted while waiting for chunk " + chunk + "."); + } lazyGeneratedChunks.addAndGet(1); }); } @@ -257,16 +259,13 @@ public class LazyPregenerator extends Thread implements Listener { } save(); jobs.remove(world.getName()); - new BukkitRunnable() { - @Override - public void run() { - while (lazyFile.exists()){ - lazyFile.delete(); - J.sleep(1000); - } - Iris.info("LazyGen: " + C.IRIS + world.getName() + C.BLUE + " File deleted and instance closed."); + J.a(() -> { + while (lazyFile.exists()) { + lazyFile.delete(); + J.sleep(1000); } - }.runTaskLater(Iris.instance, 20L); + Iris.info("LazyGen: " + C.IRIS + world.getName() + C.BLUE + " File deleted and instance closed."); + }, 20); } catch (Exception e) { Iris.error("Failed to shutdown Lazygen for " + world.getName()); e.printStackTrace(); diff --git a/core/src/main/java/art/arcane/iris/core/pregenerator/TurboPregenerator.java b/core/src/main/java/art/arcane/iris/core/pregenerator/TurboPregenerator.java index de9522e61..303506966 100644 --- a/core/src/main/java/art/arcane/iris/core/pregenerator/TurboPregenerator.java +++ b/core/src/main/java/art/arcane/iris/core/pregenerator/TurboPregenerator.java @@ -26,7 +26,6 @@ import org.bukkit.World; import org.bukkit.event.EventHandler; import org.bukkit.event.Listener; import org.bukkit.event.world.WorldUnloadEvent; -import org.bukkit.scheduler.BukkitRunnable; import org.checkerframework.checker.units.qual.N; import java.io.File; @@ -233,7 +232,9 @@ public class TurboPregenerator extends Thread implements Listener { }); try { latch.await(); - } catch (InterruptedException ignored) { + } catch (InterruptedException ex) { + Thread.currentThread().interrupt(); + Iris.verbose("Turbo pregenerator worker interrupted while waiting for chunk " + chunk + "."); } turboGeneratedChunks.addAndGet(1); }); @@ -321,16 +322,13 @@ public class TurboPregenerator extends Thread implements Listener { } save(); jobs.remove(world.getName()); - new BukkitRunnable() { - @Override - public void run() { - while (turboFile.exists()) { - turboFile.delete(); - J.sleep(1000); - } - Iris.info("turboGen: " + C.IRIS + world.getName() + C.BLUE + " File deleted and instance closed."); + J.a(() -> { + while (turboFile.exists()) { + turboFile.delete(); + J.sleep(1000); } - }.runTaskLater(Iris.instance, 20L); + Iris.info("turboGen: " + C.IRIS + world.getName() + C.BLUE + " File deleted and instance closed."); + }, 20); } catch (Exception e) { Iris.error("Failed to shutdown turbogen for " + world.getName()); e.printStackTrace(); diff --git a/core/src/main/java/art/arcane/iris/core/service/BoardSVC.java b/core/src/main/java/art/arcane/iris/core/service/BoardSVC.java index 4cd666eaf..2e7201dcc 100644 --- a/core/src/main/java/art/arcane/iris/core/service/BoardSVC.java +++ b/core/src/main/java/art/arcane/iris/core/service/BoardSVC.java @@ -23,7 +23,6 @@ import art.arcane.iris.core.IrisSettings; import art.arcane.iris.core.loader.IrisData; import art.arcane.iris.core.tools.IrisToolbelt; import art.arcane.volmlib.util.board.Board; -import art.arcane.volmlib.util.board.BoardManager; import art.arcane.volmlib.util.board.BoardProvider; import art.arcane.volmlib.util.board.BoardSettings; import art.arcane.volmlib.util.board.ScoreDirection; @@ -43,40 +42,43 @@ import org.bukkit.event.player.PlayerQuitEvent; import java.util.ArrayList; import java.util.List; -import java.util.concurrent.Executors; -import java.util.concurrent.ScheduledExecutorService; -import java.util.concurrent.ScheduledFuture; -import java.util.concurrent.TimeUnit; public class BoardSVC implements IrisService, BoardProvider { private final KMap boards = new KMap<>(); - private ScheduledExecutorService executor; - private BoardManager manager; + private BoardSettings settings; + private boolean boardEnabled; @Override public void onEnable() { - executor = Executors.newScheduledThreadPool(0, Thread.ofVirtual().factory()); - manager = new BoardManager<>(Iris.instance, BoardSettings.builder() + boardEnabled = true; + settings = BoardSettings.builder() .boardProvider(this) .scoreDirection(ScoreDirection.DOWN) - .build(), Board::new); + .build(); + + for (Player player : Iris.instance.getServer().getOnlinePlayers()) { + J.runEntity(player, () -> updatePlayer(player)); + } } @Override public void onDisable() { - executor.shutdownNow(); - manager.onDisable(); + boardEnabled = false; + for (PlayerBoard board : new ArrayList<>(boards.values())) { + board.cancel(); + } boards.clear(); + settings = null; } @EventHandler public void on(PlayerChangedWorldEvent e) { - J.s(() -> updatePlayer(e.getPlayer())); + J.runEntity(e.getPlayer(), () -> updatePlayer(e.getPlayer())); } @EventHandler public void on(PlayerJoinEvent e) { - J.s(() -> updatePlayer(e.getPlayer())); + J.runEntity(e.getPlayer(), () -> updatePlayer(e.getPlayer())); } @EventHandler @@ -85,16 +87,34 @@ public class BoardSVC implements IrisService, BoardProvider { } public void updatePlayer(Player p) { + if (!boardEnabled || settings == null) { + return; + } + + if (!J.isOwnedByCurrentRegion(p)) { + J.runEntity(p, () -> updatePlayer(p)); + return; + } + if (IrisToolbelt.isIrisStudioWorld(p.getWorld())) { - manager.remove(p); - manager.setup(p); + boards.computeIfAbsent(p, PlayerBoard::new); } else remove(p); } private void remove(Player player) { - manager.remove(player); + if (player == null) { + return; + } + + if (!J.isOwnedByCurrentRegion(player)) { + J.runEntity(player, () -> remove(player)); + return; + } + var board = boards.remove(player); - if (board != null) board.task.cancel(true); + if (board != null) { + board.cancel(); + } } @Override @@ -104,27 +124,60 @@ public class BoardSVC implements IrisService, BoardProvider { @Override public List getLines(Player player) { - return boards.computeIfAbsent(player, PlayerBoard::new).lines; + PlayerBoard board = boards.get(player); + if (board == null) { + return List.of(); + } + return board.lines; } @Data public class PlayerBoard { private final Player player; - private final ScheduledFuture task; + private final Board board; private volatile List lines; + private volatile boolean cancelled; public PlayerBoard(Player player) { this.player = player; + this.board = new Board(player, settings); this.lines = new ArrayList<>(); - this.task = executor.scheduleAtFixedRate(this::tick, 0, 1, TimeUnit.SECONDS); + this.cancelled = false; + schedule(0); + } + + private void schedule(int delayTicks) { + if (cancelled || !boardEnabled || !player.isOnline()) { + return; + } + J.runEntity(player, this::tick, delayTicks); } private void tick() { + if (cancelled || !boardEnabled || !player.isOnline()) { + return; + } + + if (!IrisToolbelt.isIrisStudioWorld(player.getWorld())) { + boards.remove(player); + cancel(); + return; + } + if (!Iris.service(StudioSVC.class).isProjectOpen()) { + board.update(); + schedule(20); return; } update(); + board.update(); + schedule(20); + } + + public void cancel() { + cancelled = true; + J.runEntity(player, board::remove); } public void update() { diff --git a/core/src/main/java/art/arcane/iris/core/service/EditSVC.java b/core/src/main/java/art/arcane/iris/core/service/EditSVC.java index 675d7f2b2..8e20c5b8c 100644 --- a/core/src/main/java/art/arcane/iris/core/service/EditSVC.java +++ b/core/src/main/java/art/arcane/iris/core/service/EditSVC.java @@ -24,7 +24,7 @@ import art.arcane.iris.core.edit.BukkitBlockEditor; import art.arcane.volmlib.util.collection.KMap; import art.arcane.volmlib.util.math.M; import art.arcane.iris.util.plugin.IrisService; -import org.bukkit.Bukkit; +import art.arcane.iris.util.scheduling.J; import org.bukkit.World; import org.bukkit.block.Biome; import org.bukkit.block.data.BlockData; @@ -33,16 +33,21 @@ import org.bukkit.event.world.WorldUnloadEvent; public class EditSVC implements IrisService { private KMap editors; + private int updateTaskId = -1; public static boolean deletingWorld = false; @Override public void onEnable() { this.editors = new KMap<>(); - Bukkit.getScheduler().scheduleSyncRepeatingTask(Iris.instance, this::update, 1000, 1000); + updateTaskId = J.sr(this::update, 1000); } @Override public void onDisable() { + if (updateTaskId != -1) { + J.csr(updateTaskId); + updateTaskId = -1; + } flushNow(); } 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 b192885ca..c8f3e7791 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 @@ -24,6 +24,7 @@ import org.bukkit.event.world.WorldLoadEvent; import org.bukkit.event.world.WorldUnloadEvent; import org.jetbrains.annotations.Nullable; +import java.util.Locale; import java.util.concurrent.*; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicLong; @@ -231,6 +232,20 @@ public class IrisEngineSVC implements IrisService { updateTicker.start(); } + private static boolean isMantleClosed(Throwable throwable) { + Throwable current = throwable; + while (current != null) { + String message = current.getMessage(); + if (message != null && message.toLowerCase(Locale.ROOT).contains("mantle is closed")) { + return true; + } + + current = current.getCause(); + } + + return false; + } + private final class Registered { private final String name; private final PlatformChunkGenerator access; @@ -261,12 +276,19 @@ public class IrisEngineSVC implements IrisService { if (trimmer == null || trimmer.isDone() || trimmer.isCancelled()) { trimmer = service.scheduleAtFixedRate(() -> { Engine engine = getEngine(); - if (engine == null || !engine.getMantle().getMantle().shouldReduce(engine)) + if (engine == null + || engine.isClosed() + || engine.getMantle().getMantle().isClosed() + || !engine.getMantle().getMantle().shouldReduce(engine)) return; try { engine.getMantle().trim(tectonicLimit()); } catch (Throwable e) { + if (isMantleClosed(e)) { + close(); + return; + } Iris.reportError(e); Iris.error("EngineSVC: Failed to trim for " + name); e.printStackTrace(); @@ -277,7 +299,10 @@ public class IrisEngineSVC implements IrisService { if (unloader == null || unloader.isDone() || unloader.isCancelled()) { unloader = service.scheduleAtFixedRate(() -> { Engine engine = getEngine(); - if (engine == null || !engine.getMantle().getMantle().shouldReduce(engine)) + if (engine == null + || engine.isClosed() + || engine.getMantle().getMantle().isClosed() + || !engine.getMantle().getMantle().shouldReduce(engine)) return; try { @@ -287,6 +312,10 @@ public class IrisEngineSVC implements IrisService { Iris.debug(C.GOLD + "Unloaded " + C.YELLOW + count + " TectonicPlates in " + C.RED + Form.duration(System.currentTimeMillis() - unloadStart, 2)); } } catch (Throwable e) { + if (isMantleClosed(e)) { + close(); + return; + } Iris.reportError(e); Iris.error("EngineSVC: Failed to unload for " + name); e.printStackTrace(); diff --git a/core/src/main/java/art/arcane/iris/core/service/ObjectSVC.java b/core/src/main/java/art/arcane/iris/core/service/ObjectSVC.java index ba4c28a20..3182cca14 100644 --- a/core/src/main/java/art/arcane/iris/core/service/ObjectSVC.java +++ b/core/src/main/java/art/arcane/iris/core/service/ObjectSVC.java @@ -22,7 +22,6 @@ import art.arcane.iris.Iris; import art.arcane.iris.util.plugin.IrisService; import art.arcane.iris.util.scheduling.J; import lombok.Getter; -import org.bukkit.Bukkit; import org.bukkit.block.Block; import org.bukkit.block.data.BlockData; @@ -71,7 +70,7 @@ public class ObjectSVC implements IrisService { */ private void revert(Map blocks) { Iterator> it = blocks.entrySet().iterator(); - Bukkit.getScheduler().runTask(Iris.instance, () -> { + J.s(() -> { int amount = 0; while (it.hasNext()) { Map.Entry entry = it.next(); @@ -88,4 +87,4 @@ public class ObjectSVC implements IrisService { } }); } -} \ No newline at end of file +} 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 b80219671..48841e365 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 @@ -341,8 +341,8 @@ public class StudioSVC implements IrisService { }); } catch (Exception e) { Iris.reportError(e); - sender.sendMessage("Error when creating studio world:"); - e.printStackTrace(); + sender.sendMessage("Failed to open studio world: " + e.getMessage()); + Iris.error("Studio world creation failed: " + e.getMessage()); } } diff --git a/core/src/main/java/art/arcane/iris/core/tools/IrisCreator.java b/core/src/main/java/art/arcane/iris/core/tools/IrisCreator.java index f45c48cd0..a1cae7fc2 100644 --- a/core/src/main/java/art/arcane/iris/core/tools/IrisCreator.java +++ b/core/src/main/java/art/arcane/iris/core/tools/IrisCreator.java @@ -114,6 +114,10 @@ public class IrisCreator { throw new IrisException("You cannot invoke create() on the main thread."); } + if (J.isFolia()) { + throw new IrisException("Folia does not support runtime world creation via Bukkit.createWorld(). Configure worlds before startup and restart the server."); + } + IrisDimension d = IrisToolbelt.getDimension(dimension()); if (d == null) { @@ -171,6 +175,9 @@ public class IrisCreator { world = J.sfut(() -> INMS.get().createWorld(wc)).get(); } catch (Throwable e) { done.set(true); + if (containsCreateWorldUnsupportedOperation(e)) { + throw new IrisException("Runtime world creation is not supported on this server variant. Configure worlds before startup and restart the server.", e); + } throw new IrisException("Failed to create world!", e); } @@ -226,6 +233,22 @@ public class IrisCreator { return world; } + private static boolean containsCreateWorldUnsupportedOperation(Throwable throwable) { + Throwable cursor = throwable; + while (cursor != null) { + if (cursor instanceof UnsupportedOperationException) { + for (StackTraceElement element : cursor.getStackTrace()) { + if ("org.bukkit.craftbukkit.CraftServer".equals(element.getClassName()) + && "createWorld".equals(element.getMethodName())) { + return true; + } + } + } + cursor = cursor.getCause(); + } + return false; + } + private void addToBukkitYml() { YamlConfiguration yml = YamlConfiguration.loadConfiguration(BUKKIT_YML); String gen = "Iris:" + dimension; 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 d513dd0d8..792b99b61 100644 --- a/core/src/main/java/art/arcane/iris/engine/IrisWorldManager.java +++ b/core/src/main/java/art/arcane/iris/engine/IrisWorldManager.java @@ -62,7 +62,6 @@ import java.util.List; import java.util.Map; import java.util.Set; import java.util.concurrent.*; -import java.util.concurrent.atomic.AtomicBoolean; import java.util.function.Predicate; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -192,52 +191,94 @@ public class IrisWorldManager extends EngineAssignedWorldManager { private void discoverChunks() { var mantle = getEngine().getMantle().getMantle(); - for (Player i : getEngine().getWorld().realWorld().getPlayers()) { - int r = 1; - - for (int x = -r; x <= r; x++) { - for (int z = -r; z <= r; z++) { - mantle.getChunk(i.getLocation().getChunk()).flag(MantleFlag.DISCOVERED, true); - } - } + World world = getEngine().getWorld().realWorld(); + if (world == null) { + return; } + + J.s(() -> { + for (Player player : world.getPlayers()) { + if (player == null || !player.isOnline()) { + continue; + } + + J.runEntity(player, () -> { + int centerX = player.getLocation().getBlockX() >> 4; + int centerZ = player.getLocation().getBlockZ() >> 4; + 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); + } + } + }); + } + }); } private void updateChunks() { - for (Player i : getEngine().getWorld().realWorld().getPlayers()) { - int r = 1; + World world = getEngine().getWorld().realWorld(); + if (world == null) { + return; + } - Chunk c = i.getLocation().getChunk(); - for (int x = -r; x <= r; x++) { - for (int z = -r; z <= r; z++) { - if (c.getWorld().isChunkLoaded(c.getX() + x, c.getZ() + z) && Chunks.isSafe(getEngine().getWorld().realWorld(), c.getX() + x, c.getZ() + z)) { + J.s(() -> { + for (Player player : world.getPlayers()) { + if (player == null || !player.isOnline()) { + continue; + } - if (IrisSettings.get().getWorld().isPostLoadBlockUpdates()) { - getEngine().updateChunk(c.getWorld().getChunkAt(c.getX() + x, c.getZ() + z)); - } + J.runEntity(player, () -> { + int centerX = player.getLocation().getBlockX() >> 4; + int centerZ = player.getLocation().getBlockZ() >> 4; + int radius = 1; - if (IrisSettings.get().getWorld().isMarkerEntitySpawningSystem()) { - Chunk cx = getEngine().getWorld().realWorld().getChunkAt(c.getX() + x, c.getZ() + z); - int finalX = c.getX() + x; - int finalZ = c.getZ() + z; - J.a(() -> getMantle().raiseFlag(finalX, finalZ, MantleFlag.INITIAL_SPAWNED_MARKER, - () -> { - J.a(() -> spawnIn(cx, true), RNG.r.i(5, 200)); - getSpawnersFromMarkers(cx).forEach((blockf, spawners) -> { - if (spawners.isEmpty()) { - return; - } - - IrisPosition block = new IrisPosition(blockf.getX(), blockf.getY() + getEngine().getWorld().minHeight(), blockf.getZ()); - IrisSpawner s = new KList<>(spawners).getRandom(); - spawn(block, s, true); - }); - })); + for (int x = -radius; x <= radius; x++) { + for (int z = -radius; z <= radius; z++) { + int targetX = centerX + x; + int targetZ = centerZ + z; + J.runRegion(world, targetX, targetZ, () -> updateChunkRegion(world, targetX, targetZ)); } } - } + }); } + }); + } + + private void updateChunkRegion(World world, int chunkX, int chunkZ) { + if (world == null || !world.isChunkLoaded(chunkX, chunkZ) || !Chunks.isSafe(world, chunkX, chunkZ)) { + return; } + + Chunk chunk = world.getChunkAt(chunkX, chunkZ); + + if (IrisSettings.get().getWorld().isPostLoadBlockUpdates()) { + getEngine().updateChunk(chunk); + } + + if (!IrisSettings.get().getWorld().isMarkerEntitySpawningSystem()) { + return; + } + + getMantle().raiseFlag(chunkX, chunkZ, MantleFlag.INITIAL_SPAWNED_MARKER, () -> { + int delay = RNG.r.i(5, 200); + J.runRegion(world, chunkX, chunkZ, () -> { + if (!world.isChunkLoaded(chunkX, chunkZ)) { + return; + } + spawnIn(world.getChunkAt(chunkX, chunkZ), true); + }, delay); + + getSpawnersFromMarkers(chunk).forEach((blockf, spawners) -> { + if (spawners.isEmpty()) { + 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 boolean onAsyncTick() { @@ -274,8 +315,12 @@ public class IrisWorldManager extends EngineAssignedWorldManager { } int spawnBuffer = RNG.r.i(2, 12); + World world = getEngine().getWorld().realWorld(); + if (world == null) { + return false; + } - Chunk[] cc = getEngine().getWorld().realWorld().getLoadedChunks(); + Chunk[] cc = getLoadedChunksSnapshot(world); while (spawnBuffer-- > 0) { if (cc.length == 0) { Iris.debug("Can't spawn. No chunks!"); @@ -283,18 +328,60 @@ public class IrisWorldManager extends EngineAssignedWorldManager { } Chunk c = cc[RNG.r.nextInt(cc.length)]; - - if (!c.isLoaded() || !Chunks.isSafe(c.getWorld(), c.getX(), c.getZ())) { - continue; - } - - spawnIn(c, false); + spawnChunkSafely(world, c.getX(), c.getZ(), false); } energy -= (actuallySpawned / 2D); return actuallySpawned > 0; } + private Chunk[] getLoadedChunksSnapshot(World world) { + if (world == null) { + return new Chunk[0]; + } + + CompletableFuture future = new CompletableFuture<>(); + J.s(() -> { + try { + future.complete(world.getLoadedChunks()); + } catch (Throwable e) { + future.completeExceptionally(e); + } + }); + + try { + return future.get(2, TimeUnit.SECONDS); + } catch (Throwable e) { + Iris.reportError(e); + return new Chunk[0]; + } + } + + private void spawnChunkSafely(World world, int chunkX, int chunkZ, boolean initial) { + if (world == null) { + return; + } + + CompletableFuture future = new CompletableFuture<>(); + J.runRegion(world, chunkX, chunkZ, () -> { + try { + if (!world.isChunkLoaded(chunkX, chunkZ) || !Chunks.isSafe(world, chunkX, chunkZ)) { + return; + } + + spawnIn(world.getChunkAt(chunkX, chunkZ), initial); + } finally { + future.complete(null); + } + }); + + try { + future.get(5, TimeUnit.SECONDS); + } catch (Throwable e) { + Iris.reportError(e); + } + } + private void fixEnergy() { energy = M.clip(energy, 1D, getDimension().getMaximumEnergy()); } @@ -317,7 +404,7 @@ public class IrisWorldManager extends EngineAssignedWorldManager { IrisPosition block = new IrisPosition(blockf.getX(), blockf.getY() + getEngine().getWorld().minHeight(), blockf.getZ()); IrisSpawner s = new KList<>(spawners).getRandom(); spawn(block, s, false); - J.a(() -> getMantle().raiseFlag(c.getX(), c.getZ(), MantleFlag.INITIAL_SPAWNED_MARKER, + J.runRegion(c.getWorld(), c.getX(), c.getZ(), () -> getMantle().raiseFlag(c.getX(), c.getZ(), MantleFlag.INITIAL_SPAWNED_MARKER, () -> spawn(block, s, true))); }); } @@ -359,7 +446,7 @@ public class IrisWorldManager extends EngineAssignedWorldManager { try { spawn(c, v); } catch (Throwable e) { - J.s(() -> spawn(c, v)); + J.runRegion(c.getWorld(), c.getX(), c.getZ(), () -> spawn(c, v)); } } @@ -537,8 +624,11 @@ public class IrisWorldManager extends EngineAssignedWorldManager { public void execute(Future chunkFuture) { try { chunkFuture.get(); - } catch (InterruptedException | ExecutionException ignored) { - + } catch (InterruptedException ex) { + Thread.currentThread().interrupt(); + Iris.verbose("Chunk warmup interrupted while loading async teleport chunk."); + } catch (ExecutionException ex) { + Iris.reportError(ex); } } @@ -562,19 +652,10 @@ public class IrisWorldManager extends EngineAssignedWorldManager { IrisPosition pos = new IrisPosition((c.getX() << 4) + x, y, (c.getZ() << 4) + z); if (mark.isEmptyAbove()) { - AtomicBoolean remove = new AtomicBoolean(false); + boolean remove = c.getBlock(x, y + 1, z).getBlockData().getMaterial().isSolid() + || c.getBlock(x, y + 2, z).getBlockData().getMaterial().isSolid(); - try { - J.sfut(() -> { - if (c.getBlock(x, y + 1, z).getBlockData().getMaterial().isSolid() || c.getBlock(x, y + 2, z).getBlockData().getMaterial().isSolid()) { - remove.set(true); - } - }).get(); - } catch (InterruptedException | ExecutionException e) { - e.printStackTrace(); - } - - if (remove.get()) { + if (remove) { b.add(pos); return; } 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 ebdf6fee3..5a0f2e1e9 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 @@ -297,20 +297,20 @@ public interface Engine extends DataProvider, Fallible, LootProvider, BlockUpdat try { Semaphore semaphore = new Semaphore(1024); chunk.raiseFlagUnchecked(MantleFlag.ETCHED, () -> { - chunk.raiseFlagUnchecked(MantleFlag.TILE, run(semaphore, () -> { + chunk.raiseFlagUnchecked(MantleFlag.TILE, run(semaphore, c, () -> { chunk.iterate(TileWrapper.class, (x, y, z, v) -> { Block block = c.getBlock(x & 15, y + getWorld().minHeight(), z & 15); if (!TileData.setTileState(block, v.getData())) Iris.warn("Failed to set tile entity data at [%d %d %d | %s] for tile %s!", block.getX(), block.getY(), block.getZ(), block.getType().getKey(), v.getData().getMaterial().getKey()); }); }, 0)); - chunk.raiseFlagUnchecked(MantleFlag.CUSTOM, run(semaphore, () -> { + chunk.raiseFlagUnchecked(MantleFlag.CUSTOM, run(semaphore, c, () -> { chunk.iterate(Identifier.class, (x, y, z, v) -> { Iris.service(ExternalDataSVC.class).processUpdate(this, c.getBlock(x & 15, y + getWorld().minHeight(), z & 15), v); }); }, 0)); - chunk.raiseFlagUnchecked(MantleFlag.UPDATE, run(semaphore, () -> { + chunk.raiseFlagUnchecked(MantleFlag.UPDATE, run(semaphore, c, () -> { PrecisionStopwatch p = PrecisionStopwatch.start(); int[][] grid = new int[16][16]; for (int x = 0; x < 16; x++) { @@ -362,19 +362,22 @@ public interface Engine extends DataProvider, Fallible, LootProvider, BlockUpdat return; for (var script : scripts) { - getExecution().updateChunk(script, chunk, c, (delay, task) -> run(semaphore, task, delay)); + getExecution().updateChunk(script, chunk, c, (delay, task) -> run(semaphore, c, task, delay)); } }); try { semaphore.acquire(1024); - } catch (InterruptedException ignored) {} + } catch (InterruptedException ex) { + Thread.currentThread().interrupt(); + Iris.reportError(ex); + } } finally { chunk.release(); } } - private static Runnable run(Semaphore semaphore, Runnable runnable, int delay) { + private static Runnable run(Semaphore semaphore, Chunk contextChunk, Runnable runnable, int delay) { return () -> { try { semaphore.acquire(); @@ -382,13 +385,14 @@ public interface Engine extends DataProvider, Fallible, LootProvider, BlockUpdat throw new RuntimeException(e); } - J.s(() -> { + int effectiveDelay = J.isFolia() ? 0 : delay; + J.runRegion(contextChunk.getWorld(), contextChunk.getX(), contextChunk.getZ(), () -> { try { runnable.run(); } finally { semaphore.release(); } - }, delay); + }, effectiveDelay); }; } diff --git a/core/src/main/java/art/arcane/iris/engine/framework/EngineAssignedWorldManager.java b/core/src/main/java/art/arcane/iris/engine/framework/EngineAssignedWorldManager.java index 8950974f2..9606a3f5d 100644 --- a/core/src/main/java/art/arcane/iris/engine/framework/EngineAssignedWorldManager.java +++ b/core/src/main/java/art/arcane/iris/engine/framework/EngineAssignedWorldManager.java @@ -21,10 +21,11 @@ package art.arcane.iris.engine.framework; import art.arcane.iris.Iris; import art.arcane.iris.core.events.IrisEngineHotloadEvent; import art.arcane.volmlib.util.collection.KList; -import art.arcane.iris.util.format.C; -import art.arcane.iris.util.math.Position2; -import art.arcane.iris.util.plugin.VolmitSender; -import org.bukkit.*; +import art.arcane.iris.util.format.C; +import art.arcane.iris.util.math.Position2; +import art.arcane.iris.util.plugin.VolmitSender; +import art.arcane.iris.util.scheduling.J; +import org.bukkit.*; import org.bukkit.entity.EnderSignal; import org.bukkit.entity.Player; import org.bukkit.event.EventHandler; @@ -50,11 +51,11 @@ public abstract class EngineAssignedWorldManager extends EngineAssignedComponent taskId = -1; } - public EngineAssignedWorldManager(Engine engine) { - super(engine, "World"); - Iris.instance.registerListener(this); - taskId = Bukkit.getScheduler().scheduleSyncRepeatingTask(Iris.instance, this::onTick, 0, 0); - } + public EngineAssignedWorldManager(Engine engine) { + super(engine, "World"); + Iris.instance.registerListener(this); + taskId = J.sr(this::onTick, 1); + } @EventHandler public void on(IrisEngineHotloadEvent e) { @@ -134,9 +135,11 @@ public abstract class EngineAssignedWorldManager extends EngineAssignedComponent } @Override - public void close() { - super.close(); - Iris.instance.unregisterListener(this); - Bukkit.getScheduler().cancelTask(taskId); - } -} + public void close() { + super.close(); + Iris.instance.unregisterListener(this); + if (taskId != -1) { + J.csr(taskId); + } + } +} diff --git a/core/src/main/java/art/arcane/iris/engine/object/IrisCommand.java b/core/src/main/java/art/arcane/iris/engine/object/IrisCommand.java index 87ba6d320..f04ee6d5c 100644 --- a/core/src/main/java/art/arcane/iris/engine/object/IrisCommand.java +++ b/core/src/main/java/art/arcane/iris/engine/object/IrisCommand.java @@ -23,6 +23,7 @@ import art.arcane.iris.engine.object.annotations.ArrayType; import art.arcane.iris.engine.object.annotations.Desc; import art.arcane.iris.engine.object.annotations.Required; import art.arcane.iris.engine.object.annotations.Snippet; +import art.arcane.iris.util.scheduling.J; import art.arcane.volmlib.util.collection.KList; import lombok.Data; import lombok.NoArgsConstructor; @@ -73,11 +74,13 @@ public class IrisCommand { .replaceAll("\\Q{y}\\E", String.valueOf(at.getBlockY())) .replaceAll("\\Q{z}\\E", String.valueOf(at.getBlockZ())); final String finalCommand = command; + int safeDelay = (int) Math.max(0, Math.min(Integer.MAX_VALUE, delay)); if (repeat) { - Bukkit.getScheduler().scheduleSyncRepeatingTask(Iris.instance, () -> Bukkit.getServer().dispatchCommand(Bukkit.getConsoleSender(), finalCommand), delay, repeatDelay); + int safeRepeatDelay = (int) Math.max(1, Math.min(Integer.MAX_VALUE, repeatDelay)); + J.s(() -> J.sr(() -> Bukkit.getServer().dispatchCommand(Bukkit.getConsoleSender(), finalCommand), safeRepeatDelay), safeDelay); } else { - Bukkit.getScheduler().scheduleSyncDelayedTask(Iris.instance, () -> Bukkit.getServer().dispatchCommand(Bukkit.getConsoleSender(), finalCommand), delay); + J.s(() -> Bukkit.getServer().dispatchCommand(Bukkit.getConsoleSender(), finalCommand), safeDelay); } } } -} \ No newline at end of file +} diff --git a/core/src/main/java/art/arcane/iris/engine/object/IrisEntity.java b/core/src/main/java/art/arcane/iris/engine/object/IrisEntity.java index d808cff0f..634df49df 100644 --- a/core/src/main/java/art/arcane/iris/engine/object/IrisEntity.java +++ b/core/src/main/java/art/arcane/iris/engine/object/IrisEntity.java @@ -338,9 +338,7 @@ public class IrisEntity extends IrisRegistrant { if (e instanceof Villager) { Villager villager = (Villager) e; villager.setRemoveWhenFarAway(false); - Bukkit.getScheduler().scheduleSyncDelayedTask(Iris.instance, () -> { - villager.setPersistent(true); - }, 1); + J.s(() -> villager.setPersistent(true), 1); } if (e instanceof Mob) { diff --git a/core/src/main/java/art/arcane/iris/engine/object/LegacyTileData.java b/core/src/main/java/art/arcane/iris/engine/object/LegacyTileData.java index ab01d881b..f8378d822 100644 --- a/core/src/main/java/art/arcane/iris/engine/object/LegacyTileData.java +++ b/core/src/main/java/art/arcane/iris/engine/object/LegacyTileData.java @@ -80,7 +80,9 @@ public class LegacyTileData extends TileData { @Override public void toBukkit(Block block) { - J.s(() -> handler.toBukkit(block)); + if (!J.runAt(block.getLocation(), () -> handler.toBukkit(block))) { + J.s(() -> handler.toBukkit(block)); + } } @Override diff --git a/core/src/main/java/art/arcane/iris/util/common/plugin/VirtualCommand.java b/core/src/main/java/art/arcane/iris/util/common/plugin/VirtualCommand.java index 81e88a80a..4f6cac945 100644 --- a/core/src/main/java/art/arcane/iris/util/common/plugin/VirtualCommand.java +++ b/core/src/main/java/art/arcane/iris/util/common/plugin/VirtualCommand.java @@ -23,8 +23,8 @@ import art.arcane.iris.core.IrisSettings; import art.arcane.volmlib.util.collection.KList; import art.arcane.volmlib.util.collection.KMap; import art.arcane.iris.util.format.C; +import art.arcane.iris.util.scheduling.J; import art.arcane.volmlib.util.reflect.V; -import org.bukkit.Bukkit; import org.bukkit.Sound; import org.bukkit.command.CommandSender; @@ -171,7 +171,7 @@ public class VirtualCommand { for (String i : command.getRequiredPermissions()) { if (!sender.hasPermission(i)) { failed = true; - Bukkit.getScheduler().scheduleSyncDelayedTask(Iris.instance, () -> sender.sendMessage("- " + C.WHITE + i), 0); + J.s(() -> sender.sendMessage("- " + C.WHITE + i), 0); } } diff --git a/core/src/main/java/art/arcane/iris/util/common/plugin/VolmitPlugin.java b/core/src/main/java/art/arcane/iris/util/common/plugin/VolmitPlugin.java index bac02d157..7f084cd4b 100644 --- a/core/src/main/java/art/arcane/iris/util/common/plugin/VolmitPlugin.java +++ b/core/src/main/java/art/arcane/iris/util/common/plugin/VolmitPlugin.java @@ -245,7 +245,7 @@ public abstract class VolmitPlugin extends JavaPlugin implements Listener { @Override public void onDisable() { stop(); - Bukkit.getScheduler().cancelTasks(this); + J.cancelPluginTasks(); unregisterListener(this); unregisterAll(); } @@ -339,6 +339,10 @@ public abstract class VolmitPlugin extends JavaPlugin implements Listener { @Override public List onTabComplete(CommandSender sender, Command command, String alias, String[] args) { + if (commands == null || commands.isEmpty()) { + return super.onTabComplete(sender, command, alias, args); + } + KList chain = new KList<>(); for (String i : args) { @@ -370,6 +374,9 @@ public abstract class VolmitPlugin extends JavaPlugin implements Listener { if (bad) { return false; } + if (commands == null || commands.isEmpty()) { + return false; + } KList chain = new KList<>(); chain.add(args); @@ -470,6 +477,9 @@ public abstract class VolmitPlugin extends JavaPlugin implements Listener { if (bad) { return; } + if (commands == null || commands.isEmpty()) { + return; + } for (VirtualCommand i : commands.v()) { try { unregisterCommand(i.getCommand()); @@ -485,8 +495,15 @@ public abstract class VolmitPlugin extends JavaPlugin implements Listener { return; } for (org.bukkit.permissions.Permission i : computePermissions()) { - Bukkit.getPluginManager().removePermission(i); - v("Unregistered Permission " + i.getName()); + if (i == null) { + continue; + } + try { + Bukkit.getPluginManager().removePermission(i); + v("Unregistered Permission " + i.getName()); + } catch (Throwable e) { + Iris.reportError(e); + } } } diff --git a/core/src/main/java/art/arcane/iris/util/common/scheduling/J.java b/core/src/main/java/art/arcane/iris/util/common/scheduling/J.java index 827c68022..e69ba8e2e 100644 --- a/core/src/main/java/art/arcane/iris/util/common/scheduling/J.java +++ b/core/src/main/java/art/arcane/iris/util/common/scheduling/J.java @@ -20,29 +20,39 @@ package art.arcane.iris.util.scheduling; import art.arcane.iris.Iris; import art.arcane.iris.core.service.PreservationSVC; +import art.arcane.iris.util.parallel.MultiBurst; import art.arcane.volmlib.util.function.NastyFunction; import art.arcane.volmlib.util.function.NastyFuture; import art.arcane.volmlib.util.function.NastyRunnable; import art.arcane.volmlib.util.function.NastySupplier; +import art.arcane.volmlib.util.math.FinalInteger; import art.arcane.volmlib.util.scheduling.AR; +import art.arcane.volmlib.util.scheduling.FoliaScheduler; import art.arcane.volmlib.util.scheduling.JSupport; import art.arcane.volmlib.util.scheduling.SR; import art.arcane.volmlib.util.scheduling.SchedulerBridge; import art.arcane.volmlib.util.scheduling.StartupQueueSupport; -import art.arcane.volmlib.util.math.FinalInteger; -import art.arcane.iris.util.parallel.MultiBurst; import org.bukkit.Bukkit; +import org.bukkit.Location; +import org.bukkit.World; +import org.bukkit.entity.Entity; +import org.bukkit.plugin.Plugin; +import java.util.Map; import java.util.concurrent.Callable; import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.Future; +import java.util.concurrent.atomic.AtomicInteger; import java.util.function.Consumer; import java.util.function.Function; import java.util.function.Supplier; @SuppressWarnings("ALL") public class J { - private static int tid = 0; + private static final long TICK_MS = 50L; + private static final AtomicInteger TASK_IDS = new AtomicInteger(1); + private static final Map REPEATING_CANCELLERS = new ConcurrentHashMap<>(); private static final StartupQueueSupport STARTUP_QUEUE = new StartupQueueSupport(); static { @@ -100,10 +110,13 @@ public class J { } public static void aBukkit(Runnable a) { - if (!Bukkit.getPluginManager().isPluginEnabled(Iris.instance)) { + if (!isPluginEnabled()) { return; } - Bukkit.getScheduler().scheduleAsyncDelayedTask(Iris.instance, a); + + if (!runAsyncImmediate(a)) { + a(a, 0); + } } public static Future a(Callable a) { @@ -142,89 +155,210 @@ public class J { return JSupport.attempt(t::get, i, Iris::reportError); } - /** - * Dont call this unless you know what you are doing! - */ public static void executeAfterStartupQueue() { JSupport.executeAfterStartupQueue(STARTUP_QUEUE, J::s, J::a); } - /** - * Schedule a sync task to be run right after startup. If the server has already - * started ticking, it will simply run it in a sync task. - *

- * If you dont know if you should queue this or not, do so, it's pretty - * forgiving. - * - * @param r the runnable - */ public static void ass(Runnable r) { JSupport.enqueueAfterStartupSync(STARTUP_QUEUE, r, J::s); } - /** - * Schedule an async task to be run right after startup. If the server has - * already started ticking, it will simply run it in an async task. - *

- * If you dont know if you should queue this or not, do so, it's pretty - * forgiving. - * - * @param r the runnable - */ public static void asa(Runnable r) { JSupport.enqueueAfterStartupAsync(STARTUP_QUEUE, r, J::a); } - /** - * Queue a sync task - * - * @param r the runnable - */ - public static void s(Runnable r) { - if (!Bukkit.getPluginManager().isPluginEnabled(Iris.instance)) { + public static boolean isFolia() { + return FoliaScheduler.isFolia(Iris.instance); + } + + public static boolean isPrimaryThread() { + return FoliaScheduler.isPrimaryThread(); + } + + public static boolean isOwnedByCurrentRegion(Entity entity) { + return FoliaScheduler.isOwnedByCurrentRegion(entity); + } + + public static boolean isOwnedByCurrentRegion(World world, int chunkX, int chunkZ) { + return FoliaScheduler.isOwnedByCurrentRegion(world, chunkX, chunkZ); + } + + public static boolean runEntity(Entity entity, Runnable runnable) { + if (entity == null || runnable == null) { + return false; + } + + if (isFolia()) { + if (isOwnedByCurrentRegion(entity)) { + runnable.run(); + return true; + } + + return runEntityImmediate(entity, runnable); + } + + if (isPrimaryThread()) { + runnable.run(); + return true; + } + + s(runnable); + return true; + } + + public static boolean runEntity(Entity entity, Runnable runnable, int delayTicks) { + if (entity == null || runnable == null) { + return false; + } + + if (delayTicks <= 0) { + return runEntity(entity, runnable); + } + + if (isFolia() && runEntityDelayed(entity, runnable, delayTicks)) { + return true; + } + + s(() -> runEntity(entity, runnable), delayTicks); + return true; + } + + public static boolean runRegion(World world, int chunkX, int chunkZ, Runnable runnable) { + if (world == null || runnable == null) { + return false; + } + + if (isFolia() && isOwnedByCurrentRegion(world, chunkX, chunkZ)) { + runnable.run(); + return true; + } + + if (runRegionImmediate(world, chunkX, chunkZ, runnable)) { + return true; + } + + if (isFolia()) { + Iris.verbose("Failed to schedule immediate region task for " + world.getName() + "@" + chunkX + "," + chunkZ + " on Folia."); + return false; + } + + s(runnable); + return true; + } + + public static boolean runRegion(World world, int chunkX, int chunkZ, Runnable runnable, int delayTicks) { + if (world == null || runnable == null) { + return false; + } + + if (delayTicks <= 0) { + return runRegion(world, chunkX, chunkZ, runnable); + } + + if (runRegionDelayed(world, chunkX, chunkZ, runnable, delayTicks)) { + return true; + } + + if (isFolia()) { + Iris.verbose("Failed to schedule delayed region task for " + world.getName() + "@" + chunkX + "," + chunkZ + + " (" + delayTicks + "t) on Folia."); + return false; + } + + s(runnable, delayTicks); + return true; + } + + public static boolean runAt(Location location, Runnable runnable) { + if (location == null || runnable == null || location.getWorld() == null) { + return false; + } + + return runRegion(location.getWorld(), location.getBlockX() >> 4, location.getBlockZ() >> 4, runnable); + } + + public static boolean runAt(Location location, Runnable runnable, int delayTicks) { + if (location == null || runnable == null || location.getWorld() == null) { + return false; + } + + return runRegion(location.getWorld(), location.getBlockX() >> 4, location.getBlockZ() >> 4, runnable, delayTicks); + } + + public static void cancelPluginTasks() { + if (Iris.instance == null) { return; } - Bukkit.getScheduler().scheduleSyncDelayedTask(Iris.instance, r); + + FoliaScheduler.cancelTasks(Iris.instance); + + try { + Bukkit.getScheduler().cancelTasks(Iris.instance); + } catch (UnsupportedOperationException ex) { + // Folia blocks BukkitScheduler usage. + Iris.verbose("Skipping BukkitScheduler#cancelTasks for Iris on this server."); + } + } + + public static void s(Runnable r) { + if (!isPluginEnabled()) { + return; + } + + if (!runGlobalImmediate(r)) { + try { + Bukkit.getScheduler().scheduleSyncDelayedTask(Iris.instance, r); + } catch (UnsupportedOperationException e) { + throw new IllegalStateException("Failed to schedule sync task (Folia scheduler unavailable, BukkitScheduler unsupported).", e); + } + } } public static CompletableFuture sfut(Runnable r) { CompletableFuture f = new CompletableFuture(); - if (!Bukkit.getPluginManager().isPluginEnabled(Iris.instance)) { + if (!isPluginEnabled()) { return null; } - Bukkit.getScheduler().scheduleSyncDelayedTask(Iris.instance, () -> { + + s(() -> { r.run(); f.complete(null); }); + return f; } public static CompletableFuture sfut(Supplier r) { CompletableFuture f = new CompletableFuture<>(); - if (!Bukkit.getPluginManager().isPluginEnabled(Iris.instance)) { + + if (!isPluginEnabled()) { return null; } - Bukkit.getScheduler().scheduleSyncDelayedTask(Iris.instance, () -> { + + s(() -> { try { f.complete(r.get()); } catch (Throwable e) { f.completeExceptionally(e); } }); + return f; } public static CompletableFuture sfut(Runnable r, int delay) { CompletableFuture f = new CompletableFuture(); - if (!Bukkit.getPluginManager().isPluginEnabled(Iris.instance)) { + if (!isPluginEnabled()) { return null; } - Bukkit.getScheduler().scheduleSyncDelayedTask(Iris.instance, () -> { + + s(() -> { r.run(); f.complete(null); }, delay); + return f; } @@ -237,53 +371,58 @@ public class J { return f; } - /** - * Queue a sync task - * - * @param r the runnable - * @param delay the delay to wait in ticks before running - */ public static void s(Runnable r, int delay) { + if (!isPluginEnabled()) { + return; + } + + if (delay <= 0) { + s(r); + return; + } + try { - if (!Bukkit.getPluginManager().isPluginEnabled(Iris.instance)) { - return; + if (!runGlobalDelayed(r, delay)) { + Bukkit.getScheduler().scheduleSyncDelayedTask(Iris.instance, r, delay); } - Bukkit.getScheduler().scheduleSyncDelayedTask(Iris.instance, r, delay); } catch (Throwable e) { Iris.reportError(e); } } - /** - * Cancel a sync repeating task - * - * @param id the task id - */ public static void csr(int id) { - Bukkit.getScheduler().cancelTask(id); + cancelRepeatingTask(id); } - /** - * Start a sync repeating task - * - * @param r the runnable - * @param interval the interval - * @return the task id - */ public static int sr(Runnable r, int interval) { - if (!Bukkit.getPluginManager().isPluginEnabled(Iris.instance)) { + if (!isPluginEnabled()) { return -1; } - return Bukkit.getScheduler().scheduleSyncRepeatingTask(Iris.instance, r, 0, interval); + + int safeInterval = Math.max(1, interval); + RepeatingState state = new RepeatingState(); + int taskId = trackRepeatingTask(() -> state.cancelled = true); + + Runnable[] loop = new Runnable[1]; + loop[0] = () -> { + if (state.cancelled || !isPluginEnabled()) { + REPEATING_CANCELLERS.remove(taskId); + return; + } + + r.run(); + if (state.cancelled || !isPluginEnabled()) { + REPEATING_CANCELLERS.remove(taskId); + return; + } + + s(loop[0], safeInterval); + }; + + s(loop[0]); + return taskId; } - /** - * Start a sync repeating task for a limited amount of ticks - * - * @param r the runnable - * @param interval the interval in ticks - * @param intervals the maximum amount of intervals to run - */ public static void sr(Runnable r, int interval, int intervals) { FinalInteger fi = new FinalInteger(0); @@ -300,50 +439,60 @@ public class J { }; } - /** - * Call an async task dealyed - * - * @param r the runnable - * @param delay the delay to wait before running - */ - @SuppressWarnings("deprecation") public static void a(Runnable r, int delay) { - if (Bukkit.getPluginManager().isPluginEnabled(Iris.instance)) { - Bukkit.getScheduler().scheduleAsyncDelayedTask(Iris.instance, r, delay); + if (!isPluginEnabled()) { + return; + } + + if (delay <= 0) { + if (!runAsyncImmediate(r)) { + a(r); + } + return; + } + + if (!runAsyncDelayed(r, delay)) { + a(() -> { + if (sleep(ticksToMilliseconds(delay))) { + r.run(); + } + }); } } - /** - * Cancel an async repeat task - * - * @param id the id - */ public static void car(int id) { - Bukkit.getScheduler().cancelTask(id); + cancelRepeatingTask(id); } - /** - * Start an async repeat task - * - * @param r the runnable - * @param interval the interval in ticks - * @return the task id - */ - @SuppressWarnings("deprecation") public static int ar(Runnable r, int interval) { - if (!Bukkit.getPluginManager().isPluginEnabled(Iris.instance)) { + if (!isPluginEnabled()) { return -1; } - return Bukkit.getScheduler().scheduleAsyncRepeatingTask(Iris.instance, r, 0, interval); + + int safeInterval = Math.max(1, interval); + RepeatingState state = new RepeatingState(); + int taskId = trackRepeatingTask(() -> state.cancelled = true); + + Runnable[] loop = new Runnable[1]; + loop[0] = () -> { + if (state.cancelled || !isPluginEnabled()) { + REPEATING_CANCELLERS.remove(taskId); + return; + } + + r.run(); + if (state.cancelled || !isPluginEnabled()) { + REPEATING_CANCELLERS.remove(taskId); + return; + } + + a(loop[0], safeInterval); + }; + + a(loop[0], 0); + return taskId; } - /** - * Start an async repeating task for a limited time - * - * @param r the runnable - * @param interval the interval - * @param intervals the intervals to run - */ public static void ar(Runnable r, int interval, int intervals) { FinalInteger fi = new FinalInteger(0); @@ -359,4 +508,61 @@ public class J { } }; } + + private static int trackRepeatingTask(Runnable cancelAction) { + int id = TASK_IDS.getAndIncrement(); + REPEATING_CANCELLERS.put(id, cancelAction); + return id; + } + + private static void cancelRepeatingTask(int id) { + Runnable cancelAction = REPEATING_CANCELLERS.remove(id); + if (cancelAction != null) { + cancelAction.run(); + } + } + + private static boolean isPluginEnabled() { + return Iris.instance != null && Bukkit.getPluginManager().isPluginEnabled(Iris.instance); + } + + private static long ticksToMilliseconds(int ticks) { + return Math.max(0L, ticks) * TICK_MS; + } + + private static boolean runGlobalImmediate(Runnable runnable) { + return FoliaScheduler.runGlobal(Iris.instance, runnable); + } + + private static boolean runGlobalDelayed(Runnable runnable, int delayTicks) { + return FoliaScheduler.runGlobal(Iris.instance, runnable, Math.max(0, delayTicks)); + } + + private static boolean runRegionImmediate(World world, int chunkX, int chunkZ, Runnable runnable) { + return FoliaScheduler.runRegion(Iris.instance, world, chunkX, chunkZ, runnable); + } + + private static boolean runRegionDelayed(World world, int chunkX, int chunkZ, Runnable runnable, int delayTicks) { + return FoliaScheduler.runRegion(Iris.instance, world, chunkX, chunkZ, runnable, Math.max(0, delayTicks)); + } + + private static boolean runAsyncImmediate(Runnable runnable) { + return FoliaScheduler.runAsync(Iris.instance, runnable); + } + + private static boolean runAsyncDelayed(Runnable runnable, int delayTicks) { + return FoliaScheduler.runAsync(Iris.instance, runnable, Math.max(0, delayTicks)); + } + + private static boolean runEntityImmediate(Entity entity, Runnable runnable) { + return FoliaScheduler.runEntity(Iris.instance, entity, runnable); + } + + private static boolean runEntityDelayed(Entity entity, Runnable runnable, int delayTicks) { + return FoliaScheduler.runEntity(Iris.instance, entity, runnable, Math.max(0, delayTicks)); + } + + private static final class RepeatingState { + private volatile boolean cancelled; + } } diff --git a/core/src/main/java/art/arcane/iris/util/project/agent/Agent.java b/core/src/main/java/art/arcane/iris/util/project/agent/Agent.java index 69edd90fe..616b67efc 100644 --- a/core/src/main/java/art/arcane/iris/util/project/agent/Agent.java +++ b/core/src/main/java/art/arcane/iris/util/project/agent/Agent.java @@ -5,6 +5,7 @@ import net.bytebuddy.agent.ByteBuddyAgent; import net.bytebuddy.dynamic.loading.ClassReloadingStrategy; import java.io.File; +import java.io.InputStream; import java.lang.instrument.Instrumentation; import java.nio.file.Files; import java.nio.file.StandardCopyOption; @@ -30,17 +31,57 @@ public class Agent { public static boolean install() { if (isInstalled()) return true; + + if (!ensureAgentJar()) + return false; + try { - Files.copy(Iris.instance.getResource("agent.jar"), AGENT_JAR.toPath(), StandardCopyOption.REPLACE_EXISTING); Iris.info("Installing Java Agent..."); Iris.info("Note: JVM [Attach Listener/ERROR] [STDERR] warning lines during this step are expected and not Iris errors."); ByteBuddyAgent.attach(AGENT_JAR, ByteBuddyAgent.ProcessProvider.ForCurrentVm.INSTANCE); } catch (Throwable e) { - e.printStackTrace(); + Iris.error("Failed to install Java Agent: " + e.getMessage()); + Iris.reportError(e); } return doGetInstrumentation() != null; } + private static boolean ensureAgentJar() { + File parent = AGENT_JAR.getParentFile(); + if (parent != null && !parent.exists() && !parent.mkdirs() && !parent.exists()) { + Iris.error("Failed to create Iris plugin data folder for Java agent: " + parent.getAbsolutePath()); + return false; + } + + try (InputStream in = openBundledAgentJar()) { + if (in == null) { + if (AGENT_JAR.isFile() && AGENT_JAR.length() > 0) { + Iris.warn("Bundled agent.jar not found in Iris plugin jar. Reusing existing " + AGENT_JAR.getAbsolutePath()); + return true; + } + + Iris.error("Bundled agent.jar was not found in Iris plugin jar. Rebuild/deploy Iris with embedded agent.jar."); + return false; + } + + Files.copy(in, AGENT_JAR.toPath(), StandardCopyOption.REPLACE_EXISTING); + return true; + } catch (Throwable e) { + Iris.error("Failed to prepare Java agent jar: " + e.getMessage()); + Iris.reportError(e); + return false; + } + } + + private static InputStream openBundledAgentJar() { + InputStream stream = Iris.instance.getResource("agent.jar"); + if (stream != null) { + return stream; + } + + return Agent.class.getClassLoader().getResourceAsStream("agent.jar"); + } + private static Instrumentation doGetInstrumentation() { try { return (Instrumentation) Class.forName(NAME, true, ClassLoader.getSystemClassLoader()).getMethod("getInstrumentation").invoke(null); diff --git a/core/src/main/java/art/arcane/iris/util/project/profile/MsptTimings.java b/core/src/main/java/art/arcane/iris/util/project/profile/MsptTimings.java index d94eaa27d..ab5fbbeda 100644 --- a/core/src/main/java/art/arcane/iris/util/project/profile/MsptTimings.java +++ b/core/src/main/java/art/arcane/iris/util/project/profile/MsptTimings.java @@ -3,7 +3,6 @@ package art.arcane.iris.util.profile; import art.arcane.volmlib.util.math.M; import art.arcane.iris.util.scheduling.J; import art.arcane.volmlib.util.scheduling.Looper; -import org.bukkit.Bukkit; import java.util.concurrent.atomic.AtomicInteger; import java.util.function.Consumer; @@ -52,12 +51,13 @@ public abstract class MsptTimings extends Looper { protected abstract void update(int mspt); private boolean startTickTask() { - if (taskId != -1 && (Bukkit.getScheduler().isQueued(taskId) || Bukkit.getScheduler().isCurrentlyRunning(taskId))) + if (taskId != -1) return false; taskId = J.sr(() -> { if (isInterrupted()) { J.csr(taskId); + taskId = -1; return; } diff --git a/core/src/main/kotlin/art/arcane/iris/core/safeguard/IrisSafeguard.kt b/core/src/main/kotlin/art/arcane/iris/core/safeguard/IrisSafeguard.kt index 6a68d2bb8..33ac7517f 100644 --- a/core/src/main/kotlin/art/arcane/iris/core/safeguard/IrisSafeguard.kt +++ b/core/src/main/kotlin/art/arcane/iris/core/safeguard/IrisSafeguard.kt @@ -1,14 +1,12 @@ package art.arcane.iris.core.safeguard import art.arcane.iris.Iris -import art.arcane.iris.core.IrisSettings import art.arcane.iris.core.safeguard.task.Diagnostic import art.arcane.iris.core.safeguard.task.Task import art.arcane.iris.core.safeguard.task.ValueWithDiagnostics import art.arcane.iris.core.safeguard.task.tasks import art.arcane.iris.util.format.C import art.arcane.iris.util.scheduling.J -import org.bukkit.Bukkit import java.util.* object IrisSafeguard { @@ -93,51 +91,18 @@ object IrisSafeguard { private fun warning() { Iris.warn(C.GOLD.toString() + "Iris is running in Warning Mode") - - Iris.warn("") - Iris.warn(C.DARK_GRAY.toString() + "--==<" + C.GOLD + " IMPORTANT " + C.DARK_GRAY + ">==--") - Iris.warn(C.GOLD.toString() + "Iris is running in warning mode which may cause the following issues:") - Iris.warn("- Data Loss") - Iris.warn("- Errors") - Iris.warn("- Broken worlds") - Iris.warn("- Unexpected behavior.") - Iris.warn("- And perhaps further complications.") + Iris.warn(C.GRAY.toString() + "Some startup checks need attention. Review the messages above for tuning suggestions.") + Iris.warn(C.GRAY.toString() + "Iris will continue startup normally.") Iris.warn("") } private fun unstable() { - Iris.error(C.DARK_RED.toString() + "Iris is running in Unstable Mode") - + Iris.error(C.DARK_RED.toString() + "Iris is running in Danger Mode") Iris.error("") Iris.error(C.DARK_GRAY.toString() + "--==<" + C.RED + " IMPORTANT " + C.DARK_GRAY + ">==--") - Iris.error("Iris is running in unstable mode which may cause the following issues:") - Iris.error(C.DARK_RED.toString() + "Server Issues") - Iris.error("- Server won't boot") - Iris.error("- Data Loss") - Iris.error("- Unexpected behavior.") - Iris.error("- And More...") - Iris.error(C.DARK_RED.toString() + "World Issues") - Iris.error("- Worlds can't load due to corruption.") - Iris.error("- Worlds may slowly corrupt until they can't load.") - Iris.error("- World data loss.") - Iris.error("- And More...") - Iris.error(C.DARK_RED.toString() + "ATTENTION: " + C.RED + "While running Iris in unstable mode, you won't be eligible for support.") - - if (IrisSettings.get().general.isDoomsdayAnnihilationSelfDestructMode) { - Iris.error(C.DARK_RED.toString() + "Boot Unstable is set to true, continuing with the startup process in 10 seconds.") - J.sleep(10000L) - } else { - Iris.error(C.DARK_RED.toString() + "Go to plugins/iris/settings.json and set DoomsdayAnnihilationSelfDestructMode to true if you wish to proceed.") - Iris.error(C.DARK_RED.toString() + "The server will shutdown in 10 seconds.") - J.sleep(10000L) - Iris.error(C.DARK_RED.toString() + "Shutting down server.") - forceShutdown = true - try { - Bukkit.getPluginManager().disablePlugins() - } finally { - Runtime.getRuntime().halt(42) - } - } + Iris.error("Critical startup checks failed. Iris will continue startup in 10 seconds.") + Iris.error("Review and resolve the errors above as soon as possible.") + J.sleep(10000L) Iris.info("") } -} \ No newline at end of file +} 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 7ebd3ad9a..7707b157a 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 @@ -13,18 +13,25 @@ import art.arcane.iris.util.misc.getHardware import org.bukkit.Bukkit import java.util.Locale import java.util.stream.Collectors -import javax.tools.ToolProvider import kotlin.properties.PropertyDelegateProvider import kotlin.properties.ReadOnlyProperty private val memory by task { val mem = getHardware.getProcessMemory() - if (mem >= 5999) STABLE.withDiagnostics() - else STABLE.withDiagnostics( - WARN.create("Low Memory"), - WARN.create("- 6GB+ Ram is recommended"), - WARN.create("- Process Memory: $mem MB") - ) + when { + mem >= 3072 -> STABLE.withDiagnostics() + mem > 2048 -> STABLE.withDiagnostics( + INFO.create("Memory Recommendation"), + INFO.create("- 3GB+ process memory is recommended for Iris."), + INFO.create("- Process Memory: $mem MB") + ) + else -> WARNING.withDiagnostics( + WARN.create("Low Memory"), + WARN.create("- Iris is running with 2GB or less process memory."), + WARN.create("- 3GB+ process memory is recommended for Iris."), + WARN.create("- Process Memory: $mem MB") + ) + } } private val incompatibilities by task { @@ -49,6 +56,7 @@ private val incompatibilities by task { private val software by task { val supported = setOf( + "folia", "purpur", "pufferfish", "paper", @@ -59,7 +67,7 @@ private val software by task { if (supported.any { server.name.contains(it, true) }) STABLE.withDiagnostics() else WARNING.withDiagnostics( WARN.create("Unsupported Server Software"), - WARN.create("- Please consider using Paper or Purpur instead.") + WARN.create("- Please consider using Folia, Paper, or Purpur instead.") ) } @@ -118,12 +126,17 @@ private val diskSpace by task { private val java by task { val version = Iris.getJavaVersion() - val jdk = runCatching { ToolProvider.getSystemJavaCompiler() }.getOrNull() != null - if (version in setOf(21) && jdk) STABLE.withDiagnostics() - else WARNING.withDiagnostics( - WARN.create("Unsupported Java version"), - WARN.create("- Please consider using JDK 21 Instead of ${if(jdk) "JDK" else "JRE"} $version") - ) + when { + version == 21 -> STABLE.withDiagnostics() + version > 21 -> STABLE.withDiagnostics( + INFO.create("Java Runtime"), + INFO.create("- Running Java $version. Iris is tested primarily on Java 21.") + ) + else -> WARNING.withDiagnostics( + WARN.create("Unsupported Java version"), + WARN.create("- Java 21+ is recommended. Current runtime: Java $version") + ) + } } @@ -141,7 +154,7 @@ val tasks = listOf( private val server get() = Bukkit.getServer() private fun isPaperPreferredServer(): Boolean { val name = server.name.lowercase(Locale.ROOT) - return name.contains("paper") || name.contains("purpur") || name.contains("pufferfish") + return name.contains("folia") || name.contains("paper") || name.contains("purpur") || name.contains("pufferfish") } private fun MutableList.addAll(vararg values: T) = values.forEach(this::add) fun task(action: () -> ValueWithDiagnostics) = PropertyDelegateProvider> { _, _ -> diff --git a/core/src/main/resources/plugin.yml b/core/src/main/resources/plugin.yml index 99fa326be..ec4201169 100644 --- a/core/src/main/resources/plugin.yml +++ b/core/src/main/resources/plugin.yml @@ -1,11 +1,12 @@ -name: ${name} -version: ${version} -main: ${main} -load: STARTUP -authors: [ cyberpwn, NextdoorPsycho, Vatuu ] +name: ${name} +version: ${version} +main: ${main} +folia-supported: true +load: STARTUP +authors: [ cyberpwn, NextdoorPsycho, Vatuu ] website: volmit.com description: More than a Dimension! commands: iris: aliases: [ ir, irs ] -api-version: '${apiVersion}' \ No newline at end of file +api-version: '${apiVersion}' diff --git a/nms/v1_21_R7/src/main/java/art/arcane/iris/core/nms/v1_21_R7/NMSBinding.java b/nms/v1_21_R7/src/main/java/art/arcane/iris/core/nms/v1_21_R7/NMSBinding.java index 4193fed6c..8cbbaba6a 100644 --- a/nms/v1_21_R7/src/main/java/art/arcane/iris/core/nms/v1_21_R7/NMSBinding.java +++ b/nms/v1_21_R7/src/main/java/art/arcane/iris/core/nms/v1_21_R7/NMSBinding.java @@ -215,25 +215,46 @@ public class NMSBinding implements INMSBinding { @Override public void deserializeTile(KMap map, Location pos) { - net.minecraft.nbt.CompoundTag tag = (net.minecraft.nbt.CompoundTag) convertToTag(map, 0, 64); + if (map == null || pos == null || pos.getWorld() == null) { + return; + } + + Tag converted = convertToTag(map, 0, 64); + if (!(converted instanceof net.minecraft.nbt.CompoundTag tag)) { + return; + } + var level = ((CraftWorld) pos.getWorld()).getHandle(); var blockPos = new BlockPos(pos.getBlockX(), pos.getBlockY(), pos.getBlockZ()); - J.s(() -> merge(level, blockPos, tag)); + if (!J.runAt(pos, () -> merge(level, blockPos, tag))) { + Iris.warn("[NMS] Failed to schedule tile deserialize at " + blockPos + " in world " + pos.getWorld().getName()); + } } private void merge(ServerLevel level, BlockPos blockPos, net.minecraft.nbt.CompoundTag tag) { - var blockEntity = level.getBlockEntity(blockPos); - if (blockEntity == null) { - Iris.warn("[NMS] BlockEntity not found at " + blockPos); - var state = level.getBlockState(blockPos); - if (!state.hasBlockEntity()) - return; - - blockEntity = ((EntityBlock) state.getBlock()) - .newBlockEntity(blockPos, state); + if (level == null || blockPos == null || tag == null) { + return; + } + + try { + var blockEntity = level.getBlockEntity(blockPos); + if (blockEntity == null) { + Iris.warn("[NMS] BlockEntity not found at " + blockPos); + var state = level.getBlockState(blockPos); + if (!state.hasBlockEntity()) { + return; + } + + blockEntity = ((EntityBlock) state.getBlock()) + .newBlockEntity(blockPos, state); + } + + var accessor = new BlockDataAccessor(blockEntity, blockPos); + accessor.setData(accessor.getData().merge(tag)); + } catch (Throwable e) { + Iris.warn("[NMS] Failed to merge tile data at " + blockPos + ": " + e.getMessage()); + Iris.reportError(e); } - var accessor = new BlockDataAccessor(blockEntity, blockPos); - accessor.setData(accessor.getData().merge(tag)); } private Tag convertToTag(Object object, int depth, int maxDepth) { @@ -853,4 +874,4 @@ public class NMSBinding implements INMSBinding { } } } -} \ No newline at end of file +}