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] 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()); +}