diff --git a/Cargo.toml b/Cargo.toml index f63eda2db..cd23b0830 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -112,7 +112,7 @@ proc-macro-error2 = "2" aes = "0.9" async-compression = "0.4.41" base64 = "0.22.1" -bitflags = "2.11.0" +bitflags = "2.11.1" cesu8 = "1.1" cfb8 = "0.9" colored = "3.1" @@ -131,7 +131,7 @@ hmac = "=0.13.0" indexmap = "2.14" itertools = "0.14.0" libloading = "0.9" -lru = "0.16.3" +lru = "0.17.0" lz4-java-wrc = "0.2.0" md5 = "0.8" num-bigint = "0.4" diff --git a/README.md b/README.md index 813607ff7..70e1ee8a2 100644 --- a/README.md +++ b/README.md @@ -74,9 +74,8 @@ and customizable experience. It prioritizes performance and player enjoyment whi - [x] Mobs (W.I.P) - [x] Animals (W.I.P) - [Entity AI](https://github.com/Pumpkin-MC/Pumpkin/issues/1406) - - [ ] Boss + - [x] Boss - [ ] Villagers - - [ ] Mobs Inventory - [X] Entity Saving - Server - [Plugins](https://github.com/Pumpkin-MC/Pumpkin/issues/1407) diff --git a/pumpkin-api-macros/src/lib.rs b/pumpkin-api-macros/src/lib.rs index 46974d762..69fa1d2c9 100644 --- a/pumpkin-api-macros/src/lib.rs +++ b/pumpkin-api-macros/src/lib.rs @@ -87,6 +87,7 @@ pub fn plugin_impl(_attr: TokenStream, item: TokenStream) -> TokenStream { version: env!("CARGO_PKG_VERSION").to_string(), authors: env!("CARGO_PKG_AUTHORS").split(',').map(String::from).collect(), description: env!("CARGO_PKG_DESCRIPTION").to_string(), + dependencies: Vec::new(), } }); diff --git a/pumpkin-plugin-api/src/lib.rs b/pumpkin-plugin-api/src/lib.rs index 3d3bb88c5..f030d4a2e 100644 --- a/pumpkin-plugin-api/src/lib.rs +++ b/pumpkin-plugin-api/src/lib.rs @@ -19,6 +19,7 @@ //! version: "0.1.0".into(), //! authors: vec!["you".into()], //! description: "An example plugin.".into(), +//! dependencies: vec![], //! } //! } //! } @@ -78,6 +79,8 @@ pub struct PluginMetadata { pub authors: Vec, /// A short description of what the plugin does. pub description: String, + /// The list of plugin dependencies. + pub dependencies: Vec, } impl wit::exports::pumpkin::plugin::metadata::Guest for Component { @@ -89,6 +92,7 @@ impl wit::exports::pumpkin::plugin::metadata::Guest for Component { version: metadata.version, authors: metadata.authors, description: metadata.description, + dependencies: metadata.dependencies, } } } diff --git a/pumpkin-plugin-wit/v0.1.0/metadata.wit b/pumpkin-plugin-wit/v0.1.0/metadata.wit index f13f421e7..697d1af76 100644 --- a/pumpkin-plugin-wit/v0.1.0/metadata.wit +++ b/pumpkin-plugin-wit/v0.1.0/metadata.wit @@ -9,6 +9,8 @@ interface metadata { authors: list, /// Short description of the plugin. description: string, + /// Plugin dependencies. + dependencies: list, } get-metadata: func() -> plugin-metadata; diff --git a/pumpkin/src/plugin/api/mod.rs b/pumpkin/src/plugin/api/mod.rs index 114ddd5f5..b4630ba59 100644 --- a/pumpkin/src/plugin/api/mod.rs +++ b/pumpkin/src/plugin/api/mod.rs @@ -22,6 +22,8 @@ pub struct PluginMetadata { pub authors: Vec, /// A description of the plugin. pub description: String, + /// The dependencies of the plugin. + pub dependencies: Vec, } /// This type represents a future for the plugin. diff --git a/pumpkin/src/plugin/loader/wasm/wasm_host/wit/v0_1_0/mod.rs b/pumpkin/src/plugin/loader/wasm/wasm_host/wit/v0_1_0/mod.rs index 3bfaaf739..ca0862e46 100644 --- a/pumpkin/src/plugin/loader/wasm/wasm_host/wit/v0_1_0/mod.rs +++ b/pumpkin/src/plugin/loader/wasm/wasm_host/wit/v0_1_0/mod.rs @@ -64,6 +64,7 @@ pub async fn init_plugin( version: metadata.version, authors: metadata.authors, description: metadata.description, + dependencies: metadata.dependencies, }; Ok(( diff --git a/pumpkin/src/plugin/mod.rs b/pumpkin/src/plugin/mod.rs index 02fbd2890..25d8b0017 100644 --- a/pumpkin/src/plugin/mod.rs +++ b/pumpkin/src/plugin/mod.rs @@ -201,6 +201,9 @@ pub enum ManagerError { #[error("Plugin manager not initialized properly")] ManagerNotInitialized, + + #[error("Dependency error: {0}")] + DependencyError(String), } impl Default for PluginManager { @@ -291,6 +294,180 @@ impl PluginManager { self.loaders.read().await.clone() } + /// Helper for topological sort of plugins based on dependencies + fn topological_sort(plugins: &[(String, Vec)]) -> Result, String> { + fn visit( + name: &str, + deps_map: &HashMap>, + plugin_names: &HashSet, + visited: &mut HashSet, + current_path: &mut HashSet, + sorted: &mut Vec, + ) -> Result<(), String> { + if current_path.contains(name) { + return Err(format!( + "Circular dependency detected involving plugin: {name}" + )); + } + if !visited.contains(name) { + current_path.insert(name.to_string()); + if let Some(deps) = deps_map.get(name) { + for dep in *deps { + if !plugin_names.contains(dep) { + return Err(format!("Plugin {name} depends on missing plugin: {dep}")); + } + visit(dep, deps_map, plugin_names, visited, current_path, sorted)?; + } + } + current_path.remove(name); + visited.insert(name.to_string()); + sorted.push(name.to_string()); + } + Ok(()) + } + let mut sorted = Vec::new(); + let mut visited = HashSet::new(); + let mut current_path = HashSet::new(); + let plugin_names: HashSet = plugins.iter().map(|(n, _)| n.clone()).collect(); + let deps_map: HashMap> = + plugins.iter().map(|(n, d)| (n.clone(), d)).collect(); + + for (name, _) in plugins { + visit( + name, + &deps_map, + &plugin_names, + &mut visited, + &mut current_path, + &mut sorted, + )?; + } + + Ok(sorted) + } + + /// Spawn initialization for a single plugin + async fn spawn_plugin_initialization( + &self, + mut instance: Box, + metadata: PluginMetadata, + loader_data: Box, + loader: Arc, + ) -> Result, ManagerError> { + // Mark plugin as loading + self.plugin_states + .write() + .await + .insert(metadata.name.clone(), PluginState::Loading); + + let self_ref = self + .self_ref + .read() + .await + .clone() + .ok_or(ManagerError::ServerNotInitialized)?; + + let context = Arc::new(Context::new( + metadata.clone(), + Arc::clone( + &self + .server + .read() + .await + .clone() + .ok_or(ManagerError::ServerNotInitialized)?, + ), + Arc::clone(&self.handlers), + Arc::clone(&self_ref), + Arc::clone(&LOGGER_IMPL), + )); + + // Create the plugin structure first + let plugin = LoadedPlugin { + metadata: metadata.clone(), + instance: None, // Will be set after successful initialization + loader: loader.clone(), + loader_data: Some(loader_data), + is_active: false, // Will be set to true after successful initialization + context: context.clone(), + }; + + let plugin_index = { + let mut plugins = self.plugins.write().await; + plugins.push(plugin); + plugins.len() - 1 + }; + + // Spawn async task for plugin initialization + let self_ref_clone = Arc::clone(&self_ref); + let state_notify = Arc::clone(&self.state_notify); + let plugin_name = metadata.name.clone(); + let loader_clone = loader.clone(); + + let task = tokio::spawn(async move { + // Initialize the plugin + match instance.on_load(context.clone()).await { + Ok(()) => { + // Update plugin state to loaded + { + let mut plugins = self_ref_clone.plugins.write().await; + if let Some(plugin) = plugins.get_mut(plugin_index) { + plugin.instance = Some(instance); + plugin.is_active = true; + } + } + self_ref_clone + .plugin_states + .write() + .await + .insert(plugin_name.clone(), PluginState::Loaded); + state_notify.notify_waiters(); + + info!("Loaded {} ({})", metadata.name, metadata.version); + } + Err(e) => { + // Handle initialization failure + let error_msg = format!("Initialization failed: {e}"); + let _ = instance.on_unload(context).await; + + // Get the loader data before removing the plugin + let loader_data: Option> = { + let mut plugins = self_ref_clone.plugins.write().await; + if let Some(plugin) = plugins.get_mut(plugin_index) { + plugin.loader_data.take() + } else { + None + } + }; + + // Try to unload the plugin data + if let Some(data) = loader_data { + tokio::spawn(async move { + loader_clone.unload(data).await.ok(); + }); + } + + { + let mut plugins = self_ref_clone.plugins.write().await; + if plugin_index < plugins.len() { + plugins.remove(plugin_index); + } + }; + self_ref_clone + .plugin_states + .write() + .await + .insert(plugin_name.clone(), PluginState::Failed(error_msg.clone())); + state_notify.notify_waiters(); + + error!("Failed to initialize plugin {plugin_name}: {error_msg}",); + } + } + }); + + Ok(task) + } + /// Load all plugins from the plugin directory pub async fn load_plugins(&self) -> Result<(), ManagerError> { const PLUGIN_DIR: &str = "./plugins"; @@ -301,7 +478,7 @@ impl PluginManager { return Ok(()); } - let mut load_tasks = Vec::new(); + let mut prepared_plugins = Vec::new(); for entry in std::fs::read_dir(path)? { let entry = entry?; @@ -319,143 +496,87 @@ impl PluginManager { continue; } - // Start loading plugin concurrently - match self.start_loading_plugin(&path).await { - Ok(task) => load_tasks.push(task), - Err(err) => error!("{}", err), + // Find a loader that can handle this file + let loaders = self.loaders.read().await; + let mut loader_found = false; + for loader in loaders.iter() { + if loader.can_load(&path) { + match loader.load(&path).await { + Ok((instance, metadata, loader_data)) => { + prepared_plugins.push(( + instance, + metadata, + loader_data, + loader.clone(), + )); + loader_found = true; + } + Err(err) => error!("Failed to load plugin from {:?}: {}", path, err), + } + break; + } + } + + if !loader_found { + self.unloaded_files.write().await.insert(path.clone()); } } - // Wait for all plugins to complete loading (but don't block on individual plugin initialization) - join_all(load_tasks).await; + // Resolve dependencies + let metadata_list: Vec<(String, Vec)> = prepared_plugins + .iter() + .map(|(_, m, _, _)| (m.name.clone(), m.dependencies.clone())) + .collect(); + + let sorted_names = + Self::topological_sort(&metadata_list).map_err(ManagerError::DependencyError)?; + + // Map names back to prepared plugins + #[expect(clippy::type_complexity)] + let mut plugins_map: HashMap< + String, + ( + Box, + PluginMetadata, + Box, + Arc, + ), + > = prepared_plugins + .into_iter() + .map(|(i, m, d, l)| (m.name.clone(), (i, m, d, l))) + .collect(); + + for name in sorted_names { + if let Some((instance, metadata, loader_data, loader)) = plugins_map.remove(&name) { + match self + .spawn_plugin_initialization(instance, metadata, loader_data, loader) + .await + { + Ok(task) => { + // We must await each initialization to ensure dependencies are ready + if let Err(err) = task.await { + error!("Plugin initialization task panicked: {}", err); + } + } + Err(err) => error!("{}", err), + } + } + } Ok(()) } /// Start loading a plugin asynchronously - #[expect(clippy::too_many_lines)] async fn start_loading_plugin( &self, path: &Path, ) -> Result, ManagerError> { for loader in self.loaders.read().await.iter() { if loader.can_load(path) { - let (mut instance, metadata, loader_data) = loader.load(path).await?; - - // Mark plugin as loading - self.plugin_states - .write() - .await - .insert(metadata.name.clone(), PluginState::Loading); - - let self_ref = self - .self_ref - .read() - .await - .clone() - .ok_or(ManagerError::ServerNotInitialized)?; - - let context = Arc::new(Context::new( - metadata.clone(), - Arc::clone( - &self - .server - .read() - .await - .clone() - .ok_or(ManagerError::ServerNotInitialized)?, - ), - Arc::clone(&self.handlers), - Arc::clone(&self_ref), - Arc::clone(&LOGGER_IMPL), - )); - - // Create the plugin structure first - let plugin = LoadedPlugin { - metadata: metadata.clone(), - instance: None, // Will be set after successful initialization - loader: loader.clone(), - loader_data: Some(loader_data), - is_active: false, // Will be set to true after successful initialization - context: context.clone(), - }; - - let plugin_index = { - let mut plugins = self.plugins.write().await; - plugins.push(plugin); - plugins.len() - 1 - }; - - // Remove from unloaded files if it was there - self.unloaded_files.write().await.remove(path); - - // Spawn async task for plugin initialization - let self_ref_clone = Arc::clone(&self_ref); - let state_notify = Arc::clone(&self.state_notify); - let plugin_name = metadata.name.clone(); - let loader_clone = loader.clone(); - - let task = tokio::spawn(async move { - // Initialize the plugin - match instance.on_load(context.clone()).await { - Ok(()) => { - // Update plugin state to loaded - { - let mut plugins = self_ref_clone.plugins.write().await; - if let Some(plugin) = plugins.get_mut(plugin_index) { - plugin.instance = Some(instance); - plugin.is_active = true; - } - } - self_ref_clone - .plugin_states - .write() - .await - .insert(plugin_name.clone(), PluginState::Loaded); - state_notify.notify_waiters(); - - info!("Loaded {} ({})", metadata.name, metadata.version); - } - Err(e) => { - // Handle initialization failure - let error_msg = format!("Initialization failed: {e}"); - let _ = instance.on_unload(context).await; - - // Get the loader data before removing the plugin - let loader_data: Option> = { - let mut plugins = self_ref_clone.plugins.write().await; - if let Some(plugin) = plugins.get_mut(plugin_index) { - plugin.loader_data.take() - } else { - None - } - }; - - // Try to unload the plugin data - if let Some(data) = loader_data { - tokio::spawn(async move { - loader_clone.unload(data).await.ok(); - }); - } - - { - let mut plugins = self_ref_clone.plugins.write().await; - if plugin_index < plugins.len() { - plugins.remove(plugin_index); - } - }; - self_ref_clone.plugin_states.write().await.insert( - plugin_name.clone(), - PluginState::Failed(error_msg.clone()), - ); - state_notify.notify_waiters(); - - error!("Failed to initialize plugin {plugin_name}: {error_msg}",); - } - } - }); - - return Ok(task); + let (instance, metadata, loader_data) = loader.load(path).await?; + return self + .spawn_plugin_initialization(instance, metadata, loader_data, loader.clone()) + .await; } } @@ -657,3 +778,41 @@ impl PluginManager { event } } + +#[cfg(test)] +mod tests { + use super::*; + + #[tokio::test] + async fn topological_sort() { + let plugins = vec![ + ("A".to_string(), vec!["B".to_string()]), + ("B".to_string(), vec!["C".to_string()]), + ("C".to_string(), vec![]), + ]; + let sorted = PluginManager::topological_sort(&plugins).unwrap(); + assert_eq!(sorted, vec!["C", "B", "A"]); + + let plugins_complex = vec![ + ("A".to_string(), vec!["B".to_string(), "C".to_string()]), + ("B".to_string(), vec!["D".to_string()]), + ("C".to_string(), vec!["D".to_string()]), + ("D".to_string(), vec![]), + ]; + let sorted = PluginManager::topological_sort(&plugins_complex).unwrap(); + // Multiple valid sorts possible, but D must be before B and C, and B, C must be before A. + assert_eq!(sorted[0], "D"); + assert!(sorted[1] == "B" || sorted[1] == "C"); + assert!(sorted[2] == "B" || sorted[2] == "C"); + assert_eq!(sorted[3], "A"); + + let plugins_circular = vec![ + ("A".to_string(), vec!["B".to_string()]), + ("B".to_string(), vec!["A".to_string()]), + ]; + assert!(PluginManager::topological_sort(&plugins_circular).is_err()); + + let plugins_missing = vec![("A".to_string(), vec!["B".to_string()])]; + assert!(PluginManager::topological_sort(&plugins_missing).is_err()); + } +}