diff --git a/.gitignore b/.gitignore index 4b08876e..e65a6cd5 100644 --- a/.gitignore +++ b/.gitignore @@ -102,7 +102,9 @@ Cargo.lock #.idea/ # === PROJECT SPECIFIC === -plugins/* +plugins/**/*.so +plugins/**/*.dylib +plugins/**/*.dll world/* # docker-compose diff --git a/Cargo.toml b/Cargo.toml index 7122a354..b785b5d8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,7 @@ [workspace] resolver = "2" members = [ + "pumpkin-api-macros", "pumpkin-config", "pumpkin-core", "pumpkin-entity", diff --git a/README.md b/README.md index 7b807ac8..866e83a7 100644 --- a/README.md +++ b/README.md @@ -73,7 +73,7 @@ and customizable experience. It prioritizes performance and player enjoyment whi - [ ] Entity AI - [ ] Boss - Server - - [ ] Plugins + - [x] Plugins - [x] Query - [x] RCON - [x] Inventories diff --git a/plugins/hello-plugin-source/Cargo.toml b/plugins/hello-plugin-source/Cargo.toml new file mode 100644 index 00000000..88b2dba9 --- /dev/null +++ b/plugins/hello-plugin-source/Cargo.toml @@ -0,0 +1,32 @@ +[workspace] + +[package] +name = "hello-plugin-source" +edition = "2021" +version = "0.1.0" +authors = ["vyPal"] +description = "An example plugin for Pumpkin" + +[lib] +crate-type = ["dylib"] + +[dependencies] +pumpkin = { path = "../../pumpkin" } +pumpkin-core = { path = "../../pumpkin-core" } +pumpkin-protocol = { path = "../../pumpkin-protocol" } +pumpkin-api-macros = { path = "../../pumpkin-api-macros" } +serde = { version = "1.0", features = ["derive"] } +toml = "0.8.19" +async-trait = "0.1.83" +tokio = { version = "1.42", features = [ + "fs", + "io-util", + "macros", + "net", + "rt-multi-thread", + "sync", + "io-std", + "signal", +] } +env_logger = "0.11.6" +log = "0.4.22" \ No newline at end of file diff --git a/plugins/hello-plugin-source/data.toml b/plugins/hello-plugin-source/data.toml new file mode 100644 index 00000000..f82aff0d --- /dev/null +++ b/plugins/hello-plugin-source/data.toml @@ -0,0 +1,2 @@ +[bans] +players = [] diff --git a/plugins/hello-plugin-source/src/lib.rs b/plugins/hello-plugin-source/src/lib.rs new file mode 100644 index 00000000..004fae30 --- /dev/null +++ b/plugins/hello-plugin-source/src/lib.rs @@ -0,0 +1,214 @@ +use async_trait::async_trait; +use pumpkin::command::args::arg_block::BlockArgumentConsumer; +use pumpkin::command::args::arg_position_block::BlockPosArgumentConsumer; +use pumpkin::command::args::ConsumedArgs; +use pumpkin::command::args::FindArg; +use pumpkin::command::dispatcher::CommandError; +use pumpkin::command::tree::CommandTree; +use pumpkin::command::tree_builder::argument; +use pumpkin::command::tree_builder::literal; +use pumpkin::command::tree_builder::require; +use pumpkin::command::CommandExecutor; +use pumpkin::command::CommandSender; +use pumpkin::plugin::api::types::player::PlayerEvent; +use pumpkin::plugin::*; +use pumpkin::server::Server; +use pumpkin_api_macros::{plugin_event, plugin_impl, plugin_method, with_runtime}; +use pumpkin_core::text::color::NamedColor; +use pumpkin_core::text::TextComponent; +use pumpkin_core::PermissionLvl; +use serde::{Deserialize, Serialize}; +use std::fs; + +#[derive(Serialize, Deserialize, Debug)] +struct Config { + bans: Bans, +} + +#[derive(Serialize, Deserialize, Debug)] +struct Bans { + players: Vec, +} + +const NAMES: [&str; 1] = ["setblock2"]; + +const DESCRIPTION: &str = "Place a block."; + +const ARG_BLOCK: &str = "block"; +const ARG_BLOCK_POS: &str = "position"; + +#[derive(Clone, Copy)] +enum Mode { + /// with particles + item drops + Destroy, + + /// only replaces air + Keep, + + /// default; without particles + Replace, +} + +struct SetblockExecutor(Mode); + +// IMPORTANT: If using something that requires a tokio runtime, the #[with_runtime] attribute must be used. +// EVEN MORE IMPORTANT: The #[with_runtime] attribute must be used **BRFORE** the #[async_trait] attribute. +#[with_runtime(global)] +#[async_trait] +impl CommandExecutor for SetblockExecutor { + async fn execute<'a>( + &self, + sender: &mut CommandSender<'a>, + _server: &Server, + args: &ConsumedArgs<'a>, + ) -> Result<(), CommandError> { + let block = BlockArgumentConsumer::find_arg(args, ARG_BLOCK)?; + let block_state_id = block.default_state_id; + let pos = BlockPosArgumentConsumer::find_arg(args, ARG_BLOCK_POS)?; + let mode = self.0; + // TODO: allow console to use the command (seed sender.world) + let world = sender.world().ok_or(CommandError::InvalidRequirement)?; + + let success = match mode { + Mode::Destroy => { + world.break_block(pos, None).await; + world.set_block_state(pos, block_state_id).await; + true + } + Mode::Replace => { + world.set_block_state(pos, block_state_id).await; + true + } + Mode::Keep => match world.get_block_state(pos).await { + Ok(old_state) if old_state.air => { + world.set_block_state(pos, block_state_id).await; + true + } + Ok(_) => false, + Err(e) => return Err(CommandError::OtherPumpkin(e.into())), + }, + }; + + sender + .send_message(if success { + TextComponent::text(format!("Placed block {} at {pos}", block.name,)) + } else { + TextComponent::text(format!("Kept block at {pos}")).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::Two) && sender.world().is_some()) + .with_child( + argument(ARG_BLOCK_POS, BlockPosArgumentConsumer).with_child( + argument(ARG_BLOCK, BlockArgumentConsumer) + .with_child(literal("replace").execute(SetblockExecutor(Mode::Replace))) + .with_child(literal("destroy").execute(SetblockExecutor(Mode::Destroy))) + .with_child(literal("keep").execute(SetblockExecutor(Mode::Keep))) + .execute(SetblockExecutor(Mode::Replace)), + ), + ), + ) +} + +#[plugin_method] +async fn on_load(&mut self, server: &Context) -> Result<(), String> { + env_logger::init(); + server + .register_command(init_command_tree(), PermissionLvl::Two) + .await; + let data_folder = server.get_data_folder(); + if !fs::exists(format!("{}/data.toml", data_folder)).unwrap() { + let cfg = toml::to_string(&self.config).unwrap(); + fs::write(format!("{}/data.toml", data_folder), cfg).unwrap(); + server + .get_logger() + .info(format!("Created config in {} with {:#?}", data_folder, self.config).as_str()); + } else { + let data = fs::read_to_string(format!("{}/data.toml", data_folder)).unwrap(); + self.config = toml::from_str(data.as_str()).unwrap(); + server + .get_logger() + .info(format!("Loaded config from {} with {:#?}", data_folder, self.config).as_str()); + } + + server.get_logger().info("Plugin loaded!"); + Ok(()) +} + +#[plugin_method] +async fn on_unload(&mut self, server: &Context) -> Result<(), String> { + let data_folder = server.get_data_folder(); + let cfg = toml::to_string(&self.config).unwrap(); + fs::write(format!("{}/data.toml", data_folder), cfg).unwrap(); + + server.get_logger().info("Plugin unloaded!"); + Ok(()) +} + +#[plugin_event(blocking = true, priority = Highest)] +async fn on_player_join(&mut self, server: &Context, player: &PlayerEvent) -> Result { + server.get_logger().info( + format!( + "Player {} joined the game. Config is {:#?}", + player.gameprofile.name, self.config + ) + .as_str(), + ); + + if self.config.bans.players.contains(&player.gameprofile.name) { + let _ = player + .kick(TextComponent::text("You are banned from the server")) + .await; + return Ok(true); + } + + let _ = player + .send_message( + TextComponent::text(format!( + "Hello {}, welocme to the server", + player.gameprofile.name + )) + .color_named(NamedColor::Green), + ) + .await; + Ok(true) +} + +#[plugin_event] +async fn on_player_leave( + &mut self, + server: &Context, + player: &PlayerEvent, +) -> Result { + server + .get_logger() + .info(format!("Player {} left the game", player.gameprofile.name).as_str()); + Ok(false) +} + +#[plugin_impl] +pub struct MyPlugin { + config: Config, +} + +impl MyPlugin { + pub fn new() -> Self { + MyPlugin { + config: Config { + bans: Bans { players: vec![] }, + }, + } + } +} + +impl Default for MyPlugin { + fn default() -> Self { + Self::new() + } +} diff --git a/pumpkin-api-macros/Cargo.toml b/pumpkin-api-macros/Cargo.toml new file mode 100644 index 00000000..ebfc7939 --- /dev/null +++ b/pumpkin-api-macros/Cargo.toml @@ -0,0 +1,14 @@ +[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" +once_cell = "1.20.2" +pumpkin = { path = "../pumpkin" } \ 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 00000000..894fd4f9 --- /dev/null +++ b/pumpkin-api-macros/src/lib.rs @@ -0,0 +1,251 @@ +use once_cell::sync::Lazy; +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: Lazy>>> = + Lazy::new(|| Mutex::new(HashMap::new())); +static PLUGIN_HOOKS: Lazy>>> = + Lazy::new(|| Mutex::new(HashMap::new())); +static PLUGIN_EVENT_NAMES: Lazy>>> = + Lazy::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_event(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 mut struct_name = "MyPlugin".to_string(); + let mut priority = quote! { pumpkin::plugin::EventPriority::Normal }; + let mut blocking = quote! { false }; + + if !attr.is_empty() { + let attr_string = attr.to_string(); + for pair in attr_string.split(',').map(str::trim) { + if let Some((key, value)) = pair.split_once('=') { + let key = key.trim(); + let value = value.trim().trim_matches('"'); + + match key { + "struct_name" => { + struct_name = value.to_string(); + } + "priority" => { + priority = match value { + "Highest" => quote! { pumpkin::plugin::EventPriority::Highest }, + "High" => quote! { pumpkin::plugin::EventPriority::High }, + "Normal" => quote! { pumpkin::plugin::EventPriority::Normal }, + "Low" => quote! { pumpkin::plugin::EventPriority::Low }, + "Lowest" => quote! { pumpkin::plugin::EventPriority::Lowest }, + _ => priority, + }; + } + "blocking" => { + blocking = match value { + "true" => quote! { true }, + "false" => quote! { false }, + _ => blocking, + }; + } + _ => {} + } + } + } + } + + 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(); + + let binding = fn_name.to_string().to_owned(); + let fn_name_quoted = binding.trim_start_matches("on_"); + + let event = quote! { + pumpkin::plugin::EventDescriptor { + name: #fn_name_quoted, + priority: #priority, + blocking: #blocking, + } + } + .to_string(); + + PLUGIN_HOOKS + .lock() + .unwrap() + .entry(struct_name.clone()) + .or_default() + .push(method); + + PLUGIN_EVENT_NAMES + .lock() + .unwrap() + .entry(struct_name) + .or_default() + .push(event); + + 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(); + + let hooks = PLUGIN_HOOKS + .lock() + .unwrap() + .remove(&struct_name.to_string()) + .unwrap_or_default(); + + let hooks: Vec = hooks + .iter() + .filter_map(|method_str| method_str.parse().ok()) + .collect(); + + let events = PLUGIN_EVENT_NAMES + .lock() + .unwrap() + .remove(&struct_name.to_string()) + .unwrap_or_default(); + + let events: Vec = events + .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 + + impl pumpkin::plugin::Plugin for #struct_ident {} + + #[async_trait::async_trait] + impl pumpkin::plugin::PluginMethods for #struct_ident { + #(#methods)* + } + + #[async_trait::async_trait] + impl pumpkin::plugin::Hooks for #struct_ident { + fn registered_events(&self) -> Result<&'static [pumpkin::plugin::EventDescriptor], String> { + static EVENTS: &[EventDescriptor] = &[#(#events),*]; + Ok(EVENTS) + } + + #(#hooks)* + } + + #[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 87b03020..aceaeb58 100644 --- a/pumpkin/Cargo.toml +++ b/pumpkin/Cargo.toml @@ -10,6 +10,10 @@ FileDescription = "Pumpkin" OriginalFilename = "pumpkin.exe" LegalCopyright = "Copyright © 2024 Aleksander Medvedev" +# Required to expose pumpkin plugin API +[lib] +doctest = false + [dependencies] # pumpkin pumpkin-core = { path = "../pumpkin-core" } @@ -70,6 +74,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 da21e1bb..ecff1dff 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 4d363d2b..930444ea 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 cd12c87e..2084918e 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 9081bc3f..46cd946a 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 1fc61375..26e64301 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 ba2da50b..4feb5e4b 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 939ce91a..c3c9a146 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 acc7482a..8cff042e 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 2049ad82..68ceb7e3 100644 --- a/pumpkin/src/command/args/arg_gamemode.rs +++ b/pumpkin/src/command/args/arg_gamemode.rs @@ -14,7 +14,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 2fa4feab..687874ac 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 29feda6b..37d5b4a1 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 beafeffa..9954705b 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 e97dbda5..caf27f32 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 d8b0fef3..99c74370 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 07001b00..c2c4a530 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 9241fd24..030c9f7b 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 fbf63cd1..b27b38f9 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 f1fa8f85..16a1dd80 100644 --- a/pumpkin/src/command/args/mod.rs +++ b/pumpkin/src/command/args/mod.rs @@ -18,29 +18,29 @@ 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 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; 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>, @@ -59,19 +59,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>), @@ -94,7 +94,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; @@ -106,7 +106,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 00000000..40f1550a --- /dev/null +++ b/pumpkin/src/command/commands/cmd_plugin.rs @@ -0,0 +1,188 @@ +use async_trait::async_trait; +use pumpkin_core::{ + 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 00000000..a066d9a1 --- /dev/null +++ b/pumpkin/src/command/commands/cmd_plugins.rs @@ -0,0 +1,67 @@ +use async_trait::async_trait; +use pumpkin_core::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 5b463593..5871da38 100644 --- a/pumpkin/src/command/commands/mod.rs +++ b/pumpkin/src/command/commands/mod.rs @@ -8,6 +8,8 @@ pub mod cmd_kick; pub mod cmd_kill; pub mod cmd_list; pub mod cmd_op; +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 417c0fb5..ca80972e 100644 --- a/pumpkin/src/command/dispatcher.rs +++ b/pumpkin/src/command/dispatcher.rs @@ -15,7 +15,7 @@ use pumpkin_core::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 212054af..f40415e7 100644 --- a/pumpkin/src/command/mod.rs +++ b/pumpkin/src/command/mod.rs @@ -12,7 +12,8 @@ use async_trait::async_trait; use commands::cmd_op; use commands::{ cmd_clear, cmd_fill, cmd_gamemode, cmd_give, cmd_help, cmd_kick, cmd_kill, cmd_list, - cmd_pumpkin, cmd_say, cmd_setblock, cmd_stop, cmd_teleport, cmd_time, cmd_worldborder, + cmd_plugin, cmd_plugins, cmd_pumpkin, cmd_say, cmd_setblock, cmd_stop, cmd_teleport, cmd_time, + cmd_worldborder, }; use dispatcher::CommandError; use pumpkin_core::math::vector3::Vector3; @@ -23,8 +24,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 +121,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); @@ -136,7 +139,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 f9d5bff9..e0fad776 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 f70f161c..b3a5121f 100644 --- a/pumpkin/src/entity/player.rs +++ b/pumpkin/src/entity/player.rs @@ -216,11 +216,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(); diff --git a/pumpkin/src/lib.rs b/pumpkin/src/lib.rs new file mode 100644 index 00000000..e43c54f7 --- /dev/null +++ b/pumpkin/src/lib.rs @@ -0,0 +1,20 @@ +use std::sync::LazyLock; + +use plugin::PluginManager; +use pumpkin_core::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 3dec495f..c9ed2e59 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 { @@ -166,6 +178,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); @@ -214,7 +232,24 @@ async fn main() { id ); - let client = Arc::new(Client::new(connection, addr, id)); + let (tx, mut rx) = tokio::sync::mpsc::channel(16); + 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(); + } + } + }); let server = server.clone(); tokio::spawn(async move { @@ -223,7 +258,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; }; @@ -243,7 +278,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; }; @@ -256,6 +291,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 53b25e92..fdc365d8 100644 --- a/pumpkin/src/net/mod.rs +++ b/pumpkin/src/net/mod.rs @@ -39,7 +39,7 @@ use pumpkin_protocol::{ 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/query.rs b/pumpkin/src/net/query.rs index 0968c58c..099eb4cd 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 00000000..2df65752 --- /dev/null +++ b/pumpkin/src/plugin/api/context.rs @@ -0,0 +1,128 @@ +use std::{fs, path::Path, sync::Arc}; + +use pumpkin_core::PermissionLvl; +use tokio::sync::mpsc::{self, Sender}; + +use crate::server::Server; + +use super::{ + types::player::{player_event_handler, PlayerEvent}, + PluginMetadata, +}; + +pub struct Context { + metadata: PluginMetadata<'static>, + channel: Sender, +} +impl Context { + #[must_use] + pub fn new(metadata: PluginMetadata<'static>, channel: Sender) -> Self { + Self { metadata, channel } + } + + #[must_use] + pub fn get_logger(&self) -> Logger { + Logger { + plugin_name: self.metadata.name.to_string(), + } + } + + #[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) -> Result { + let (send, recv) = oneshot::channel(); + let _ = self + .channel + .send(ContextAction::GetPlayerByName { + player_name, + response: send, + }) + .await; + recv.await.unwrap() + } + + pub async fn register_command( + &self, + tree: crate::command::tree::CommandTree, + permission: PermissionLvl, + ) { + let _ = self + .channel + .send(ContextAction::RegisterCommand(tree, permission)) + .await; + } +} + +pub enum ContextAction { + // TODO: Implement when dispatcher is mutable + GetPlayerByName { + player_name: String, + response: oneshot::Sender>, + }, + RegisterCommand( + crate::command::tree::CommandTree, + pumpkin_core::PermissionLvl, + ), +} + +pub fn handle_context( + metadata: PluginMetadata<'static>, /* , dispatcher: Arc> */ + server: &Arc, +) -> Context { + let (send, mut recv) = mpsc::channel(1); + let server = server.clone(); + tokio::spawn(async move { + while let Some(action) = recv.recv().await { + match action { + /* ContextAction::RegisterCommand(_tree) => { + // TODO: Implement when dispatcher is mutable + } */ + ContextAction::GetPlayerByName { + player_name, + response, + } => { + let player = server.get_player_by_name(&player_name).await; + if let Some(player) = player { + response + .send(Ok( + player_event_handler(server.clone(), player.clone()).await + )) + .unwrap(); + } else { + response.send(Err("Player not found".to_string())).unwrap(); + } + } + ContextAction::RegisterCommand(tree, permission) => { + let mut dispatcher_lock = server.command_dispatcher.write().await; + dispatcher_lock.register(tree, permission); + } + } + } + }); + Context::new(metadata, send) +} + +pub struct Logger { + plugin_name: String, +} + +impl Logger { + pub fn info(&self, message: &str) { + log::info!("[{}] {}", self.plugin_name, message); + } + + pub fn warn(&self, message: &str) { + log::warn!("[{}] {}", self.plugin_name, message); + } + + pub fn error(&self, message: &str) { + log::error!("[{}] {}", self.plugin_name, message); + } +} diff --git a/pumpkin/src/plugin/api/events.rs b/pumpkin/src/plugin/api/events.rs new file mode 100644 index 00000000..e4a845ee --- /dev/null +++ b/pumpkin/src/plugin/api/events.rs @@ -0,0 +1,45 @@ +use async_trait::async_trait; + +use super::context::Context; +use super::types::player::PlayerEvent; + +#[derive(Eq, PartialEq, Ord, PartialOrd)] +pub enum EventPriority { + Highest, + High, + Normal, + Low, + Lowest, +} + +pub struct EventDescriptor { + pub name: &'static str, + pub priority: EventPriority, + pub blocking: bool, +} + +#[async_trait] +pub trait Hooks: Send + Sync + 'static { + /// Returns an array of events that the + fn registered_events(&self) -> Result<&'static [EventDescriptor], String> { + Ok(&[]) + } + + /// Called when a player joins the server. + async fn on_player_join( + &mut self, + _server: &Context, + _player: &PlayerEvent, + ) -> Result { + Ok(false) + } + + /// Called when a player leaves the server. + async fn on_player_leave( + &mut self, + _server: &Context, + _player: &PlayerEvent, + ) -> Result { + Ok(false) + } +} diff --git a/pumpkin/src/plugin/api/mod.rs b/pumpkin/src/plugin/api/mod.rs new file mode 100644 index 00000000..e4eff68d --- /dev/null +++ b/pumpkin/src/plugin/api/mod.rs @@ -0,0 +1,34 @@ +pub mod context; +pub mod events; +pub mod types; + +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 PluginMethods: 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(()) + } +} + +pub trait Plugin: PluginMethods + Hooks {} diff --git a/pumpkin/src/plugin/api/types/mod.rs b/pumpkin/src/plugin/api/types/mod.rs new file mode 100644 index 00000000..f28d7c20 --- /dev/null +++ b/pumpkin/src/plugin/api/types/mod.rs @@ -0,0 +1 @@ +pub mod player; diff --git a/pumpkin/src/plugin/api/types/player.rs b/pumpkin/src/plugin/api/types/player.rs new file mode 100644 index 00000000..ef0a719c --- /dev/null +++ b/pumpkin/src/plugin/api/types/player.rs @@ -0,0 +1,156 @@ +use std::{ops::Deref, sync::Arc}; + +use pumpkin_core::text::TextComponent; +use tokio::sync::mpsc; +use uuid::Uuid; + +use crate::{entity::player::Player, server::Server}; + +pub enum PlayerEventAction { + SendMessage { + message: TextComponent, + player_id: Uuid, + response: oneshot::Sender<()>, + }, + Kick { + reason: TextComponent, + player_id: Uuid, + response: oneshot::Sender<()>, + }, + SetHealth { + health: f32, + food: i32, + saturation: f32, + player_id: Uuid, + response: oneshot::Sender<()>, + }, + Kill { + player_id: Uuid, + response: oneshot::Sender<()>, + }, + SetGameMode { + game_mode: pumpkin_core::GameMode, + player_id: Uuid, + response: oneshot::Sender<()>, + }, +} + +pub struct PlayerEvent { + pub player: Arc, + channel: mpsc::Sender, +} + +impl Deref for PlayerEvent { + type Target = Player; + + fn deref(&self) -> &Self::Target { + &self.player + } +} + +impl PlayerEvent { + #[must_use] + pub fn new(player: Arc, channel: mpsc::Sender) -> Self { + Self { player, channel } + } + + pub async fn send_message(&self, message: TextComponent) { + let (tx, rx) = oneshot::channel(); + self.channel + .send(PlayerEventAction::SendMessage { + message, + player_id: self.player.gameprofile.id, + response: tx, + }) + .await + .unwrap(); + rx.await.unwrap(); + } + + pub async fn kick(&self, reason: TextComponent) { + let (tx, rx) = oneshot::channel(); + self.channel + .send(PlayerEventAction::Kick { + reason, + player_id: self.player.gameprofile.id, + response: tx, + }) + .await + .unwrap(); + rx.await.unwrap(); + } +} + +pub async fn player_event_handler(server: Arc, player: Arc) -> PlayerEvent { + let (send, mut recv) = mpsc::channel(1); + let player_event = PlayerEvent::new(player.clone(), send); + let players_copy = server.get_all_players().await; + tokio::spawn(async move { + while let Some(action) = recv.recv().await { + match action { + PlayerEventAction::SendMessage { + message, + player_id, + response, + } => { + if let Some(player) = + players_copy.iter().find(|p| p.gameprofile.id == player_id) + { + player.send_system_message(&message).await; + } + response.send(()).unwrap(); + } + PlayerEventAction::Kick { + reason, + player_id, + response, + } => { + if let Some(player) = + players_copy.iter().find(|p| p.gameprofile.id == player_id) + { + player.kick(reason).await; + } + response.send(()).unwrap(); + } + PlayerEventAction::SetHealth { + health, + food, + saturation, + player_id, + response, + } => { + if let Some(player) = + players_copy.iter().find(|p| p.gameprofile.id == player_id) + { + player.set_health(health, food, saturation).await; + } + response.send(()).unwrap(); + } + PlayerEventAction::Kill { + player_id, + response, + } => { + if let Some(player) = + players_copy.iter().find(|p| p.gameprofile.id == player_id) + { + player.kill().await; + } + response.send(()).unwrap(); + } + PlayerEventAction::SetGameMode { + game_mode, + player_id, + response, + } => { + if let Some(player) = + players_copy.iter().find(|p| p.gameprofile.id == player_id) + { + player.set_gamemode(game_mode).await; + } + response.send(()).unwrap(); + } + } + } + }); + player_event +} diff --git a/pumpkin/src/plugin/mod.rs b/pumpkin/src/plugin/mod.rs new file mode 100644 index 00000000..35dda213 --- /dev/null +++ b/pumpkin/src/plugin/mod.rs @@ -0,0 +1,254 @@ +pub mod api; + +pub use api::*; +use std::{any::Any, fs, future::Future, path::Path, pin::Pin, sync::Arc}; + +use crate::server::Server; + +type PluginData = ( + PluginMetadata<'static>, + Box, + libloading::Library, + bool, +); + +pub struct PluginManager { + plugins: Vec, + server: Option>, +} + +impl Default for PluginManager { + fn default() -> Self { + Self::new() + } +} + +const EVENT_PLAYER_JOIN: &str = "player_join"; +const EVENT_PLAYER_LEAVE: &str = "player_leave"; + +type EventResult = Result; +type EventFuture<'a> = Pin + Send + 'a>>; + +fn create_default_handler() -> EventFuture<'static> { + Box::pin(async { Ok(false) }) +} + +fn handle_player_event<'a>( + hooks: &'a mut Box, + context: &'a Context, + event: &'a (dyn Any + Send + Sync), + handler: impl Fn( + &'a mut Box, + &'a Context, + &'a types::player::PlayerEvent, + ) -> EventFuture<'a>, +) -> EventFuture<'a> { + event + .downcast_ref::() + .map_or_else(|| create_default_handler(), |e| handler(hooks, context, e)) +} + +fn match_event<'a>( + event_name: &str, + hooks: &'a mut Box, + context: &'a Context, + event: &'a (dyn Any + Send + Sync), +) -> EventFuture<'a> { + match event_name { + EVENT_PLAYER_JOIN => { + handle_player_event(hooks, context, event, |h, c, e| h.on_player_join(c, e)) + } + EVENT_PLAYER_LEAVE => { + handle_player_event(hooks, context, event, |h, c, e| h.on_player_leave(c, e)) + } + _ => create_default_handler(), + } +} + +impl PluginManager { + #[must_use] + pub fn new() -> Self { + Self { + plugins: vec![], + server: None, + } + } + + 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"; + + 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 = handle_context( + metadata.clone(), /* , dispatcher */ + &self.server.clone().expect("Server not set"), + ); + 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 = handle_context( + metadata.clone(), /* , dispatcher */ + &self.server.clone().expect("Server not set"), + ); + 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 = handle_context( + metadata.clone(), /* , dispatcher */ + &self.server.clone().expect("Server not set"), + ); + 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 emit(&mut self, event_name: &str, event: &T) -> bool { + let mut blocking_hooks = Vec::new(); + let mut non_blocking_hooks = Vec::new(); + + /* let dispatcher = self.command_dispatcher + .clone() + .expect("Command dispatcher not set"); // This should not happen */ + + for (metadata, hooks, _, loaded) in &mut self.plugins { + if !*loaded { + continue; + } + + let registered_events = match hooks.registered_events() { + Ok(events) => events, + Err(e) => { + log::error!("Failed to get registered events: {}", e); + continue; + } + }; + + if let Some(matching_event) = registered_events.iter().find(|e| e.name == event_name) { + let context = handle_context( + metadata.clone(), /* , dispatcher.clone() */ + &self.server.clone().expect("Server not set"), + ); + + if matching_event.blocking { + blocking_hooks.push((context, hooks)); + } else { + non_blocking_hooks.push((context, hooks)); + } + } + } + + let event_sort = |a: &(_, &mut Box), b: &(_, &mut Box)| { + b.1.registered_events() + .unwrap() + .iter() + .find(|e| e.name == event_name) + .unwrap() + .priority + .cmp( + &a.1.registered_events() + .unwrap() + .iter() + .find(|e| e.name == event_name) + .unwrap() + .priority, + ) + }; + + blocking_hooks.sort_by(event_sort); + non_blocking_hooks.sort_by(event_sort); + + let event = event as &(dyn Any + Sync + Send); + + for (context, hooks) in blocking_hooks { + match match_event(event_name, hooks, &context, event).await { + Ok(true) => return true, + Err(e) => log::error!("Error in plugin: {}", e), + _ => {} + } + } + + for (context, hooks) in non_blocking_hooks { + match match_event(event_name, hooks, &context, event).await { + Ok(true) => continue, + Err(e) => log::error!("Error in plugin: {}", e), + _ => {} + } + } + + false + } +} diff --git a/pumpkin/src/world/mod.rs b/pumpkin/src/world/mod.rs index c11d3715..00131e5a 100644 --- a/pumpkin/src/world/mod.rs +++ b/pumpkin/src/world/mod.rs @@ -7,7 +7,9 @@ use crate::{ command::client_cmd_suggestions, entity::{living::LivingEntity, player::Player, Entity}, error::PumpkinError, + plugin::types::player::{PlayerEvent, PlayerEventAction}, server::Server, + PLUGIN_MANAGER, }; use level_time::LevelTime; use pumpkin_config::BasicConfiguration; @@ -719,14 +721,88 @@ impl World { let mut current_players = self.current_players.lock().await; current_players.insert(uuid, player.clone()); - // Handle join message - // TODO: Config - let msg_txt = format!("{} joined the game.", player.gameprofile.name.as_str()); - let msg_comp = TextComponent::text(msg_txt).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 (send, mut recv) = mpsc::channel(1); + let player_event = PlayerEvent::new(player.clone(), send); + let players_copy = current_players.lock().await.clone(); + tokio::spawn(async move { + while let Some(action) = recv.recv().await { + match action { + PlayerEventAction::SendMessage { + message, + player_id, + response, + } => { + if let Some(player) = players_copy.get(&player_id) { + player.send_system_message(&message).await; + } + response.send(()).unwrap(); + } + PlayerEventAction::Kick { + reason, + player_id, + response, + } => { + if let Some(player) = players_copy.get(&player_id) { + player.kick(reason).await; + } + response.send(()).unwrap(); + } + PlayerEventAction::SetHealth { + health, + food, + saturation, + player_id, + response, + } => { + if let Some(player) = players_copy.get(&player_id) { + player.set_health(health, food, saturation).await; + } + response.send(()).unwrap(); + } + PlayerEventAction::Kill { + player_id, + response, + } => { + if let Some(player) = players_copy.get(&player_id) { + player.kill().await; + } + response.send(()).unwrap(); + } + PlayerEventAction::SetGameMode { + game_mode, + player_id, + response, + } => { + if let Some(player) = players_copy.get(&player_id) { + player.set_gamemode(game_mode).await; + } + response.send(()).unwrap(); + } + } + } + }); + if !PLUGIN_MANAGER + .lock() + .await + .emit::( + "player_join", + &player_event, + ) + .await + { + // Handle join message + // TODO: Config + let msg_txt = format!("{} joined the game.", player.gameprofile.name.as_str()); + let msg_comp = TextComponent::text(msg_txt).color_named(NamedColor::Yellow); + let players = current_players.lock().await; + for player in players.values() { + player.send_system_message(&msg_comp).await; + } + log::info!("{}", msg_comp.to_pretty_console()); + } + }); } /// Removes a player from the world and broadcasts a disconnect message if enabled. @@ -747,7 +823,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 @@ -760,15 +836,82 @@ 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_txt = format!("{} left the game.", player.gameprofile.name.as_str()); - let disconn_msg_cmp = TextComponent::text(disconn_msg_txt).color_named(NamedColor::Yellow); - for player in self.current_players.lock().await.values() { - player.send_system_message(&disconn_msg_cmp).await; + let (send, mut recv) = mpsc::channel(1); + let player_event = PlayerEvent::new(player.clone(), send); + let players_copy = self.current_players.lock().await.clone(); + tokio::spawn(async move { + while let Some(action) = recv.recv().await { + match action { + PlayerEventAction::SendMessage { + message, + player_id, + response, + } => { + if let Some(player) = players_copy.get(&player_id) { + player.send_system_message(&message).await; + } + response.send(()).unwrap(); + } + PlayerEventAction::Kick { + reason, + player_id, + response, + } => { + if let Some(player) = players_copy.get(&player_id) { + player.kick(reason).await; + } + response.send(()).unwrap(); + } + PlayerEventAction::SetHealth { + health, + food, + saturation, + player_id, + response, + } => { + if let Some(player) = players_copy.get(&player_id) { + player.set_health(health, food, saturation).await; + } + response.send(()).unwrap(); + } + PlayerEventAction::Kill { + player_id, + response, + } => { + if let Some(player) = players_copy.get(&player_id) { + player.kill().await; + } + response.send(()).unwrap(); + } + PlayerEventAction::SetGameMode { + game_mode, + player_id, + response, + } => { + if let Some(player) = players_copy.get(&player_id) { + player.set_gamemode(game_mode).await; + } + response.send(()).unwrap(); + } + } + } + }); + if !PLUGIN_MANAGER + .lock() + .await + .emit::("player_leave", &player_event) + .await + { + // Send disconnect message / quit message to players in the same world + // TODO: Config + let disconn_msg_txt = format!("{} left the game.", player.gameprofile.name.as_str()); + let disconn_msg_cmp = + TextComponent::text(disconn_msg_txt).color_named(NamedColor::Yellow); + for player in self.current_players.lock().await.values() { + player.send_system_message(&disconn_msg_cmp).await; + } + log::info!("{}", disconn_msg_cmp.to_pretty_console()); } - log::info!("{}", disconn_msg_cmp.to_pretty_console()); } /// Adds a living entity to the world.