use std::{ collections::{BinaryHeap, HashMap, HashSet}, path::{Path, PathBuf}, sync::Arc, }; use anyhow::Result; use jni::Env; use pumpkin::plugin::Context; use tokio::sync::mpsc; use crate::{ config::{ paper::PaperPluginYml, spigot::{Command, SpigotPluginYml}, }, java::{jvm::commands::JvmCommand, plugin::command_manager::CommandManager}, }; #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum PluginState { /// Plugin config has been loaded, but not yet initialized in JVM Registered, /// Plugin class has been loaded and instance created Loaded, /// Plugin is enabled (onEnable called) Enabled, /// Plugin is disabled (onDisable called) Disabled, /// Plugin failed to load or enable Errored, } #[expect(clippy::large_enum_variant)] #[derive(Debug)] pub enum PluginType { Paper(PaperPluginData), Spigot(SpigotPluginData), } #[derive(Debug)] pub struct PaperPluginData { pub paper_config: PaperPluginYml, pub spigot_config: Option, } #[derive(Debug)] pub struct SpigotPluginData { pub spigot_config: SpigotPluginYml, } pub struct Plugin { /// Unique plugin name pub name: String, /// Plugin version pub version: String, /// Main class fully qualified name pub main_class: String, /// Path to the JAR file pub path: PathBuf, /// Plugin-specific data (Paper or Spigot) pub plugin_type: PluginType, /// Current state pub state: PluginState, /// Data folder for this plugin pub data_folder: PathBuf, // The registered commands pub commands: 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 { pub plugins: HashMap, } impl Default for PluginManager { fn default() -> Self { Self::new() } } impl PluginManager { #[must_use] pub fn new() -> Self { Self { plugins: HashMap::new(), } } pub fn add_plugin(&mut self, plugin: Plugin) { let key = normalize_name(&plugin.name); if self.plugins.contains_key(&key) { tracing::warn!( "Duplicate plugin name detected ({}). Keeping the first instance.", plugin.name ); return; } self.plugins.insert(key, plugin); } pub fn load_paper_plugin>( &mut self, jar_path: P, paper_plugin_config: &str, spigot_plugin_config: &Option, ) -> Result<()> { let parsed_paper_plugin = PaperPluginYml::parse(paper_plugin_config)?; let parsed_spigot_plugin = match spigot_plugin_config { Some(config) => Some(SpigotPluginYml::parse(config)?), 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 = parsed_spigot_plugin .as_ref() .map_or_else(HashMap::new, |config| { config.commands.clone().unwrap_or_default() }); let mut libraries = if spigot_skip_libraries { Vec::new() } else { spigot_libraries }; if let Some(paper_libs) = &parsed_paper_plugin.libraries { libraries.extend(paper_libs.clone()); } if let Some(repos) = &parsed_paper_plugin.repositories { if let Some(arr) = repos.as_array() { for item in arr { if let Some(s) = item.as_str() { libraries.push(format!("repo:{s}")); } else if let Some(obj) = item.as_object() && let Some(u) = obj.get("url").and_then(|v| v.as_str()) { libraries.push(format!("repo:{u}")); } } } else if let Some(map) = repos.as_object() { for (_, v) in map { if let Some(s) = v.as_str() { libraries.push(format!("repo:{s}")); } } } } libraries.extend(crate::java::jar::read_libraries_from_jar(&jar_path)); 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, version, main_class, plugin_type: PluginType::Paper(PaperPluginData { 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(), commands, 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); Ok(()) } pub fn load_spigot_plugin>( &mut self, jar_path: P, spigot_plugin_config: &str, ) -> Result<()> { let parsed_spigot_plugin = SpigotPluginYml::parse(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 mut libraries = if parsed_spigot_plugin.paper_skip_libraries.unwrap_or(false) { Vec::new() } else { parsed_spigot_plugin.libraries.clone().unwrap_or_default() }; libraries.extend(crate::java::jar::read_libraries_from_jar(&jar_path)); 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.unwrap_or_default(); let plugin = Plugin { 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(), commands, 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); Ok(()) } pub fn enable_all_plugins(&mut self, _env: &mut Env) -> Result<()> { Ok(()) } pub fn disable_all_plugins(&mut self, _env: &mut Env) -> Result<()> { Ok(()) } pub fn instantiate_all_plugins( &mut self, env: &mut Env, plugin_folder: &Path, _server: &Arc, _command_tx: mpsc::Sender, _command_manager: &mut CommandManager, ) -> Result<()> { tracing::info!("Bootstrapping Java plugins from {:?}", plugin_folder); let path_str = plugin_folder.to_string_lossy(); let j_str = env.new_string(&path_str)?; let result = env.call_static_method( jni::jni_str!("org/patchbukkit/loader/PatchBukkitBootstrap"), jni::jni_str!("bootstrapPlugins"), jni::jni_sig!("(Ljava/lang/String;)Z"), &[(&j_str).into()], ); match result { Ok(val) => { if !val.z()? { tracing::error!("Bootstrap of Java plugins reported errors"); } } Err(e) => { tracing::error!("Failed to bootstrap Java plugins: {:?}", e); return Err(e.into()); } } Ok(()) } pub fn unload_all_plugins(&mut self) -> Result<()> { self.plugins.clear(); Ok(()) } #[allow(dead_code)] 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() } } #[allow(dead_code)] 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); tracing::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) && 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) && 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) && 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(); tracing::warn!( "Detected plugin dependency cycle(s). Loading remaining plugins in name order: {}", remaining.join(", ") ); order.extend(remaining); } order } #[allow(dead_code)] 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) && let Some(dep_plugin) = self.plugins.get(&resolved) { paths.push(dep_plugin.path.to_string_lossy().to_string()); } } paths.join(if cfg!(windows) { ";" } else { ":" }) } #[allow(dead_code)] fn library_string_for(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 } #[allow(dead_code)] 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()) && let Some(count) = indegree.get_mut(to) { *count += 1; } }