diff --git a/Cargo.toml b/Cargo.toml index 781450180..67dac9184 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -22,6 +22,7 @@ default = [ "player_list", "scoreboard", "world_border", + "command", "weather", "testing", ] @@ -34,6 +35,7 @@ network = ["dep:valence_network"] player_list = ["dep:valence_player_list"] scoreboard = ["dep:valence_scoreboard"] world_border = ["dep:valence_world_border"] +command = ["dep:valence_command", "dep:valence_command_macros"] weather = ["dep:valence_weather"] testing = [] @@ -42,25 +44,28 @@ anyhow.workspace = true bevy_app.workspace = true bevy_ecs.workspace = true bevy_log = { workspace = true, optional = true } -uuid.workspace = true bytes.workspace = true rand.workspace = true +uuid.workspace = true valence_advancement = { workspace = true, optional = true } valence_anvil = { workspace = true, optional = true, features = [ "bevy_plugin", ] } valence_boss_bar = { workspace = true, optional = true } -valence_server.workspace = true +valence_command = { workspace = true, optional = true } +valence_command_macros = { workspace = true, optional = true } +valence_ident_macros.workspace = true +valence_ident.workspace = true valence_inventory = { workspace = true, optional = true } +valence_lang.workspace = true valence_network = { workspace = true, optional = true } valence_player_list = { workspace = true, optional = true } valence_registry.workspace = true valence_scoreboard = { workspace = true, optional = true } +valence_server.workspace = true +valence_text.workspace = true valence_weather = { workspace = true, optional = true } valence_world_border = { workspace = true, optional = true } -valence_lang.workspace = true -valence_text.workspace = true -valence_ident.workspace = true [dev-dependencies] anyhow.workspace = true @@ -106,6 +111,7 @@ async-trait = "0.1.60" atty = "0.2.14" base64 = "0.21.0" bevy_app = { version = "0.11", default-features = false } +bevy_derive = "0.11.2" bevy_ecs = { version = "0.11", default-features = false, features = [ "multi-threaded", ] } @@ -135,14 +141,16 @@ hmac = "0.12.1" image = "0.24.6" indexmap = "2.0.0" itertools = "0.11.0" -java_string = { path = "crates/java_string", version = "0.1.1" } +java_string = { path = "crates/java_string", version = "0.1.2" } lru = "0.12.0" noise = "0.8.2" num = "0.4.0" num-bigint = "0.4.3" owo-colors = "3.5.0" +ordered-float = "3.7.0" parking_lot = "0.12.1" paste = "1.0.11" +petgraph = "0.6.3" pretty_assertions = "1.3.0" proc-macro2 = "1.0.56" quote = "1.0.26" @@ -155,6 +163,7 @@ rsa = "0.9.2" rsa-der = "0.3.0" rustc-hash = "1.1.0" serde = "1.0.160" +serde-value = "0.7.0" serde_json = "1.0.96" sha1 = "0.10.5" sha2 = "0.10.6" @@ -174,6 +183,8 @@ valence_advancement = { path = "crates/valence_advancement", version = "0.2.0-al valence_anvil = { path = "crates/valence_anvil", version = "0.1.0" } valence_boss_bar = { path = "crates/valence_boss_bar", version = "0.2.0-alpha.1" } valence_build_utils = { path = "crates/valence_build_utils", version = "0.2.0-alpha.1" } +valence_command = { path = "crates/valence_command", version = "0.2.0-alpha.1" } +valence_command_macros = { path = "crates/valence_command_macros", version = "0.2.0-alpha.1" } valence_entity = { path = "crates/valence_entity", version = "0.2.0-alpha.1" } valence_generated = { path = "crates/valence_generated", version = "0.2.0-alpha.1" } valence_ident = { path = "crates/valence_ident", version = "0.2.0-alpha.1" } diff --git a/assets/depgraph.svg b/assets/depgraph.svg index 8bb16f718..c5380973c 100644 --- a/assets/depgraph.svg +++ b/assets/depgraph.svg @@ -4,16 +4,16 @@ - + %3 - + 0 - -java_string + +java_string @@ -24,140 +24,140 @@ 2 - -valence_server + +valence_server 1->2 - - + + 3 - -valence_entity + +valence_entity 2->3 - - + + 12 - -valence_registry + +valence_registry 2->12 - - + + 11 - -valence_server_common + +valence_server_common 3->11 - - + + 12->11 - - + + 7 - -valence_protocol + +valence_protocol 11->7 - - + + 4 - -valence_math + +valence_math 5 - -valence_nbt + +valence_nbt 6 - -valence_ident + +valence_ident 8 - -valence_generated + +valence_generated 7->8 - - + + 10 - -valence_text + +valence_text 7->10 - - + + 8->4 - - + + 8->6 - - + + 10->5 - - + + 10->6 - - + + 9 - -valence_build_utils + +valence_build_utils @@ -168,8 +168,8 @@ 13->2 - - + + @@ -180,206 +180,224 @@ 14->2 - - + + 15 -valence_inventory +valence_command 15->2 - - + + 16 - -valence_lang + +valence_inventory + + + +16->2 + + 17 - -valence_network - - - -17->2 - - - - - -17->16 - - + +valence_lang 18 - -valence_player_list + +valence_network - + 18->2 - - + + + + + +18->17 + + 19 - -valence_scoreboard + +valence_player_list 19->2 - - + + 20 - -valence_spatial + +valence_scoreboard + + + +20->2 + + 21 - -valence_weather - - - -21->2 - - + +valence_spatial 22 - -valence_world_border + +valence_weather 22->2 - - + + 23 - -dump_schedule + +valence_world_border + + + +23->2 + + 24 - -valence + +dump_schedule - - -23->24 - - + + +25 + +valence - + -24->1 - - +24->25 + + - + -24->13 - - +25->1 + + - + -24->14 - - +25->13 + + - + -24->15 - - +25->14 + + - + -24->17 - - +25->15 + + - + -24->18 - - +25->16 + + - + -24->19 - - +25->18 + + - + -24->21 - - +25->19 + + - + -24->22 - - +25->20 + + - - -25 - -packet_inspector - - + -25->7 - - +25->22 + + + + + +25->23 + + 26 - -playground + +packet_inspector - - -26->24 - - + + +26->7 + + 27 - -stresser - - - -27->7 - - + +playground + + + +27->25 + + + + + +28 + +stresser + + + +28->7 + + diff --git a/crates/java_string/Cargo.toml b/crates/java_string/Cargo.toml index 2a0b65be0..0465f63c2 100644 --- a/crates/java_string/Cargo.toml +++ b/crates/java_string/Cargo.toml @@ -2,7 +2,7 @@ name = "java_string" description = "An implementation of Java strings, tolerant of invalid UTF-16 encoding" readme = "README.md" -version = "0.1.1" +version = "0.1.2" keywords = ["java", "string", "utf16"] edition.workspace = true repository.workspace = true diff --git a/crates/java_string/src/lib.rs b/crates/java_string/src/lib.rs index 3cdbb579d..3b6623a00 100644 --- a/crates/java_string/src/lib.rs +++ b/crates/java_string/src/lib.rs @@ -12,7 +12,6 @@ mod serde; mod slice; pub(crate) mod validations; -pub use cesu8::*; pub use char::*; pub use error::*; pub use iter::*; diff --git a/crates/valence_command/Cargo.toml b/crates/valence_command/Cargo.toml new file mode 100644 index 000000000..125f68e92 --- /dev/null +++ b/crates/valence_command/Cargo.toml @@ -0,0 +1,23 @@ +[package] +name = "valence_command" +description = "Command management for Valence" +readme = "README.md" +version.workspace = true +edition.workspace = true +repository.workspace = true +documentation.workspace = true +license.workspace = true + +[dependencies] +anyhow.workspace = true +bevy_app.workspace = true +bevy_derive.workspace = true +bevy_ecs.workspace = true +byteorder.workspace = true +ordered-float.workspace = true +petgraph.workspace = true +thiserror.workspace = true +tracing.workspace = true + +valence_server.workspace = true +valence_text.workspace = true diff --git a/crates/valence_command/README.md b/crates/valence_command/README.md new file mode 100644 index 000000000..82c2ce7ce --- /dev/null +++ b/crates/valence_command/README.md @@ -0,0 +1,13 @@ +# Valence Command + +This plugin manages the command system for a valence server. It is responsible for parsing, storing, managing and +dispatching commands. + +#### This plugin manages the following: + +- Registering commands to a Command Graph which is used parse commands. +- Receiving commands from the client and turning them into events. +- Parsing commands and dispatching them in the registered executable format. +- Sending the command graph to clients. + +See the module level documentation for more information. diff --git a/crates/valence_command/src/graph.rs b/crates/valence_command/src/graph.rs new file mode 100644 index 000000000..239a6f143 --- /dev/null +++ b/crates/valence_command/src/graph.rs @@ -0,0 +1,533 @@ +//! # Command graph implementation +//! +//! This is the core of the command system. It is a graph of `CommandNode`s that +//! are connected by the `CommandEdgeType`. The graph is used to determine what +//! command to run when a command is entered. The graph is also used to generate +//! the command tree that is sent to the client. +//! +//! ### The graph is a directed graph with 3 types of nodes: +//! * Root node ([NodeData::Root]) - This is the root of the graph. It is used +//! to connect all the +//! other nodes to the graph. It is always present and there should only be one. +//! * Literal node ([NodeData::Literal]) - This is a literal part of a command. +//! It is a string that +//! must be matched exactly by the client to trigger the validity of the node. +//! For example, the command `/teleport` would have a literal node with the name +//! `teleport` which is a child of the root node. +//! * Argument node ([NodeData::Argument]) - This is a node that represents an +//! argument in a +//! command. It is a string that is matched by the client and checked by the +//! server. For example, the command `/teleport 0 0 0` would have 1 argument +//! node with the name "" and the parser [Parser::Vec3] +//! which is a child of the literal node with the name `teleport`. +//! +//! #### and 2 types of edges: +//! * Child edge ([CommandEdgeType::Child]) - This is an edge that connects a +//! parent node to a +//! child node. It is used to determine what nodes are valid children of a +//! parent node. for example, the literal node with the name `teleport` would +//! have a child edge to the argument node with the name +//! "". This means that the argument node is a valid child +//! of the literal node. +//! * Redirect edge ([CommandEdgeType::Redirect]) - This edge is special. It is +//! used to redirect the +//! client to another node. For example, the literal node with the name `tp` +//! would have a Redirect edge to the literal node with the name `teleport`. +//! This means that if the client enters the command `/tp` the server will +//! redirect the client to the literal node with the name `teleport`. Making the +//! command `/tp` functionally equivalent to `/teleport`. +//! +//! # Cool Example Graph For Possible Implementation Of Teleport Command (made with graphviz) +//! ```text +//! ┌────────────────────────────────┐ +//! │ Root │ ─┐ +//! └────────────────────────────────┘ │ +//! │ │ +//! │ Child │ +//! ▼ │ +//! ┌────────────────────────────────┐ │ +//! │ Literal: tp │ │ +//! └────────────────────────────────┘ │ +//! │ │ +//! │ Redirect │ Child +//! ▼ ▼ +//! ┌──────────────────────────────────┐ Child ┌──────────────────────────────────────────────────────────────────────────────┐ +//! │ Argument: │ ◀─────── │ Literal: teleport │ +//! └──────────────────────────────────┘ └──────────────────────────────────────────────────────────────────────────────┘ +//! │ │ +//! │ Child │ Child +//! ▼ ▼ +//! ┌──────────────────────────────────┐ Child ┌────────────────────────────────┐ ┌──────────────────────────────────┐ +//! │ Argument: │ ◀─────── │ Argument: │ │ Argument: │ +//! └──────────────────────────────────┘ └────────────────────────────────┘ └──────────────────────────────────┘ +//! │ +//! │ Child +//! ▼ +//! ┌────────────────────────────────┐ +//! │ Argument: │ +//! └────────────────────────────────┘ +//! ``` +//! If you want a cool graph of your own command graph you can use the display +//! trait on the [CommandGraph] struct. Then you can use a tool like +//! [Graphviz Online](https://dreampuf.github.io/GraphvizOnline) to look at the graph. + +use std::collections::HashMap; +use std::fmt::{Display, Formatter}; + +use petgraph::dot::Dot; +use petgraph::prelude::*; +use valence_server::protocol::packets::play::command_tree_s2c::{ + Node, NodeData, Parser, StringArg, +}; +use valence_server::protocol::packets::play::CommandTreeS2c; +use valence_server::protocol::VarInt; + +use crate::modifier_value::ModifierValue; +use crate::parsers::{CommandArg, ParseInput}; +use crate::{CommandRegistry, CommandScopeRegistry}; + +/// This struct is used to store the command graph. (see module level docs for +/// more info) +#[derive(Debug, Clone)] +pub struct CommandGraph { + pub graph: Graph, + pub root: NodeIndex, +} + +impl Default for CommandGraph { + fn default() -> Self { + Self::new() + } +} + +/// Output the graph in graphviz dot format to do visual debugging. (this was +/// used to make the cool graph in the module level docs) +impl Display for CommandGraph { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", Dot::new(&self.graph)) + } +} + +impl CommandGraph { + pub fn new() -> Self { + let mut graph = Graph::::new(); + let root = graph.add_node(CommandNode { + executable: false, + data: NodeData::Root, + scopes: vec![], + }); + + Self { graph, root } + } +} + +/// Data for the nodes in the graph (see module level docs for more info) +#[derive(Clone, Debug, PartialEq)] +pub struct CommandNode { + pub executable: bool, + pub data: NodeData, + pub scopes: Vec, +} + +impl Display for CommandNode { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + match &self.data { + NodeData::Root => write!(f, "Root"), + NodeData::Literal { name } => write!(f, "Literal: {}", name), + NodeData::Argument { name, .. } => write!(f, "Argument: <{}>", name), + } + } +} + +#[derive(Copy, Clone, PartialEq, Eq, Debug, Hash)] +pub enum CommandEdgeType { + Redirect, + Child, +} + +impl Display for CommandEdgeType { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + match self { + CommandEdgeType::Redirect => write!(f, "Redirect"), + CommandEdgeType::Child => write!(f, "Child"), + } + } +} + +impl From for CommandTreeS2c { + fn from(command_graph: CommandGraph) -> Self { + let graph = command_graph.graph; + let nodes_and_edges = graph.into_nodes_edges(); + + let mut nodes: Vec = nodes_and_edges + .0 + .into_iter() + .map(|node| Node { + children: Vec::new(), + data: node.weight.data, + executable: node.weight.executable, + redirect_node: None, + }) + .collect(); + + let edges = nodes_and_edges.1; + + for edge in edges { + match edge.weight { + CommandEdgeType::Child => { + nodes[edge.source().index()] + .children + .push(VarInt::from(edge.target().index() as i32)); + } + CommandEdgeType::Redirect => { + nodes[edge.source().index()].redirect_node = + Some(VarInt::from(edge.target().index() as i32)); + } + } + } + + CommandTreeS2c { + commands: nodes, + root_index: VarInt::from(command_graph.root.index() as i32), + } + } +} + +/// Ergonomic builder pattern for adding executables, literals and arguments to +/// a command graph. See the derive macro for a more ergonomic way of doing this +/// for a basic command with an enum. +/// +/// # Type Parameters +/// * `T` - the type that should be constructed by an executable when the +/// command is executed +/// +/// # Example +/// ``` +/// use std::collections::HashMap; +/// use petgraph::visit::{EdgeCount, NodeCount}; +/// use valence_command::graph::{ +/// CommandGraph, CommandGraphBuilder +/// }; +/// use valence_command::{CommandRegistry}; +/// use valence_command::parsers::CommandArg; +/// +/// struct TestCommand { +/// test: i32, +/// } +/// +/// let mut command_graph = CommandRegistry::default(); +/// let mut executable_map = HashMap::new(); +/// let mut parser_map = HashMap::new(); +/// let mut modifier_map = HashMap::new(); +/// let mut command_graph_builder = CommandGraphBuilder::::new(&mut command_graph, &mut executable_map, &mut parser_map, &mut modifier_map); +/// +/// // simple command +/// let simple_command = command_graph_builder +/// .root() // transition to the root node +/// .literal("test") // add a literal node then transition to it +/// .argument("test") +/// // a player needs one of these scopes to execute the command +/// //(note: if you want an admin scope you should use the link method on the scope registry.) +/// .with_scopes(vec!["test:admin", "command:test"]) +/// .with_parser::() +/// // it is reasonably safe to unwrap here because we know that the argument is an integer +/// .with_executable(|args| TestCommand { test: i32::parse_arg(args).unwrap() }) +/// .id(); +/// +/// // complex command (redirects back to the simple command) +/// command_graph_builder +/// .root() +/// .literal("test") +/// .literal("command") +/// .redirect_to(simple_command); +/// +/// assert_eq!(command_graph.graph.graph.node_count(), 5); // root, test, command, , test +/// // 5 edges, 2 for the simple command, 2 for the complex command and 1 for the redirect +/// assert_eq!(command_graph.graph.graph.edge_count(), 5); +/// ``` +/// +/// in this example we can execute either of the following commands for the same +/// result: +/// - `/test test 1` +/// - `/test command test 1` +/// +/// the executables from these commands will both return a `TestCommand` with +/// the value `1` +#[allow(clippy::type_complexity)] +pub struct CommandGraphBuilder<'a, T> { + // We do not own the graph, we just have a mutable reference to it + graph: &'a mut CommandGraph, + current_node: NodeIndex, + executables: &'a mut HashMap T>, + parsers: &'a mut HashMap bool>, + modifiers: &'a mut HashMap)>, + scopes_added: Vec, /* we need to keep track of added scopes so we can add them to + * the registry later */ +} + +impl<'a, T> CommandGraphBuilder<'a, T> { + /// Creates a new command graph builder + /// + /// # Arguments + /// * registry - the command registry to add the commands to + /// * executables - the map of node indices to executable parser functions + #[allow(clippy::type_complexity)] + pub fn new( + registry: &'a mut CommandRegistry, + executables: &'a mut HashMap T>, + parsers: &'a mut HashMap bool>, + modifiers: &'a mut HashMap< + NodeIndex, + fn(String, &mut HashMap), + >, + ) -> Self { + CommandGraphBuilder { + current_node: registry.graph.root, + graph: &mut registry.graph, + executables, + parsers, + modifiers, + scopes_added: Vec::new(), + } + } + + /// Transitions to the root node. Use this to start building a new command + /// from root. + pub fn root(&mut self) -> &mut Self { + self.current_node = self.graph.root; + self + } + + /// Creates a new literal node and transitions to it. + /// + /// # Default Values + /// * executable - `false` + /// * scopes - `Vec::new()` + pub fn literal(&mut self, literal: impl Into) -> &mut Self { + let graph = &mut self.graph.graph; + let current_node = &mut self.current_node; + + let literal_node = graph.add_node(CommandNode { + executable: false, + data: NodeData::Literal { + name: literal.into(), + }, + scopes: Vec::new(), + }); + + graph.add_edge(*current_node, literal_node, CommandEdgeType::Child); + + *current_node = literal_node; + + self + } + + /// Creates a new argument node and transitions to it. + /// + /// # Default Values + /// * executable - `false` + /// * scopes - `Vec::new()` + /// * parser - `StringArg::SingleWord` + /// * suggestion - `None` + pub fn argument(&mut self, argument: impl Into) -> &mut Self { + let graph = &mut self.graph.graph; + let current_node = &mut self.current_node; + + let argument_node = graph.add_node(CommandNode { + executable: false, + data: NodeData::Argument { + name: argument.into(), + parser: Parser::String(StringArg::SingleWord), + suggestion: None, + }, + scopes: Vec::new(), + }); + + graph.add_edge(*current_node, argument_node, CommandEdgeType::Child); + + *current_node = argument_node; + + self + } + + /// Creates a new redirect edge from the current node to the node specified. + /// For info on what a redirect edge is, see the module level documentation. + /// + /// # Example + /// ``` + /// use std::collections::HashMap; + /// + /// use valence_command::graph::CommandGraphBuilder; + /// use valence_command::CommandRegistry; + /// + /// struct TestCommand; + /// + /// let mut command_graph = CommandRegistry::default(); + /// let mut executable_map = HashMap::new(); + /// let mut parser_map = HashMap::new(); + /// let mut modifier_map = HashMap::new(); + /// let mut command_graph_builder = CommandGraphBuilder::::new( + /// &mut command_graph, + /// &mut executable_map, + /// &mut parser_map, + /// &mut modifier_map, + /// ); + /// + /// let simple_command = command_graph_builder + /// .root() // transition to the root node + /// .literal("test") // add a literal node then transition to it + /// .id(); // get the id of the literal node + /// + /// command_graph_builder + /// .root() // transition to the root node + /// .literal("test") // add a literal node then transition to it + /// .literal("command") // add a literal node then transition to it + /// .redirect_to(simple_command); // redirect to the simple command + /// ``` + pub fn redirect_to(&mut self, node: NodeIndex) -> &mut Self { + let graph = &mut self.graph.graph; + let current_node = &mut self.current_node; + + graph.add_edge(*current_node, node, CommandEdgeType::Redirect); + + *current_node = node; + + self + } + + /// Sets up the executable function for the current node. This function will + /// be called when the command is executed and should parse the args and + /// return the `T` type. + /// + /// # Arguments + /// * executable - the executable function to add + /// + /// # Example + /// have a look at the example for [CommandGraphBuilder] + pub fn with_executable(&mut self, executable: fn(&mut ParseInput) -> T) -> &mut Self { + let graph = &mut self.graph.graph; + let current_node = &mut self.current_node; + + let node = graph.node_weight_mut(*current_node).unwrap(); + + node.executable = true; + self.executables.insert(*current_node, executable); + + self + } + + /// Adds a modifier to the current node + /// + /// # Arguments + /// * modifier - the modifier function to add + /// + /// # Example + /// ``` + /// use std::collections::HashMap; + /// + /// use valence_command::graph::CommandGraphBuilder; + /// use valence_command::CommandRegistry; + /// + /// struct TestCommand; + /// + /// let mut command_graph = CommandRegistry::default(); + /// let mut executable_map = HashMap::new(); + /// let mut parser_map = HashMap::new(); + /// let mut modifier_map = HashMap::new(); + /// let mut command_graph_builder = + /// CommandGraphBuilder::::new(&mut command_graph, &mut executable_map, &mut parser_map, &mut modifier_map); + /// + /// command_graph_builder + /// .root() // transition to the root node + /// .literal("test") // add a literal node then transition to it + /// .with_modifier(|_, modifiers| { + /// modifiers.insert("test".into(), "test".into()); // this will trigger when the node is passed + /// }) + /// .literal("command") // add a literal node then transition to it + /// .with_executable(|_| TestCommand); + /// ``` + pub fn with_modifier( + &mut self, + modifier: fn(String, &mut HashMap), + ) -> &mut Self { + let current_node = &mut self.current_node; + + self.modifiers.insert(*current_node, modifier); + + self + } + + /// Sets the required scopes for the current node + /// + /// # Arguments + /// * scopes - a list of scopes for that are aloud to access a command node + /// and its children (list of strings following the system described in + /// [command_scopes](crate::scopes)) + pub fn with_scopes(&mut self, scopes: Vec>) -> &mut Self { + let graph = &mut self.graph.graph; + let current_node = &mut self.current_node; + + let node = graph.node_weight_mut(*current_node).unwrap(); + + node.scopes = scopes.into_iter().map(|s| s.into()).collect(); + self.scopes_added.extend(node.scopes.clone()); + + self + } + + /// Applies the scopes to the registry + /// + /// # Arguments + /// * registry - the registry to apply the scopes to + pub fn apply_scopes(&mut self, registry: &mut CommandScopeRegistry) -> &mut Self { + for scope in self.scopes_added.clone() { + registry.add_scope(scope); + } + self.scopes_added.clear(); + self + } + + /// Sets the parser for the current node. This will decide how the argument + /// is parsed client side and will be used to check the argument before + /// it is passed to the executable. The node should be an argument node + /// or nothing will happen. + /// + /// # Type Parameters + /// * `P` - the parser to use for the current node (must be [CommandArg]) + pub fn with_parser(&mut self) -> &mut Self { + let graph = &mut self.graph.graph; + let current_node = self.current_node; + + let node = graph.node_weight_mut(current_node).unwrap(); + self.parsers + .insert(current_node, |input| P::parse_arg(input).is_ok()); + + let parser = P::display(); + + node.data = match node.data.clone() { + NodeData::Argument { + name, suggestion, .. + } => NodeData::Argument { + name, + parser, + suggestion, + }, + NodeData::Literal { name } => NodeData::Literal { name }, + NodeData::Root => NodeData::Root, + }; + + self + } + + /// Transitions to the node specified. + pub fn at(&mut self, node: NodeIndex) -> &mut Self { + self.current_node = node; + self + } + + /// Gets the id of the current node (useful for commands that have multiple + /// children). + pub fn id(&self) -> NodeIndex { + self.current_node + } +} diff --git a/crates/valence_command/src/handler.rs b/crates/valence_command/src/handler.rs new file mode 100644 index 000000000..b9f342b02 --- /dev/null +++ b/crates/valence_command/src/handler.rs @@ -0,0 +1,126 @@ +use std::collections::HashMap; +use std::marker::PhantomData; + +use bevy_app::{App, Plugin, PostStartup}; +use bevy_ecs::change_detection::ResMut; +use bevy_ecs::event::{Event, EventReader, EventWriter}; +use bevy_ecs::prelude::{Entity, IntoSystemConfigs, Resource}; +use petgraph::prelude::NodeIndex; +use valence_server::EventLoopPreUpdate; + +use crate::graph::CommandGraphBuilder; +use crate::modifier_value::ModifierValue; +use crate::parsers::ParseInput; +use crate::{ + Command, CommandProcessedEvent, CommandRegistry, CommandScopeRegistry, CommandSystemSet, +}; + +impl Plugin for CommandHandlerPlugin +where + T: Command + Send + Sync + 'static, +{ + fn build(&self, app: &mut App) { + app.add_event::>() + .insert_resource(CommandResource::::new()) + .add_systems(PostStartup, command_startup_system::) + .add_systems( + EventLoopPreUpdate, + command_event_system::.after(CommandSystemSet), + ); + } +} + +pub struct CommandHandlerPlugin +where + T: Command, +{ + command: PhantomData, +} + +impl Default for CommandHandlerPlugin { + fn default() -> Self { + Self::new() + } +} + +impl CommandHandlerPlugin +where + T: Command, +{ + pub fn new() -> Self { + CommandHandlerPlugin { + command: PhantomData, + } + } +} + +#[derive(Resource)] +struct CommandResource { + command: PhantomData, + executables: HashMap T>, +} + +impl CommandResource { + pub fn new() -> Self { + CommandResource { + command: PhantomData, + executables: HashMap::new(), + } + } +} + +#[derive(Event)] +pub struct CommandResultEvent +where + T: Command, + T: Send + Sync + 'static, +{ + pub result: T, + pub executor: Entity, + pub modifiers: HashMap, +} + +fn command_startup_system( + mut registry: ResMut, + mut scope_registry: ResMut, + mut command: ResMut>, +) where + T: Command + Send + Sync + 'static, +{ + let mut executables = HashMap::new(); + let mut parsers = HashMap::new(); + let mut modifiers = HashMap::new(); + let graph_builder = &mut CommandGraphBuilder::new( + &mut registry, + &mut executables, + &mut parsers, + &mut modifiers, + ); + T::assemble_graph(graph_builder); + graph_builder.apply_scopes(&mut scope_registry); + + command.executables.extend(executables.clone()); + registry.parsers.extend(parsers); + registry.modifiers.extend(modifiers); + registry.executables.extend(executables.keys()); +} + +/// This system reads incoming command events. +fn command_event_system( + mut commands_executed: EventReader, + mut events: EventWriter>, + command: ResMut>, +) where + T: Command + Send + Sync, +{ + for command_event in commands_executed.iter() { + if let Some(executable) = command.executables.get(&command_event.node) { + let result = executable(&mut ParseInput::new(&command_event.command)); + events.send(CommandResultEvent { + result, + executor: command_event.executor, + modifiers: command_event.modifiers.clone(), + }); + } + } +} diff --git a/crates/valence_command/src/lib.rs b/crates/valence_command/src/lib.rs new file mode 100644 index 000000000..03c1017db --- /dev/null +++ b/crates/valence_command/src/lib.rs @@ -0,0 +1,48 @@ +pub mod graph; +pub mod handler; +pub mod manager; +mod modifier_value; +pub mod parsers; +pub mod scopes; + +use std::collections::{HashMap, HashSet}; +use std::fmt::Debug; + +use bevy_app::App; +use bevy_ecs::prelude::{Resource, SystemSet}; +pub use manager::{CommandExecutionEvent, CommandProcessedEvent}; +pub use modifier_value::ModifierValue; +use petgraph::prelude::NodeIndex; +pub use scopes::CommandScopeRegistry; + +use crate::graph::{CommandGraph, CommandGraphBuilder}; +use crate::handler::CommandHandlerPlugin; +use crate::parsers::ParseInput; + +#[derive(SystemSet, Clone, PartialEq, Eq, Hash, Debug)] +pub struct CommandSystemSet; + +#[derive(Resource, Default)] +#[allow(clippy::type_complexity)] +pub struct CommandRegistry { + pub graph: CommandGraph, + pub parsers: HashMap bool>, + pub modifiers: HashMap)>, + pub executables: HashSet, +} + +pub trait Command { + fn assemble_graph(graph: &mut CommandGraphBuilder) + where + Self: Sized; +} + +pub trait AddCommand { + fn add_command(&mut self) -> &mut Self; +} + +impl AddCommand for App { + fn add_command(&mut self) -> &mut Self { + self.add_plugins(CommandHandlerPlugin::::new()) + } +} diff --git a/crates/valence_command/src/manager.rs b/crates/valence_command/src/manager.rs new file mode 100644 index 000000000..b352abbbe --- /dev/null +++ b/crates/valence_command/src/manager.rs @@ -0,0 +1,417 @@ +use std::collections::{HashMap, HashSet}; + +use bevy_app::{App, Plugin, PreUpdate}; +use bevy_ecs::entity::Entity; +use bevy_ecs::prelude::{ + Added, Changed, Commands, DetectChanges, Event, EventReader, EventWriter, IntoSystemConfigs, + Mut, Or, Query, Res, +}; +use petgraph::graph::NodeIndex; +use petgraph::prelude::EdgeRef; +use petgraph::{Direction, Graph}; +use tracing::{debug, warn}; +use valence_server::client::{Client, SpawnClientsSet}; +use valence_server::event_loop::PacketEvent; +use valence_server::protocol::packets::play::command_tree_s2c::NodeData; +use valence_server::protocol::packets::play::{CommandExecutionC2s, CommandTreeS2c}; +use valence_server::protocol::WritePacket; +use valence_server::EventLoopPreUpdate; + +use crate::graph::{CommandEdgeType, CommandGraph, CommandNode}; +use crate::parsers::ParseInput; +use crate::scopes::{CommandScopePlugin, CommandScopes}; +use crate::{CommandRegistry, CommandScopeRegistry, CommandSystemSet, ModifierValue}; + +pub struct CommandPlugin; + +impl Plugin for CommandPlugin { + fn build(&self, app: &mut App) { + app.add_plugins(CommandScopePlugin) + .add_event::() + .add_event::() + .add_systems(PreUpdate, insert_scope_component.after(SpawnClientsSet)) + .add_systems( + EventLoopPreUpdate, + ( + update_command_tree, + command_tree_update_with_client, + read_incoming_packets.before(CommandSystemSet), + parse_incoming_commands.in_set(CommandSystemSet), + ), + ); + + let graph: CommandGraph = CommandGraph::new(); + let modifiers = HashMap::new(); + let parsers = HashMap::new(); + let executables = HashSet::new(); + + app.insert_resource(CommandRegistry { + graph, + modifiers, + parsers, + executables, + }); + } +} + +/// This event is sent when a command is sent (you can send this with any +/// entity) +#[derive(Debug, Clone, PartialEq, Eq, Hash, Event)] +pub struct CommandExecutionEvent { + /// the command that was executed eg. "teleport @p 0 ~ 0" + pub command: String, + /// usually the Client entity but it could be a command block or something + /// (whatever the library user wants) + pub executor: Entity, +} + +/// This will only be sent if the command was successfully parsed and an +/// executable was found +#[derive(Debug, Clone, PartialEq, Eq, Event)] +pub struct CommandProcessedEvent { + /// the command that was executed eg. "teleport @p 0 ~ 0" + pub command: String, + /// usually the Client entity but it could be a command block or something + /// (whatever the library user wants) + pub executor: Entity, + /// the modifiers that were applied to the command + pub modifiers: HashMap, + /// the node that was executed + pub node: NodeIndex, +} + +fn insert_scope_component(mut clients: Query>, mut commands: Commands) { + for client in clients.iter_mut() { + commands.entity(client).insert(CommandScopes::new()); + } +} + +fn read_incoming_packets( + mut packets: EventReader, + mut event_writer: EventWriter, +) { + for packet in packets.iter() { + let client = packet.client; + if let Some(packet) = packet.decode::() { + event_writer.send(CommandExecutionEvent { + command: packet.command.to_string(), + executor: client, + }); + } + } +} + +#[allow(clippy::type_complexity)] +fn command_tree_update_with_client( + command_registry: Res, + scope_registry: Res, + mut updated_clients: Query< + (&mut Client, &CommandScopes), + Or<(Added, Changed)>, + >, +) { + update_client_command_tree( + &command_registry, + scope_registry, + &mut updated_clients.iter_mut().collect(), + ); +} + +fn update_command_tree( + command_registry: Res, + scope_registry: Res, + mut clients: Query<(&mut Client, &CommandScopes)>, +) { + if command_registry.is_changed() { + update_client_command_tree( + &command_registry, + scope_registry, + &mut clients.iter_mut().collect(), + ); + } +} + +fn update_client_command_tree( + command_registry: &Res, + scope_registry: Res, + updated_clients: &mut Vec<(Mut, &CommandScopes)>, +) { + for (ref mut client, client_scopes) in updated_clients { + let time = std::time::Instant::now(); + + let old_graph = &command_registry.graph; + let mut new_graph = Graph::new(); + + // collect a new graph into only nodes that are allowed to be executed + let root = old_graph.root; + + let mut to_visit = vec![(None, root)]; + let mut already_visited = HashSet::new(); // prevent recursion + let mut old_to_new = HashMap::new(); + let mut new_root = None; + + while let Some((parent, node)) = to_visit.pop() { + if already_visited.contains(&(parent.map(|(_, edge)| edge), node)) { + continue; + } + already_visited.insert((parent.map(|(_, edge)| edge), node)); + let node_scopes = &old_graph.graph[node].scopes; + if !node_scopes.is_empty() { + let mut has_scope = false; + for scope in node_scopes { + if scope_registry.any_grants( + &client_scopes.0.iter().map(|scope| scope.as_str()).collect(), + scope, + ) { + has_scope = true; + break; + } + } + if !has_scope { + continue; + } + } + + let new_node = *old_to_new + .entry(node) + .or_insert_with(|| new_graph.add_node(old_graph.graph[node].clone())); + + for neighbor in old_graph.graph.edges_directed(node, Direction::Outgoing) { + to_visit.push((Some((new_node, neighbor.weight())), neighbor.target())); + } + + if let Some(parent) = parent { + new_graph.add_edge(parent.0, new_node, *parent.1); + } else { + new_root = Some(new_node); + } + } + + match new_root { + Some(new_root) => { + let command_graph = CommandGraph { + graph: new_graph, + root: new_root, + }; + let packet: CommandTreeS2c = command_graph.into(); + + client.write_packet(&packet); + } + None => { + warn!( + "Client has no permissions to execute any commands so we sent them nothing. \ + It is generally a bad idea to scope the root node of the command graph as it \ + can cause undefined behavior. For example, if the player has permission to \ + execute a command before you change the scope of the root node, the packet \ + will not be sent to the client and so the client will still think they can \ + execute the command." + ) + } + } + + debug!("command tree update took {:?}", time.elapsed()); + } +} + +fn parse_incoming_commands( + mut event_reader: EventReader, + mut event_writer: EventWriter, + command_registry: Res, + scope_registry: Res, + entity_scopes: Query<&CommandScopes>, +) { + for command_event in event_reader.iter() { + let executor = command_event.executor; + // these are the leafs of the graph that are executable under this command + // group + let executable_leafs = command_registry + .executables + .iter() + .collect::>(); + let root = command_registry.graph.root; + + let command_input = &*command_event.command; + let graph = &command_registry.graph.graph; + let input = ParseInput::new(command_input); + + let mut to_be_executed = Vec::new(); + + let mut args = Vec::new(); + let mut modifiers_to_be_executed = Vec::new(); + + parse_command_args( + &mut args, + &mut modifiers_to_be_executed, + input, + graph, + &executable_leafs, + command_registry.as_ref(), + &mut to_be_executed, + root, + executor, + &entity_scopes, + scope_registry.as_ref(), + false, + ); + + let mut modifiers = HashMap::new(); + for (node, modifier) in modifiers_to_be_executed { + command_registry.modifiers.get(&node).unwrap()(modifier, &mut modifiers); + } + + debug!("Command processed: /{}", command_event.command); + + for node in to_be_executed { + println!("executing node: {:?}", node); + event_writer.send(CommandProcessedEvent { + command: args.join(" "), + executor, + modifiers: modifiers.clone(), + node, + }); + } + } +} + +#[allow(clippy::too_many_arguments)] +/// recursively parse the command args. +fn parse_command_args( + command_args: &mut Vec, + modifiers_to_be_executed: &mut Vec<(NodeIndex, String)>, + mut input: ParseInput, + graph: &Graph, + executable_leafs: &[&NodeIndex], + command_registry: &CommandRegistry, + to_be_executed: &mut Vec, + current_node: NodeIndex, + executor: Entity, + scopes: &Query<&CommandScopes>, + scope_registry: &CommandScopeRegistry, + coming_from_redirect: bool, +) -> bool { + let node_scopes = &graph[current_node].scopes; + let default_scopes = CommandScopes::new(); + let client_scopes: Vec<&str> = scopes + .get(executor) + .unwrap_or(&default_scopes) + .0 + .iter() + .map(|scope| scope.as_str()) + .collect(); + // if empty, we assume the node is global + if !node_scopes.is_empty() { + let mut has_scope = false; + for scope in node_scopes { + if scope_registry.any_grants(&client_scopes, scope) { + has_scope = true; + break; + } + } + if !has_scope { + return false; + } + } + + if !coming_from_redirect { + // we want to skip whitespace before matching the node + input.skip_whitespace(); + match &graph[current_node].data { + // no real need to check for root node + NodeData::Root => { + if command_registry.modifiers.contains_key(¤t_node) { + modifiers_to_be_executed.push((current_node, String::new())); + } + } + // if the node is a literal, we want to match the name of the literal + // to the input + NodeData::Literal { name } => { + match input.match_next(name) { + true => { + if !input.match_next(" ") && !input.is_done() { + return false; + } // we want to pop the whitespace after the literal + if command_registry.modifiers.contains_key(¤t_node) { + modifiers_to_be_executed.push((current_node, String::new())); + } + } + false => return false, + } + } + // if the node is an argument, we want to parse the argument + NodeData::Argument { .. } => { + let parser = match command_registry.parsers.get(¤t_node) { + Some(parser) => parser, + None => { + return false; + } + }; + // we want to save the input before and after parsing + // this is so we can save the argument to the command args + let pre_input = input.clone().into_inner(); + let valid = parser(&mut input); + if valid { + let Some(arg) = pre_input + .get(..input.len() - pre_input.len()) + .map(|s| s.to_string()) + else { + panic!( + "Parser replaced input with another string. This is not allowed. \ + Attempting to parse: {}", + input.into_inner() + ); + }; + + if command_registry.modifiers.contains_key(¤t_node) { + modifiers_to_be_executed.push((current_node, arg.clone())); + } + command_args.push(arg); + } else { + return false; + } + } + } + } else { + command_args.clear(); + } + + input.skip_whitespace(); + if input.is_done() && executable_leafs.contains(&¤t_node) { + to_be_executed.push(current_node); + return true; + } + + let mut all_invalid = true; + for neighbor in graph.neighbors(current_node) { + let pre_input = input.clone(); + let mut args = command_args.clone(); + let mut modifiers = modifiers_to_be_executed.clone(); + let valid = parse_command_args( + &mut args, + &mut modifiers, + input.clone(), + graph, + executable_leafs, + command_registry, + to_be_executed, + neighbor, + executor, + scopes, + scope_registry, + { + let edge = graph.find_edge(current_node, neighbor).unwrap(); + matches!(&graph[edge], CommandEdgeType::Redirect) + }, + ); + if valid { + *command_args = args; + *modifiers_to_be_executed = modifiers; + all_invalid = false; + } else { + input = pre_input; + } + } + if all_invalid { + return false; + } + true +} diff --git a/crates/valence_command/src/modifier_value.rs b/crates/valence_command/src/modifier_value.rs new file mode 100644 index 000000000..811e6e96c --- /dev/null +++ b/crates/valence_command/src/modifier_value.rs @@ -0,0 +1,219 @@ +use std::cmp::Ordering; +use std::collections::{BTreeMap, HashMap}; +use std::hash::{Hash, Hasher}; + +use ordered_float::OrderedFloat; + +/// Used to store keys values for command modifiers. Heavily inspired by +/// serde-value. +#[derive(Clone, Debug)] +pub enum ModifierValue { + Bool(bool), + + U8(u8), + U16(u16), + U32(u32), + U64(u64), + + I8(i8), + I16(i16), + I32(i32), + I64(i64), + + F32(f32), + F64(f64), + + Char(char), + String(String), + + Unit, + Option(Option>), + Seq(Vec), + Map(BTreeMap), +} + +#[allow(clippy::unit_hash)] +impl Hash for ModifierValue { + fn hash(&self, hasher: &mut H) + where + H: Hasher, + { + self.discriminant().hash(hasher); + match *self { + ModifierValue::Bool(v) => v.hash(hasher), + ModifierValue::U8(v) => v.hash(hasher), + ModifierValue::U16(v) => v.hash(hasher), + ModifierValue::U32(v) => v.hash(hasher), + ModifierValue::U64(v) => v.hash(hasher), + ModifierValue::I8(v) => v.hash(hasher), + ModifierValue::I16(v) => v.hash(hasher), + ModifierValue::I32(v) => v.hash(hasher), + ModifierValue::I64(v) => v.hash(hasher), + ModifierValue::F32(v) => OrderedFloat(v).hash(hasher), + ModifierValue::F64(v) => OrderedFloat(v).hash(hasher), + ModifierValue::Char(v) => v.hash(hasher), + ModifierValue::String(ref v) => v.hash(hasher), + ModifierValue::Unit => ().hash(hasher), + ModifierValue::Option(ref v) => v.hash(hasher), + ModifierValue::Seq(ref v) => v.hash(hasher), + ModifierValue::Map(ref v) => v.hash(hasher), + } + } +} + +impl PartialEq for ModifierValue { + fn eq(&self, rhs: &Self) -> bool { + match (self, rhs) { + (&ModifierValue::Bool(v0), &ModifierValue::Bool(v1)) if v0 == v1 => true, + (&ModifierValue::U8(v0), &ModifierValue::U8(v1)) if v0 == v1 => true, + (&ModifierValue::U16(v0), &ModifierValue::U16(v1)) if v0 == v1 => true, + (&ModifierValue::U32(v0), &ModifierValue::U32(v1)) if v0 == v1 => true, + (&ModifierValue::U64(v0), &ModifierValue::U64(v1)) if v0 == v1 => true, + (&ModifierValue::I8(v0), &ModifierValue::I8(v1)) if v0 == v1 => true, + (&ModifierValue::I16(v0), &ModifierValue::I16(v1)) if v0 == v1 => true, + (&ModifierValue::I32(v0), &ModifierValue::I32(v1)) if v0 == v1 => true, + (&ModifierValue::I64(v0), &ModifierValue::I64(v1)) if v0 == v1 => true, + (&ModifierValue::F32(v0), &ModifierValue::F32(v1)) + if OrderedFloat(v0) == OrderedFloat(v1) => + { + true + } + (&ModifierValue::F64(v0), &ModifierValue::F64(v1)) + if OrderedFloat(v0) == OrderedFloat(v1) => + { + true + } + (&ModifierValue::Char(v0), &ModifierValue::Char(v1)) if v0 == v1 => true, + (ModifierValue::String(v0), ModifierValue::String(v1)) if v0 == v1 => true, + (ModifierValue::Unit, ModifierValue::Unit) => true, + (ModifierValue::Option(v0), ModifierValue::Option(v1)) if v0 == v1 => true, + (ModifierValue::Seq(v0), ModifierValue::Seq(v1)) if v0 == v1 => true, + (ModifierValue::Map(v0), ModifierValue::Map(v1)) if v0 == v1 => true, + _ => false, + } + } +} + +impl Ord for ModifierValue { + fn cmp(&self, rhs: &Self) -> Ordering { + match (self, rhs) { + (&ModifierValue::Bool(v0), ModifierValue::Bool(v1)) => v0.cmp(v1), + (&ModifierValue::U8(v0), ModifierValue::U8(v1)) => v0.cmp(v1), + (&ModifierValue::U16(v0), ModifierValue::U16(v1)) => v0.cmp(v1), + (&ModifierValue::U32(v0), ModifierValue::U32(v1)) => v0.cmp(v1), + (&ModifierValue::U64(v0), ModifierValue::U64(v1)) => v0.cmp(v1), + (&ModifierValue::I8(v0), ModifierValue::I8(v1)) => v0.cmp(v1), + (&ModifierValue::I16(v0), ModifierValue::I16(v1)) => v0.cmp(v1), + (&ModifierValue::I32(v0), ModifierValue::I32(v1)) => v0.cmp(v1), + (&ModifierValue::I64(v0), ModifierValue::I64(v1)) => v0.cmp(v1), + (&ModifierValue::F32(v0), &ModifierValue::F32(v1)) => { + OrderedFloat(v0).cmp(&OrderedFloat(v1)) + } + (&ModifierValue::F64(v0), &ModifierValue::F64(v1)) => { + OrderedFloat(v0).cmp(&OrderedFloat(v1)) + } + (ModifierValue::Char(v0), ModifierValue::Char(ref v1)) => v0.cmp(v1), + (ModifierValue::String(ref v0), ModifierValue::String(ref v1)) => v0.cmp(v1), + (ModifierValue::Unit, &ModifierValue::Unit) => Ordering::Equal, + (ModifierValue::Option(ref v0), ModifierValue::Option(ref v1)) => v0.cmp(v1), + (ModifierValue::Seq(ref v0), ModifierValue::Seq(ref v1)) => v0.cmp(v1), + (ModifierValue::Map(ref v0), ModifierValue::Map(ref v1)) => v0.cmp(v1), + (v0, v1) => v0.discriminant().cmp(&v1.discriminant()), + } + } +} + +impl ModifierValue { + fn discriminant(&self) -> usize { + match *self { + ModifierValue::Bool(..) => 0, + ModifierValue::U8(..) => 1, + ModifierValue::U16(..) => 2, + ModifierValue::U32(..) => 3, + ModifierValue::U64(..) => 4, + ModifierValue::I8(..) => 5, + ModifierValue::I16(..) => 6, + ModifierValue::I32(..) => 7, + ModifierValue::I64(..) => 8, + ModifierValue::F32(..) => 9, + ModifierValue::F64(..) => 10, + ModifierValue::Char(..) => 11, + ModifierValue::String(..) => 12, + ModifierValue::Unit => 13, + ModifierValue::Option(..) => 14, + ModifierValue::Seq(..) => 16, + ModifierValue::Map(..) => 17, + } + } +} + +impl Eq for ModifierValue {} +impl PartialOrd for ModifierValue { + fn partial_cmp(&self, rhs: &Self) -> Option { + Some(self.cmp(rhs)) + } +} + +macro_rules! impl_from { + ($ty:ty, $variant:ident) => { + impl From<$ty> for ModifierValue { + fn from(v: $ty) -> Self { + ModifierValue::$variant(v) + } + } + }; +} + +impl_from!(bool, Bool); + +impl_from!(u8, U8); +impl_from!(u16, U16); +impl_from!(u32, U32); +impl_from!(u64, U64); + +impl_from!(i8, I8); +impl_from!(i16, I16); +impl_from!(i32, I32); +impl_from!(i64, I64); + +impl_from!(f32, F32); +impl_from!(f64, F64); + +impl_from!(char, Char); +impl_from!(String, String); + +impl From<&str> for ModifierValue { + fn from(v: &str) -> Self { + ModifierValue::String(v.to_owned()) + } +} + +impl From<()> for ModifierValue { + fn from(_: ()) -> Self { + ModifierValue::Unit + } +} + +impl From> for ModifierValue { + fn from(v: Option) -> Self { + ModifierValue::Option(v.map(Box::new)) + } +} + +impl> From> for ModifierValue { + fn from(v: Vec) -> Self { + ModifierValue::Seq(v.into_iter().map(Into::into).collect()) + } +} + +impl, V: Into> From> for ModifierValue { + fn from(v: BTreeMap) -> Self { + ModifierValue::Map(v.into_iter().map(|(k, v)| (k.into(), v.into())).collect()) + } +} + +impl, V: Into> From> for ModifierValue { + fn from(v: HashMap) -> Self { + ModifierValue::Map(v.into_iter().map(|(k, v)| (k.into(), v.into())).collect()) + } +} diff --git a/crates/valence_command/src/parsers.rs b/crates/valence_command/src/parsers.rs new file mode 100644 index 000000000..956b70ee0 --- /dev/null +++ b/crates/valence_command/src/parsers.rs @@ -0,0 +1,309 @@ +//! A collection of parses for use in command argument nodes. +pub mod angle; +pub mod block_pos; +pub mod bool; +pub mod color; +pub mod column_pos; +pub mod entity_anchor; +pub mod entity_selector; +pub mod gamemode; +pub mod inventory_slot; +pub mod numbers; +pub mod rotation; +pub mod score_holder; +pub mod strings; +pub mod swizzle; +pub mod time; +pub mod vec2; +pub mod vec3; + +use std::ops::Add; + +pub use block_pos::BlockPos; +pub use column_pos::ColumnPos; +pub use entity_anchor::EntityAnchor; +pub use entity_selector::EntitySelector; +pub use inventory_slot::InventorySlot; +pub use rotation::Rotation; +pub use score_holder::ScoreHolder; +pub use strings::{GreedyString, QuotableString}; +pub use swizzle::Swizzle; +use thiserror::Error; +pub use time::Time; +use tracing::error; +pub(crate) use valence_server::protocol::packets::play::command_tree_s2c::Parser; +pub use vec2::Vec2; +pub use vec3::Vec3; + +pub trait CommandArg: Sized { + fn arg_from_str(string: &str) -> Result { + Self::parse_arg(&mut ParseInput::new(string)) + } + + fn parse_arg(input: &mut ParseInput) -> Result; + /// what will the client be sent + fn display() -> Parser; +} + +/// +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct ParseInput<'a>(&'a str); + +impl<'a> ParseInput<'a> { + fn advance(&mut self) { + self.advance_n_chars(1); + } + + fn advance_n_chars(&mut self, n: usize) { + if self.is_done() { + return; + } + match self.0.char_indices().nth(n) { + Some((len, _)) => { + self.0 = &self.0[len..]; + } + None => { + self.0 = &self.0[self.0.len()..]; + } + } + } + + fn advance_n_bytes(&mut self, n: usize) { + if self.is_done() { + return; + } + self.0 = &self.0[n..]; + } + pub fn new(input: &'a str) -> Self { + ParseInput(input) + } + + /// Returns the next character without advancing the input + pub fn peek(&self) -> Option { + self.0.chars().next() + } + + /// Returns the next n characters without advancing the input + pub fn peek_n(&self, n: usize) -> &'a str { + self.0 + .char_indices() + .nth(n) + .map(|(idx, _)| &self.0[..idx]) + .unwrap_or(self.0) + } + + /// Returns the next word without advancing the input + pub fn peek_word(&self) -> &'a str { + self.0 + .char_indices() + .find(|(_, c)| c.is_whitespace()) + .map(|(idx, _)| &self.0[..idx]) + .unwrap_or(self.0) + } + + /// Checks if the input is empty + pub fn is_done(&self) -> bool { + self.0.is_empty() + } + + /// Returns the next character and advances the input + pub fn pop(&mut self) -> Option { + let c = self.peek()?; + self.advance(); + Some(c) + } + + /// Returns the next n characters and advances the input + pub fn pop_n(&mut self, n: usize) -> &str { + let s = self.peek_n(n); + self.advance_n_bytes(s.len()); + s + } + + /// Returns the next word and advances the input + pub fn pop_word(&mut self) -> &str { + let s = self.peek_word(); + self.advance_n_bytes(s.len()); + s + } + + /// Returns the rest of the input and advances the input + pub fn pop_all(&mut self) -> Option<&str> { + let s = self.0; + self.advance_n_bytes(self.0.len()); + Some(s) + } + + /// Returns the next word and advances the input + pub fn pop_to_next(&mut self, c: char) -> Option<&str> { + let pos = self.0.find(c)?; + let s = &self.0[..pos]; + self.advance_n_bytes(pos); + Some(s) + } + + /// Matches the case-insensitive string and advances the input if it matches + pub fn match_next(&mut self, string: &str) -> bool { + if self + .0 + .to_lowercase() + .starts_with(string.to_lowercase().as_str()) + { + self.advance_n_bytes(string.len()); + true + } else { + false + } + } + + /// Skip all whitespace at the front of the input + pub fn skip_whitespace(&mut self) { + while let Some(c) = self.peek() { + if c.is_whitespace() { + self.advance(); + } else { + break; + } + } + } + + /// Set the inner string + pub fn into_inner(self) -> &'a str { + self.0 + } + + #[allow(clippy::len_without_is_empty)] + pub fn len(&self) -> usize { + self.0.len() + } +} + +#[test] +fn test_parse_input() { + let mut input = ParseInput::new("The QuIck brown FOX jumps over the lazy dog"); + assert_eq!(input.peek(), Some('T')); + assert_eq!(input.peek_n(0), ""); + assert_eq!(input.peek_n(1), "T"); + assert_eq!(input.peek_n(2), "Th"); + assert_eq!(input.peek_n(3), "The"); + + assert_eq!(input.peek_word(), "The"); + input.pop_word(); + input.skip_whitespace(); + assert_eq!(input.peek_word(), "QuIck"); + + assert!(input.match_next("quick")); + input.pop(); + assert_eq!(input.peek_word(), "brown"); + + assert!(input.match_next("brown fox")); + assert_eq!(input.pop_all(), Some(" jumps over the lazy dog")); +} + +#[derive(Debug, Error)] +pub enum CommandArgParseError { + // these should be player facing and not disclose internal information + #[error("invalid argument, expected {expected} got {got}")] // e.g. "integer" number + InvalidArgument { expected: String, got: String }, + #[error("invalid argument length")] + InvalidArgLength, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum AbsoluteOrRelative { + Absolute(T), + Relative(T), // current value + T +} + +impl AbsoluteOrRelative +where + T: Add + Copy, +{ + pub fn get(&self, original: T) -> T { + match self { + Self::Absolute(num) => *num, + Self::Relative(num) => *num + original, + } + } +} + +impl CommandArg for AbsoluteOrRelative +where + T: CommandArg + Default, +{ + fn parse_arg(input: &mut ParseInput) -> Result { + input.skip_whitespace(); + if input.peek() == Some('~') { + input.advance(); + if input.peek() == Some(' ') || input.peek().is_none() { + Ok(AbsoluteOrRelative::Relative(T::default())) + } else { + Ok(AbsoluteOrRelative::Relative(T::parse_arg(input)?)) + } + } else if input.peek() == Some(' ') || input.peek().is_none() { + Err(CommandArgParseError::InvalidArgLength) + } else { + Ok(AbsoluteOrRelative::Absolute(T::parse_arg(input)?)) + } + } + + fn display() -> Parser { + T::display() + } +} + +#[test] +fn test_absolute_or_relative() { + let mut input = ParseInput::new("~"); + assert_eq!( + AbsoluteOrRelative::::parse_arg(&mut input).unwrap(), + AbsoluteOrRelative::Relative(0) + ); + assert!(input.is_done()); + + let mut input = ParseInput::new("~1"); + assert_eq!( + AbsoluteOrRelative::::parse_arg(&mut input).unwrap(), + AbsoluteOrRelative::Relative(1) + ); + assert!(input.is_done()); + + let mut input = ParseInput::new("~1.5"); + assert_eq!( + AbsoluteOrRelative::::parse_arg(&mut input).unwrap(), + AbsoluteOrRelative::Relative(1.5) + ); + assert!(input.is_done()); + + let mut input = ParseInput::new("1"); + assert_eq!( + AbsoluteOrRelative::::parse_arg(&mut input).unwrap(), + AbsoluteOrRelative::Absolute(1) + ); + assert!(input.is_done()); + + let mut input = ParseInput::new("1.5 "); + assert_eq!( + AbsoluteOrRelative::::parse_arg(&mut input).unwrap(), + AbsoluteOrRelative::Absolute(1.5) + ); + assert!(!input.is_done()); + + let mut input = ParseInput::new("1.5 2"); + assert_eq!( + AbsoluteOrRelative::::parse_arg(&mut input).unwrap(), + AbsoluteOrRelative::Absolute(1.5) + ); + assert!(!input.is_done()); + assert_eq!( + AbsoluteOrRelative::::parse_arg(&mut input).unwrap(), + AbsoluteOrRelative::Absolute(2.0) + ); + assert!(input.is_done()); +} + +impl Default for AbsoluteOrRelative { + fn default() -> Self { + AbsoluteOrRelative::Absolute(T::default()) + } +} diff --git a/crates/valence_command/src/parsers/angle.rs b/crates/valence_command/src/parsers/angle.rs new file mode 100644 index 000000000..f2c69fee6 --- /dev/null +++ b/crates/valence_command/src/parsers/angle.rs @@ -0,0 +1,20 @@ +use bevy_derive::Deref; + +use super::Parser; +use crate::parsers::{CommandArg, CommandArgParseError, ParseInput}; + +#[derive(Debug, Clone, Copy, PartialEq, Default, Deref)] +pub struct Angle(pub f32); + +impl CommandArg for Angle { + fn parse_arg(input: &mut ParseInput) -> Result { + input.skip_whitespace(); + let angle = f32::parse_arg(input)?; + + Ok(Angle(angle)) + } + + fn display() -> Parser { + Parser::Angle + } +} diff --git a/crates/valence_command/src/parsers/block_pos.rs b/crates/valence_command/src/parsers/block_pos.rs new file mode 100644 index 000000000..700b443a1 --- /dev/null +++ b/crates/valence_command/src/parsers/block_pos.rs @@ -0,0 +1,62 @@ +use super::Parser; +use crate::parsers::{AbsoluteOrRelative, CommandArg, CommandArgParseError, ParseInput}; + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] +pub struct BlockPos { + pub x: AbsoluteOrRelative, + pub y: AbsoluteOrRelative, + pub z: AbsoluteOrRelative, +} + +impl CommandArg for BlockPos { + fn parse_arg(input: &mut ParseInput) -> Result { + input.skip_whitespace(); + let x = AbsoluteOrRelative::::parse_arg(input)?; + input.skip_whitespace(); + let y = AbsoluteOrRelative::::parse_arg(input)?; + input.skip_whitespace(); + let z = AbsoluteOrRelative::::parse_arg(input)?; + + Ok(BlockPos { x, y, z }) + } + + fn display() -> Parser { + Parser::BlockPos + } +} + +#[test] +fn test_block_pos() { + let mut input = ParseInput::new("~-1 2 3"); + assert_eq!( + BlockPos::parse_arg(&mut input).unwrap(), + BlockPos { + x: AbsoluteOrRelative::Relative(-1), + y: AbsoluteOrRelative::Absolute(2), + z: AbsoluteOrRelative::Absolute(3) + } + ); + assert!(input.is_done()); + + let mut input = ParseInput::new("-1 ~2 3 "); + assert_eq!( + BlockPos::parse_arg(&mut input).unwrap(), + BlockPos { + x: AbsoluteOrRelative::Absolute(-1), + y: AbsoluteOrRelative::Relative(2), + z: AbsoluteOrRelative::Absolute(3) + } + ); + assert!(!input.is_done()); + + let mut input = ParseInput::new("-1 2 ~3 4"); + assert_eq!( + BlockPos::parse_arg(&mut input).unwrap(), + BlockPos { + x: AbsoluteOrRelative::Absolute(-1), + y: AbsoluteOrRelative::Absolute(2), + z: AbsoluteOrRelative::Relative(3) + } + ); + assert!(!input.is_done()); +} diff --git a/crates/valence_command/src/parsers/bool.rs b/crates/valence_command/src/parsers/bool.rs new file mode 100644 index 000000000..9dd83f541 --- /dev/null +++ b/crates/valence_command/src/parsers/bool.rs @@ -0,0 +1,42 @@ +use super::Parser; +use crate::parsers::{CommandArg, CommandArgParseError, ParseInput}; + +impl CommandArg for bool { + fn parse_arg(input: &mut ParseInput) -> Result { + input.skip_whitespace(); + if input.match_next("true") { + Ok(true) + } else if input.match_next("false") { + Ok(false) + } else { + Err(CommandArgParseError::InvalidArgument { + expected: "bool".to_string(), + got: input.peek_word().to_string(), + }) + } + } + + fn display() -> Parser { + Parser::Bool + } +} + +#[test] +fn test_bool() { + let mut input = ParseInput::new("true"); + assert!(bool::parse_arg(&mut input).unwrap()); + assert!(input.is_done()); + + let mut input = ParseInput::new("false"); + assert!(!bool::parse_arg(&mut input).unwrap()); + assert!(input.is_done()); + + let mut input = ParseInput::new("false "); + assert!(!bool::parse_arg(&mut input).unwrap()); + assert!(!input.is_done()); + + let mut input = ParseInput::new("falSe trUe"); + assert!(!bool::parse_arg(&mut input).unwrap()); + assert!(bool::parse_arg(&mut input).unwrap()); + assert!(input.is_done()); +} diff --git a/crates/valence_command/src/parsers/color.rs b/crates/valence_command/src/parsers/color.rs new file mode 100644 index 000000000..4695f38b3 --- /dev/null +++ b/crates/valence_command/src/parsers/color.rs @@ -0,0 +1,54 @@ +use valence_text::Color; + +use super::Parser; +use crate::parsers::{CommandArg, CommandArgParseError, ParseInput}; + +impl CommandArg for Color { + fn parse_arg(input: &mut ParseInput) -> Result { + input.skip_whitespace(); + if input.match_next("black") { + Ok(Self::BLACK) + } else if input.match_next("dark_blue") { + Ok(Self::DARK_BLUE) + } else if input.match_next("dark_green") { + Ok(Self::DARK_GREEN) + } else if input.match_next("dark_aqua") { + Ok(Self::DARK_AQUA) + } else if input.match_next("dark_red") { + Ok(Self::DARK_RED) + } else if input.match_next("dark_purple") { + Ok(Self::DARK_PURPLE) + } else if input.match_next("gold") { + Ok(Self::GOLD) + } else if input.match_next("gray") { + Ok(Self::GRAY) + } else if input.match_next("dark_gray") { + Ok(Self::DARK_GRAY) + } else if input.match_next("blue") { + Ok(Self::BLUE) + } else if input.match_next("green") { + Ok(Self::GREEN) + } else if input.match_next("aqua") { + Ok(Self::AQUA) + } else if input.match_next("red") { + Ok(Self::RED) + } else if input.match_next("light_purple") { + Ok(Self::LIGHT_PURPLE) + } else if input.match_next("yellow") { + Ok(Self::YELLOW) + } else if input.match_next("white") { + Ok(Self::WHITE) + } else if input.match_next("reset") { + Ok(Self::Reset) + } else { + Err(CommandArgParseError::InvalidArgument { + expected: "chat_color".to_string(), + got: "not a valid chat color".to_string(), + }) + } + } + + fn display() -> Parser { + Parser::Color + } +} diff --git a/crates/valence_command/src/parsers/column_pos.rs b/crates/valence_command/src/parsers/column_pos.rs new file mode 100644 index 000000000..041c3e3dd --- /dev/null +++ b/crates/valence_command/src/parsers/column_pos.rs @@ -0,0 +1,64 @@ +use super::Parser; +use crate::parsers::{AbsoluteOrRelative, CommandArg, CommandArgParseError, ParseInput}; + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] +pub struct ColumnPos { + pub x: AbsoluteOrRelative, + pub y: AbsoluteOrRelative, + pub z: AbsoluteOrRelative, +} + +impl CommandArg for ColumnPos { + fn parse_arg(input: &mut ParseInput) -> Result { + input.skip_whitespace(); + let x = AbsoluteOrRelative::::parse_arg(input)?; + input.skip_whitespace(); + let y = AbsoluteOrRelative::::parse_arg(input)?; + input.skip_whitespace(); + let z = AbsoluteOrRelative::::parse_arg(input)?; + + Ok(ColumnPos { x, y, z }) + } + + fn display() -> Parser { + Parser::ColumnPos + } +} + +#[test] +fn test_column_pos() { + let mut input = ParseInput::new("~-1 2 3"); + assert_eq!( + ColumnPos::parse_arg(&mut input).unwrap(), + ColumnPos { + x: AbsoluteOrRelative::Relative(-1), + y: AbsoluteOrRelative::Absolute(2), + z: AbsoluteOrRelative::Absolute(3) + } + ); + assert!(input.is_done()); + + let mut input = ParseInput::new("-1 ~2 3 hello"); + assert_eq!( + ColumnPos::parse_arg(&mut input).unwrap(), + ColumnPos { + x: AbsoluteOrRelative::Absolute(-1), + y: AbsoluteOrRelative::Relative(2), + z: AbsoluteOrRelative::Absolute(3) + } + ); + assert!(!input.is_done()); + input.skip_whitespace(); + assert!(input.match_next("hello")); + + let mut input = ParseInput::new("-1 2 ~3 4"); + assert_eq!( + ColumnPos::parse_arg(&mut input).unwrap(), + ColumnPos { + x: AbsoluteOrRelative::Absolute(-1), + y: AbsoluteOrRelative::Absolute(2), + z: AbsoluteOrRelative::Relative(3) + } + ); + assert!(!input.is_done()); +} diff --git a/crates/valence_command/src/parsers/entity_anchor.rs b/crates/valence_command/src/parsers/entity_anchor.rs new file mode 100644 index 000000000..88e42acb1 --- /dev/null +++ b/crates/valence_command/src/parsers/entity_anchor.rs @@ -0,0 +1,29 @@ +use super::Parser; +use crate::parsers::{CommandArg, CommandArgParseError, ParseInput}; + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] +pub enum EntityAnchor { + #[default] + Eyes, + Feet, +} + +impl CommandArg for EntityAnchor { + fn parse_arg(input: &mut ParseInput) -> Result { + input.skip_whitespace(); + if input.match_next("eyes") { + Ok(EntityAnchor::Eyes) + } else if input.match_next("feet") { + Ok(EntityAnchor::Feet) + } else { + Err(CommandArgParseError::InvalidArgument { + expected: "entity_anchor".to_string(), + got: "not a valid entity anchor".to_string(), + }) + } + } + + fn display() -> Parser { + Parser::EntityAnchor + } +} diff --git a/crates/valence_command/src/parsers/entity_selector.rs b/crates/valence_command/src/parsers/entity_selector.rs new file mode 100644 index 000000000..af48f76ac --- /dev/null +++ b/crates/valence_command/src/parsers/entity_selector.rs @@ -0,0 +1,136 @@ +use super::Parser; +use crate::parsers::{CommandArg, CommandArgParseError, ParseInput}; + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum EntitySelector { + SimpleSelector(EntitySelectors), + ComplexSelector(EntitySelectors, String), +} + +#[derive(Debug, Clone, PartialEq, Eq, Default)] +pub enum EntitySelectors { + AllEntities, + SinglePlayer(String), + #[default] + AllPlayers, + SelfPlayer, + NearestPlayer, + RandomPlayer, +} + +impl CommandArg for EntitySelector { + // we want to get either a simple string [`@e`, `@a`, `@p`, `@r`, + // ``] or a full selector: [`@e[]`, `@a[]`, + // `@p[]`, `@r[]`] the selectors can have spaces in + // them, so we need to be careful + fn parse_arg(input: &mut ParseInput) -> Result { + input.skip_whitespace(); + let mut simple_selector = None; + while let Some(c) = input.peek() { + match c { + '@' => { + input.pop(); // pop the '@' + match input.pop() { + Some('e') => simple_selector = Some(EntitySelectors::AllEntities), + Some('a') => simple_selector = Some(EntitySelectors::AllPlayers), + Some('p') => simple_selector = Some(EntitySelectors::NearestPlayer), + Some('r') => simple_selector = Some(EntitySelectors::RandomPlayer), + Some('s') => simple_selector = Some(EntitySelectors::SelfPlayer), + _ => { + return Err(CommandArgParseError::InvalidArgument { + expected: "entity selector".to_string(), + got: c.to_string(), + }) + } + } + if input.peek() != Some('[') { + // if there's no complex selector, we're done + return Ok(EntitySelector::SimpleSelector(simple_selector.unwrap())); + } + } + '[' => { + input.pop(); + if simple_selector.is_none() { + return Err(CommandArgParseError::InvalidArgument { + expected: "entity selector".to_string(), + got: c.to_string(), + }); + } + let mut s = String::new(); + while let Some(c) = input.pop() { + if c == ']' { + return Ok(EntitySelector::ComplexSelector( + simple_selector.unwrap(), + s.trim().to_string(), + )); + } else { + s.push(c); + } + } + } + _ => { + return Ok(EntitySelector::SimpleSelector( + EntitySelectors::SinglePlayer(String::parse_arg(input)?), + )) + } + } + } + Err(CommandArgParseError::InvalidArgLength) + } + + fn display() -> Parser { + Parser::Entity { + only_players: false, + single: false, + } + } +} + +#[test] +fn test_entity_selector() { + let mut input = ParseInput::new("@e"); + assert_eq!( + EntitySelector::parse_arg(&mut input).unwrap(), + EntitySelector::SimpleSelector(EntitySelectors::AllEntities) + ); + assert!(input.is_done()); + + let mut input = ParseInput::new("@e[distance=..5]"); + assert_eq!( + EntitySelector::parse_arg(&mut input).unwrap(), + EntitySelector::ComplexSelector(EntitySelectors::AllEntities, "distance=..5".to_string()) + ); + assert!(input.is_done()); + + let mut input = ParseInput::new("@s[distance=..5"); + assert!(EntitySelector::parse_arg(&mut input).is_err()); + assert!(input.is_done()); + + let mut input = ParseInput::new("@r[distance=..5] hello"); + assert_eq!( + EntitySelector::parse_arg(&mut input).unwrap(), + EntitySelector::ComplexSelector(EntitySelectors::RandomPlayer, "distance=..5".to_string()) + ); + assert!(!input.is_done()); + + let mut input = ParseInput::new("@p[distance=..5]hello"); + assert_eq!( + EntitySelector::parse_arg(&mut input).unwrap(), + EntitySelector::ComplexSelector(EntitySelectors::NearestPlayer, "distance=..5".to_string()) + ); + assert!(!input.is_done()); + + let mut input = ParseInput::new("@e[distance=..5] hello world"); + assert_eq!( + EntitySelector::parse_arg(&mut input).unwrap(), + EntitySelector::ComplexSelector(EntitySelectors::AllEntities, "distance=..5".to_string()) + ); + assert!(!input.is_done()); + + let mut input = ParseInput::new("@e[distance=..5]hello world"); + assert_eq!( + EntitySelector::parse_arg(&mut input).unwrap(), + EntitySelector::ComplexSelector(EntitySelectors::AllEntities, "distance=..5".to_string()) + ); + assert!(!input.is_done()); +} diff --git a/crates/valence_command/src/parsers/gamemode.rs b/crates/valence_command/src/parsers/gamemode.rs new file mode 100644 index 000000000..c2c8a6c68 --- /dev/null +++ b/crates/valence_command/src/parsers/gamemode.rs @@ -0,0 +1,28 @@ +use valence_server::protocol::packets::play::command_tree_s2c::Parser; +use valence_server::GameMode; + +use crate::parsers::{CommandArg, CommandArgParseError, ParseInput}; + +impl CommandArg for GameMode { + fn parse_arg(input: &mut ParseInput) -> Result { + input.skip_whitespace(); + if input.match_next("survival") { + Ok(GameMode::Survival) + } else if input.match_next("creative") { + Ok(GameMode::Creative) + } else if input.match_next("adventure") { + Ok(GameMode::Adventure) + } else if input.match_next("spectator") { + Ok(GameMode::Spectator) + } else { + Err(CommandArgParseError::InvalidArgument { + expected: "game_mode".to_string(), + got: input.peek_word().to_string(), + }) + } + } + + fn display() -> Parser { + Parser::GameMode + } +} diff --git a/crates/valence_command/src/parsers/inventory_slot.rs b/crates/valence_command/src/parsers/inventory_slot.rs new file mode 100644 index 000000000..d93129481 --- /dev/null +++ b/crates/valence_command/src/parsers/inventory_slot.rs @@ -0,0 +1,20 @@ +use bevy_derive::Deref; + +use super::Parser; +use crate::parsers::{CommandArg, CommandArgParseError, ParseInput}; + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Deref)] +pub struct InventorySlot(pub u32); + +impl CommandArg for InventorySlot { + fn parse_arg(input: &mut ParseInput) -> Result { + input.skip_whitespace(); + let slot = u32::parse_arg(input)?; + + Ok(InventorySlot(slot)) + } + + fn display() -> Parser { + Parser::ItemSlot + } +} diff --git a/crates/valence_command/src/parsers/numbers.rs b/crates/valence_command/src/parsers/numbers.rs new file mode 100644 index 000000000..9f8ad9761 --- /dev/null +++ b/crates/valence_command/src/parsers/numbers.rs @@ -0,0 +1,55 @@ +use super::Parser; +use crate::parsers::{CommandArg, CommandArgParseError, ParseInput}; +macro_rules! impl_parser_for_number { + ($type:ty, $name:expr, $parser:ident) => { + impl CommandArg for $type { + fn parse_arg(input: &mut ParseInput) -> Result { + input.skip_whitespace(); + let s = input.pop_word(); + + let parsed = s.parse::<$type>(); + + parsed.map_err(|_| CommandArgParseError::InvalidArgument { + expected: $name.to_string(), + got: s.to_string(), + }) + } + + fn display() -> Parser { + Parser::$parser { + min: None, + max: None, + } + } + } + }; +} + +impl_parser_for_number!(f32, "float", Float); +impl_parser_for_number!(f64, "double", Double); +impl_parser_for_number!(i32, "integer", Integer); +impl_parser_for_number!(i64, "long", Long); +impl_parser_for_number!(u32, "unsigned integer", Integer); + +#[test] +fn test_number() { + let mut input = ParseInput::new("1"); + assert_eq!(1, i32::parse_arg(&mut input).unwrap()); + assert!(input.is_done()); + + let mut input = ParseInput::new("1"); + assert_eq!(1, i64::parse_arg(&mut input).unwrap()); + assert!(input.is_done()); + + let mut input = ParseInput::new("1.0"); + assert_eq!(1.0, f32::parse_arg(&mut input).unwrap()); + assert!(input.is_done()); + + let mut input = ParseInput::new("1.0"); + assert_eq!(1.0, f64::parse_arg(&mut input).unwrap()); + assert!(input.is_done()); + + let mut input = ParseInput::new("3.40282347e+38 "); + assert_eq!(f32::MAX, f32::parse_arg(&mut input).unwrap()); + assert!(!input.is_done()); +} diff --git a/crates/valence_command/src/parsers/rotation.rs b/crates/valence_command/src/parsers/rotation.rs new file mode 100644 index 000000000..5bcb554c6 --- /dev/null +++ b/crates/valence_command/src/parsers/rotation.rs @@ -0,0 +1,21 @@ +use bevy_derive::Deref; + +use super::Parser; +use crate::parsers::vec2::Vec2; +use crate::parsers::{CommandArg, CommandArgParseError, ParseInput}; + +#[derive(Debug, Clone, Copy, PartialEq, Default, Deref)] +pub struct Rotation(pub Vec2); + +impl CommandArg for Rotation { + fn parse_arg(input: &mut ParseInput) -> Result { + input.skip_whitespace(); + let vec2 = Vec2::parse_arg(input)?; + + Ok(Rotation(vec2)) + } + + fn display() -> Parser { + Parser::Rotation + } +} diff --git a/crates/valence_command/src/parsers/score_holder.rs b/crates/valence_command/src/parsers/score_holder.rs new file mode 100644 index 000000000..654a5d3e8 --- /dev/null +++ b/crates/valence_command/src/parsers/score_holder.rs @@ -0,0 +1,27 @@ +use super::Parser; +use crate::parsers::entity_selector::EntitySelector; +use crate::parsers::{CommandArg, CommandArgParseError, ParseInput}; + +#[derive(Debug, Clone, PartialEq, Eq, Default)] +pub enum ScoreHolder { + Entity(EntitySelector), + #[default] + All, +} + +impl CommandArg for ScoreHolder { + fn parse_arg(input: &mut ParseInput) -> Result { + input.skip_whitespace(); + if input.peek() == Some('*') { + Ok(ScoreHolder::All) + } else { + Ok(ScoreHolder::Entity(EntitySelector::parse_arg(input)?)) + } + } + + fn display() -> Parser { + Parser::ScoreHolder { + allow_multiple: false, + } + } +} diff --git a/crates/valence_command/src/parsers/strings.rs b/crates/valence_command/src/parsers/strings.rs new file mode 100644 index 000000000..ceeabe159 --- /dev/null +++ b/crates/valence_command/src/parsers/strings.rs @@ -0,0 +1,107 @@ +use bevy_derive::Deref; +use valence_server::protocol::packets::play::command_tree_s2c::StringArg; + +use super::Parser; +use crate::parsers::{CommandArg, CommandArgParseError, ParseInput}; + +impl CommandArg for String { + fn parse_arg(input: &mut ParseInput) -> Result { + input.skip_whitespace(); + Ok(input.pop_word().to_string()) + } + + fn display() -> Parser { + Parser::String(StringArg::SingleWord) + } +} + +#[test] +fn test_string() { + let mut input = ParseInput::new("hello world"); + assert_eq!("hello", String::parse_arg(&mut input).unwrap()); + assert_eq!("world", String::parse_arg(&mut input).unwrap()); + assert!(input.is_done()); +} + +#[derive(Debug, Clone, PartialEq, Eq, Default, Deref)] +pub struct GreedyString(pub String); + +impl CommandArg for GreedyString { + fn parse_arg(input: &mut ParseInput) -> Result { + input.skip_whitespace(); + Ok(GreedyString( + match input.pop_all() { + Some(s) => s, + None => return Err(CommandArgParseError::InvalidArgLength), + } + .to_string(), + )) + } + + fn display() -> Parser { + Parser::String(StringArg::GreedyPhrase) + } +} + +#[test] +fn test_greedy_string() { + let mut input = ParseInput::new("hello world"); + assert_eq!( + "hello world", + GreedyString::parse_arg(&mut input).unwrap().0 + ); + assert!(input.is_done()); +} + +#[derive(Debug, Clone, PartialEq, Eq, Default, Deref)] +pub struct QuotableString(pub String); + +impl CommandArg for QuotableString { + fn parse_arg(input: &mut ParseInput) -> Result { + input.skip_whitespace(); + match input.peek() { + Some('"') => { + input.pop(); + let mut s = String::new(); + let mut escaped = false; + while let Some(c) = input.pop() { + if escaped { + s.push(c); + escaped = false; + } else if c == '\\' { + escaped = true; + } else if c == '"' { + return Ok(QuotableString(s)); + } else { + s.push(c); + } + } + Err(CommandArgParseError::InvalidArgLength) + } + Some(_) => Ok(QuotableString(String::parse_arg(input)?)), + None => Err(CommandArgParseError::InvalidArgLength), + } + } + + fn display() -> Parser { + Parser::String(StringArg::QuotablePhrase) + } +} + +#[test] +fn test_quotable_string() { + let mut input = ParseInput::new("\"hello world\""); + assert_eq!( + "hello world", + QuotableString::parse_arg(&mut input).unwrap().0 + ); + assert!(input.is_done()); + + let mut input = ParseInput::new("\"hello w\"orld"); + assert_eq!("hello w", QuotableString::parse_arg(&mut input).unwrap().0); + assert!(!input.is_done()); + + let mut input = ParseInput::new("hello world\""); + assert_eq!("hello", QuotableString::parse_arg(&mut input).unwrap().0); + assert!(!input.is_done()); +} diff --git a/crates/valence_command/src/parsers/swizzle.rs b/crates/valence_command/src/parsers/swizzle.rs new file mode 100644 index 000000000..161c94dd6 --- /dev/null +++ b/crates/valence_command/src/parsers/swizzle.rs @@ -0,0 +1,117 @@ +use super::Parser; +use crate::parsers::{CommandArg, CommandArgParseError, ParseInput}; + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] +pub struct Swizzle { + pub x: bool, + pub y: bool, + pub z: bool, +} + +impl CommandArg for Swizzle { + fn parse_arg(input: &mut ParseInput) -> Result { + input.skip_whitespace(); + let mut swizzle = Swizzle::default(); + while let Some(c) = input.peek() { + match c { + 'x' => swizzle.x = true, + 'y' => swizzle.y = true, + 'z' => swizzle.z = true, + _ => break, + } + input.pop(); + } + + Ok(swizzle) + } + + fn display() -> Parser { + Parser::Swizzle + } +} + +#[test] +fn test_swizzle() { + let mut input = ParseInput::new("xyzzzz"); + let swizzle = Swizzle::parse_arg(&mut input).unwrap(); + assert_eq!( + swizzle, + Swizzle { + x: true, + y: true, + z: true + } + ); + assert!(input.is_done()); + + let mut input = ParseInput::new("xzy"); + let swizzle = Swizzle::parse_arg(&mut input).unwrap(); + assert_eq!( + swizzle, + Swizzle { + x: true, + y: true, + z: true + } + ); + assert!(input.is_done()); + + let mut input = ParseInput::new("x"); + let swizzle = Swizzle::parse_arg(&mut input).unwrap(); + assert_eq!( + swizzle, + Swizzle { + x: true, + y: false, + z: false + } + ); + assert!(input.is_done()); + + let mut input = ParseInput::new("x y z zy xyz"); + let swizzle_a = Swizzle::parse_arg(&mut input).unwrap(); + let swizzle_b = Swizzle::parse_arg(&mut input).unwrap(); + let swizzle_c = Swizzle::parse_arg(&mut input).unwrap(); + let swizzle_d = Swizzle::parse_arg(&mut input).unwrap(); + let swizzle_e = Swizzle::parse_arg(&mut input).unwrap(); + assert_eq!( + swizzle_a, + Swizzle { + x: true, + y: false, + z: false + } + ); + assert_eq!( + swizzle_b, + Swizzle { + x: false, + y: true, + z: false + } + ); + assert_eq!( + swizzle_c, + Swizzle { + x: false, + y: false, + z: true + } + ); + assert_eq!( + swizzle_d, + Swizzle { + x: false, + y: true, + z: true + } + ); + assert_eq!( + swizzle_e, + Swizzle { + x: true, + y: true, + z: true + } + ); +} diff --git a/crates/valence_command/src/parsers/time.rs b/crates/valence_command/src/parsers/time.rs new file mode 100644 index 000000000..d1eff8ccd --- /dev/null +++ b/crates/valence_command/src/parsers/time.rs @@ -0,0 +1,83 @@ +use super::Parser; +use crate::parsers::{CommandArg, CommandArgParseError, ParseInput}; + +#[derive(Debug, Clone, Copy, PartialEq)] +pub enum Time { + Ticks(f32), + Seconds(f32), + Days(f32), +} + +impl CommandArg for Time { + fn parse_arg(input: &mut ParseInput) -> Result { + input.skip_whitespace(); + let mut number_str = String::new(); + while let Some(c) = input.pop() { + match c { + 't' => { + return Ok(Time::Ticks(number_str.parse::().map_err(|_| { + CommandArgParseError::InvalidArgument { + expected: "time".to_string(), + got: "not a valid time".to_string(), + } + })?)); + } + 's' => { + return Ok(Time::Seconds(number_str.parse::().map_err(|_| { + CommandArgParseError::InvalidArgument { + expected: "time".to_string(), + got: "not a valid time".to_string(), + } + })?)); + } + 'd' => { + return Ok(Time::Days(number_str.parse::().map_err(|_| { + CommandArgParseError::InvalidArgument { + expected: "time".to_string(), + got: "not a valid time".to_string(), + } + })?)); + } + _ => { + number_str.push(c); + } + } + } + if !number_str.is_empty() { + return Ok(Time::Ticks(number_str.parse::().map_err(|_| { + CommandArgParseError::InvalidArgument { + expected: "time".to_string(), + got: "not a valid time".to_string(), + } + })?)); + } + + Err(CommandArgParseError::InvalidArgument { + expected: "time".to_string(), + got: "not a valid time".to_string(), + }) + } + + fn display() -> Parser { + Parser::Time + } +} + +#[test] +fn test_time() { + let mut input = ParseInput::new("42.31t"); + let time = Time::parse_arg(&mut input).unwrap(); + assert_eq!(time, Time::Ticks(42.31)); + + let mut input = ParseInput::new("42.31"); + let time = Time::parse_arg(&mut input).unwrap(); + assert_eq!(time, Time::Ticks(42.31)); + + let mut input = ParseInput::new("1239.72s"); + let time = Time::parse_arg(&mut input).unwrap(); + assert_eq!(time, Time::Seconds(1239.72)); + + let mut input = ParseInput::new("133.1d"); + let time = Time::parse_arg(&mut input).unwrap(); + assert_eq!(time, Time::Days(133.1)); +} diff --git a/crates/valence_command/src/parsers/vec2.rs b/crates/valence_command/src/parsers/vec2.rs new file mode 100644 index 000000000..02235a8cf --- /dev/null +++ b/crates/valence_command/src/parsers/vec2.rs @@ -0,0 +1,56 @@ +use super::Parser; +use crate::parsers::{AbsoluteOrRelative, CommandArg, CommandArgParseError, ParseInput}; + +#[derive(Debug, Clone, Copy, PartialEq, Default)] +pub struct Vec2 { + pub x: AbsoluteOrRelative, + pub y: AbsoluteOrRelative, +} + +impl CommandArg for Vec2 { + fn parse_arg(input: &mut ParseInput) -> Result { + input.skip_whitespace(); + let x = AbsoluteOrRelative::::parse_arg(input)?; + input.skip_whitespace(); + let y = AbsoluteOrRelative::::parse_arg(input)?; + + Ok(Vec2 { x, y }) + } + + fn display() -> Parser { + Parser::Vec2 + } +} + +#[test] +fn test_vec2() { + let mut input = ParseInput::new("~-1.5 2.5"); + assert_eq!( + Vec2::parse_arg(&mut input).unwrap(), + Vec2 { + x: AbsoluteOrRelative::Relative(-1.5), + y: AbsoluteOrRelative::Absolute(2.5), + } + ); + assert!(input.is_done()); + + let mut input = ParseInput::new("-1.5 ~2.5 "); + assert_eq!( + Vec2::parse_arg(&mut input).unwrap(), + Vec2 { + x: AbsoluteOrRelative::Absolute(-1.5), + y: AbsoluteOrRelative::Relative(2.5), + } + ); + assert!(!input.is_done()); + + let mut input = ParseInput::new("-1.5 2.5 3.5"); + assert_eq!( + Vec2::parse_arg(&mut input).unwrap(), + Vec2 { + x: AbsoluteOrRelative::Absolute(-1.5), + y: AbsoluteOrRelative::Absolute(2.5), + } + ); + assert!(!input.is_done()); +} diff --git a/crates/valence_command/src/parsers/vec3.rs b/crates/valence_command/src/parsers/vec3.rs new file mode 100644 index 000000000..f5befdbed --- /dev/null +++ b/crates/valence_command/src/parsers/vec3.rs @@ -0,0 +1,59 @@ +use super::Parser; +use crate::parsers::{AbsoluteOrRelative, CommandArg, CommandArgParseError, ParseInput}; + +#[derive(Debug, Clone, Copy, PartialEq, Default)] +pub struct Vec3 { + pub x: AbsoluteOrRelative, + pub y: AbsoluteOrRelative, + pub z: AbsoluteOrRelative, +} + +impl CommandArg for Vec3 { + fn parse_arg(input: &mut ParseInput) -> Result { + let x = AbsoluteOrRelative::::parse_arg(input)?; + let y = AbsoluteOrRelative::::parse_arg(input)?; + let z = AbsoluteOrRelative::::parse_arg(input)?; + + Ok(Vec3 { x, y, z }) + } + + fn display() -> Parser { + Parser::Vec3 + } +} + +#[test] +fn test_vec3() { + let mut input = ParseInput::new("~-1.5 2.5 3.5"); + assert_eq!( + Vec3::parse_arg(&mut input).unwrap(), + Vec3 { + x: AbsoluteOrRelative::Relative(-1.5), + y: AbsoluteOrRelative::Absolute(2.5), + z: AbsoluteOrRelative::Absolute(3.5) + } + ); + assert!(input.is_done()); + + let mut input = ParseInput::new("-1.5 ~2.5 3.5 "); + assert_eq!( + Vec3::parse_arg(&mut input).unwrap(), + Vec3 { + x: AbsoluteOrRelative::Absolute(-1.5), + y: AbsoluteOrRelative::Relative(2.5), + z: AbsoluteOrRelative::Absolute(3.5) + } + ); + assert!(!input.is_done()); + + let mut input = ParseInput::new("-1.5 2.5 ~3.5 4.5"); + assert_eq!( + Vec3::parse_arg(&mut input).unwrap(), + Vec3 { + x: AbsoluteOrRelative::Absolute(-1.5), + y: AbsoluteOrRelative::Absolute(2.5), + z: AbsoluteOrRelative::Relative(3.5) + } + ); + assert!(!input.is_done()); +} diff --git a/crates/valence_command/src/scopes.rs b/crates/valence_command/src/scopes.rs new file mode 100644 index 000000000..ddfcca5ca --- /dev/null +++ b/crates/valence_command/src/scopes.rs @@ -0,0 +1,298 @@ +//! Scope graph for the Valence Command system. +//! +//! ## Breakdown +//! Each scope is a node in a graph. A path from one node to another indicates +//! that the first scope implies the second. A dot in the scope name indicates +//! a sub-scope. You can use this to create a hierarchy of scopes. For example, +//! the scope "valence.command" implies "valence.command.tp". this means that if +//! a player has the "valence.command" scope, they can use the "tp" command. +//! +//! You may also link scopes together in the registry. This is useful for admin +//! scope umbrellas. For example, if the scope "valence.admin" is linked to +//! "valence.command", It means that if a player has the "valence.admin" scope, +//! they can use all commands under the command scope. +//! +//! # Example +//! ``` +//! use valence_command::scopes::CommandScopeRegistry; +//! +//! let mut registry = CommandScopeRegistry::new(); +//! +//! // add a scope to the registry +//! registry.add_scope("valence.command.teleport"); +//! +//! // we added 4 scopes to the registry. "valence", "valence.command", "valence.command.teleport", +//! // and the root scope. +//! assert_eq!(registry.scope_count(), 4); +//! +//! registry.add_scope("valence.admin"); +//! +//! // add a scope to the registry with a link to another scope +//! registry.link("valence.admin", "valence.command.teleport"); +//! +//! // the "valence.admin" scope implies the "valence.command.teleport" scope +//! assert_eq!( +//! registry.grants("valence.admin", "valence.command.teleport"), +//! true +//! ); +//! ``` + +use std::collections::{BTreeSet, HashMap}; +use std::fmt::{Debug, Formatter}; + +use bevy_app::{App, Plugin, Update}; +use bevy_derive::{Deref, DerefMut}; +use bevy_ecs::prelude::{Component, ResMut}; +use bevy_ecs::query::Changed; +use bevy_ecs::system::{Query, Resource}; +use petgraph::dot; +use petgraph::dot::Dot; +use petgraph::prelude::*; + +pub struct CommandScopePlugin; + +impl Plugin for CommandScopePlugin { + fn build(&self, app: &mut App) { + app.init_resource::() + .add_systems(Update, add_new_scopes); + } +} + +/// Command scope Component for players. This is a list of scopes that a player +/// has. If a player has a scope, they can use any command that requires +/// that scope. +#[derive( + Debug, Clone, PartialEq, Eq, Hash, Ord, PartialOrd, Component, Default, Deref, DerefMut, +)] +pub struct CommandScopes(pub BTreeSet); + +/// This system makes it a bit easier to add new scopes to the registry without +/// having to explicitly add them to the registry on app startup. +fn add_new_scopes( + mut registry: ResMut, + scopes: Query<&CommandScopes, Changed>, +) { + for scopes in scopes.iter() { + for scope in scopes.iter() { + if !registry.string_to_node.contains_key(scope) { + registry.add_scope(scope); + } + } + } +} + +impl CommandScopes { + /// create a new scope component + pub fn new() -> Self { + Self::default() + } + + /// add a scope to this component + pub fn add(&mut self, scope: &str) { + self.0.insert(scope.into()); + } +} + +/// Store the scope graph and provide methods for querying it. +#[derive(Clone, Resource)] +pub struct CommandScopeRegistry { + graph: Graph, + string_to_node: HashMap, + root: NodeIndex, +} + +impl Default for CommandScopeRegistry { + fn default() -> Self { + let mut graph = Graph::new(); + let root = graph.add_node("root".to_string()); + Self { + graph, + string_to_node: HashMap::from([("root".to_string(), root)]), + root, + } + } +} + +impl Debug for CommandScopeRegistry { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!( + f, + "{:?}", + Dot::with_config(&self.graph, &[dot::Config::EdgeNoLabel]) + )?; + Ok(()) + } +} + +impl CommandScopeRegistry { + /// Create a new scope registry. + pub fn new() -> Self { + Self::default() + } + + /// Add a scope to the registry. + /// + /// # Example + /// ``` + /// use valence_command::CommandScopeRegistry; + /// + /// let mut registry = CommandScopeRegistry::new(); + /// + /// // creates two nodes. "valence" and "command" with an edge from "valence" to "command" + /// registry.add_scope("valence.command"); + /// // creates one node. "valence.command.tp" with an edge from "valence.command" to + /// // "valence.command.tp" + /// registry.add_scope("valence.command.tp"); + /// + /// // the root node is always present + /// assert_eq!(registry.scope_count(), 4); + /// ``` + pub fn add_scope(&mut self, scope: impl Into) { + let scope = scope.into(); + if self.string_to_node.contains_key(&scope) { + return; + } + let mut current_node = self.root; + let mut prefix = String::new(); + for part in scope.split('.') { + let node = self + .string_to_node + .entry(prefix.clone() + part) + .or_insert_with(|| { + let node = self.graph.add_node(part.to_string()); + self.graph.add_edge(current_node, node, ()); + node + }); + current_node = *node; + + prefix = prefix + part + "."; + } + } + + /// Remove a scope from the registry. + /// + /// # Example + /// ``` + /// use valence_command::CommandScopeRegistry; + /// + /// let mut registry = CommandScopeRegistry::new(); + /// + /// registry.add_scope("valence.command"); + /// registry.add_scope("valence.command.tp"); + /// + /// assert_eq!(registry.scope_count(), 4); + /// + /// registry.remove_scope("valence.command.tp"); + /// + /// assert_eq!(registry.scope_count(), 3); + /// ``` + pub fn remove_scope(&mut self, scope: &str) { + if let Some(node) = self.string_to_node.remove(scope) { + self.graph.remove_node(node); + }; + } + + /// Check if a scope grants another scope. + /// + /// # Example + /// ``` + /// use valence_command::CommandScopeRegistry; + /// + /// let mut registry = CommandScopeRegistry::new(); + /// + /// registry.add_scope("valence.command"); + /// registry.add_scope("valence.command.tp"); + /// + /// assert!(registry.grants("valence.command", "valence.command.tp")); // command implies tp + /// assert!(!registry.grants("valence.command.tp", "valence.command")); // tp does not imply command + /// ``` + pub fn grants(&self, scope: &str, other: &str) -> bool { + if scope == other { + return true; + } + + let scope_idx = match self.string_to_node.get(scope) { + None => { + return false; + } + Some(idx) => *idx, + }; + let other_idx = match self.string_to_node.get(other) { + None => { + return false; + } + Some(idx) => *idx, + }; + + if scope_idx == self.root { + return true; + } + + // if we can reach the other scope from the scope, then the scope + // grants the other scope + let mut dfs = Dfs::new(&self.graph, scope_idx); + while let Some(node) = dfs.next(&self.graph) { + if node == other_idx { + return true; + } + } + false + } + + /// do any of the scopes in the list grant the other scope? + /// + /// # Example + /// ``` + /// use valence_command::CommandScopeRegistry; + /// + /// let mut registry = CommandScopeRegistry::new(); + /// + /// registry.add_scope("valence.command"); + /// registry.add_scope("valence.command.tp"); + /// registry.add_scope("valence.admin"); + /// + /// assert!(registry.any_grants( + /// &vec!["valence.admin", "valence.command"], + /// "valence.command.tp" + /// )); + /// ``` + pub fn any_grants(&self, scopes: &Vec<&str>, other: &str) -> bool { + for scope in scopes { + if self.grants(scope, other) { + return true; + } + } + false + } + + /// Create a link between two scopes so that one implies the other. It will + /// add them if they don't exist. + /// + /// # Example + /// ``` + /// use valence_command::CommandScopeRegistry; + /// + /// let mut registry = CommandScopeRegistry::new(); + /// + /// registry.add_scope("valence.command.tp"); + /// + /// registry.link("valence.admin", "valence.command"); + /// + /// assert!(registry.grants("valence.admin", "valence.command")); + /// assert!(registry.grants("valence.admin", "valence.command.tp")); + /// ``` + pub fn link(&mut self, scope: &str, other: &str) { + self.add_scope(scope); + self.add_scope(other); + + let scope_idx = self.string_to_node.get(scope).unwrap(); + let other_idx = self.string_to_node.get(other).unwrap(); + + self.graph.add_edge(*scope_idx, *other_idx, ()); + } + + /// Get the number of scopes in the registry. + pub fn scope_count(&self) -> usize { + self.graph.node_count() + } +} diff --git a/crates/valence_command_macros/Cargo.toml b/crates/valence_command_macros/Cargo.toml new file mode 100644 index 000000000..60657a4f5 --- /dev/null +++ b/crates/valence_command_macros/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "valence_command_macros" +description = "Simplify the creation of Valence commands with a derive macro." +readme = "README.md" +version.workspace = true +edition.workspace = true +repository.workspace = true +documentation.workspace = true +license.workspace = true + +[lib] +proc-macro = true + +[dependencies] +syn = { version = "2.0", features = ["full"] } +quote = "1.0" +proc-macro2 = "1.0" + diff --git a/crates/valence_command_macros/README.md b/crates/valence_command_macros/README.md new file mode 100644 index 000000000..263919005 --- /dev/null +++ b/crates/valence_command_macros/README.md @@ -0,0 +1,143 @@ +Simplify the creation of Valence commands with a derive macro. + +## Usage + +```rust +#[derive(Command, Debug, Clone)] +#[paths("teleport", "tp")] +#[scopes("valence.command.teleport")] +enum TeleportCommand { + #[paths = "{location}"] + ExecutorToLocation { location: Vec3Parser }, + #[paths = "{target}"] + ExecutorToTarget { target: EntitySelector }, + #[paths = "{from} {to}"] + TargetToTarget { + from: EntitySelector, + to: EntitySelector, + }, + #[paths = "{target} {location}"] + TargetToLocation { + target: EntitySelector, + location: Vec3Parser, + }, +} + +#[derive(Command, Debug, Clone)] +#[paths("gamemode", "gm")] +#[scopes("valence.command.gamemode")] +enum GamemodeCommand { + #[paths("survival", "{/} gms")] + Survival, + #[paths("creative", "{/} gmc")] + Creative, + #[paths("adventure", "{/} gma")] + Adventure, + #[paths("spectator", "{/} gmsp")] + Spectator, +} + +#[derive(Command, Debug, Clone)] +#[paths("test", "t")] +#[scopes("valence.command.test")] +#[allow(dead_code)] +enum TestCommand { + // 3 literals with an arg each + #[paths("a {a} b {b} c {c}", "{a} {b} {c}")] + A { a: String, b: i32, c: f32 }, + // 2 literals with an arg last being optional (Because of the greedy string before the end + // this is technically unreachable) + #[paths = "a {a} {b} b {c?}"] + B { + a: Vec3Parser, + b: GreedyString, + c: Option, + }, + // greedy string optional arg + #[paths = "a {a} b {b?}"] + C { a: String, b: Option }, + // greedy string required arg + #[paths = "a {a} b {b}"] + D { a: String, b: GreedyString }, + // five optional args and an ending greedyString + #[paths("options {a?} {b?} {c?} {d?} {e?}", "options {b?} {a?} {d?} {c?} {e?}")] + E { + a: Option, + b: Option, + c: Option, + d: Option, + e: Option, + }, +} +``` + +## Attributes + +### `#[paths(...)]` or `#[paths = "..."]` + +The `#[paths(...)]` or `#[paths = "..."]` attribute is used to specify the different paths that can be used to invoke +the command. The paths are specified as string literals, where any arguments are enclosed in curly braces `{}`. +The arguments are then mapped to fields in the command enum variant. + +For example, in the `Teleport` enum, the `ExecutorToLocation` variant has a path of `{location}`, which means it expects +a single argument called `location` of type `Vec3Parser`. The `ExecutorToTarget` variant has a path of `{target}`, which +expects a single argument called `target` of type `EntitySelector`. + +The paths attribute can have multiple values separated by commas, representing alternative paths that can be used to +invoke the command. These alternative paths can have different argument orders, but they must have the same arguments. + +Their are two special paths that can be used. The first is `{/}`, which represents the root command, this can only be +used at the start of the command to specify it as a direct child of the root node. The second is `{?}`, which +represents an optional argument. The optional argument must only be followed by other optional arguments or the end of +the path. + +### `#[scopes(...)]` or `#[scopes = "..."]` + +The `#[scopes(...)]` or `#[scopes = "..."]` attribute is used to specify the scopes that the command belongs to. Scopes +are used to specify who can use the command. The scopes are specified as string literals, where each scope is separated +by a colon. + +For example, in the `Teleport` enum, the variants are assigned the scope `valence:command:teleport`, which means they +can be used by anyone with the `valence:command:teleport`, `valence:command` or `valence` scope. + +The scopes attribute can have multiple values separated by commas, representing the different scopes that the command +belongs to. + +## How do command graphs work anyway? + +This is the core of the command system. It is a graph of `CommandNode`s that are connected by the `CommandEdgeType`. The +graph is used to determine what command to run when a command is entered. The graph is also used to generate the command +tree that is sent to the client. You can think of it as a tree where each leaf is part of a command, and the path to the +leaf is the command. See the documentation for `command.rs` in `valence_command` for more information. + + +### Our teleport command from the example (made with graphviz) +```text + ┌────────────────────────────────┐ + │ Root │ ─┐ + └────────────────────────────────┘ │ + │ │ + │ Child │ + ▼ │ + ┌────────────────────────────────┐ │ + │ Literal: tp │ │ + └────────────────────────────────┘ │ + │ │ + │ Redirect │ Child + ▼ ▼ +┌──────────────────────────────────┐ Child ┌──────────────────────────────────────────────────────────────────────────────┐ +│ Argument: │ ◀─────── │ Literal: teleport │ +└──────────────────────────────────┘ └──────────────────────────────────────────────────────────────────────────────┘ + │ │ + │ Child │ Child + ▼ ▼ +┌──────────────────────────────────┐ Child ┌────────────────────────────────┐ ┌──────────────────────────────────┐ +│ Argument: │ ◀─────── │ Argument: │ │ Argument: │ +└──────────────────────────────────┘ └────────────────────────────────┘ └──────────────────────────────────┘ + │ + │ Child + ▼ + ┌────────────────────────────────┐ + │ Argument: │ + └────────────────────────────────┘ +``` \ No newline at end of file diff --git a/crates/valence_command_macros/src/lib.rs b/crates/valence_command_macros/src/lib.rs new file mode 100644 index 000000000..17abad06b --- /dev/null +++ b/crates/valence_command_macros/src/lib.rs @@ -0,0 +1,706 @@ +use proc_macro::TokenStream; +use proc_macro2::{Ident, TokenTree}; +use quote::{format_ident, quote}; +use syn::{parse_macro_input, Attribute, Data, DeriveInput, Error, Expr, Fields, Meta, Result}; + +#[proc_macro_derive(Command, attributes(command, scopes, paths))] +pub fn derive_command(input: TokenStream) -> TokenStream { + let input = parse_macro_input!(input as DeriveInput); + match command(input) { + Ok(expansion) => expansion, + Err(err) => err.to_compile_error().into(), + } +} + +fn command(input: DeriveInput) -> Result { + let input_name = input.ident; + + let outer_scopes = input + .attrs + .iter() + .filter_map(|attr| get_lit_list_attr(attr, "scopes")) + .next() + .unwrap_or(Vec::new()); + + match input.data { + Data::Enum(ref data_enum) => { + let mut alias_paths = match input.attrs.iter().filter_map(parse_path).next() { + // there should only be one base command name set + Some(paths) => paths, + None => { + return Err(Error::new_spanned( + input_name, + "No paths attribute found for command enum", + )) + } + }; + + let base_path = alias_paths.remove(0); + + let fields = &data_enum.variants; + let mut paths = Vec::new(); + + for variant in fields { + for attr in variant.attrs.iter() { + if let Some(attr_paths) = parse_path(attr) { + paths.push((attr_paths, variant.fields.clone(), variant.ident.clone())); + } + } + } + + let mut expanded_nodes = Vec::new(); + + for (paths, fields, variant_ident) in paths { + expanded_nodes.push({ + let processed = process_paths_enum( + &input_name, + paths, + &fields, + variant_ident.clone(), + true, + ); + quote! { #processed; } + }); + } + + let base_command_expansion = { + let processed = process_paths_enum( + &input_name, + vec![base_path], + &Fields::Unit, + format_ident!("{}Root", input_name), // this is more of placeholder + // (should never be used) + false, + ); // this will error if the base path has args + let mut expanded_main_command = quote! { + let command_root_node = #processed + }; + + if !outer_scopes.is_empty() { + expanded_main_command = quote! { + #expanded_main_command + .with_scopes(vec![#(#outer_scopes),*]) + } + } + + quote! { + #expanded_main_command.id(); + } + }; + + let command_alias_expansion = { + let mut alias_expansion = quote! {}; + for path in alias_paths { + let processed = process_paths_enum( + &input_name, + vec![path], + &Fields::Unit, + format_ident!("{}Root", input_name), + false, + ); + + alias_expansion = quote! { + #alias_expansion + + #processed + .redirect_to(command_root_node) + }; + + if !outer_scopes.is_empty() { + alias_expansion = quote! { + #alias_expansion + .with_scopes(vec![#(#outer_scopes),*]) + } + } + + alias_expansion = quote! { + #alias_expansion; + } + } + + alias_expansion + }; + + let _new_struct = format_ident!("{}Command", input_name); + + Ok(TokenStream::from(quote! { + + impl valence::command::Command for #input_name { + fn assemble_graph(command_graph: &mut valence::command::graph::CommandGraphBuilder) { + use valence::command::parsers::CommandArg; + #base_command_expansion + + #command_alias_expansion + + #(#expanded_nodes)* + } + } + })) + } + Data::Struct(x) => { + let mut paths = Vec::new(); + + for attr in input.attrs.iter() { + if let Some(attr_paths) = parse_path(attr) { + paths.push(attr_paths); + } + } + + let mut expanded_nodes = Vec::new(); + + for path in paths { + expanded_nodes.push({ + let mut processed = + process_paths_struct(&input_name, path, &x.fields, outer_scopes.clone()); + // add scopes + + if !outer_scopes.is_empty() { + processed = quote! { + #processed + .with_scopes(vec![#(#outer_scopes),*]) + } + } + + quote! { #processed; } + }); + } + + Ok(TokenStream::from(quote! { + + impl valence::command::Command for #input_name { + fn assemble_graph(command_graph: &mut valence::command::graph::CommandGraphBuilder) { + use valence::command::parsers::CommandArg; + #(#expanded_nodes)* + } + } + })) + } + Data::Union(x) => Err(Error::new_spanned( + x.union_token, + "Command enum must be an enum, not a union", + )), + } +} + +fn process_paths_enum( + enum_name: &Ident, + paths: Vec<(Vec, bool)>, + fields: &Fields, + variant_ident: Ident, + executables: bool, +) -> proc_macro2::TokenStream { + let mut inner_expansion = quote! {}; + let mut first = true; + + for path in paths { + if !first { + inner_expansion = if executables && !path.1 { + quote! { + #inner_expansion; + + command_graph.at(command_root_node) + } + } else { + quote! { + #inner_expansion; + + command_graph.root() + } + }; + } else { + inner_expansion = if executables && !path.1 { + quote! { + command_graph.at(command_root_node) + } + } else { + quote! { + command_graph.root() + } + }; + + first = false; + } + + let path = path.0; + + let mut final_executable = Vec::new(); + for (i, arg) in path.iter().enumerate() { + match arg { + CommandArg::Literal(lit) => { + inner_expansion = quote! { + #inner_expansion.literal(#lit) + + }; + if executables && i == path.len() - 1 { + inner_expansion = quote! { + #inner_expansion + .with_executable(|s| #enum_name::#variant_ident{#(#final_executable,)*}) + }; + } + } + CommandArg::Required(ident) => { + let field_type = &fields + .iter() + .find(|field| field.ident.as_ref().unwrap() == ident) + .expect("Required arg not found") + .ty; + let ident_string = ident.to_string(); + + inner_expansion = quote! { + #inner_expansion + .argument(#ident_string) + .with_parser::<#field_type>() + }; + + final_executable.push(quote! { + #ident: #field_type::parse_arg(s).unwrap() + }); + + if i == path.len() - 1 { + inner_expansion = quote! { + #inner_expansion + .with_executable(|s| { + #enum_name::#variant_ident { + #(#final_executable,)* + } + }) + }; + } + } + CommandArg::Optional(ident) => { + let field_type = &fields + .iter() + .find(|field| field.ident.as_ref().unwrap() == ident) + .expect("Optional arg not found") + .ty; + let so_far_ident = format_ident!("graph_til_{}", ident); + + // get what is inside the Option<...> + let option_inner = match field_type { + syn::Type::Path(ref type_path) => { + let path = &type_path.path; + if path.segments.len() != 1 { + return Error::new_spanned( + path, + "Option type must be a single path segment", + ) + .into_compile_error(); + } + let segment = &path.segments.first().unwrap(); + if segment.ident != "Option" { + return Error::new_spanned( + &segment.ident, + "Option type must be a option", + ) + .into_compile_error(); + } + match &segment.arguments { + syn::PathArguments::AngleBracketed(ref angle_bracketed) => { + if angle_bracketed.args.len() != 1 { + return Error::new_spanned( + angle_bracketed, + "Option type must have a single generic argument", + ) + .into_compile_error(); + } + match angle_bracketed.args.first().unwrap() { + syn::GenericArgument::Type(ref generic_type) => { + generic_type + } + _ => { + return Error::new_spanned( + angle_bracketed, + "Option type must have a single generic argument", + ) + .into_compile_error(); + } + } + } + _ => { + return Error::new_spanned( + segment, + "Option type must have a single generic argument", + ) + .into_compile_error(); + } + } + } + _ => { + return Error::new_spanned( + field_type, + "Option type must be a single path segment", + ) + .into_compile_error(); + } + }; + + let ident_string = ident.to_string(); + + // find the ident of all following optional args + let mut next_optional_args = Vec::new(); + for next_arg in path.iter().skip(i + 1) { + match next_arg { + CommandArg::Optional(ident) => next_optional_args.push(ident), + _ => { + return Error::new_spanned( + variant_ident, + "Only optional args can follow an optional arg", + ) + .into_compile_error(); + } + } + } + + inner_expansion = quote! { + let #so_far_ident = {#inner_expansion + .with_executable(|s| { + #enum_name::#variant_ident { + #(#final_executable,)* + #ident: None, + #(#next_optional_args: None,)* + } + }) + .id()}; + + command_graph.at(#so_far_ident) + .argument(#ident_string) + .with_parser::<#option_inner>() + }; + + final_executable.push(quote! { + #ident: Some(#option_inner::parse_arg(s).unwrap()) + }); + + if i == path.len() - 1 { + inner_expansion = quote! { + #inner_expansion + .with_executable(|s| { + #enum_name::#variant_ident { + #(#final_executable,)* + } + }) + }; + } + } + } + } + } + quote!(#inner_expansion) +} + +fn process_paths_struct( + struct_name: &Ident, + paths: Vec<(Vec, bool)>, + fields: &Fields, + outer_scopes: Vec, +) -> proc_macro2::TokenStream { + let mut inner_expansion = quote! {}; + let mut first = true; + + for path in paths { + if !first { + inner_expansion = quote! { + #inner_expansion; + + command_graph.root() + }; + } else { + inner_expansion = quote! { + command_graph.root() + }; + + first = false; + } + + let path = path.0; + + let mut final_executable = Vec::new(); + let mut path_first = true; + for (i, arg) in path.iter().enumerate() { + match arg { + CommandArg::Literal(lit) => { + inner_expansion = quote! { + #inner_expansion.literal(#lit) + + }; + if i == path.len() - 1 { + inner_expansion = quote! { + #inner_expansion + .with_executable(|s| #struct_name{#(#final_executable,)*}) + }; + } + + if path_first { + inner_expansion = quote! { + #inner_expansion + .with_scopes(vec![#(#outer_scopes),*]) + }; + path_first = false; + } + } + CommandArg::Required(ident) => { + let field_type = &fields + .iter() + .find(|field| field.ident.as_ref().unwrap() == ident) + .expect("Required arg not found") + .ty; + let ident_string = ident.to_string(); + + inner_expansion = quote! { + #inner_expansion + .argument(#ident_string) + .with_parser::<#field_type>() + }; + + final_executable.push(quote! { + #ident: #field_type::parse_arg(s).unwrap() + }); + + if i == path.len() - 1 { + inner_expansion = quote! { + #inner_expansion + .with_executable(|s| { + #struct_name { + #(#final_executable,)* + } + }) + }; + } + + if path_first { + inner_expansion = quote! { + #inner_expansion + .with_scopes(vec![#(#outer_scopes),*]) + }; + path_first = false; + } + } + CommandArg::Optional(ident) => { + let field_type = &fields + .iter() + .find(|field| field.ident.as_ref().unwrap() == ident) + .expect("Optional arg not found") + .ty; + let so_far_ident = format_ident!("graph_til_{}", ident); + + // get what is inside the Option<...> + let option_inner = match field_type { + syn::Type::Path(ref type_path) => { + let path = &type_path.path; + if path.segments.len() != 1 { + return Error::new_spanned( + path, + "Option type must be a single path segment", + ) + .into_compile_error(); + } + let segment = &path.segments.first().unwrap(); + if segment.ident != "Option" { + return Error::new_spanned( + &segment.ident, + "Option type must be a option", + ) + .into_compile_error(); + } + match &segment.arguments { + syn::PathArguments::AngleBracketed(ref angle_bracketed) => { + if angle_bracketed.args.len() != 1 { + return Error::new_spanned( + angle_bracketed, + "Option type must have a single generic argument", + ) + .into_compile_error(); + } + match angle_bracketed.args.first().unwrap() { + syn::GenericArgument::Type(ref generic_type) => { + generic_type + } + _ => { + return Error::new_spanned( + angle_bracketed, + "Option type must have a single generic argument", + ) + .into_compile_error(); + } + } + } + _ => { + return Error::new_spanned( + segment, + "Option type must have a single generic argument", + ) + .into_compile_error(); + } + } + } + _ => { + return Error::new_spanned( + field_type, + "Option type must be a single path segment", + ) + .into_compile_error(); + } + }; + + let ident_string = ident.to_string(); + + // find the ident of all following optional args + let mut next_optional_args = Vec::new(); + for next_arg in path.iter().skip(i + 1) { + match next_arg { + CommandArg::Optional(ident) => next_optional_args.push(ident), + _ => { + return Error::new_spanned( + struct_name, + "Only optional args can follow an optional arg", + ) + .into_compile_error(); + } + } + } + + inner_expansion = quote! { + let #so_far_ident = {#inner_expansion + .with_executable(|s| { + #struct_name { + #(#final_executable,)* + #ident: None, + #(#next_optional_args: None,)* + } + }) + .id()}; + + command_graph.at(#so_far_ident) + .argument(#ident_string) + .with_parser::<#option_inner>() + }; + + final_executable.push(quote! { + #ident: Some(#option_inner::parse_arg(s).unwrap()) + }); + + if i == path.len() - 1 { + inner_expansion = quote! { + #inner_expansion + .with_executable(|s| { + #struct_name { + #(#final_executable,)* + } + }) + }; + } + + if path_first { + inner_expansion = quote! { + #inner_expansion + .with_scopes(vec![#(#outer_scopes),*]) + }; + path_first = false; + } + } + } + } + } + quote!(#inner_expansion) +} + +#[derive(Debug)] +enum CommandArg { + Required(Ident), + Optional(Ident), + Literal(String), +} + +// example input: #[paths = "strawberry {0?}"] +// example output: [CommandArg::Literal("Strawberry"), CommandArg::Optional(0)] +fn parse_path(path: &Attribute) -> Option, bool)>> { + let path_strings: Vec = get_lit_list_attr(path, "paths")?; + + let mut paths = Vec::new(); + // we now have the path as a string eg "strawberry {0?}" + // the first word is a literal + // the next word is an optional arg with the index 0 + for path_str in path_strings { + let mut args = Vec::new(); + let at_root = path_str.starts_with("{/}"); + + for word in path_str + .split_whitespace() + .skip(if at_root { 1 } else { 0 }) + { + if word.starts_with('{') && word.ends_with('}') { + if word.ends_with("?}") { + args.push(CommandArg::Optional(format_ident!( + "{}", + word[1..word.len() - 2].to_string() + ))); + } else { + args.push(CommandArg::Required(format_ident!( + "{}", + word[1..word.len() - 1].to_string() + ))); + } + } else { + args.push(CommandArg::Literal(word.to_string())); + } + } + paths.push((args, at_root)); + } + + Some(paths) +} + +fn get_lit_list_attr(attr: &Attribute, ident: &str) -> Option> { + match attr.meta { + Meta::NameValue(ref key_value) => { + if !key_value.path.is_ident(ident) { + return None; + } + + match key_value.value { + Expr::Lit(ref lit) => match lit.lit { + syn::Lit::Str(ref lit_str) => Some(vec![lit_str.value()]), + _ => None, + }, + _ => None, + } + } + Meta::List(ref list) => { + if !list.path.is_ident(ident) { + return None; + } + + let mut path_strings = Vec::new(); + // parse as array with strings + let mut comma_next = false; + for token in list.tokens.clone() { + match token { + TokenTree::Literal(lit) => { + if comma_next { + return None; + } + let lit_str = lit.to_string(); + path_strings.push( + lit_str + .strip_prefix('"') + .unwrap() + .strip_suffix('"') + .unwrap() + .to_string(), + ); + comma_next = true; + } + TokenTree::Punct(punct) => { + if punct.as_char() != ',' || !comma_next { + return None; + } + comma_next = false; + } + _ => return None, + } + } + Some(path_strings) + } + _ => None, + } +} diff --git a/crates/valence_protocol/src/packets/play/command_tree_s2c.rs b/crates/valence_protocol/src/packets/play/command_tree_s2c.rs index 6a6330b47..b0da45033 100644 --- a/crates/valence_protocol/src/packets/play/command_tree_s2c.rs +++ b/crates/valence_protocol/src/packets/play/command_tree_s2c.rs @@ -8,28 +8,28 @@ use valence_ident::Ident; use crate::{Decode, Encode, Packet, VarInt}; #[derive(Clone, Debug, Encode, Decode, Packet)] -pub struct CommandTreeS2c<'a> { - pub commands: Vec>, +pub struct CommandTreeS2c { + pub commands: Vec, pub root_index: VarInt, } #[derive(Clone, Debug)] -pub struct Node<'a> { - pub children: Vec, - pub data: NodeData<'a>, +pub struct Node { + pub data: NodeData, pub executable: bool, + pub children: Vec, pub redirect_node: Option, } -#[derive(Clone, Debug)] -pub enum NodeData<'a> { +#[derive(Clone, Debug, PartialEq)] +pub enum NodeData { Root, Literal { - name: &'a str, + name: String, }, Argument { - name: &'a str, - parser: Parser<'a>, + name: String, + parser: Parser, suggestion: Option, }, } @@ -43,8 +43,8 @@ pub enum Suggestion { SummonableEntities, } -#[derive(Clone, Debug)] -pub enum Parser<'a> { +#[derive(Clone, Debug, PartialEq)] +pub enum Parser { Bool, Float { min: Option, max: Option }, Double { min: Option, max: Option }, @@ -86,10 +86,10 @@ pub enum Parser<'a> { Dimension, GameMode, Time, - ResourceOrTag { registry: Ident> }, - ResourceOrTagKey { registry: Ident> }, - Resource { registry: Ident> }, - ResourceKey { registry: Ident> }, + ResourceOrTag { registry: Ident }, + ResourceOrTagKey { registry: Ident }, + Resource { registry: Ident }, + ResourceKey { registry: Ident }, TemplateMirror, TemplateRotation, Uuid, @@ -102,7 +102,7 @@ pub enum StringArg { GreedyPhrase, } -impl Encode for Node<'_> { +impl Encode for Node { fn encode(&self, mut w: impl Write) -> anyhow::Result<()> { let node_type = match &self.data { NodeData::Root => 0, @@ -161,7 +161,7 @@ impl Encode for Node<'_> { } } -impl<'a> Decode<'a> for Node<'a> { +impl<'a> Decode<'a> for Node { fn decode(r: &mut &'a [u8]) -> anyhow::Result { let flags = u8::decode(r)?; @@ -176,10 +176,10 @@ impl<'a> Decode<'a> for Node<'a> { let node_data = match flags & 0x3 { 0 => NodeData::Root, 1 => NodeData::Literal { - name: <&str>::decode(r)?, + name: ::decode(r)?, }, 2 => NodeData::Argument { - name: <&str>::decode(r)?, + name: ::decode(r)?, parser: Parser::decode(r)?, suggestion: if flags & 0x10 != 0 { Some(match Ident::>::decode(r)?.as_str() { @@ -206,7 +206,7 @@ impl<'a> Decode<'a> for Node<'a> { } } -impl Encode for Parser<'_> { +impl Encode for Parser { fn encode(&self, mut w: impl Write) -> anyhow::Result<()> { match self { Parser::Bool => 0u8.encode(&mut w)?, @@ -335,7 +335,7 @@ impl Encode for Parser<'_> { } } -impl<'a> Decode<'a> for Parser<'a> { +impl<'a> Decode<'a> for Parser { fn decode(r: &mut &'a [u8]) -> anyhow::Result { fn decode_min_max<'a, T: Decode<'a>>( r: &mut &'a [u8], diff --git a/examples/command.rs b/examples/command.rs new file mode 100644 index 000000000..703ec4fc5 --- /dev/null +++ b/examples/command.rs @@ -0,0 +1,676 @@ +#![allow(clippy::type_complexity)] + +use std::ops::DerefMut; + +use command::graph::CommandGraphBuilder; +use command::handler::CommandResultEvent; +use command::parsers::entity_selector::{EntitySelector, EntitySelectors}; +use command::parsers::{CommandArg, GreedyString, QuotableString}; +use command::scopes::CommandScopes; +use command::{parsers, AddCommand, Command, CommandScopeRegistry, ModifierValue}; +use command_macros::Command; +use parsers::{Vec2 as Vec2Parser, Vec3 as Vec3Parser}; +use rand::prelude::IteratorRandom; +use valence::entity::living::LivingEntity; +use valence::prelude::*; +use valence::*; +use valence_server::op_level::OpLevel; + +const SPAWN_Y: i32 = 64; + +#[derive(Command, Debug, Clone)] +#[paths("teleport", "tp")] +#[scopes("valence.command.teleport")] +enum TeleportCommand { + #[paths = "{location}"] + ExecutorToLocation { location: Vec3Parser }, + #[paths = "{target}"] + ExecutorToTarget { target: EntitySelector }, + #[paths = "{from} {to}"] + TargetToTarget { + from: EntitySelector, + to: EntitySelector, + }, + #[paths = "{target} {location}"] + TargetToLocation { + target: EntitySelector, + location: Vec3Parser, + }, +} + +#[derive(Command, Debug, Clone)] +#[paths("gamemode", "gm")] +#[scopes("valence.command.gamemode")] +enum GamemodeCommand { + #[paths("survival {target?}", "{/} gms {target?}")] + Survival { target: Option }, + #[paths("creative {target?}", "{/} gmc {target?}")] + Creative { target: Option }, + #[paths("adventure {target?}", "{/} gma {target?}")] + Adventure { target: Option }, + #[paths("spectator {target?}", "{/} gmspec {target?}")] + Spectator { target: Option }, +} + +#[derive(Command, Debug, Clone)] +#[paths("struct {gamemode} {target?}")] +#[scopes("valence.command.gamemode")] +#[allow(dead_code)] +pub(crate) struct StructCommand { + gamemode: GameMode, + target: Option, +} + +#[derive(Command, Debug, Clone)] +#[paths("test", "t")] +#[scopes("valence.command.test")] +#[allow(dead_code)] +enum TestCommand { + // 3 literals with an arg each + #[paths("a {a} b {b} c {c}", "{a} {b} {c}")] + A { a: String, b: i32, c: f32 }, + // 2 literals with an arg last being optional (Because of the greedy string before the end + // this is technically unreachable) + #[paths = "a {a} {b} b {c?}"] + B { + a: Vec3Parser, + b: GreedyString, + c: Option, + }, + // greedy string optional arg + #[paths = "a {a} b {b?}"] + C { a: String, b: Option }, + // greedy string required arg + #[paths = "a {a} b {b}"] + D { a: String, b: GreedyString }, + // five optional args and an ending greedyString + #[paths("options {a?} {b?} {c?} {d?} {e?}", "options {b?} {a?} {d?} {c?} {e?}")] + E { + a: Option, + b: Option, + c: Option, + d: Option, + e: Option, + }, +} + +#[derive(Debug, Clone)] +enum ComplexRedirectionCommand { + A(Vec3Parser), + B, + C(Vec2Parser), + D, + E(Vec3Parser), +} + +impl Command for ComplexRedirectionCommand { + fn assemble_graph(graph: &mut CommandGraphBuilder) + where + Self: Sized, + { + let root = graph.root().id(); + + let command_root = graph + .literal("complex") + .with_scopes(vec!["valence.command.complex"]) + .id(); + let a = graph.literal("a").id(); + + graph + .at(a) + .argument("a") + .with_parser::() + .with_executable(|input| { + ComplexRedirectionCommand::A(Vec3Parser::parse_arg(input).unwrap()) + }); + + let b = graph.literal("b").id(); + + graph + .at(b) + .with_executable(|_| ComplexRedirectionCommand::B); + graph.at(b).redirect_to(root); + + let c = graph.literal("c").id(); + + graph + .at(c) + .argument("c") + .with_parser::() + .with_executable(|input| { + ComplexRedirectionCommand::C(Vec2Parser::parse_arg(input).unwrap()) + }); + + let d = graph + .at(command_root) + .literal("d") + .with_modifier(|_, modifiers| { + let entry = modifiers.entry("d_pass_count".into()).or_insert(0.into()); + if let ModifierValue::I32(i) = entry { + *i += 1; + } + }) + .id(); + + graph + .at(d) + .with_executable(|_| ComplexRedirectionCommand::D); + graph.at(d).redirect_to(command_root); + + let e = graph.literal("e").id(); + + graph + .at(e) + .argument("e") + .with_parser::() + .with_executable(|input| { + ComplexRedirectionCommand::E(Vec3Parser::parse_arg(input).unwrap()) + }); + } +} + +pub fn main() { + App::new() + .add_plugins(DefaultPlugins) + .add_command::() + .add_command::() + .add_command::() + .add_command::() + .add_command::() + .add_systems(Startup, setup) + .add_systems( + Update, + ( + init_clients, + despawn_disconnected_clients, + // Command handlers + handle_test_command, + handle_teleport_command, + handle_complex_command, + handle_gamemode_command, + handle_struct_command, + ), + ) + .run(); +} + +enum TeleportTarget { + Targets(Vec), +} + +#[derive(Debug)] +enum TeleportDestination { + Location(Vec3Parser), + Target(Option), +} + +fn handle_teleport_command( + mut events: EventReader>, + living_entities: Query>, + mut clients: Query<(Entity, &mut Client)>, + entity_layers: Query<&EntityLayerId>, + mut positions: Query<&mut Position>, + usernames: Query<(Entity, &Username)>, +) { + for event in events.iter() { + let compiled_command = match &event.result { + TeleportCommand::ExecutorToLocation { location } => ( + TeleportTarget::Targets(vec![event.executor]), + TeleportDestination::Location(*location), + ), + TeleportCommand::ExecutorToTarget { target } => ( + TeleportTarget::Targets(vec![event.executor]), + TeleportDestination::Target( + find_targets( + &living_entities, + &mut clients, + &positions, + &entity_layers, + &usernames, + event, + target, + ) + .first() + .copied(), + ), + ), + TeleportCommand::TargetToTarget { from, to } => ( + TeleportTarget::Targets( + find_targets( + &living_entities, + &mut clients, + &positions, + &entity_layers, + &usernames, + event, + from, + ) + .to_vec(), + ), + TeleportDestination::Target( + find_targets( + &living_entities, + &mut clients, + &positions, + &entity_layers, + &usernames, + event, + to, + ) + .first() + .copied(), + ), + ), + TeleportCommand::TargetToLocation { target, location } => ( + TeleportTarget::Targets( + find_targets( + &living_entities, + &mut clients, + &positions, + &entity_layers, + &usernames, + event, + target, + ) + .to_vec(), + ), + TeleportDestination::Location(*location), + ), + }; + + let (TeleportTarget::Targets(targets), destination) = compiled_command; + + println!( + "executing teleport command {:#?} -> {:#?}", + targets, destination + ); + match destination { + TeleportDestination::Location(location) => { + for target in targets { + let mut pos = positions.get_mut(target).unwrap(); + pos.0.x = location.x.get(pos.0.x as f32) as f64; + pos.0.y = location.y.get(pos.0.y as f32) as f64; + pos.0.z = location.z.get(pos.0.z as f32) as f64; + } + } + TeleportDestination::Target(target) => { + let target = target.unwrap(); + let target_pos = **positions.get(target).unwrap(); + for target in targets { + let mut position = positions.get_mut(target).unwrap(); + position.0 = target_pos; + } + } + } + } +} + +fn find_targets( + living_entities: &Query>, + clients: &mut Query<(Entity, &mut Client)>, + positions: &Query<&mut Position>, + entity_layers: &Query<&EntityLayerId>, + usernames: &Query<(Entity, &Username)>, + event: &CommandResultEvent, + target: &EntitySelector, +) -> Vec { + match target { + EntitySelector::SimpleSelector(selector) => match selector { + EntitySelectors::AllEntities => { + let executor_entity_layer = *entity_layers.get(event.executor).unwrap(); + living_entities + .iter() + .filter(|entity| { + let entity_layer = entity_layers.get(*entity).unwrap(); + entity_layer.0 == executor_entity_layer.0 + }) + .collect() + } + EntitySelectors::SinglePlayer(name) => { + let target = usernames.iter().find(|(_, username)| username.0 == *name); + match target { + None => { + let client = &mut clients.get_mut(event.executor).unwrap().1; + client.send_chat_message(format!("Could not find target: {}", name)); + vec![] + } + Some(target_entity) => { + vec![target_entity.0] + } + } + } + EntitySelectors::AllPlayers => { + let executor_entity_layer = *entity_layers.get(event.executor).unwrap(); + clients + .iter_mut() + .filter_map(|(entity, ..)| { + let entity_layer = entity_layers.get(entity).unwrap(); + if entity_layer.0 == executor_entity_layer.0 { + Some(entity) + } else { + None + } + }) + .collect() + } + EntitySelectors::SelfPlayer => { + vec![event.executor] + } + EntitySelectors::NearestPlayer => { + let executor_entity_layer = *entity_layers.get(event.executor).unwrap(); + let executor_pos = positions.get(event.executor).unwrap(); + let target = clients + .iter_mut() + .filter(|(entity, ..)| { + *entity_layers.get(*entity).unwrap() == executor_entity_layer + }) + .filter(|(target, ..)| *target != event.executor) + .map(|(target, ..)| target) + .min_by(|target, target2| { + let target_pos = positions.get(*target).unwrap(); + let target2_pos = positions.get(*target2).unwrap(); + let target_dist = target_pos.distance(**executor_pos); + let target2_dist = target2_pos.distance(**executor_pos); + target_dist.partial_cmp(&target2_dist).unwrap() + }); + match target { + None => { + let mut client = clients.get_mut(event.executor).unwrap().1; + client.send_chat_message("Could not find target".to_string()); + vec![] + } + Some(target_entity) => { + vec![target_entity] + } + } + } + EntitySelectors::RandomPlayer => { + let executor_entity_layer = *entity_layers.get(event.executor).unwrap(); + let target = clients + .iter_mut() + .filter(|(entity, ..)| { + *entity_layers.get(*entity).unwrap() == executor_entity_layer + }) + .choose(&mut rand::thread_rng()) + .map(|(target, ..)| target); + match target { + None => { + let mut client = clients.get_mut(event.executor).unwrap().1; + client.send_chat_message("Could not find target".to_string()); + vec![] + } + Some(target_entity) => { + vec![target_entity] + } + } + } + }, + EntitySelector::ComplexSelector(_, _) => { + let mut client = clients.get_mut(event.executor).unwrap().1; + client.send_chat_message("complex selector not implemented".to_string()); + vec![] + } + } +} + +fn handle_test_command( + mut events: EventReader>, + mut clients: Query<&mut Client>, +) { + for event in events.iter() { + let client = &mut clients.get_mut(event.executor).unwrap(); + client.send_chat_message(format!( + "Test command executed with data:\n {:#?}", + &event.result + )); + } +} + +fn handle_complex_command( + mut events: EventReader>, + mut clients: Query<&mut Client>, +) { + for event in events.iter() { + let client = &mut clients.get_mut(event.executor).unwrap(); + client.send_chat_message(format!( + "complex command executed with data:\n {:#?}\n and with the modifiers:\n {:#?}", + &event.result, &event.modifiers + )); + } +} + +fn handle_struct_command( + mut events: EventReader>, + mut clients: Query<&mut Client>, +) { + for event in events.iter() { + let client = &mut clients.get_mut(event.executor).unwrap(); + client.send_chat_message(format!( + "Struct command executed with data:\n {:#?}", + &event.result + )); + } +} + +fn handle_gamemode_command( + mut events: EventReader>, + mut clients: Query<(&mut Client, &mut GameMode, &Username, Entity)>, + positions: Query<&Position>, +) { + for event in events.iter() { + let game_mode_to_set = match &event.result { + GamemodeCommand::Survival { .. } => GameMode::Survival, + GamemodeCommand::Creative { .. } => GameMode::Creative, + GamemodeCommand::Adventure { .. } => GameMode::Adventure, + GamemodeCommand::Spectator { .. } => GameMode::Spectator, + }; + + let selector = match &event.result { + GamemodeCommand::Survival { target } => target.clone(), + GamemodeCommand::Creative { target } => target.clone(), + GamemodeCommand::Adventure { target } => target.clone(), + GamemodeCommand::Spectator { target } => target.clone(), + }; + + match selector { + None => { + let (mut client, mut game_mode, ..) = clients.get_mut(event.executor).unwrap(); + *game_mode = game_mode_to_set; + client.send_chat_message(format!( + "Gamemode command executor -> self executed with data:\n {:#?}", + &event.result + )); + } + Some(selector) => match selector { + EntitySelector::SimpleSelector(selector) => match selector { + EntitySelectors::AllEntities => { + for (mut client, mut game_mode, ..) in &mut clients.iter_mut() { + *game_mode = game_mode_to_set; + client.send_chat_message(format!( + "Gamemode command executor -> all entities executed with data:\n \ + {:#?}", + &event.result + )); + } + } + EntitySelectors::SinglePlayer(name) => { + let target = clients + .iter_mut() + .find(|(.., username, _)| username.0 == *name) + .map(|(.., target)| target); + + match target { + None => { + let client = &mut clients.get_mut(event.executor).unwrap().0; + client + .send_chat_message(format!("Could not find target: {}", name)); + } + Some(target) => { + let mut game_mode = clients.get_mut(target).unwrap().1; + *game_mode = game_mode_to_set; + + let client = &mut clients.get_mut(event.executor).unwrap().0; + client.send_chat_message(format!( + "Gamemode command executor -> single player executed with \ + data:\n {:#?}", + &event.result + )); + } + } + } + EntitySelectors::AllPlayers => { + for (mut client, mut game_mode, ..) in &mut clients.iter_mut() { + *game_mode = game_mode_to_set; + client.send_chat_message(format!( + "Gamemode command executor -> all entities executed with data:\n \ + {:#?}", + &event.result + )); + } + } + EntitySelectors::SelfPlayer => { + let (mut client, mut game_mode, ..) = + clients.get_mut(event.executor).unwrap(); + *game_mode = game_mode_to_set; + client.send_chat_message(format!( + "Gamemode command executor -> self executed with data:\n {:#?}", + &event.result + )); + } + EntitySelectors::NearestPlayer => { + let executor_pos = positions.get(event.executor).unwrap(); + let target = clients + .iter_mut() + .filter(|(.., target)| *target != event.executor) + .min_by(|(.., target), (.., target2)| { + let target_pos = positions.get(*target).unwrap(); + let target2_pos = positions.get(*target2).unwrap(); + let target_dist = target_pos.distance(**executor_pos); + let target2_dist = target2_pos.distance(**executor_pos); + target_dist.partial_cmp(&target2_dist).unwrap() + }) + .map(|(.., target)| target); + + match target { + None => { + let client = &mut clients.get_mut(event.executor).unwrap().0; + client.send_chat_message("Could not find target".to_string()); + } + Some(target) => { + let mut game_mode = clients.get_mut(target).unwrap().1; + *game_mode = game_mode_to_set; + + let client = &mut clients.get_mut(event.executor).unwrap().0; + client.send_chat_message(format!( + "Gamemode command executor -> single player executed with \ + data:\n {:#?}", + &event.result + )); + } + } + } + EntitySelectors::RandomPlayer => { + let target = clients + .iter_mut() + .choose(&mut rand::thread_rng()) + .map(|(.., target)| target); + + match target { + None => { + let client = &mut clients.get_mut(event.executor).unwrap().0; + client.send_chat_message("Could not find target".to_string()); + } + Some(target) => { + let mut game_mode = clients.get_mut(target).unwrap().1; + *game_mode = game_mode_to_set; + + let client = &mut clients.get_mut(event.executor).unwrap().0; + client.send_chat_message(format!( + "Gamemode command executor -> single player executed with \ + data:\n {:#?}", + &event.result + )); + } + } + } + }, + EntitySelector::ComplexSelector(_, _) => { + let client = &mut clients.get_mut(event.executor).unwrap().0; + client + .send_chat_message("Complex selectors are not implemented yet".to_string()); + } + }, + } + } +} + +fn setup( + mut commands: Commands, + server: Res, + mut dimensions: ResMut, + biomes: Res, + mut command_scopes: ResMut, +) { + dimensions + .deref_mut() + .insert(Ident::new("pooland").unwrap(), DimensionType::default()); + + let mut layer = LayerBundle::new(ident!("overworld"), &dimensions, &biomes, &server); + + for z in -5..5 { + for x in -5..5 { + layer.chunk.insert_chunk([x, z], UnloadedChunk::new()); + } + } + + for z in -25..25 { + for x in -25..25 { + layer + .chunk + .set_block([x, SPAWN_Y, z], BlockState::GRASS_BLOCK); + } + } + + command_scopes.link("valence.admin", "valence.command"); + + commands.spawn(layer); +} + +fn init_clients( + mut clients: Query< + ( + &mut EntityLayerId, + &mut VisibleChunkLayer, + &mut VisibleEntityLayers, + &mut CommandScopes, + &mut Position, + &mut GameMode, + &mut OpLevel, + ), + Added, + >, + layers: Query, With)>, +) { + for ( + mut layer_id, + mut visible_chunk_layer, + mut visible_entity_layers, + mut permissions, + mut pos, + mut game_mode, + mut op_level, + ) in &mut clients + { + let layer = layers.single(); + + layer_id.0 = layer; + visible_chunk_layer.0 = layer; + visible_entity_layers.0.insert(layer); + + pos.0 = [0.0, SPAWN_Y as f64 + 1.0, 0.0].into(); + *game_mode = GameMode::Creative; + op_level.set(4); + + permissions.add("valence.admin"); + } +} diff --git a/src/lib.rs b/src/lib.rs index bee173f9b..d9a7b58e3 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -43,6 +43,10 @@ pub use valence_advancement as advancement; pub use valence_anvil as anvil; #[cfg(feature = "boss_bar")] pub use valence_boss_bar as boss_bar; +#[cfg(feature = "command")] +pub use valence_command as command; +#[cfg(feature = "command")] +pub use valence_command_macros as command_macros; #[cfg(feature = "inventory")] pub use valence_inventory as inventory; pub use valence_lang as lang; @@ -237,6 +241,11 @@ impl PluginGroup for DefaultPlugins { group = group.add(valence_boss_bar::BossBarPlugin); } + #[cfg(feature = "command")] + { + group = group.add(valence_command::manager::CommandPlugin); + } + #[cfg(feature = "scoreboard")] { group = group.add(valence_scoreboard::ScoreboardPlugin); diff --git a/src/testing.rs b/src/testing.rs index ba83c02d3..58fdc1470 100644 --- a/src/testing.rs +++ b/src/testing.rs @@ -10,14 +10,13 @@ use uuid::Uuid; use valence_ident::ident; use valence_network::NetworkPlugin; use valence_registry::{BiomeRegistry, DimensionTypeRegistry}; -use valence_server::client::ClientBundleArgs; +use valence_server::client::{ClientBundle, ClientBundleArgs, ClientConnection, ReceivedPacket}; use valence_server::keepalive::KeepaliveSettings; use valence_server::protocol::decode::PacketFrame; use valence_server::protocol::packets::play::{PlayerPositionLookS2c, TeleportConfirmC2s}; use valence_server::protocol::{Decode, Encode, Packet, PacketDecoder, PacketEncoder, VarInt}; use valence_server::{ChunkLayer, EntityLayer, Server, ServerSettings}; -use crate::client::{ClientBundle, ClientConnection, ReceivedPacket}; use crate::DefaultPlugins; pub struct ScenarioSingleClient { /// The new bevy application. diff --git a/typos.toml b/typos.toml index c59189146..ad9370a63 100644 --- a/typos.toml +++ b/typos.toml @@ -2,4 +2,4 @@ extend-exclude = ["*.svg", "*.json", "crates/java_string/src/slice.rs"] [default] -extend-ignore-re = ['\d+ths', 'CC BY-NC-ND'] +extend-ignore-re = ['\d+ths', 'CC BY-NC-ND', "trUe", "fAlse"]