diff --git a/.gitignore b/.gitignore index 9feb853cd..e9a82deaa 100644 --- a/.gitignore +++ b/.gitignore @@ -101,7 +101,9 @@ Cargo.lock #.idea/ # === PROJECT SPECIFIC === -plugins/* +plugins/**/*.so +plugins/**/*.dylib +plugins/**/*.dll world/* # docker-compose diff --git a/README.md b/README.md index 354a243ec..d309357f2 100644 --- a/README.md +++ b/README.md @@ -75,7 +75,7 @@ and customizable experience. It prioritizes performance and player enjoyment whi - [x] Entity AI - [ ] Boss - Server - - [ ] Plugins + - [x] Plugins - [x] Query - [x] RCON - [x] Inventories diff --git a/pumpkin-api-macros/Cargo.toml b/pumpkin-api-macros/Cargo.toml new file mode 100644 index 000000000..6c0d4bcc8 --- /dev/null +++ b/pumpkin-api-macros/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "pumpkin-api-macros" +version.workspace = true +edition.workspace = true + +[lib] +proc-macro = true + +[dependencies] +syn = { version = "2.0.89", features = ["full"] } +quote = "1.0.37" +proc-macro2 = "1.0.92" \ No newline at end of file diff --git a/pumpkin-api-macros/src/lib.rs b/pumpkin-api-macros/src/lib.rs new file mode 100644 index 000000000..6cd2e0725 --- /dev/null +++ b/pumpkin-api-macros/src/lib.rs @@ -0,0 +1,128 @@ +use std::sync::LazyLock; +use proc_macro::TokenStream; +use quote::quote; +use std::collections::HashMap; +use std::sync::Mutex; +use syn::{parse_macro_input, parse_quote, ImplItem, ItemFn, ItemImpl, ItemStruct}; + +static PLUGIN_METHODS: LazyLock>>> = + LazyLock::new(|| Mutex::new(HashMap::new())); + +#[proc_macro_attribute] +pub fn plugin_method(attr: TokenStream, item: TokenStream) -> TokenStream { + let input_fn = parse_macro_input!(item as ItemFn); + let fn_name = &input_fn.sig.ident; + let fn_inputs = &input_fn.sig.inputs; + let fn_output = &input_fn.sig.output; + let fn_body = &input_fn.block; + + let struct_name = if attr.is_empty() { + "MyPlugin".to_string() + } else { + attr.to_string().trim().to_string() + }; + + let method = quote! { + #[allow(unused_mut)] + async fn #fn_name(#fn_inputs) #fn_output { + crate::GLOBAL_RUNTIME.block_on(async move { + #fn_body + }) + } + } + .to_string(); + + PLUGIN_METHODS + .lock() + .unwrap() + .entry(struct_name) + .or_default() + .push(method); + + TokenStream::new() +} + +#[proc_macro_attribute] +pub fn plugin_impl(attr: TokenStream, item: TokenStream) -> TokenStream { + // Parse the input struct + let input_struct = parse_macro_input!(item as ItemStruct); + let struct_ident = &input_struct.ident; + + // Get the custom name from attribute or use the struct's name + let struct_name = if attr.is_empty() { + struct_ident.clone() + } else { + let attr_str = attr.to_string(); + quote::format_ident!("{}", attr_str.trim()) + }; + + let methods = PLUGIN_METHODS + .lock() + .unwrap() + .remove(&struct_name.to_string()) + .unwrap_or_default(); + + let methods: Vec = methods + .iter() + .filter_map(|method_str| method_str.parse().ok()) + .collect(); + + // Combine the original struct definition with the impl block and plugin() function + let expanded = quote! { + pub static GLOBAL_RUNTIME: std::sync::LazyLock> = + std::sync::LazyLock::new(|| std::sync::Arc::new(tokio::runtime::Runtime::new().unwrap())); + + #[no_mangle] + pub static METADATA: pumpkin::plugin::PluginMetadata = pumpkin::plugin::PluginMetadata { + name: env!("CARGO_PKG_NAME"), + version: env!("CARGO_PKG_VERSION"), + authors: env!("CARGO_PKG_AUTHORS"), + description: env!("CARGO_PKG_DESCRIPTION"), + }; + + #input_struct + + #[async_trait::async_trait] + impl pumpkin::plugin::Plugin for #struct_ident { + #(#methods)* + } + + #[no_mangle] + pub fn plugin() -> Box { + Box::new(#struct_ident::new()) + } + }; + + TokenStream::from(expanded) +} + +#[proc_macro_attribute] +pub fn with_runtime(attr: TokenStream, item: TokenStream) -> TokenStream { + let mut input = parse_macro_input!(item as ItemImpl); + + let use_global = attr.to_string() == "global"; + + for item in &mut input.items { + if let ImplItem::Fn(method) = item { + let original_body = &method.block; + + method.block = if use_global { + parse_quote!({ + GLOBAL_RUNTIME.block_on(async move { + #original_body + }) + }) + } else { + parse_quote!({ + tokio::runtime::Runtime::new() + .unwrap() + .block_on(async move { + #original_body + }) + }) + }; + } + } + + TokenStream::from(quote!(#input)) +} diff --git a/pumpkin/Cargo.toml b/pumpkin/Cargo.toml index b9945eae3..03a1fa7d4 100644 --- a/pumpkin/Cargo.toml +++ b/pumpkin/Cargo.toml @@ -11,6 +11,10 @@ FileDescription = "Pumpkin" OriginalFilename = "pumpkin.exe" LegalCopyright = "Copyright © 2025 Aleksander Medvedev" +# Required to expose pumpkin plugin API +[lib] +doctest = false + [dependencies] # pumpkin pumpkin-util = { path = "../pumpkin-util" } @@ -69,6 +73,10 @@ time = "0.3.37" # commands async-trait = "0.1.83" + +# plugins +libloading = "0.8.5" +oneshot = "0.1.8" [build-dependencies] git-version = "0.3.9" # This makes it so the entire project doesn't recompile on each build on linux. diff --git a/pumpkin/src/command/args/arg_block.rs b/pumpkin/src/command/args/arg_block.rs index da21e1bb8..ecff1dff1 100644 --- a/pumpkin/src/command/args/arg_block.rs +++ b/pumpkin/src/command/args/arg_block.rs @@ -14,7 +14,7 @@ use super::{ Arg, DefaultNameArgConsumer, FindArg, GetClientSideArgParser, }; -pub(crate) struct BlockArgumentConsumer; +pub struct BlockArgumentConsumer; impl GetClientSideArgParser for BlockArgumentConsumer { fn get_client_side_parser(&self) -> ProtoCmdArgParser { diff --git a/pumpkin/src/command/args/arg_bool.rs b/pumpkin/src/command/args/arg_bool.rs index 4d363d2b5..930444eaf 100644 --- a/pumpkin/src/command/args/arg_bool.rs +++ b/pumpkin/src/command/args/arg_bool.rs @@ -8,7 +8,7 @@ use pumpkin_protocol::client::play::{ CommandSuggestion, ProtoCmdArgParser, ProtoCmdArgSuggestionType, }; -pub(crate) struct BoolArgConsumer; +pub struct BoolArgConsumer; impl GetClientSideArgParser for BoolArgConsumer { fn get_client_side_parser(&self) -> ProtoCmdArgParser { diff --git a/pumpkin/src/command/args/arg_bossbar_color.rs b/pumpkin/src/command/args/arg_bossbar_color.rs index cd12c87e5..2084918e3 100644 --- a/pumpkin/src/command/args/arg_bossbar_color.rs +++ b/pumpkin/src/command/args/arg_bossbar_color.rs @@ -11,7 +11,7 @@ use pumpkin_protocol::client::play::{ CommandSuggestion, ProtoCmdArgParser, ProtoCmdArgSuggestionType, }; -pub(crate) struct BossbarColorArgumentConsumer; +pub struct BossbarColorArgumentConsumer; impl GetClientSideArgParser for BossbarColorArgumentConsumer { fn get_client_side_parser(&self) -> ProtoCmdArgParser { diff --git a/pumpkin/src/command/args/arg_bossbar_style.rs b/pumpkin/src/command/args/arg_bossbar_style.rs index 9081bc3f8..46cd946ae 100644 --- a/pumpkin/src/command/args/arg_bossbar_style.rs +++ b/pumpkin/src/command/args/arg_bossbar_style.rs @@ -11,7 +11,7 @@ use pumpkin_protocol::client::play::{ CommandSuggestion, ProtoCmdArgParser, ProtoCmdArgSuggestionType, }; -pub(crate) struct BossbarStyleArgumentConsumer; +pub struct BossbarStyleArgumentConsumer; impl GetClientSideArgParser for BossbarStyleArgumentConsumer { fn get_client_side_parser(&self) -> ProtoCmdArgParser { diff --git a/pumpkin/src/command/args/arg_bounded_num.rs b/pumpkin/src/command/args/arg_bounded_num.rs index 1fc613752..26e643011 100644 --- a/pumpkin/src/command/args/arg_bounded_num.rs +++ b/pumpkin/src/command/args/arg_bounded_num.rs @@ -80,10 +80,10 @@ impl FindArg<'_> for BoundedNumArgumentConsumer { } } -pub(crate) type NotInBounds = (); +pub type NotInBounds = (); #[derive(Clone, Copy)] -pub(crate) enum Number { +pub enum Number { F64(f64), F32(f32), I32(i32), diff --git a/pumpkin/src/command/args/arg_command.rs b/pumpkin/src/command/args/arg_command.rs index ba2da50bb..4feb5e4bc 100644 --- a/pumpkin/src/command/args/arg_command.rs +++ b/pumpkin/src/command/args/arg_command.rs @@ -15,7 +15,7 @@ use crate::{ use super::{Arg, ArgumentConsumer, DefaultNameArgConsumer, FindArg, GetClientSideArgParser}; -pub(crate) struct CommandTreeArgumentConsumer; +pub struct CommandTreeArgumentConsumer; impl GetClientSideArgParser for CommandTreeArgumentConsumer { fn get_client_side_parser(&self) -> ProtoCmdArgParser { diff --git a/pumpkin/src/command/args/arg_entities.rs b/pumpkin/src/command/args/arg_entities.rs index 939ce91ae..c3c9a1462 100644 --- a/pumpkin/src/command/args/arg_entities.rs +++ b/pumpkin/src/command/args/arg_entities.rs @@ -18,7 +18,7 @@ use super::{Arg, DefaultNameArgConsumer, FindArg, GetClientSideArgParser}; /// todo: implement (currently just calls [`super::arg_player::PlayerArgumentConsumer`]) /// /// For selecting zero, one or multiple entities, eg. using @s, a player name, @a or @e -pub(crate) struct EntitiesArgumentConsumer; +pub struct EntitiesArgumentConsumer; impl GetClientSideArgParser for EntitiesArgumentConsumer { fn get_client_side_parser(&self) -> ProtoCmdArgParser { diff --git a/pumpkin/src/command/args/arg_entity.rs b/pumpkin/src/command/args/arg_entity.rs index acc7482ae..8cff042ed 100644 --- a/pumpkin/src/command/args/arg_entity.rs +++ b/pumpkin/src/command/args/arg_entity.rs @@ -19,7 +19,7 @@ use super::{Arg, DefaultNameArgConsumer, FindArg, GetClientSideArgParser}; /// For selecting a single entity, eg. using @s, a player name or entity uuid. /// /// Use [`super::arg_entities::EntitiesArgumentConsumer`] when there may be multiple targets. -pub(crate) struct EntityArgumentConsumer; +pub struct EntityArgumentConsumer; impl GetClientSideArgParser for EntityArgumentConsumer { fn get_client_side_parser(&self) -> ProtoCmdArgParser { diff --git a/pumpkin/src/command/args/arg_gamemode.rs b/pumpkin/src/command/args/arg_gamemode.rs index df75ddd81..b3b8ca2e6 100644 --- a/pumpkin/src/command/args/arg_gamemode.rs +++ b/pumpkin/src/command/args/arg_gamemode.rs @@ -13,7 +13,7 @@ use crate::{ use super::{Arg, ArgumentConsumer, DefaultNameArgConsumer, FindArg, GetClientSideArgParser}; -pub(crate) struct GamemodeArgumentConsumer; +pub struct GamemodeArgumentConsumer; impl GetClientSideArgParser for GamemodeArgumentConsumer { fn get_client_side_parser(&self) -> ProtoCmdArgParser { diff --git a/pumpkin/src/command/args/arg_item.rs b/pumpkin/src/command/args/arg_item.rs index 2fa4feab4..687874ac2 100644 --- a/pumpkin/src/command/args/arg_item.rs +++ b/pumpkin/src/command/args/arg_item.rs @@ -14,7 +14,7 @@ use super::{ Arg, DefaultNameArgConsumer, FindArg, GetClientSideArgParser, }; -pub(crate) struct ItemArgumentConsumer; +pub struct ItemArgumentConsumer; impl GetClientSideArgParser for ItemArgumentConsumer { fn get_client_side_parser(&self) -> ProtoCmdArgParser { diff --git a/pumpkin/src/command/args/arg_message.rs b/pumpkin/src/command/args/arg_message.rs index 29feda6bd..37d5b4a13 100644 --- a/pumpkin/src/command/args/arg_message.rs +++ b/pumpkin/src/command/args/arg_message.rs @@ -14,7 +14,7 @@ use super::{ }; /// Consumes all remaining words/args. Does not consume if there is no word. -pub(crate) struct MsgArgConsumer; +pub struct MsgArgConsumer; impl GetClientSideArgParser for MsgArgConsumer { fn get_client_side_parser(&self) -> ProtoCmdArgParser { diff --git a/pumpkin/src/command/args/arg_players.rs b/pumpkin/src/command/args/arg_players.rs index beafeffa1..9954705b1 100644 --- a/pumpkin/src/command/args/arg_players.rs +++ b/pumpkin/src/command/args/arg_players.rs @@ -15,7 +15,7 @@ use super::super::args::ArgumentConsumer; use super::{Arg, DefaultNameArgConsumer, FindArg, GetClientSideArgParser}; /// Select zero, one or multiple players -pub(crate) struct PlayersArgumentConsumer; +pub struct PlayersArgumentConsumer; impl GetClientSideArgParser for PlayersArgumentConsumer { fn get_client_side_parser(&self) -> ProtoCmdArgParser { diff --git a/pumpkin/src/command/args/arg_position_2d.rs b/pumpkin/src/command/args/arg_position_2d.rs index 0e875694e..52060a4dc 100644 --- a/pumpkin/src/command/args/arg_position_2d.rs +++ b/pumpkin/src/command/args/arg_position_2d.rs @@ -17,7 +17,7 @@ use super::{Arg, DefaultNameArgConsumer, FindArg, GetClientSideArgParser}; /// x and z coordinates only /// /// todo: implememnt ~ ^ notations -pub(crate) struct Position2DArgumentConsumer; +pub struct Position2DArgumentConsumer; impl GetClientSideArgParser for Position2DArgumentConsumer { fn get_client_side_parser(&self) -> ProtoCmdArgParser { diff --git a/pumpkin/src/command/args/arg_position_3d.rs b/pumpkin/src/command/args/arg_position_3d.rs index 70b6d5367..97389faf7 100644 --- a/pumpkin/src/command/args/arg_position_3d.rs +++ b/pumpkin/src/command/args/arg_position_3d.rs @@ -14,7 +14,7 @@ use super::coordinate::MaybeRelativeCoordinate; use super::{Arg, DefaultNameArgConsumer, FindArg, GetClientSideArgParser}; /// x, y and z coordinates -pub(crate) struct Position3DArgumentConsumer; +pub struct Position3DArgumentConsumer; impl GetClientSideArgParser for Position3DArgumentConsumer { fn get_client_side_parser(&self) -> ProtoCmdArgParser { diff --git a/pumpkin/src/command/args/arg_position_block.rs b/pumpkin/src/command/args/arg_position_block.rs index 6d1b9966c..2203c3804 100644 --- a/pumpkin/src/command/args/arg_position_block.rs +++ b/pumpkin/src/command/args/arg_position_block.rs @@ -15,7 +15,7 @@ use super::coordinate::MaybeRelativeBlockCoordinate; use super::{Arg, DefaultNameArgConsumer, FindArg, GetClientSideArgParser}; /// x, y and z coordinates -pub(crate) struct BlockPosArgumentConsumer; +pub struct BlockPosArgumentConsumer; impl GetClientSideArgParser for BlockPosArgumentConsumer { fn get_client_side_parser(&self) -> ProtoCmdArgParser { diff --git a/pumpkin/src/command/args/arg_rotation.rs b/pumpkin/src/command/args/arg_rotation.rs index 9241fd241..030c9f7b8 100644 --- a/pumpkin/src/command/args/arg_rotation.rs +++ b/pumpkin/src/command/args/arg_rotation.rs @@ -12,7 +12,7 @@ use super::super::args::ArgumentConsumer; use super::{Arg, DefaultNameArgConsumer, FindArg, GetClientSideArgParser}; /// yaw and pitch -pub(crate) struct RotationArgumentConsumer; +pub struct RotationArgumentConsumer; impl GetClientSideArgParser for RotationArgumentConsumer { fn get_client_side_parser(&self) -> ProtoCmdArgParser { diff --git a/pumpkin/src/command/args/arg_simple.rs b/pumpkin/src/command/args/arg_simple.rs index fbf63cd16..b27b38f91 100644 --- a/pumpkin/src/command/args/arg_simple.rs +++ b/pumpkin/src/command/args/arg_simple.rs @@ -15,7 +15,7 @@ use super::{ /// Should never be a permanent solution #[allow(unused)] -pub(crate) struct SimpleArgConsumer; +pub struct SimpleArgConsumer; impl GetClientSideArgParser for SimpleArgConsumer { fn get_client_side_parser(&self) -> ProtoCmdArgParser { diff --git a/pumpkin/src/command/args/mod.rs b/pumpkin/src/command/args/mod.rs index 11356f1f3..4196deec9 100644 --- a/pumpkin/src/command/args/mod.rs +++ b/pumpkin/src/command/args/mod.rs @@ -19,31 +19,31 @@ use super::{ use crate::world::bossbar::{BossbarColor, BossbarDivisions}; use crate::{entity::player::Player, server::Server}; -pub(crate) mod arg_block; -pub(crate) mod arg_bool; -pub(crate) mod arg_bossbar_color; -pub(crate) mod arg_bossbar_style; -pub(crate) mod arg_bounded_num; -pub(crate) mod arg_command; -pub(crate) mod arg_entities; -pub(crate) mod arg_entity; -pub(crate) mod arg_gamemode; -pub(crate) mod arg_item; -pub(crate) mod arg_message; -pub(crate) mod arg_players; -pub(crate) mod arg_position_2d; -pub(crate) mod arg_position_3d; -pub(crate) mod arg_position_block; -pub(crate) mod arg_resource_location; -pub(crate) mod arg_rotation; -pub(crate) mod arg_simple; -pub(crate) mod arg_sound; -pub(crate) mod arg_textcomponent; +pub mod arg_block; +pub mod arg_bool; +pub mod arg_bossbar_color; +pub mod arg_bossbar_style; +pub mod arg_bounded_num; +pub mod arg_command; +pub mod arg_entities; +pub mod arg_entity; +pub mod arg_gamemode; +pub mod arg_item; +pub mod arg_message; +pub mod arg_players; +pub mod arg_position_2d; +pub mod arg_position_3d; +pub mod arg_position_block; +pub mod arg_resource_location; +pub mod arg_rotation; +pub mod arg_simple; +pub mod arg_sound; +pub mod arg_textcomponent; mod coordinate; /// see [`crate::commands::tree_builder::argument`] #[async_trait] -pub(crate) trait ArgumentConsumer: Sync + GetClientSideArgParser { +pub trait ArgumentConsumer: Sync + GetClientSideArgParser { async fn consume<'a>( &'a self, sender: &CommandSender<'a>, @@ -62,19 +62,19 @@ pub(crate) trait ArgumentConsumer: Sync + GetClientSideArgParser { ) -> Result>, CommandError>; } -pub(crate) trait GetClientSideArgParser { +pub trait GetClientSideArgParser { /// Return the parser the client should use while typing a command in chat. fn get_client_side_parser(&self) -> ProtoCmdArgParser; /// Usually this should return None. This can be used to force suggestions to be processed on serverside. fn get_client_side_suggestion_type_override(&self) -> Option; } -pub(crate) trait DefaultNameArgConsumer: ArgumentConsumer { +pub trait DefaultNameArgConsumer: ArgumentConsumer { fn default_name(&self) -> String; } #[derive(Clone)] -pub(crate) enum Arg<'a> { +pub enum Arg<'a> { Entities(Vec>), Entity(Arc), Players(Vec>), @@ -98,7 +98,7 @@ pub(crate) enum Arg<'a> { } /// see [`crate::commands::tree_builder::argument`] and [`CommandTree::execute`]/[`crate::commands::tree_builder::NonLeafNodeBuilder::execute`] -pub(crate) type ConsumedArgs<'a> = HashMap<&'a str, Arg<'a>>; +pub type ConsumedArgs<'a> = HashMap<&'a str, Arg<'a>>; pub(crate) trait GetCloned { fn get_cloned(&self, key: &K) -> Option; @@ -110,7 +110,7 @@ impl GetCloned for HashMap { } } -pub(crate) trait FindArg<'a> { +pub trait FindArg<'a> { type Data; fn find_arg(args: &'a ConsumedArgs, name: &str) -> Result; diff --git a/pumpkin/src/command/commands/cmd_plugin.rs b/pumpkin/src/command/commands/cmd_plugin.rs new file mode 100644 index 000000000..8f980e4d2 --- /dev/null +++ b/pumpkin/src/command/commands/cmd_plugin.rs @@ -0,0 +1,188 @@ +use async_trait::async_trait; +use pumpkin_util::{ + text::{color::NamedColor, hover::HoverEvent, TextComponent}, + PermissionLvl, +}; + +use crate::{ + command::{ + args::{arg_simple::SimpleArgConsumer, Arg, ConsumedArgs}, + tree::CommandTree, + tree_builder::{argument, literal, require}, + CommandError, CommandExecutor, CommandSender, + }, + PLUGIN_MANAGER, +}; + +use crate::command::CommandError::InvalidConsumption; + +const NAMES: [&str; 1] = ["plugin"]; + +const DESCRIPTION: &str = "Manage plugins."; + +const PLUGIN_NAME: &str = "plugin_name"; + +struct ListExecutor; + +#[async_trait] +impl CommandExecutor for ListExecutor { + async fn execute<'a>( + &self, + sender: &mut CommandSender<'a>, + _server: &crate::server::Server, + _args: &ConsumedArgs<'a>, + ) -> Result<(), CommandError> { + let plugin_manager = PLUGIN_MANAGER.lock().await; + let plugins = plugin_manager.list_plugins(); + + let message_text = if plugins.is_empty() { + "There are no loaded plugins.".to_string() + } else if plugins.len() == 1 { + "There is 1 plugin loaded:\n".to_string() + } else { + format!("There are {} plugins loaded:\n", plugins.len()) + }; + let mut message = TextComponent::text(message_text); + + for (i, (metadata, loaded)) in plugins.clone().into_iter().enumerate() { + let fmt = if i == plugins.len() - 1 { + metadata.name.to_string() + } else { + format!("{}, ", metadata.name) + }; + let hover_text = format!( + "Version: {}\nAuthors: {}\nDescription: {}", + metadata.version, metadata.authors, metadata.description + ); + let component = if *loaded { + TextComponent::text(fmt) + .color_named(NamedColor::Green) + .hover_event(HoverEvent::ShowText(hover_text.into())) + } else { + TextComponent::text(fmt) + .color_named(NamedColor::Red) + .hover_event(HoverEvent::ShowText(hover_text.into())) + }; + message = message.add_child(component); + } + + sender.send_message(message).await; + + Ok(()) + } +} + +struct LoadExecutor; + +#[async_trait] +impl CommandExecutor for LoadExecutor { + async fn execute<'a>( + &self, + sender: &mut CommandSender<'a>, + _server: &crate::server::Server, + args: &ConsumedArgs<'a>, + ) -> Result<(), CommandError> { + let Some(Arg::Simple(plugin_name)) = args.get(PLUGIN_NAME) else { + return Err(InvalidConsumption(Some(PLUGIN_NAME.into()))); + }; + let mut plugin_manager = PLUGIN_MANAGER.lock().await; + + if plugin_manager.is_plugin_loaded(plugin_name) { + sender + .send_message( + TextComponent::text(format!("Plugin {plugin_name} is already loaded")) + .color_named(NamedColor::Red), + ) + .await; + return Ok(()); + } + + let result = plugin_manager.load_plugin(plugin_name).await; + + match result { + Ok(()) => { + sender + .send_message( + TextComponent::text(format!("Plugin {plugin_name} loaded successfully")) + .color_named(NamedColor::Green), + ) + .await; + } + Err(e) => { + sender + .send_message( + TextComponent::text(format!("Failed to load plugin {plugin_name}: {e}")) + .color_named(NamedColor::Red), + ) + .await; + } + } + + Ok(()) + } +} + +struct UnloadExecutor; + +#[async_trait] +impl CommandExecutor for UnloadExecutor { + async fn execute<'a>( + &self, + sender: &mut CommandSender<'a>, + _server: &crate::server::Server, + args: &ConsumedArgs<'a>, + ) -> Result<(), CommandError> { + let Some(Arg::Simple(plugin_name)) = args.get(PLUGIN_NAME) else { + return Err(InvalidConsumption(Some(PLUGIN_NAME.into()))); + }; + let mut plugin_manager = PLUGIN_MANAGER.lock().await; + + if !plugin_manager.is_plugin_loaded(plugin_name) { + sender + .send_message( + TextComponent::text(format!("Plugin {plugin_name} is not loaded")) + .color_named(NamedColor::Red), + ) + .await; + return Ok(()); + } + + let result = plugin_manager.unload_plugin(plugin_name).await; + + match result { + Ok(()) => { + sender + .send_message( + TextComponent::text(format!("Plugin {plugin_name} unloaded successfully",)) + .color_named(NamedColor::Green), + ) + .await; + } + Err(e) => { + sender + .send_message( + TextComponent::text(format!("Failed to unload plugin {plugin_name}: {e}")) + .color_named(NamedColor::Red), + ) + .await; + } + } + + Ok(()) + } +} + +pub fn init_command_tree() -> CommandTree { + CommandTree::new(NAMES, DESCRIPTION).with_child( + require(|sender| sender.has_permission_lvl(PermissionLvl::Three)) + .with_child( + literal("load") + .with_child(argument(PLUGIN_NAME, SimpleArgConsumer).execute(LoadExecutor)), + ) + .with_child( + literal("unload") + .with_child(argument(PLUGIN_NAME, SimpleArgConsumer).execute(UnloadExecutor)), + ) + .with_child(literal("list").execute(ListExecutor)), + ) +} diff --git a/pumpkin/src/command/commands/cmd_plugins.rs b/pumpkin/src/command/commands/cmd_plugins.rs new file mode 100644 index 000000000..b2d4a3fe8 --- /dev/null +++ b/pumpkin/src/command/commands/cmd_plugins.rs @@ -0,0 +1,67 @@ +use async_trait::async_trait; +use pumpkin_util::text::{color::NamedColor, hover::HoverEvent, TextComponent}; + +use crate::{ + command::{ + args::ConsumedArgs, tree::CommandTree, CommandError, CommandExecutor, CommandSender, + }, + PLUGIN_MANAGER, +}; + +const NAMES: [&str; 1] = ["plugins"]; + +const DESCRIPTION: &str = "List all available plugins."; + +struct ListExecutor; + +#[async_trait] +impl CommandExecutor for ListExecutor { + async fn execute<'a>( + &self, + sender: &mut CommandSender<'a>, + _server: &crate::server::Server, + _args: &ConsumedArgs<'a>, + ) -> Result<(), CommandError> { + let plugin_manager = PLUGIN_MANAGER.lock().await; + let plugins = plugin_manager.list_plugins(); + + let message_text = if plugins.is_empty() { + "There are no loaded plugins.".to_string() + } else if plugins.len() == 1 { + "There is 1 plugin loaded:\n".to_string() + } else { + format!("There are {} plugins loaded:\n", plugins.len()) + }; + let mut message = TextComponent::text(message_text); + + for (i, (metadata, loaded)) in plugins.clone().into_iter().enumerate() { + let fmt = if i == plugins.len() - 1 { + metadata.name.to_string() + } else { + format!("{}, ", metadata.name) + }; + let hover_text = format!( + "Version: {}\nAuthors: {}\nDescription: {}", + metadata.version, metadata.authors, metadata.description + ); + let component = if *loaded { + TextComponent::text(fmt) + .color_named(NamedColor::Green) + .hover_event(HoverEvent::ShowText(hover_text.into())) + } else { + TextComponent::text(fmt) + .color_named(NamedColor::Red) + .hover_event(HoverEvent::ShowText(hover_text.into())) + }; + message = message.add_child(component); + } + + sender.send_message(message).await; + + Ok(()) + } +} + +pub fn init_command_tree() -> CommandTree { + CommandTree::new(NAMES, DESCRIPTION).execute(ListExecutor) +} diff --git a/pumpkin/src/command/commands/mod.rs b/pumpkin/src/command/commands/mod.rs index 1a3e34f50..ef551051f 100644 --- a/pumpkin/src/command/commands/mod.rs +++ b/pumpkin/src/command/commands/mod.rs @@ -11,6 +11,8 @@ pub mod cmd_list; pub mod cmd_me; pub mod cmd_op; pub mod cmd_playsound; +pub mod cmd_plugin; +pub mod cmd_plugins; pub mod cmd_pumpkin; pub mod cmd_say; pub mod cmd_seed; diff --git a/pumpkin/src/command/dispatcher.rs b/pumpkin/src/command/dispatcher.rs index a1d99c3bb..7bfa01782 100644 --- a/pumpkin/src/command/dispatcher.rs +++ b/pumpkin/src/command/dispatcher.rs @@ -15,7 +15,7 @@ use pumpkin_util::text::color::{Color, NamedColor}; use std::collections::{HashMap, HashSet}; #[derive(Debug)] -pub(crate) enum CommandError { +pub enum CommandError { /// This error means that there was an error while parsing a previously consumed argument. /// That only happens when consumption is wrongly implemented, as it should ensure parsing may /// never fail. diff --git a/pumpkin/src/command/mod.rs b/pumpkin/src/command/mod.rs index 2527678ea..36e480421 100644 --- a/pumpkin/src/command/mod.rs +++ b/pumpkin/src/command/mod.rs @@ -11,8 +11,8 @@ use args::ConsumedArgs; use async_trait::async_trait; use commands::{ cmd_clear, cmd_deop, cmd_fill, cmd_gamemode, cmd_give, cmd_help, cmd_kick, cmd_kill, cmd_list, - cmd_me, cmd_op, cmd_playsound, cmd_pumpkin, cmd_say, cmd_setblock, cmd_stop, cmd_teleport, - cmd_time, cmd_worldborder, + cmd_me, cmd_op, cmd_playsound, cmd_plugin, cmd_plugins, cmd_pumpkin, cmd_say, cmd_setblock, + cmd_stop, cmd_teleport, cmd_time, cmd_worldborder, }; use dispatcher::CommandError; use pumpkin_util::math::vector3::Vector3; @@ -23,8 +23,8 @@ pub mod args; pub mod client_cmd_suggestions; mod commands; pub mod dispatcher; -mod tree; -mod tree_builder; +pub mod tree; +pub mod tree_builder; mod tree_format; pub enum CommandSender<'a> { @@ -120,6 +120,8 @@ pub fn default_dispatcher() -> CommandDispatcher { dispatcher.register(cmd_help::init_command_tree(), PermissionLvl::Zero); dispatcher.register(cmd_kill::init_command_tree(), PermissionLvl::Two); dispatcher.register(cmd_kick::init_command_tree(), PermissionLvl::Three); + dispatcher.register(cmd_plugin::init_command_tree(), PermissionLvl::Three); + dispatcher.register(cmd_plugins::init_command_tree(), PermissionLvl::Three); dispatcher.register(cmd_worldborder::init_command_tree(), PermissionLvl::Two); dispatcher.register(cmd_teleport::init_command_tree(), PermissionLvl::Two); dispatcher.register(cmd_time::init_command_tree(), PermissionLvl::Two); @@ -139,7 +141,7 @@ pub fn default_dispatcher() -> CommandDispatcher { } #[async_trait] -pub(crate) trait CommandExecutor: Sync { +pub trait CommandExecutor: Sync { async fn execute<'a>( &self, sender: &mut CommandSender<'a>, diff --git a/pumpkin/src/command/tree_builder.rs b/pumpkin/src/command/tree_builder.rs index f9d5bff99..e0fad776a 100644 --- a/pumpkin/src/command/tree_builder.rs +++ b/pumpkin/src/command/tree_builder.rs @@ -8,6 +8,7 @@ use crate::command::CommandSender; impl CommandTree { /// Add a child [Node] to the root of this [`CommandTree`]. + #[must_use] pub fn with_child(mut self, child: impl NodeBuilder) -> Self { let node = child.build(&mut self); self.children.push(self.nodes.len()); @@ -16,6 +17,7 @@ impl CommandTree { } /// provide at least one name + #[must_use] pub fn new( names: impl IntoIterator>, description: impl Into, @@ -37,6 +39,7 @@ impl CommandTree { /// desired type. /// /// Also see [`NonLeafNodeBuilder::execute`]. + #[must_use] pub fn execute(mut self, executor: impl CommandExecutor + 'static + Send) -> Self { let node = Node { node_type: NodeType::ExecuteLeaf { @@ -100,6 +103,7 @@ impl NodeBuilder for NonLeafNodeBuilder { impl NonLeafNodeBuilder { /// Add a child [Node] to this one. + #[must_use] pub fn with_child(mut self, child: Self) -> Self { self.child_nodes.push(child); self @@ -112,6 +116,7 @@ impl NonLeafNodeBuilder { /// desired type. /// /// Also see [`CommandTree::execute`]. + #[must_use] pub fn execute(mut self, executor: impl CommandExecutor + 'static + Send) -> Self { self.leaf_nodes.push(LeafNodeBuilder { node_type: NodeType::ExecuteLeaf { @@ -124,6 +129,7 @@ impl NonLeafNodeBuilder { } /// Matches a sting literal. +#[must_use] pub fn literal(string: impl Into) -> NonLeafNodeBuilder { NonLeafNodeBuilder { node_type: NodeType::Literal { diff --git a/pumpkin/src/entity/player.rs b/pumpkin/src/entity/player.rs index 4581ee090..bde2bed9d 100644 --- a/pumpkin/src/entity/player.rs +++ b/pumpkin/src/entity/player.rs @@ -225,11 +225,11 @@ impl Player { /// Removes the Player out of the current World #[allow(unused_variables)] - pub async fn remove(&self) { + pub async fn remove(self: Arc) { let world = self.world(); self.cancel_tasks.notify_waiters(); - world.remove_player(self).await; + world.remove_player(self.clone()).await; let cylindrical = self.watched_section.load(); @@ -816,7 +816,8 @@ impl Player { .await; } SPlayerAction::PACKET_ID => { - self.handle_player_action(SPlayerAction::read(bytebuf)?, server) + self.clone() + .handle_player_action(SPlayerAction::read(bytebuf)?, server) .await; } SPlayerCommand::PACKET_ID => { diff --git a/pumpkin/src/lib.rs b/pumpkin/src/lib.rs new file mode 100644 index 000000000..719455c5e --- /dev/null +++ b/pumpkin/src/lib.rs @@ -0,0 +1,20 @@ +use std::sync::LazyLock; + +use plugin::PluginManager; +use pumpkin_util::text::TextComponent; +use tokio::sync::Mutex; + +pub mod block; +pub mod command; +pub mod data; +pub mod entity; +pub mod error; +pub mod net; +pub mod plugin; +pub mod server; +pub mod world; + +const GIT_VERSION: &str = env!("GIT_VERSION"); + +pub static PLUGIN_MANAGER: LazyLock> = + LazyLock::new(|| Mutex::new(PluginManager::new())); diff --git a/pumpkin/src/main.rs b/pumpkin/src/main.rs index f232cd547..328b4bf46 100644 --- a/pumpkin/src/main.rs +++ b/pumpkin/src/main.rs @@ -36,13 +36,21 @@ compile_error!("Compiling for WASI targets is not supported!"); use log::LevelFilter; use net::{lan_broadcast, query, rcon::RCONServer, Client}; +use plugin::PluginManager; use server::{ticker::Ticker, Server}; -use std::io::{self}; -use tokio::io::{AsyncBufReadExt, BufReader}; +use std::{ + io::{self}, + sync::LazyLock, +}; #[cfg(not(unix))] use tokio::signal::ctrl_c; #[cfg(unix)] use tokio::signal::unix::{signal, SignalKind}; +use tokio::sync::Mutex; +use tokio::{ + io::{AsyncBufReadExt, AsyncReadExt, AsyncWriteExt, BufReader}, + net::tcp::OwnedReadHalf, +}; use std::sync::Arc; @@ -59,9 +67,13 @@ pub mod data; pub mod entity; pub mod error; pub mod net; +pub mod plugin; pub mod server; pub mod world; +pub static PLUGIN_MANAGER: LazyLock> = + LazyLock::new(|| Mutex::new(PluginManager::new())); + fn scrub_address(ip: &str) -> String { use pumpkin_config::BASIC_CONFIG; if BASIC_CONFIG.scrub_ips { @@ -155,6 +167,12 @@ async fn main() { let server = Arc::new(Server::new()); let mut ticker = Ticker::new(BASIC_CONFIG.tps); + { + let mut loader_lock = PLUGIN_MANAGER.lock().await; + loader_lock.set_server(server.clone()); + loader_lock.load_plugins().await.unwrap(); + }; + log::info!("Started Server took {}ms", time.elapsed().as_millis()); log::info!("You now can connect to the server, Listening on {}", addr); @@ -203,7 +221,25 @@ async fn main() { id ); - let client = Arc::new(Client::new(connection, addr, id)); + let (tx, mut rx) = tokio::sync::mpsc::channel(64); + let (connection_reader, connection_writer) = connection.into_split(); + let connection_reader = Arc::new(Mutex::new(connection_reader)); + let connection_writer = Arc::new(Mutex::new(connection_writer)); + + let client = Arc::new(Client::new(tx, addr, id)); + + let client_clone = client.clone(); + tokio::spawn(async move { + while (rx.recv().await).is_some() { + let mut enc = client_clone.enc.lock().await; + let buf = enc.take(); + if let Err(e) = connection_writer.lock().await.write_all(&buf).await { + log::warn!("Failed to write packet to client: {e}"); + client_clone.close(); + break; + } + } + }); let server = server.clone(); tokio::spawn(async move { @@ -212,7 +248,7 @@ async fn main() { .make_player .load(std::sync::atomic::Ordering::Relaxed) { - let open = client.poll().await; + let open = poll(&client, connection_reader.clone()).await; if open { client.process_packets(&server).await; }; @@ -232,7 +268,7 @@ async fn main() { .closed .load(core::sync::atomic::Ordering::Relaxed) { - let open = player.client.poll().await; + let open = poll(&player.client, connection_reader.clone()).await; if open { player.process_packets(&server).await; }; @@ -245,6 +281,53 @@ async fn main() { } } +async fn poll(client: &Client, connection_reader: Arc>) -> bool { + loop { + if client.closed.load(std::sync::atomic::Ordering::Relaxed) { + // If we manually close (like a kick) we dont want to keep reading bytes + return false; + } + + let mut dec = client.dec.lock().await; + + match dec.decode() { + Ok(Some(packet)) => { + client.add_packet(packet).await; + return true; + } + Ok(None) => (), //log::debug!("Waiting for more data to complete packet..."), + Err(err) => { + log::warn!("Failed to decode packet for: {}", err.to_string()); + client.close(); + return false; // return to avoid reserving additional bytes + } + } + + dec.reserve(4096); + let mut buf = dec.take_capacity(); + + let bytes_read = connection_reader.lock().await.read_buf(&mut buf).await; + match bytes_read { + Ok(cnt) => { + //log::debug!("Read {} bytes", cnt); + if cnt == 0 { + client.close(); + return false; + } + } + Err(error) => { + log::error!("Error while reading incoming packet {}", error); + client.close(); + return false; + } + }; + + // This should always be an O(1) unsplit because we reserved space earlier and + // the call to `read_buf` shouldn't have grown the allocation. + dec.queue_bytes(buf); + } +} + fn handle_interrupt() { log::warn!( "{}", diff --git a/pumpkin/src/net/mod.rs b/pumpkin/src/net/mod.rs index d02dbc0b5..e04916840 100644 --- a/pumpkin/src/net/mod.rs +++ b/pumpkin/src/net/mod.rs @@ -39,7 +39,7 @@ use pumpkin_util::{text::TextComponent, ProfileAction}; use serde::Deserialize; use sha1::Digest; use sha2::Sha256; -use tokio::io::{AsyncReadExt, AsyncWriteExt}; +use tokio::sync::mpsc; use tokio::sync::Mutex; use thiserror::Error; @@ -128,15 +128,14 @@ pub struct Client { pub connection_state: AtomicCell, /// Indicates if the client connection is closed. pub closed: AtomicBool, - /// The underlying TCP connection to the client. - pub connection_reader: Arc>, - pub connection_writer: Arc>, /// The client's IP address. pub address: Mutex, /// The packet encoder for outgoing packets. - enc: Arc>, + pub enc: Arc>, /// The packet decoder for incoming packets. - dec: Arc>, + pub dec: Arc>, + /// A channel for sending packets to the client. + pub server_packets_channel: mpsc::Sender<()>, /// A queue of raw packets received from the client, waiting to be processed. pub client_packets_queue: Arc>>, /// Indicates whether the client should be converted into a player. @@ -145,8 +144,7 @@ pub struct Client { impl Client { #[must_use] - pub fn new(connection: tokio::net::TcpStream, address: SocketAddr, id: u16) -> Self { - let (connection_reader, connection_writer) = connection.into_split(); + pub fn new(server_packets_channel: mpsc::Sender<()>, address: SocketAddr, id: u16) -> Self { Self { id, protocol_version: AtomicI32::new(0), @@ -156,11 +154,10 @@ impl Client { server_address: Mutex::new(String::new()), address: Mutex::new(address), connection_state: AtomicCell::new(ConnectionState::HandShake), - connection_reader: Arc::new(Mutex::new(connection_reader)), - connection_writer: Arc::new(Mutex::new(connection_writer)), enc: Arc::new(Mutex::new(PacketEncoder::default())), dec: Arc::new(Mutex::new(PacketDecoder::default())), closed: AtomicBool::new(false), + server_packets_channel, client_packets_queue: Arc::new(Mutex::new(VecDeque::new())), make_player: AtomicBool::new(false), } @@ -252,10 +249,12 @@ impl Client { return; } - let mut writer = self.connection_writer.lock().await; + let _ = self.server_packets_channel.send(()).await; + + /* let mut writer = self.connection_writer.lock().await; if let Err(error) = writer.write_all(&enc.take()).await { log::debug!("Unable to write to connection: {}", error.to_string()); - } + } */ /* else if let Err(error) = writer.flush().await { @@ -297,8 +296,10 @@ impl Client { let mut enc = self.enc.lock().await; enc.append_packet(packet)?; - let mut writer = self.connection_writer.lock().await; - let _ = writer.write_all(&enc.take()).await; + let _ = self.server_packets_channel.send(()).await; + + /* let mut writer = self.connection_writer.lock().await; + let _ = writer.write_all(&enc.take()).await; */ /* writer @@ -507,56 +508,6 @@ impl Client { Ok(()) } - /// Reads the connection until our buffer of len 4096 is full, then decode - /// Close connection when an error occurs or when the Client closed the connection - /// Returns if connection is still open - pub async fn poll(&self) -> bool { - loop { - if self.closed.load(std::sync::atomic::Ordering::Relaxed) { - // If we manually close (like a kick) we dont want to keep reading bytes - return false; - } - - let mut dec = self.dec.lock().await; - - match dec.decode() { - Ok(Some(packet)) => { - self.add_packet(packet).await; - return true; - } - Ok(None) => (), //log::debug!("Waiting for more data to complete packet..."), - Err(err) => { - log::warn!("Failed to decode packet for: {}", err.to_string()); - self.close(); - return false; // return to avoid reserving additional bytes - } - } - - dec.reserve(4096); - let mut buf = dec.take_capacity(); - - let bytes_read = self.connection_reader.lock().await.read_buf(&mut buf).await; - match bytes_read { - Ok(cnt) => { - //log::debug!("Read {} bytes", cnt); - if cnt == 0 { - self.close(); - return false; - } - } - Err(error) => { - log::error!("Error while reading incoming packet {}", error); - self.close(); - return false; - } - }; - - // This should always be an O(1) unsplit because we reserved space earlier and - // the call to `read_buf` shouldn't have grown the allocation. - dec.queue_bytes(buf); - } - } - /// Disconnects a client from the server with a specified reason. /// /// This function kicks a client identified by its ID from the server. The appropriate disconnect packet is sent based on the client's current connection state. diff --git a/pumpkin/src/net/packet/play.rs b/pumpkin/src/net/packet/play.rs index 6f3c15cc0..a9125c095 100644 --- a/pumpkin/src/net/packet/play.rs +++ b/pumpkin/src/net/packet/play.rs @@ -737,11 +737,14 @@ impl Player { } } - pub async fn handle_player_action(&self, player_action: SPlayerAction, server: &Server) { + pub async fn handle_player_action( + self: Arc, + player_action: SPlayerAction, + server: &Server, + ) { if !self.has_client_loaded() { return; } - match Status::try_from(player_action.status.0) { Ok(status) => match status { Status::StartedDigging => { @@ -762,12 +765,12 @@ impl Player { let world = &entity.world; let block = world.get_block(&location).await; - world.break_block(&location, Some(self)).await; + world.break_block(&location, Some(self.clone())).await; if let Ok(block) = block { server .block_manager - .on_broken(block, self, location, server) + .on_broken(block, &self, location, server) .await; } } @@ -800,12 +803,12 @@ impl Player { let world = &entity.world; let block = world.get_block(&location).await; - world.break_block(&location, Some(self)).await; + world.break_block(&location, Some(self.clone())).await; if let Ok(block) = block { server .block_manager - .on_broken(block, self, location, server) + .on_broken(block, &self, location, server) .await; } } diff --git a/pumpkin/src/net/query.rs b/pumpkin/src/net/query.rs index c56ee8318..b43df3e80 100644 --- a/pumpkin/src/net/query.rs +++ b/pumpkin/src/net/query.rs @@ -133,11 +133,19 @@ async fn handle_packet( } } + let plugin_manager = crate::PLUGIN_MANAGER.lock().await; + let plugins = plugin_manager + .list_plugins() + .iter() + .map(|(meta, _)| meta.name.to_string()) + .reduce(|acc, name| format!("{acc}, {name}")) + .unwrap_or_default(); + let response = CFullStatus { session_id: packet.session_id, hostname: CString::new(BASIC_CONFIG.motd.as_str())?, version: CString::new(CURRENT_MC_VERSION)?, - plugins: CString::new("Pumpkin on 1.21.4")?, // TODO: Fill this with plugins when plugins are working + plugins: CString::new(plugins)?, map: CString::new("world")?, // TODO: Get actual world name num_players: server.get_player_count().await, max_players: BASIC_CONFIG.max_players as usize, diff --git a/pumpkin/src/plugin/api/context.rs b/pumpkin/src/plugin/api/context.rs new file mode 100644 index 000000000..61312c020 --- /dev/null +++ b/pumpkin/src/plugin/api/context.rs @@ -0,0 +1,77 @@ +use std::{fs, path::Path, sync::Arc}; + +use pumpkin_util::PermissionLvl; +use tokio::sync::RwLock; + +use crate::{ + entity::player::Player, + plugin::{EventHandler, HandlerMap, TypedEventHandler}, + server::Server, +}; + +use super::{Event, EventPriority, PluginMetadata}; + +pub struct Context { + metadata: PluginMetadata<'static>, + pub server: Arc, + handlers: Arc>, +} +impl Context { + #[must_use] + pub fn new( + metadata: PluginMetadata<'static>, + server: Arc, + handlers: Arc>, + ) -> Self { + Self { + metadata, + server, + handlers, + } + } + + #[must_use] + pub fn get_data_folder(&self) -> String { + let path = format!("./plugins/{}", self.metadata.name); + if !Path::new(&path).exists() { + fs::create_dir_all(&path).unwrap(); + } + path + } + + pub async fn get_player_by_name(&self, player_name: String) -> Option> { + self.server.get_player_by_name(&player_name).await + } + + pub async fn register_command( + &self, + tree: crate::command::tree::CommandTree, + permission: PermissionLvl, + ) { + let mut dispatcher_lock = self.server.command_dispatcher.write().await; + dispatcher_lock.register(tree, permission); + } + + pub async fn register_event( + &self, + handler: H, + priority: EventPriority, + blocking: bool, + ) where + H: EventHandler + 'static, + { + let mut handlers = self.handlers.write().await; + + let handlers_vec = handlers + .entry(E::get_name_static()) + .or_insert_with(Vec::new); + + let typed_handler = TypedEventHandler { + handler, + priority, + blocking, + _phantom: std::marker::PhantomData, + }; + handlers_vec.push(Box::new(typed_handler)); + } +} diff --git a/pumpkin/src/plugin/api/events/block/break.rs b/pumpkin/src/plugin/api/events/block/break.rs new file mode 100644 index 000000000..4069e316b --- /dev/null +++ b/pumpkin/src/plugin/api/events/block/break.rs @@ -0,0 +1,89 @@ +use std::sync::Arc; + +use pumpkin_world::block::block_registry::Block; + +use crate::{ + entity::player::Player, + plugin::{CancellableEvent, Event}, +}; + +use super::{BlockBreakEvent, BlockEvent, BlockExpEvent}; + +pub struct BlockBreakEventImpl { + player: Option>, + block: Block, + exp: u32, + drop: bool, + is_cancelled: bool, +} + +impl BlockBreakEventImpl { + #[must_use] + pub fn new(player: Option>, block: Block, exp: u32, drop: bool) -> Self { + Self { + player, + block, + exp, + drop, + is_cancelled: false, + } + } +} + +impl BlockBreakEvent for BlockBreakEventImpl { + fn get_player(&self) -> Option> { + self.player.clone() + } + + fn will_drop(&self) -> bool { + self.drop + } + + fn set_drop(&mut self, drop: bool) { + self.drop = drop; + } +} + +impl BlockExpEvent for BlockBreakEventImpl { + fn get_exp_to_drop(&self) -> u32 { + self.exp + } + + fn set_exp_to_drop(&mut self, exp: u32) { + self.exp = exp; + } +} + +impl BlockEvent for BlockBreakEventImpl { + fn get_block(&self) -> &Block { + &self.block + } +} + +impl CancellableEvent for BlockBreakEventImpl { + fn is_cancelled(&self) -> bool { + self.is_cancelled + } + + fn set_cancelled(&mut self, cancelled: bool) { + self.is_cancelled = cancelled; + } +} + +impl Event for BlockBreakEventImpl { + fn get_name_static() -> &'static str { + "BlockBreakEvent" + } + + fn get_name(&self) -> &'static str { + "BlockBreakEvent" + } + + fn as_any_mut(&mut self) -> &mut dyn std::any::Any { + self + } + + fn as_any(&self) -> &dyn std::any::Any { + self + } +} diff --git a/pumpkin/src/plugin/api/events/block/burn.rs b/pumpkin/src/plugin/api/events/block/burn.rs new file mode 100644 index 000000000..96d7f4724 --- /dev/null +++ b/pumpkin/src/plugin/api/events/block/burn.rs @@ -0,0 +1,51 @@ +use pumpkin_world::block::block_registry::Block; + +use crate::plugin::{CancellableEvent, Event}; + +use super::{BlockBurnEvent, BlockEvent}; + +pub struct BlockBurnEventImpl { + igniting_block: Block, + block: Block, + is_cancelled: bool, +} + +impl BlockBurnEvent for BlockBurnEventImpl { + fn get_igniting_block(&self) -> &Block { + &self.igniting_block + } +} + +impl BlockEvent for BlockBurnEventImpl { + fn get_block(&self) -> &Block { + &self.block + } +} + +impl CancellableEvent for BlockBurnEventImpl { + fn is_cancelled(&self) -> bool { + self.is_cancelled + } + + fn set_cancelled(&mut self, cancelled: bool) { + self.is_cancelled = cancelled; + } +} + +impl Event for BlockBurnEventImpl { + fn get_name_static() -> &'static str { + "BlockBurnEvent" + } + + fn get_name(&self) -> &'static str { + "BlockBurnEvent" + } + + fn as_any_mut(&mut self) -> &mut dyn std::any::Any { + self + } + + fn as_any(&self) -> &dyn std::any::Any { + self + } +} diff --git a/pumpkin/src/plugin/api/events/block/can_build.rs b/pumpkin/src/plugin/api/events/block/can_build.rs new file mode 100644 index 000000000..da9fee947 --- /dev/null +++ b/pumpkin/src/plugin/api/events/block/can_build.rs @@ -0,0 +1,69 @@ +use pumpkin_world::block::block_registry::Block; +use std::sync::Arc; + +use crate::{ + entity::player::Player, + plugin::{CancellableEvent, Event}, +}; + +use super::{BlockCanBuildEvent, BlockEvent}; + +pub struct BlockCanBuildEventImpl { + block_to_build: Block, + buildable: bool, + player: Arc, + block: Block, + is_cancelled: bool, +} + +impl BlockCanBuildEvent for BlockCanBuildEventImpl { + fn get_block_to_build(&self) -> &Block { + &self.block_to_build + } + + fn is_buildable(&self) -> bool { + self.buildable + } + + fn set_buildable(&mut self, buildable: bool) { + self.buildable = buildable; + } + + fn get_player(&self) -> Option> { + Some(self.player.clone()) + } +} + +impl BlockEvent for BlockCanBuildEventImpl { + fn get_block(&self) -> &Block { + &self.block + } +} + +impl CancellableEvent for BlockCanBuildEventImpl { + fn is_cancelled(&self) -> bool { + self.is_cancelled + } + + fn set_cancelled(&mut self, cancelled: bool) { + self.is_cancelled = cancelled; + } +} + +impl Event for BlockCanBuildEventImpl { + fn get_name_static() -> &'static str { + "BlockCanBuildEvent" + } + + fn get_name(&self) -> &'static str { + "BlockCanBuildEvent" + } + + fn as_any_mut(&mut self) -> &mut dyn std::any::Any { + self + } + + fn as_any(&self) -> &dyn std::any::Any { + self + } +} diff --git a/pumpkin/src/plugin/api/events/block/mod.rs b/pumpkin/src/plugin/api/events/block/mod.rs new file mode 100644 index 000000000..6ee2ed852 --- /dev/null +++ b/pumpkin/src/plugin/api/events/block/mod.rs @@ -0,0 +1,46 @@ +use std::sync::Arc; + +use pumpkin_world::block::block_registry::Block; + +use crate::entity::player::Player; + +use super::CancellableEvent; + +pub mod r#break; +pub mod burn; +pub mod can_build; +pub mod place; + +pub trait BlockEvent: CancellableEvent { + fn get_block(&self) -> &Block; +} + +pub trait BlockExpEvent: BlockEvent { + fn get_exp_to_drop(&self) -> u32; + fn set_exp_to_drop(&mut self, exp: u32); +} + +pub trait BlockBreakEvent: BlockExpEvent { + fn get_player(&self) -> Option>; + fn will_drop(&self) -> bool; + fn set_drop(&mut self, drop: bool); +} + +pub trait BlockBurnEvent: BlockEvent { + fn get_igniting_block(&self) -> &Block; +} + +pub trait BlockCanBuildEvent: BlockEvent { + fn get_block_to_build(&self) -> &Block; + fn is_buildable(&self) -> bool; + fn set_buildable(&mut self, buildable: bool); + fn get_player(&self) -> Option>; +} + +pub trait BlockPlaceEvent: BlockEvent { + fn get_player(&self) -> Option>; + fn can_build(&self) -> bool; + fn set_build(&mut self, build: bool); + fn get_block_placed_against(&self) -> &Block; + fn get_block_placed(&self) -> &Block; +} diff --git a/pumpkin/src/plugin/api/events/block/place.rs b/pumpkin/src/plugin/api/events/block/place.rs new file mode 100644 index 000000000..b8566ebe8 --- /dev/null +++ b/pumpkin/src/plugin/api/events/block/place.rs @@ -0,0 +1,73 @@ +use pumpkin_world::block::block_registry::Block; +use std::sync::Arc; + +use crate::{ + entity::player::Player, + plugin::{CancellableEvent, Event}, +}; + +use super::{BlockEvent, BlockPlaceEvent}; + +pub struct BlockPlaceEventImpl { + player: Arc, + block_placed: Block, + block_placed_against: Block, + can_build: bool, + is_cancelled: bool, +} + +impl BlockPlaceEvent for BlockPlaceEventImpl { + fn get_player(&self) -> Option> { + Some(self.player.clone()) + } + + fn can_build(&self) -> bool { + self.can_build + } + + fn set_build(&mut self, build: bool) { + self.can_build = build; + } + + fn get_block_placed_against(&self) -> &Block { + &self.block_placed_against + } + + fn get_block_placed(&self) -> &Block { + &self.block_placed + } +} + +impl BlockEvent for BlockPlaceEventImpl { + fn get_block(&self) -> &Block { + &self.block_placed + } +} + +impl CancellableEvent for BlockPlaceEventImpl { + fn is_cancelled(&self) -> bool { + self.is_cancelled + } + + fn set_cancelled(&mut self, cancelled: bool) { + self.is_cancelled = cancelled; + } +} + +impl Event for BlockPlaceEventImpl { + fn get_name_static() -> &'static str { + "BlockPlaceEvent" + } + + fn get_name(&self) -> &'static str { + "BlockPlaceEvent" + } + + fn as_any_mut(&mut self) -> &mut dyn std::any::Any { + self + } + + fn as_any(&self) -> &dyn std::any::Any { + self + } +} diff --git a/pumpkin/src/plugin/api/events/mod.rs b/pumpkin/src/plugin/api/events/mod.rs new file mode 100644 index 000000000..0958d1482 --- /dev/null +++ b/pumpkin/src/plugin/api/events/mod.rs @@ -0,0 +1,28 @@ +use std::any::Any; + +pub mod block; +pub mod player; + +pub trait Event: Any + Send + Sync { + fn get_name_static() -> &'static str + where + Self: Sized; + fn get_name(&self) -> &'static str; + fn as_any_mut(&mut self) -> &mut dyn Any; + fn as_any(&self) -> &dyn Any; +} + +pub trait CancellableEvent: Event { + fn is_cancelled(&self) -> bool; + fn set_cancelled(&mut self, cancelled: bool); +} + +#[derive(Eq, PartialEq, Ord, PartialOrd, Clone)] +// Lowest priority events are executed first, so that higher priority events can override their changes +pub enum EventPriority { + Highest, + High, + Normal, + Low, + Lowest, +} diff --git a/pumpkin/src/plugin/api/events/player/join.rs b/pumpkin/src/plugin/api/events/player/join.rs new file mode 100644 index 000000000..ff9de9424 --- /dev/null +++ b/pumpkin/src/plugin/api/events/player/join.rs @@ -0,0 +1,70 @@ +use std::sync::Arc; + +use pumpkin_util::text::TextComponent; + +use crate::{ + entity::player::Player, + plugin::{CancellableEvent, Event}, +}; + +use super::{PlayerEvent, PlayerJoinEvent}; + +pub struct PlayerJoinEventImpl { + player: Arc, + join_message: TextComponent, + is_cancelled: bool, +} + +impl PlayerJoinEventImpl { + pub fn new(player: Arc, join_message: TextComponent) -> Self { + Self { + player, + join_message, + is_cancelled: false, + } + } +} + +impl PlayerJoinEvent for PlayerJoinEventImpl { + fn get_join_message(&self) -> &TextComponent { + &self.join_message + } + + fn set_join_message(&mut self, message: TextComponent) { + self.join_message = message; + } +} + +impl PlayerEvent for PlayerJoinEventImpl { + fn get_player(&self) -> Arc { + self.player.clone() + } +} + +impl CancellableEvent for PlayerJoinEventImpl { + fn is_cancelled(&self) -> bool { + self.is_cancelled + } + + fn set_cancelled(&mut self, cancelled: bool) { + self.is_cancelled = cancelled; + } +} + +impl Event for PlayerJoinEventImpl { + fn get_name_static() -> &'static str { + "PlayerJoinEvent" + } + + fn get_name(&self) -> &'static str { + "PlayerJoinEvent" + } + + fn as_any_mut(&mut self) -> &mut dyn std::any::Any { + self + } + + fn as_any(&self) -> &dyn std::any::Any { + self + } +} diff --git a/pumpkin/src/plugin/api/events/player/leave.rs b/pumpkin/src/plugin/api/events/player/leave.rs new file mode 100644 index 000000000..90a687e7f --- /dev/null +++ b/pumpkin/src/plugin/api/events/player/leave.rs @@ -0,0 +1,70 @@ +use std::sync::Arc; + +use pumpkin_util::text::TextComponent; + +use crate::{ + entity::player::Player, + plugin::{CancellableEvent, Event}, +}; + +use super::{PlayerEvent, PlayerLeaveEvent}; + +pub struct PlayerLeaveEventImpl { + player: Arc, + leave_message: TextComponent, + is_cancelled: bool, +} + +impl PlayerLeaveEventImpl { + pub fn new(player: Arc, leave_message: TextComponent) -> Self { + Self { + player, + leave_message, + is_cancelled: false, + } + } +} + +impl PlayerLeaveEvent for PlayerLeaveEventImpl { + fn get_leave_message(&self) -> &TextComponent { + &self.leave_message + } + + fn set_leave_message(&mut self, message: TextComponent) { + self.leave_message = message; + } +} + +impl PlayerEvent for PlayerLeaveEventImpl { + fn get_player(&self) -> Arc { + self.player.clone() + } +} + +impl CancellableEvent for PlayerLeaveEventImpl { + fn is_cancelled(&self) -> bool { + self.is_cancelled + } + + fn set_cancelled(&mut self, cancelled: bool) { + self.is_cancelled = cancelled; + } +} + +impl Event for PlayerLeaveEventImpl { + fn get_name_static() -> &'static str { + "PlayerLeaveEvent" + } + + fn get_name(&self) -> &'static str { + "PlayerLeaveEvent" + } + + fn as_any_mut(&mut self) -> &mut dyn std::any::Any { + self + } + + fn as_any(&self) -> &dyn std::any::Any { + self + } +} diff --git a/pumpkin/src/plugin/api/events/player/mod.rs b/pumpkin/src/plugin/api/events/player/mod.rs new file mode 100644 index 000000000..0101da718 --- /dev/null +++ b/pumpkin/src/plugin/api/events/player/mod.rs @@ -0,0 +1,23 @@ +use pumpkin_util::text::TextComponent; +use std::sync::Arc; + +use crate::entity::player::Player; + +use super::CancellableEvent; + +pub mod join; +pub mod leave; + +pub trait PlayerEvent: CancellableEvent { + fn get_player(&self) -> Arc; +} + +pub trait PlayerJoinEvent: PlayerEvent { + fn get_join_message(&self) -> &TextComponent; + fn set_join_message(&mut self, message: TextComponent); +} + +pub trait PlayerLeaveEvent: PlayerEvent { + fn get_leave_message(&self) -> &TextComponent; + fn set_leave_message(&mut self, message: TextComponent); +} diff --git a/pumpkin/src/plugin/api/mod.rs b/pumpkin/src/plugin/api/mod.rs new file mode 100644 index 000000000..1d924438f --- /dev/null +++ b/pumpkin/src/plugin/api/mod.rs @@ -0,0 +1,31 @@ +pub mod context; +pub mod events; + +use async_trait::async_trait; +pub use context::*; +pub use events::*; + +#[derive(Debug, Clone)] +pub struct PluginMetadata<'s> { + /// The name of the plugin. + pub name: &'s str, + /// The version of the plugin. + pub version: &'s str, + /// The authors of the plugin. + pub authors: &'s str, + /// A description of the plugin. + pub description: &'s str, +} + +#[async_trait] +pub trait Plugin: Send + Sync + 'static { + /// Called when the plugin is loaded. + async fn on_load(&mut self, _server: &Context) -> Result<(), String> { + Ok(()) + } + + /// Called when the plugin is unloaded. + async fn on_unload(&mut self, _server: &Context) -> Result<(), String> { + Ok(()) + } +} diff --git a/pumpkin/src/plugin/mod.rs b/pumpkin/src/plugin/mod.rs new file mode 100644 index 000000000..16288d2a6 --- /dev/null +++ b/pumpkin/src/plugin/mod.rs @@ -0,0 +1,272 @@ +pub mod api; + +pub use api::*; +use async_trait::async_trait; +use std::{collections::HashMap, fs, path::Path, sync::Arc}; +use tokio::sync::RwLock; + +use crate::server::Server; + +type PluginData = ( + PluginMetadata<'static>, + Box, + libloading::Library, + bool, +); + +#[async_trait] +pub trait DynEventHandler: Send + Sync { + async fn handle_dyn(&self, event: &(dyn Event + Send + Sync)); + async fn handle_blocking_dyn(&self, _event: &mut (dyn Event + Send + Sync)); + fn is_blocking(&self) -> bool; + fn get_priority(&self) -> EventPriority; +} + +#[async_trait] +pub trait EventHandler: Send + Sync { + async fn handle(&self, _event: &E) { + unimplemented!(); + } + async fn handle_blocking(&self, _event: &mut E) { + unimplemented!(); + } +} + +struct TypedEventHandler +where + E: Event + Send + Sync + 'static, + H: EventHandler + Send + Sync, +{ + handler: H, + priority: EventPriority, + blocking: bool, + _phantom: std::marker::PhantomData, +} + +#[async_trait] +impl DynEventHandler for TypedEventHandler +where + E: Event + Send + Sync + 'static, + H: EventHandler + Send + Sync, +{ + async fn handle_blocking_dyn(&self, event: &mut (dyn Event + Send + Sync)) { + // Check if the event is the same type as E. We can not use the type_id because it is + // different in the plugin and the main program + if E::get_name_static() == event.get_name() { + // This is fully safe as long as the event's get_name() and get_name_static() + // functions are correctly implemented and don't conflict with other events + let event = unsafe { + &mut *std::ptr::from_mut::(event.as_any_mut()).cast::() + }; + self.handler.handle_blocking(event).await; + } + } + + async fn handle_dyn(&self, event: &(dyn Event + Send + Sync)) { + // Check if the event is the same type as E. We can not use the type_id because it is + // different in the plugin and the main program + if E::get_name_static() == event.get_name() { + // This is fully safe as long as the event's get_name() and get_name_static() + // functions are correctly implemented and don't conflict with other events + let event = + unsafe { &*std::ptr::from_ref::(event.as_any()).cast::() }; + self.handler.handle(event).await; + } + } + + fn is_blocking(&self) -> bool { + self.blocking + } + + fn get_priority(&self) -> EventPriority { + self.priority.clone() + } +} + +pub type HandlerMap = HashMap<&'static str, Vec>>; + +pub struct PluginManager { + plugins: Vec, + server: Option>, + handlers: Arc>, +} + +impl Default for PluginManager { + fn default() -> Self { + Self::new() + } +} + +impl PluginManager { + #[must_use] + pub fn new() -> Self { + Self { + plugins: vec![], + server: None, + handlers: Arc::new(RwLock::new(HashMap::new())), + } + } + + pub fn set_server(&mut self, server: Arc) { + self.server = Some(server); + } + + pub async fn load_plugins(&mut self) -> Result<(), String> { + const PLUGIN_DIR: &str = "./plugins"; + + if !Path::new(PLUGIN_DIR).exists() { + fs::create_dir(PLUGIN_DIR).unwrap(); + } + + let dir_entires = fs::read_dir(PLUGIN_DIR); + + for entry in dir_entires.unwrap() { + if !entry.as_ref().unwrap().path().is_file() { + continue; + } + self.try_load_plugin(entry.unwrap().path().as_path()).await; + } + + Ok(()) + } + + async fn try_load_plugin(&mut self, path: &Path) { + let library = unsafe { libloading::Library::new(path).unwrap() }; + + let plugin_fn = unsafe { library.get:: Box>(b"plugin").unwrap() }; + let metadata: &PluginMetadata = + unsafe { &**library.get::<*const PluginMetadata>(b"METADATA").unwrap() }; + + // The chance that this will panic is non-existent, but just in case + let context = Context::new( + metadata.clone(), + self.server.clone().expect("Server not set"), + self.handlers.clone(), + ); + let mut plugin_box = plugin_fn(); + let res = plugin_box.on_load(&context).await; + let mut loaded = true; + if let Err(e) = res { + log::error!("Error loading plugin: {}", e); + loaded = false; + } + + self.plugins + .push((metadata.clone(), plugin_box, library, loaded)); + } + + #[must_use] + pub fn is_plugin_loaded(&self, name: &str) -> bool { + self.plugins + .iter() + .any(|(metadata, _, _, loaded)| metadata.name == name && *loaded) + } + + pub async fn load_plugin(&mut self, name: &str) -> Result<(), String> { + let plugin = self + .plugins + .iter_mut() + .find(|(metadata, _, _, _)| metadata.name == name); + + if let Some((metadata, plugin, _, loaded)) = plugin { + if *loaded { + return Err(format!("Plugin {name} is already loaded")); + } + + let context = Context::new( + metadata.clone(), + self.server.clone().expect("Server not set"), + self.handlers.clone(), + ); + let res = plugin.on_load(&context).await; + res?; + *loaded = true; + Ok(()) + } else { + Err(format!("Plugin {name} not found")) + } + } + + pub async fn unload_plugin(&mut self, name: &str) -> Result<(), String> { + let plugin = self + .plugins + .iter_mut() + .find(|(metadata, _, _, _)| metadata.name == name); + + if let Some((metadata, plugin, _, loaded)) = plugin { + let context = Context::new( + metadata.clone(), + self.server.clone().expect("Server not set"), + self.handlers.clone(), + ); + let res = plugin.on_unload(&context).await; + res?; + *loaded = false; + Ok(()) + } else { + Err(format!("Plugin {name} not found")) + } + } + + #[must_use] + pub fn list_plugins(&self) -> Vec<(&PluginMetadata, &bool)> { + self.plugins + .iter() + .map(|(metadata, _, _, loaded)| (metadata, loaded)) + .collect() + } + + pub async fn register( + &self, + handler: H, + priority: EventPriority, + blocking: bool, + ) where + H: EventHandler + 'static, + { + let mut handlers = self.handlers.write().await; + + let handlers_vec = handlers + .entry(E::get_name_static()) + .or_insert_with(Vec::new); + + let typed_handler = TypedEventHandler { + handler, + priority, + blocking, + _phantom: std::marker::PhantomData, + }; + + handlers_vec.push(Box::new(typed_handler)); + } + + pub async fn fire(&self, mut event: E) -> E { + // Take a snapshot of handlers to avoid lifetime issues + let handlers = self.handlers.read().await; + + log::debug!("Firing event: {}", E::get_name_static()); + + if let Some(handlers_vec) = handlers.get(&E::get_name_static()) { + log::debug!( + "Found {} handlers for event: {}", + handlers_vec.len(), + E::get_name_static() + ); + + let (blocking_handlers, non_blocking_handlers): (Vec<_>, Vec<_>) = handlers_vec + .iter() + .partition(|handler| handler.is_blocking()); + + for handler in blocking_handlers { + handler.handle_blocking_dyn(&mut event).await; + } + + // TODO: Run non-blocking handlers in parallel + for handler in non_blocking_handlers { + handler.handle_dyn(&event).await; + } + } + + event + } +} diff --git a/pumpkin/src/world/mod.rs b/pumpkin/src/world/mod.rs index 89a6f57de..70b0d732a 100644 --- a/pumpkin/src/world/mod.rs +++ b/pumpkin/src/world/mod.rs @@ -7,7 +7,16 @@ use crate::{ command::client_cmd_suggestions, entity::{living::LivingEntity, mob::MobEntity, player::Player, Entity, EntityId}, error::PumpkinError, + plugin::{ + block::r#break::BlockBreakEventImpl, + player::{ + join::PlayerJoinEventImpl, leave::PlayerLeaveEventImpl, PlayerJoinEvent, + PlayerLeaveEvent, + }, + CancellableEvent, + }, server::Server, + PLUGIN_MANAGER, }; use level_time::LevelTime; use pumpkin_config::BasicConfiguration; @@ -756,20 +765,35 @@ impl World { /// * `uuid`: The unique UUID of the player to add. /// * `player`: An `Arc` reference to the player object. pub async fn add_player(&self, uuid: uuid::Uuid, player: Arc) { - let mut current_players = self.current_players.lock().await; - current_players.insert(uuid, player.clone()); + { + let mut current_players = self.current_players.lock().await; + current_players.insert(uuid, player.clone()) + }; - // Handle join message - // TODO: Config - let msg_comp = TextComponent::translate( - "multiplayer.player.joined", - [TextComponent::text(player.gameprofile.name.clone())].into(), - ) - .color_named(NamedColor::Yellow); - for player in current_players.values() { - player.send_system_message(&msg_comp).await; - } - log::info!("{}", msg_comp.to_pretty_console()); + let current_players = self.current_players.clone(); + tokio::spawn(async move { + let msg_comp = TextComponent::translate( + "multiplayer.player.joined", + [TextComponent::text(player.gameprofile.name.clone())].into(), + ) + .color_named(NamedColor::Yellow); + let event = PlayerJoinEventImpl::new(player.clone(), msg_comp); + + let event = PLUGIN_MANAGER + .lock() + .await + .fire::(event) + .await; + + if !event.is_cancelled() { + let current_players = current_players.clone(); + let players = current_players.lock().await; + for player in players.values() { + player.send_system_message(event.get_join_message()).await; + } + log::info!("{}", event.get_join_message().clone().to_pretty_console()); + } + }); } /// Removes a player from the world and broadcasts a disconnect message if enabled. @@ -790,7 +814,7 @@ impl World { /// /// - This function assumes `broadcast_packet_expect` and `remove_entity` are defined elsewhere. /// - The disconnect message sending is currently optional. Consider making it a configurable option. - pub async fn remove_player(&self, player: &Player) { + pub async fn remove_player(&self, player: Arc) { self.current_players .lock() .await @@ -804,17 +828,26 @@ impl World { .await; self.remove_entity(&player.living_entity.entity).await; - // Send disconnect message / quit message to players in the same world - // TODO: Config - let disconn_msg = TextComponent::translate( + let msg_comp = TextComponent::translate( "multiplayer.player.left", [TextComponent::text(player.gameprofile.name.clone())].into(), ) .color_named(NamedColor::Yellow); - for player in self.current_players.lock().await.values() { - player.send_system_message(&disconn_msg).await; + let event = PlayerLeaveEventImpl::new(player.clone(), msg_comp); + + let event = PLUGIN_MANAGER + .lock() + .await + .fire::(event) + .await; + + if !event.is_cancelled() { + let players = self.current_players.lock().await; + for player in players.values() { + player.send_system_message(event.get_leave_message()).await; + } + log::info!("{}", event.get_leave_message().clone().to_pretty_console()); } - log::info!("{}", disconn_msg.to_pretty_console()); } /// Adds a living entity to the world. @@ -903,22 +936,33 @@ impl World { chunk } - pub async fn break_block(&self, position: &BlockPos, cause: Option<&Player>) { - let broken_block_state_id = self.set_block_state(position, 0).await; + pub async fn break_block(&self, position: &BlockPos, cause: Option>) { + let block = self.get_block(position).await.unwrap(); + let event = BlockBreakEventImpl::new(cause.clone(), block.clone(), 0, false); - let particles_packet = CWorldEvent::new( - WorldEvent::BlockBroken as i32, - position, - broken_block_state_id.into(), - false, - ); + let event = PLUGIN_MANAGER + .lock() + .await + .fire::(event) + .await; + + if !event.is_cancelled() { + let broken_block_state_id = self.set_block_state(position, 0).await; - match cause { - Some(player) => { - self.broadcast_packet_except(&[player.gameprofile.id], &particles_packet) - .await; + let particles_packet = CWorldEvent::new( + WorldEvent::BlockBroken as i32, + position, + broken_block_state_id.into(), + false, + ); + + match cause { + Some(player) => { + self.broadcast_packet_except(&[player.gameprofile.id], &particles_packet) + .await; + } + None => self.broadcast_packet_all(&particles_packet).await, } - None => self.broadcast_packet_all(&particles_packet).await, } }