From b233a520beeb72941343d60e22c1048c7537479c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nils=20Kr=C3=BCger?= Date: Mon, 23 Dec 2024 23:54:16 +0100 Subject: [PATCH 01/32] feat: commands --- Cargo.toml | 4 +- src/lib/commands/Cargo.toml | 16 ++++ src/lib/commands/src/arg/mod.rs | 19 ++++ src/lib/commands/src/arg/parser/int.rs | 25 +++++ src/lib/commands/src/arg/parser/mod.rs | 19 ++++ src/lib/commands/src/arg/parser/string.rs | 50 ++++++++++ src/lib/commands/src/arg/parser/utils.rs | 7 ++ src/lib/commands/src/ctx.rs | 43 +++++++++ src/lib/commands/src/errors.rs | 7 ++ src/lib/commands/src/infrastructure.rs | 34 +++++++ src/lib/commands/src/input.rs | 109 ++++++++++++++++++++++ src/lib/commands/src/lib.rs | 50 ++++++++++ src/lib/commands/src/tests.rs | 83 ++++++++++++++++ 13 files changed, 464 insertions(+), 2 deletions(-) create mode 100644 src/lib/commands/Cargo.toml create mode 100644 src/lib/commands/src/arg/mod.rs create mode 100644 src/lib/commands/src/arg/parser/int.rs create mode 100644 src/lib/commands/src/arg/parser/mod.rs create mode 100644 src/lib/commands/src/arg/parser/string.rs create mode 100644 src/lib/commands/src/arg/parser/utils.rs create mode 100644 src/lib/commands/src/ctx.rs create mode 100644 src/lib/commands/src/errors.rs create mode 100644 src/lib/commands/src/infrastructure.rs create mode 100644 src/lib/commands/src/input.rs create mode 100644 src/lib/commands/src/lib.rs create mode 100644 src/lib/commands/src/tests.rs diff --git a/Cargo.toml b/Cargo.toml index 66091ef7..753b9fa4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,6 +16,7 @@ members = [ "src/lib/adapters/anvil", "src/lib/adapters/nbt", "src/lib/adapters/nbt", + "src/lib/commands", "src/lib/core", "src/lib/core/state", "src/lib/derive_macros", @@ -86,6 +87,7 @@ codegen-units = 1 ferrumc-anvil = { path = "src/lib/adapters/anvil" } ferrumc-config = { path = "src/lib/utils/config" } ferrumc-core = { path = "src/lib/core" } +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" } @@ -181,5 +183,3 @@ memmap2 = "0.9.5" # Benchmarking criterion = { version = "0.5.1", features = ["html_reports"] } - - diff --git a/src/lib/commands/Cargo.toml b/src/lib/commands/Cargo.toml new file mode 100644 index 00000000..2ce9e463 --- /dev/null +++ b/src/lib/commands/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "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 } + +[dev-dependencies] # Needed for the ServerState mock... :concern: +ferrumc-ecs = { workspace = true } +ferrumc-world = { 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..b49dcab5 --- /dev/null +++ b/src/lib/commands/src/arg/parser/int.rs @@ -0,0 +1,25 @@ +use std::sync::{Arc, Mutex}; + +use crate::{ctx::CommandContext, input::CommandInput, ParserResult}; + +use super::{utils::error, ArgumentParser}; + +pub struct IntParser; + +impl ArgumentParser for IntParser { + fn parse(&self, _ctx: Arc<&CommandContext>, 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 + } +} \ No newline at end of file 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..c9cb1f92 --- /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(crate) mod utils; + +pub trait ArgumentParser: Send + Sync { + fn parse( + &self, + context: Arc<&CommandContext>, + input: Arc>, + ) -> ParserResult; + fn new() -> Self + where + Self: Sized; +} 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..8d542081 --- /dev/null +++ b/src/lib/commands/src/arg/parser/string.rs @@ -0,0 +1,50 @@ +use std::sync::{Arc, Mutex}; + +use crate::{ctx::CommandContext, input::CommandInput, ParserResult}; + +use super::ArgumentParser; + +pub struct SingleStringParser; + +impl ArgumentParser for SingleStringParser { + fn parse(&self, _ctx: Arc<&CommandContext>, input: Arc>) -> ParserResult { + Ok(Box::new(input.lock().unwrap().read_string())) + } + + fn new() -> Self + where + Self: Sized, + { + SingleStringParser + } +} + +pub struct GreedyStringParser; + +impl ArgumentParser for GreedyStringParser { + fn parse(&self, _ctx: Arc<&CommandContext>, input: Arc>) -> ParserResult { + let mut result = String::new(); + + loop { + let token = input.lock().unwrap().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 + } +} 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..aeef1e31 --- /dev/null +++ b/src/lib/commands/src/arg/parser/utils.rs @@ -0,0 +1,7 @@ +use std::error::Error; + +use ferrumc_text::{NamedColor, TextComponent, TextComponentBuilder}; + +pub(crate) fn error(err: impl Error) -> TextComponent { + TextComponentBuilder::new(err.to_string()).color(NamedColor::Red).build() +} \ No newline at end of file diff --git a/src/lib/commands/src/ctx.rs b/src/lib/commands/src/ctx.rs new file mode 100644 index 00000000..87c3c9ab --- /dev/null +++ b/src/lib/commands/src/ctx.rs @@ -0,0 +1,43 @@ +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, +} + +impl CommandContext { + pub fn new(input: CommandInput, command: Arc, state: GlobalState) -> Arc { + Arc::new(Self { + input: Arc::new(Mutex::new(input)), + command, + state, + }) + } + + pub fn arg(&self, 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(Arc::new(self), input); + + return match result { + Ok(b) => match b.downcast::() { + Ok(value) => *value, + Err(_) => { + todo!("failed downcasting command argument, change design of this fn"); + } + }, + Err(_) => unreachable!("arg has already been validated") + }; + } else { + todo!(); + } + } +} diff --git a/src/lib/commands/src/errors.rs b/src/lib/commands/src/errors.rs new file mode 100644 index 00000000..0a047a83 --- /dev/null +++ b/src/lib/commands/src/errors.rs @@ -0,0 +1,7 @@ +use thiserror::Error; + +#[derive(Debug, Clone, Error)] +pub enum CommandError { + #[error("Something failed lol")] + SomeError, +} diff --git a/src/lib/commands/src/infrastructure.rs b/src/lib/commands/src/infrastructure.rs new file mode 100644 index 00000000..8d23a245 --- /dev/null +++ b/src/lib/commands/src/infrastructure.rs @@ -0,0 +1,34 @@ +use dashmap::DashMap; +use std::sync::{Arc, LazyLock}; + +use crate::Command; + +static COMMANDS: LazyLock>> = LazyLock::new(DashMap::new); + +pub fn register_command(command: Arc) { + COMMANDS.insert(command.name, command); +} + +pub fn get_command_by_name(name: &'static str) -> Option> { + COMMANDS.get(name).map(|cmd_ref| Arc::clone(&cmd_ref)) +} + +pub fn find_command(input: &'static str) -> Option> { + let mut command = None; + let mut input = input; + + while !input.is_empty() { + command = get_command_by_name(input); + if command.is_some() { + break; + } + + if let Some(pos) = input.rfind(' ') { + input = &input[..pos]; + } else { + input = ""; + } + } + + command +} diff --git a/src/lib/commands/src/input.rs b/src/lib/commands/src/input.rs new file mode 100644 index 00000000..e1dd0277 --- /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().map_or(false, |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 + } +} \ No newline at end of file diff --git a/src/lib/commands/src/lib.rs b/src/lib/commands/src/lib.rs new file mode 100644 index 00000000..7abf30b9 --- /dev/null +++ b/src/lib/commands/src/lib.rs @@ -0,0 +1,50 @@ +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 errors; +pub mod input; +pub mod ctx; +pub mod arg; +pub mod infrastructure; + +#[cfg(test)] +pub(crate) mod tests; + +pub type ParserResult = Result, TextComponent>; +pub type CommandResult = Result; +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<&CommandContext>, 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..4a4455fe --- /dev/null +++ b/src/lib/commands/src/tests.rs @@ -0,0 +1,83 @@ +use std::sync::Arc; + +use ferrumc_ecs::Universe; +use ferrumc_state::{GlobalState, ServerState}; +use ferrumc_text::{TextComponentBuilder, TextContent}; +use ferrumc_world::World; +use tokio::net::TcpListener; + +use crate::{arg::{parser::int::IntParser, CommandArgument}, ctx::CommandContext, executor, infrastructure::{find_command, register_command}, input::CommandInput, 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 num = ctx.arg::("number"); + Ok(TextComponentBuilder::new(num.to_string()).build()) + } + + 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); + + let result = command.execute(ctx).await; + let TextContent::Text { text } = result.unwrap().content else { + panic!("result is not text") + }; + + assert_eq!(text, "42".to_string()); +} + +#[tokio::test] +async fn parse_test() { + async fn test_executor(ctx: Arc) -> CommandResult { + let num = ctx.arg::("number"); + Ok(TextComponentBuilder::new(num.to_string()).build()) + } + + 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); + + register_command(command.clone()); + + let found_command = find_command("input_test 42").unwrap(); + + let result = found_command.execute(ctx).await; + let TextContent::Text { text } = result.unwrap().content else { + panic!("result is not text") + }; + + assert_eq!(text, "42".to_string()); +} From 11a4d90905a75248fdc6f47f36e327ae2715d01c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nils=20Kr=C3=BCger?= Date: Tue, 24 Dec 2024 00:03:23 +0100 Subject: [PATCH 02/32] refactor(commands): rename module, style(commands): reformat --- src/lib/commands/Cargo.toml | 2 +- src/lib/commands/src/arg/parser/int.rs | 4 +- src/lib/commands/src/arg/parser/mod.rs | 7 +--- src/lib/commands/src/arg/parser/utils.rs | 6 ++- src/lib/commands/src/ctx.rs | 2 +- src/lib/commands/src/input.rs | 2 +- src/lib/commands/src/lib.rs | 30 +++++++++------ src/lib/commands/src/tests.rs | 49 +++++++++++++----------- 8 files changed, 57 insertions(+), 45 deletions(-) diff --git a/src/lib/commands/Cargo.toml b/src/lib/commands/Cargo.toml index 2ce9e463..88d08ffa 100644 --- a/src/lib/commands/Cargo.toml +++ b/src/lib/commands/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "commands" +name = "ferrumc-commands" version = "0.1.0" edition = "2021" diff --git a/src/lib/commands/src/arg/parser/int.rs b/src/lib/commands/src/arg/parser/int.rs index b49dcab5..2569e52b 100644 --- a/src/lib/commands/src/arg/parser/int.rs +++ b/src/lib/commands/src/arg/parser/int.rs @@ -12,7 +12,7 @@ impl ArgumentParser for IntParser { match token.parse::() { Ok(int) => Ok(Box::new(int)), - Err(err) => Err(error(err)) + Err(err) => Err(error(err)), } } @@ -22,4 +22,4 @@ impl ArgumentParser for IntParser { { IntParser } -} \ No newline at end of file +} diff --git a/src/lib/commands/src/arg/parser/mod.rs b/src/lib/commands/src/arg/parser/mod.rs index c9cb1f92..1b2bc038 100644 --- a/src/lib/commands/src/arg/parser/mod.rs +++ b/src/lib/commands/src/arg/parser/mod.rs @@ -8,11 +8,8 @@ pub mod string; pub(crate) mod utils; pub trait ArgumentParser: Send + Sync { - fn parse( - &self, - context: Arc<&CommandContext>, - input: Arc>, - ) -> ParserResult; + fn parse(&self, context: Arc<&CommandContext>, input: Arc>) + -> ParserResult; fn new() -> Self where Self: Sized; diff --git a/src/lib/commands/src/arg/parser/utils.rs b/src/lib/commands/src/arg/parser/utils.rs index aeef1e31..4c688462 100644 --- a/src/lib/commands/src/arg/parser/utils.rs +++ b/src/lib/commands/src/arg/parser/utils.rs @@ -3,5 +3,7 @@ use std::error::Error; use ferrumc_text::{NamedColor, TextComponent, TextComponentBuilder}; pub(crate) fn error(err: impl Error) -> TextComponent { - TextComponentBuilder::new(err.to_string()).color(NamedColor::Red).build() -} \ No newline at end of file + TextComponentBuilder::new(err.to_string()) + .color(NamedColor::Red) + .build() +} diff --git a/src/lib/commands/src/ctx.rs b/src/lib/commands/src/ctx.rs index 87c3c9ab..e1d205ae 100644 --- a/src/lib/commands/src/ctx.rs +++ b/src/lib/commands/src/ctx.rs @@ -34,7 +34,7 @@ impl CommandContext { todo!("failed downcasting command argument, change design of this fn"); } }, - Err(_) => unreachable!("arg has already been validated") + Err(_) => unreachable!("arg has already been validated"), }; } else { todo!(); diff --git a/src/lib/commands/src/input.rs b/src/lib/commands/src/input.rs index e1dd0277..fb885eca 100644 --- a/src/lib/commands/src/input.rs +++ b/src/lib/commands/src/input.rs @@ -106,4 +106,4 @@ impl CommandInput { self.skip_whitespace(u32::MAX, preserve_single); read_string } -} \ No newline at end of file +} diff --git a/src/lib/commands/src/lib.rs b/src/lib/commands/src/lib.rs index 7abf30b9..c31bb2b4 100644 --- a/src/lib/commands/src/lib.rs +++ b/src/lib/commands/src/lib.rs @@ -1,23 +1,29 @@ -use std::{any::Any, future::Future, pin::Pin, sync::{Arc, Mutex}}; +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 errors; -pub mod input; -pub mod ctx; pub mod arg; +pub mod ctx; +pub mod errors; pub mod infrastructure; +pub mod input; #[cfg(test)] -pub(crate) mod tests; +mod tests; pub type ParserResult = Result, TextComponent>; pub type CommandResult = Result; -pub type CommandOutput = Pin + Send + 'static>>; -pub type CommandExecutor = Arc Fn(Arc) -> CommandOutput + Send + Sync + 'static>; +pub type CommandOutput = Pin + Send + 'static>>; +pub type CommandExecutor = + Arc Fn(Arc) -> CommandOutput + Send + Sync + 'static>; pub struct Command { pub name: &'static str, @@ -30,7 +36,11 @@ impl Command { (self.executor)(ctx) } - pub fn validate(&self, ctx: Arc<&CommandContext>, input: Arc>) -> Result<(), TextComponent> { + pub fn validate( + &self, + ctx: Arc<&CommandContext>, + input: Arc>, + ) -> Result<(), TextComponent> { for arg in &self.args { arg.parser.parse(ctx.clone(), input.clone())?; } @@ -44,7 +54,5 @@ where F: Fn(Arc) -> Fut + Send + Sync + 'static, Fut: Future + Send + 'static, { - Arc::new(move |ctx: Arc| { - Box::pin(func(ctx)) as CommandOutput - }) + 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 index 4a4455fe..5a0c3ad4 100644 --- a/src/lib/commands/src/tests.rs +++ b/src/lib/commands/src/tests.rs @@ -6,16 +6,21 @@ use ferrumc_text::{TextComponentBuilder, TextContent}; use ferrumc_world::World; use tokio::net::TcpListener; -use crate::{arg::{parser::int::IntParser, CommandArgument}, ctx::CommandContext, executor, infrastructure::{find_command, register_command}, input::CommandInput, CommandResult}; +use crate::{ + arg::{parser::int::IntParser, CommandArgument}, + ctx::CommandContext, + executor, + infrastructure::{find_command, register_command}, + input::CommandInput, + 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 - } - ) + Arc::new(ServerState { + universe: Universe::new(), + tcp_listener: TcpListener::bind("0.0.0.0:0").await.unwrap(), + world: World::new().await, + }) } #[tokio::test] @@ -30,21 +35,21 @@ async fn arg_parse_test() { args: vec![CommandArgument { name: "number".to_string(), required: true, - parser: Box::new(IntParser) + parser: Box::new(IntParser), }], - executor: executor(test_executor) + 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); - + let result = command.execute(ctx).await; let TextContent::Text { text } = result.unwrap().content else { panic!("result is not text") }; - + assert_eq!(text, "42".to_string()); } @@ -60,24 +65,24 @@ async fn parse_test() { args: vec![CommandArgument { name: "number".to_string(), required: true, - parser: Box::new(IntParser) + parser: Box::new(IntParser), }], - executor: executor(test_executor) + 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); - + register_command(command.clone()); - + let found_command = find_command("input_test 42").unwrap(); - + let result = found_command.execute(ctx).await; let TextContent::Text { text } = result.unwrap().content else { panic!("result is not text") }; - + assert_eq!(text, "42".to_string()); } From fab00e9dc2bbf9db8ee054709c575db471f50512 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nils=20Kr=C3=BCger?= Date: Tue, 24 Dec 2024 00:30:48 +0100 Subject: [PATCH 03/32] feat(commands): quoted string parser, parser errors --- src/lib/commands/src/arg/parser/mod.rs | 3 +- src/lib/commands/src/arg/parser/string.rs | 60 ++++++++++++++++++++++- src/lib/commands/src/arg/parser/utils.rs | 8 ++- src/lib/commands/src/ctx.rs | 2 +- src/lib/commands/src/errors.rs | 2 + src/lib/commands/src/tests.rs | 37 ++++++++++---- 6 files changed, 97 insertions(+), 15 deletions(-) diff --git a/src/lib/commands/src/arg/parser/mod.rs b/src/lib/commands/src/arg/parser/mod.rs index 1b2bc038..282df95a 100644 --- a/src/lib/commands/src/arg/parser/mod.rs +++ b/src/lib/commands/src/arg/parser/mod.rs @@ -4,8 +4,7 @@ use crate::{ctx::CommandContext, input::CommandInput, ParserResult}; pub mod int; pub mod string; - -pub(crate) mod utils; +pub mod utils; pub trait ArgumentParser: Send + Sync { fn parse(&self, context: Arc<&CommandContext>, input: Arc>) diff --git a/src/lib/commands/src/arg/parser/string.rs b/src/lib/commands/src/arg/parser/string.rs index 8d542081..24a0dc9b 100644 --- a/src/lib/commands/src/arg/parser/string.rs +++ b/src/lib/commands/src/arg/parser/string.rs @@ -2,7 +2,7 @@ use std::sync::{Arc, Mutex}; use crate::{ctx::CommandContext, input::CommandInput, ParserResult}; -use super::ArgumentParser; +use super::{utils::parser_error, ArgumentParser}; pub struct SingleStringParser; @@ -48,3 +48,61 @@ impl ArgumentParser for GreedyStringParser { GreedyStringParser } } +pub struct QuotedStringParser; + +impl ArgumentParser for QuotedStringParser { + fn parse(&self, _ctx: Arc<&CommandContext>, input: Arc>) -> ParserResult { + let mut input = input.lock().unwrap(); + + input.skip_whitespace(u32::MAX, false); + + if input.peek() != Some('"') { + return Err(parser_error("expected opening quote")); + } + + input.read(1); + + 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")) + } + + fn new() -> Self + where + Self: Sized, + { + QuotedStringParser + } +} diff --git a/src/lib/commands/src/arg/parser/utils.rs b/src/lib/commands/src/arg/parser/utils.rs index 4c688462..deb59faf 100644 --- a/src/lib/commands/src/arg/parser/utils.rs +++ b/src/lib/commands/src/arg/parser/utils.rs @@ -2,7 +2,13 @@ use std::error::Error; use ferrumc_text::{NamedColor, TextComponent, TextComponentBuilder}; -pub(crate) fn error(err: impl Error) -> TextComponent { +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/ctx.rs b/src/lib/commands/src/ctx.rs index e1d205ae..1d5558ec 100644 --- a/src/lib/commands/src/ctx.rs +++ b/src/lib/commands/src/ctx.rs @@ -34,7 +34,7 @@ impl CommandContext { todo!("failed downcasting command argument, change design of this fn"); } }, - Err(_) => unreachable!("arg has already been validated"), + 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 index 0a047a83..d2b6b10c 100644 --- a/src/lib/commands/src/errors.rs +++ b/src/lib/commands/src/errors.rs @@ -4,4 +4,6 @@ use thiserror::Error; pub enum CommandError { #[error("Something failed lol")] SomeError, + #[error("Parser error: {0}")] + ParserError(String), } diff --git a/src/lib/commands/src/tests.rs b/src/lib/commands/src/tests.rs index 5a0c3ad4..be4e6d4c 100644 --- a/src/lib/commands/src/tests.rs +++ b/src/lib/commands/src/tests.rs @@ -7,7 +7,13 @@ use ferrumc_world::World; use tokio::net::TcpListener; use crate::{ - arg::{parser::int::IntParser, CommandArgument}, + arg::{ + parser::{ + int::IntParser, + string::{GreedyStringParser, QuotedStringParser}, + }, + CommandArgument, + }, ctx::CommandContext, executor, infrastructure::{find_command, register_command}, @@ -26,31 +32,42 @@ async fn state() -> GlobalState { #[tokio::test] async fn arg_parse_test() { async fn test_executor(ctx: Arc) -> CommandResult { - let num = ctx.arg::("number"); - Ok(TextComponentBuilder::new(num.to_string()).build()) + let quoted = ctx.arg::("quoted"); + let greedy = ctx.arg::("greedy"); + + Ok(TextComponentBuilder::new(format!("{quoted:?} {greedy}")).build()) } let command = crate::Command { name: "input_test", - args: vec![CommandArgument { - name: "number".to_string(), - required: true, - parser: Box::new(IntParser), - }], + 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 ctx = CommandContext::new(CommandInput::of("42".to_string()), command.clone(), state); + let input = "\"hello\" no no no please no I'm so sorry"; + + let ctx = CommandContext::new(CommandInput::of(input.to_string()), command.clone(), state); let result = command.execute(ctx).await; let TextContent::Text { text } = result.unwrap().content else { panic!("result is not text") }; - assert_eq!(text, "42".to_string()); + assert_eq!(text, input); } #[tokio::test] From ede9f8b357b76a4cccc3f1b0c7967e219875bdcf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nils=20Kr=C3=BCger?= Date: Tue, 24 Dec 2024 13:26:50 +0100 Subject: [PATCH 04/32] initial shitty macrso --- Cargo.toml | 2 +- src/lib/commands/Cargo.toml | 2 + src/lib/commands/src/tests.rs | 8 ++ src/lib/derive_macros/src/commands/mod.rs | 119 ++++++++++++++++++++++ src/lib/derive_macros/src/lib.rs | 6 ++ 5 files changed, 136 insertions(+), 1 deletion(-) create mode 100644 src/lib/derive_macros/src/commands/mod.rs diff --git a/Cargo.toml b/Cargo.toml index 753b9fa4..1c35c2f9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -150,7 +150,7 @@ uuid = { version = "1.1", features = ["v4", "v3", "serde"] } # 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" diff --git a/src/lib/commands/Cargo.toml b/src/lib/commands/Cargo.toml index 88d08ffa..11976cf7 100644 --- a/src/lib/commands/Cargo.toml +++ b/src/lib/commands/Cargo.toml @@ -14,3 +14,5 @@ ferrumc-state = { workspace = true } [dev-dependencies] # Needed for the ServerState mock... :concern: ferrumc-ecs = { workspace = true } ferrumc-world = { workspace = true } +ferrumc-macros = { workspace = true } +ctor = { workspace = true } diff --git a/src/lib/commands/src/tests.rs b/src/lib/commands/src/tests.rs index be4e6d4c..50056bee 100644 --- a/src/lib/commands/src/tests.rs +++ b/src/lib/commands/src/tests.rs @@ -1,6 +1,7 @@ use std::sync::Arc; use ferrumc_ecs::Universe; +use ferrumc_macros::Command; use ferrumc_state::{GlobalState, ServerState}; use ferrumc_text::{TextComponentBuilder, TextContent}; use ferrumc_world::World; @@ -103,3 +104,10 @@ async fn parse_test() { assert_eq!(text, "42".to_string()); } + +#[derive(Command)] +struct TestCommand { + #[sender] + sender: String, + message: GreedyStringParser, +} 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..c075ac94 --- /dev/null +++ b/src/lib/derive_macros/src/commands/mod.rs @@ -0,0 +1,119 @@ +use proc_macro::TokenStream; +use quote::{format_ident, quote}; +use syn::{parse_macro_input, Attribute, DeriveInput, Meta}; + +pub fn derive_command(input: TokenStream) -> TokenStream { + let input = parse_macro_input!(input as DeriveInput); + + let command_names = get_command_names(&input.attrs); + + let struct_name = &input.ident; + let parser_struct_name = format_ident!("__{}Parser", struct_name); + + let fields = match &input.data { + syn::Data::Struct(data) => &data.fields, + _ => panic!("Command can only be derived for structs"), + }; + + let mut parser_fields = Vec::new(); + let mut result_fields = Vec::new(); + let mut field_conversions = Vec::new(); + + for field in fields { + let field_name = field.ident.as_ref().unwrap(); + let field_type = &field.ty; + + let is_sender = field.attrs.iter().any(|attr| { + let Meta::Path(path) = &attr.meta else { + return false; + }; + + path.is_ident("sender") + }); + + if is_sender { + result_fields.push(quote! { + #field_name: #field_type + }); + field_conversions.push(quote! { + #field_name: "" // TODO + }); + } else { + parser_fields.push(quote! { + #field_name: Box + }); + result_fields.push(quote! { + #field_name: #field_type + }); + field_conversions.push(quote! { + #field_name: ctx.arg(stringify!(#field_name)) + }); + } + } + + let expanded = quote! { + struct #parser_struct_name { + #(#parser_fields,)* + } + + struct #struct_name { + #(#result_fields,)* + } + + impl #struct_name { + async fn execute(command: #struct_name) -> ::ferrumc_commands::CommandResult { + Ok(TextComponentBuilder::new("").build()) + } + + #[ctor::ctor] + fn register() { + let command = Arc::new(Command { + name: #(#command_names,)*[0], + args: vec![ + #(::ferrumc_commands::arg::CommandArgument { + name: stringify!(#parser_fields).to_string(), + required: true, + parser: self.#parser_fields.clone(), + },)* + ], + executor: executor(|ctx: Arc<::ferrumc_commands::ctx::CommandContext>| async move { + let command = #struct_name { + #(#field_conversions,)* + }; + + #struct_name::execute(command) + }), + }); + + for &name in &[#(#command_names,)*] { + ::ferrumc_commands::infrastructure::register_command(Arc::clone(&command)); + } + } + } + }; + + TokenStream::from(expanded) +} + +/// Gets the command names, i.e. primary name and aliases from an attribute list +fn get_command_names(attrs: &[Attribute]) -> Vec { + for attr in attrs { + if let Meta::List(_) = &attr.meta { + if attr.path().is_ident("command") { + let mut names = Vec::new(); + + if let Err(_) = attr.parse_nested_meta(|meta| { + if let Some(ident) = meta.path.get_ident() { + names.push(ident.to_string()); + } + Ok(()) + }) { + continue; + } + + return names; + } + } + } + vec![] +} diff --git a/src/lib/derive_macros/src/lib.rs b/src/lib/derive_macros/src/lib.rs index 06fd84f8..a3963a1e 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; @@ -54,3 +55,8 @@ pub fn bake_packet_registry(input: TokenStream) -> TokenStream { net::packets::bake_registry(input) } // #=================== PACKETS ===================# + +#[proc_macro_derive(Command, attributes(command, sender))] +pub fn derive_command(input: TokenStream) -> TokenStream { + commands::derive_command(input) +} From f87861020e157a1a2af9916fe66e8e09e1155d8d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nils=20Kr=C3=BCger?= Date: Wed, 25 Dec 2024 11:25:27 +0100 Subject: [PATCH 05/32] still broken macros --- src/lib/commands/src/arg/mod.rs | 5 +- src/lib/commands/src/arg/parser/int.rs | 4 +- src/lib/commands/src/arg/parser/mod.rs | 5 +- src/lib/commands/src/arg/parser/string.rs | 12 ++- src/lib/commands/src/ctx.rs | 4 +- src/lib/commands/src/lib.rs | 2 +- src/lib/commands/src/tests.rs | 1 + src/lib/derive_macros/src/commands/mod.rs | 92 ++++++++++++----------- 8 files changed, 71 insertions(+), 54 deletions(-) diff --git a/src/lib/commands/src/arg/mod.rs b/src/lib/commands/src/arg/mod.rs index 9962e6f1..68a4e104 100644 --- a/src/lib/commands/src/arg/mod.rs +++ b/src/lib/commands/src/arg/mod.rs @@ -1,3 +1,4 @@ +use std::any::Any; use parser::ArgumentParser; pub mod parser; @@ -5,11 +6,11 @@ pub mod parser; pub struct CommandArgument { pub name: String, pub required: bool, - pub parser: Box, + pub parser: Box>, } impl CommandArgument { - pub fn new(name: String, required: bool, parser: Box) -> Self { + pub fn new(name: String, required: bool, parser: Box>) -> Self { CommandArgument { name, required, diff --git a/src/lib/commands/src/arg/parser/int.rs b/src/lib/commands/src/arg/parser/int.rs index 2569e52b..5b049c04 100644 --- a/src/lib/commands/src/arg/parser/int.rs +++ b/src/lib/commands/src/arg/parser/int.rs @@ -7,7 +7,9 @@ use super::{utils::error, ArgumentParser}; pub struct IntParser; impl ArgumentParser for IntParser { - fn parse(&self, _ctx: Arc<&CommandContext>, input: Arc>) -> ParserResult { + type Output = u32; + + fn parse(&self, _ctx: Arc<&CommandContext>, input: Arc>) -> ParserResult { let token = input.lock().unwrap().read_string(); match token.parse::() { diff --git a/src/lib/commands/src/arg/parser/mod.rs b/src/lib/commands/src/arg/parser/mod.rs index 282df95a..4b294c92 100644 --- a/src/lib/commands/src/arg/parser/mod.rs +++ b/src/lib/commands/src/arg/parser/mod.rs @@ -1,3 +1,4 @@ +use std::any::Any; use std::sync::{Arc, Mutex}; use crate::{ctx::CommandContext, input::CommandInput, ParserResult}; @@ -7,8 +8,10 @@ pub mod string; pub mod utils; pub trait ArgumentParser: Send + Sync { + type Output: Any + ?Sized; + fn parse(&self, context: Arc<&CommandContext>, input: Arc>) - -> ParserResult; + -> ParserResult; fn new() -> Self where Self: Sized; diff --git a/src/lib/commands/src/arg/parser/string.rs b/src/lib/commands/src/arg/parser/string.rs index 24a0dc9b..a748752c 100644 --- a/src/lib/commands/src/arg/parser/string.rs +++ b/src/lib/commands/src/arg/parser/string.rs @@ -7,7 +7,9 @@ use super::{utils::parser_error, ArgumentParser}; pub struct SingleStringParser; impl ArgumentParser for SingleStringParser { - fn parse(&self, _ctx: Arc<&CommandContext>, input: Arc>) -> ParserResult { + type Output = String; + + fn parse(&self, _ctx: Arc<&CommandContext>, input: Arc>) -> ParserResult { Ok(Box::new(input.lock().unwrap().read_string())) } @@ -22,7 +24,9 @@ impl ArgumentParser for SingleStringParser { pub struct GreedyStringParser; impl ArgumentParser for GreedyStringParser { - fn parse(&self, _ctx: Arc<&CommandContext>, input: Arc>) -> ParserResult { + type Output = String; + + fn parse(&self, _ctx: Arc<&CommandContext>, input: Arc>) -> ParserResult { let mut result = String::new(); loop { @@ -51,7 +55,9 @@ impl ArgumentParser for GreedyStringParser { pub struct QuotedStringParser; impl ArgumentParser for QuotedStringParser { - fn parse(&self, _ctx: Arc<&CommandContext>, input: Arc>) -> ParserResult { + type Output = String; + + fn parse(&self, _ctx: Arc<&CommandContext>, input: Arc>) -> ParserResult { let mut input = input.lock().unwrap(); input.skip_whitespace(u32::MAX, false); diff --git a/src/lib/commands/src/ctx.rs b/src/lib/commands/src/ctx.rs index 1d5558ec..199e4ef4 100644 --- a/src/lib/commands/src/ctx.rs +++ b/src/lib/commands/src/ctx.rs @@ -27,7 +27,7 @@ impl CommandContext { let input = self.input.clone(); let result = arg.parser.parse(Arc::new(self), input); - return match result { + match result { Ok(b) => match b.downcast::() { Ok(value) => *value, Err(_) => { @@ -35,7 +35,7 @@ impl CommandContext { } }, Err(err) => unreachable!("arg should have already been validated: {err}"), - }; + } } else { todo!(); } diff --git a/src/lib/commands/src/lib.rs b/src/lib/commands/src/lib.rs index c31bb2b4..d5f38efb 100644 --- a/src/lib/commands/src/lib.rs +++ b/src/lib/commands/src/lib.rs @@ -19,7 +19,7 @@ pub mod input; #[cfg(test)] mod tests; -pub type ParserResult = Result, TextComponent>; +pub type ParserResult = Result, TextComponent>; pub type CommandResult = Result; pub type CommandOutput = Pin + Send + 'static>>; pub type CommandExecutor = diff --git a/src/lib/commands/src/tests.rs b/src/lib/commands/src/tests.rs index 50056bee..a483c52b 100644 --- a/src/lib/commands/src/tests.rs +++ b/src/lib/commands/src/tests.rs @@ -106,6 +106,7 @@ async fn parse_test() { } #[derive(Command)] +#[command(test)] struct TestCommand { #[sender] sender: String, diff --git a/src/lib/derive_macros/src/commands/mod.rs b/src/lib/derive_macros/src/commands/mod.rs index c075ac94..5a620865 100644 --- a/src/lib/derive_macros/src/commands/mod.rs +++ b/src/lib/derive_macros/src/commands/mod.rs @@ -15,6 +15,8 @@ pub fn derive_command(input: TokenStream) -> TokenStream { _ => panic!("Command can only be derived for structs"), }; + let mut parser_field_names = Vec::new(); + let mut parser_field_types = Vec::new(); let mut parser_fields = Vec::new(); let mut result_fields = Vec::new(); let mut field_conversions = Vec::new(); @@ -24,11 +26,10 @@ pub fn derive_command(input: TokenStream) -> TokenStream { let field_type = &field.ty; let is_sender = field.attrs.iter().any(|attr| { - let Meta::Path(path) = &attr.meta else { - return false; - }; - - path.is_ident("sender") + if let Meta::Path(path) = &attr.meta { + return path.is_ident("sender"); + } + false }); if is_sender { @@ -36,14 +37,18 @@ pub fn derive_command(input: TokenStream) -> TokenStream { #field_name: #field_type }); field_conversions.push(quote! { - #field_name: "" // TODO + #field_name: "rad" // TODO: Replace with actual sender retrieval logic }); } else { parser_fields.push(quote! { - #field_name: Box + #field_name: Box::new(<#field_type as crate::arg::parser::ArgumentParser>::Output::default()) + }); + parser_field_names.push(field_name.clone()); + parser_field_types.push(quote! { + #field_name: Box }); result_fields.push(quote! { - #field_name: #field_type + #field_name: <#field_type as crate::arg::ArgumentParser>::Output }); field_conversions.push(quote! { #field_name: ctx.arg(stringify!(#field_name)) @@ -51,9 +56,11 @@ pub fn derive_command(input: TokenStream) -> TokenStream { } } + let command_name = command_names.first().expect("command has no name"); + let expanded = quote! { struct #parser_struct_name { - #(#parser_fields,)* + #(#parser_field_types,)* } struct #struct_name { @@ -61,56 +68,53 @@ pub fn derive_command(input: TokenStream) -> TokenStream { } impl #struct_name { - async fn execute(command: #struct_name) -> ::ferrumc_commands::CommandResult { - Ok(TextComponentBuilder::new("").build()) + async fn execute(command: #struct_name) -> crate::CommandResult { + Ok(ferrumc_text::builders::TextComponentBuilder::new("").build()) } - #[ctor::ctor] fn register() { - let command = Arc::new(Command { - name: #(#command_names,)*[0], - args: vec![ - #(::ferrumc_commands::arg::CommandArgument { - name: stringify!(#parser_fields).to_string(), - required: true, - parser: self.#parser_fields.clone(), - },)* - ], - executor: executor(|ctx: Arc<::ferrumc_commands::ctx::CommandContext>| async move { - let command = #struct_name { - #(#field_conversions,)* - }; - - #struct_name::execute(command) - }), + static INIT: std::sync::Once = std::sync::Once::new(); + INIT.call_once(|| { + let command = std::sync::Arc::new(crate::Command { + name: #command_name, + args: vec![ + #(crate::arg::CommandArgument { + name: stringify!(#parser_field_names).to_string(), + required: true, + parser: Box::new(#parser_fields), + },)* + ], + executor: crate::executor(|ctx: std::sync::Arc| async move { + let command = #struct_name { + #(#field_conversions,)* + }; + + #struct_name::execute(command).await + }), + }); + + for &name in &[#(#command_names,)*] { + crate::infrastructure::register_command(std::sync::Arc::clone(&command)); + } }); - - for &name in &[#(#command_names,)*] { - ::ferrumc_commands::infrastructure::register_command(Arc::clone(&command)); - } } } }; + + println!("{expanded}"); TokenStream::from(expanded) } -/// Gets the command names, i.e. primary name and aliases from an attribute list fn get_command_names(attrs: &[Attribute]) -> Vec { for attr in attrs { - if let Meta::List(_) = &attr.meta { - if attr.path().is_ident("command") { + if let Meta::List(meta_list) = &attr.meta { + if meta_list.path.is_ident("command") { let mut names = Vec::new(); - - if let Err(_) = attr.parse_nested_meta(|meta| { - if let Some(ident) = meta.path.get_ident() { - names.push(ident.to_string()); - } - Ok(()) - }) { - continue; + let input = meta_list.clone().tokens.to_string(); + for name in input.split(", ") { + names.push(name.to_string()); } - return names; } } From b99e7f3eae1363a47f2b331b59b9c8d5eb7bf66d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nils=20Kr=C3=BCger?= Date: Thu, 26 Dec 2024 13:32:03 +0100 Subject: [PATCH 06/32] feat: working command macros --- src/lib/commands/src/arg/mod.rs | 5 +- src/lib/commands/src/arg/parser/int.rs | 4 +- src/lib/commands/src/arg/parser/mod.rs | 5 +- src/lib/commands/src/arg/parser/string.rs | 12 +- src/lib/commands/src/lib.rs | 2 +- src/lib/commands/src/tests.rs | 24 ++- src/lib/derive_macros/src/commands/mod.rs | 205 +++++++++++----------- src/lib/derive_macros/src/lib.rs | 12 +- 8 files changed, 133 insertions(+), 136 deletions(-) diff --git a/src/lib/commands/src/arg/mod.rs b/src/lib/commands/src/arg/mod.rs index 68a4e104..9962e6f1 100644 --- a/src/lib/commands/src/arg/mod.rs +++ b/src/lib/commands/src/arg/mod.rs @@ -1,4 +1,3 @@ -use std::any::Any; use parser::ArgumentParser; pub mod parser; @@ -6,11 +5,11 @@ pub mod parser; pub struct CommandArgument { pub name: String, pub required: bool, - pub parser: Box>, + pub parser: Box, } impl CommandArgument { - pub fn new(name: String, required: bool, parser: Box>) -> Self { + pub fn new(name: String, required: bool, parser: Box) -> Self { CommandArgument { name, required, diff --git a/src/lib/commands/src/arg/parser/int.rs b/src/lib/commands/src/arg/parser/int.rs index 5b049c04..2569e52b 100644 --- a/src/lib/commands/src/arg/parser/int.rs +++ b/src/lib/commands/src/arg/parser/int.rs @@ -7,9 +7,7 @@ use super::{utils::error, ArgumentParser}; pub struct IntParser; impl ArgumentParser for IntParser { - type Output = u32; - - fn parse(&self, _ctx: Arc<&CommandContext>, input: Arc>) -> ParserResult { + fn parse(&self, _ctx: Arc<&CommandContext>, input: Arc>) -> ParserResult { let token = input.lock().unwrap().read_string(); match token.parse::() { diff --git a/src/lib/commands/src/arg/parser/mod.rs b/src/lib/commands/src/arg/parser/mod.rs index 4b294c92..282df95a 100644 --- a/src/lib/commands/src/arg/parser/mod.rs +++ b/src/lib/commands/src/arg/parser/mod.rs @@ -1,4 +1,3 @@ -use std::any::Any; use std::sync::{Arc, Mutex}; use crate::{ctx::CommandContext, input::CommandInput, ParserResult}; @@ -8,10 +7,8 @@ pub mod string; pub mod utils; pub trait ArgumentParser: Send + Sync { - type Output: Any + ?Sized; - fn parse(&self, context: Arc<&CommandContext>, input: Arc>) - -> ParserResult; + -> ParserResult; fn new() -> Self where Self: Sized; diff --git a/src/lib/commands/src/arg/parser/string.rs b/src/lib/commands/src/arg/parser/string.rs index a748752c..24a0dc9b 100644 --- a/src/lib/commands/src/arg/parser/string.rs +++ b/src/lib/commands/src/arg/parser/string.rs @@ -7,9 +7,7 @@ use super::{utils::parser_error, ArgumentParser}; pub struct SingleStringParser; impl ArgumentParser for SingleStringParser { - type Output = String; - - fn parse(&self, _ctx: Arc<&CommandContext>, input: Arc>) -> ParserResult { + fn parse(&self, _ctx: Arc<&CommandContext>, input: Arc>) -> ParserResult { Ok(Box::new(input.lock().unwrap().read_string())) } @@ -24,9 +22,7 @@ impl ArgumentParser for SingleStringParser { pub struct GreedyStringParser; impl ArgumentParser for GreedyStringParser { - type Output = String; - - fn parse(&self, _ctx: Arc<&CommandContext>, input: Arc>) -> ParserResult { + fn parse(&self, _ctx: Arc<&CommandContext>, input: Arc>) -> ParserResult { let mut result = String::new(); loop { @@ -55,9 +51,7 @@ impl ArgumentParser for GreedyStringParser { pub struct QuotedStringParser; impl ArgumentParser for QuotedStringParser { - type Output = String; - - fn parse(&self, _ctx: Arc<&CommandContext>, input: Arc>) -> ParserResult { + fn parse(&self, _ctx: Arc<&CommandContext>, input: Arc>) -> ParserResult { let mut input = input.lock().unwrap(); input.skip_whitespace(u32::MAX, false); diff --git a/src/lib/commands/src/lib.rs b/src/lib/commands/src/lib.rs index d5f38efb..45a56587 100644 --- a/src/lib/commands/src/lib.rs +++ b/src/lib/commands/src/lib.rs @@ -19,7 +19,7 @@ pub mod input; #[cfg(test)] mod tests; -pub type ParserResult = Result, TextComponent>; +pub type ParserResult = Result, TextComponent>; pub type CommandResult = Result; pub type CommandOutput = Pin + Send + 'static>>; pub type CommandExecutor = diff --git a/src/lib/commands/src/tests.rs b/src/lib/commands/src/tests.rs index a483c52b..e9fdf411 100644 --- a/src/lib/commands/src/tests.rs +++ b/src/lib/commands/src/tests.rs @@ -1,9 +1,9 @@ use std::sync::Arc; use ferrumc_ecs::Universe; -use ferrumc_macros::Command; +use ferrumc_macros::{command, arg}; use ferrumc_state::{GlobalState, ServerState}; -use ferrumc_text::{TextComponentBuilder, TextContent}; +use ferrumc_text::{TextComponent, TextComponentBuilder, TextContent}; use ferrumc_world::World; use tokio::net::TcpListener; @@ -20,6 +20,7 @@ use crate::{ infrastructure::{find_command, register_command}, input::CommandInput, CommandResult, + Command, }; async fn state() -> GlobalState { @@ -105,10 +106,17 @@ async fn parse_test() { assert_eq!(text, "42".to_string()); } -#[derive(Command)] -#[command(test)] -struct TestCommand { - #[sender] - sender: String, - message: GreedyStringParser, +#[arg("quoted", QuotedStringParser)] +#[command("test")] +async fn execute_test_command(_ctx: Arc) -> CommandResult { + Ok(TextComponent::default()) } + +#[tokio::test] +async fn macro_test() { + let found_command = find_command("test").unwrap(); + assert_eq!(found_command.args.len(), 1); + let ctx = CommandContext::new(CommandInput::of("".to_string()), found_command.clone(), state().await); + found_command.execute(ctx).await.unwrap(); +} + diff --git a/src/lib/derive_macros/src/commands/mod.rs b/src/lib/derive_macros/src/commands/mod.rs index 5a620865..7728058e 100644 --- a/src/lib/derive_macros/src/commands/mod.rs +++ b/src/lib/derive_macros/src/commands/mod.rs @@ -1,123 +1,118 @@ +use std::{ + collections::HashMap, + sync::{Mutex, OnceLock}, +}; + use proc_macro::TokenStream; use quote::{format_ident, quote}; -use syn::{parse_macro_input, Attribute, DeriveInput, Meta}; +use syn::{ + parse::Parse, parse::ParseStream, parse_macro_input, Ident, ItemFn, LitBool, LitStr, + Result as SynResult, Token, +}; -pub fn derive_command(input: TokenStream) -> TokenStream { - let input = parse_macro_input!(input as DeriveInput); +static PENDING_ARGS: OnceLock>>> = OnceLock::new(); - let command_names = get_command_names(&input.attrs); +struct CommandAttr { + name: String, +} - let struct_name = &input.ident; - let parser_struct_name = format_ident!("__{}Parser", struct_name); +impl Parse for CommandAttr { + fn parse(input: ParseStream) -> SynResult { + let name = input.parse::()?.value(); + Ok(CommandAttr { name }) + } +} - let fields = match &input.data { - syn::Data::Struct(data) => &data.fields, - _ => panic!("Command can only be derived for structs"), - }; +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_string(); - let mut parser_field_names = Vec::new(); - let mut parser_field_types = Vec::new(); - let mut parser_fields = Vec::new(); - let mut result_fields = Vec::new(); - let mut field_conversions = Vec::new(); - - for field in fields { - let field_name = field.ident.as_ref().unwrap(); - let field_type = &field.ty; - - let is_sender = field.attrs.iter().any(|attr| { - if let Meta::Path(path) = &attr.meta { - return path.is_ident("sender"); - } - false - }); - - if is_sender { - result_fields.push(quote! { - #field_name: #field_type - }); - field_conversions.push(quote! { - #field_name: "rad" // TODO: Replace with actual sender retrieval logic - }); + let required = if input.peek(Token![,]) { + input.parse::()?; + input.parse::()?.value() } else { - parser_fields.push(quote! { - #field_name: Box::new(<#field_type as crate::arg::parser::ArgumentParser>::Output::default()) - }); - parser_field_names.push(field_name.clone()); - parser_field_types.push(quote! { - #field_name: Box - }); - result_fields.push(quote! { - #field_name: <#field_type as crate::arg::ArgumentParser>::Output - }); - field_conversions.push(quote! { - #field_name: ctx.arg(stringify!(#field_name)) - }); - } + true + }; + + Ok(ArgAttr { + name, + parser, + required, + }) } +} - let command_name = command_names.first().expect("command has no name"); +fn get_args_storage() -> &'static Mutex>> { + PENDING_ARGS.get_or_init(|| Mutex::new(HashMap::new())) +} - let expanded = quote! { - struct #parser_struct_name { - #(#parser_field_types,)* - } +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(); - struct #struct_name { - #(#result_fields,)* - } + let storage = get_args_storage(); + let mut pending_args = storage.lock().unwrap(); + pending_args + .entry(fn_name) + .or_default() + .push(arg_attr); - impl #struct_name { - async fn execute(command: #struct_name) -> crate::CommandResult { - Ok(ferrumc_text::builders::TextComponentBuilder::new("").build()) - } - - fn register() { - static INIT: std::sync::Once = std::sync::Once::new(); - INIT.call_once(|| { - let command = std::sync::Arc::new(crate::Command { - name: #command_name, - args: vec![ - #(crate::arg::CommandArgument { - name: stringify!(#parser_field_names).to_string(), - required: true, - parser: Box::new(#parser_fields), - },)* - ], - executor: crate::executor(|ctx: std::sync::Arc| async move { - let command = #struct_name { - #(#field_conversions,)* - }; - - #struct_name::execute(command).await - }), - }); - - for &name in &[#(#command_names,)*] { - crate::infrastructure::register_command(std::sync::Arc::clone(&command)); - } - }); - } + 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| format_ident!("{}", arg.parser)) + .collect::>(); + let arg_required = args.iter().map(|arg| arg.required).collect::>(); + + let register_fn_name = format_ident!("__register__{}_command", command_name); + + 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 }; - - println!("{expanded}"); TokenStream::from(expanded) } - -fn get_command_names(attrs: &[Attribute]) -> Vec { - for attr in attrs { - if let Meta::List(meta_list) = &attr.meta { - if meta_list.path.is_ident("command") { - let mut names = Vec::new(); - let input = meta_list.clone().tokens.to_string(); - for name in input.split(", ") { - names.push(name.to_string()); - } - return names; - } - } - } - vec![] -} diff --git a/src/lib/derive_macros/src/lib.rs b/src/lib/derive_macros/src/lib.rs index a3963a1e..7d0dfe06 100644 --- a/src/lib/derive_macros/src/lib.rs +++ b/src/lib/derive_macros/src/lib.rs @@ -56,7 +56,13 @@ pub fn bake_packet_registry(input: TokenStream) -> TokenStream { } // #=================== PACKETS ===================# -#[proc_macro_derive(Command, attributes(command, sender))] -pub fn derive_command(input: TokenStream) -> TokenStream { - commands::derive_command(input) +#[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) +} + From ce5c4103f083814d73a89c00c3cb69411126d157 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nils=20Kr=C3=BCger?= Date: Thu, 26 Dec 2024 13:34:07 +0100 Subject: [PATCH 07/32] style: reformat --- src/lib/commands/src/tests.rs | 12 +++++++----- src/lib/derive_macros/src/commands/mod.rs | 5 +---- src/lib/derive_macros/src/lib.rs | 3 +-- 3 files changed, 9 insertions(+), 11 deletions(-) diff --git a/src/lib/commands/src/tests.rs b/src/lib/commands/src/tests.rs index e9fdf411..ac969577 100644 --- a/src/lib/commands/src/tests.rs +++ b/src/lib/commands/src/tests.rs @@ -1,7 +1,7 @@ use std::sync::Arc; use ferrumc_ecs::Universe; -use ferrumc_macros::{command, arg}; +use ferrumc_macros::{arg, command}; use ferrumc_state::{GlobalState, ServerState}; use ferrumc_text::{TextComponent, TextComponentBuilder, TextContent}; use ferrumc_world::World; @@ -19,8 +19,7 @@ use crate::{ executor, infrastructure::{find_command, register_command}, input::CommandInput, - CommandResult, - Command, + Command, CommandResult, }; async fn state() -> GlobalState { @@ -116,7 +115,10 @@ async fn execute_test_command(_ctx: Arc) -> CommandResult { async fn macro_test() { let found_command = find_command("test").unwrap(); assert_eq!(found_command.args.len(), 1); - let ctx = CommandContext::new(CommandInput::of("".to_string()), found_command.clone(), state().await); + let ctx = CommandContext::new( + CommandInput::of("".to_string()), + found_command.clone(), + state().await, + ); found_command.execute(ctx).await.unwrap(); } - diff --git a/src/lib/derive_macros/src/commands/mod.rs b/src/lib/derive_macros/src/commands/mod.rs index 7728058e..4c56fa14 100644 --- a/src/lib/derive_macros/src/commands/mod.rs +++ b/src/lib/derive_macros/src/commands/mod.rs @@ -61,10 +61,7 @@ pub fn arg(attr: TokenStream, item: TokenStream) -> TokenStream { let storage = get_args_storage(); let mut pending_args = storage.lock().unwrap(); - pending_args - .entry(fn_name) - .or_default() - .push(arg_attr); + pending_args.entry(fn_name).or_default().push(arg_attr); TokenStream::from(quote!(#input_fn)) } diff --git a/src/lib/derive_macros/src/lib.rs b/src/lib/derive_macros/src/lib.rs index 7d0dfe06..3b1b4ae4 100644 --- a/src/lib/derive_macros/src/lib.rs +++ b/src/lib/derive_macros/src/lib.rs @@ -62,7 +62,6 @@ pub fn command(attr: TokenStream, input: TokenStream) -> TokenStream { } #[proc_macro_attribute] - pub fn arg(attr: TokenStream, input: TokenStream) -> TokenStream { +pub fn arg(attr: TokenStream, input: TokenStream) -> TokenStream { commands::arg(attr, input) } - From 4d2dc2a6f6453044ade976e2ec11647863f22cb8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nils=20Kr=C3=BCger?= Date: Thu, 26 Dec 2024 23:41:45 +0100 Subject: [PATCH 08/32] feat(net): commands and system messages, refactor commands a bit, feat: default commands --- Cargo.toml | 2 + src/bin/Cargo.toml | 2 + src/bin/src/main.rs | 3 + src/lib/commands/Cargo.toml | 6 +- src/lib/commands/src/arg/parser/int.rs | 2 +- src/lib/commands/src/arg/parser/mod.rs | 3 +- src/lib/commands/src/arg/parser/string.rs | 6 +- src/lib/commands/src/ctx.rs | 13 ++- src/lib/commands/src/infrastructure.rs | 16 ++-- src/lib/commands/src/lib.rs | 6 +- src/lib/commands/src/tests.rs | 47 +++++------ src/lib/default_commands/Cargo.toml | 13 +++ src/lib/default_commands/src/echo.rs | 47 +++++++++++ src/lib/default_commands/src/lib.rs | 3 + src/lib/net/Cargo.toml | 1 + src/lib/net/src/packets/incoming/command.rs | 84 +++++++++++++++++++ src/lib/net/src/packets/incoming/mod.rs | 2 + src/lib/net/src/packets/outgoing/mod.rs | 1 + .../src/packets/outgoing/system_message.rs | 16 ++++ 19 files changed, 227 insertions(+), 46 deletions(-) create mode 100644 src/lib/default_commands/Cargo.toml create mode 100644 src/lib/default_commands/src/echo.rs create mode 100644 src/lib/default_commands/src/lib.rs create mode 100644 src/lib/net/src/packets/incoming/command.rs create mode 100644 src/lib/net/src/packets/outgoing/system_message.rs diff --git a/Cargo.toml b/Cargo.toml index 1c35c2f9..6e4058e3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,6 +17,7 @@ members = [ "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 +88,7 @@ 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" } diff --git a/src/bin/Cargo.toml b/src/bin/Cargo.toml index ef9f0c03..6e7d0dc8 100644 --- a/src/bin/Cargo.toml +++ b/src/bin/Cargo.toml @@ -33,6 +33,8 @@ 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 } ctor = { workspace = true } parking_lot = { workspace = true } diff --git a/src/bin/src/main.rs b/src/bin/src/main.rs index 62fd09b4..012d3b65 100644 --- a/src/bin/src/main.rs +++ b/src/bin/src/main.rs @@ -99,6 +99,9 @@ async fn entry(cli_args: CLIArgs) -> Result<()> { let state = create_state().await?; let global_state = Arc::new(state); + // 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/lib/commands/Cargo.toml b/src/lib/commands/Cargo.toml index 11976cf7..472ac85f 100644 --- a/src/lib/commands/Cargo.toml +++ b/src/lib/commands/Cargo.toml @@ -11,8 +11,10 @@ tokio = { workspace = true } ferrumc-text = { workspace = true } ferrumc-state = { workspace = true } -[dev-dependencies] # Needed for the ServerState mock... :concern: +# Default commands. Should probably be moved into a seperate crate ferrumc-ecs = { workspace = true } -ferrumc-world = { workspace = true } ferrumc-macros = { workspace = true } ctor = { workspace = true } + +[dev-dependencies] # Needed for the ServerState mock... :concern: +ferrumc-world = { workspace = true } diff --git a/src/lib/commands/src/arg/parser/int.rs b/src/lib/commands/src/arg/parser/int.rs index 2569e52b..8d4a1854 100644 --- a/src/lib/commands/src/arg/parser/int.rs +++ b/src/lib/commands/src/arg/parser/int.rs @@ -7,7 +7,7 @@ use super::{utils::error, ArgumentParser}; pub struct IntParser; impl ArgumentParser for IntParser { - fn parse(&self, _ctx: Arc<&CommandContext>, input: Arc>) -> ParserResult { + fn parse(&self, _ctx: Arc, input: Arc>) -> ParserResult { let token = input.lock().unwrap().read_string(); match token.parse::() { diff --git a/src/lib/commands/src/arg/parser/mod.rs b/src/lib/commands/src/arg/parser/mod.rs index 282df95a..1f2d62fd 100644 --- a/src/lib/commands/src/arg/parser/mod.rs +++ b/src/lib/commands/src/arg/parser/mod.rs @@ -7,8 +7,7 @@ pub mod string; pub mod utils; pub trait ArgumentParser: Send + Sync { - fn parse(&self, context: Arc<&CommandContext>, input: Arc>) - -> ParserResult; + fn parse(&self, context: Arc, input: Arc>) -> ParserResult; fn new() -> Self where Self: Sized; diff --git a/src/lib/commands/src/arg/parser/string.rs b/src/lib/commands/src/arg/parser/string.rs index 24a0dc9b..e7f52bde 100644 --- a/src/lib/commands/src/arg/parser/string.rs +++ b/src/lib/commands/src/arg/parser/string.rs @@ -7,7 +7,7 @@ use super::{utils::parser_error, ArgumentParser}; pub struct SingleStringParser; impl ArgumentParser for SingleStringParser { - fn parse(&self, _ctx: Arc<&CommandContext>, input: Arc>) -> ParserResult { + fn parse(&self, _ctx: Arc, input: Arc>) -> ParserResult { Ok(Box::new(input.lock().unwrap().read_string())) } @@ -22,7 +22,7 @@ impl ArgumentParser for SingleStringParser { pub struct GreedyStringParser; impl ArgumentParser for GreedyStringParser { - fn parse(&self, _ctx: Arc<&CommandContext>, input: Arc>) -> ParserResult { + fn parse(&self, _ctx: Arc, input: Arc>) -> ParserResult { let mut result = String::new(); loop { @@ -51,7 +51,7 @@ impl ArgumentParser for GreedyStringParser { pub struct QuotedStringParser; impl ArgumentParser for QuotedStringParser { - fn parse(&self, _ctx: Arc<&CommandContext>, input: Arc>) -> ParserResult { + fn parse(&self, _ctx: Arc, input: Arc>) -> ParserResult { let mut input = input.lock().unwrap(); input.skip_whitespace(u32::MAX, false); diff --git a/src/lib/commands/src/ctx.rs b/src/lib/commands/src/ctx.rs index 199e4ef4..fa71a3db 100644 --- a/src/lib/commands/src/ctx.rs +++ b/src/lib/commands/src/ctx.rs @@ -11,21 +11,28 @@ 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) -> Arc { + 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, name: &str) -> T { + 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(Arc::new(self), input); + let result = arg.parser.parse(self.clone(), input); match result { Ok(b) => match b.downcast::() { diff --git a/src/lib/commands/src/infrastructure.rs b/src/lib/commands/src/infrastructure.rs index 8d23a245..da2b40a1 100644 --- a/src/lib/commands/src/infrastructure.rs +++ b/src/lib/commands/src/infrastructure.rs @@ -9,24 +9,24 @@ pub fn register_command(command: Arc) { COMMANDS.insert(command.name, command); } -pub fn get_command_by_name(name: &'static str) -> Option> { +pub fn get_command_by_name(name: &str) -> Option> { COMMANDS.get(name).map(|cmd_ref| Arc::clone(&cmd_ref)) } -pub fn find_command(input: &'static str) -> Option> { +pub fn find_command(input: &str) -> Option> { let mut command = None; - let mut input = input; + let mut current = input; - while !input.is_empty() { - command = get_command_by_name(input); + while !current.is_empty() { + command = get_command_by_name(current); if command.is_some() { break; } - if let Some(pos) = input.rfind(' ') { - input = &input[..pos]; + if let Some(pos) = current.rfind(' ') { + current = ¤t[..pos]; } else { - input = ""; + current = ""; } } diff --git a/src/lib/commands/src/lib.rs b/src/lib/commands/src/lib.rs index 45a56587..15cbbcda 100644 --- a/src/lib/commands/src/lib.rs +++ b/src/lib/commands/src/lib.rs @@ -20,7 +20,7 @@ pub mod input; mod tests; pub type ParserResult = Result, TextComponent>; -pub type CommandResult = Result; +pub type CommandResult = Result<(), TextComponent>; pub type CommandOutput = Pin + Send + 'static>>; pub type CommandExecutor = Arc Fn(Arc) -> CommandOutput + Send + Sync + 'static>; @@ -38,8 +38,8 @@ impl Command { pub fn validate( &self, - ctx: Arc<&CommandContext>, - input: Arc>, + ctx: &Arc, + input: &Arc>, ) -> Result<(), TextComponent> { for arg in &self.args { arg.parser.parse(ctx.clone(), input.clone())?; diff --git a/src/lib/commands/src/tests.rs b/src/lib/commands/src/tests.rs index ac969577..a35e079d 100644 --- a/src/lib/commands/src/tests.rs +++ b/src/lib/commands/src/tests.rs @@ -3,7 +3,6 @@ use std::sync::Arc; use ferrumc_ecs::Universe; use ferrumc_macros::{arg, command}; use ferrumc_state::{GlobalState, ServerState}; -use ferrumc_text::{TextComponent, TextComponentBuilder, TextContent}; use ferrumc_world::World; use tokio::net::TcpListener; @@ -36,7 +35,12 @@ async fn arg_parse_test() { let quoted = ctx.arg::("quoted"); let greedy = ctx.arg::("greedy"); - Ok(TextComponentBuilder::new(format!("{quoted:?} {greedy}")).build()) + assert_eq!( + format!("{quoted:?} {greedy}"), + ctx.input.lock().unwrap().input + ); + + Ok(()) } let command = crate::Command { @@ -61,21 +65,22 @@ async fn arg_parse_test() { let input = "\"hello\" no no no please no I'm so sorry"; - let ctx = CommandContext::new(CommandInput::of(input.to_string()), command.clone(), state); - - let result = command.execute(ctx).await; - let TextContent::Text { text } = result.unwrap().content else { - panic!("result is not text") - }; + let ctx = CommandContext::new( + CommandInput::of(input.to_string()), + command.clone(), + state, + 0, + ); - assert_eq!(text, input); + command.execute(ctx).await.unwrap(); } #[tokio::test] async fn parse_test() { async fn test_executor(ctx: Arc) -> CommandResult { let num = ctx.arg::("number"); - Ok(TextComponentBuilder::new(num.to_string()).build()) + assert_eq!(num.to_string(), ctx.input.lock().unwrap().input); + Ok(()) } let command = crate::Command { @@ -91,34 +96,28 @@ async fn parse_test() { let state = state().await; - let ctx = CommandContext::new(CommandInput::of("42".to_string()), command.clone(), state); + 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(); - let result = found_command.execute(ctx).await; - let TextContent::Text { text } = result.unwrap().content else { - panic!("result is not text") - }; - - assert_eq!(text, "42".to_string()); + found_command.execute(ctx).await.unwrap(); } #[arg("quoted", QuotedStringParser)] #[command("test")] async fn execute_test_command(_ctx: Arc) -> CommandResult { - Ok(TextComponent::default()) + Ok(()) } #[tokio::test] async fn macro_test() { let found_command = find_command("test").unwrap(); assert_eq!(found_command.args.len(), 1); - let ctx = CommandContext::new( - CommandInput::of("".to_string()), - found_command.clone(), - state().await, - ); - found_command.execute(ctx).await.unwrap(); } diff --git a/src/lib/default_commands/Cargo.toml b/src/lib/default_commands/Cargo.toml new file mode 100644 index 00000000..e80aa4d9 --- /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-net = { workspace = true } +ferrumc-ecs = { workspace = true } +ferrumc-net-codec = { workspace = true } +ferrumc-macros = { workspace = true } +ferrumc-text = { workspace = true } +ctor = { 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..b9b11840 --- /dev/null +++ b/src/lib/default_commands/src/echo.rs @@ -0,0 +1,47 @@ +use std::sync::Arc; + +use ferrumc_commands::{ + arg::parser::string::GreedyStringParser, arg::CommandArgument, ctx::CommandContext, executor, + infrastructure::register_command, Command, CommandResult, +}; +use ferrumc_macros::{arg, command}; +use ferrumc_net::connection::StreamWriter; +use ferrumc_net::packets::outgoing::system_message::SystemMessagePacket; +use ferrumc_net_codec::encode::NetEncodeOpts; +use ferrumc_text::{NamedColor, TextComponentBuilder}; + +#[arg("message", GreedyStringParser)] +#[command("echo")] +async fn echo(ctx: Arc) -> CommandResult { + println!("Hi"); + let message = ctx.arg::("message"); + let mut writer = match ctx + .state + .universe + .get_mut::(ctx.connection_id) + { + Ok(writer) => writer, + Err(e) => { + println!("No stream writer :/ {e:#?}"); + return Ok(()); + } + }; + + if let Err(e) = writer + .send_packet( + &SystemMessagePacket::new( + TextComponentBuilder::new(message) + .color(NamedColor::Green) + .build(), + false, + ), + &NetEncodeOpts::WithLength, + ) + .await + { + println!("Failed sending packet :/ {e:#?}"); + return Ok(()); + } + + 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..0bf1d5da --- /dev/null +++ b/src/lib/default_commands/src/lib.rs @@ -0,0 +1,3 @@ +pub mod echo; + +pub fn init() {} diff --git a/src/lib/net/Cargo.toml b/src/lib/net/Cargo.toml index dd1ad654..daa37386 100644 --- a/src/lib/net/Cargo.toml +++ b/src/lib/net/Cargo.toml @@ -9,6 +9,7 @@ thiserror = { workspace = true } ferrumc-net-encryption = { workspace = true } ferrumc-net-codec = { workspace = true } ferrumc-macros = { workspace = true } +ferrumc-commands = { workspace = true } ferrumc-config = { workspace = true } ferrumc-ecs = { workspace = true } ferrumc-events = { workspace = true } 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..c2eca04e --- /dev/null +++ b/src/lib/net/src/packets/incoming/command.rs @@ -0,0 +1,84 @@ +use std::sync::{Arc, Mutex}; + +use ferrumc_commands::{ctx::CommandContext, infrastructure::find_command, input::CommandInput}; +use ferrumc_macros::{packet, NetDecode}; +use ferrumc_net_codec::encode::NetEncodeOpts; +use ferrumc_state::ServerState; +use ferrumc_text::{NamedColor, TextComponentBuilder}; + +use crate::{ + connection::StreamWriter, + packets::{outgoing::system_message::SystemMessagePacket, IncomingPacket}, + NetResult, +}; + +#[derive(NetDecode, Debug, Clone)] +#[packet(packet_id = 0x04, state = "play")] +pub struct ChatCommandPacket { + command: String, +} + +impl IncomingPacket for ChatCommandPacket { + async fn handle(self, conn_id: usize, state: Arc) -> NetResult<()> { + let mut writer = state.universe.get_mut::(conn_id)?; + + let command = find_command(self.command.as_str()); + if let None = command { + // invalid command TwT, we send a message that the given command + // could not be found. + writer + .send_packet( + &SystemMessagePacket::new( + TextComponentBuilder::new("unknown command") + .color(NamedColor::Red) + .build(), + false, + ), + &NetEncodeOpts::WithLength, + ) + .await?; + return Ok(()); + } + + let command = command.unwrap(); + + let input = &self + .command + .strip_prefix(command.name) + .unwrap_or(&self.command); + let input = CommandInput::of(input.to_string()); + let ctx = CommandContext::new(input.clone(), command.clone(), state, conn_id); + if let Err(err) = command.validate(&ctx, &Arc::new(Mutex::new(input))) { + writer + .send_packet( + &SystemMessagePacket::new( + TextComponentBuilder::new("invalid args: ") + .extra(err) + .color(NamedColor::Red) + .build(), + false, + ), + &NetEncodeOpts::WithLength, + ) + .await?; + return Ok(()); + } + + if let Err(err) = command.execute(ctx).await { + writer + .send_packet( + &SystemMessagePacket::new( + TextComponentBuilder::new("command error: ") + .extra(err) + .color(NamedColor::Red) + .build(), + false, + ), + &NetEncodeOpts::WithLength, + ) + .await?; + }; + + Ok(()) + } +} diff --git a/src/lib/net/src/packets/incoming/mod.rs b/src/lib/net/src/packets/incoming/mod.rs index 2f5254a1..b7994e38 100644 --- a/src/lib/net/src/packets/incoming/mod.rs +++ b/src/lib/net/src/packets/incoming/mod.rs @@ -14,3 +14,5 @@ pub mod packet_skeleton; pub mod set_player_position; pub mod set_player_position_and_rotation; pub mod set_player_rotation; + +pub mod command; diff --git a/src/lib/net/src/packets/outgoing/mod.rs b/src/lib/net/src/packets/outgoing/mod.rs index 9c63db7c..e61af82d 100644 --- a/src/lib/net/src/packets/outgoing/mod.rs +++ b/src/lib/net/src/packets/outgoing/mod.rs @@ -13,4 +13,5 @@ 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; 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..aae4e6cf --- /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 = 0x6c)] +pub struct SystemMessagePacket { + message: TextComponent, + overlay: bool, +} + +impl SystemMessagePacket { + pub fn new(message: TextComponent, overlay: bool) -> Self { + Self { message, overlay } + } +} From 3724ddeb0cfa9d934673babfe5efd3cf3b03b051 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nils=20Kr=C3=BCger?= Date: Fri, 27 Dec 2024 08:58:25 +0100 Subject: [PATCH 09/32] fix(net): deadlock when command accesses streamwriter, fix(storage): clippy warning --- src/lib/default_commands/Cargo.toml | 1 + src/lib/default_commands/src/echo.rs | 13 ++++++++----- src/lib/net/src/packets/incoming/command.rs | 4 +++- src/lib/storage/src/lib.rs | 1 - 4 files changed, 12 insertions(+), 7 deletions(-) diff --git a/src/lib/default_commands/Cargo.toml b/src/lib/default_commands/Cargo.toml index e80aa4d9..50515c4c 100644 --- a/src/lib/default_commands/Cargo.toml +++ b/src/lib/default_commands/Cargo.toml @@ -11,3 +11,4 @@ ferrumc-net-codec = { workspace = true } ferrumc-macros = { workspace = true } ferrumc-text = { 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 index b9b11840..131c8dcd 100644 --- a/src/lib/default_commands/src/echo.rs +++ b/src/lib/default_commands/src/echo.rs @@ -9,11 +9,11 @@ use ferrumc_net::connection::StreamWriter; use ferrumc_net::packets::outgoing::system_message::SystemMessagePacket; use ferrumc_net_codec::encode::NetEncodeOpts; use ferrumc_text::{NamedColor, TextComponentBuilder}; +use tracing::error; #[arg("message", GreedyStringParser)] #[command("echo")] async fn echo(ctx: Arc) -> CommandResult { - println!("Hi"); let message = ctx.arg::("message"); let mut writer = match ctx .state @@ -21,13 +21,16 @@ async fn echo(ctx: Arc) -> CommandResult { .get_mut::(ctx.connection_id) { Ok(writer) => writer, - Err(e) => { - println!("No stream writer :/ {e:#?}"); + Err(err) => { + error!( + "failed retrieving stream writer for conn id {}: {err}", + ctx.connection_id + ); return Ok(()); } }; - if let Err(e) = writer + if let Err(err) = writer .send_packet( &SystemMessagePacket::new( TextComponentBuilder::new(message) @@ -39,7 +42,7 @@ async fn echo(ctx: Arc) -> CommandResult { ) .await { - println!("Failed sending packet :/ {e:#?}"); + error!("failed sending packet: {err}"); return Ok(()); } diff --git a/src/lib/net/src/packets/incoming/command.rs b/src/lib/net/src/packets/incoming/command.rs index c2eca04e..62c2b811 100644 --- a/src/lib/net/src/packets/incoming/command.rs +++ b/src/lib/net/src/packets/incoming/command.rs @@ -47,7 +47,7 @@ impl IncomingPacket for ChatCommandPacket { .strip_prefix(command.name) .unwrap_or(&self.command); let input = CommandInput::of(input.to_string()); - let ctx = CommandContext::new(input.clone(), command.clone(), state, conn_id); + let ctx = CommandContext::new(input.clone(), command.clone(), state.clone(), conn_id); if let Err(err) = command.validate(&ctx, &Arc::new(Mutex::new(input))) { writer .send_packet( @@ -64,7 +64,9 @@ impl IncomingPacket for ChatCommandPacket { return Ok(()); } + 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::(conn_id)?; writer .send_packet( &SystemMessagePacket::new( diff --git a/src/lib/storage/src/lib.rs b/src/lib/storage/src/lib.rs index 5ee1dd33..0f739ffb 100644 --- a/src/lib/storage/src/lib.rs +++ b/src/lib/storage/src/lib.rs @@ -1,4 +1,3 @@ -#![feature(async_closure)] pub mod backends; pub mod compressors; pub mod errors; From f88ec8f1e8aad43345a43a0db98b2cb80ace8bc8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nils=20Kr=C3=BCger?= Date: Fri, 27 Dec 2024 09:53:39 +0100 Subject: [PATCH 10/32] fix(storage): remove non existant mod --- src/lib/storage/src/lib.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/lib/storage/src/lib.rs b/src/lib/storage/src/lib.rs index 5a37d927..4bbb6e3d 100644 --- a/src/lib/storage/src/lib.rs +++ b/src/lib/storage/src/lib.rs @@ -1,4 +1,3 @@ -pub mod backends; pub mod compressors; pub mod errors; pub mod lmdb; From dfa71d856e5a295388962d48d3973d84bbbf76b8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nils=20Kr=C3=BCger?= Date: Fri, 27 Dec 2024 10:00:48 +0100 Subject: [PATCH 11/32] make clippy happy --- src/lib/commands/src/input.rs | 2 +- src/lib/net/src/packets/incoming/command.rs | 4 +--- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/src/lib/commands/src/input.rs b/src/lib/commands/src/input.rs index fb885eca..f8b8138e 100644 --- a/src/lib/commands/src/input.rs +++ b/src/lib/commands/src/input.rs @@ -39,7 +39,7 @@ impl CommandInput { let mut i = 0; while i < max_spaces && self.has_remaining_input() - && self.peek().map_or(false, |c| c.is_whitespace()) + && self.peek().is_some_and(|c| c.is_whitespace()) { self.read(1); i += 1; diff --git a/src/lib/net/src/packets/incoming/command.rs b/src/lib/net/src/packets/incoming/command.rs index 62c2b811..2ebb8ba6 100644 --- a/src/lib/net/src/packets/incoming/command.rs +++ b/src/lib/net/src/packets/incoming/command.rs @@ -23,9 +23,7 @@ impl IncomingPacket for ChatCommandPacket { let mut writer = state.universe.get_mut::(conn_id)?; let command = find_command(self.command.as_str()); - if let None = command { - // invalid command TwT, we send a message that the given command - // could not be found. + if command.is_none() { writer .send_packet( &SystemMessagePacket::new( From 0815ccaf95b2c0b01a6c2153bedfa6cb479325e3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nils=20Kr=C3=BCger?= Date: Fri, 27 Dec 2024 10:52:21 +0100 Subject: [PATCH 12/32] style(adapters/anvil): make clippy happy --- src/lib/adapters/anvil/src/lib.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/lib/adapters/anvil/src/lib.rs b/src/lib/adapters/anvil/src/lib.rs index d289c9cc..16b0e047 100644 --- a/src/lib/adapters/anvil/src/lib.rs +++ b/src/lib/adapters/anvil/src/lib.rs @@ -100,10 +100,10 @@ impl LoadedAnvilFile { pub fn get_locations(&self) -> Vec { let mut locations = Vec::with_capacity(1024); for i in 0..1024 { - 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]); + 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])); if location != 0 { locations.push(location); } From fb625f8e33e109fecb995a1bd57c273354a239b2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nils=20Kr=C3=BCger?= Date: Fri, 27 Dec 2024 21:17:16 +0100 Subject: [PATCH 13/32] refactor(commands): move test dependencies to dev-dependencies section --- src/lib/commands/Cargo.toml | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/lib/commands/Cargo.toml b/src/lib/commands/Cargo.toml index 472ac85f..c29aab33 100644 --- a/src/lib/commands/Cargo.toml +++ b/src/lib/commands/Cargo.toml @@ -11,10 +11,8 @@ tokio = { workspace = true } ferrumc-text = { workspace = true } ferrumc-state = { workspace = true } -# Default commands. Should probably be moved into a seperate crate +[dev-dependencies] # Needed for the ServerState mock... :concern: ferrumc-ecs = { workspace = true } ferrumc-macros = { workspace = true } -ctor = { workspace = true } - -[dev-dependencies] # Needed for the ServerState mock... :concern: ferrumc-world = { workspace = true } +ctor = { workspace = true } From f1ddf72d29027af26f4cfb49d861274541bb94e6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nils=20Kr=C3=BCger?= Date: Sat, 28 Dec 2024 00:26:01 +0100 Subject: [PATCH 14/32] feat(commands): command nodes and graph --- Cargo.toml | 1 + src/lib/commands/Cargo.toml | 1 + src/lib/commands/src/graph/mod.rs | 96 +++++++++++++ src/lib/commands/src/graph/node.rs | 167 ++++++++++++++++++++++ src/lib/commands/src/lib.rs | 1 + src/lib/commands/src/tests.rs | 29 +++- src/lib/derive_macros/src/commands/mod.rs | 2 +- 7 files changed, 290 insertions(+), 7 deletions(-) create mode 100644 src/lib/commands/src/graph/mod.rs create mode 100644 src/lib/commands/src/graph/node.rs diff --git a/Cargo.toml b/Cargo.toml index 6f233e50..f102bb87 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -184,6 +184,7 @@ colored = "2.1.0" # Misc deepsize = "0.2.0" page_size = "0.6.0" +enum-ordinalize = "4.3.0" # I/O tempfile = "3.12.0" diff --git a/src/lib/commands/Cargo.toml b/src/lib/commands/Cargo.toml index c29aab33..25a3e514 100644 --- a/src/lib/commands/Cargo.toml +++ b/src/lib/commands/Cargo.toml @@ -10,6 +10,7 @@ dashmap = { workspace = true } tokio = { workspace = true } ferrumc-text = { workspace = true } ferrumc-state = { workspace = true } +enum-ordinalize = { workspace = true } [dev-dependencies] # Needed for the ServerState mock... :concern: ferrumc-ecs = { workspace = true } diff --git a/src/lib/commands/src/graph/mod.rs b/src/lib/commands/src/graph/mod.rs new file mode 100644 index 00000000..afb04476 --- /dev/null +++ b/src/lib/commands/src/graph/mod.rs @@ -0,0 +1,96 @@ +use std::sync::Arc; +use std::collections::HashMap; + +use node::{CommandNode, CommandNodeFlag, CommandNodeType}; + +use crate::Command; + +pub mod node; + +#[derive(Clone, Debug, PartialEq)] +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: 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; + let node_type = if is_last { + CommandNodeType::Literal + } else { + CommandNodeType::Literal + }; + + if let Some(&child_index) = self.node_to_indices.get(part) { + current_node_index = child_index; + } else { + let mut node = CommandNode { + flags: CommandNodeFlag::NodeType(node_type).bitmask(), + children: 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); + self.root_node.children.push(node_index); + + self.nodes[current_node_index as usize].children.push(node_index); + current_node_index = node_index; + } + } + + for arg in &command.args { + let mut arg_node = CommandNode { + flags: CommandNodeFlag::NodeType(CommandNodeType::Argument).bitmask(), + children: Vec::new(), + redirect_node: None, + name: Some(arg.name.clone()), + parser_id: None, + properties: None, + suggestions_type: None, + }; + + if arg.required { + arg_node.flags |= CommandNodeFlag::Executable.bitmask(); + } + + let arg_node_index = self.nodes.len() as u32; + self.nodes.push(arg_node); + self.nodes[current_node_index as usize].children.push(arg_node_index); + } + } +} diff --git a/src/lib/commands/src/graph/node.rs b/src/lib/commands/src/graph/node.rs new file mode 100644 index 00000000..a2d6a66d --- /dev/null +++ b/src/lib/commands/src/graph/node.rs @@ -0,0 +1,167 @@ +use std::{collections::HashMap, fmt}; + +use enum_ordinalize::Ordinalize; + +#[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, Debug, PartialEq)] +pub enum CommandNodeProperties { + IntRange { min: i32, max: i32 }, + FloatRange { min: f32, max: f32 }, + String { max_length: u32 }, + Other(HashMap), +} + +#[derive(Clone, PartialEq)] +pub struct CommandNode { + pub flags: u8, + pub children: Vec, + 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("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 + } +} + +#[derive(Clone, Debug, PartialEq, Ordinalize)] +pub enum MinecraftCommandParser { + 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 +} + diff --git a/src/lib/commands/src/lib.rs b/src/lib/commands/src/lib.rs index 15cbbcda..537ca83b 100644 --- a/src/lib/commands/src/lib.rs +++ b/src/lib/commands/src/lib.rs @@ -15,6 +15,7 @@ pub mod ctx; pub mod errors; pub mod infrastructure; pub mod input; +pub mod graph; #[cfg(test)] mod tests; diff --git a/src/lib/commands/src/tests.rs b/src/lib/commands/src/tests.rs index a35e079d..9256cc15 100644 --- a/src/lib/commands/src/tests.rs +++ b/src/lib/commands/src/tests.rs @@ -13,12 +13,7 @@ use crate::{ string::{GreedyStringParser, QuotedStringParser}, }, CommandArgument, - }, - ctx::CommandContext, - executor, - infrastructure::{find_command, register_command}, - input::CommandInput, - Command, CommandResult, + }, ctx::CommandContext, executor, graph::{node::CommandNodeType, CommandGraph}, infrastructure::{find_command, register_command}, input::CommandInput, Command, CommandResult }; async fn state() -> GlobalState { @@ -121,3 +116,25 @@ 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/derive_macros/src/commands/mod.rs b/src/lib/derive_macros/src/commands/mod.rs index 4c56fa14..7bb44e60 100644 --- a/src/lib/derive_macros/src/commands/mod.rs +++ b/src/lib/derive_macros/src/commands/mod.rs @@ -85,7 +85,7 @@ pub fn command(attr: TokenStream, item: TokenStream) -> TokenStream { .collect::>(); let arg_required = args.iter().map(|arg| arg.required).collect::>(); - let register_fn_name = format_ident!("__register__{}_command", command_name); + let register_fn_name = format_ident!("__register_{}_command", command_name); let expanded = quote! { #[ctor::ctor] From 522a97874da860115c32af7631ebc0ce7e72878b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nils=20Kr=C3=BCger?= Date: Sat, 28 Dec 2024 02:15:36 +0100 Subject: [PATCH 15/32] style(commands): make clippy happy --- src/lib/commands/src/graph/mod.rs | 22 ++++++++++++---------- src/lib/commands/src/graph/node.rs | 7 +++---- src/lib/commands/src/lib.rs | 2 +- src/lib/commands/src/tests.rs | 9 +++++++-- 4 files changed, 23 insertions(+), 17 deletions(-) diff --git a/src/lib/commands/src/graph/mod.rs b/src/lib/commands/src/graph/mod.rs index afb04476..2ade3c2c 100644 --- a/src/lib/commands/src/graph/mod.rs +++ b/src/lib/commands/src/graph/mod.rs @@ -1,5 +1,5 @@ -use std::sync::Arc; use std::collections::HashMap; +use std::sync::Arc; use node::{CommandNode, CommandNodeFlag, CommandNodeType}; @@ -40,17 +40,12 @@ impl CommandGraph { for (i, part) in command.name.split_whitespace().enumerate() { let is_last = i == command.name.split_whitespace().count() - 1; - let node_type = if is_last { - CommandNodeType::Literal - } else { - CommandNodeType::Literal - }; if let Some(&child_index) = self.node_to_indices.get(part) { current_node_index = child_index; } else { let mut node = CommandNode { - flags: CommandNodeFlag::NodeType(node_type).bitmask(), + flags: CommandNodeFlag::NodeType(CommandNodeType::Literal).bitmask(), children: Vec::new(), redirect_node: None, name: Some(part.to_string()), @@ -59,7 +54,10 @@ impl CommandGraph { suggestions_type: None, }; - if is_last && !command.args.is_empty() && command.args.first().is_some_and(|arg| !arg.required) { + if is_last + && !command.args.is_empty() + && command.args.first().is_some_and(|arg| !arg.required) + { node.flags |= CommandNodeFlag::Executable.bitmask(); } @@ -68,7 +66,9 @@ impl CommandGraph { self.node_to_indices.insert(part.to_string(), node_index); self.root_node.children.push(node_index); - self.nodes[current_node_index as usize].children.push(node_index); + self.nodes[current_node_index as usize] + .children + .push(node_index); current_node_index = node_index; } } @@ -90,7 +90,9 @@ impl CommandGraph { let arg_node_index = self.nodes.len() as u32; self.nodes.push(arg_node); - self.nodes[current_node_index as usize].children.push(arg_node_index); + self.nodes[current_node_index as usize] + .children + .push(arg_node_index); } } } diff --git a/src/lib/commands/src/graph/node.rs b/src/lib/commands/src/graph/node.rs index a2d6a66d..3a0f230b 100644 --- a/src/lib/commands/src/graph/node.rs +++ b/src/lib/commands/src/graph/node.rs @@ -24,7 +24,7 @@ pub enum CommandNodeFlag { NodeType(CommandNodeType), Executable, HasRedirect, - HasSuggestionsType + HasSuggestionsType, } impl CommandNodeFlag { @@ -56,7 +56,7 @@ pub struct CommandNode { pub name: Option, pub parser_id: Option, pub properties: Option, - pub suggestions_type: Option + pub suggestions_type: Option, } // We want to display the actual flags and not the encoded value @@ -162,6 +162,5 @@ pub enum MinecraftCommandParser { TemplateMirror, TemplateRotation, Heightmap, - UUID + UUID, } - diff --git a/src/lib/commands/src/lib.rs b/src/lib/commands/src/lib.rs index 537ca83b..200b97d5 100644 --- a/src/lib/commands/src/lib.rs +++ b/src/lib/commands/src/lib.rs @@ -13,9 +13,9 @@ use input::CommandInput; pub mod arg; pub mod ctx; pub mod errors; +pub mod graph; pub mod infrastructure; pub mod input; -pub mod graph; #[cfg(test)] mod tests; diff --git a/src/lib/commands/src/tests.rs b/src/lib/commands/src/tests.rs index 9256cc15..24287bef 100644 --- a/src/lib/commands/src/tests.rs +++ b/src/lib/commands/src/tests.rs @@ -13,7 +13,13 @@ use crate::{ string::{GreedyStringParser, QuotedStringParser}, }, CommandArgument, - }, ctx::CommandContext, executor, graph::{node::CommandNodeType, CommandGraph}, infrastructure::{find_command, register_command}, input::CommandInput, Command, CommandResult + }, + ctx::CommandContext, + executor, + graph::{node::CommandNodeType, CommandGraph}, + infrastructure::{find_command, register_command}, + input::CommandInput, + Command, CommandResult, }; async fn state() -> GlobalState { @@ -137,4 +143,3 @@ async fn graph_test() { assert!(arg_node.is_executable()); assert!(!literal_node.is_executable()); } - From 37c5fe67e83649ebcdf13bcbb9b0df5f4b2717d9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nils=20Kr=C3=BCger?= Date: Sat, 28 Dec 2024 23:41:14 +0100 Subject: [PATCH 16/32] refactor(commands): move command handling to events in bin --- src/bin/Cargo.toml | 1 + src/bin/src/packet_handlers/mod.rs | 1 + src/lib/commands/Cargo.toml | 3 +- src/lib/net/Cargo.toml | 1 - src/lib/net/src/packets/incoming/command.rs | 84 +++++---------------- 5 files changed, 21 insertions(+), 69 deletions(-) diff --git a/src/bin/Cargo.toml b/src/bin/Cargo.toml index 91e3a3a7..892541c4 100644 --- a/src/bin/Cargo.toml +++ b/src/bin/Cargo.toml @@ -28,6 +28,7 @@ ferrumc-general-purpose = { workspace = true } ferrumc-state = { workspace = true } ferrumc-commands = { workspace = true } ferrumc-default-commands = { workspace = true } +ferrumc-text = { workspace = true } ctor = { workspace = true } parking_lot = { workspace = true } diff --git a/src/bin/src/packet_handlers/mod.rs b/src/bin/src/packet_handlers/mod.rs index 32983a54..81aa4141 100644 --- a/src/bin/src/packet_handlers/mod.rs +++ b/src/bin/src/packet_handlers/mod.rs @@ -2,3 +2,4 @@ mod handshake; mod login_process; mod tick_handler; mod transform; +mod commands; diff --git a/src/lib/commands/Cargo.toml b/src/lib/commands/Cargo.toml index 25a3e514..76948041 100644 --- a/src/lib/commands/Cargo.toml +++ b/src/lib/commands/Cargo.toml @@ -11,9 +11,10 @@ tokio = { workspace = true } ferrumc-text = { workspace = true } ferrumc-state = { workspace = true } enum-ordinalize = { workspace = true } +ferrumc-macros = { workspace = true } +ferrumc-net = { workspace = true } [dev-dependencies] # Needed for the ServerState mock... :concern: ferrumc-ecs = { workspace = true } -ferrumc-macros = { workspace = true } ferrumc-world = { workspace = true } ctor = { workspace = true } diff --git a/src/lib/net/Cargo.toml b/src/lib/net/Cargo.toml index daa37386..dd1ad654 100644 --- a/src/lib/net/Cargo.toml +++ b/src/lib/net/Cargo.toml @@ -9,7 +9,6 @@ thiserror = { workspace = true } ferrumc-net-encryption = { workspace = true } ferrumc-net-codec = { workspace = true } ferrumc-macros = { workspace = true } -ferrumc-commands = { workspace = true } ferrumc-config = { workspace = true } ferrumc-ecs = { workspace = true } ferrumc-events = { workspace = true } diff --git a/src/lib/net/src/packets/incoming/command.rs b/src/lib/net/src/packets/incoming/command.rs index 2ebb8ba6..72a1636a 100644 --- a/src/lib/net/src/packets/incoming/command.rs +++ b/src/lib/net/src/packets/incoming/command.rs @@ -1,14 +1,11 @@ -use std::sync::{Arc, Mutex}; +use std::sync::Arc; -use ferrumc_commands::{ctx::CommandContext, infrastructure::find_command, input::CommandInput}; -use ferrumc_macros::{packet, NetDecode}; -use ferrumc_net_codec::encode::NetEncodeOpts; +use ferrumc_events::infrastructure::Event; +use ferrumc_macros::{packet, Event, NetDecode}; use ferrumc_state::ServerState; -use ferrumc_text::{NamedColor, TextComponentBuilder}; use crate::{ - connection::StreamWriter, - packets::{outgoing::system_message::SystemMessagePacket, IncomingPacket}, + packets::IncomingPacket, NetResult, }; @@ -18,67 +15,20 @@ pub struct ChatCommandPacket { command: String, } -impl IncomingPacket for ChatCommandPacket { - async fn handle(self, conn_id: usize, state: Arc) -> NetResult<()> { - let mut writer = state.universe.get_mut::(conn_id)?; - - let command = find_command(self.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(()); - } - - let command = command.unwrap(); - - let input = &self - .command - .strip_prefix(command.name) - .unwrap_or(&self.command); - let input = CommandInput::of(input.to_string()); - let ctx = CommandContext::new(input.clone(), command.clone(), state.clone(), conn_id); - if let Err(err) = command.validate(&ctx, &Arc::new(Mutex::new(input))) { - writer - .send_packet( - &SystemMessagePacket::new( - TextComponentBuilder::new("invalid args: ") - .extra(err) - .color(NamedColor::Red) - .build(), - false, - ), - &NetEncodeOpts::WithLength, - ) - .await?; - return Ok(()); - } +#[derive(Event)] +pub struct CommandDispatchEvent { + pub command: String, + pub conn_id: usize, +} - 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::(conn_id)?; - writer - .send_packet( - &SystemMessagePacket::new( - TextComponentBuilder::new("command error: ") - .extra(err) - .color(NamedColor::Red) - .build(), - false, - ), - &NetEncodeOpts::WithLength, - ) - .await?; - }; +impl CommandDispatchEvent { + pub fn new(command: String, conn_id: usize) -> Self { + Self { command, conn_id } + } +} - Ok(()) +impl IncomingPacket for ChatCommandPacket { + async fn handle(self, conn_id: usize, state: Arc) -> NetResult<()> { + CommandDispatchEvent::trigger(CommandDispatchEvent::new(self.command, conn_id), state).await } } From 0e6e536ddda8797cb9a9f4080d56607a4eb24d78 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nils=20Kr=C3=BCger?= Date: Sun, 29 Dec 2024 02:12:06 +0100 Subject: [PATCH 17/32] feat(commands): working command node sending --- src/bin/src/packet_handlers/login_process.rs | 6 + src/bin/src/packet_handlers/mod.rs | 2 +- src/lib/commands/Cargo.toml | 4 + src/lib/commands/src/graph/mod.rs | 200 ++++++++++++++++-- src/lib/commands/src/graph/node.rs | 17 +- src/lib/commands/src/infrastructure.rs | 41 ++-- src/lib/default_commands/src/lib.rs | 1 + src/lib/default_commands/src/nested.rs | 17 ++ src/lib/derive_macros/src/commands/mod.rs | 2 +- .../src/net_types/length_prefixed_vec.rs | 7 +- src/lib/net/src/packets/incoming/command.rs | 5 +- 11 files changed, 250 insertions(+), 52 deletions(-) create mode 100644 src/lib/default_commands/src/nested.rs diff --git a/src/bin/src/packet_handlers/login_process.rs b/src/bin/src/packet_handlers/login_process.rs index c26609aa..cdc713b7 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_core::identity::player_identity::PlayerIdentity; use ferrumc_core::transform::grounded::OnGround; use ferrumc_core::transform::position::Position; @@ -168,6 +169,11 @@ async fn handle_ack_finish_configuration( &NetEncodeOpts::WithLength, ) .await?; + println!("{:#?}", CommandsPacket::create()); + writer + .send_packet(&CommandsPacket::create(), &NetEncodeOpts::WithLength) + .await?; + send_keep_alive(conn_id, state, &mut writer).await?; Ok(ack_finish_configuration_event) diff --git a/src/bin/src/packet_handlers/mod.rs b/src/bin/src/packet_handlers/mod.rs index 81aa4141..b6362dcc 100644 --- a/src/bin/src/packet_handlers/mod.rs +++ b/src/bin/src/packet_handlers/mod.rs @@ -1,5 +1,5 @@ +mod commands; mod handshake; mod login_process; mod tick_handler; mod transform; -mod commands; diff --git a/src/lib/commands/Cargo.toml b/src/lib/commands/Cargo.toml index 76948041..d3c2cc82 100644 --- a/src/lib/commands/Cargo.toml +++ b/src/lib/commands/Cargo.toml @@ -12,7 +12,11 @@ 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 } diff --git a/src/lib/commands/src/graph/mod.rs b/src/lib/commands/src/graph/mod.rs index 2ade3c2c..8b705c1c 100644 --- a/src/lib/commands/src/graph/mod.rs +++ b/src/lib/commands/src/graph/mod.rs @@ -1,13 +1,18 @@ -use std::collections::HashMap; use std::sync::Arc; +use std::{collections::HashMap, io::Write}; -use node::{CommandNode, CommandNodeFlag, CommandNodeType}; +use enum_ordinalize::Ordinalize; +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, MinecraftCommandParser}; +use crate::infrastructure::get_graph; use crate::Command; pub mod node; -#[derive(Clone, Debug, PartialEq)] +#[derive(Clone, Debug)] pub struct CommandGraph { pub root_node: CommandNode, pub nodes: Vec, @@ -18,7 +23,7 @@ impl Default for CommandGraph { fn default() -> Self { let root_node = CommandNode { flags: CommandNodeFlag::NodeType(CommandNodeType::Root).bitmask(), - children: Vec::new(), + children: LengthPrefixedVec::new(Vec::new()), redirect_node: None, name: None, parser_id: None, @@ -45,8 +50,9 @@ impl CommandGraph { current_node_index = child_index; } else { let mut node = CommandNode { - flags: CommandNodeFlag::NodeType(CommandNodeType::Literal).bitmask(), - children: Vec::new(), + flags: CommandNodeFlag::NodeType(CommandNodeType::Literal).bitmask() + | CommandNodeFlag::Executable.bitmask(), + children: LengthPrefixedVec::new(Vec::new()), redirect_node: None, name: Some(part.to_string()), parser_id: None, @@ -64,35 +70,191 @@ impl CommandGraph { let node_index = self.nodes.len() as u32; self.nodes.push(node); self.node_to_indices.insert(part.to_string(), node_index); - self.root_node.children.push(node_index); + let node_index_varint = VarInt::new(node_index as i32); - self.nodes[current_node_index as usize] - .children - .push(node_index); + self.root_node.children.push(node_index_varint.clone()); + + let node = self.nodes.get_mut(current_node_index as usize).unwrap(); + node.children.push(node_index_varint); current_node_index = node_index; } } for arg in &command.args { - let mut arg_node = CommandNode { + let arg_node = CommandNode { flags: CommandNodeFlag::NodeType(CommandNodeType::Argument).bitmask(), - children: Vec::new(), + children: LengthPrefixedVec::new(Vec::new()), redirect_node: None, name: Some(arg.name.clone()), - parser_id: None, - properties: None, + parser_id: Some(VarInt::new(MinecraftCommandParser::String.ordinal() as i32)), + properties: Some(node::CommandNodeProperties::String { + behavior: VarInt::new(0), + }), suggestions_type: None, }; - if arg.required { - arg_node.flags |= CommandNodeFlag::Executable.bitmask(); - } - let arg_node_index = self.nodes.len() as u32; self.nodes.push(arg_node); self.nodes[current_node_index as usize] .children - .push(arg_node_index); + .push(VarInt::new(arg_node_index as i32)); + } + } + + 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 = 0x11)] +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 index 3a0f230b..fd3b101b 100644 --- a/src/lib/commands/src/graph/node.rs +++ b/src/lib/commands/src/graph/node.rs @@ -1,6 +1,8 @@ -use std::{collections::HashMap, fmt}; +use std::{collections::HashMap, fmt, io::Write}; use enum_ordinalize::Ordinalize; +use ferrumc_macros::NetEncode; +use ferrumc_net_codec::net_types::{length_prefixed_vec::LengthPrefixedVec, var_int::VarInt}; #[derive(Clone, Debug, PartialEq)] pub enum CommandNodeType { @@ -40,21 +42,21 @@ impl CommandNodeFlag { } } -#[derive(Clone, Debug, PartialEq)] +#[derive(Clone, Debug, PartialEq, NetEncode)] pub enum CommandNodeProperties { IntRange { min: i32, max: i32 }, FloatRange { min: f32, max: f32 }, - String { max_length: u32 }, + String { behavior: VarInt }, Other(HashMap), } -#[derive(Clone, PartialEq)] +#[derive(Clone, NetEncode)] pub struct CommandNode { pub flags: u8, - pub children: Vec, - pub redirect_node: Option, + pub children: LengthPrefixedVec, + pub redirect_node: Option, pub name: Option, - pub parser_id: Option, + pub parser_id: Option, pub properties: Option, pub suggestions_type: Option, } @@ -78,6 +80,7 @@ impl fmt::Debug for CommandNode { .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) diff --git a/src/lib/commands/src/infrastructure.rs b/src/lib/commands/src/infrastructure.rs index da2b40a1..29fb6f50 100644 --- a/src/lib/commands/src/infrastructure.rs +++ b/src/lib/commands/src/infrastructure.rs @@ -1,12 +1,25 @@ use dashmap::DashMap; -use std::sync::{Arc, LazyLock}; +use std::sync::{Arc, LazyLock, RwLock}; -use crate::Command; +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); + 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> { @@ -14,21 +27,11 @@ pub fn get_command_by_name(name: &str) -> Option> { } pub fn find_command(input: &str) -> Option> { - let mut command = None; - let mut current = input; - - while !current.is_empty() { - command = get_command_by_name(current); - if command.is_some() { - break; - } - - if let Some(pos) = current.rfind(' ') { - current = ¤t[..pos]; - } else { - current = ""; - } + let graph = get_graph(); + let name = graph.find_command_by_input(input); + if let Some(name) = name { + get_command_by_name(&name) + } else { + None } - - command } diff --git a/src/lib/default_commands/src/lib.rs b/src/lib/default_commands/src/lib.rs index 0bf1d5da..68dd4b81 100644 --- a/src/lib/default_commands/src/lib.rs +++ b/src/lib/default_commands/src/lib.rs @@ -1,3 +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..fbcbb301 --- /dev/null +++ b/src/lib/default_commands/src/nested.rs @@ -0,0 +1,17 @@ +use std::sync::Arc; + +use ferrumc_commands::{ctx::CommandContext, CommandResult, executor, infrastructure::register_command, Command, arg::{parser::string::GreedyStringParser, CommandArgument}}; +use ferrumc_macros::{arg, command}; + +#[command("nested")] +async fn root(ctx: Arc) -> CommandResult { + println!("Executed root"); + Ok(()) +} + +#[arg("message", GreedyStringParser)] +#[command("nested abc")] +async fn abc(ctx: Arc) -> CommandResult { + println!("Executed abc with message {}", ctx.arg::("message")); + Ok(()) +} diff --git a/src/lib/derive_macros/src/commands/mod.rs b/src/lib/derive_macros/src/commands/mod.rs index 7bb44e60..77eeb6fd 100644 --- a/src/lib/derive_macros/src/commands/mod.rs +++ b/src/lib/derive_macros/src/commands/mod.rs @@ -85,7 +85,7 @@ pub fn command(attr: TokenStream, item: TokenStream) -> TokenStream { .collect::>(); let arg_required = args.iter().map(|arg| arg.required).collect::>(); - let register_fn_name = format_ident!("__register_{}_command", command_name); + let register_fn_name = format_ident!("__register_{}_command", command_name.replace(" ", "_")); let expanded = quote! { #[ctor::ctor] 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 2f0edabd..6a7482f7 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, @@ -17,6 +17,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/command.rs b/src/lib/net/src/packets/incoming/command.rs index 72a1636a..8fa36837 100644 --- a/src/lib/net/src/packets/incoming/command.rs +++ b/src/lib/net/src/packets/incoming/command.rs @@ -4,10 +4,7 @@ use ferrumc_events::infrastructure::Event; use ferrumc_macros::{packet, Event, NetDecode}; use ferrumc_state::ServerState; -use crate::{ - packets::IncomingPacket, - NetResult, -}; +use crate::{packets::IncomingPacket, NetResult}; #[derive(NetDecode, Debug, Clone)] #[packet(packet_id = 0x04, state = "play")] From b7840dd75859cfc38d57d1d5a4fe82c229345735 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nils=20Kr=C3=BCger?= Date: Sun, 29 Dec 2024 02:37:45 +0100 Subject: [PATCH 18/32] fix(commands): CommandInput::remaining_input doing wrong things, cargo fmt, fix some graph stuff --- src/bin/src/packet_handlers/login_process.rs | 5 ++++- src/lib/commands/src/arg/parser/string.rs | 14 ++++++++++++-- src/lib/commands/src/graph/mod.rs | 8 ++++---- src/lib/commands/src/infrastructure.rs | 2 +- src/lib/commands/src/input.rs | 2 +- src/lib/commands/src/tests.rs | 4 ++-- src/lib/default_commands/src/nested.rs | 10 ++++++++-- .../codec/src/net_types/length_prefixed_vec.rs | 2 +- 8 files changed, 33 insertions(+), 14 deletions(-) diff --git a/src/bin/src/packet_handlers/login_process.rs b/src/bin/src/packet_handlers/login_process.rs index cdc713b7..ac8637cc 100644 --- a/src/bin/src/packet_handlers/login_process.rs +++ b/src/bin/src/packet_handlers/login_process.rs @@ -169,7 +169,10 @@ async fn handle_ack_finish_configuration( &NetEncodeOpts::WithLength, ) .await?; - println!("{:#?}", CommandsPacket::create()); + trace!( + "Sending command graph: {:#?}", + ferrumc_commands::infrastructure::get_graph() + ); writer .send_packet(&CommandsPacket::create(), &NetEncodeOpts::WithLength) .await?; diff --git a/src/lib/commands/src/arg/parser/string.rs b/src/lib/commands/src/arg/parser/string.rs index e7f52bde..d01dbaff 100644 --- a/src/lib/commands/src/arg/parser/string.rs +++ b/src/lib/commands/src/arg/parser/string.rs @@ -8,7 +8,12 @@ pub struct SingleStringParser; impl ArgumentParser for SingleStringParser { fn parse(&self, _ctx: Arc, input: Arc>) -> ParserResult { - Ok(Box::new(input.lock().unwrap().read_string())) + 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 @@ -23,10 +28,15 @@ 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.lock().unwrap().read_string_skip_whitespace(false); + let token = input.read_string_skip_whitespace(false); if token.is_empty() { break; diff --git a/src/lib/commands/src/graph/mod.rs b/src/lib/commands/src/graph/mod.rs index 8b705c1c..869ed7bb 100644 --- a/src/lib/commands/src/graph/mod.rs +++ b/src/lib/commands/src/graph/mod.rs @@ -88,7 +88,7 @@ impl CommandGraph { name: Some(arg.name.clone()), parser_id: Some(VarInt::new(MinecraftCommandParser::String.ordinal() as i32)), properties: Some(node::CommandNodeProperties::String { - behavior: VarInt::new(0), + behavior: VarInt::new(2), }), suggestions_type: None, }; @@ -233,9 +233,9 @@ impl CommandGraph { 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) - }) + matches + .first() + .map(|(node_index, _remaining)| self.get_command_name(*node_index)) } } diff --git a/src/lib/commands/src/infrastructure.rs b/src/lib/commands/src/infrastructure.rs index 29fb6f50..398216f7 100644 --- a/src/lib/commands/src/infrastructure.rs +++ b/src/lib/commands/src/infrastructure.rs @@ -1,7 +1,7 @@ use dashmap::DashMap; use std::sync::{Arc, LazyLock, RwLock}; -use crate::{graph::{CommandGraph}, Command}; +use crate::{graph::CommandGraph, Command}; static COMMANDS: LazyLock>> = LazyLock::new(DashMap::new); static COMMAND_GRAPH: LazyLock> = diff --git a/src/lib/commands/src/input.rs b/src/lib/commands/src/input.rs index f8b8138e..dfd64942 100644 --- a/src/lib/commands/src/input.rs +++ b/src/lib/commands/src/input.rs @@ -46,7 +46,7 @@ impl CommandInput { } } pub fn remaining_input(&self) -> String { - self.input[..self.cursor as usize].to_string() + self.input[self.cursor as usize..].to_string() } pub fn peek_string_chars(&self, chars: u32) -> String { let remaining = self.remaining_input(); diff --git a/src/lib/commands/src/tests.rs b/src/lib/commands/src/tests.rs index 24287bef..4ad2c7b9 100644 --- a/src/lib/commands/src/tests.rs +++ b/src/lib/commands/src/tests.rs @@ -140,6 +140,6 @@ async fn graph_test() { 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()); + assert!(!arg_node.is_executable()); + assert!(literal_node.is_executable()); } diff --git a/src/lib/default_commands/src/nested.rs b/src/lib/default_commands/src/nested.rs index fbcbb301..4e4cfab3 100644 --- a/src/lib/default_commands/src/nested.rs +++ b/src/lib/default_commands/src/nested.rs @@ -1,10 +1,16 @@ use std::sync::Arc; -use ferrumc_commands::{ctx::CommandContext, CommandResult, executor, infrastructure::register_command, Command, arg::{parser::string::GreedyStringParser, CommandArgument}}; +use ferrumc_commands::{ + arg::{parser::string::GreedyStringParser, CommandArgument}, + ctx::CommandContext, + executor, + infrastructure::register_command, + Command, CommandResult, +}; use ferrumc_macros::{arg, command}; #[command("nested")] -async fn root(ctx: Arc) -> CommandResult { +async fn root(_ctx: Arc) -> CommandResult { println!("Executed root"); Ok(()) } 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 6a7482f7..c7f0eec1 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 @@ -17,7 +17,7 @@ impl LengthPrefixedVec { data, } } - + pub fn push(&mut self, data: T) { self.data.push(data); self.length = VarInt::new(self.length.val + 1); From 49c38b5d85f197235bbf7a8820ce42f276361616 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nils=20Kr=C3=BCger?= Date: Sun, 29 Dec 2024 02:40:34 +0100 Subject: [PATCH 19/32] fix: packet_handlers::commands being in muh global gitignore --- src/bin/src/packet_handlers/commands.rs | 82 +++++++++++++++++++++++++ 1 file changed, 82 insertions(+) create mode 100644 src/bin/src/packet_handlers/commands.rs 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) +} From f7971f146e86ebd69e0927e1c5041480d3d4ffb1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nils=20Kr=C3=BCger?= Date: Sun, 29 Dec 2024 02:42:41 +0100 Subject: [PATCH 20/32] style(commands): make clippy happy --- src/lib/commands/src/graph/mod.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/lib/commands/src/graph/mod.rs b/src/lib/commands/src/graph/mod.rs index 869ed7bb..0ebb6271 100644 --- a/src/lib/commands/src/graph/mod.rs +++ b/src/lib/commands/src/graph/mod.rs @@ -162,7 +162,7 @@ impl CommandGraph { 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() + remaining_input[name.len()..].trim_start() } else { "" }; @@ -183,7 +183,7 @@ impl CommandGraph { // 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() + remaining_input[input_words[0].len()..].trim_start() } else { "" }; From 5080189799dbf5684801612fc25a22b01389e8a5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nils=20Kr=C3=BCger?= Date: Sun, 29 Dec 2024 12:51:24 +0100 Subject: [PATCH 21/32] feat(commands): vanilla arg types and props, fix some graph issues --- src/lib/commands/src/arg/parser/int.rs | 15 ++- src/lib/commands/src/arg/parser/mod.rs | 2 + src/lib/commands/src/arg/parser/string.rs | 107 +++++++++++------ .../commands/src/arg/parser/vanilla/float.rs | 42 +++++++ .../commands/src/arg/parser/vanilla/int.rs | 42 +++++++ .../commands/src/arg/parser/vanilla/long.rs | 42 +++++++ .../commands/src/arg/parser/vanilla/mod.rs | 112 ++++++++++++++++++ .../commands/src/arg/parser/vanilla/string.rs | 32 +++++ src/lib/commands/src/ctx.rs | 19 +++ src/lib/commands/src/graph/mod.rs | 44 ++++--- src/lib/commands/src/graph/node.rs | 71 +---------- src/lib/default_commands/Cargo.toml | 2 - src/lib/default_commands/src/echo.rs | 47 ++------ src/lib/default_commands/src/nested.rs | 33 +++++- 14 files changed, 446 insertions(+), 164 deletions(-) create mode 100644 src/lib/commands/src/arg/parser/vanilla/float.rs create mode 100644 src/lib/commands/src/arg/parser/vanilla/int.rs create mode 100644 src/lib/commands/src/arg/parser/vanilla/long.rs create mode 100644 src/lib/commands/src/arg/parser/vanilla/mod.rs create mode 100644 src/lib/commands/src/arg/parser/vanilla/string.rs diff --git a/src/lib/commands/src/arg/parser/int.rs b/src/lib/commands/src/arg/parser/int.rs index 8d4a1854..f22074f5 100644 --- a/src/lib/commands/src/arg/parser/int.rs +++ b/src/lib/commands/src/arg/parser/int.rs @@ -2,7 +2,13 @@ use std::sync::{Arc, Mutex}; use crate::{ctx::CommandContext, input::CommandInput, ParserResult}; -use super::{utils::error, ArgumentParser}; +use super::{ + utils::error, + vanilla::{ + int::IntParserFlags, MinecraftArgument, MinecraftArgumentProperties, MinecraftArgumentType, + }, + ArgumentParser, +}; pub struct IntParser; @@ -22,4 +28,11 @@ impl ArgumentParser for IntParser { { IntParser } + + fn vanilla(&self) -> MinecraftArgument { + MinecraftArgument { + argument_type: MinecraftArgumentType::Int, + props: MinecraftArgumentProperties::Int(IntParserFlags::default()), + } + } } diff --git a/src/lib/commands/src/arg/parser/mod.rs b/src/lib/commands/src/arg/parser/mod.rs index 1f2d62fd..192c713f 100644 --- a/src/lib/commands/src/arg/parser/mod.rs +++ b/src/lib/commands/src/arg/parser/mod.rs @@ -5,10 +5,12 @@ 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, context: Arc, input: Arc>) -> ParserResult; 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 index d01dbaff..983425c8 100644 --- a/src/lib/commands/src/arg/parser/string.rs +++ b/src/lib/commands/src/arg/parser/string.rs @@ -2,7 +2,14 @@ use std::sync::{Arc, Mutex}; use crate::{ctx::CommandContext, input::CommandInput, ParserResult}; -use super::{utils::parser_error, ArgumentParser}; +use super::{ + utils::parser_error, + vanilla::{ + string::StringParsingBehavior, MinecraftArgument, MinecraftArgumentProperties, + MinecraftArgumentType, + }, + ArgumentParser, +}; pub struct SingleStringParser; @@ -22,6 +29,13 @@ impl ArgumentParser for SingleStringParser { { SingleStringParser } + + fn vanilla(&self) -> MinecraftArgument { + MinecraftArgument { + argument_type: MinecraftArgumentType::String, + props: MinecraftArgumentProperties::String(StringParsingBehavior::default()), + } + } } pub struct GreedyStringParser; @@ -57,56 +71,68 @@ impl ArgumentParser for GreedyStringParser { { GreedyStringParser } + + fn vanilla(&self) -> MinecraftArgument { + MinecraftArgument { + argument_type: MinecraftArgumentType::String, + props: MinecraftArgumentProperties::String(StringParsingBehavior::Greedy), + } + } } 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 input.peek() != Some('"') { - return Err(parser_error("expected opening quote")); - } - - input.read(1); - - 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); + // 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), } - } - escaped = false; - } else { - match c { - '"' => return Ok(Box::new(result)), - '\\' => escaped = true, - _ => result.push(c), } } } } - } - Err(parser_error("unterminated quoted string")) + 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 @@ -115,4 +141,11 @@ impl ArgumentParser for QuotedStringParser { { QuotedStringParser } + + fn vanilla(&self) -> MinecraftArgument { + MinecraftArgument { + argument_type: MinecraftArgumentType::String, + props: MinecraftArgumentProperties::String(StringParsingBehavior::Quotable), + } + } } 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 index fa71a3db..4bb7ceea 100644 --- a/src/lib/commands/src/ctx.rs +++ b/src/lib/commands/src/ctx.rs @@ -3,7 +3,12 @@ use std::{ sync::{Arc, Mutex}, }; +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; use crate::{input::CommandInput, Command}; @@ -47,4 +52,18 @@ impl CommandContext { todo!(); } } + + pub async fn reply(&self, text: TextComponent) -> NetResult<()> { + let mut stream_writer = self + .state + .universe + .get_mut::(self.connection_id)?; + + stream_writer + .send_packet( + &SystemMessagePacket::new(text, false), + &NetEncodeOpts::WithLength, + ) + .await + } } diff --git a/src/lib/commands/src/graph/mod.rs b/src/lib/commands/src/graph/mod.rs index 0ebb6271..18c7d310 100644 --- a/src/lib/commands/src/graph/mod.rs +++ b/src/lib/commands/src/graph/mod.rs @@ -1,11 +1,10 @@ use std::sync::Arc; use std::{collections::HashMap, io::Write}; -use enum_ordinalize::Ordinalize; 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, MinecraftCommandParser}; +use node::{CommandNode, CommandNodeFlag, CommandNodeType}; use crate::infrastructure::get_graph; use crate::Command; @@ -50,8 +49,7 @@ impl CommandGraph { current_node_index = child_index; } else { let mut node = CommandNode { - flags: CommandNodeFlag::NodeType(CommandNodeType::Literal).bitmask() - | CommandNodeFlag::Executable.bitmask(), + flags: CommandNodeFlag::NodeType(CommandNodeType::Literal).bitmask(), children: LengthPrefixedVec::new(Vec::new()), redirect_node: None, name: Some(part.to_string()), @@ -61,8 +59,8 @@ impl CommandGraph { }; if is_last - && !command.args.is_empty() - && command.args.first().is_some_and(|arg| !arg.required) + && (command.args.is_empty() + || command.args.first().is_some_and(|arg| !arg.required)) { node.flags |= CommandNodeFlag::Executable.bitmask(); } @@ -70,34 +68,46 @@ impl CommandGraph { let node_index = self.nodes.len() as u32; self.nodes.push(node); self.node_to_indices.insert(part.to_string(), node_index); - let node_index_varint = VarInt::new(node_index as i32); - self.root_node.children.push(node_index_varint.clone()); + 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)); + } - let node = self.nodes.get_mut(current_node_index as usize).unwrap(); - node.children.push(node_index_varint); current_node_index = node_index; } } - for arg in &command.args { - let arg_node = CommandNode { + 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(VarInt::new(MinecraftCommandParser::String.ordinal() as i32)), - properties: Some(node::CommandNodeProperties::String { - behavior: VarInt::new(2), - }), + 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[current_node_index as usize] + + self.nodes[prev_node_index as usize] .children .push(VarInt::new(arg_node_index as i32)); + + prev_node_index = arg_node_index; } } diff --git a/src/lib/commands/src/graph/node.rs b/src/lib/commands/src/graph/node.rs index fd3b101b..e3204efe 100644 --- a/src/lib/commands/src/graph/node.rs +++ b/src/lib/commands/src/graph/node.rs @@ -1,9 +1,10 @@ -use std::{collections::HashMap, fmt, io::Write}; +use std::{fmt, io::Write}; -use enum_ordinalize::Ordinalize; 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, @@ -42,22 +43,14 @@ impl CommandNodeFlag { } } -#[derive(Clone, Debug, PartialEq, NetEncode)] -pub enum CommandNodeProperties { - IntRange { min: i32, max: i32 }, - FloatRange { min: f32, max: f32 }, - String { behavior: VarInt }, - Other(HashMap), -} - #[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 parser_id: Option, + pub properties: Option, pub suggestions_type: Option, } @@ -113,57 +106,3 @@ impl CommandNode { self.flags & 0x10 != 0 } } - -#[derive(Clone, Debug, PartialEq, Ordinalize)] -pub enum MinecraftCommandParser { - 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, -} diff --git a/src/lib/default_commands/Cargo.toml b/src/lib/default_commands/Cargo.toml index 50515c4c..7796c8ff 100644 --- a/src/lib/default_commands/Cargo.toml +++ b/src/lib/default_commands/Cargo.toml @@ -5,9 +5,7 @@ edition = "2021" [dependencies] ferrumc-commands = { workspace = true } -ferrumc-net = { workspace = true } ferrumc-ecs = { workspace = true } -ferrumc-net-codec = { workspace = true } ferrumc-macros = { workspace = true } ferrumc-text = { workspace = true } ctor = { workspace = true } diff --git a/src/lib/default_commands/src/echo.rs b/src/lib/default_commands/src/echo.rs index 131c8dcd..f07c6068 100644 --- a/src/lib/default_commands/src/echo.rs +++ b/src/lib/default_commands/src/echo.rs @@ -5,46 +5,21 @@ use ferrumc_commands::{ infrastructure::register_command, Command, CommandResult, }; use ferrumc_macros::{arg, command}; -use ferrumc_net::connection::StreamWriter; -use ferrumc_net::packets::outgoing::system_message::SystemMessagePacket; -use ferrumc_net_codec::encode::NetEncodeOpts; use ferrumc_text::{NamedColor, TextComponentBuilder}; -use tracing::error; #[arg("message", GreedyStringParser)] #[command("echo")] async fn echo(ctx: Arc) -> CommandResult { let message = ctx.arg::("message"); - let mut writer = match ctx - .state - .universe - .get_mut::(ctx.connection_id) - { - Ok(writer) => writer, - Err(err) => { - error!( - "failed retrieving stream writer for conn id {}: {err}", - ctx.connection_id - ); - return Ok(()); - } - }; - - if let Err(err) = writer - .send_packet( - &SystemMessagePacket::new( - TextComponentBuilder::new(message) - .color(NamedColor::Green) - .build(), - false, - ), - &NetEncodeOpts::WithLength, - ) - .await - { - error!("failed sending packet: {err}"); - return Ok(()); - } - - Ok(()) + ctx.reply( + TextComponentBuilder::new(message) + .color(NamedColor::Green) + .build(), + ) + .await + .map_err(|err| { + TextComponentBuilder::new(err.to_string()) + .color(NamedColor::Red) + .build() + }) } diff --git a/src/lib/default_commands/src/nested.rs b/src/lib/default_commands/src/nested.rs index 4e4cfab3..3a00136e 100644 --- a/src/lib/default_commands/src/nested.rs +++ b/src/lib/default_commands/src/nested.rs @@ -1,23 +1,46 @@ use std::sync::Arc; use ferrumc_commands::{ - arg::{parser::string::GreedyStringParser, CommandArgument}, + arg::{ + parser::{ + int::IntParser, + string::{QuotedStringParser, SingleStringParser}, + }, + CommandArgument, + }, ctx::CommandContext, executor, infrastructure::register_command, Command, CommandResult, }; use ferrumc_macros::{arg, command}; +use ferrumc_text::TextComponentBuilder; #[command("nested")] -async fn root(_ctx: Arc) -> CommandResult { - println!("Executed root"); +async fn root(ctx: Arc) -> CommandResult { + ctx.reply(TextComponentBuilder::new("Executed /nested").build()) + .await + .unwrap(); Ok(()) } -#[arg("message", GreedyStringParser)] +#[arg("message", QuotedStringParser)] +#[arg("word", SingleStringParser)] +#[arg("number", IntParser)] #[command("nested abc")] async fn abc(ctx: Arc) -> CommandResult { - println!("Executed abc with message {}", ctx.arg::("message")); + let message = ctx.arg::("message"); + let word = ctx.arg::("word"); + let number = ctx.arg::("number"); + + ctx.reply( + TextComponentBuilder::new(format!( + "Message: {message:?}, Word: {word:?}, Message: {number}" + )) + .build(), + ) + .await + .unwrap(); + Ok(()) } From 4870143a314eb0f2afcd8bf50bd3e1b1ad05e25b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nils=20Kr=C3=BCger?= Date: Sun, 29 Dec 2024 12:55:57 +0100 Subject: [PATCH 22/32] fix(commands): fix graph test being inverted --- src/lib/commands/src/tests.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/lib/commands/src/tests.rs b/src/lib/commands/src/tests.rs index 4ad2c7b9..24287bef 100644 --- a/src/lib/commands/src/tests.rs +++ b/src/lib/commands/src/tests.rs @@ -140,6 +140,6 @@ async fn graph_test() { 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()); + assert!(arg_node.is_executable()); + assert!(!literal_node.is_executable()); } From 5b737daba5a401bbb7891ddfc34d903b9193fb7f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nils=20Kr=C3=BCger?= Date: Sun, 29 Dec 2024 12:58:07 +0100 Subject: [PATCH 23/32] refactor(commands/int_parser): return i32 instead of u32 --- src/lib/commands/src/arg/parser/int.rs | 2 +- src/lib/commands/src/tests.rs | 2 +- src/lib/default_commands/src/nested.rs | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/lib/commands/src/arg/parser/int.rs b/src/lib/commands/src/arg/parser/int.rs index f22074f5..e8f4a715 100644 --- a/src/lib/commands/src/arg/parser/int.rs +++ b/src/lib/commands/src/arg/parser/int.rs @@ -16,7 +16,7 @@ impl ArgumentParser for IntParser { fn parse(&self, _ctx: Arc, input: Arc>) -> ParserResult { let token = input.lock().unwrap().read_string(); - match token.parse::() { + match token.parse::() { Ok(int) => Ok(Box::new(int)), Err(err) => Err(error(err)), } diff --git a/src/lib/commands/src/tests.rs b/src/lib/commands/src/tests.rs index 24287bef..709bc060 100644 --- a/src/lib/commands/src/tests.rs +++ b/src/lib/commands/src/tests.rs @@ -79,7 +79,7 @@ async fn arg_parse_test() { #[tokio::test] async fn parse_test() { async fn test_executor(ctx: Arc) -> CommandResult { - let num = ctx.arg::("number"); + let num = ctx.arg::("number"); assert_eq!(num.to_string(), ctx.input.lock().unwrap().input); Ok(()) } diff --git a/src/lib/default_commands/src/nested.rs b/src/lib/default_commands/src/nested.rs index 3a00136e..74487df4 100644 --- a/src/lib/default_commands/src/nested.rs +++ b/src/lib/default_commands/src/nested.rs @@ -31,11 +31,11 @@ async fn root(ctx: Arc) -> CommandResult { async fn abc(ctx: Arc) -> CommandResult { let message = ctx.arg::("message"); let word = ctx.arg::("word"); - let number = ctx.arg::("number"); + let number = ctx.arg::("number"); ctx.reply( TextComponentBuilder::new(format!( - "Message: {message:?}, Word: {word:?}, Message: {number}" + "Message: {message:?}, Word: {word:?}, Number: {number}" )) .build(), ) From b86d99af09fc290a1109217200711ec16274e9c2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nils=20Kr=C3=BCger?= Date: Sun, 29 Dec 2024 22:20:28 +0100 Subject: [PATCH 24/32] feat(macros/arg): parse parser as expr instead of ident allowing for fn calls + FQNs --- src/lib/default_commands/Cargo.toml | 1 + src/lib/default_commands/src/echo.rs | 9 ++++++--- src/lib/derive_macros/src/commands/mod.rs | 13 ++++++------- 3 files changed, 13 insertions(+), 10 deletions(-) diff --git a/src/lib/default_commands/Cargo.toml b/src/lib/default_commands/Cargo.toml index 7796c8ff..a637bac9 100644 --- a/src/lib/default_commands/Cargo.toml +++ b/src/lib/default_commands/Cargo.toml @@ -8,5 +8,6 @@ ferrumc-commands = { workspace = true } ferrumc-ecs = { workspace = true } ferrumc-macros = { workspace = true } ferrumc-text = { workspace = true } +ferrumc-core = { 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 index f07c6068..8b7aff81 100644 --- a/src/lib/default_commands/src/echo.rs +++ b/src/lib/default_commands/src/echo.rs @@ -1,18 +1,21 @@ use std::sync::Arc; use ferrumc_commands::{ - arg::parser::string::GreedyStringParser, arg::CommandArgument, ctx::CommandContext, executor, + arg::{parser::{string::GreedyStringParser, ArgumentParser}, CommandArgument}, ctx::CommandContext, executor, infrastructure::register_command, Command, CommandResult, }; +use ferrumc_core::identity::player_identity::PlayerIdentity; use ferrumc_macros::{arg, command}; use ferrumc_text::{NamedColor, TextComponentBuilder}; -#[arg("message", GreedyStringParser)] +#[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.reply( - TextComponentBuilder::new(message) + TextComponentBuilder::new(format!("{} said: {message}", identity.username)) .color(NamedColor::Green) .build(), ) diff --git a/src/lib/derive_macros/src/commands/mod.rs b/src/lib/derive_macros/src/commands/mod.rs index 77eeb6fd..d804b63d 100644 --- a/src/lib/derive_macros/src/commands/mod.rs +++ b/src/lib/derive_macros/src/commands/mod.rs @@ -1,13 +1,12 @@ use std::{ collections::HashMap, - sync::{Mutex, OnceLock}, + sync::{Arc, Mutex, OnceLock}, }; use proc_macro::TokenStream; -use quote::{format_ident, quote}; +use quote::{format_ident, quote, ToTokens}; use syn::{ - parse::Parse, parse::ParseStream, parse_macro_input, Ident, ItemFn, LitBool, LitStr, - Result as SynResult, Token, + parse::{Parse, ParseStream}, parse_macro_input, Expr, Ident, ItemFn, LitBool, LitStr, Result as SynResult, Token }; static PENDING_ARGS: OnceLock>>> = OnceLock::new(); @@ -33,7 +32,7 @@ impl Parse for ArgAttr { fn parse(input: ParseStream) -> SynResult { let name = input.parse::()?.value(); input.parse::()?; - let parser = input.parse::()?.to_string(); + let parser = input.parse::()?.to_token_stream().to_string(); let required = if input.peek(Token![,]) { input.parse::()?; @@ -81,8 +80,8 @@ pub fn command(attr: TokenStream, item: TokenStream) -> TokenStream { let arg_names = args.iter().map(|arg| &arg.name).collect::>(); let arg_parsers = args .iter() - .map(|arg| format_ident!("{}", arg.parser)) - .collect::>(); + .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(" ", "_")); From 1771a50c1db78a50f8da55a64bedc2966f715d6a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nils=20Kr=C3=BCger?= Date: Sun, 29 Dec 2024 22:30:07 +0100 Subject: [PATCH 25/32] cargo fmt --- src/lib/default_commands/src/echo.rs | 16 +++++++++++++--- src/lib/derive_macros/src/commands/mod.rs | 5 +++-- 2 files changed, 16 insertions(+), 5 deletions(-) diff --git a/src/lib/default_commands/src/echo.rs b/src/lib/default_commands/src/echo.rs index 8b7aff81..1f94ec58 100644 --- a/src/lib/default_commands/src/echo.rs +++ b/src/lib/default_commands/src/echo.rs @@ -1,8 +1,14 @@ use std::sync::Arc; use ferrumc_commands::{ - arg::{parser::{string::GreedyStringParser, ArgumentParser}, CommandArgument}, ctx::CommandContext, executor, - infrastructure::register_command, Command, CommandResult, + arg::{ + parser::{string::GreedyStringParser, ArgumentParser}, + CommandArgument, + }, + ctx::CommandContext, + executor, + infrastructure::register_command, + Command, CommandResult, }; use ferrumc_core::identity::player_identity::PlayerIdentity; use ferrumc_macros::{arg, command}; @@ -12,7 +18,11 @@ use ferrumc_text::{NamedColor, TextComponentBuilder}; #[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"); + let identity = ctx + .state + .universe + .get::(ctx.connection_id) + .expect("failed to get identity"); ctx.reply( TextComponentBuilder::new(format!("{} said: {message}", identity.username)) diff --git a/src/lib/derive_macros/src/commands/mod.rs b/src/lib/derive_macros/src/commands/mod.rs index d804b63d..c98fc544 100644 --- a/src/lib/derive_macros/src/commands/mod.rs +++ b/src/lib/derive_macros/src/commands/mod.rs @@ -1,12 +1,13 @@ use std::{ collections::HashMap, - sync::{Arc, Mutex, OnceLock}, + sync::{Mutex, OnceLock}, }; use proc_macro::TokenStream; use quote::{format_ident, quote, ToTokens}; use syn::{ - parse::{Parse, ParseStream}, parse_macro_input, Expr, Ident, ItemFn, LitBool, LitStr, Result as SynResult, Token + parse::{Parse, ParseStream}, + parse_macro_input, Expr, ItemFn, LitBool, LitStr, Result as SynResult, Token, }; static PENDING_ARGS: OnceLock>>> = OnceLock::new(); From 231b8fbb34197c9f3aab0304ba0b4907cf48a2dd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nils=20Kr=C3=BCger?= Date: Mon, 30 Dec 2024 00:32:58 +0100 Subject: [PATCH 26/32] feat: basic unsigned chat --- src/bin/src/packet_handlers/chat_message.rs | 24 +++++++++++ src/bin/src/packet_handlers/mod.rs | 1 + .../net/crates/codec/src/decode/primitives.rs | 18 ++++++++ .../net/src/packets/incoming/chat_message.rs | 41 +++++++++++++++++++ src/lib/net/src/packets/incoming/mod.rs | 1 + 5 files changed, 85 insertions(+) create mode 100644 src/bin/src/packet_handlers/chat_message.rs create mode 100644 src/lib/net/src/packets/incoming/chat_message.rs 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/mod.rs b/src/bin/src/packet_handlers/mod.rs index b6362dcc..59affd21 100644 --- a/src/bin/src/packet_handlers/mod.rs +++ b/src/bin/src/packet_handlers/mod.rs @@ -1,3 +1,4 @@ +mod chat_message; mod commands; mod handshake; mod login_process; diff --git a/src/lib/net/crates/codec/src/decode/primitives.rs b/src/lib/net/crates/codec/src/decode/primitives.rs index 3e00e794..a7b6c672 100644 --- a/src/lib/net/crates/codec/src/decode/primitives.rs +++ b/src/lib/net/crates/codec/src/decode/primitives.rs @@ -83,6 +83,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/src/packets/incoming/chat_message.rs b/src/lib/net/src/packets/incoming/chat_message.rs new file mode 100644 index 00000000..c073368c --- /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 = 0x06, 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/mod.rs b/src/lib/net/src/packets/incoming/mod.rs index b7994e38..609f328f 100644 --- a/src/lib/net/src/packets/incoming/mod.rs +++ b/src/lib/net/src/packets/incoming/mod.rs @@ -15,4 +15,5 @@ pub mod set_player_position; pub mod set_player_position_and_rotation; pub mod set_player_rotation; +pub mod chat_message; pub mod command; From 30664f67a16694a92cdbb82e6d67cbba38b84a29 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nils=20Kr=C3=BCger?= Date: Mon, 30 Dec 2024 12:54:07 +0100 Subject: [PATCH 27/32] feat: entity utils --- Cargo.toml | 3 +- src/lib/commands/src/ctx.rs | 19 ------------- src/lib/default_commands/Cargo.toml | 1 + src/lib/default_commands/src/echo.rs | 23 +++++++-------- src/lib/default_commands/src/nested.rs | 27 +++++++++++------- src/lib/entity_utils/Cargo.toml | 11 ++++++++ src/lib/entity_utils/src/lib.rs | 1 + src/lib/entity_utils/src/send_message.rs | 36 ++++++++++++++++++++++++ 8 files changed, 80 insertions(+), 41 deletions(-) create mode 100644 src/lib/entity_utils/Cargo.toml create mode 100644 src/lib/entity_utils/src/lib.rs create mode 100644 src/lib/entity_utils/src/send_message.rs diff --git a/Cargo.toml b/Cargo.toml index cfb3dbcb..d909232a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -35,6 +35,7 @@ members = [ "src/lib/utils/logging", "src/lib/utils/profiling", "src/lib/world", + "src/lib/entity_utils", ] #================== Lints ==================# @@ -106,7 +107,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 tokio = { version = "1.40.0", features = ["full"] } diff --git a/src/lib/commands/src/ctx.rs b/src/lib/commands/src/ctx.rs index 4bb7ceea..fa71a3db 100644 --- a/src/lib/commands/src/ctx.rs +++ b/src/lib/commands/src/ctx.rs @@ -3,12 +3,7 @@ use std::{ sync::{Arc, Mutex}, }; -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; use crate::{input::CommandInput, Command}; @@ -52,18 +47,4 @@ impl CommandContext { todo!(); } } - - pub async fn reply(&self, text: TextComponent) -> NetResult<()> { - let mut stream_writer = self - .state - .universe - .get_mut::(self.connection_id)?; - - stream_writer - .send_packet( - &SystemMessagePacket::new(text, false), - &NetEncodeOpts::WithLength, - ) - .await - } } diff --git a/src/lib/default_commands/Cargo.toml b/src/lib/default_commands/Cargo.toml index a637bac9..c1349694 100644 --- a/src/lib/default_commands/Cargo.toml +++ b/src/lib/default_commands/Cargo.toml @@ -9,5 +9,6 @@ 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 index 1f94ec58..5bfd761f 100644 --- a/src/lib/default_commands/src/echo.rs +++ b/src/lib/default_commands/src/echo.rs @@ -11,6 +11,7 @@ use ferrumc_commands::{ 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}; @@ -24,15 +25,15 @@ async fn echo(ctx: Arc) -> CommandResult { .get::(ctx.connection_id) .expect("failed to get identity"); - ctx.reply( - TextComponentBuilder::new(format!("{} said: {message}", identity.username)) - .color(NamedColor::Green) - .build(), - ) - .await - .map_err(|err| { - TextComponentBuilder::new(err.to_string()) - .color(NamedColor::Red) - .build() - }) + ctx.connection_id + .send_message( + TextComponentBuilder::new(format!("{} said: {message}", identity.username)) + .color(NamedColor::Green) + .build(), + &ctx.state.universe, + ) + .await + .expect("failed sending message"); + + Ok(()) } diff --git a/src/lib/default_commands/src/nested.rs b/src/lib/default_commands/src/nested.rs index 74487df4..44dbbc65 100644 --- a/src/lib/default_commands/src/nested.rs +++ b/src/lib/default_commands/src/nested.rs @@ -13,14 +13,19 @@ use ferrumc_commands::{ 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.reply(TextComponentBuilder::new("Executed /nested").build()) + ctx.connection_id + .send_message( + TextComponentBuilder::new("Executed /nested").build(), + &ctx.state.universe, + ) .await - .unwrap(); + .expect("failed sending message"); Ok(()) } @@ -33,14 +38,16 @@ async fn abc(ctx: Arc) -> CommandResult { let word = ctx.arg::("word"); let number = ctx.arg::("number"); - ctx.reply( - TextComponentBuilder::new(format!( - "Message: {message:?}, Word: {word:?}, Number: {number}" - )) - .build(), - ) - .await - .unwrap(); + ctx.connection_id + .send_message( + TextComponentBuilder::new(format!( + "Message: {message:?}, Word: {word:?}, Number: {number}" + )) + .build(), + &ctx.state.universe, + ) + .await + .expect("failed sending message"); Ok(()) } diff --git a/src/lib/entity_utils/Cargo.toml b/src/lib/entity_utils/Cargo.toml new file mode 100644 index 00000000..5cb59d75 --- /dev/null +++ b/src/lib/entity_utils/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "ferrumc-entity-utils" +version = "0.1.0" +edition = "2024" + +[dependencies] +ferrumc-net = { workspace = true } +ferrumc-net-codec = { workspace = true } +ferrumc-ecs = { workspace = true } +ferrumc-text = { 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..16f1f487 --- /dev/null +++ b/src/lib/entity_utils/src/send_message.rs @@ -0,0 +1,36 @@ +use async_trait::async_trait; +use ferrumc_ecs::Universe; +use ferrumc_net::{ + NetResult, connection::StreamWriter, packets::outgoing::system_message::SystemMessagePacket, +}; +use ferrumc_net_codec::encode::NetEncodeOpts; +use ferrumc_text::TextComponent; + +#[async_trait] +pub trait SendMessageExt { + async fn send_message(&self, message: TextComponent, universe: &Universe) -> NetResult<()>; + async fn send_actionbar(&self, message: TextComponent, universe: &Universe) -> NetResult<()>; +} + +#[async_trait] +impl SendMessageExt for usize { + async fn send_message(&self, message: TextComponent, universe: &Universe) -> NetResult<()> { + let mut writer = universe.get_mut::(*self)?; + writer + .send_packet( + &SystemMessagePacket::new(message, false), + &NetEncodeOpts::WithLength, + ) + .await + } + + async fn send_actionbar(&self, message: TextComponent, universe: &Universe) -> NetResult<()> { + let mut writer = universe.get_mut::(*self)?; + writer + .send_packet( + &SystemMessagePacket::new(message, true), + &NetEncodeOpts::WithLength, + ) + .await + } +} From 5c9c4cd875e192f9659685ae75a8c3d4cf6d6eb6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nils=20Kr=C3=BCger?= Date: Mon, 30 Dec 2024 13:24:27 +0100 Subject: [PATCH 28/32] refactor(entity_utils): make SendMessageExt use state instead of universe --- src/lib/default_commands/src/echo.rs | 2 +- src/lib/default_commands/src/nested.rs | 4 ++-- src/lib/entity_utils/Cargo.toml | 1 + src/lib/entity_utils/src/send_message.rs | 14 +++++++------- 4 files changed, 11 insertions(+), 10 deletions(-) diff --git a/src/lib/default_commands/src/echo.rs b/src/lib/default_commands/src/echo.rs index 5bfd761f..e239241a 100644 --- a/src/lib/default_commands/src/echo.rs +++ b/src/lib/default_commands/src/echo.rs @@ -30,7 +30,7 @@ async fn echo(ctx: Arc) -> CommandResult { TextComponentBuilder::new(format!("{} said: {message}", identity.username)) .color(NamedColor::Green) .build(), - &ctx.state.universe, + ctx.state.clone(), ) .await .expect("failed sending message"); diff --git a/src/lib/default_commands/src/nested.rs b/src/lib/default_commands/src/nested.rs index 44dbbc65..d925d2ad 100644 --- a/src/lib/default_commands/src/nested.rs +++ b/src/lib/default_commands/src/nested.rs @@ -22,7 +22,7 @@ async fn root(ctx: Arc) -> CommandResult { ctx.connection_id .send_message( TextComponentBuilder::new("Executed /nested").build(), - &ctx.state.universe, + ctx.state.clone(), ) .await .expect("failed sending message"); @@ -44,7 +44,7 @@ async fn abc(ctx: Arc) -> CommandResult { "Message: {message:?}, Word: {word:?}, Number: {number}" )) .build(), - &ctx.state.universe, + ctx.state.clone(), ) .await .expect("failed sending message"); diff --git a/src/lib/entity_utils/Cargo.toml b/src/lib/entity_utils/Cargo.toml index 5cb59d75..6817628b 100644 --- a/src/lib/entity_utils/Cargo.toml +++ b/src/lib/entity_utils/Cargo.toml @@ -8,4 +8,5 @@ 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/send_message.rs b/src/lib/entity_utils/src/send_message.rs index 16f1f487..c1903c05 100644 --- a/src/lib/entity_utils/src/send_message.rs +++ b/src/lib/entity_utils/src/send_message.rs @@ -1,21 +1,21 @@ use async_trait::async_trait; -use ferrumc_ecs::Universe; use ferrumc_net::{ NetResult, connection::StreamWriter, packets::outgoing::system_message::SystemMessagePacket, }; 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, universe: &Universe) -> NetResult<()>; - async fn send_actionbar(&self, message: TextComponent, universe: &Universe) -> NetResult<()>; + 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, universe: &Universe) -> NetResult<()> { - let mut writer = universe.get_mut::(*self)?; + 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), @@ -24,8 +24,8 @@ impl SendMessageExt for usize { .await } - async fn send_actionbar(&self, message: TextComponent, universe: &Universe) -> NetResult<()> { - let mut writer = universe.get_mut::(*self)?; + 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), From 40d39049fca71ad6038176f65074bc2c674c9d70 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nils=20Kr=C3=BCger?= Date: Mon, 30 Dec 2024 13:28:46 +0100 Subject: [PATCH 29/32] refactor(entity_utils): make SendMessageExt use &GlobalState --- src/lib/default_commands/src/echo.rs | 2 +- src/lib/default_commands/src/nested.rs | 4 ++-- src/lib/entity_utils/src/send_message.rs | 8 ++++---- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/lib/default_commands/src/echo.rs b/src/lib/default_commands/src/echo.rs index e239241a..3f850845 100644 --- a/src/lib/default_commands/src/echo.rs +++ b/src/lib/default_commands/src/echo.rs @@ -30,7 +30,7 @@ async fn echo(ctx: Arc) -> CommandResult { TextComponentBuilder::new(format!("{} said: {message}", identity.username)) .color(NamedColor::Green) .build(), - ctx.state.clone(), + &ctx.state, ) .await .expect("failed sending message"); diff --git a/src/lib/default_commands/src/nested.rs b/src/lib/default_commands/src/nested.rs index d925d2ad..45676a4e 100644 --- a/src/lib/default_commands/src/nested.rs +++ b/src/lib/default_commands/src/nested.rs @@ -22,7 +22,7 @@ async fn root(ctx: Arc) -> CommandResult { ctx.connection_id .send_message( TextComponentBuilder::new("Executed /nested").build(), - ctx.state.clone(), + &ctx.state, ) .await .expect("failed sending message"); @@ -44,7 +44,7 @@ async fn abc(ctx: Arc) -> CommandResult { "Message: {message:?}, Word: {word:?}, Number: {number}" )) .build(), - ctx.state.clone(), + &ctx.state, ) .await .expect("failed sending message"); diff --git a/src/lib/entity_utils/src/send_message.rs b/src/lib/entity_utils/src/send_message.rs index c1903c05..8bd111a8 100644 --- a/src/lib/entity_utils/src/send_message.rs +++ b/src/lib/entity_utils/src/send_message.rs @@ -8,13 +8,13 @@ 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 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<()> { + async fn send_message(&self, message: TextComponent, state: &GlobalState) -> NetResult<()> { let mut writer = state.universe.get_mut::(*self)?; writer .send_packet( @@ -24,7 +24,7 @@ impl SendMessageExt for usize { .await } - async fn send_actionbar(&self, message: TextComponent, state: GlobalState) -> NetResult<()> { + async fn send_actionbar(&self, message: TextComponent, state: &GlobalState) -> NetResult<()> { let mut writer = state.universe.get_mut::(*self)?; writer .send_packet( From 4f1db520ebc27369ea1699be305b5fdd7466cbe8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nils=20Kr=C3=BCger?= Date: Mon, 30 Dec 2024 18:09:30 +0100 Subject: [PATCH 30/32] feat(commands): completion API --- src/lib/commands/src/arg/parser/int.rs | 30 +++++++++++++++++++++++ src/lib/commands/src/arg/parser/mod.rs | 5 +++- src/lib/commands/src/arg/parser/string.rs | 25 +++++++++++++++++++ 3 files changed, 59 insertions(+), 1 deletion(-) diff --git a/src/lib/commands/src/arg/parser/int.rs b/src/lib/commands/src/arg/parser/int.rs index e8f4a715..7a5f2ac6 100644 --- a/src/lib/commands/src/arg/parser/int.rs +++ b/src/lib/commands/src/arg/parser/int.rs @@ -35,4 +35,34 @@ impl ArgumentParser for IntParser { 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 index 192c713f..7b58852e 100644 --- a/src/lib/commands/src/arg/parser/mod.rs +++ b/src/lib/commands/src/arg/parser/mod.rs @@ -8,7 +8,10 @@ pub mod utils; pub mod vanilla; pub trait ArgumentParser: Send + Sync { - fn parse(&self, context: Arc, input: Arc>) -> ParserResult; + fn parse(&self, ctx: Arc, input: Arc>) -> ParserResult; + fn completions(&self, ctx: Arc, input: Arc>) + -> Vec; + fn new() -> Self where Self: Sized; diff --git a/src/lib/commands/src/arg/parser/string.rs b/src/lib/commands/src/arg/parser/string.rs index 983425c8..7650d2e2 100644 --- a/src/lib/commands/src/arg/parser/string.rs +++ b/src/lib/commands/src/arg/parser/string.rs @@ -36,6 +36,14 @@ impl ArgumentParser for SingleStringParser { props: MinecraftArgumentProperties::String(StringParsingBehavior::default()), } } + + fn completions( + &self, + _ctx: Arc, + _input: Arc>, + ) -> Vec { + vec![] + } } pub struct GreedyStringParser; @@ -78,7 +86,16 @@ impl ArgumentParser for GreedyStringParser { props: MinecraftArgumentProperties::String(StringParsingBehavior::Greedy), } } + + fn completions( + &self, + _ctx: Arc, + _input: Arc>, + ) -> Vec { + vec![] + } } + pub struct QuotedStringParser; impl ArgumentParser for QuotedStringParser { @@ -148,4 +165,12 @@ impl ArgumentParser for QuotedStringParser { props: MinecraftArgumentProperties::String(StringParsingBehavior::Quotable), } } + + fn completions( + &self, + _ctx: Arc, + _input: Arc>, + ) -> Vec { + vec![] + } } From ebb5c52e43aa2c63ecc7f480781b89cd75074bba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nils=20Kr=C3=BCger?= Date: Mon, 30 Dec 2024 18:10:09 +0100 Subject: [PATCH 31/32] fix(entity_utils): rust edition --- src/lib/entity_utils/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib/entity_utils/Cargo.toml b/src/lib/entity_utils/Cargo.toml index 6817628b..b5a0dff2 100644 --- a/src/lib/entity_utils/Cargo.toml +++ b/src/lib/entity_utils/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "ferrumc-entity-utils" version = "0.1.0" -edition = "2024" +edition = "2021" [dependencies] ferrumc-net = { workspace = true } From 335c1e1622d736cba5463392ad77877e6dc18c03 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nils=20Kr=C3=BCger?= Date: Sat, 4 Jan 2025 06:49:53 +0100 Subject: [PATCH 32/32] fix my fucked merge --- src/bin/src/packet_handlers/login_process.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/bin/src/packet_handlers/login_process.rs b/src/bin/src/packet_handlers/login_process.rs index f992eb33..1c52451d 100644 --- a/src/bin/src/packet_handlers/login_process.rs +++ b/src/bin/src/packet_handlers/login_process.rs @@ -220,6 +220,9 @@ async fn handle_ack_finish_configuration( chunk_recv.calculate_chunks().await; } + player_info_update_packets(entity_id, &state).await?; + broadcast_spawn_entity_packet(entity_id, &state).await?; + Ok(ack_finish_configuration_event) } async fn send_keep_alive(