From 54094ce1d5811282ba4441f50e0aedd863fa863b Mon Sep 17 00:00:00 2001 From: Pyrbu Date: Mon, 16 Oct 2023 04:31:25 +0200 Subject: [PATCH] dynamic library loading (500kb jar :o) --- plugin/build.gradle | 21 ++- .../java/lol/pyr/znpcsplus/ZNpcsPlus.java | 69 ++++---- .../lol/pyr/znpcsplus/ZNpcsPlusBootstrap.java | 90 +++++++++++ .../znpcsplus/libraries/LibraryLoader.java | 119 ++++++++++++++ .../libraries/UrlClassLoaderAccess.java | 152 ++++++++++++++++++ .../updater/UpdateNotificationListener.java | 10 +- plugin/src/main/resources/plugin.yml | 2 +- 7 files changed, 416 insertions(+), 47 deletions(-) create mode 100644 plugin/src/main/java/lol/pyr/znpcsplus/ZNpcsPlusBootstrap.java create mode 100644 plugin/src/main/java/lol/pyr/znpcsplus/libraries/LibraryLoader.java create mode 100644 plugin/src/main/java/lol/pyr/znpcsplus/libraries/UrlClassLoaderAccess.java diff --git a/plugin/build.gradle b/plugin/build.gradle index 2e83d84..670d8a4 100644 --- a/plugin/build.gradle +++ b/plugin/build.gradle @@ -17,17 +17,18 @@ processResources { dependencies { compileOnly "me.clip:placeholderapi:2.11.3" // Placeholder support - implementation "com.google.code.gson:gson:2.10.1" // JSON parsing - implementation "org.bstats:bstats-bukkit:3.0.2" // Plugin stats - implementation "com.github.robertlit:SpigotResourcesAPI:2.0" // Spigot API wrapper for update checker - implementation "com.github.retrooper.packetevents:spigot:2.1.0-SNAPSHOT" // Packets - implementation "space.arim.dazzleconf:dazzleconf-ext-snakeyaml:1.2.1" // Configs - implementation "lol.pyr:director-adventure:2.1.1" // Commands + compileOnly "com.google.code.gson:gson:2.10.1" // JSON parsing + compileOnly "org.bstats:bstats-bukkit:3.0.2" // Plugin stats + compileOnly "com.github.robertlit:SpigotResourcesAPI:2.0" // Spigot API wrapper for update checker + compileOnly "com.github.retrooper.packetevents:spigot:2.1.0-SNAPSHOT" // Packets + compileOnly "space.arim.dazzleconf:dazzleconf-ext-snakeyaml:1.2.1" // Configs + compileOnly "lol.pyr:director-adventure:2.1.1" // Commands // Fancy text library - implementation "net.kyori:adventure-platform-bukkit:4.3.0" - implementation "net.kyori:adventure-text-minimessage:4.14.0" + compileOnly "net.kyori:adventure-platform-bukkit:4.3.1" + compileOnly "net.kyori:adventure-text-minimessage:4.14.0" + implementation "me.lucko:jar-relocator:1.7" implementation project(":api") } @@ -35,6 +36,10 @@ shadowJar { archivesBaseName = "ZNPCsPlus" archiveClassifier.set "" + relocate "org.objectweb.asm", "lol.pyr.znpcsplus.lib.asm" + relocate "me.lucko.jarrelocator", "lol.pyr.znpcsplus.lib.jarrelocator" + + // When changing anything here remember to also update the bootstrap relocate "org.bstats", "lol.pyr.znpcsplus.lib.bstats" relocate "me.robertlit.spigotresources", "lol.pyr.znpcsplus.lib.spigotresources" relocate "net.kyori", "lol.pyr.znpcsplus.lib.kyori" diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/ZNpcsPlus.java b/plugin/src/main/java/lol/pyr/znpcsplus/ZNpcsPlus.java index e076bc5..0d2131b 100644 --- a/plugin/src/main/java/lol/pyr/znpcsplus/ZNpcsPlus.java +++ b/plugin/src/main/java/lol/pyr/znpcsplus/ZNpcsPlus.java @@ -40,8 +40,8 @@ import lol.pyr.znpcsplus.scheduling.TaskScheduler; import lol.pyr.znpcsplus.skin.cache.MojangSkinCache; import lol.pyr.znpcsplus.skin.cache.SkinCacheCleanTask; import lol.pyr.znpcsplus.tasks.HologramRefreshTask; -import lol.pyr.znpcsplus.tasks.ViewableHideOnLeaveListener; import lol.pyr.znpcsplus.tasks.NpcProcessorTask; +import lol.pyr.znpcsplus.tasks.ViewableHideOnLeaveListener; import lol.pyr.znpcsplus.updater.UpdateChecker; import lol.pyr.znpcsplus.updater.UpdateNotificationListener; import lol.pyr.znpcsplus.user.UserListener; @@ -51,34 +51,36 @@ import net.kyori.adventure.platform.bukkit.BukkitAudiences; import net.kyori.adventure.text.Component; import net.kyori.adventure.text.format.NamedTextColor; import net.kyori.adventure.text.format.TextColor; -import net.kyori.adventure.text.minimessage.MiniMessage; import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer; import org.bstats.bukkit.Metrics; import org.bukkit.*; import org.bukkit.plugin.Plugin; +import org.bukkit.plugin.PluginDescriptionFile; import org.bukkit.plugin.PluginManager; -import org.bukkit.plugin.java.JavaPlugin; import java.io.File; import java.io.IOException; import java.io.PrintWriter; -import java.io.Reader; import java.nio.file.Files; import java.nio.file.StandardOpenOption; -import java.util.*; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.List; -public class ZNpcsPlus extends JavaPlugin { +public class ZNpcsPlus { private final LegacyComponentSerializer textSerializer = LegacyComponentSerializer.builder() .character('&') .hexCharacter('#') .hexColors().build(); private final List shutdownTasks = new ArrayList<>(); - private PacketEventsAPI packetEvents; + private final PacketEventsAPI packetEvents; + private final ZNpcsPlusBootstrap bootstrap; - @Override - public void onLoad() { - packetEvents = SpigotPacketEventsBuilder.build(this); + public ZNpcsPlus(ZNpcsPlusBootstrap bootstrap) { + this.bootstrap = bootstrap; + packetEvents = SpigotPacketEventsBuilder.build(bootstrap); PacketEvents.setAPI(packetEvents); packetEvents.getSettings().checkForUpdates(false); packetEvents.load(); @@ -88,7 +90,6 @@ public class ZNpcsPlus extends JavaPlugin { Bukkit.getConsoleSender().sendMessage(str); } - @Override public void onEnable() { getDataFolder().mkdirs(); @@ -106,7 +107,7 @@ public class ZNpcsPlus extends JavaPlugin { } catch (IOException e) { log(ChatColor.RED + " * Moving legacy files to subfolder failed, plugin will shut down."); e.printStackTrace(); - pluginManager.disablePlugin(this); + pluginManager.disablePlugin(bootstrap); return; } @@ -114,12 +115,12 @@ public class ZNpcsPlus extends JavaPlugin { packetEvents.init(); - BukkitAudiences adventure = BukkitAudiences.create(this); + BukkitAudiences adventure = BukkitAudiences.create(bootstrap); shutdownTasks.add(adventure::close); log(ChatColor.WHITE + " * Initializing components..."); - TaskScheduler scheduler = FoliaUtil.isFolia() ? new FoliaScheduler(this) : new SpigotScheduler(this); + TaskScheduler scheduler = FoliaUtil.isFolia() ? new FoliaScheduler(bootstrap) : new SpigotScheduler(bootstrap); shutdownTasks.add(scheduler::cancelAll); ConfigManager configManager = new ConfigManager(getDataFolder()); @@ -136,8 +137,8 @@ public class ZNpcsPlus extends JavaPlugin { UserManager userManager = new UserManager(); shutdownTasks.add(userManager::shutdown); - - BungeeConnector bungeeConnector = new BungeeConnector(this); + + BungeeConnector bungeeConnector = new BungeeConnector(bootstrap); DataImporterRegistry importerRegistry = new DataImporterRegistry(configManager, adventure, scheduler, packetFactory, textSerializer, typeRegistry, getDataFolder().getParentFile(), propertyRegistry, skinCache, npcRegistry, bungeeConnector); @@ -150,23 +151,23 @@ public class ZNpcsPlus extends JavaPlugin { typeRegistry.registerDefault(packetEvents, propertyRegistry); actionRegistry.registerTypes(scheduler, adventure, textSerializer, bungeeConnector); packetEvents.getEventManager().registerListener(new InteractionPacketListener(userManager, npcRegistry, scheduler), PacketListenerPriority.MONITOR); - new Metrics(this, 18244); - pluginManager.registerEvents(new UserListener(userManager), this); + new Metrics(bootstrap, 18244); + pluginManager.registerEvents(new UserListener(userManager), bootstrap); registerCommands(npcRegistry, skinCache, adventure, actionRegistry, typeRegistry, propertyRegistry, importerRegistry, configManager); log(ChatColor.WHITE + " * Starting tasks..."); if (configManager.getConfig().checkForUpdates()) { - UpdateChecker updateChecker = new UpdateChecker(this.getDescription()); + UpdateChecker updateChecker = new UpdateChecker(getDescription()); scheduler.runDelayedTimerAsync(updateChecker, 5L, 6000L); - pluginManager.registerEvents(new UpdateNotificationListener(this, adventure, updateChecker), this); + pluginManager.registerEvents(new UpdateNotificationListener(this, adventure, updateChecker, scheduler), bootstrap); } scheduler.runDelayedTimerAsync(new NpcProcessorTask(npcRegistry, configManager, propertyRegistry), 60L, 3L); scheduler.runDelayedTimerAsync(new HologramRefreshTask(npcRegistry), 60L, 20L); scheduler.runDelayedTimerAsync(new SkinCacheCleanTask(skinCache), 1200, 1200); - pluginManager.registerEvents(new ViewableHideOnLeaveListener(), this); + pluginManager.registerEvents(new ViewableHideOnLeaveListener(), bootstrap); log(ChatColor.WHITE + " * Loading data..."); npcRegistry.reload(); @@ -188,7 +189,7 @@ public class ZNpcsPlus extends JavaPlugin { } } - NpcApiProvider.register(this, new ZNpcsPlusApi(npcRegistry, typeRegistry, propertyRegistry, skinCache)); + NpcApiProvider.register(bootstrap, new ZNpcsPlusApi(npcRegistry, typeRegistry, propertyRegistry, skinCache)); log(ChatColor.WHITE + " * Loading complete! (" + (System.currentTimeMillis() - before) + "ms)"); log(""); @@ -208,7 +209,6 @@ public class ZNpcsPlus extends JavaPlugin { } } - @Override public void onDisable() { NpcApiProvider.unregister(); for (Runnable runnable : shutdownTasks) runnable.run(); @@ -238,7 +238,7 @@ public class ZNpcsPlus extends JavaPlugin { ConfigManager configManager) { Message incorrectUsageMessage = context -> context.send(Component.text("Incorrect usage: /" + context.getUsage(), NamedTextColor.RED)); - CommandManager manager = new CommandManager(this, adventure, incorrectUsageMessage); + CommandManager manager = new CommandManager(bootstrap, adventure, incorrectUsageMessage); manager.registerParser(NpcTypeImpl.class, new NpcTypeParser(incorrectUsageMessage, typeRegistry)); manager.registerParser(NpcEntryImpl.class, new NpcEntryParser(npcRegistry, incorrectUsageMessage)); @@ -280,7 +280,7 @@ public class ZNpcsPlus extends JavaPlugin { registerEnumParser(manager, RabbitType.class, incorrectUsageMessage); registerEnumParser(manager, AttachDirection.class, incorrectUsageMessage); - manager.registerCommand("npc", new MultiCommand(loadHelpMessage("root")) + manager.registerCommand("npc", new MultiCommand(bootstrap.loadHelpMessage("root")) .addSubcommand("center", new CenterCommand(npcRegistry)) .addSubcommand("create", new CreateCommand(npcRegistry, typeRegistry)) .addSubcommand("reloadconfig", new ReloadConfigCommand(configManager)) @@ -292,14 +292,14 @@ public class ZNpcsPlus extends JavaPlugin { .addSubcommand("list", new ListCommand(npcRegistry)) .addSubcommand("near", new NearCommand(npcRegistry)) .addSubcommand("type", new TypeCommand(npcRegistry, typeRegistry)) - .addSubcommand("property", new MultiCommand(loadHelpMessage("property")) + .addSubcommand("property", new MultiCommand(bootstrap.loadHelpMessage("property")) .addSubcommand("set", new PropertySetCommand(npcRegistry)) .addSubcommand("remove", new PropertyRemoveCommand(npcRegistry))) - .addSubcommand("storage", new MultiCommand(loadHelpMessage("storage")) + .addSubcommand("storage", new MultiCommand(bootstrap.loadHelpMessage("storage")) .addSubcommand("save", new SaveAllCommand(npcRegistry)) .addSubcommand("reload", new LoadAllCommand(npcRegistry)) .addSubcommand("import", new ImportCommand(npcRegistry, importerRegistry))) - .addSubcommand("holo", new MultiCommand(loadHelpMessage("holo")) + .addSubcommand("holo", new MultiCommand(bootstrap.loadHelpMessage("holo")) .addSubcommand("add", new HoloAddCommand(npcRegistry)) .addSubcommand("additem", new HoloAddItemCommand(npcRegistry)) .addSubcommand("delete", new HoloDeleteCommand(npcRegistry)) @@ -310,7 +310,7 @@ public class ZNpcsPlus extends JavaPlugin { .addSubcommand("setitem", new HoloSetItemCommand(npcRegistry)) .addSubcommand("offset", new HoloOffsetCommand(npcRegistry)) .addSubcommand("refreshdelay", new HoloRefreshDelayCommand(npcRegistry))) - .addSubcommand("action", new MultiCommand(loadHelpMessage("action")) + .addSubcommand("action", new MultiCommand(bootstrap.loadHelpMessage("action")) .addSubcommand("add", new ActionAddCommand(npcRegistry, actionRegistry)) .addSubcommand("clear", new ActionClearCommand(npcRegistry)) .addSubcommand("delete", new ActionDeleteCommand(npcRegistry)) @@ -323,10 +323,11 @@ public class ZNpcsPlus extends JavaPlugin { manager.registerParser(clazz, new EnumParser<>(clazz, message)); } - private Message loadHelpMessage(String name) { - Reader reader = getTextResource("help-messages/" + name + ".txt"); - if (reader == null) throw new RuntimeException(name + ".txt is missing from the help-messages folder in the ZNPCsPlus jar!"); - Component component = MiniMessage.miniMessage().deserialize(FileUtil.dumpReaderAsString(reader)); - return context -> context.send(component); + public File getDataFolder() { + return bootstrap.getDataFolder(); + } + + public PluginDescriptionFile getDescription() { + return bootstrap.getDescription(); } } diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/ZNpcsPlusBootstrap.java b/plugin/src/main/java/lol/pyr/znpcsplus/ZNpcsPlusBootstrap.java new file mode 100644 index 0000000..867b36b --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/ZNpcsPlusBootstrap.java @@ -0,0 +1,90 @@ +package lol.pyr.znpcsplus; + +import lol.pyr.director.adventure.command.CommandContext; +import lol.pyr.director.common.message.Message; +import lol.pyr.znpcsplus.libraries.LibraryLoader; +import lol.pyr.znpcsplus.util.FileUtil; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.minimessage.MiniMessage; +import org.bukkit.plugin.java.JavaPlugin; + +import java.io.File; +import java.io.Reader; + +public class ZNpcsPlusBootstrap extends JavaPlugin { + private ZNpcsPlus zNpcsPlus; + + @Override + public void onLoad() { + getLogger().info("Downloading and loading libraries, this might take a while if this is the first time you're launching the plugin"); + LibraryLoader loader = new LibraryLoader(this, new File(getDataFolder(), "libraries")); + + loader.addRelocation(decrypt("org..bstats"), "lol.pyr.znpcsplus.lib.bstats"); + loader.addRelocation(decrypt("me..robertlit..spigotresources"), "lol.pyr.znpcsplus.lib.spigotresources"); + loader.addRelocation(decrypt("net..kyori"), "lol.pyr.znpcsplus.lib.kyori"); + loader.addRelocation(decrypt("org..checkerframework"), "lol.pyr.znpcsplus.lib.checkerframework"); + loader.addRelocation(decrypt("com..google"), "lol.pyr.znpcsplus.lib.google"); + loader.addRelocation(decrypt("com..github..retrooper..packetevents"), "lol.pyr.znpcsplus.lib.packetevents.api"); + loader.addRelocation(decrypt("io..github..retrooper..packetevents"), "lol.pyr.znpcsplus.lib.packetevents.impl"); + loader.addRelocation(decrypt("org..yaml..snakeyaml"), "lol.pyr.znpcsplus.lib.snakeyaml"); + loader.addRelocation(decrypt("space..arim..dazzleconf"), "lol.pyr.znpcsplus.lib.dazzleconf"); + loader.addRelocation(decrypt("lol..pyr..director"), "lol.pyr.znpcsplus.lib.command"); + + loader.loadLibrary(decrypt("com..google..guava"), "guava", "18.0"); + loader.loadLibrary(decrypt("com..google..code..gson"), "gson", "2.10.1"); + + loader.loadLibrary(decrypt("org..bstats"), "bstats-base", "3.0.2"); + loader.loadLibrary(decrypt("org..bstats"), "bstats-bukkit", "3.0.2"); + + loader.loadLibrary("com.github.robertlit", "SpigotResourcesAPI", "2.0", "https://jitpack.io"); + + loader.loadSnapshotLibrary(decrypt("com..github..retrooper..packetevents"), "api", "2.1.0-SNAPSHOT", "2.1.0-20231011.171910-4", "https://repo.codemc.io/repository/maven-snapshots/"); + loader.loadSnapshotLibrary(decrypt("com..github..retrooper..packetevents"), "spigot", "2.1.0-SNAPSHOT", "2.1.0-20231011.171910-4", "https://repo.codemc.io/repository/maven-snapshots/"); + + loader.loadLibrary(decrypt("space..arim..dazzleconf"), "dazzleconf-core", "1.2.1"); + loader.loadLibrary(decrypt("space..arim..dazzleconf"), "dazzleconf-ext-snakeyaml", "1.2.1"); + loader.loadLibrary("org.yaml", "snakeyaml", "1.33"); + + loader.loadLibrary("lol.pyr", "director-adventure", "2.1.1", "https://repo.pyr.lol/releases"); + + loader.loadLibrary(decrypt("net..kyori"), "adventure-api", "4.14.0"); + loader.loadLibrary(decrypt("net..kyori"), "adventure-key", "4.14.0"); + loader.loadLibrary(decrypt("net..kyori"), "adventure-nbt", "4.14.0"); + loader.loadLibrary(decrypt("net..kyori"), "adventure-platform-facet", "4.3.1"); + loader.loadLibrary(decrypt("net..kyori"), "adventure-platform-api", "4.3.1"); + loader.loadLibrary(decrypt("net..kyori"), "adventure-platform-bukkit", "4.3.1"); + loader.loadLibrary(decrypt("net..kyori"), "adventure-text-minimessage", "4.14.0"); + loader.loadLibrary(decrypt("net..kyori"), "adventure-text-serializer-bungeecord", "4.3.1"); + loader.loadLibrary(decrypt("net..kyori"), "adventure-text-serializer-gson", "4.14.0"); + loader.loadLibrary(decrypt("net..kyori"), "adventure-text-serializer-json", "4.14.0"); + loader.loadLibrary(decrypt("net..kyori"), "adventure-text-serializer-legacy", "4.14.0"); + loader.loadLibrary(decrypt("net..kyori"), "examination-api", "1.3.0"); + loader.loadLibrary(decrypt("net..kyori"), "examination-string", "1.3.0"); + loader.deleteUnloadedLibraries(); + + getLogger().info("Loaded " + loader.loadedLibraryCount() + " libraries!"); + zNpcsPlus = new ZNpcsPlus(this); + } + + @Override + public void onEnable() { + if (zNpcsPlus != null) zNpcsPlus.onEnable(); + } + + @Override + public void onDisable() { + if (zNpcsPlus != null) zNpcsPlus.onDisable(); + } + + protected Message loadHelpMessage(String name) { + Reader reader = getTextResource("help-messages/" + name + ".txt"); + if (reader == null) throw new RuntimeException(name + ".txt is missing from the help-messages folder in the ZNPCsPlus jar!"); + Component component = MiniMessage.miniMessage().deserialize(FileUtil.dumpReaderAsString(reader)); + return context -> context.send(component); + } + + // Ugly hack because of https://github.com/johnrengelman/shadow/issues/232 + private static String decrypt(String packageName) { + return packageName.replace("..", "."); + } +} diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/libraries/LibraryLoader.java b/plugin/src/main/java/lol/pyr/znpcsplus/libraries/LibraryLoader.java new file mode 100644 index 0000000..7d94674 --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/libraries/LibraryLoader.java @@ -0,0 +1,119 @@ +package lol.pyr.znpcsplus.libraries; + +import me.lucko.jarrelocator.JarRelocator; +import me.lucko.jarrelocator.Relocation; +import org.bukkit.plugin.Plugin; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.net.MalformedURLException; +import java.net.URL; +import java.net.URLClassLoader; +import java.nio.file.Files; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.logging.Logger; + +public class LibraryLoader { + private final static Logger logger = Logger.getLogger("ZNPCsPlus Library Loader"); + + private final UrlClassLoaderAccess loaderAccess; + private final File librariesFolder; + private final Set loadedLibraries = new HashSet<>(); + private final List relocationRules = new ArrayList<>(); + + public LibraryLoader(Plugin plugin, File librariesFolder) { + loaderAccess = UrlClassLoaderAccess.create((URLClassLoader) plugin.getClass().getClassLoader()); + this.librariesFolder = librariesFolder; + if (!librariesFolder.exists()) librariesFolder.mkdirs(); + } + + public void deleteUnloadedLibraries() { + File[] files = librariesFolder.listFiles(); + if (files == null) return; + for (File file : files) if (!loadedLibraries.contains(file)) file.delete(); + } + + public void addRelocation(String pre, String post) { + relocationRules.add(new Relocation(pre, post)); + } + + public void loadSnapshotLibrary(String groupId, String artifactId, String version, String snapshotVersion, String repoUrl) { + try { + loadLibrary(groupId + ":" + artifactId + ":" + version, + getDependencyFile(groupId, artifactId, version), + getSnapshotDependencyUrl(groupId, artifactId, version, snapshotVersion, repoUrl)); + } catch (MalformedURLException e) { + throw new RuntimeException(e); + } + } + + public int loadedLibraryCount() { + return loadedLibraries.size(); + } + + public void loadLibrary(String groupId, String artifactId, String version) { + loadLibrary(groupId, artifactId, version, "https://repo1.maven.org/maven2"); + } + + public void loadLibrary(String groupId, String artifactId, String version, String repoUrl) { + try { + loadLibrary(groupId + ":" + artifactId + ":" + version, + getDependencyFile(groupId, artifactId, version), + getDependencyUrl(groupId, artifactId, version, repoUrl)); + } catch (MalformedURLException e) { + throw new RuntimeException(e); + } + } + + private void loadLibrary(String name, File file, URL url) { + if (!file.exists()) { + try (InputStream in = url.openStream()) { + File temp = new File(file.getParentFile(), file.getName() + ".temp"); + Files.copy(in, temp.toPath()); + new JarRelocator(temp, file, relocationRules).run(); + temp.delete(); + // logger.info("Downloaded library " + name); + } catch (IOException e) { + logger.severe("Failed to download library " + name); + e.printStackTrace(); + } + } + + try { + loaderAccess.addURL(file.toURI().toURL()); + loadedLibraries.add(file); + // logger.info("Loaded library " + name); + } catch (Exception e) { + logger.severe("Failed to load library, plugin may not work correctly (" + name + ")"); + e.printStackTrace(); + } + } + + private File getDependencyFile(String groupId, String artifactId, String version) { + return new File(librariesFolder, groupId.replace(".", "-") + "-" + + artifactId.replace(".", "-") + "-" + + version.replace(".", "-") + ".jar"); + } + + private static URL getDependencyUrl(String groupId, String artifactId, String version, String repoUrl) throws MalformedURLException { + String url = repoUrl.endsWith("/") ? repoUrl : repoUrl + "/"; + url += groupId.replace(".", "/") + "/"; + url += artifactId + "/"; + url += version + "/"; + url += artifactId + "-" + version + ".jar"; + return new URL(url); + } + + private static URL getSnapshotDependencyUrl(String groupId, String artifactId, String version, String snapshotVersion, String repoUrl) throws MalformedURLException { + String url = repoUrl.endsWith("/") ? repoUrl : repoUrl + "/"; + url += groupId.replace(".", "/") + "/"; + url += artifactId + "/"; + url += version + "/"; + url += artifactId + "-" + snapshotVersion + ".jar"; + return new URL(url); + } +} diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/libraries/UrlClassLoaderAccess.java b/plugin/src/main/java/lol/pyr/znpcsplus/libraries/UrlClassLoaderAccess.java new file mode 100644 index 0000000..0e2b11d --- /dev/null +++ b/plugin/src/main/java/lol/pyr/znpcsplus/libraries/UrlClassLoaderAccess.java @@ -0,0 +1,152 @@ +package lol.pyr.znpcsplus.libraries; + +import javax.annotation.Nonnull; +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.net.URL; +import java.net.URLClassLoader; +import java.util.Collection; + +/** + * Provides access to {@link URLClassLoader}#addURL. + * From https://github.com/lucko/helper/blob/master/helper/src/main/java/me/lucko/helper/maven/URLClassLoaderAccess.java + */ +public abstract class UrlClassLoaderAccess { + + /** + * Creates a {@link UrlClassLoaderAccess} for the given class loader. + * + * @param classLoader the class loader + * @return the access object + */ + static UrlClassLoaderAccess create(URLClassLoader classLoader) { + if (Reflection.isSupported()) { + return new Reflection(classLoader); + } else if (Unsafe.isSupported()) { + return new Unsafe(classLoader); + } else { + return Noop.INSTANCE; + } + } + + private final URLClassLoader classLoader; + + protected UrlClassLoaderAccess(URLClassLoader classLoader) { + this.classLoader = classLoader; + } + + + /** + * Adds the given URL to the class loader. + * + * @param url the URL to add + */ + public abstract void addURL(@Nonnull URL url); + + /** + * Accesses using reflection, not supported on Java 9+. + */ + private static class Reflection extends UrlClassLoaderAccess { + private static final Method ADD_URL_METHOD; + + static { + Method addUrlMethod; + try { + addUrlMethod = URLClassLoader.class.getDeclaredMethod("addURL", URL.class); + addUrlMethod.setAccessible(true); + } catch (Exception e) { + addUrlMethod = null; + } + ADD_URL_METHOD = addUrlMethod; + } + + private static boolean isSupported() { + return ADD_URL_METHOD != null; + } + + Reflection(URLClassLoader classLoader) { + super(classLoader); + } + + @Override + public void addURL(@Nonnull URL url) { + try { + ADD_URL_METHOD.invoke(super.classLoader, url); + } catch (ReflectiveOperationException e) { + throw new RuntimeException(e); + } + } + } + + /** + * Accesses using sun.misc.Unsafe, supported on Java 9+. + * + * @author Vaishnav Anil (https://github.com/slimjar/slimjar) + */ + private static class Unsafe extends UrlClassLoaderAccess { + private static final sun.misc.Unsafe UNSAFE; + + static { + sun.misc.Unsafe unsafe; + try { + Field unsafeField = sun.misc.Unsafe.class.getDeclaredField("theUnsafe"); + unsafeField.setAccessible(true); + unsafe = (sun.misc.Unsafe) unsafeField.get(null); + } catch (Throwable t) { + unsafe = null; + } + UNSAFE = unsafe; + } + + private static boolean isSupported() { + return UNSAFE != null; + } + + private final Collection unopenedURLs; + private final Collection pathURLs; + + @SuppressWarnings("unchecked") + Unsafe(URLClassLoader classLoader) { + super(classLoader); + + Collection unopenedURLs; + Collection pathURLs; + try { + Object ucp = fetchField(URLClassLoader.class, classLoader, "ucp"); + unopenedURLs = (Collection) fetchField(ucp.getClass(), ucp, "unopenedUrls"); + pathURLs = (Collection) fetchField(ucp.getClass(), ucp, "path"); + } catch (Throwable e) { + unopenedURLs = null; + pathURLs = null; + } + this.unopenedURLs = unopenedURLs; + this.pathURLs = pathURLs; + } + + private static Object fetchField(final Class clazz, final Object object, final String name) throws NoSuchFieldException { + Field field = clazz.getDeclaredField(name); + long offset = UNSAFE.objectFieldOffset(field); + return UNSAFE.getObject(object, offset); + } + + @Override + public void addURL(@Nonnull URL url) { + this.unopenedURLs.add(url); + this.pathURLs.add(url); + } + } + + private static class Noop extends UrlClassLoaderAccess { + private static final Noop INSTANCE = new Noop(); + + private Noop() { + super(null); + } + + @Override + public void addURL(@Nonnull URL url) { + throw new UnsupportedOperationException(); + } + } + +} \ No newline at end of file diff --git a/plugin/src/main/java/lol/pyr/znpcsplus/updater/UpdateNotificationListener.java b/plugin/src/main/java/lol/pyr/znpcsplus/updater/UpdateNotificationListener.java index cfdf44b..f3b2493 100644 --- a/plugin/src/main/java/lol/pyr/znpcsplus/updater/UpdateNotificationListener.java +++ b/plugin/src/main/java/lol/pyr/znpcsplus/updater/UpdateNotificationListener.java @@ -1,11 +1,11 @@ package lol.pyr.znpcsplus.updater; import lol.pyr.znpcsplus.ZNpcsPlus; +import lol.pyr.znpcsplus.scheduling.TaskScheduler; import net.kyori.adventure.platform.bukkit.BukkitAudiences; import net.kyori.adventure.text.Component; import net.kyori.adventure.text.event.ClickEvent; import net.kyori.adventure.text.format.NamedTextColor; -import org.bukkit.Bukkit; import org.bukkit.event.EventHandler; import org.bukkit.event.Listener; import org.bukkit.event.player.PlayerJoinEvent; @@ -14,22 +14,24 @@ public class UpdateNotificationListener implements Listener { private final ZNpcsPlus plugin; private final BukkitAudiences adventure; private final UpdateChecker updateChecker; + private final TaskScheduler scheduler; - public UpdateNotificationListener(ZNpcsPlus plugin, BukkitAudiences adventure, UpdateChecker updateChecker) { + public UpdateNotificationListener(ZNpcsPlus plugin, BukkitAudiences adventure, UpdateChecker updateChecker, TaskScheduler scheduler) { this.plugin = plugin; this.adventure = adventure; this.updateChecker = updateChecker; + this.scheduler = scheduler; } @EventHandler public void onJoin(PlayerJoinEvent event) { if (!event.getPlayer().hasPermission("znpcsplus.updates")) return; if (updateChecker.getStatus() != UpdateChecker.Status.UPDATE_NEEDED) return; - Bukkit.getScheduler().runTaskLater(plugin, () -> { + scheduler.runLaterAsync(() -> { if (!event.getPlayer().isOnline()) return; adventure.player(event.getPlayer()) .sendMessage(Component.text(plugin.getDescription().getName() + " v" + updateChecker.getLatestVersion() + " is available now!", NamedTextColor.GOLD).appendNewline() - .append(Component.text("Click this message to open the Spigot page (CLICK)", NamedTextColor.YELLOW)).clickEvent(ClickEvent.openUrl(UpdateChecker.DOWNLOAD_LINK))); + .append(Component.text("Click this message to open the Spigot page (CLICK)", NamedTextColor.YELLOW)).clickEvent(ClickEvent.openUrl(UpdateChecker.DOWNLOAD_LINK))); }, 100L); } } diff --git a/plugin/src/main/resources/plugin.yml b/plugin/src/main/resources/plugin.yml index d6beb5b..44dc2d4 100644 --- a/plugin/src/main/resources/plugin.yml +++ b/plugin/src/main/resources/plugin.yml @@ -3,7 +3,7 @@ authors: - Pyr - D3v1s0m -main: lol.pyr.znpcsplus.ZNpcsPlus +main: lol.pyr.znpcsplus.ZNpcsPlusBootstrap load: POSTWORLD version: ${version}