From 34af0e46a6a7cabd1144d8e6a19d8fa77ca09652 Mon Sep 17 00:00:00 2001 From: yunuservices Date: Mon, 2 Feb 2026 13:25:07 +0300 Subject: [PATCH] feat: commandSender implementation --- java/patchbukkit/build.gradle.kts | 5 + .../java/org/patchbukkit/LibraryResolver.java | 131 ++++++ .../PatchBukkitPluginClassLoader.java | 26 +- .../patchbukkit/PatchBukkitPluginLoader.java | 43 +- .../patchbukkit/PatchBukkitPluginManager.java | 6 +- .../org/patchbukkit/PatchBukkitServer.java | 8 +- .../command/PatchBukkitCommandSender.java | 180 ++++++++ .../PatchBukkitConsoleCommandSender.java | 100 +++-- rust/src/java/plugin/manager.rs | 414 +++++++++++++++++- 9 files changed, 840 insertions(+), 73 deletions(-) create mode 100644 java/patchbukkit/src/main/java/org/patchbukkit/LibraryResolver.java create mode 100644 java/patchbukkit/src/main/java/org/patchbukkit/command/PatchBukkitCommandSender.java diff --git a/java/patchbukkit/build.gradle.kts b/java/patchbukkit/build.gradle.kts index 3887e8a..e6ff59b 100644 --- a/java/patchbukkit/build.gradle.kts +++ b/java/patchbukkit/build.gradle.kts @@ -13,6 +13,11 @@ dependencies { compileOnly("io.papermc.paper:paper-api:1.21.11-R0.1-SNAPSHOT") implementation("net.sf.jopt-simple:jopt-simple:6.0-alpha-3") implementation("io.github.astonbitecode:j4rs:0.24.0") + implementation("org.apache.maven:maven-resolver-provider:3.9.6") + implementation("org.apache.maven.resolver:maven-resolver-impl:1.9.18") + implementation("org.apache.maven.resolver:maven-resolver-connector-basic:1.9.18") + implementation("org.apache.maven.resolver:maven-resolver-transport-http:1.9.18") + implementation("org.apache.maven.resolver:maven-resolver-util:1.9.18") } java { diff --git a/java/patchbukkit/src/main/java/org/patchbukkit/LibraryResolver.java b/java/patchbukkit/src/main/java/org/patchbukkit/LibraryResolver.java new file mode 100644 index 0000000..881a637 --- /dev/null +++ b/java/patchbukkit/src/main/java/org/patchbukkit/LibraryResolver.java @@ -0,0 +1,131 @@ +package org.patchbukkit.loader; + +import java.io.File; +import java.util.ArrayList; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Set; +import org.apache.maven.repository.internal.MavenRepositorySystemUtils; +import org.eclipse.aether.DefaultRepositorySystemSession; +import org.eclipse.aether.RepositorySystem; +import org.eclipse.aether.RepositorySystemSession; +import org.eclipse.aether.artifact.DefaultArtifact; +import org.eclipse.aether.collection.CollectRequest; +import org.eclipse.aether.graph.Dependency; +import org.eclipse.aether.repository.LocalRepository; +import org.eclipse.aether.repository.RemoteRepository; +import org.eclipse.aether.resolution.DependencyRequest; +import org.eclipse.aether.resolution.DependencyResult; +import org.eclipse.aether.spi.connector.RepositoryConnectorFactory; +import org.eclipse.aether.spi.connector.transport.TransporterFactory; +import org.eclipse.aether.transport.http.HttpTransporterFactory; +import org.eclipse.aether.connector.basic.BasicRepositoryConnectorFactory; + +public final class LibraryResolver { + private LibraryResolver() {} + + public static List resolveLibraries( + String coordinates, + File baseDir + ) { + if (coordinates == null || coordinates.isBlank()) { + return List.of(); + } + + Set unique = new LinkedHashSet<>(); + for (String line : coordinates.split("\n")) { + String trimmed = line.trim(); + if (!trimmed.isEmpty()) { + unique.add(trimmed); + } + } + + if (unique.isEmpty()) { + return List.of(); + } + + if (!baseDir.exists()) { + baseDir.mkdirs(); + } + + RepositorySystem system = newRepositorySystem(); + RepositorySystemSession session = newSession(system, baseDir); + List repos = List.of( + new RemoteRepository.Builder( + "papermc", + "default", + "https://repo.papermc.io/repository/maven-public/" + ).build(), + new RemoteRepository.Builder( + "central", + "default", + "https://repo1.maven.org/maven2/" + ).build() + ); + + Set files = new LinkedHashSet<>(); + for (String coord : unique) { + try { + Dependency dependency = new Dependency( + new DefaultArtifact(coord), + "runtime" + ); + CollectRequest collectRequest = new CollectRequest( + dependency, + repos + ); + DependencyRequest request = new DependencyRequest( + collectRequest, + null + ); + DependencyResult result = system.resolveDependencies( + session, + request + ); + result + .getArtifactResults() + .forEach(artifactResult -> { + File file = artifactResult.getArtifact().getFile(); + if (file != null && file.exists()) { + files.add(file); + } + }); + } catch (Exception e) { + System.err.println( + "[PatchBukkit] Failed to resolve library " + + coord + + ": " + + e.getMessage() + ); + } + } + + return new ArrayList<>(files); + } + + private static RepositorySystem newRepositorySystem() { + var locator = MavenRepositorySystemUtils.newServiceLocator(); + locator.addService( + RepositoryConnectorFactory.class, + BasicRepositoryConnectorFactory.class + ); + locator.addService( + TransporterFactory.class, + HttpTransporterFactory.class + ); + return locator.getService(RepositorySystem.class); + } + + private static RepositorySystemSession newSession( + RepositorySystem system, + File baseDir + ) { + DefaultRepositorySystemSession session = + MavenRepositorySystemUtils.newSession(); + LocalRepository localRepo = new LocalRepository(baseDir); + session.setLocalRepositoryManager( + system.newLocalRepositoryManager(session, localRepo) + ); + return session; + } +} diff --git a/java/patchbukkit/src/main/java/org/patchbukkit/PatchBukkitPluginClassLoader.java b/java/patchbukkit/src/main/java/org/patchbukkit/PatchBukkitPluginClassLoader.java index 8cf4eee..21009c4 100644 --- a/java/patchbukkit/src/main/java/org/patchbukkit/PatchBukkitPluginClassLoader.java +++ b/java/patchbukkit/src/main/java/org/patchbukkit/PatchBukkitPluginClassLoader.java @@ -9,6 +9,8 @@ import java.io.InputStream; import java.net.MalformedURLException; import java.net.URL; import java.net.URLClassLoader; +import java.util.ArrayList; +import java.util.List; import java.util.jar.JarEntry; import java.util.jar.JarFile; import org.bukkit.plugin.InvalidDescriptionException; @@ -32,12 +34,34 @@ public class PatchBukkitPluginClassLoader public PatchBukkitPluginClassLoader(ClassLoader parent, File file) throws MalformedURLException, InvalidDescriptionException { - super(new URL[] { file.toURI().toURL() }, parent); + this(parent, file, new URL[0]); + } + + public PatchBukkitPluginClassLoader( + ClassLoader parent, + File file, + URL[] extraUrls + ) throws MalformedURLException, InvalidDescriptionException { + super(buildUrls(file, extraUrls), parent); this.file = file; this.description = loadDescription(file); this.dataFolder = new File(file.getParentFile(), description.getName()); } + private static URL[] buildUrls(File file, URL[] extraUrls) + throws MalformedURLException { + List urls = new ArrayList<>(); + urls.add(file.toURI().toURL()); + if (extraUrls != null) { + for (URL url : extraUrls) { + if (url != null) { + urls.add(url); + } + } + } + return urls.toArray(new URL[0]); + } + private static PluginDescriptionFile loadDescription(File file) throws InvalidDescriptionException { try (JarFile jar = new JarFile(file)) { diff --git a/java/patchbukkit/src/main/java/org/patchbukkit/PatchBukkitPluginLoader.java b/java/patchbukkit/src/main/java/org/patchbukkit/PatchBukkitPluginLoader.java index 8a33072..4a32cd4 100644 --- a/java/patchbukkit/src/main/java/org/patchbukkit/PatchBukkitPluginLoader.java +++ b/java/patchbukkit/src/main/java/org/patchbukkit/PatchBukkitPluginLoader.java @@ -1,6 +1,9 @@ package org.patchbukkit.loader; import java.io.File; +import java.net.URL; +import java.util.LinkedHashSet; +import java.util.List; import java.util.Map; import java.util.Set; import java.util.regex.Pattern; @@ -19,7 +22,12 @@ import org.bukkit.plugin.java.JavaPlugin; @SuppressWarnings({ "deprecation", "removal" }) public class PatchBukkitPluginLoader implements PluginLoader { - public static JavaPlugin createPlugin(String jarPath, String mainClass) { + public static JavaPlugin createPlugin( + String jarPath, + String mainClass, + String extraClasspath, + String libraryCoordinates + ) { try { File jarFile = new File(jarPath); if (!jarFile.exists()) { @@ -29,10 +37,41 @@ public class PatchBukkitPluginLoader implements PluginLoader { return null; } + LinkedHashSet extraUrls = new LinkedHashSet<>(); + if (extraClasspath != null && !extraClasspath.isBlank()) { + String[] paths = extraClasspath.split(File.pathSeparator); + for (String path : paths) { + if (path == null || path.isBlank()) { + continue; + } + File extraFile = new File(path.trim()); + if (extraFile.exists()) { + extraUrls.add(extraFile.toURI().toURL()); + } + } + } + + if (libraryCoordinates != null && !libraryCoordinates.isBlank()) { + File libsDir = new File(jarFile.getParentFile(), "patchbukkit-libs"); + if (!libsDir.exists()) { + libsDir.mkdirs(); + } + List libraries = LibraryResolver.resolveLibraries( + libraryCoordinates, + libsDir + ); + for (File lib : libraries) { + if (lib != null && lib.exists()) { + extraUrls.add(lib.toURI().toURL()); + } + } + } + PatchBukkitPluginClassLoader classLoader = new PatchBukkitPluginClassLoader( PatchBukkitPluginLoader.class.getClassLoader(), - jarFile + jarFile, + extraUrls.toArray(new URL[0]) ); Class jarClass = Class.forName(mainClass, true, classLoader); diff --git a/java/patchbukkit/src/main/java/org/patchbukkit/PatchBukkitPluginManager.java b/java/patchbukkit/src/main/java/org/patchbukkit/PatchBukkitPluginManager.java index 92aaed9..409d9d9 100644 --- a/java/patchbukkit/src/main/java/org/patchbukkit/PatchBukkitPluginManager.java +++ b/java/patchbukkit/src/main/java/org/patchbukkit/PatchBukkitPluginManager.java @@ -38,9 +38,13 @@ public class PatchBukkitPluginManager implements PluginManager { public void registerInterface(@NotNull Class loader) throws IllegalArgumentException {} + public void registerPlugin(@NotNull Plugin plugin) { + plugins.put(plugin.getName().toLowerCase(), plugin); + } + @Override public @Nullable Plugin getPlugin(@NotNull String name) { - return plugins.get(name); + return plugins.get(name.toLowerCase()); } @Override diff --git a/java/patchbukkit/src/main/java/org/patchbukkit/PatchBukkitServer.java b/java/patchbukkit/src/main/java/org/patchbukkit/PatchBukkitServer.java index 1351356..bbfb6ec 100644 --- a/java/patchbukkit/src/main/java/org/patchbukkit/PatchBukkitServer.java +++ b/java/patchbukkit/src/main/java/org/patchbukkit/PatchBukkitServer.java @@ -97,6 +97,8 @@ public class PatchBukkitServer implements Server { private final String bukkitVersion = Versioning.getBukkitVersion(); private final CommandMap commandMap = new PatchBukkitCommandMap(); private final BukkitScheduler scheduler = new PatchBukkitScheduler(); + private final PatchBukkitPluginManager pluginManager = + new PatchBukkitPluginManager(this); private final Map onlinePlayers = new java.util.concurrent.ConcurrentHashMap<>(); @@ -122,6 +124,10 @@ public class PatchBukkitServer implements Server { } } + public void registerPlugin(@NotNull Plugin plugin) { + this.pluginManager.registerPlugin(plugin); + } + @Override public void sendPluginMessage( @NotNull Plugin source, @@ -457,7 +463,7 @@ public class PatchBukkitServer implements Server { @Override public @NotNull PluginManager getPluginManager() { - return new PatchBukkitPluginManager(this); + return this.pluginManager; } @Override diff --git a/java/patchbukkit/src/main/java/org/patchbukkit/command/PatchBukkitCommandSender.java b/java/patchbukkit/src/main/java/org/patchbukkit/command/PatchBukkitCommandSender.java new file mode 100644 index 0000000..7d5cd6a --- /dev/null +++ b/java/patchbukkit/src/main/java/org/patchbukkit/command/PatchBukkitCommandSender.java @@ -0,0 +1,180 @@ +package org.patchbukkit.command; + +import java.util.Set; +import java.util.UUID; + +import org.bukkit.Bukkit; +import org.bukkit.Server; +import org.bukkit.command.CommandSender; +import org.bukkit.permissions.PermissibleBase; +import org.bukkit.permissions.Permission; +import org.bukkit.permissions.PermissionAttachment; +import org.bukkit.permissions.PermissionAttachmentInfo; +import org.bukkit.permissions.ServerOperator; +import org.bukkit.plugin.Plugin; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import net.kyori.adventure.text.Component; +import net.md_5.bungee.api.chat.BaseComponent; + +public class PatchBukkitCommandSender implements CommandSender, ServerOperator { + + private final Server server; + private final String name; + private final PermissibleBase perm; + private boolean op; + + private final Spigot spigot = new Spigot() { + @Override + public void sendMessage(@NotNull BaseComponent component) { + PatchBukkitCommandSender.this.sendMessage(BaseComponent.toLegacyText(component)); + } + + @Override + public void sendMessage(@NotNull BaseComponent... components) { + PatchBukkitCommandSender.this.sendMessage(BaseComponent.toLegacyText(components)); + } + + @Override + public void sendMessage(@NotNull UUID sender, @NotNull BaseComponent component) { + PatchBukkitCommandSender.this.sendMessage(sender, BaseComponent.toLegacyText(component)); + } + + @Override + public void sendMessage(@NotNull UUID sender, @NotNull BaseComponent... components) { + PatchBukkitCommandSender.this.sendMessage(sender, BaseComponent.toLegacyText(components)); + } + }; + + public PatchBukkitCommandSender() { + this(Bukkit.getServer(), "CommandSender"); + } + + public PatchBukkitCommandSender(@Nullable Server server, @Nullable String name) { + this.server = server != null ? server : Bukkit.getServer(); + this.name = name != null ? name : "CommandSender"; + this.op = false; + this.perm = new PermissibleBase(this); + } + + @Override + public boolean isPermissionSet(@NotNull String name) { + return this.perm.isPermissionSet(name); + } + + @Override + public boolean isPermissionSet(@NotNull Permission perm) { + return this.perm.isPermissionSet(perm); + } + + @Override + public boolean hasPermission(@NotNull String name) { + return this.perm.hasPermission(name); + } + + @Override + public boolean hasPermission(@NotNull Permission perm) { + return this.perm.hasPermission(perm); + } + + @Override + public @NotNull PermissionAttachment addAttachment(@NotNull Plugin plugin, @NotNull String name, boolean value) { + return this.perm.addAttachment(plugin, name, value); + } + + @Override + public @NotNull PermissionAttachment addAttachment(@NotNull Plugin plugin) { + return this.perm.addAttachment(plugin); + } + + @Override + public @Nullable PermissionAttachment addAttachment(@NotNull Plugin plugin, @NotNull String name, boolean value, + int ticks) { + return this.perm.addAttachment(plugin, name, value, ticks); + } + + @Override + public @Nullable PermissionAttachment addAttachment(@NotNull Plugin plugin, int ticks) { + return this.perm.addAttachment(plugin, ticks); + } + + @Override + public void removeAttachment(@NotNull PermissionAttachment attachment) { + this.perm.removeAttachment(attachment); + } + + @Override + public void recalculatePermissions() { + this.perm.recalculatePermissions(); + } + + @Override + public @NotNull Set getEffectivePermissions() { + return this.perm.getEffectivePermissions(); + } + + @Override + public boolean isOp() { + return this.op; + } + + @Override + public void setOp(boolean value) { + this.op = value; + this.perm.recalculatePermissions(); + } + + @Override + public void sendMessage(@NotNull String message) { + if (message == null) { + return; + } + if (this.server != null && this.server.getLogger() != null) { + this.server.getLogger().info(message); + } else { + System.out.println(message); + } + } + + @Override + public void sendMessage(@NotNull String... messages) { + if (messages == null) { + return; + } + for (String message : messages) { + this.sendMessage(message); + } + } + + @Override + public void sendMessage(@Nullable UUID sender, @NotNull String message) { + this.sendMessage(message); + } + + @Override + public void sendMessage(@Nullable UUID sender, @NotNull String... messages) { + this.sendMessage(messages); + } + + @Override + public @NotNull Server getServer() { + return this.server != null ? this.server : Bukkit.getServer(); + } + + @Override + public @NotNull String getName() { + return this.name; + } + + @Override + public @NotNull Spigot spigot() { + return this.spigot; + } + + @Override + public @NotNull Component name() { + return Component.text(this.getName()); + } + +} diff --git a/java/patchbukkit/src/main/java/org/patchbukkit/command/PatchBukkitConsoleCommandSender.java b/java/patchbukkit/src/main/java/org/patchbukkit/command/PatchBukkitConsoleCommandSender.java index 9117b05..4c2d049 100644 --- a/java/patchbukkit/src/main/java/org/patchbukkit/command/PatchBukkitConsoleCommandSender.java +++ b/java/patchbukkit/src/main/java/org/patchbukkit/command/PatchBukkitConsoleCommandSender.java @@ -12,35 +12,40 @@ import org.bukkit.permissions.Permission; import org.bukkit.permissions.PermissionAttachment; import org.bukkit.permissions.PermissionAttachmentInfo; import org.bukkit.plugin.Plugin; -import org.checkerframework.checker.units.qual.m; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; -import org.patchbukkit.PatchBukkitServer; import net.kyori.adventure.text.Component; -public class PatchBukkitConsoleCommandSender implements ConsoleCommandSender { +public class PatchBukkitConsoleCommandSender + extends PatchBukkitCommandSender + implements ConsoleCommandSender +{ + + private @Nullable Conversation conversation; + + public PatchBukkitConsoleCommandSender() { + super(Bukkit.getServer(), "CONSOLE"); + } @Override public void sendMessage(@NotNull String message) { - System.out.println(message); + super.sendMessage(message); } @Override public void sendMessage(@NotNull String... messages) { - System.out.println(messages); + super.sendMessage(messages); } @Override public void sendMessage(@Nullable UUID sender, @NotNull String message) { - // TODO Auto-generated method stub - throw new UnsupportedOperationException("Unimplemented method 'sendMessage'"); + super.sendMessage(sender, message); } @Override public void sendMessage(@Nullable UUID sender, @NotNull String... messages) { - // TODO Auto-generated method stub - throw new UnsupportedOperationException("Unimplemented method 'sendMessage'"); + super.sendMessage(sender, messages); } @Override @@ -50,85 +55,73 @@ public class PatchBukkitConsoleCommandSender implements ConsoleCommandSender { @Override public @NotNull String getName() { - return "CONSOLE"; + return super.getName(); } @Override public @NotNull Spigot spigot() { - // TODO Auto-generated method stub - throw new UnsupportedOperationException("Unimplemented method 'spigot'"); + return super.spigot(); } @Override public @NotNull Component name() { - return Component.text("CONSOLE"); + return Component.text(getName()); } @Override public boolean isPermissionSet(@NotNull String name) { - // TODO Auto-generated method stub - throw new UnsupportedOperationException("Unimplemented method 'isPermissionSet'"); + return super.isPermissionSet(name); } @Override public boolean isPermissionSet(@NotNull Permission perm) { - // TODO Auto-generated method stub - throw new UnsupportedOperationException("Unimplemented method 'isPermissionSet'"); + return super.isPermissionSet(perm); } @Override public boolean hasPermission(@NotNull String name) { - // TODO Auto-generated method stub - throw new UnsupportedOperationException("Unimplemented method 'hasPermission'"); + return super.hasPermission(name); } @Override public boolean hasPermission(@NotNull Permission perm) { - // TODO Auto-generated method stub - throw new UnsupportedOperationException("Unimplemented method 'hasPermission'"); + return super.hasPermission(perm); } @Override public @NotNull PermissionAttachment addAttachment(@NotNull Plugin plugin, @NotNull String name, boolean value) { - // TODO Auto-generated method stub - throw new UnsupportedOperationException("Unimplemented method 'addAttachment'"); + return super.addAttachment(plugin, name, value); } @Override public @NotNull PermissionAttachment addAttachment(@NotNull Plugin plugin) { - // TODO Auto-generated method stub - throw new UnsupportedOperationException("Unimplemented method 'addAttachment'"); + return super.addAttachment(plugin); } @Override public @Nullable PermissionAttachment addAttachment(@NotNull Plugin plugin, @NotNull String name, boolean value, int ticks) { - // TODO Auto-generated method stub - throw new UnsupportedOperationException("Unimplemented method 'addAttachment'"); + return super.addAttachment(plugin, name, value, ticks); } @Override public @Nullable PermissionAttachment addAttachment(@NotNull Plugin plugin, int ticks) { - // TODO Auto-generated method stub - throw new UnsupportedOperationException("Unimplemented method 'addAttachment'"); + return super.addAttachment(plugin, ticks); } @Override public void removeAttachment(@NotNull PermissionAttachment attachment) { - // TODO Auto-generated method stub - throw new UnsupportedOperationException("Unimplemented method 'removeAttachment'"); + super.removeAttachment(attachment); } @Override public void recalculatePermissions() { - // TODO Auto-generated method stub - throw new UnsupportedOperationException("Unimplemented method 'recalculatePermissions'"); + super.recalculatePermissions(); } @Override public @NotNull Set getEffectivePermissions() { - // TODO Auto-generated method stub - throw new UnsupportedOperationException("Unimplemented method 'getEffectivePermissions'"); + return super.getEffectivePermissions(); } @Override @@ -138,50 +131,55 @@ public class PatchBukkitConsoleCommandSender implements ConsoleCommandSender { @Override public void setOp(boolean value) { - // TODO Auto-generated method stub - throw new UnsupportedOperationException("Unimplemented method 'setOp'"); + // Console is always op; ignore attempts to change. } @Override public boolean isConversing() { - // TODO Auto-generated method stub - throw new UnsupportedOperationException("Unimplemented method 'isConversing'"); + return this.conversation != null; } @Override public void acceptConversationInput(@NotNull String input) { - // TODO Auto-generated method stub - throw new UnsupportedOperationException("Unimplemented method 'acceptConversationInput'"); + if (this.conversation != null) { + this.conversation.acceptInput(input); + } } @Override public boolean beginConversation(@NotNull Conversation conversation) { - // TODO Auto-generated method stub - throw new UnsupportedOperationException("Unimplemented method 'beginConversation'"); + if (this.conversation != null) { + return false; + } + this.conversation = conversation; + conversation.begin(); + return true; } @Override public void abandonConversation(@NotNull Conversation conversation) { - // TODO Auto-generated method stub - throw new UnsupportedOperationException("Unimplemented method 'abandonConversation'"); + if (this.conversation != null && this.conversation.equals(conversation)) { + conversation.abandon(); + this.conversation = null; + } } @Override public void abandonConversation(@NotNull Conversation conversation, @NotNull ConversationAbandonedEvent details) { - // TODO Auto-generated method stub - throw new UnsupportedOperationException("Unimplemented method 'abandonConversation'"); + if (this.conversation != null && this.conversation.equals(conversation)) { + conversation.abandon(details); + this.conversation = null; + } } @Override public void sendRawMessage(@NotNull String message) { - // TODO Auto-generated method stub - throw new UnsupportedOperationException("Unimplemented method 'sendRawMessage'"); + this.sendMessage(message); } @Override public void sendRawMessage(@Nullable UUID sender, @NotNull String message) { - // TODO Auto-generated method stub - throw new UnsupportedOperationException("Unimplemented method 'sendRawMessage'"); + this.sendMessage(sender, message); } } diff --git a/rust/src/java/plugin/manager.rs b/rust/src/java/plugin/manager.rs index 5f0f9f7..0e1e5d4 100644 --- a/rust/src/java/plugin/manager.rs +++ b/rust/src/java/plugin/manager.rs @@ -1,5 +1,5 @@ use std::{ - collections::HashMap, + collections::{BinaryHeap, HashMap, HashSet}, path::{Path, PathBuf}, sync::Arc, }; @@ -68,6 +68,17 @@ pub struct Plugin { pub commands: HashMap, pub listeners: HashMap, + + // Dependency metadata (normalized to lowercase) + pub provides: Vec, + pub depends: Vec, + pub soft_depends: Vec, + pub load_before: Vec, + pub load_after: Vec, + pub classpath_deps: Vec, + + // Library coordinates (original casing preserved) + pub libraries: Vec, } pub struct PluginManager { @@ -82,7 +93,15 @@ impl PluginManager { } pub fn add_plugin(&mut self, plugin: Plugin) { - self.plugins.insert(plugin.name.clone(), plugin); + let key = normalize_name(&plugin.name); + if self.plugins.contains_key(&key) { + log::warn!( + "Duplicate plugin name detected ({}). Keeping the first instance.", + plugin.name + ); + return; + } + self.plugins.insert(key, plugin); } pub fn load_paper_plugin>( @@ -97,20 +116,110 @@ impl PluginManager { None => None, }; + let (spigot_depends, spigot_soft_depends, spigot_load_before, spigot_load_after, spigot_provides, spigot_libraries, spigot_skip_libraries) = match &parsed_spigot_plugin { + Some(config) => ( + normalize_names(config.depend.clone()), + normalize_names(config.softdepend.clone()), + normalize_names(config.loadbefore.clone()), + Vec::new(), + normalize_names(config.provides.clone()), + config.libraries.clone().unwrap_or_default(), + config.paper_skip_libraries.unwrap_or(false), + ), + None => (Vec::new(), Vec::new(), Vec::new(), Vec::new(), Vec::new(), Vec::new(), false), + }; + + let mut paper_depends = Vec::new(); + let mut paper_soft_depends = Vec::new(); + let mut paper_load_before = Vec::new(); + let mut paper_load_after = Vec::new(); + let mut paper_classpath = Vec::new(); + + if let Some(ref deps) = parsed_paper_plugin.dependencies { + for (name, dep) in deps + .get_bootstrap_deps() + .into_iter() + .chain(deps.get_server_deps()) + { + let normalized = normalize_name(name); + if dep.required { + paper_depends.push(normalized.clone()); + } else { + paper_soft_depends.push(normalized.clone()); + } + + match dep.load { + crate::config::paper::LoadOrder::Before => { + paper_load_before.push(normalized.clone()); + } + crate::config::paper::LoadOrder::After => { + paper_load_after.push(normalized.clone()); + } + crate::config::paper::LoadOrder::Omit => {} + } + + if dep.join_classpath { + paper_classpath.push(normalized); + } + } + } + + let mut provides = normalize_names(parsed_paper_plugin.provides.clone()); + provides.extend(spigot_provides); + + let mut depends = paper_depends; + depends.extend(spigot_depends); + + let mut soft_depends = paper_soft_depends; + soft_depends.extend(spigot_soft_depends); + + let mut load_before = paper_load_before; + load_before.extend(spigot_load_before); + + let mut load_after = paper_load_after; + load_after.extend(spigot_load_after); + + let classpath_deps = paper_classpath; + + let commands = match &parsed_spigot_plugin { + Some(config) => config.commands.clone().unwrap_or_default(), + None => HashMap::new(), + }; + + let libraries = if spigot_skip_libraries { + Vec::new() + } else { + spigot_libraries + }; + + let name = parsed_paper_plugin.name.clone(); + let version = parsed_paper_plugin.version.clone(); + let main_class = parsed_paper_plugin.main.clone(); + let paper_config = parsed_paper_plugin; + let spigot_config = parsed_spigot_plugin; + let plugin = Plugin { - name: parsed_paper_plugin.name.clone(), - version: parsed_paper_plugin.version.clone(), - main_class: parsed_paper_plugin.main.clone(), + name, + version, + main_class, plugin_type: PluginType::Paper(PaperPluginData { - paper_config: parsed_paper_plugin, - spigot_config: parsed_spigot_plugin, + paper_config, + spigot_config, }), state: PluginState::Registered, data_folder: jar_path.as_ref().parent().unwrap().join("data"), path: jar_path.as_ref().to_path_buf(), instance: None, - commands: HashMap::new(), + commands, listeners: HashMap::new(), + + provides, + depends: dedupe_names(depends), + soft_depends: dedupe_names(soft_depends), + load_before: dedupe_names(load_before), + load_after: dedupe_names(load_after), + classpath_deps: dedupe_names(classpath_deps), + libraries: dedupe_strings(libraries), }; self.add_plugin(plugin); @@ -124,19 +233,41 @@ impl PluginManager { ) -> Result<()> { let parsed_spigot_plugin = SpigotPluginYml::from_str(spigot_plugin_config)?; + let depends = normalize_names(parsed_spigot_plugin.depend.clone()); + let soft_depends = normalize_names(parsed_spigot_plugin.softdepend.clone()); + let load_before = normalize_names(parsed_spigot_plugin.loadbefore.clone()); + let provides = normalize_names(parsed_spigot_plugin.provides.clone()); + let libraries = if parsed_spigot_plugin.paper_skip_libraries.unwrap_or(false) { + Vec::new() + } else { + parsed_spigot_plugin.libraries.clone().unwrap_or_default() + }; + + let name = parsed_spigot_plugin.name.clone(); + let version = parsed_spigot_plugin.version.clone(); + let main_class = parsed_spigot_plugin.main.clone(); + let spigot_config = parsed_spigot_plugin.clone(); + let commands = parsed_spigot_plugin.commands.clone().unwrap_or_default(); + let plugin = Plugin { - name: parsed_spigot_plugin.name.clone(), - version: parsed_spigot_plugin.version.clone(), - main_class: parsed_spigot_plugin.main.clone(), - plugin_type: PluginType::Spigot(SpigotPluginData { - spigot_config: parsed_spigot_plugin.clone(), - }), + name, + version, + main_class, + plugin_type: PluginType::Spigot(SpigotPluginData { spigot_config }), state: PluginState::Registered, data_folder: jar_path.as_ref().parent().unwrap().join("data"), path: jar_path.as_ref().to_path_buf(), instance: None, - commands: parsed_spigot_plugin.commands.unwrap_or(HashMap::new()), + commands, listeners: HashMap::new(), + + provides, + depends: dedupe_names(depends), + soft_depends: dedupe_names(soft_depends), + load_before: dedupe_names(load_before), + load_after: Vec::new(), + classpath_deps: Vec::new(), + libraries: dedupe_strings(libraries), }; self.add_plugin(plugin); @@ -151,6 +282,9 @@ impl PluginManager { InvocationArg::empty(), )?; for (_plugin_name, plugin) in &mut self.plugins { + if plugin.instance.is_none() { + continue; + } let plugin_instance = plugin.instance.as_ref().unwrap(); let plugin_instance = jvm.clone_instance(&plugin_instance).unwrap(); @@ -185,6 +319,9 @@ impl PluginManager { InvocationArg::empty(), )?; for (_plugin_name, plugin) in &mut self.plugins { + if plugin.instance.is_none() { + continue; + } let plugin_instance = plugin.instance.as_ref().unwrap(); let plugin_instance = jvm.clone_instance(&plugin_instance).unwrap(); @@ -219,18 +356,33 @@ impl PluginManager { command_tx: mpsc::Sender, command_manager: &mut CommandManager, ) -> Result<()> { - for (_plugin_name, plugin) in &mut self.plugins { + let load_order = self.compute_load_order(); + + for plugin_key in load_order { + let (classpath, libraries) = match self.plugins.get(&plugin_key) { + Some(plugin) => ( + self.classpath_string_for(plugin), + self.library_string_for(plugin), + ), + None => continue, + }; + + let plugin = match self.plugins.get_mut(&plugin_key) { + Some(plugin) => plugin, + None => continue, + }; let plugin_instance = jvm.invoke_static( "org.patchbukkit.loader.PatchBukkitPluginLoader", "createPlugin", &[ InvocationArg::try_from(&plugin.path.to_string_lossy().to_string())?, InvocationArg::try_from(&plugin.main_class)?, + InvocationArg::try_from(&classpath)?, + InvocationArg::try_from(&libraries)?, ], )?; plugin.instance = Some(plugin_instance); - for (cmd_name, cmd_data) in &plugin.commands { match command_manager .register_command( @@ -265,4 +417,232 @@ impl PluginManager { self.plugins.clear(); Ok(()) } + + fn resolve_dependency_name( + &self, + name: &str, + provides_map: &HashMap, + ) -> Option { + let key = normalize_name(name); + if self.plugins.contains_key(&key) { + Some(key) + } else { + provides_map.get(&key).cloned() + } + } + + fn compute_load_order(&mut self) -> Vec { + let mut provides_map: HashMap = HashMap::new(); + for (key, plugin) in &self.plugins { + for provide in &plugin.provides { + if provide.is_empty() { + continue; + } + provides_map.entry(provide.clone()).or_insert_with(|| key.clone()); + } + } + + let mut active: HashSet = self.plugins.keys().cloned().collect(); + + let mut missing_required_by_plugin: HashMap> = HashMap::new(); + for (key, plugin) in &self.plugins { + let mut missing_required = Vec::new(); + for dep in &plugin.depends { + if self.resolve_dependency_name(dep, &provides_map).is_none() { + missing_required.push(dep.clone()); + } + } + if !missing_required.is_empty() { + missing_required_by_plugin.insert(key.clone(), missing_required); + } + } + + for (key, missing_required) in missing_required_by_plugin { + if let Some(plugin) = self.plugins.get_mut(&key) { + plugin.state = PluginState::Errored; + active.remove(&key); + log::warn!( + "Skipping plugin {} due to missing required dependencies: {}", + plugin.name, + missing_required.join(", ") + ); + } + } + + let mut edges: HashMap> = HashMap::new(); + let mut indegree: HashMap = + active.iter().map(|k| (k.clone(), 0)).collect(); + + for (key, plugin) in &self.plugins { + if !active.contains(key) { + continue; + } + + let mut before = Vec::new(); + for dep in plugin + .depends + .iter() + .chain(plugin.soft_depends.iter()) + { + if let Some(resolved) = self.resolve_dependency_name(dep, &provides_map) { + if resolved != *key && active.contains(&resolved) { + before.push(resolved); + } + } + } + + for dep in before { + add_edge(&mut edges, &mut indegree, &dep, key); + } + + for target in &plugin.load_before { + if let Some(resolved) = self.resolve_dependency_name(target, &provides_map) { + if resolved != *key && active.contains(&resolved) { + add_edge(&mut edges, &mut indegree, key, &resolved); + } + } + } + + for target in &plugin.load_after { + if let Some(resolved) = self.resolve_dependency_name(target, &provides_map) { + if resolved != *key && active.contains(&resolved) { + add_edge(&mut edges, &mut indegree, &resolved, key); + } + } + } + } + + let mut ready: BinaryHeap> = indegree + .iter() + .filter_map(|(key, °)| { + if deg == 0 { + Some(std::cmp::Reverse(key.clone())) + } else { + None + } + }) + .collect(); + let mut order = Vec::with_capacity(active.len()); + + while let Some(std::cmp::Reverse(node)) = ready.pop() { + order.push(node.clone()); + if let Some(children) = edges.get(&node) { + for child in children { + if let Some(deg) = indegree.get_mut(child) { + if *deg > 0 { + *deg -= 1; + } + if *deg == 0 { + ready.push(std::cmp::Reverse(child.clone())); + } + } + } + } + } + + if order.len() != active.len() { + let mut remaining: Vec = active + .into_iter() + .filter(|k| !order.contains(k)) + .collect(); + remaining.sort(); + log::warn!( + "Detected plugin dependency cycle(s). Loading remaining plugins in name order: {}", + remaining.join(", ") + ); + order.extend(remaining); + } + + order + } + + fn classpath_string_for(&self, plugin: &Plugin) -> String { + if plugin.classpath_deps.is_empty() { + return String::new(); + } + + let mut provides_map: HashMap = HashMap::new(); + for (key, plugin) in &self.plugins { + for provide in &plugin.provides { + if provide.is_empty() { + continue; + } + provides_map.entry(provide.clone()).or_insert_with(|| key.clone()); + } + } + + let mut paths = Vec::new(); + for dep in &plugin.classpath_deps { + if let Some(resolved) = self.resolve_dependency_name(dep, &provides_map) { + if let Some(dep_plugin) = self.plugins.get(&resolved) { + paths.push(dep_plugin.path.to_string_lossy().to_string()); + } + } + } + + paths.join(";") + } + + fn library_string_for(&self, plugin: &Plugin) -> String { + if plugin.libraries.is_empty() { + return String::new(); + } + plugin.libraries.join("\n") + } +} + +fn normalize_name(name: &str) -> String { + name.trim().to_lowercase() +} + +fn normalize_names(names: Option>) -> Vec { + names + .unwrap_or_default() + .into_iter() + .map(|name| normalize_name(&name)) + .filter(|name| !name.is_empty()) + .collect() +} + +fn dedupe_names(names: Vec) -> Vec { + let mut seen = HashSet::new(); + let mut output = Vec::new(); + for name in names { + if seen.insert(name.clone()) { + output.push(name); + } + } + output +} + +fn dedupe_strings(values: Vec) -> Vec { + let mut seen = HashSet::new(); + let mut output = Vec::new(); + for value in values { + let trimmed = value.trim().to_string(); + if trimmed.is_empty() { + continue; + } + if seen.insert(trimmed.clone()) { + output.push(trimmed); + } + } + output +} + +fn add_edge( + edges: &mut HashMap>, + indegree: &mut HashMap, + from: &str, + to: &str, +) { + if from == to { + return; + } + let entry = edges.entry(from.to_string()).or_default(); + if entry.insert(to.to_string()) { + if let Some(count) = indegree.get_mut(to) { + *count += 1; + } + } }