diff --git a/Cargo.toml b/Cargo.toml index b77081da..cea4a6b5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,6 +17,8 @@ members = [ "src/lib/adapters/anvil", "src/lib/adapters/nbt", "src/lib/adapters/nbt", + "src/lib/commands", + "src/lib/default_commands", "src/lib/core", "src/lib/core/state", "src/lib/derive_macros", @@ -34,6 +36,7 @@ members = [ "src/lib/utils/logging", "src/lib/utils/profiling", "src/lib/world", + "src/lib/entity_utils", ] #================== Lints ==================# @@ -87,6 +90,8 @@ codegen-units = 1 ferrumc-anvil = { path = "src/lib/adapters/anvil" } ferrumc-config = { path = "src/lib/utils/config" } ferrumc-core = { path = "src/lib/core" } +ferrumc-default-commands = { path = "src/lib/default_commands" } +ferrumc-commands = { path = "src/lib/commands" } ferrumc-ecs = { path = "src/lib/ecs" } ferrumc-events = { path = "src/lib/events"} ferrumc-general-purpose = { path = "src/lib/utils/general_purpose" } @@ -103,7 +108,7 @@ ferrumc-storage = { path = "src/lib/storage" } ferrumc-text = { path = "src/lib/text" } ferrumc-utils = { path = "src/lib/utils" } ferrumc-world = { path = "src/lib/world" } - +ferrumc-entity-utils = { path = "src/lib/entity_utils" } # Asynchronous @@ -156,7 +161,7 @@ whirlwind = "0.1.1" # Macros lazy_static = "1.5.0" quote = "1.0.37" -syn = "2.0.77" +syn = { version = "2.0.77", features = ["full"] } proc-macro2 = "1.0.86" proc-macro-crate = "3.2.0" paste = "1.0.15" @@ -187,6 +192,7 @@ colored = "2.1.0" # Misc deepsize = "0.2.0" page_size = "0.6.0" +enum-ordinalize = "4.3.0" regex = "1.11.1" # I/O @@ -195,5 +201,3 @@ memmap2 = "0.9.5" # Benchmarking criterion = { version = "0.5.1", features = ["html_reports"] } - - diff --git a/src/bin/Cargo.toml b/src/bin/Cargo.toml index 1451fff7..1b8040fe 100644 --- a/src/bin/Cargo.toml +++ b/src/bin/Cargo.toml @@ -26,6 +26,9 @@ ferrumc-macros = { workspace = true } ferrumc-nbt = { workspace = true } ferrumc-general-purpose = { workspace = true } ferrumc-state = { workspace = true } +ferrumc-commands = { workspace = true } +ferrumc-default-commands = { workspace = true } +ferrumc-text = { workspace = true } parking_lot = { workspace = true, features = ["deadlock_detection"] } tracing = { workspace = true } diff --git a/src/bin/src/main.rs b/src/bin/src/main.rs index 6c58f78a..8c7ab4c7 100644 --- a/src/bin/src/main.rs +++ b/src/bin/src/main.rs @@ -78,6 +78,9 @@ async fn entry() -> Result<()> { let global_state = Arc::new(state); create_whitelist().await; + // Needed for some reason because ctor doesn't really want to do ctor things otherwise. + ferrumc_default_commands::init(); + let all_system_handles = tokio::spawn(definition::start_all_systems(global_state.clone())); //Start the systems and wait until all of them are done diff --git a/src/bin/src/packet_handlers/chat_message.rs b/src/bin/src/packet_handlers/chat_message.rs new file mode 100644 index 00000000..0d69812c --- /dev/null +++ b/src/bin/src/packet_handlers/chat_message.rs @@ -0,0 +1,24 @@ +use ferrumc_core::identity::player_identity::PlayerIdentity; +use ferrumc_macros::event_handler; +use ferrumc_net::{ + packets::{ + incoming::chat_message::ChatMessageEvent, outgoing::system_message::SystemMessagePacket, + }, + utils::broadcast::{BroadcastOptions, BroadcastToAll}, + NetResult, +}; +use ferrumc_state::GlobalState; +use ferrumc_text::TextComponentBuilder; + +#[event_handler] +async fn chat_message(event: ChatMessageEvent, state: GlobalState) -> NetResult { + let identity = state.universe.get::(event.player_conn_id)?; + let message = + TextComponentBuilder::new(format!("<{}> {}", identity.username, event.message)).build(); + let packet = SystemMessagePacket::new(message, false); + state + .broadcast(&packet, BroadcastOptions::default().all()) + .await?; + + Ok(event) +} diff --git a/src/bin/src/packet_handlers/commands.rs b/src/bin/src/packet_handlers/commands.rs new file mode 100644 index 00000000..3de2950f --- /dev/null +++ b/src/bin/src/packet_handlers/commands.rs @@ -0,0 +1,82 @@ +use std::sync::{Arc, Mutex}; + +use ferrumc_commands::{ctx::CommandContext, infrastructure::find_command, input::CommandInput}; +use ferrumc_macros::event_handler; +use ferrumc_net::{ + connection::StreamWriter, + errors::NetError, + packets::{ + incoming::command::CommandDispatchEvent, outgoing::system_message::SystemMessagePacket, + }, +}; +use ferrumc_net_codec::encode::NetEncodeOpts; +use ferrumc_state::GlobalState; +use ferrumc_text::{NamedColor, TextComponentBuilder}; + +#[event_handler] +async fn handle_command_dispatch( + event: CommandDispatchEvent, + state: GlobalState, +) -> Result { + let mut writer = state.universe.get_mut::(event.conn_id)?; + + let command = find_command(event.command.as_str()); + if command.is_none() { + writer + .send_packet( + &SystemMessagePacket::new( + TextComponentBuilder::new("Unknown command") + .color(NamedColor::Red) + .build(), + false, + ), + &NetEncodeOpts::WithLength, + ) + .await?; + return Ok(event); + } + + let command = command.unwrap(); + + let input = &event + .command + .strip_prefix(command.name) + .unwrap_or(&event.command) + .trim_start(); + let input = CommandInput::of(input.to_string()); + let ctx = CommandContext::new(input.clone(), command.clone(), state.clone(), event.conn_id); + if let Err(err) = command.validate(&ctx, &Arc::new(Mutex::new(input))) { + writer + .send_packet( + &SystemMessagePacket::new( + TextComponentBuilder::new("Invalid arguments: ") + .extra(err) + .color(NamedColor::Red) + .build(), + false, + ), + &NetEncodeOpts::WithLength, + ) + .await?; + return Ok(event); + } + + drop(writer); // Avoid deadlocks if the executor accesses the stream writer + if let Err(err) = command.execute(ctx).await { + let mut writer = state.universe.get_mut::(event.conn_id)?; + writer + .send_packet( + &SystemMessagePacket::new( + TextComponentBuilder::new("Failed executing command: ") + .extra(err) + .color(NamedColor::Red) + .build(), + false, + ), + &NetEncodeOpts::WithLength, + ) + .await?; + }; + + Ok(event) +} diff --git a/src/bin/src/packet_handlers/login_process.rs b/src/bin/src/packet_handlers/login_process.rs index 0d2159e5..8621ecf6 100644 --- a/src/bin/src/packet_handlers/login_process.rs +++ b/src/bin/src/packet_handlers/login_process.rs @@ -1,3 +1,4 @@ +use ferrumc_commands::graph::CommandsPacket; use ferrumc_config::statics::{get_global_config, get_whitelist}; use ferrumc_core::chunks::chunk_receiver::ChunkReceiver; use ferrumc_core::identity::player_identity::PlayerIdentity; @@ -203,6 +204,14 @@ async fn handle_ack_finish_configuration( ) .await?; + trace!( + "Sending command graph: {:#?}", + ferrumc_commands::infrastructure::get_graph() + ); + writer + .send_packet(&CommandsPacket::create(), &NetEncodeOpts::WithLength) + .await?; + send_keep_alive(entity_id, &state, &mut writer).await?; let pos = state.universe.get_mut::(entity_id)?; diff --git a/src/bin/src/packet_handlers/mod.rs b/src/bin/src/packet_handlers/mod.rs index 21fee1f7..53d152ce 100644 --- a/src/bin/src/packet_handlers/mod.rs +++ b/src/bin/src/packet_handlers/mod.rs @@ -1,4 +1,6 @@ mod animations; +mod chat_message; +mod commands; mod handshake; mod login_process; mod player; diff --git a/src/lib/adapters/anvil/src/lib.rs b/src/lib/adapters/anvil/src/lib.rs index 895d6721..16b0e047 100644 --- a/src/lib/adapters/anvil/src/lib.rs +++ b/src/lib/adapters/anvil/src/lib.rs @@ -103,7 +103,7 @@ impl LoadedAnvilFile { let location = (u32::from(self.table[i * 4]) << 24) | (u32::from(self.table[i * 4 + 1]) << 16) | (u32::from(self.table[i * 4 + 2]) << 8) - | u32::from(self.table[i * 4 + 3]); + | (u32::from(self.table[i * 4 + 3])); if location != 0 { locations.push(location); } diff --git a/src/lib/commands/Cargo.toml b/src/lib/commands/Cargo.toml new file mode 100644 index 00000000..d3c2cc82 --- /dev/null +++ b/src/lib/commands/Cargo.toml @@ -0,0 +1,24 @@ +[package] +name = "ferrumc-commands" +version = "0.1.0" +edition = "2021" + +[dependencies] +thiserror = { workspace = true } +tracing = { workspace = true } +dashmap = { workspace = true } +tokio = { workspace = true } +ferrumc-text = { workspace = true } +ferrumc-state = { workspace = true } +enum-ordinalize = { workspace = true } +ferrumc-macros = { workspace = true } + +ferrumc-net-codec = { workspace = true } +ferrumc-net = { workspace = true } +ferrumc-config = { workspace = true } +flate2 = { workspace = true } + +[dev-dependencies] # Needed for the ServerState mock... :concern: +ferrumc-ecs = { workspace = true } +ferrumc-world = { workspace = true } +ctor = { workspace = true } diff --git a/src/lib/commands/src/arg/mod.rs b/src/lib/commands/src/arg/mod.rs new file mode 100644 index 00000000..9962e6f1 --- /dev/null +++ b/src/lib/commands/src/arg/mod.rs @@ -0,0 +1,19 @@ +use parser::ArgumentParser; + +pub mod parser; + +pub struct CommandArgument { + pub name: String, + pub required: bool, + pub parser: Box, +} + +impl CommandArgument { + pub fn new(name: String, required: bool, parser: Box) -> Self { + CommandArgument { + name, + required, + parser, + } + } +} diff --git a/src/lib/commands/src/arg/parser/int.rs b/src/lib/commands/src/arg/parser/int.rs new file mode 100644 index 00000000..7a5f2ac6 --- /dev/null +++ b/src/lib/commands/src/arg/parser/int.rs @@ -0,0 +1,68 @@ +use std::sync::{Arc, Mutex}; + +use crate::{ctx::CommandContext, input::CommandInput, ParserResult}; + +use super::{ + utils::error, + vanilla::{ + int::IntParserFlags, MinecraftArgument, MinecraftArgumentProperties, MinecraftArgumentType, + }, + ArgumentParser, +}; + +pub struct IntParser; + +impl ArgumentParser for IntParser { + fn parse(&self, _ctx: Arc, input: Arc>) -> ParserResult { + let token = input.lock().unwrap().read_string(); + + match token.parse::() { + Ok(int) => Ok(Box::new(int)), + Err(err) => Err(error(err)), + } + } + + fn new() -> Self + where + Self: Sized, + { + IntParser + } + + fn vanilla(&self) -> MinecraftArgument { + MinecraftArgument { + argument_type: MinecraftArgumentType::Int, + props: MinecraftArgumentProperties::Int(IntParserFlags::default()), + } + } + + fn completions( + &self, + _ctx: Arc, + input: Arc>, + ) -> Vec { + let input = input.lock().unwrap(); + + let mut numbers = Vec::new(); + let token = input.peek_string(); + + let input_num = if token == "-" { + "-0".to_string() + } else if token.is_empty() { + "0".to_string() + } else { + token + }; + + if input_num.parse::().is_err() { + return numbers; + } + + for n in 0..=9 { + let n = n.to_string(); + numbers.push(input_num.clone() + &n); + } + + numbers + } +} diff --git a/src/lib/commands/src/arg/parser/mod.rs b/src/lib/commands/src/arg/parser/mod.rs new file mode 100644 index 00000000..7b58852e --- /dev/null +++ b/src/lib/commands/src/arg/parser/mod.rs @@ -0,0 +1,19 @@ +use std::sync::{Arc, Mutex}; + +use crate::{ctx::CommandContext, input::CommandInput, ParserResult}; + +pub mod int; +pub mod string; +pub mod utils; +pub mod vanilla; + +pub trait ArgumentParser: Send + Sync { + fn parse(&self, ctx: Arc, input: Arc>) -> ParserResult; + fn completions(&self, ctx: Arc, input: Arc>) + -> Vec; + + fn new() -> Self + where + Self: Sized; + fn vanilla(&self) -> vanilla::MinecraftArgument; +} diff --git a/src/lib/commands/src/arg/parser/string.rs b/src/lib/commands/src/arg/parser/string.rs new file mode 100644 index 00000000..7650d2e2 --- /dev/null +++ b/src/lib/commands/src/arg/parser/string.rs @@ -0,0 +1,176 @@ +use std::sync::{Arc, Mutex}; + +use crate::{ctx::CommandContext, input::CommandInput, ParserResult}; + +use super::{ + utils::parser_error, + vanilla::{ + string::StringParsingBehavior, MinecraftArgument, MinecraftArgumentProperties, + MinecraftArgumentType, + }, + ArgumentParser, +}; + +pub struct SingleStringParser; + +impl ArgumentParser for SingleStringParser { + fn parse(&self, _ctx: Arc, input: Arc>) -> ParserResult { + let mut input = input.lock().unwrap(); + if input.peek_string().is_empty() { + return Err(parser_error("input cannot be empty")); + } + + Ok(Box::new(input.read_string())) + } + + fn new() -> Self + where + Self: Sized, + { + SingleStringParser + } + + fn vanilla(&self) -> MinecraftArgument { + MinecraftArgument { + argument_type: MinecraftArgumentType::String, + props: MinecraftArgumentProperties::String(StringParsingBehavior::default()), + } + } + + fn completions( + &self, + _ctx: Arc, + _input: Arc>, + ) -> Vec { + vec![] + } +} + +pub struct GreedyStringParser; + +impl ArgumentParser for GreedyStringParser { + fn parse(&self, _ctx: Arc, input: Arc>) -> ParserResult { + let mut input = input.lock().unwrap(); + let mut result = String::new(); + + if input.peek_string().is_empty() { + return Err(parser_error("input cannot be empty")); + } + + loop { + let token = input.read_string_skip_whitespace(false); + + if token.is_empty() { + break; + } + + if !result.is_empty() { + result.push(' '); + } + result.push_str(&token); + } + + Ok(Box::new(result)) + } + + fn new() -> Self + where + Self: Sized, + { + GreedyStringParser + } + + fn vanilla(&self) -> MinecraftArgument { + MinecraftArgument { + argument_type: MinecraftArgumentType::String, + props: MinecraftArgumentProperties::String(StringParsingBehavior::Greedy), + } + } + + fn completions( + &self, + _ctx: Arc, + _input: Arc>, + ) -> Vec { + vec![] + } +} + +pub struct QuotedStringParser; + +impl ArgumentParser for QuotedStringParser { + fn parse(&self, _ctx: Arc, input: Arc>) -> ParserResult { + let mut input = input.lock().unwrap(); + input.skip_whitespace(u32::MAX, false); + + // If it starts with a quote, use quoted string parsing + if input.peek() == Some('"') { + input.read(1); // consume the opening quote + + let mut result = String::new(); + let mut escaped = false; + + while input.has_remaining_input() { + let current = input.peek(); + + match current { + None => return Err(parser_error("unterminated quoted string")), + Some(c) => { + input.read(1); + + if escaped { + match c { + '"' | '\\' => result.push(c), + 'n' => result.push('\n'), + 'r' => result.push('\r'), + 't' => result.push('\t'), + _ => { + result.push('\\'); + result.push(c); + } + } + escaped = false; + } else { + match c { + '"' => return Ok(Box::new(result)), + '\\' => escaped = true, + _ => result.push(c), + } + } + } + } + } + + Err(parser_error("unterminated quoted string")) + } else { + // If no quotes, parse as single word + if input.peek_string().is_empty() { + return Err(parser_error("input cannot be empty")); + } + + Ok(Box::new(input.read_string())) + } + } + + fn new() -> Self + where + Self: Sized, + { + QuotedStringParser + } + + fn vanilla(&self) -> MinecraftArgument { + MinecraftArgument { + argument_type: MinecraftArgumentType::String, + props: MinecraftArgumentProperties::String(StringParsingBehavior::Quotable), + } + } + + fn completions( + &self, + _ctx: Arc, + _input: Arc>, + ) -> Vec { + vec![] + } +} diff --git a/src/lib/commands/src/arg/parser/utils.rs b/src/lib/commands/src/arg/parser/utils.rs new file mode 100644 index 00000000..deb59faf --- /dev/null +++ b/src/lib/commands/src/arg/parser/utils.rs @@ -0,0 +1,15 @@ +use std::error::Error; + +use ferrumc_text::{NamedColor, TextComponent, TextComponentBuilder}; + +use crate::errors::CommandError; + +pub fn parser_error(message: &'static str) -> TextComponent { + error(CommandError::ParserError(message.to_string())) +} + +pub fn error(err: impl Error) -> TextComponent { + TextComponentBuilder::new(err.to_string()) + .color(NamedColor::Red) + .build() +} diff --git a/src/lib/commands/src/arg/parser/vanilla/float.rs b/src/lib/commands/src/arg/parser/vanilla/float.rs new file mode 100644 index 00000000..0c23f336 --- /dev/null +++ b/src/lib/commands/src/arg/parser/vanilla/float.rs @@ -0,0 +1,42 @@ +use std::io::Write; + +use ferrumc_net_codec::encode::{NetEncode, NetEncodeOpts, NetEncodeResult}; +use tokio::io::AsyncWrite; + +#[derive(Clone, Debug, PartialEq, Default)] +pub struct FloatParserFlags { + pub min: Option, + pub max: Option, +} + +impl NetEncode for FloatParserFlags { + fn encode(&self, writer: &mut W, opts: &NetEncodeOpts) -> NetEncodeResult<()> { + let mut flags = 0u8; + if self.min.is_some() { + flags |= 0x01; + } + if self.max.is_some() { + flags |= 0x02; + } + flags.encode(writer, opts)?; + self.min.encode(writer, opts)?; + self.max.encode(writer, opts) + } + + async fn encode_async( + &self, + writer: &mut W, + opts: &NetEncodeOpts, + ) -> NetEncodeResult<()> { + let mut flags = 0u8; + if self.min.is_some() { + flags |= 0x01; + } + if self.max.is_some() { + flags |= 0x02; + } + flags.encode_async(writer, opts).await?; + self.min.encode_async(writer, opts).await?; + self.max.encode_async(writer, opts).await + } +} diff --git a/src/lib/commands/src/arg/parser/vanilla/int.rs b/src/lib/commands/src/arg/parser/vanilla/int.rs new file mode 100644 index 00000000..11dd7020 --- /dev/null +++ b/src/lib/commands/src/arg/parser/vanilla/int.rs @@ -0,0 +1,42 @@ +use std::io::Write; + +use ferrumc_net_codec::encode::{NetEncode, NetEncodeOpts, NetEncodeResult}; +use tokio::io::AsyncWrite; + +#[derive(Clone, Debug, PartialEq, Default)] +pub struct IntParserFlags { + pub min: Option, + pub max: Option, +} + +impl NetEncode for IntParserFlags { + fn encode(&self, writer: &mut W, opts: &NetEncodeOpts) -> NetEncodeResult<()> { + let mut flags = 0u8; + if self.min.is_some() { + flags |= 0x01; + } + if self.max.is_some() { + flags |= 0x02; + } + flags.encode(writer, opts)?; + self.min.encode(writer, opts)?; + self.max.encode(writer, opts) + } + + async fn encode_async( + &self, + writer: &mut W, + opts: &NetEncodeOpts, + ) -> NetEncodeResult<()> { + let mut flags = 0u8; + if self.min.is_some() { + flags |= 0x01; + } + if self.max.is_some() { + flags |= 0x02; + } + flags.encode_async(writer, opts).await?; + self.min.encode_async(writer, opts).await?; + self.max.encode_async(writer, opts).await + } +} diff --git a/src/lib/commands/src/arg/parser/vanilla/long.rs b/src/lib/commands/src/arg/parser/vanilla/long.rs new file mode 100644 index 00000000..504f99d6 --- /dev/null +++ b/src/lib/commands/src/arg/parser/vanilla/long.rs @@ -0,0 +1,42 @@ +use std::io::Write; + +use ferrumc_net_codec::encode::{NetEncode, NetEncodeOpts, NetEncodeResult}; +use tokio::io::AsyncWrite; + +#[derive(Clone, Debug, PartialEq, Default)] +pub struct LongParserFlags { + pub min: Option, + pub max: Option, +} + +impl NetEncode for LongParserFlags { + fn encode(&self, writer: &mut W, opts: &NetEncodeOpts) -> NetEncodeResult<()> { + let mut flags = 0u8; + if self.min.is_some() { + flags |= 0x01; + } + if self.max.is_some() { + flags |= 0x02; + } + flags.encode(writer, opts)?; + self.min.encode(writer, opts)?; + self.max.encode(writer, opts) + } + + async fn encode_async( + &self, + writer: &mut W, + opts: &NetEncodeOpts, + ) -> NetEncodeResult<()> { + let mut flags = 0u8; + if self.min.is_some() { + flags |= 0x01; + } + if self.max.is_some() { + flags |= 0x02; + } + flags.encode_async(writer, opts).await?; + self.min.encode_async(writer, opts).await?; + self.max.encode_async(writer, opts).await + } +} diff --git a/src/lib/commands/src/arg/parser/vanilla/mod.rs b/src/lib/commands/src/arg/parser/vanilla/mod.rs new file mode 100644 index 00000000..71667cbf --- /dev/null +++ b/src/lib/commands/src/arg/parser/vanilla/mod.rs @@ -0,0 +1,112 @@ +//! TODO: +//! * Entity +//! * Double (does rust even have a double type?) +//! * Score Holder +//! * Time +//! * Resource or Tag +//! * Resource or Tag Key +//! * Resource +//! * Resource Key + +use std::io::Write; + +use enum_ordinalize::Ordinalize; +use ferrumc_macros::NetEncode; +use ferrumc_net_codec::{ + encode::{NetEncode, NetEncodeOpts, NetEncodeResult}, + net_types::var_int::VarInt, +}; +use float::FloatParserFlags; +use int::IntParserFlags; +use long::LongParserFlags; +use string::StringParsingBehavior; +use tokio::io::AsyncWrite; + +pub mod float; +pub mod int; +pub mod long; +pub mod string; + +#[derive(Clone, Debug, PartialEq)] +pub struct MinecraftArgument { + pub argument_type: MinecraftArgumentType, + pub props: MinecraftArgumentProperties, +} + +#[derive(Clone, Debug, PartialEq, NetEncode)] +pub enum MinecraftArgumentProperties { + Float(FloatParserFlags), + Int(IntParserFlags), + Long(LongParserFlags), + String(StringParsingBehavior), +} + +#[derive(Clone, Debug, PartialEq, Ordinalize)] +pub enum MinecraftArgumentType { + Bool, + Float, + Double, + Int, + Long, + String, + Entity, + GameProfile, + BlockPos, + ColumnPos, + Vec3, + Vec2, + BlockState, + BlockPredicate, + ItemStack, + ItemPredicate, + Color, + Component, + Style, + Message, + Nbt, + NbtTag, + NbtPath, + Objective, + ObjectiveCriteria, + Operator, + Particle, + Angle, + Rotation, + ScoreboardDisplaySlot, + ScoreHolder, + UpTo3Axes, + Team, + ItemSlot, + ResourceLocation, + Function, + EntityAnchor, + IntRange, + FloatRange, + Dimension, + GameMode, + Time, + ResourceOrTag, + ResourceOrTagKey, + Resource, + ResourceKey, + TemplateMirror, + TemplateRotation, + Heightmap, + UUID, +} + +impl NetEncode for MinecraftArgumentType { + fn encode(&self, writer: &mut W, opts: &NetEncodeOpts) -> NetEncodeResult<()> { + VarInt::new(self.ordinal() as i32).encode(writer, opts) + } + + async fn encode_async( + &self, + writer: &mut W, + opts: &NetEncodeOpts, + ) -> NetEncodeResult<()> { + VarInt::new(self.ordinal() as i32) + .encode_async(writer, opts) + .await + } +} diff --git a/src/lib/commands/src/arg/parser/vanilla/string.rs b/src/lib/commands/src/arg/parser/vanilla/string.rs new file mode 100644 index 00000000..e279dae3 --- /dev/null +++ b/src/lib/commands/src/arg/parser/vanilla/string.rs @@ -0,0 +1,32 @@ +use std::io::Write; + +use enum_ordinalize::Ordinalize; +use ferrumc_net_codec::{ + encode::{NetEncode, NetEncodeOpts, NetEncodeResult}, + net_types::var_int::VarInt, +}; +use tokio::io::AsyncWrite; + +#[derive(Clone, Debug, PartialEq, Ordinalize, Default)] +pub enum StringParsingBehavior { + #[default] + SingleWord, + Quotable, + Greedy, +} + +impl NetEncode for StringParsingBehavior { + fn encode(&self, writer: &mut W, opts: &NetEncodeOpts) -> NetEncodeResult<()> { + VarInt::new(self.ordinal() as i32).encode(writer, opts) + } + + async fn encode_async( + &self, + writer: &mut W, + opts: &NetEncodeOpts, + ) -> NetEncodeResult<()> { + VarInt::new(self.ordinal() as i32) + .encode_async(writer, opts) + .await + } +} diff --git a/src/lib/commands/src/ctx.rs b/src/lib/commands/src/ctx.rs new file mode 100644 index 00000000..fa71a3db --- /dev/null +++ b/src/lib/commands/src/ctx.rs @@ -0,0 +1,50 @@ +use std::{ + any::Any, + sync::{Arc, Mutex}, +}; + +use ferrumc_state::GlobalState; + +use crate::{input::CommandInput, Command}; + +pub struct CommandContext { + pub input: Arc>, + pub command: Arc, + pub state: GlobalState, + pub connection_id: usize, +} + +impl CommandContext { + pub fn new( + input: CommandInput, + command: Arc, + state: GlobalState, + connection_id: usize, + ) -> Arc { + Arc::new(Self { + input: Arc::new(Mutex::new(input)), + command, + state, + connection_id, + }) + } + + pub fn arg(self: &Arc, name: &str) -> T { + if let Some(arg) = self.command.args.iter().find(|a| a.name == name) { + let input = self.input.clone(); + let result = arg.parser.parse(self.clone(), input); + + match result { + Ok(b) => match b.downcast::() { + Ok(value) => *value, + Err(_) => { + todo!("failed downcasting command argument, change design of this fn"); + } + }, + Err(err) => unreachable!("arg should have already been validated: {err}"), + } + } else { + todo!(); + } + } +} diff --git a/src/lib/commands/src/errors.rs b/src/lib/commands/src/errors.rs new file mode 100644 index 00000000..d2b6b10c --- /dev/null +++ b/src/lib/commands/src/errors.rs @@ -0,0 +1,9 @@ +use thiserror::Error; + +#[derive(Debug, Clone, Error)] +pub enum CommandError { + #[error("Something failed lol")] + SomeError, + #[error("Parser error: {0}")] + ParserError(String), +} diff --git a/src/lib/commands/src/graph/mod.rs b/src/lib/commands/src/graph/mod.rs new file mode 100644 index 00000000..10fc024f --- /dev/null +++ b/src/lib/commands/src/graph/mod.rs @@ -0,0 +1,270 @@ +use std::sync::Arc; +use std::{collections::HashMap, io::Write}; + +use ferrumc_macros::{packet, NetEncode}; +use ferrumc_net_codec::net_types::length_prefixed_vec::LengthPrefixedVec; +use ferrumc_net_codec::net_types::var_int::VarInt; +use node::{CommandNode, CommandNodeFlag, CommandNodeType}; + +use crate::infrastructure::get_graph; +use crate::Command; + +pub mod node; + +#[derive(Clone, Debug)] +pub struct CommandGraph { + pub root_node: CommandNode, + pub nodes: Vec, + pub node_to_indices: HashMap, +} + +impl Default for CommandGraph { + fn default() -> Self { + let root_node = CommandNode { + flags: CommandNodeFlag::NodeType(CommandNodeType::Root).bitmask(), + children: LengthPrefixedVec::new(Vec::new()), + redirect_node: None, + name: None, + parser_id: None, + properties: None, + suggestions_type: None, + }; + + Self { + root_node: root_node.clone(), + nodes: vec![root_node], + node_to_indices: HashMap::new(), + } + } +} + +impl CommandGraph { + pub fn push(&mut self, command: Arc) { + let mut current_node_index = 0; + + for (i, part) in command.name.split_whitespace().enumerate() { + let is_last = i == command.name.split_whitespace().count() - 1; + + if let Some(&child_index) = self.node_to_indices.get(part) { + current_node_index = child_index; + } else { + let mut node = CommandNode { + flags: CommandNodeFlag::NodeType(CommandNodeType::Literal).bitmask(), + children: LengthPrefixedVec::new(Vec::new()), + redirect_node: None, + name: Some(part.to_string()), + parser_id: None, + properties: None, + suggestions_type: None, + }; + + if is_last + && (command.args.is_empty() + || command.args.first().is_some_and(|arg| !arg.required)) + { + node.flags |= CommandNodeFlag::Executable.bitmask(); + } + + let node_index = self.nodes.len() as u32; + self.nodes.push(node); + self.node_to_indices.insert(part.to_string(), node_index); + + if i == 0 { + self.nodes[0].children.push(VarInt::new(node_index as i32)); + } else { + let parent_node = self.nodes.get_mut(current_node_index as usize).unwrap(); + parent_node.children.push(VarInt::new(node_index as i32)); + } + + current_node_index = node_index; + } + } + + let mut prev_node_index = current_node_index; + + for (i, arg) in command.args.iter().enumerate() { + let vanilla = arg.parser.vanilla(); + let is_last = i == command.args.len() - 1; + + let mut arg_node = CommandNode { + flags: CommandNodeFlag::NodeType(CommandNodeType::Argument).bitmask(), + children: LengthPrefixedVec::new(Vec::new()), + redirect_node: None, + name: Some(arg.name.clone()), + parser_id: Some(vanilla.argument_type), + properties: Some(vanilla.props), + suggestions_type: None, + }; + + if is_last { + arg_node.flags |= CommandNodeFlag::Executable.bitmask(); + } + + let arg_node_index = self.nodes.len() as u32; + self.nodes.push(arg_node); + + self.nodes[prev_node_index as usize] + .children + .push(VarInt::new(arg_node_index as i32)); + + prev_node_index = arg_node_index; + } + } + + pub fn traverse(&self, mut f: F) + where + F: FnMut(&CommandNode, u32, usize, Option), + { + self.traverse_node(0, 0, None, &mut f); + } + + fn traverse_node(&self, node_index: u32, depth: usize, parent: Option, f: &mut F) + where + F: FnMut(&CommandNode, u32, usize, Option), + { + let current_node = &self.nodes[node_index as usize]; + + f(current_node, node_index, depth, parent); + + for child_index in current_node.children.data.iter() { + self.traverse_node(child_index.val as u32, depth + 1, Some(node_index), f); + } + } + + pub fn find_command<'a>(&'a self, input: &'a str) -> Vec<(u32, &'a str)> { + let mut matches = Vec::new(); + let input = input.trim(); + + self.find_command_recursive(0, input, &mut matches); + matches + } + + fn find_command_recursive<'a>( + &'a self, + node_index: u32, + remaining_input: &'a str, + matches: &mut Vec<(u32, &'a str)>, + ) { + let current_node = &self.nodes[node_index as usize]; + let input_words: Vec<&str> = remaining_input.split_whitespace().collect(); + + // once the input is empty and the currently selected node is executable, we've found it. + if remaining_input.is_empty() && current_node.is_executable() { + matches.push((node_index, remaining_input)); + return; + } + + // once the input is empty but the currently selected node is not executable, we check the children. + if remaining_input.is_empty() { + return; + } + + match current_node.node_type() { + CommandNodeType::Root => { + // the root node is the root of all evil. + for child_index in current_node.children.data.iter() { + self.find_command_recursive(child_index.val as u32, remaining_input, matches); + } + } + CommandNodeType::Literal => { + // for literal nodes, everything must match exactly. + if let Some(name) = ¤t_node.name { + if !input_words.is_empty() && input_words[0] == name { + // we found a match, we continue with the remaining input. + let remaining = if input_words.len() > 1 { + remaining_input[name.len()..].trim_start() + } else { + "" + }; + + // once we found a node that is executable and the remaining input is empty, we've found something. + if remaining.is_empty() && current_node.is_executable() { + matches.push((node_index, remaining)); + } + + // we continue checking the other children. + for child_index in current_node.children.data.iter() { + self.find_command_recursive(child_index.val as u32, remaining, matches); + } + } + } + } + CommandNodeType::Argument => { + // for argument nodes, we consume one argument and then continue. + if !input_words.is_empty() { + let remaining = if input_words.len() > 1 { + remaining_input[input_words[0].len()..].trim_start() + } else { + "" + }; + + // if this node is executable, we add it. + matches.push((node_index, remaining)); + + // continue checking anyway. + for child_index in current_node.children.data.iter() { + self.find_command_recursive(child_index.val as u32, remaining, matches); + } + } + } + } + } + + fn collect_command_parts(&self, node_index: u32, parts: &mut Vec) { + let node = &self.nodes[node_index as usize]; + + if let Some(name) = &node.name { + if node.node_type() == CommandNodeType::Literal { + parts.push(name.clone()); + } + } + + // find the parent + for (parent_idx, parent_node) in self.nodes.iter().enumerate() { + if parent_node + .children + .data + .iter() + .any(|child| child.val as u32 == node_index) + { + self.collect_command_parts(parent_idx as u32, parts); + break; + } + } + } + + pub fn get_command_name(&self, node_index: u32) -> String { + let mut parts = Vec::new(); + self.collect_command_parts(node_index, &mut parts); + parts.reverse(); // reverse since we want the command name in proper order + parts.join(" ") + } + + pub fn find_command_by_input(&self, input: &str) -> Option { + let matches = self.find_command(input); + + matches + .first() + .map(|(node_index, _remaining)| self.get_command_name(*node_index)) + } +} + +#[derive(NetEncode, Debug)] +#[packet(packet_id = "commands", state = "play")] +pub struct CommandsPacket { + pub graph: LengthPrefixedVec, + pub root_idx: VarInt, +} + +impl CommandsPacket { + pub fn new(graph: CommandGraph) -> Self { + Self { + graph: LengthPrefixedVec::new(graph.nodes), + root_idx: VarInt::new(0), + } + } + + pub fn create() -> Self { + Self::new(get_graph()) + } +} diff --git a/src/lib/commands/src/graph/node.rs b/src/lib/commands/src/graph/node.rs new file mode 100644 index 00000000..e3204efe --- /dev/null +++ b/src/lib/commands/src/graph/node.rs @@ -0,0 +1,108 @@ +use std::{fmt, io::Write}; + +use ferrumc_macros::NetEncode; +use ferrumc_net_codec::net_types::{length_prefixed_vec::LengthPrefixedVec, var_int::VarInt}; + +use crate::arg::parser::vanilla::{MinecraftArgumentProperties, MinecraftArgumentType}; + +#[derive(Clone, Debug, PartialEq)] +pub enum CommandNodeType { + Root, + Literal, + Argument, +} + +impl CommandNodeType { + pub const fn id(&self) -> u8 { + match self { + Self::Root => 0, + Self::Literal => 1, + Self::Argument => 2, + } + } +} + +#[derive(Clone, Debug, PartialEq)] +pub enum CommandNodeFlag { + NodeType(CommandNodeType), + Executable, + HasRedirect, + HasSuggestionsType, +} + +impl CommandNodeFlag { + pub const fn bitmask(&self) -> u8 { + match self { + CommandNodeFlag::NodeType(CommandNodeType::Root) => 0x00, + CommandNodeFlag::NodeType(CommandNodeType::Literal) => 0x01, + CommandNodeFlag::NodeType(CommandNodeType::Argument) => 0x02, + CommandNodeFlag::Executable => 0x04, + CommandNodeFlag::HasRedirect => 0x08, + CommandNodeFlag::HasSuggestionsType => 0x10, + } + } +} + +#[derive(Clone, NetEncode)] +pub struct CommandNode { + pub flags: u8, + pub children: LengthPrefixedVec, + pub redirect_node: Option, + pub name: Option, + pub parser_id: Option, + pub properties: Option, + pub suggestions_type: Option, +} + +// We want to display the actual flags and not the encoded value +impl fmt::Debug for CommandNode { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let node_type = match self.flags & 0x03 { + 0 => CommandNodeType::Root, + 1 => CommandNodeType::Literal, + 2 => CommandNodeType::Argument, + _ => panic!("Invalid node type"), + }; + + let executable = self.flags & 0x04 != 0; + let has_redirect = self.flags & 0x08 != 0; + let has_suggestions_type = self.flags & 0x10 != 0; + + f.debug_struct("CommandNode") + .field("node_type", &node_type) + .field("executable", &executable) + .field("has_redirect", &has_redirect) + .field("has_suggestions_type", &has_suggestions_type) + .field("flags", &self.flags) + .field("children", &self.children) + .field("redirect_node", &self.redirect_node) + .field("name", &self.name) + .field("parser_id", &self.parser_id) + .field("properties", &self.properties) + .field("suggestions_type", &self.suggestions_type) + .finish() + } +} + +impl CommandNode { + pub fn node_type(&self) -> CommandNodeType { + match self.flags & 0x03 { + 0 => CommandNodeType::Root, + 1 => CommandNodeType::Literal, + 2 => CommandNodeType::Argument, + _ => panic!("Invalid node type"), + } + } + + pub fn is_executable(&self) -> bool { + self.flags & 0x04 != 0 + } + + pub fn has_redirect(&self) -> bool { + self.flags & 0x08 != 0 + } + + pub fn has_suggestions_type(&self) -> bool { + self.flags & 0x10 != 0 + } +} diff --git a/src/lib/commands/src/infrastructure.rs b/src/lib/commands/src/infrastructure.rs new file mode 100644 index 00000000..398216f7 --- /dev/null +++ b/src/lib/commands/src/infrastructure.rs @@ -0,0 +1,37 @@ +use dashmap::DashMap; +use std::sync::{Arc, LazyLock, RwLock}; + +use crate::{graph::CommandGraph, Command}; + +static COMMANDS: LazyLock>> = LazyLock::new(DashMap::new); +static COMMAND_GRAPH: LazyLock> = + LazyLock::new(|| RwLock::new(CommandGraph::default())); + +pub fn register_command(command: Arc) { + COMMANDS.insert(command.name, command.clone()); + if let Ok(mut graph) = COMMAND_GRAPH.write() { + graph.push(command); + } +} + +pub fn get_graph() -> CommandGraph { + if let Ok(graph) = COMMAND_GRAPH.read() { + graph.clone() + } else { + CommandGraph::default() + } +} + +pub fn get_command_by_name(name: &str) -> Option> { + COMMANDS.get(name).map(|cmd_ref| Arc::clone(&cmd_ref)) +} + +pub fn find_command(input: &str) -> Option> { + let graph = get_graph(); + let name = graph.find_command_by_input(input); + if let Some(name) = name { + get_command_by_name(&name) + } else { + None + } +} diff --git a/src/lib/commands/src/input.rs b/src/lib/commands/src/input.rs new file mode 100644 index 00000000..dfd64942 --- /dev/null +++ b/src/lib/commands/src/input.rs @@ -0,0 +1,109 @@ +/// Very based on Cloud, this is gonna have to be changed up a bit probably. +#[derive(Clone)] +pub struct CommandInput { + pub input: String, + pub cursor: u32, +} + +impl CommandInput { + pub fn of(string: String) -> Self { + Self { + input: string, + cursor: 0, + } + } + pub fn append_string(&mut self, string: String) { + self.input += &*string; + } + pub fn move_cursor(&mut self, chars: u32) { + if self.cursor + chars > self.input.len() as u32 { + return; + } + + self.cursor += chars; + } + pub fn remaining_length(&self) -> u32 { + self.input.len() as u32 - self.cursor + } + pub fn peek(&self) -> Option { + self.input.chars().nth(self.cursor as usize) + } + pub fn has_remaining_input(&self) -> bool { + self.cursor < self.input.len() as u32 + } + pub fn skip_whitespace(&mut self, max_spaces: u32, preserve_single: bool) { + if preserve_single && self.remaining_length() == 1 && self.peek() == Some(' ') { + return; + } + + let mut i = 0; + while i < max_spaces + && self.has_remaining_input() + && self.peek().is_some_and(|c| c.is_whitespace()) + { + self.read(1); + i += 1; + } + } + pub fn remaining_input(&self) -> String { + self.input[self.cursor as usize..].to_string() + } + pub fn peek_string_chars(&self, chars: u32) -> String { + let remaining = self.remaining_input(); + if chars > remaining.len() as u32 { + return "".to_string(); + } + + remaining[0..chars as usize].to_string() + } + pub fn read(&mut self, chars: u32) -> String { + let read_string = self.peek_string_chars(chars); + self.move_cursor(chars); + read_string + } + pub fn remaining_tokens(&self) -> u32 { + let count = self.remaining_input().split(' ').count() as u32; + if self.remaining_input().ends_with(' ') { + return count + 1; + } + count + } + pub fn read_string(&mut self) -> String { + self.skip_whitespace(u32::MAX, false); + let mut result = String::new(); + while let Some(c) = self.peek() { + if c.is_whitespace() { + break; + } + result.push(c); + self.move_cursor(1); + } + result + } + pub fn peek_string(&self) -> String { + let remaining = self.remaining_input(); + remaining + .split_whitespace() + .next() + .unwrap_or("") + .to_string() + } + pub fn read_until(&mut self, separator: char) -> String { + self.skip_whitespace(u32::MAX, false); + let mut result = String::new(); + while let Some(c) = self.peek() { + if c == separator { + self.move_cursor(1); + break; + } + result.push(c); + self.move_cursor(1); + } + result + } + pub fn read_string_skip_whitespace(&mut self, preserve_single: bool) -> String { + let read_string = self.read_string(); + self.skip_whitespace(u32::MAX, preserve_single); + read_string + } +} diff --git a/src/lib/commands/src/lib.rs b/src/lib/commands/src/lib.rs new file mode 100644 index 00000000..200b97d5 --- /dev/null +++ b/src/lib/commands/src/lib.rs @@ -0,0 +1,59 @@ +use std::{ + any::Any, + future::Future, + pin::Pin, + sync::{Arc, Mutex}, +}; + +use arg::CommandArgument; +use ctx::CommandContext; +use ferrumc_text::TextComponent; +use input::CommandInput; + +pub mod arg; +pub mod ctx; +pub mod errors; +pub mod graph; +pub mod infrastructure; +pub mod input; + +#[cfg(test)] +mod tests; + +pub type ParserResult = Result, TextComponent>; +pub type CommandResult = Result<(), TextComponent>; +pub type CommandOutput = Pin + Send + 'static>>; +pub type CommandExecutor = + Arc Fn(Arc) -> CommandOutput + Send + Sync + 'static>; + +pub struct Command { + pub name: &'static str, + pub args: Vec, + pub executor: CommandExecutor, +} + +impl Command { + pub fn execute(&self, ctx: Arc) -> CommandOutput { + (self.executor)(ctx) + } + + pub fn validate( + &self, + ctx: &Arc, + input: &Arc>, + ) -> Result<(), TextComponent> { + for arg in &self.args { + arg.parser.parse(ctx.clone(), input.clone())?; + } + + Ok(()) + } +} + +pub fn executor(func: F) -> Arc) -> CommandOutput + Send + Sync> +where + F: Fn(Arc) -> Fut + Send + Sync + 'static, + Fut: Future + Send + 'static, +{ + Arc::new(move |ctx: Arc| Box::pin(func(ctx)) as CommandOutput) +} diff --git a/src/lib/commands/src/tests.rs b/src/lib/commands/src/tests.rs new file mode 100644 index 00000000..709bc060 --- /dev/null +++ b/src/lib/commands/src/tests.rs @@ -0,0 +1,145 @@ +use std::sync::Arc; + +use ferrumc_ecs::Universe; +use ferrumc_macros::{arg, command}; +use ferrumc_state::{GlobalState, ServerState}; +use ferrumc_world::World; +use tokio::net::TcpListener; + +use crate::{ + arg::{ + parser::{ + int::IntParser, + string::{GreedyStringParser, QuotedStringParser}, + }, + CommandArgument, + }, + ctx::CommandContext, + executor, + graph::{node::CommandNodeType, CommandGraph}, + infrastructure::{find_command, register_command}, + input::CommandInput, + Command, CommandResult, +}; + +async fn state() -> GlobalState { + Arc::new(ServerState { + universe: Universe::new(), + tcp_listener: TcpListener::bind("0.0.0.0:0").await.unwrap(), + world: World::new().await, + }) +} + +#[tokio::test] +async fn arg_parse_test() { + async fn test_executor(ctx: Arc) -> CommandResult { + let quoted = ctx.arg::("quoted"); + let greedy = ctx.arg::("greedy"); + + assert_eq!( + format!("{quoted:?} {greedy}"), + ctx.input.lock().unwrap().input + ); + + Ok(()) + } + + let command = crate::Command { + name: "input_test", + args: vec![ + CommandArgument { + name: "quoted".to_string(), + required: true, + parser: Box::new(QuotedStringParser), + }, + CommandArgument { + name: "greedy".to_string(), + required: true, + parser: Box::new(GreedyStringParser), + }, + ], + executor: executor(test_executor), + }; + let command = Arc::new(command); + + let state = state().await; + + let input = "\"hello\" no no no please no I'm so sorry"; + + let ctx = CommandContext::new( + CommandInput::of(input.to_string()), + command.clone(), + state, + 0, + ); + + command.execute(ctx).await.unwrap(); +} + +#[tokio::test] +async fn parse_test() { + async fn test_executor(ctx: Arc) -> CommandResult { + let num = ctx.arg::("number"); + assert_eq!(num.to_string(), ctx.input.lock().unwrap().input); + Ok(()) + } + + let command = crate::Command { + name: "input_test", + args: vec![CommandArgument { + name: "number".to_string(), + required: true, + parser: Box::new(IntParser), + }], + executor: executor(test_executor), + }; + let command = Arc::new(command); + + let state = state().await; + + let ctx = CommandContext::new( + CommandInput::of("42".to_string()), + command.clone(), + state, + 0, + ); + + register_command(command.clone()); + + let found_command = find_command("input_test 42").unwrap(); + + found_command.execute(ctx).await.unwrap(); +} + +#[arg("quoted", QuotedStringParser)] +#[command("test")] +async fn execute_test_command(_ctx: Arc) -> CommandResult { + Ok(()) +} + +#[tokio::test] +async fn macro_test() { + let found_command = find_command("test").unwrap(); + assert_eq!(found_command.args.len(), 1); +} + +#[tokio::test] +async fn graph_test() { + let command = find_command("test").unwrap(); + let mut graph = CommandGraph::default(); + graph.push(command); + + for node in &graph.nodes { + println!("{node:#?}"); + } + + assert_eq!(&graph.nodes.len(), &3); + + let literal_node = graph.nodes.get(1).unwrap(); + let arg_node = graph.nodes.get(2).unwrap(); + + assert_eq!(literal_node.node_type(), CommandNodeType::Literal); + assert_eq!(arg_node.node_type(), CommandNodeType::Argument); + assert!(arg_node.is_executable()); + assert!(!literal_node.is_executable()); +} diff --git a/src/lib/default_commands/Cargo.toml b/src/lib/default_commands/Cargo.toml new file mode 100644 index 00000000..c1349694 --- /dev/null +++ b/src/lib/default_commands/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "ferrumc-default-commands" +version = "0.1.0" +edition = "2021" + +[dependencies] +ferrumc-commands = { workspace = true } +ferrumc-ecs = { workspace = true } +ferrumc-macros = { workspace = true } +ferrumc-text = { workspace = true } +ferrumc-core = { workspace = true } +ferrumc-entity-utils = { workspace = true } +ctor = { workspace = true } +tracing = { workspace = true } diff --git a/src/lib/default_commands/src/echo.rs b/src/lib/default_commands/src/echo.rs new file mode 100644 index 00000000..3f850845 --- /dev/null +++ b/src/lib/default_commands/src/echo.rs @@ -0,0 +1,39 @@ +use std::sync::Arc; + +use ferrumc_commands::{ + arg::{ + parser::{string::GreedyStringParser, ArgumentParser}, + CommandArgument, + }, + ctx::CommandContext, + executor, + infrastructure::register_command, + Command, CommandResult, +}; +use ferrumc_core::identity::player_identity::PlayerIdentity; +use ferrumc_entity_utils::send_message::SendMessageExt; +use ferrumc_macros::{arg, command}; +use ferrumc_text::{NamedColor, TextComponentBuilder}; + +#[arg("message", GreedyStringParser::new())] +#[command("echo")] +async fn echo(ctx: Arc) -> CommandResult { + let message = ctx.arg::("message"); + let identity = ctx + .state + .universe + .get::(ctx.connection_id) + .expect("failed to get identity"); + + ctx.connection_id + .send_message( + TextComponentBuilder::new(format!("{} said: {message}", identity.username)) + .color(NamedColor::Green) + .build(), + &ctx.state, + ) + .await + .expect("failed sending message"); + + Ok(()) +} diff --git a/src/lib/default_commands/src/lib.rs b/src/lib/default_commands/src/lib.rs new file mode 100644 index 00000000..68dd4b81 --- /dev/null +++ b/src/lib/default_commands/src/lib.rs @@ -0,0 +1,4 @@ +pub mod echo; +pub mod nested; + +pub fn init() {} diff --git a/src/lib/default_commands/src/nested.rs b/src/lib/default_commands/src/nested.rs new file mode 100644 index 00000000..45676a4e --- /dev/null +++ b/src/lib/default_commands/src/nested.rs @@ -0,0 +1,53 @@ +use std::sync::Arc; + +use ferrumc_commands::{ + arg::{ + parser::{ + int::IntParser, + string::{QuotedStringParser, SingleStringParser}, + }, + CommandArgument, + }, + ctx::CommandContext, + executor, + infrastructure::register_command, + Command, CommandResult, +}; +use ferrumc_entity_utils::send_message::SendMessageExt; +use ferrumc_macros::{arg, command}; +use ferrumc_text::TextComponentBuilder; + +#[command("nested")] +async fn root(ctx: Arc) -> CommandResult { + ctx.connection_id + .send_message( + TextComponentBuilder::new("Executed /nested").build(), + &ctx.state, + ) + .await + .expect("failed sending message"); + Ok(()) +} + +#[arg("message", QuotedStringParser)] +#[arg("word", SingleStringParser)] +#[arg("number", IntParser)] +#[command("nested abc")] +async fn abc(ctx: Arc) -> CommandResult { + let message = ctx.arg::("message"); + let word = ctx.arg::("word"); + let number = ctx.arg::("number"); + + ctx.connection_id + .send_message( + TextComponentBuilder::new(format!( + "Message: {message:?}, Word: {word:?}, Number: {number}" + )) + .build(), + &ctx.state, + ) + .await + .expect("failed sending message"); + + Ok(()) +} diff --git a/src/lib/derive_macros/src/commands/mod.rs b/src/lib/derive_macros/src/commands/mod.rs new file mode 100644 index 00000000..c98fc544 --- /dev/null +++ b/src/lib/derive_macros/src/commands/mod.rs @@ -0,0 +1,115 @@ +use std::{ + collections::HashMap, + sync::{Mutex, OnceLock}, +}; + +use proc_macro::TokenStream; +use quote::{format_ident, quote, ToTokens}; +use syn::{ + parse::{Parse, ParseStream}, + parse_macro_input, Expr, ItemFn, LitBool, LitStr, Result as SynResult, Token, +}; + +static PENDING_ARGS: OnceLock>>> = OnceLock::new(); + +struct CommandAttr { + name: String, +} + +impl Parse for CommandAttr { + fn parse(input: ParseStream) -> SynResult { + let name = input.parse::()?.value(); + Ok(CommandAttr { name }) + } +} + +struct ArgAttr { + name: String, + parser: String, + required: bool, +} + +impl Parse for ArgAttr { + fn parse(input: ParseStream) -> SynResult { + let name = input.parse::()?.value(); + input.parse::()?; + let parser = input.parse::()?.to_token_stream().to_string(); + + let required = if input.peek(Token![,]) { + input.parse::()?; + input.parse::()?.value() + } else { + true + }; + + Ok(ArgAttr { + name, + parser, + required, + }) + } +} + +fn get_args_storage() -> &'static Mutex>> { + PENDING_ARGS.get_or_init(|| Mutex::new(HashMap::new())) +} + +pub fn arg(attr: TokenStream, item: TokenStream) -> TokenStream { + let arg_attr = parse_macro_input!(attr as ArgAttr); + let input_fn = parse_macro_input!(item as ItemFn); + let fn_name = input_fn.sig.ident.to_string(); + + let storage = get_args_storage(); + let mut pending_args = storage.lock().unwrap(); + pending_args.entry(fn_name).or_default().push(arg_attr); + + TokenStream::from(quote!(#input_fn)) +} + +pub fn command(attr: TokenStream, item: TokenStream) -> TokenStream { + let input_fn = parse_macro_input!(item as ItemFn); + let fn_name = &input_fn.sig.ident; + let fn_name_str = fn_name.to_string(); + + let command_attr = parse_macro_input!(attr as CommandAttr); + let command_name = command_attr.name; + + let storage = get_args_storage(); + let mut pending_args = storage.lock().unwrap(); + let args = pending_args.remove(&fn_name_str).unwrap_or_default(); + + let arg_names = args.iter().map(|arg| &arg.name).collect::>(); + let arg_parsers = args + .iter() + .map(|arg| syn::parse_str(&arg.parser).expect("invalid argument parser")) + .collect::>(); + let arg_required = args.iter().map(|arg| arg.required).collect::>(); + + let register_fn_name = format_ident!("__register_{}_command", command_name.replace(" ", "_")); + + let expanded = quote! { + #[ctor::ctor] + fn #register_fn_name() { + let command = Command { + name: #command_name, + args: vec![ + #( + CommandArgument { + name: #arg_names.to_string(), + required: #arg_required, + parser: Box::new(#arg_parsers), + }, + )* + ], + executor: executor(#fn_name) + }; + + let command = std::sync::Arc::new(command); + register_command(command); + } + + #input_fn + }; + + TokenStream::from(expanded) +} diff --git a/src/lib/derive_macros/src/lib.rs b/src/lib/derive_macros/src/lib.rs index 6f6ca1a8..f92a7eab 100644 --- a/src/lib/derive_macros/src/lib.rs +++ b/src/lib/derive_macros/src/lib.rs @@ -2,6 +2,7 @@ use proc_macro::TokenStream; +mod commands; mod events; mod helpers; mod nbt; @@ -69,6 +70,16 @@ pub fn get_packet_entry(input: TokenStream) -> TokenStream { } // #=================== PACKETS ===================# +#[proc_macro_attribute] +pub fn command(attr: TokenStream, input: TokenStream) -> TokenStream { + commands::command(attr, input) +} + +#[proc_macro_attribute] +pub fn arg(attr: TokenStream, input: TokenStream) -> TokenStream { + commands::arg(attr, input) +} + /// Get a registry entry from the registries.json file. /// returns protocol_id (as u64) of the specified entry. #[proc_macro] diff --git a/src/lib/derive_macros/src/profiling/mod.rs b/src/lib/derive_macros/src/profiling/mod.rs index 3feb2c72..9efb9f8b 100644 --- a/src/lib/derive_macros/src/profiling/mod.rs +++ b/src/lib/derive_macros/src/profiling/mod.rs @@ -1,11 +1,13 @@ -use proc_macro::{quote, TokenStream}; -use quote::ToTokens; +use proc_macro::TokenStream; +use quote::{quote, ToTokens}; #[allow(unused_variables)] pub fn profile_fn(attr: TokenStream, item: TokenStream) -> TokenStream { let name = format!("profiler/{}", attr.to_string().replace("\"", "")).to_token_stream(); - quote! { + let res = quote! { #[tracing::instrument(name = $name)] $item - } + }; + + res.into() } diff --git a/src/lib/entity_utils/Cargo.toml b/src/lib/entity_utils/Cargo.toml new file mode 100644 index 00000000..b5a0dff2 --- /dev/null +++ b/src/lib/entity_utils/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "ferrumc-entity-utils" +version = "0.1.0" +edition = "2021" + +[dependencies] +ferrumc-net = { workspace = true } +ferrumc-net-codec = { workspace = true } +ferrumc-ecs = { workspace = true } +ferrumc-text = { workspace = true } +ferrumc-state = { workspace = true } +async-trait = { workspace = true } diff --git a/src/lib/entity_utils/src/lib.rs b/src/lib/entity_utils/src/lib.rs new file mode 100644 index 00000000..0ef7ce44 --- /dev/null +++ b/src/lib/entity_utils/src/lib.rs @@ -0,0 +1 @@ +pub mod send_message; diff --git a/src/lib/entity_utils/src/send_message.rs b/src/lib/entity_utils/src/send_message.rs new file mode 100644 index 00000000..3b809e4b --- /dev/null +++ b/src/lib/entity_utils/src/send_message.rs @@ -0,0 +1,36 @@ +use async_trait::async_trait; +use ferrumc_net::{ + connection::StreamWriter, packets::outgoing::system_message::SystemMessagePacket, NetResult, +}; +use ferrumc_net_codec::encode::NetEncodeOpts; +use ferrumc_state::GlobalState; +use ferrumc_text::TextComponent; + +#[async_trait] +pub trait SendMessageExt { + async fn send_message(&self, message: TextComponent, state: &GlobalState) -> NetResult<()>; + async fn send_actionbar(&self, message: TextComponent, state: &GlobalState) -> NetResult<()>; +} + +#[async_trait] +impl SendMessageExt for usize { + async fn send_message(&self, message: TextComponent, state: &GlobalState) -> NetResult<()> { + let mut writer = state.universe.get_mut::(*self)?; + writer + .send_packet( + &SystemMessagePacket::new(message, false), + &NetEncodeOpts::WithLength, + ) + .await + } + + async fn send_actionbar(&self, message: TextComponent, state: &GlobalState) -> NetResult<()> { + let mut writer = state.universe.get_mut::(*self)?; + writer + .send_packet( + &SystemMessagePacket::new(message, true), + &NetEncodeOpts::WithLength, + ) + .await + } +} diff --git a/src/lib/net/crates/codec/src/decode/primitives.rs b/src/lib/net/crates/codec/src/decode/primitives.rs index 651a262f..4075e058 100644 --- a/src/lib/net/crates/codec/src/decode/primitives.rs +++ b/src/lib/net/crates/codec/src/decode/primitives.rs @@ -84,6 +84,24 @@ where } } +/// This implementation assumes that the optional was written using PacketByteBuf#writeNullable and has a leading bool. +impl NetDecode for Option +where + T: NetDecode, +{ + fn decode(reader: &mut R, opts: &NetDecodeOpts) -> NetDecodeResult { + let is_some = ::decode(reader, opts)?; + + if !is_some { + return Ok(None); + } + + let value = ::decode(reader, opts)?; + + Ok(Some(value)) + } +} + /// This isn't actually a type in the Minecraft Protocol. This is just for saving data/ or for general use. /// It was created for saving/reading chunks! impl NetDecode for HashMap diff --git a/src/lib/net/crates/codec/src/net_types/length_prefixed_vec.rs b/src/lib/net/crates/codec/src/net_types/length_prefixed_vec.rs index 68a53b4e..f229b0b8 100644 --- a/src/lib/net/crates/codec/src/net_types/length_prefixed_vec.rs +++ b/src/lib/net/crates/codec/src/net_types/length_prefixed_vec.rs @@ -4,7 +4,7 @@ use crate::net_types::var_int::VarInt; use std::io::{Read, Write}; use tokio::io::AsyncWrite; -#[derive(Debug)] +#[derive(Debug, Clone)] pub struct LengthPrefixedVec { pub length: VarInt, pub data: Vec, @@ -26,6 +26,11 @@ impl LengthPrefixedVec { data, } } + + pub fn push(&mut self, data: T) { + self.data.push(data); + self.length = VarInt::new(self.length.val + 1); + } } impl NetEncode for LengthPrefixedVec diff --git a/src/lib/net/src/packets/incoming/chat_message.rs b/src/lib/net/src/packets/incoming/chat_message.rs new file mode 100644 index 00000000..1b6cd7f4 --- /dev/null +++ b/src/lib/net/src/packets/incoming/chat_message.rs @@ -0,0 +1,41 @@ +use std::sync::Arc; + +use ferrumc_events::infrastructure::Event; +use ferrumc_macros::{packet, Event, NetDecode}; +use ferrumc_net_codec::net_types::var_int::VarInt; +use ferrumc_state::ServerState; + +use crate::packets::IncomingPacket; + +#[derive(NetDecode, Debug, Clone)] +#[packet(packet_id = "chat", state = "play")] +pub struct ChatMessagePacket { + pub message: String, + pub timestamp: u64, + pub salt: u64, + pub has_signature: bool, + pub signature: Option>, + pub message_count: VarInt, + pub acknowledged: Vec, +} + +impl IncomingPacket for ChatMessagePacket { + async fn handle(self, conn_id: usize, state: Arc) -> crate::NetResult<()> { + ChatMessageEvent::trigger(ChatMessageEvent::new(conn_id, self.message), state).await + } +} + +#[derive(Debug, Event, Clone)] +pub struct ChatMessageEvent { + pub player_conn_id: usize, + pub message: String, +} + +impl ChatMessageEvent { + pub fn new(player_conn_id: usize, message: String) -> Self { + Self { + player_conn_id, + message, + } + } +} diff --git a/src/lib/net/src/packets/incoming/command.rs b/src/lib/net/src/packets/incoming/command.rs new file mode 100644 index 00000000..70129342 --- /dev/null +++ b/src/lib/net/src/packets/incoming/command.rs @@ -0,0 +1,31 @@ +use std::sync::Arc; + +use ferrumc_events::infrastructure::Event; +use ferrumc_macros::{packet, Event, NetDecode}; +use ferrumc_state::ServerState; + +use crate::{packets::IncomingPacket, NetResult}; + +#[derive(NetDecode, Debug, Clone)] +#[packet(packet_id = "chat_command", state = "play")] +pub struct ChatCommandPacket { + command: String, +} + +#[derive(Event)] +pub struct CommandDispatchEvent { + pub command: String, + pub conn_id: usize, +} + +impl CommandDispatchEvent { + pub fn new(command: String, conn_id: usize) -> Self { + Self { command, conn_id } + } +} + +impl IncomingPacket for ChatCommandPacket { + async fn handle(self, conn_id: usize, state: Arc) -> NetResult<()> { + CommandDispatchEvent::trigger(CommandDispatchEvent::new(self.command, conn_id), state).await + } +} diff --git a/src/lib/net/src/packets/incoming/mod.rs b/src/lib/net/src/packets/incoming/mod.rs index 22de710b..b37fb6b3 100644 --- a/src/lib/net/src/packets/incoming/mod.rs +++ b/src/lib/net/src/packets/incoming/mod.rs @@ -15,5 +15,7 @@ pub mod set_player_position; pub mod set_player_position_and_rotation; pub mod set_player_rotation; +pub mod chat_message; +pub mod command; pub mod player_command; pub mod swing_arm; diff --git a/src/lib/net/src/packets/outgoing/mod.rs b/src/lib/net/src/packets/outgoing/mod.rs index ce630555..30102ed1 100644 --- a/src/lib/net/src/packets/outgoing/mod.rs +++ b/src/lib/net/src/packets/outgoing/mod.rs @@ -16,6 +16,7 @@ pub mod set_default_spawn_position; pub mod set_render_distance; pub mod status_response; pub mod synchronize_player_position; +pub mod system_message; pub mod update_time; pub mod remove_entities; diff --git a/src/lib/net/src/packets/outgoing/system_message.rs b/src/lib/net/src/packets/outgoing/system_message.rs new file mode 100644 index 00000000..47f4e644 --- /dev/null +++ b/src/lib/net/src/packets/outgoing/system_message.rs @@ -0,0 +1,16 @@ +use ferrumc_macros::{packet, NetEncode}; +use ferrumc_text::TextComponent; +use std::io::Write; + +#[derive(NetEncode, Debug, Clone)] +#[packet(packet_id = "system_chat", state = "play")] +pub struct SystemMessagePacket { + message: TextComponent, + overlay: bool, +} + +impl SystemMessagePacket { + pub fn new(message: TextComponent, overlay: bool) -> Self { + Self { message, overlay } + } +} diff --git a/src/lib/utils/profiling/src/lib.rs b/src/lib/utils/profiling/src/lib.rs index a00637a1..c9bc622f 100644 --- a/src/lib/utils/profiling/src/lib.rs +++ b/src/lib/utils/profiling/src/lib.rs @@ -148,7 +148,7 @@ where S: Subscriber + for<'lookup> LookupSpan<'lookup>, { fn on_new_span(&self, _: &Attributes<'_>, id: &Id, ctx: Context<'_, S>) { - if RUNNING_PROFILERS.read().len() >= 1 { + if !RUNNING_PROFILERS.read().is_empty() { match ctx.span(id) { None => { error!("No span found") @@ -162,7 +162,7 @@ where } } fn on_close(&self, id: Id, ctx: Context<'_, S>) { - if RUNNING_PROFILERS.read().len() >= 1 { + if !RUNNING_PROFILERS.read().is_empty() { let instant = match ctx.span(&id) { None => { error!("No span found");