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 371e6e84..2f08431d 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 11339e04..ccc2974c 100644 --- a/README.md +++ b/README.md @@ -73,7 +73,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/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..bb5f2792 --- /dev/null +++ b/plugins/hello-plugin-source/src/lib.rs @@ -0,0 +1,189 @@ +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::player::join::PlayerJoinEventImpl; +use pumpkin::plugin::player::PlayerEvent; +use pumpkin::plugin::player::PlayerJoinEvent; +use pumpkin::plugin::*; +use pumpkin::server::Server; +use pumpkin_api_macros::{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)), + ), + ), + ) +} + +struct MyJoinHandler; + +#[async_trait] +impl EventHandler for MyJoinHandler { + async fn handle(&self, event: &mut PlayerJoinEventImpl) { + event.set_join_message( + TextComponent::text(format!("Welcome, {}!", event.get_player().gameprofile.name)) + .color_named(NamedColor::Green), + ); + } +} + +#[plugin_method] +async fn on_load(&mut self, server: &Context) -> Result<(), String> { + env_logger::init(); + server + .register_command(init_command_tree(), PermissionLvl::Two) + .await; + server.register_event(MyJoinHandler).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_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..a6643fb2 --- /dev/null +++ b/pumpkin-api-macros/src/lib.rs @@ -0,0 +1,128 @@ +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())); + +#[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 54fe88a1..42306560 100644 --- a/pumpkin/Cargo.toml +++ b/pumpkin/Cargo.toml @@ -10,6 +10,10 @@ FileDescription = "Pumpkin" OriginalFilename = "pumpkin.exe" LegalCopyright = "Copyright © 2025 Aleksander Medvedev" +# Required to expose pumpkin plugin API +[lib] +doctest = false + [dependencies] # pumpkin pumpkin-core = { path = "../pumpkin-core" } @@ -67,6 +71,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 77d5199d..b4b3f41a 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 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 145aaba0..0aa0195e 100644 --- a/pumpkin/src/command/args/mod.rs +++ b/pumpkin/src/command/args/mod.rs @@ -19,30 +19,30 @@ 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_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_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>, @@ -61,19 +61,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>), @@ -97,7 +97,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; @@ -109,7 +109,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 c4ae339a..0254e297 100644 --- a/pumpkin/src/command/commands/mod.rs +++ b/pumpkin/src/command/commands/mod.rs @@ -9,6 +9,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 09151bd3..1fdb7817 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 aa1fa56d..a7ee4d6d 100644 --- a/pumpkin/src/command/mod.rs +++ b/pumpkin/src/command/mod.rs @@ -11,7 +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_op, cmd_pumpkin, cmd_say, cmd_setblock, cmd_stop, cmd_teleport, cmd_time, cmd_worldborder, + cmd_op, 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; @@ -22,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> { @@ -119,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); @@ -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 424db14f..0ca2b8f9 100644 --- a/pumpkin/src/entity/player.rs +++ b/pumpkin/src/entity/player.rs @@ -215,11 +215,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(); @@ -740,7 +740,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 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 fb18b177..39871362 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 { @@ -167,6 +179,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); @@ -215,7 +233,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 { @@ -224,7 +259,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; }; @@ -244,7 +279,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; }; @@ -257,6 +292,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 36d890dc..6ca88c01 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/packet/play.rs b/pumpkin/src/net/packet/play.rs index 35b0ffd8..16842062 100644 --- a/pumpkin/src/net/packet/play.rs +++ b/pumpkin/src/net/packet/play.rs @@ -689,7 +689,11 @@ 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, + ) { match Status::try_from(player_action.status.0) { Ok(status) => match status { Status::StartedDigging => { @@ -710,12 +714,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; } } @@ -748,12 +752,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 ccfb9aae..761cf50d 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..2f13f51e --- /dev/null +++ b/pumpkin/src/plugin/api/context.rs @@ -0,0 +1,96 @@ +use std::{fs, path::Path, sync::Arc}; + +use pumpkin_core::PermissionLvl; +use tokio::sync::RwLock; + +use crate::{ + entity::player::Player, + plugin::{EventHandler, HandlerMap, TypedEventHandler}, + server::Server, +}; + +use super::{Event, PluginMetadata}; + +pub struct Context { + metadata: PluginMetadata<'static>, + 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_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) -> 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) + 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, + _phantom: std::marker::PhantomData, + }; + handlers_vec.push(Box::new(typed_handler)); + } +} + +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/block/break.rs b/pumpkin/src/plugin/api/events/block/break.rs new file mode 100644 index 00000000..4c927020 --- /dev/null +++ b/pumpkin/src/plugin/api/events/block/break.rs @@ -0,0 +1,85 @@ +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 self) -> &mut 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 00000000..1ed90c80 --- /dev/null +++ b/pumpkin/src/plugin/api/events/block/burn.rs @@ -0,0 +1,47 @@ +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 self) -> &mut 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 00000000..06e4d6c5 --- /dev/null +++ b/pumpkin/src/plugin/api/events/block/can_build.rs @@ -0,0 +1,64 @@ +use pumpkin_world::block::block_registry::Block; + +use crate::{ + entity::player::Player, + plugin::{CancellableEvent, Event}, +}; + +use super::{BlockCanBuildEvent, BlockEvent}; + +pub struct BlockCanBuildEventImpl { + block_to_build: Block, + buildable: bool, + player: Player, + 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) -> &Player { + &self.player + } +} + +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 self) -> &mut 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 00000000..db219c59 --- /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) -> &Player; +} + +pub trait BlockPlaceEvent: BlockEvent { + fn get_player(&self) -> &Player; + 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 00000000..60359d06 --- /dev/null +++ b/pumpkin/src/plugin/api/events/block/place.rs @@ -0,0 +1,68 @@ +use pumpkin_world::block::block_registry::Block; + +use crate::{ + entity::player::Player, + plugin::{CancellableEvent, Event}, +}; + +use super::{BlockEvent, BlockPlaceEvent}; + +pub struct BlockPlaceEventImpl { + player: Player, + block_placed: Block, + block_placed_against: Block, + can_build: bool, + is_cancelled: bool, +} + +impl BlockPlaceEvent for BlockPlaceEventImpl { + fn get_player(&self) -> &Player { + &self.player + } + + 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 self) -> &mut 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 00000000..3266788e --- /dev/null +++ b/pumpkin/src/plugin/api/events/mod.rs @@ -0,0 +1,27 @@ +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 self) -> &mut dyn Any; +} + +pub trait CancellableEvent: Event { + fn is_cancelled(&self) -> bool; + fn set_cancelled(&mut self, cancelled: bool); +} + +#[derive(Eq, PartialEq, Ord, PartialOrd)] +// 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 00000000..8f5beeb5 --- /dev/null +++ b/pumpkin/src/plugin/api/events/player/join.rs @@ -0,0 +1,66 @@ +use std::sync::Arc; + +use pumpkin_core::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.clone() + } + + fn set_join_message(&mut self, message: TextComponent) { + self.join_message = message; + } +} + +impl PlayerEvent for PlayerJoinEventImpl { + fn get_player(&self) -> &Player { + &self.player + } +} + +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 self) -> &mut 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 00000000..6ac3213c --- /dev/null +++ b/pumpkin/src/plugin/api/events/player/leave.rs @@ -0,0 +1,66 @@ +use std::sync::Arc; + +use pumpkin_core::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.clone() + } + + fn set_leave_message(&mut self, message: TextComponent) { + self.leave_message = message; + } +} + +impl PlayerEvent for PlayerLeaveEventImpl { + fn get_player(&self) -> &Player { + &self.player + } +} + +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 self) -> &mut 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 00000000..6a134a57 --- /dev/null +++ b/pumpkin/src/plugin/api/events/player/mod.rs @@ -0,0 +1,22 @@ +use pumpkin_core::text::TextComponent; + +use crate::entity::player::Player; + +use super::CancellableEvent; + +pub mod join; +pub mod leave; + +pub trait PlayerEvent: CancellableEvent { + fn get_player(&self) -> &Player; +} + +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 00000000..1d924438 --- /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 00000000..b9670993 --- /dev/null +++ b/pumpkin/src/plugin/mod.rs @@ -0,0 +1,299 @@ +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: &mut (dyn Event + Send + Sync)); +} + +#[async_trait] +pub trait EventHandler: Send + Sync { + async fn handle(&self, event: &mut E); +} + +struct TypedEventHandler +where + E: Event + Send + Sync + 'static, + H: EventHandler + Send + Sync, +{ + handler: H, + _phantom: std::marker::PhantomData, +} + +#[async_trait] +impl DynEventHandler for TypedEventHandler +where + E: Event + Send + Sync + 'static, + H: EventHandler + Send + Sync, +{ + async fn handle_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()).cast::() + }; + self.handler.handle(event).await; + } + } +} + +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"; + + 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) + 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, + _phantom: std::marker::PhantomData, + }; + + handlers_vec.push(Box::new(typed_handler)); + } + + pub async fn fire(&self, mut event: E) -> E { + 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() + ); + for handler in handlers_vec { + handler.handle_dyn(&mut event).await; + } + } else { + log::debug!("No handlers found for event: {}", E::get_name_static()); + } + + event + } + + /* 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 7b147cef..50094a05 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}, 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; @@ -737,17 +746,52 @@ 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_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 msg_txt = format!("{} joined the game.", player.gameprofile.name.as_str()); + let msg_comp = TextComponent::text(msg_txt).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().to_pretty_console()); + } + + /* 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. @@ -768,7 +812,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 @@ -782,14 +826,39 @@ 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 msg_txt = format!("{} left the game.", player.gameprofile.name.as_str()); + let msg_comp = TextComponent::text(msg_txt).color_named(NamedColor::Yellow); + 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().to_pretty_console()); } - log::info!("{}", disconn_msg_cmp.to_pretty_console()); + /* 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()); + } */ } /// Adds a living entity to the world. @@ -877,18 +946,29 @@ impl World { chunk } - pub async fn break_block(&self, position: WorldPosition, cause: Option<&Player>) { - let broken_block_state_id = self.set_block_state(position, 0).await; + pub async fn break_block(&self, position: WorldPosition, cause: Option>) { + let block = self.get_block(position).await.unwrap(); + let event = BlockBreakEventImpl::new(cause.clone(), block.clone(), 0, 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; - let particles_packet = - CWorldEvent::new(2001, &position, broken_block_state_id.into(), false); + let particles_packet = + CWorldEvent::new(2001, &position, broken_block_state_id.into(), false); - match cause { - Some(player) => { - self.broadcast_packet_except(&[player.gameprofile.id], &particles_packet) - .await; + 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, } }