diff --git a/Cargo.toml b/Cargo.toml index 0e2be479..ec4f7418 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", @@ -87,6 +89,9 @@ debug = true 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-events = { path = "src/lib/events"} ferrumc-general-purpose = { path = "src/lib/utils/general_purpose" } ferrumc-logging = { path = "src/lib/utils/logging" } ferrumc-macros = { path = "src/lib/derive_macros" } @@ -104,7 +109,6 @@ ferrumc-world = { path = "src/lib/world" } ferrumc-world-gen = { path = "src/lib/world_gen" } ferrumc-threadpool = { path = "src/lib/utils/threadpool" } - # Asynchronous tokio = { version = "1.45.1", features = ["full"] } @@ -164,6 +168,7 @@ enum_delegate = "0.2.0" # Magic dhat = "0.3.3" +ctor = "0.4.2" # Compression/Decompression libflate = "2.1.0" @@ -186,6 +191,7 @@ colored = "3.0.0" # Misc deepsize = "0.2.0" page_size = "0.6.0" +enum-ordinalize = "4.3.0" regex = "1.11.1" noise = "0.9.0" ctrlc = "3.4.7" diff --git a/src/bin/Cargo.toml b/src/bin/Cargo.toml index 5ecc5713..c8619ebb 100644 --- a/src/bin/Cargo.toml +++ b/src/bin/Cargo.toml @@ -24,10 +24,12 @@ ferrumc-world = { workspace = true } ferrumc-macros = { workspace = true } ferrumc-general-purpose = { workspace = true } ferrumc-state = { workspace = true } +ferrumc-commands = { workspace = true } +ferrumc-default-commands = { workspace = true } +ferrumc-text = { workspace = true } ferrumc-world-gen = { workspace = true } ferrumc-threadpool = { workspace = true } - tracing = { workspace = true } clap = { workspace = true, features = ["derive", "env"] } flate2 = { workspace = true } 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/mod.rs b/src/bin/src/packet_handlers/mod.rs index 4d117871..3c0634ab 100644 --- a/src/bin/src/packet_handlers/mod.rs +++ b/src/bin/src/packet_handlers/mod.rs @@ -1,3 +1,4 @@ +// mod commands; pub(crate) mod play_packets; mod player; diff --git a/src/bin/src/packet_handlers/play_packets/command.rs b/src/bin/src/packet_handlers/play_packets/command.rs new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/src/bin/src/packet_handlers/play_packets/command.rs @@ -0,0 +1 @@ + diff --git a/src/bin/src/packet_handlers/play_packets/mod.rs b/src/bin/src/packet_handlers/play_packets/mod.rs index ea545ef4..b41523dd 100644 --- a/src/bin/src/packet_handlers/play_packets/mod.rs +++ b/src/bin/src/packet_handlers/play_packets/mod.rs @@ -3,6 +3,7 @@ use bevy_ecs::prelude::IntoScheduleConfigs; use bevy_ecs::schedule::Schedule; mod chunk_batch_ack; +mod command; mod confirm_player_teleport; mod keep_alive; mod place_block; diff --git a/src/lib/adapters/anvil/src/lib.rs b/src/lib/adapters/anvil/src/lib.rs index 27fca7ef..91520cae 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..b9ca20d3 --- /dev/null +++ b/src/lib/commands/Cargo.toml @@ -0,0 +1,23 @@ +[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 } +bevy_ecs = { 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-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..76d2f7a6 --- /dev/null +++ b/src/lib/commands/src/ctx.rs @@ -0,0 +1,51 @@ +use std::{ + any::Any, + sync::{Arc, Mutex}, +}; + +use bevy_ecs::entity::Entity; +use ferrumc_state::GlobalState; + +use crate::{input::CommandInput, Command}; + +pub struct CommandContext { + pub input: Arc>, + pub command: Arc, + pub state: GlobalState, + pub sender: Entity, +} + +impl CommandContext { + pub fn new( + input: CommandInput, + command: Arc, + state: GlobalState, + sender: Entity, + ) -> Arc { + Arc::new(Self { + input: Arc::new(Mutex::new(input)), + command, + state, + sender, + }) + } + + 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/events.rs b/src/lib/commands/src/events.rs new file mode 100644 index 00000000..f8a16d3f --- /dev/null +++ b/src/lib/commands/src/events.rs @@ -0,0 +1,18 @@ +use std::sync::Arc; + +use bevy_ecs::{entity::Entity, prelude::Event}; + +use crate::{infrastructure, Command}; + +#[derive(Event)] +pub struct CommandDispatchEvent { + pub command: String, + pub sender: Entity, +} + +impl CommandDispatchEvent { + /// Attempts to find the command that was dispatched. + pub fn lookup(&self) -> Option> { + infrastructure::find_command(&self.command) + } +} diff --git a/src/lib/commands/src/graph/mod.rs b/src/lib/commands/src/graph/mod.rs new file mode 100644 index 00000000..e530c16f --- /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.0 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.0 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.0 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.0 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.0 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..aa8c5f9d --- /dev/null +++ b/src/lib/commands/src/lib.rs @@ -0,0 +1,60 @@ +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 events; +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..120a7649 --- /dev/null +++ b/src/lib/default_commands/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "ferrumc-default-commands" +version = "0.1.0" +edition = "2021" + +[dependencies] +ferrumc-commands = { workspace = true } +ferrumc-macros = { workspace = true } +ferrumc-text = { workspace = true } +ferrumc-core = { workspace = true } +ctor = { workspace = true } +tracing = { workspace = true } +bevy_ecs = { 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..100abac8 --- /dev/null +++ b/src/lib/default_commands/src/echo.rs @@ -0,0 +1,33 @@ +use bevy_ecs::entity::Entity; +use ferrumc_macros::command; + +#[command("test")] +fn test_command( + #[parser(GreedyStringParser)] thing: String, + #[parser(GreedyStringParser)] things: String, + #[sender] sender: Entity, +) { +} + +// #[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..23adc004 --- /dev/null +++ b/src/lib/default_commands/src/nested.rs @@ -0,0 +1,35 @@ + +// #[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..e5614b22 --- /dev/null +++ b/src/lib/derive_macros/src/commands/mod.rs @@ -0,0 +1,183 @@ +use proc_macro::TokenStream; +use quote::{format_ident, quote, ToTokens}; +use syn::{ + parse::{Parse, ParseStream}, + parse_macro_input, Expr, FnArg, ItemFn, LitStr, Pat, Result as SynResult, Type, +}; + +#[derive(Clone, Debug)] +struct Arg { + parser: String, + name: String, + required: bool, + ty: String, +} + +struct CommandAttr { + name: String, +} + +impl Parse for CommandAttr { + fn parse(input: ParseStream) -> SynResult { + let name = input.parse::()?.value(); + Ok(CommandAttr { name }) + } +} + +struct ArgAttr { + parser: String, +} + +impl Parse for ArgAttr { + fn parse(input: ParseStream) -> SynResult { + let parser = input.parse::()?.to_token_stream().to_string(); + + Ok(ArgAttr { parser }) + } +} + +pub fn command(attr: TokenStream, item: TokenStream) -> TokenStream { + let mut input_fn = parse_macro_input!(item as ItemFn); + let fn_name = input_fn.clone().sig.ident; + + let command_attr = parse_macro_input!(attr as CommandAttr); + + let mut args = Vec::new(); + let mut bevy_args = Vec::<(Box, Type)>::new(); + let mut has_sender_arg = false; + + for fn_arg in &mut input_fn.sig.inputs { + let FnArg::Typed(fn_arg) = fn_arg else { + return TokenStream::from(quote! { + compiler_error!("command handler cannot have receiver"); + }); + }; + + if fn_arg.attrs.is_empty() { + bevy_args.push((fn_arg.pat.clone(), *fn_arg.ty.clone())); + } + + let mut sender_arg_mismatched_ty = false; + + fn_arg.attrs.retain(|arg| { + let is_parser = arg.path().is_ident("parser"); + let is_sender = arg.path().is_ident("sender"); + + if is_parser { + let required = match *fn_arg.ty { + Type::Path(ref path) => path + .path + .segments + .iter() + .any(|seg| seg.ident.to_string() == "Option"), + _ => false, + }; + + args.push(Arg { + parser: arg.parse_args_with(ArgAttr::parse).unwrap().parser, + name: fn_arg.pat.to_token_stream().to_string(), + required, + ty: fn_arg.ty.to_token_stream().to_string(), + }); + } + + if is_sender { + match *fn_arg.ty { + Type::Path(ref path) => { + if !path + .path + .segments + .iter() + .any(|seg| seg.ident.to_string() == "Entity") + { + sender_arg_mismatched_ty = true; + return false; + } + } + + _ => { + sender_arg_mismatched_ty = true; + return false; + } + } + has_sender_arg = true; + } + + !is_parser && !is_sender + }); + + if sender_arg_mismatched_ty { + return TokenStream::from(quote! { + compile_error!("invalid type for sender arg - should be Entity"); + }); + } + } + + let command_struct_name = format_ident!("__command_{}", fn_name); + let arg_fields = args + .clone() + .iter() + .map(|arg| { + let ty = format_ident!("{}", arg.ty); + let name = format_ident!("{}", arg.name); + + quote! { #name: #ty, } + }) + .collect::>(); + + let system_name = format_ident!("__{}_handler", fn_name.clone()); + let system_args = bevy_args + .clone() + .iter() + .map(|arg| { + let (pat, ty) = arg; + quote! { #pat: #ty, } + }) + .collect::>(); + let system_arg_pats = bevy_args + .clone() + .iter() + .map(|arg| { + let (pat, _) = arg; + quote!(#pat) + }) + .collect::>(); + + let arg_extractors = args + .clone() + .iter() + .map(|arg| { + let name = format_ident!("{}", &arg.name); + + quote! { event.#name, } + }) + .collect::>(); + + let sender_param = if has_sender_arg { + quote! { event.__sender, } + } else { + quote! {} + }; + + TokenStream::from(quote! { + #[allow(non_camel_case_types)] + #[derive(bevy_ecs::prelude::Event, Clone)] + struct #command_struct_name { + #(#arg_fields)* + __sender: bevy_ecs::prelude::Entity, + __input: ::ferrumc_commands::input::CommandInput, + } + + #[allow(non_snake_case)] + #[allow(dead_code)] + #input_fn + + #[allow(non_snake_case)] + fn #system_name(mut events: bevy_ecs::prelude::EventReader<#command_struct_name>, #(#system_args)*) { + for mut event in events.read() { + let event = event.clone(); + #fn_name(#(#arg_extractors)* #sender_param #(#system_arg_pats)*) + } + } + }) +} diff --git a/src/lib/derive_macros/src/lib.rs b/src/lib/derive_macros/src/lib.rs index 8162184a..b1c0a0c8 100644 --- a/src/lib/derive_macros/src/lib.rs +++ b/src/lib/derive_macros/src/lib.rs @@ -1,4 +1,6 @@ use proc_macro::TokenStream; + +mod commands; mod helpers; mod nbt; mod net; @@ -55,6 +57,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/net/crates/codec/src/decode/primitives.rs b/src/lib/net/crates/codec/src/decode/primitives.rs index bb9ca220..f8aeb990 100644 --- a/src/lib/net/crates/codec/src/decode/primitives.rs +++ b/src/lib/net/crates/codec/src/decode/primitives.rs @@ -144,6 +144,38 @@ 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)) + } + + async fn decode_async( + reader: &mut R, + opts: &NetDecodeOpts, + ) -> NetDecodeResult { + let is_some = ::decode_async(reader, opts).await?; + if !is_some { + return Ok(None); + } + + let value = ::decode_async(reader, opts).await?; + + 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 6781d1f7..5ceb48bc 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::{AsyncRead, 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.0 + 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..fb98315b --- /dev/null +++ b/src/lib/net/src/packets/incoming/chat_message.rs @@ -0,0 +1,30 @@ +use bevy_ecs::prelude::Event; +use ferrumc_macros::{packet, NetDecode}; +use ferrumc_net_codec::net_types::var_int::VarInt; + +#[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, +} + +#[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..cdaa1a86 --- /dev/null +++ b/src/lib/net/src/packets/incoming/command.rs @@ -0,0 +1,21 @@ +use bevy_ecs::prelude::Event; +use ferrumc_macros::{packet, NetDecode}; +use ferrumc_state::ServerState; + +#[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 } + } +} diff --git a/src/lib/net/src/packets/incoming/mod.rs b/src/lib/net/src/packets/incoming/mod.rs index 1d2f7e16..b631e4cb 100644 --- a/src/lib/net/src/packets/incoming/mod.rs +++ b/src/lib/net/src/packets/incoming/mod.rs @@ -16,6 +16,10 @@ pub mod player_command; 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 swing_arm; pub mod chunk_batch_ack; diff --git a/src/lib/net/src/packets/outgoing/mod.rs b/src/lib/net/src/packets/outgoing/mod.rs index fc00db24..fb1bdcb8 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 remove_entities; pub mod spawn_entity; 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 } + } +}