diff --git a/Cargo.toml b/Cargo.toml index 3fd30423c..e28ac130a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -20,6 +20,7 @@ default = [ "network", "player_list", "world_border", + "command" ] advancement = ["dep:valence_advancement"] anvil = ["dep:valence_anvil"] @@ -29,6 +30,7 @@ log = ["dep:bevy_log"] network = ["dep:valence_network"] player_list = ["dep:valence_player_list"] world_border = ["dep:valence_world_border"] +command = ["dep:valence_command"] [dependencies] anyhow.workspace = true @@ -55,6 +57,7 @@ valence_network = { workspace = true, optional = true } valence_player_list = { workspace = true, optional = true } valence_registry.workspace = true valence_world_border = { workspace = true, optional = true } +valence_command = { workspace = true, optional = true } [dev-dependencies] anyhow.workspace = true @@ -108,9 +111,11 @@ bevy_utils = { version = "0.11" } bitfield-struct = "0.3.1" byteorder = "1.4.3" bytes = "1.2.1" +casey = "0.4.0" cesu8 = "1.1.0" cfb8 = "0.8.1" clap = { version = "4.0.30", features = ["derive"] } +ctor = "0.2.0" criterion = "0.4.0" directories = "5.0.0" eframe = { version = "0.22.0", default-features = false } @@ -149,6 +154,7 @@ serde = "1.0.160" serde_json = "1.0.96" sha1 = "0.10.5" sha2 = "0.10.6" +smallvec = "1.11.0" syn = "2.0.15" syntect = { version = "5.0.0", default-features = false } tempfile = "3.3.0" @@ -166,6 +172,7 @@ valence_biome.path = "crates/valence_biome" valence_block.path = "crates/valence_block" valence_build_utils.path = "crates/valence_build_utils" valence_client.path = "crates/valence_client" +valence_command.path = "crates/valence_command" valence_core_macros.path = "crates/valence_core_macros" valence_core.path = "crates/valence_core" valence_dimension.path = "crates/valence_dimension" diff --git a/crates/valence_command/Cargo.toml b/crates/valence_command/Cargo.toml new file mode 100644 index 000000000..a66a6f429 --- /dev/null +++ b/crates/valence_command/Cargo.toml @@ -0,0 +1,28 @@ +[package] +name = "valence_command" +version.workspace = true +edition.workspace = true + +[dependencies] +async-trait.workspace = true +bevy_ecs.workspace = true +bevy_app.workspace = true +bevy_hierarchy.workspace = true +byteorder.workspace = true +valence_core.workspace = true +anyhow.workspace = true +glam.workspace = true +serde_json.workspace = true +serde.workspace = true +valence_client.workspace = true +valence_block.workspace = true +valence_nbt.workspace = true +valence_entity.workspace = true +valence_instance.workspace = true +ctor.workspace = true +casey.workspace = true +paste.workspace = true +rustc-hash.workspace = true +smallvec.workspace = true +parking_lot.workspace = true +tokio.workspace = true \ No newline at end of file diff --git a/crates/valence_command/src/boolean.rs b/crates/valence_command/src/boolean.rs new file mode 100644 index 000000000..d24a69c52 --- /dev/null +++ b/crates/valence_command/src/boolean.rs @@ -0,0 +1,106 @@ +use std::any::TypeId; +use std::borrow::Cow; + +use bevy_ecs::system::SystemParamItem; +use valence_core::text::Text; +use valence_core::translation_key::{PARSING_BOOL_EXPECTED, PARSING_BOOL_INVALID}; + +use crate::command::CommandExecutorBase; +use crate::nodes::NodeSuggestion; +use crate::parse::{Parse, ParseResult}; +use crate::pkt; +use crate::reader::{ArcStrReader, StrLocated, StrReader, StrSpan}; +use crate::suggestions::Suggestion; + +#[async_trait::async_trait] +impl Parse for bool { + type Item<'a> = bool; + + type Data<'a> = (); + + type Suggestions = StrSpan; + + type SuggestionsParam = (); + + type SuggestionsAsyncData = Cow<'static, [Suggestion<'static>]>; + + const VANILLA: bool = true; + + fn parse_id() -> TypeId { + TypeId::of::() + } + + fn item_id() -> TypeId { + TypeId::of::() + } + + fn parse<'a>( + _data: &Self::Data<'a>, + suggestions: &mut Self::Suggestions, + reader: &mut StrReader<'a>, + ) -> ParseResult { + reader.span_err_located(suggestions, |reader| { + let str = reader.read_unquoted_str(); + if str.is_empty() { + Err(Text::translate(PARSING_BOOL_EXPECTED, vec![])) + } else if str.eq_ignore_ascii_case("true") { + Ok(true) + } else if str.eq_ignore_ascii_case("false") { + Ok(false) + } else { + Err(Text::translate( + PARSING_BOOL_INVALID, + vec![str.to_string().into()], + )) + } + }) + } + + fn brigadier(_data: &Self::Data<'_>) -> Option> { + Some(pkt::Parser::Bool) + } + + fn brigadier_suggestions(_data: &Self::Data<'_>) -> Option { + None + } + + fn create_suggestions_data( + _data: &Self::Data<'_>, + command: ArcStrReader, + _executor: CommandExecutorBase, + suggestions: &Self::Suggestions, + _param: SystemParamItem, + ) -> Self::SuggestionsAsyncData { + const EMPTY: &[Suggestion<'static>] = &[]; + const ONLY_TRUE: &[Suggestion<'static>] = &[Suggestion::new_str("true")]; + const ONLY_FALSE: &[Suggestion<'static>] = &[Suggestion::new_str("false")]; + const BOTH: &[Suggestion<'static>] = + &[Suggestion::new_str("true"), Suggestion::new_str("false")]; + + let str = suggestions.in_str(command.reader().str()).unwrap(); + + Cow::Borrowed(if str.len() > 5 { + EMPTY + } else { + let lc_str = str.to_ascii_lowercase(); + if str.len() == 0 { + BOTH + } else if "true".starts_with(&lc_str) { + ONLY_TRUE + } else if "false".starts_with(&lc_str) { + ONLY_FALSE + } else { + EMPTY + } + }) + } + + async fn suggestions( + _command: ArcStrReader, + _executor: CommandExecutorBase, + suggestions: Box, + async_data: Self::SuggestionsAsyncData, + ) -> StrLocated]>> { + StrLocated::new(*suggestions, async_data) + } +} diff --git a/crates/valence_command/src/builder.rs b/crates/valence_command/src/builder.rs new file mode 100644 index 000000000..5129563af --- /dev/null +++ b/crates/valence_command/src/builder.rs @@ -0,0 +1,221 @@ +use bevy_ecs::system::{Commands, IntoSystem, ResMut, System, SystemParam}; +use bevy_ecs::world::World; + +use crate::command::CommandArguments; +use crate::nodes::{NodeChildrenFlow, NodeFlow, NodeGraphInWorld, NodeId, NodeKind}; +use crate::parse::{Parse, ParseWithData}; + +#[derive(SystemParam)] +pub struct NodeGraphCommands<'w, 's> { + pub commands: Commands<'w, 's>, + pub graph: ResMut<'w, NodeGraphInWorld>, +} + +impl<'w, 's> NodeGraphCommands<'w, 's> { + pub fn spawn_literal_node<'a>(&'a mut self, name: String) -> NodeCommands<'w, 's, 'a> { + let graph = self.graph.get_mut(); + let id = graph.reserve_node_id(NodeKind::Literal { name }); + self.commands.add(move |world: &mut World| { + let mut graph = world.resource_mut::(); + let graph = graph.get_mut(); + graph.changed = true; + graph.shared.get_mut().update_nodes_len(); + }); + NodeCommands { commands: self, id } + } + + pub fn spawn_argument_node<'a, P: Parse>( + &'a mut self, + name: String, + data: P::Data<'static>, + ) -> NodeCommands<'w, 's, 'a> { + let graph = self.graph.get_mut(); + let id = graph.reserve_node_id(NodeKind::Argument { + name, + parse: Box::new(ParseWithData::

{ data, state: None }), + }); + self.commands.add(move |world: &mut World| { + let mut graph = world.resource_mut::().take(); + graph.changed = true; + graph.shared.get_mut().update_nodes_len(); + let node = graph.get_mut_node(id).unwrap(); + if let NodeKind::Argument { ref mut parse, .. } = node.kind { + parse.initialize(world); + } + world.resource_mut::().insert(graph); + }); + NodeCommands { commands: self, id } + } + + pub fn node<'a>(&'a mut self, id: NodeId) -> NodeCommands<'w, 's, 'a> { + NodeCommands { commands: self, id } + } +} + +pub struct NodeCommands<'w, 's, 'a> { + commands: &'a mut NodeGraphCommands<'w, 's>, + pub id: NodeId, +} + +impl<'w, 's, 'a> NodeCommands<'w, 's, 'a> { + pub fn execute( + &mut self, + system: impl IntoSystem, + ) -> &mut Self { + let node_id = self.id; + let mut system = IntoSystem::into_system(system); + self.commands.commands.add(move |world: &mut World| { + system.initialize(world); + let mut graph = world.resource_mut::(); + let graph = graph.get_mut(); + graph.changed = true; + graph.get_mut_node(node_id).unwrap().execute = Some(Box::new(system)); + }); + self + } + + pub fn set_redirect(&mut self, redirect: NodeId) -> &mut Self { + let id = self.id; + self.commands.commands.add(move |world: &mut World| { + let mut graph = world.resource_mut::(); + let graph = graph.get_mut(); + + graph.changed = true; + + // SAFETY: Only one mutable node reference + let node = unsafe { graph.get_mut_node_unsafe(id).unwrap() }; + + match node.flow { + NodeFlow::Children(ref mut children) => { + for child in children.children.iter().cloned() { + // SAFETY: Node can not child of itself + unsafe { graph.get_mut_node_unsafe(child).unwrap() }.remove_parent(id); + } + + node.flow = NodeFlow::Redirect(redirect); + } + NodeFlow::Redirect(ref mut previous) => { + // SAFETY: Node can not redirect to itself + unsafe { graph.get_mut_node_unsafe(*previous).unwrap() }.remove_parent(id); + + *previous = redirect + } + NodeFlow::Stop => { + node.flow = NodeFlow::Redirect(redirect); + } + } + + // SAFETY: Node can not redirect to itself + unsafe { graph.get_mut_node_unsafe(redirect).unwrap() } + .parents + .push(id); + }); + self + } + + /// Changes node's flow to the children, if it wasn't and then inserts all + /// nodes from iterator + pub fn add_children( + &mut self, + children: impl Iterator + Sync + Send + 'static, + ) -> &mut Self { + let node_id = self.id; + let children = children.filter(move |v| *v != node_id); + self.commands.commands.add(move |world: &mut World| { + let mut graph = world.resource_mut::(); + let graph = graph.get_mut(); + + graph.changed = true; + + // SAFETY: Only one mutable node reference + let node = unsafe { graph.get_mut_node_unsafe(node_id).unwrap() }; + + match node.flow { + NodeFlow::Children(ref mut children_flow) => { + for child in children { + children_flow.add( + node_id, + // SAFETY: Node's child can not be node itself + unsafe { graph.get_mut_node_unsafe(child).unwrap() }, + child, + ); + } + } + NodeFlow::Redirect(redirect) => { + // SAFETY: Node can not redirect to itself + unsafe { graph.get_mut_node_unsafe(redirect).unwrap() }.remove_parent(node_id); + + let mut children_flow = NodeChildrenFlow::default(); + + for child in children { + children_flow.add( + node_id, + // SAFETY: Node's child can not be node itself + unsafe { graph.get_mut_node_unsafe(child).unwrap() }, + child, + ); + } + + node.flow = NodeFlow::Children(Box::new(children_flow)); + } + NodeFlow::Stop => { + let mut children_flow = NodeChildrenFlow::default(); + + for child in children { + children_flow.add( + node_id, + // SAFETY: Node's child can not be node itself + unsafe { graph.get_mut_node_unsafe(child).unwrap() }, + child, + ); + } + + node.flow = NodeFlow::Children(Box::new(children_flow)); + } + } + }); + self + } + + pub fn with_literal_child( + &mut self, + name: String, + func: impl FnOnce(&mut NodeCommands), + ) -> &mut Self { + let mut child = self.commands.spawn_literal_node(name); + let _ = func(&mut child); + let id = child.id; + self.add_children([id].into_iter()) + } + + pub fn with_argument_child( + &mut self, + name: String, + data: P::Data<'static>, + func: impl FnOnce(&mut NodeCommands), + ) -> &mut Self { + let mut child = self.commands.spawn_argument_node::

(name, data); + let _ = func(&mut child); + let id = child.id; + self.add_children([id].into_iter()) + } + + pub fn root_node_child(&mut self) -> &mut Self { + let id = self.id; + assert_ne!(id, NodeId::ROOT); + self.commands.commands.add(move |world: &mut World| { + let mut graph = world.resource_mut::(); + let graph = graph.get_mut(); + // SAFETY: Only one mutable reference + let children_flow = + unsafe { graph.get_mut_children_flow_unsafe(NodeId::ROOT).unwrap() }; + children_flow.add( + NodeId::ROOT, + // SAFETY: Id is not root id + unsafe { graph.get_mut_node_unsafe(id).unwrap() }, + id, + ); + }); + self + } +} diff --git a/crates/valence_command/src/command.rs b/crates/valence_command/src/command.rs new file mode 100644 index 000000000..7b0b5177e --- /dev/null +++ b/crates/valence_command/src/command.rs @@ -0,0 +1,90 @@ +use std::ptr::NonNull; + +use bevy_ecs::prelude::Entity; +use bevy_ecs::system::{Query, SystemParam}; +use glam::DVec3; +use valence_client::message::SendMessage; +use valence_client::Client; +use valence_core::block_pos::BlockPos; +use valence_core::text::Text; + +use crate::nodes::EntityNode; +use crate::parse::ParseResultsRead; + +#[derive(Clone, Copy, Debug, PartialEq)] +pub enum RealCommandExecutor { + Player(Entity), + Console, + Misc(u32), +} + +#[derive(Clone, Copy, Debug, PartialEq, Default)] +pub struct CommandExecutor { + pub position: Option, + pub base: CommandExecutorBase, +} + +impl CommandExecutor { + pub fn node_entity(&self) -> Option { + self.base.node_entity() + } +} + +#[derive(SystemParam)] +pub struct CommandExecutorBridge<'w, 's> { + client: Query<'w, 's, &'static mut Client>, +} + +impl<'w, 's> CommandExecutorBridge<'w, 's> { + pub fn send_message(&mut self, executor: RealCommandExecutor, text: Text) { + match executor { + RealCommandExecutor::Console => todo!(), + RealCommandExecutor::Misc(_id) => todo!(), + RealCommandExecutor::Player(entity) => { + self.client.get_mut(entity).unwrap().send_chat_message(text) + } + } + } +} + +impl From for CommandExecutor { + fn from(base: CommandExecutorBase) -> Self { + Self { + base, + ..Default::default() + } + } +} + +#[derive(Clone, Copy, Debug, PartialEq, Default)] +pub enum CommandExecutorBase { + #[default] + Console, + Entity { + entity: Entity, + }, + Block { + instance: Entity, + pos: BlockPos, + }, +} + +impl CommandExecutorBase { + pub fn node_entity(&self) -> Option { + match self { + Self::Block { instance, .. } => Some(*instance), + Self::Console => None, + Self::Entity { entity } => Some(*entity), + } + } +} + +/// Usage of 'static lifetime is necessary because In argument of bevy can not +/// have any lifetimes. This will be handled correctly +pub type CommandArguments = ( + // lifetime is not 'static but the lifetime of outer function + ParseResultsRead<'static>, + RealCommandExecutor, + // a mutable reference with lifetime of function + NonNull, +); diff --git a/crates/valence_command/src/compile.rs b/crates/valence_command/src/compile.rs new file mode 100644 index 000000000..1b48037f0 --- /dev/null +++ b/crates/valence_command/src/compile.rs @@ -0,0 +1,268 @@ +use bevy_ecs::prelude::{Entity, Event, EventReader, EventWriter}; +use bevy_ecs::query::{Has, With}; +use bevy_ecs::system::{Query, Res, SystemParam}; +use rustc_hash::FxHashSet; +use valence_core::text::Text; +use valence_core::translation_key::COMMAND_EXPECTED_SEPARATOR; + +use crate::command::{CommandExecutor, CommandExecutorBridge, RealCommandExecutor}; +use crate::exec::CommandExecutionEvent; +use crate::nodes::{ + EntityNode, EntityNodeQuery, NodeChildrenFlow, NodeFlow, NodeGraph, NodeGraphInWorld, NodeId, + NodeKind, RootNode, RootNodeId, +}; +use crate::parse::{ParseObject, ParseResult, ParseResults, ParseResultsWrite}; +use crate::reader::{StrLocated, StrReader, StrSpan}; + +/// In future, command and write should be changed to a borrowed alternatives +/// like [`str`] and [`crate::parse::ParseResultsRead`] or make some pool for +/// them. +#[derive(Event)] +pub struct CompiledCommandExecutionEvent { + pub compiled: CompiledCommand, + pub executor: CommandExecutor, + pub real_executor: RealCommandExecutor, +} + +pub struct CompiledCommand { + pub(crate) results: ParseResults, + pub(crate) path: Vec, +} + +pub(crate) enum CommandCompilerPurpose<'a> { + Execution { + fill: &'a mut ParseResultsWrite, + path: &'a mut Vec, + }, + Suggestions { + node: &'a mut NodeId, + }, +} + +impl<'a> CommandCompilerPurpose<'a> { + pub(crate) fn last_node(&mut self, last_node: NodeId) { + if let Self::Suggestions { node: entity } = self { + **entity = last_node; + } + } + + pub(crate) fn parse( + &mut self, + reader: &mut StrReader, + parser: &dyn ParseObject, + ) -> ParseResult<()> { + match self { + Self::Execution { fill, .. } => parser.obj_parse(reader, fill), + Self::Suggestions { .. } => parser.obj_skip(reader), + } + .0 + } + + pub(crate) fn add_path(&mut self, node: NodeId) { + if let Self::Execution { path, .. } = self { + path.push(node); + } + } + + pub(crate) fn path( + &mut self, + node: NodeId, + reader: &mut StrReader, + executable: bool, + ) -> ParseResult<()> { + if executable { + self.add_path(node); + Ok(()) + } else { + // TODO: translated error + Err(StrLocated::new( + StrSpan::new(reader.cursor(), reader.cursor()), + Text::text("End is not executable"), + )) + } + } +} + +impl NodeGraph { + pub fn compile_command( + &self, + root: &RootNode, + command: String, + ) -> ParseResult { + let mut results = ParseResults::new_empty(command); + let (mut reader, fill) = results.to_write(); + let mut path = vec![]; + self.walk_node( + NodeId::ROOT, + root, + &mut reader, + &mut CommandCompilerPurpose::Execution { + fill, + path: &mut path, + }, + )?; + Ok(CompiledCommand { results, path }) + } + + pub(crate) fn walk_node<'a>( + &self, + node_id: NodeId, + root: &RootNode, + reader: &mut StrReader<'a>, + purpose: &mut CommandCompilerPurpose, + ) -> ParseResult<()> { + match self.get_node(node_id) { + Some(node) => match node.flow { + NodeFlow::Children(ref children_flow) => { + if reader.is_ended() { + purpose.path(node_id, reader, node.execute.is_some()) + } else { + self.walk_children(children_flow.as_ref(), root, reader, purpose) + } + } + NodeFlow::Redirect(redirect) => { + if node.execute.is_some() { + purpose.add_path(redirect); + } + if root.policy.check(redirect) { + purpose.last_node(redirect); + self.walk_node(redirect, root, reader, purpose) + } else { + purpose.last_node(node_id); + Ok(()) + } + } + NodeFlow::Stop => { + purpose.path(node_id, reader, node.execute.is_some())?; + purpose.last_node(node_id); + Ok(()) + } + }, + None => self.walk_children(&self.shared().first_layer, root, reader, purpose), + } + } + + pub(crate) fn walk_children<'a>( + &self, + children_flow: &NodeChildrenFlow, + root: &RootNode, + reader: &mut StrReader<'a>, + purpose: &mut CommandCompilerPurpose, + ) -> ParseResult<()> { + let begin = reader.cursor(); + let mut end = begin; + + if !children_flow.literals.is_empty() { + let literal = reader.read_unquoted_str(); + if let Some(node) = children_flow.literals.get(literal) { + if root.policy.check(*node) { + if reader.is_ended() { + } else if reader.skip_char(' ') { + purpose.last_node(*node); + } else { + return Err(StrLocated::new( + StrSpan::new(reader.cursor(), reader.cursor()), + Text::translate(COMMAND_EXPECTED_SEPARATOR, vec![]), + )); + } + + return self.walk_node(*node, root, reader, purpose); + } + } + end = reader.cursor(); + // SAFETY: It was a cursor of this reader + unsafe { reader.set_cursor(begin) }; + } + + let mut previous_err: Option> = None; + + for node_id in children_flow.parsers.iter().cloned() { + if !root.policy.check(node_id) { + continue; + } + + // It is impossible for root node to be here + let node = self.get_node(node_id).unwrap(); + let result = purpose.parse( + reader, + match node.kind { + NodeKind::Argument { ref parse, .. } => parse.as_ref(), + _ => unreachable!(), + }, + ); + + match result { + Ok(()) => { + if reader.is_ended() { + } else if reader.skip_char(' ') { + purpose.last_node(node_id); + } else { + return Err(StrLocated::new( + StrSpan::new(reader.cursor(), reader.cursor()), + Text::translate(COMMAND_EXPECTED_SEPARATOR, vec![]), + )); + } + return self.walk_node(node_id, root, reader, purpose); + } + Err(e) => { + // SAFETY: begin cursor was a cursor of this reader + unsafe { reader.set_cursor(begin) }; + match previous_err { + Some(ref mut previous_err) => { + let prev_span = previous_err.span; + let cur_span = e.span; + + // Checking which error message is deeper + if cur_span.is_deeper(prev_span) { + *previous_err = e; + } + } + None => { + previous_err.replace(e); + } + } + } + } + } + + Err(match previous_err { + Some(e) => e, + // TODO: translated error + None => StrLocated::new(StrSpan::new(begin, end), Text::text("Invalid")), + }) + } +} + +pub fn compile_commands( + graph: Res, + mut compiled_event: EventWriter, + mut execution_event: EventReader, + entity_node: Query, + mut cebridge: CommandExecutorBridge, +) { + let graph = graph.get(); + for event in execution_event.iter() { + let root = graph + .get_root_node( + event + .executor + .node_entity() + .and_then(|e| entity_node.get(e).ok()) + .map(|v| v.get()) + .unwrap_or(RootNodeId::SUPER), + ) + .unwrap(); + + match graph.compile_command(&root, event.command.clone()) { + Ok(compiled) => compiled_event.send(CompiledCommandExecutionEvent { + compiled, + executor: event.executor, + real_executor: event.real_executor, + }), + Err(e) => { + // TODO: error msg + cebridge.send_message(event.real_executor, e.object); + } + } + } +} diff --git a/crates/valence_command/src/exec.rs b/crates/valence_command/src/exec.rs new file mode 100644 index 000000000..f80e70199 --- /dev/null +++ b/crates/valence_command/src/exec.rs @@ -0,0 +1,208 @@ +use std::ptr::NonNull; + +use bevy_ecs::archetype::ArchetypeGeneration; +use bevy_ecs::component::{ComponentId, Tick}; +use bevy_ecs::prelude::{Entity, Event, EventReader, EventWriter}; +use bevy_ecs::query::{Access, Changed}; +use bevy_ecs::removal_detection::RemovedComponents; +use bevy_ecs::system::{ + IntoSystem, Local, ParamSet, Query, Res, ResMut, Resource, System, SystemMeta, SystemParam, +}; +use bevy_ecs::world::unsafe_world_cell::UnsafeWorldCell; +use bevy_ecs::world::{FromWorld, World}; +use rustc_hash::FxHashMap; +use valence_client::event_loop::PacketEvent; +use valence_core::protocol::packet::chat::CommandExecutionC2s; + +use crate::command::{CommandArguments, CommandExecutor, CommandExecutorBase, RealCommandExecutor}; +use crate::compile::CompiledCommandExecutionEvent; +use crate::nodes::NodeGraphInWorld; +use crate::parse::ParseResultsRead; +use crate::reader::StrReader; + +#[derive(Event, Debug)] +pub struct CommandExecutionEvent { + pub executor: CommandExecutor, + pub real_executor: RealCommandExecutor, + pub command: String, +} + +impl CommandExecutionEvent { + pub fn reader(&self) -> StrReader { + StrReader::from_command(self.command.as_str()) + } +} + +pub fn command_execution_packet( + mut event: EventReader, + mut execution_event: EventWriter, +) { + for packet_event in event.iter() { + if let Some(packet) = packet_event.decode::() { + execution_event.send(CommandExecutionEvent { + executor: CommandExecutor::from(CommandExecutorBase::Entity { + entity: packet_event.client, + }), + real_executor: RealCommandExecutor::Player(packet_event.client), + command: packet.command.to_string(), + }); + } + } +} + +#[derive(Resource)] +pub struct NodeCommandExecutionInnerSystem { + pub(crate) execution: Option>>, +} + +#[derive(Resource)] +pub struct NodeCommandExecutionInnerSystemAccess { + cid: Access, +} + +impl NodeCommandExecutionInnerSystemAccess { + pub fn is_conflicting( + &self, + system: &dyn System, + ) -> bool { + !self.cid.get_conflicts(system.component_access()).is_empty() + } +} + +#[doc(hidden)] +pub struct WorldUnsafeParam<'w>(pub(crate) UnsafeWorldCell<'w>); + +unsafe impl SystemParam for WorldUnsafeParam<'_> { + type Item<'world, 'state> = WorldUnsafeParam<'world>; + + type State = (); + + fn init_state(_world: &mut World, _system_meta: &mut SystemMeta) -> Self::State { + () + } + + unsafe fn get_param<'world, 'state>( + _state: &'state mut Self::State, + _system_meta: &SystemMeta, + world: UnsafeWorldCell<'world>, + _change_tick: Tick, + ) -> Self::Item<'world, 'state> { + WorldUnsafeParam::<'world>(world) + } +} + +pub fn node_command_execution(world: &mut World) { + fn node_execution( + mut graph: ResMut, + nce_unsafe: WorldUnsafeParam, + mut execution_events: EventReader, + access: Res, + mut old_archetype_generation: Local>, + ) { + let mut graph = graph.take(); + + let new_archetype_generation = nce_unsafe.0.archetypes().generation(); + if Some(new_archetype_generation) != *old_archetype_generation { + *old_archetype_generation = Some(new_archetype_generation); + for node in graph.shared.get_mut().nodes_mut().iter_mut() { + if let Some(ref mut system) = node.execute { + system.update_archetype_component_access(nce_unsafe.0); + } + } + } + + for event in execution_events.iter() { + let mut executor = event.executor.clone(); + let real_executor = event.real_executor; + let read = event.compiled.results.to_read(); + + // SAFETY: safety is given by system, that we are calling + let executor_ptr = NonNull::new(&mut executor as *mut CommandExecutor).unwrap(); + + // SAFETY: above + let read_static: ParseResultsRead<'static> = unsafe { std::mem::transmute(read) }; + + for path in event.compiled.path.iter() { + let node = graph.get_mut_node(*path).unwrap().execute.as_mut().unwrap(); + + // SAFETY: all systems are checked for conflicts + unsafe { + node.run_unsafe( + (read_static.clone(), real_executor, executor_ptr), + nce_unsafe.0, + ); + } + } + } + + // We want to ensure that nothing will be used further (Also drop, which can in + // theory use UnsafeWorldCell) + drop(execution_events); + drop(access); + + // SAFETY: no SystemParams, which require any world, will be used further + let world = unsafe { nce_unsafe.0.world_mut() }; + + for node in graph.shared.get_mut().nodes_mut().iter_mut() { + if let Some(ref mut system) = node.execute { + system.update_archetype_component_access(nce_unsafe.0); + } + } + + world.resource_mut::().insert(graph); + } + + let unsafe_world_cell = world.as_unsafe_world_cell(); + + // initializing system if we didn't do that + + // SAFETY: there is not mutable reference to this resource + let inner_system = match unsafe { unsafe_world_cell.world_mut() } + .get_resource_mut::() + { + Some(inner_system) => inner_system.into_inner(), + None => { + let mut inner_system = Box::new(IntoSystem::into_system(node_execution)); + + inner_system.initialize(unsafe { unsafe_world_cell.world_mut() }); + + let cid = inner_system.component_access().clone(); + // let acid = inner_system.archetype_component_access().clone(); + + // SAFETY: There is no references from this world + unsafe { unsafe_world_cell.world_mut() }.insert_resource( + NodeCommandExecutionInnerSystem { + execution: Some(inner_system), + }, + ); + + // SAFETY: we are not using any old resource references after this + unsafe { unsafe_world_cell.world_mut() } + .insert_resource(NodeCommandExecutionInnerSystemAccess { cid }); + + // SAFETY: all previous references are dropped + unsafe { unsafe_world_cell.world_mut() } + .resource_mut::() + .into_inner() + } + }; + + let mut execution_system = std::mem::replace(&mut inner_system.execution, None).unwrap(); + + // SAFETY: + // - we don't have anything from the world + let world = unsafe { unsafe_world_cell.world_mut() }; + + // launching system + + execution_system.run((), world); + + // applying system + + execution_system.apply_deferred(world); + + // we are returning our system to the resource + world + .resource_mut::() + .execution = Some(execution_system); +} diff --git a/crates/valence_command/src/lib.rs b/crates/valence_command/src/lib.rs new file mode 100644 index 000000000..bc5b22152 --- /dev/null +++ b/crates/valence_command/src/lib.rs @@ -0,0 +1,52 @@ +use bevy_app::{App, Plugin, PostUpdate, PreUpdate, Update}; +use bevy_ecs::schedule::IntoSystemConfigs; +use compile::{compile_commands, CompiledCommandExecutionEvent}; +use exec::{command_execution_packet, node_command_execution, CommandExecutionEvent}; +use nodes::{send_nodes_to_clients, update_root_nodes, NodeGraphInWorld}; +use suggestions::{ + send_calculated_suggestions, suggestions_request_packet, suggestions_spawn_tasks, + SuggestionsAnswerEvent, SuggestionsQueue, SuggestionsRequestEvent, SuggestionsTokioRuntime, +}; + +pub mod boolean; +pub mod builder; +pub mod command; +pub mod compile; +pub mod exec; +pub mod nodes; +pub mod nums; +pub mod parse; +pub mod pkt; +pub mod reader; +pub mod suggestions; + +pub struct CommandPlugin; + +impl Plugin for CommandPlugin { + fn build(&self, app: &mut App) { + app.add_systems( + PreUpdate, + ( + command_execution_packet, + compile_commands.after(command_execution_packet), + suggestions_request_packet, + ), + ) + .add_systems(Update, (node_command_execution, suggestions_spawn_tasks)) + .add_systems( + PostUpdate, + ( + update_root_nodes, + send_nodes_to_clients.after(update_root_nodes), + send_calculated_suggestions, + ), + ) + .init_resource::() + .init_resource::() + .init_resource::() + .add_event::() + .add_event::() + .add_event::() + .add_event::(); + } +} diff --git a/crates/valence_command/src/nodes.rs b/crates/valence_command/src/nodes.rs new file mode 100644 index 000000000..6d99cd3cc --- /dev/null +++ b/crates/valence_command/src/nodes.rs @@ -0,0 +1,524 @@ +use std::borrow::Cow; +use std::cell::UnsafeCell; +use std::collections::HashMap; + +use bevy_ecs::prelude::{Component, DetectChanges}; +use bevy_ecs::query::{Added, Changed, Or, WorldQuery}; +use bevy_ecs::system::{Local, ParamSet, Query, Res, ResMut, Resource, System}; +use bevy_ecs::world::Ref; +use rustc_hash::{FxHashMap, FxHashSet}; +use smallvec::SmallVec; +use valence_client::Client; +use valence_core::__private::VarInt; +use valence_core::protocol::encode::WritePacket; +use valence_core::protocol::Encode; + +use crate::command::CommandArguments; +use crate::parse::ParseObject; +use crate::pkt::{self, RawCommandTreeS2c}; + +#[derive(Resource)] +pub struct NodeGraphInWorld(pub Option); + +impl Default for NodeGraphInWorld { + fn default() -> Self { + Self(Some(NodeGraph::default())) + } +} + +impl NodeGraphInWorld { + const MESSAGE: &str = + "This NodeGraph is used by some system, which is requiring full access to it."; + + /// # Panics + /// If this method were called in an environment, which requires full access + /// to it + pub fn get(&self) -> &NodeGraph { + self.0.as_ref().expect(Self::MESSAGE) + } + + /// # Panics + /// If this method were called in an environment, which requires full access + /// to it + pub fn get_mut(&mut self) -> &mut NodeGraph { + self.0.as_mut().expect(Self::MESSAGE) + } + + pub(crate) fn take(&mut self) -> NodeGraph { + std::mem::replace(&mut self.0, None).expect(Self::MESSAGE) + } + + pub(crate) fn insert(&mut self, graph: NodeGraph) { + assert!(self.0.is_none(), "This NodeGraph is in the world"); + self.0 = Some(graph); + } +} + +pub struct NodeGraph { + pub(crate) shared: UnsafeCell, + pub(crate) changed: bool, + pub(crate) root_nodes: Vec, +} + +#[derive(Default)] +pub(crate) struct SharedNodeGraph { + pub(crate) nodes: Vec, + pub(crate) nodes_len: usize, + pub(crate) first_layer: NodeChildrenFlow, +} + +impl SharedNodeGraph { + pub fn nodes(&self) -> &[Node] { + &self.nodes[..self.nodes_len] + } + + pub fn nodes_mut(&mut self) -> &mut [Node] { + &mut self.nodes[..self.nodes_len] + } + + pub fn update_nodes_len(&mut self) { + self.nodes_len = self.nodes.len(); + } +} + +// SAFETY: UnsafeCell does not implement Sync only because it was said so +unsafe impl Sync for NodeGraph +where + SharedNodeGraph: Sync, + bool: Sync, + Vec: Sync, +{ +} + +impl Default for NodeGraph { + fn default() -> Self { + let mut node_graph = Self { + shared: Default::default(), + changed: true, + root_nodes: vec![RootNode { + bytes: vec![], + changed: false, + updated: false, + policy: RootNodePolicy::Exclude(Default::default()), + }], + }; + + node_graph.update_root_nodes(&mut HashMap::default()); + + node_graph + } +} + +impl NodeGraph { + pub(crate) fn reserve_node_id(&mut self, kind: NodeKind) -> NodeId { + let shared = self.shared.get_mut(); + let id = shared.nodes.len(); + shared.nodes.push(Node { + kind, + execute: None, + flow: NodeFlow::Stop, + parents: Default::default(), + }); + NodeId(id) + } + + pub fn has_changed(&self) -> bool { + self.changed + } + + pub(crate) fn shared(&self) -> &SharedNodeGraph { + // SAFETY: we are returning an immutable reference + unsafe { &*self.shared.get() } + } + + pub(crate) fn get_node(&self, id: NodeId) -> Option<&Node> { + self.shared().nodes.get(id.0) + } + + pub(crate) fn get_mut_node(&mut self, id: NodeId) -> Option<&mut Node> { + // SAFETY: We have a mutable reference to the NodeGraph that means that there is + // no other references from it + unsafe { self.get_mut_node_unsafe(id) } + } + + /// # Safety + /// - There may not be a mutable reference to the same node + pub(crate) unsafe fn get_mut_node_unsafe(&self, id: NodeId) -> Option<&mut Node> { + (&mut *self.shared.get()).nodes.get_mut(id.0) + } + + pub(crate) fn get_mut_children_flow(&mut self, id: NodeId) -> Option<&mut NodeChildrenFlow> { + // SAFETY: We have a mutable reference to the NodeGraph that means that there is + // no other references from it + unsafe { self.get_mut_children_flow_unsafe(id) } + } + + /// # Safety + /// - There may not be a mutable reference to the same NodeChildrenFlow + pub(crate) unsafe fn get_mut_children_flow_unsafe( + &self, + id: NodeId, + ) -> Option<&mut NodeChildrenFlow> { + let shared = &mut *self.shared.get(); + shared + .nodes + .get_mut(id.0) + .map(|v| match v.flow { + NodeFlow::Children(ref mut children_flow) => Some(children_flow.as_mut()), + _ => None, + }) + .unwrap_or(Some(&mut shared.first_layer)) + } + + pub(crate) fn update_root_nodes(&mut self, node2id: &mut FxHashMap) { + let mut root_nodes = std::mem::replace(&mut self.root_nodes, vec![]); + + for root_node in root_nodes.iter_mut() { + root_node.updated = false; + + if self.changed || root_node.changed { + root_node.changed = false; + node2id.clear(); + // Shouldn't happen, so we are not returning root nodes back if error happens + root_node.write(self, node2id).unwrap(); + } + } + + self.changed = false; + + self.root_nodes = root_nodes; + } + + pub(crate) fn get_root_node(&self, id: RootNodeId) -> Option<&RootNode> { + self.root_nodes.get(id.0) + } +} + +pub struct RootNode { + pub(crate) policy: RootNodePolicy, + changed: bool, + bytes: Vec, + updated: bool, +} + +impl RootNode { + pub(crate) fn write( + &mut self, + graph: &NodeGraph, + node2id: &mut FxHashMap, + ) -> anyhow::Result<()> { + let mut nodes = vec![]; + + self.updated = true; + + self.write_single(NodeId::ROOT, graph, node2id, &mut nodes); + + self.bytes.clear(); + pkt::CommandTreeS2c { + commands: nodes, + root_index: VarInt( + *node2id + .get(&NodeId::ROOT) + .expect("There is no root entity in entity2id map"), + ), + } + .encode(&mut self.bytes) + } + + fn write_single<'a>( + &mut self, + node: NodeId, + graph: &'a NodeGraph, + node2id: &mut FxHashMap, + nodes: &mut Vec>, + ) -> i32 { + + if let Some(id) = node2id.get(&node) { + return *id; + } + + let id = nodes.len() as i32; + + node2id.insert(node, id); + + let node = graph.get_node(node); + + // If parser can not 'immitate' itself as brigadier's one then we say that it is + // a greedy phrase. All children and redirects can be omitted in that + // case. Valence will handle executions and suggestion's requests correctly + // anyway. + let mut children_redirect_skip = false; + + nodes.push(pkt::Node { + children: vec![], + data: match node { + Some(node) => match node.kind { + NodeKind::Argument { + ref name, + ref parse, + } => match parse.obj_brigadier() { + Some(parser) => pkt::NodeData::Argument { + name: Cow::Borrowed(name.as_str()), + parser, + suggestion: parse.obj_brigadier_suggestions(), + }, + None => { + children_redirect_skip = true; + // What to do with the name? + pkt::NodeData::Argument { + name: Cow::Borrowed(name.as_str()), + parser: pkt::Parser::String(pkt::StringArg::GreedyPhrase), + suggestion: Some(NodeSuggestion::AskServer), + } + } + }, + NodeKind::Literal { ref name } => pkt::NodeData::Literal { + name: Cow::Borrowed(name.as_str()), + }, + }, + // Root + None => pkt::NodeData::Root, + }, + executable: node.and_then(|v| v.execute.as_ref()).is_some(), + redirect_node: None, + }); + + if !children_redirect_skip { + match node { + Some(node) => match node.flow { + NodeFlow::Children(ref children_flow) => { + let children = children_flow + .children + .iter() + .filter_map(|v| { + self.policy + .check(*v) + .then(|| VarInt(self.write_single(*v, graph, node2id, nodes))) + }) + .collect(); + + nodes[id as usize].children = children; + } + NodeFlow::Redirect(redirect) => { + if self.policy.check(redirect) { + let redirect = self.write_single(redirect, graph, node2id, nodes); + nodes[id as usize].redirect_node = Some(VarInt(redirect)); + } + } + NodeFlow::Stop => {} + }, + None => { + let children = graph + .shared() + .first_layer + .children + .iter() + .filter_map(|v| { + self.policy + .check(*v) + .then(|| VarInt(self.write_single(*v, graph, node2id, nodes))) + }) + .collect(); + + nodes[id as usize].children = children; + } + } + } + + id + } +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum RootNodePolicy { + Exclude(FxHashSet), + Include(FxHashSet), +} + +impl RootNodePolicy { + pub fn check(&self, node: NodeId) -> bool { + match self { + Self::Exclude(set) => !set.contains(&node), + Self::Include(set) => set.contains(&node), + } + } +} + +pub(crate) struct VecRootNodePolicy(Vec); + +impl VecRootNodePolicy { + pub(crate) fn add_node(&mut self, nodes_count: usize, flag: bool) { + if nodes_count % 8 == 0 { + self.0.push(if flag { 1 } else { 0 }) + } else { + *self.0.get_mut(nodes_count / 8).unwrap() |= 1 << (nodes_count % 8); + } + } + + pub(crate) fn set_node(&mut self, index: usize, flag: bool) { + *self.0.get_mut(index / 8).unwrap() |= 1 << (index % 8); + } + + pub(crate) fn get_node(&self, index: usize) -> Option { + self.0.get(index / 8).map(|v| (v & (1 << (index % 8))) == 0) + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub struct RootNodeId(usize); + +impl RootNodeId { + pub const SUPER: Self = Self(0); +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub struct NodeId(usize); + +impl NodeId { + pub const ROOT: Self = Self(usize::MAX); +} + +pub struct Node { + pub(crate) kind: NodeKind, + pub(crate) flow: NodeFlow, + pub(crate) parents: SmallVec<[NodeId; 2]>, + pub(crate) execute: Option>>, +} + +impl Node { + pub(crate) fn remove_parent(&mut self, node_id: NodeId) { + if let Some((index, _)) = self + .parents + .iter() + .enumerate() + .find(|(_, v)| **v == node_id) + { + self.parents.swap_remove(index); + } + } +} + +#[derive(Debug)] +pub(crate) enum NodeFlow { + Children(Box), + Redirect(NodeId), + Stop, +} + +#[derive(Debug, Default)] +pub(crate) struct NodeChildrenFlow { + pub(crate) children: FxHashSet, + pub(crate) literals: HashMap, + pub(crate) parsers: Vec, +} + +impl NodeChildrenFlow { + /// Adds a new node to the children vec with respecting literals, + /// parsers and node's parents. If this node is already added does nothing # + pub(crate) fn add(&mut self, self_id: NodeId, node: &mut Node, node_id: NodeId) { + if self.children.insert(node_id) { + node.parents.push(self_id); + match node.kind { + NodeKind::Argument { .. } => { + self.parsers.push(node_id); + } + NodeKind::Literal { ref name } => { + self.literals.insert(name.clone(), node_id); + } + } + } + } + + pub(crate) fn remove(&mut self, self_id: NodeId, node: &mut Node, node_id: NodeId) { + if self.children.remove(&node_id) { + match node.kind { + NodeKind::Argument { .. } => { + if let Some((index, _)) = self + .parsers + .iter() + .enumerate() + .find(|(_, v)| **v == node_id) + { + self.parsers.swap_remove(index); + } + } + NodeKind::Literal { ref name } => { + self.literals.remove(name.as_str()); + } + } + + node.remove_parent(self_id); + } + } +} + +pub(crate) enum NodeKind { + Argument { + name: String, + parse: Box, + }, + Literal { + name: String, + }, +} + +#[derive(Clone, Copy, Debug)] +pub enum NodeSuggestion { + AskServer, + AllRecipes, + AvailableSounds, + AvailableBiomes, + SummonableEntities, +} + +/// Node of a **bevy**'s Entity not minecraft's. Block will inherit node of +/// their instances and entity may have this component on them. If there is no +/// component present then [`PrimaryNodeRoot`] will be chosen. +#[derive(Component)] +pub struct EntityNode(pub RootNodeId); + +pub fn update_root_nodes( + mut graph: ResMut, + mut node2id: Local>, +) { + let graph = graph.get_mut(); + graph.update_root_nodes(&mut node2id); +} + +#[derive(WorldQuery)] +pub struct EntityNodeQuery(Option>); + +impl EntityNodeQueryItem<'_> { + pub fn get(&self) -> RootNodeId { + self.0.as_ref().map(|v| v.0).unwrap_or(RootNodeId::SUPER) + } + + pub fn is_changed(&self) -> bool { + self.0.as_ref().map(|v| v.is_changed()).unwrap_or(false) + } +} + +pub fn send_nodes_to_clients( + graph: Res, + mut param_set: ParamSet<( + Query<(&mut Client, EntityNodeQuery), Or<(Changed, Added)>>, + Query<(&mut Client, EntityNodeQuery)>, + )>, +) { + let graph = graph.get(); + + // If graph has changed then any of root nodes could be changed + if graph.has_changed() { + for (mut client, entity_node) in param_set.p1().iter_mut() { + let root = &graph.get_root_node(entity_node.get()).unwrap(); + if client.is_added() || entity_node.is_changed() || root.updated { + client.write_packet(&RawCommandTreeS2c(&root.bytes)); + } + } + } else { + for (mut client, entity_node) in param_set.p0().iter_mut() { + let root = &graph.get_root_node(entity_node.get()).unwrap(); + client.write_packet(&RawCommandTreeS2c(&root.bytes)); + } + } +} diff --git a/crates/valence_command/src/nums.rs b/crates/valence_command/src/nums.rs new file mode 100644 index 000000000..243beccf0 --- /dev/null +++ b/crates/valence_command/src/nums.rs @@ -0,0 +1,154 @@ +use std::any::TypeId; +use std::borrow::Cow; + +use bevy_ecs::system::SystemParamItem; +use valence_core::text::Text; +use valence_core::translation_key::{ + ARGUMENT_FLOAT_BIG, ARGUMENT_FLOAT_LOW, ARGUMENT_INTEGER_BIG, ARGUMENT_INTEGER_LOW, + PARSING_FLOAT_EXPECTED, PARSING_FLOAT_INVALID, PARSING_INT_EXPECTED, PARSING_INT_INVALID, +}; + +use crate::command::CommandExecutorBase; +use crate::nodes::NodeSuggestion; +use crate::parse::{Parse, ParseResult}; +use crate::pkt; +use crate::reader::{ArcStrReader, StrLocated, StrReader, StrSpan}; +use crate::suggestions::Suggestion; + +#[derive(Clone, Copy, Debug, PartialEq)] +pub struct NumberBounds { + pub min: Option, + pub max: Option, +} + +impl Default for NumberBounds { + fn default() -> Self { + Self { + min: None, + max: None, + } + } +} + +macro_rules! num_parse { + ($ty:ty, $parser:ident, $low:expr, $big:expr, $expected:expr, $invalid:expr) => { + #[async_trait::async_trait] + impl Parse for $ty { + type Item<'a> = Self; + + type Data<'a> = NumberBounds; + + type Suggestions = (); + + type SuggestionsAsyncData = (); + + type SuggestionsParam = (); + + const VANILLA: bool = true; + + fn parse_id() -> TypeId { + TypeId::of::() + } + + fn item_id() -> TypeId { + TypeId::of::() + } + + fn parse<'a>( + data: &Self::Data<'a>, + _suggestions: &mut Self::Suggestions, + reader: &mut StrReader<'a>, + ) -> ParseResult { + reader.err_located(|reader| { + let num_str = reader.read_num_str(); + if num_str.is_empty() { + Err(Text::translate($expected, vec![])) + } else { + match (num_str.parse::(), data.min, data.max) { + (Ok(num), Some(min), _) if num < min => Err(Text::translate( + $low, + vec![min.to_string().into(), num_str.to_string().into()], + )), + (Ok(num), _, Some(max)) if num > max => Err(Text::translate( + $big, + vec![max.to_string().into(), num_str.to_string().into()], + )), + (Ok(num), ..) => Ok(num), + (Err(_), ..) => { + Err(Text::translate($invalid, vec![num_str.to_string().into()])) + } + } + } + }) + } + + fn brigadier(data: &Self::Data<'_>) -> Option> { + Some(pkt::Parser::$parser { + min: data.min, + max: data.max, + }) + } + + fn brigadier_suggestions(_data: &Self::Data<'_>) -> Option { + None + } + + /// Creates a data which will be passed then to + /// [`Parse::suggestions`] method + fn create_suggestions_data( + _data: &Self::Data<'_>, + _command: ArcStrReader, + _executor: CommandExecutorBase, + _suggestion: &Self::Suggestions, + _param: SystemParamItem, + ) -> Self::SuggestionsAsyncData { + () + } + + async fn suggestions( + _command: ArcStrReader, + _executor: CommandExecutorBase, + _suggestion: Box, + _async_data: Self::SuggestionsAsyncData, + ) -> StrLocated]>> { + StrLocated::new(StrSpan::ZERO, Cow::Borrowed(&[])) + } + } + }; +} + +num_parse!( + i32, + Integer, + ARGUMENT_INTEGER_LOW, + ARGUMENT_INTEGER_BIG, + PARSING_INT_EXPECTED, + PARSING_INT_INVALID +); + +num_parse!( + i64, + Long, + ARGUMENT_INTEGER_LOW, + ARGUMENT_INTEGER_BIG, + PARSING_INT_EXPECTED, + PARSING_INT_INVALID +); + +num_parse!( + f32, + Float, + ARGUMENT_FLOAT_LOW, + ARGUMENT_FLOAT_BIG, + PARSING_FLOAT_EXPECTED, + PARSING_FLOAT_INVALID +); + +num_parse!( + f64, + Double, + ARGUMENT_FLOAT_LOW, + ARGUMENT_FLOAT_BIG, + PARSING_FLOAT_EXPECTED, + PARSING_FLOAT_INVALID +); diff --git a/crates/valence_command/src/parse.rs b/crates/valence_command/src/parse.rs new file mode 100644 index 000000000..834db9d79 --- /dev/null +++ b/crates/valence_command/src/parse.rs @@ -0,0 +1,338 @@ +use std::any::{Any, TypeId}; +use std::borrow::Cow; +use std::future::Future; +use std::pin::Pin; + +use bevy_ecs::system::{ReadOnlySystemParam, SystemParamItem, SystemState}; +use bevy_ecs::world::World; +use valence_core::text::Text; + +use crate::command::CommandExecutorBase; +use crate::nodes::NodeSuggestion; +use crate::pkt; +use crate::reader::{ArcStrReader, StrLocated, StrReader}; +use crate::suggestions::Suggestion; + +pub type ParseResult = Result>; + +/// Identifies any object that can be parsed in a command +#[async_trait::async_trait] +pub trait Parse: 'static { + type Item<'a>: 'a + Send + Sized; + + /// Any data that can be possibly passed to [`Parse`] to change it's + /// behaviour. + type Data<'a>: 'a + Sync + Send; + + /// A type which is used to calculate suggestions after [`Parse::parse`] or + /// [`Parse::skip`] methods were called. + type Suggestions: 'static + Sync + Send + Default; + + /// A param which is used to calculate [`Parse::SuggestionsAsyncData`] + type SuggestionsParam: 'static + ReadOnlySystemParam; + + /// A data which will be then given to the async function + /// [`Parse::suggestions`] + type SuggestionsAsyncData: 'static + Send; + + const VANILLA: bool; + + fn parse_id() -> TypeId; + + fn item_id() -> TypeId { + TypeId::of::>() + } + + /// Parses value from a given string and moves reader to the place where the + /// value is ended. + /// ### May not + /// - give different results on the same data and reader string + fn parse<'a>( + data: &Self::Data<'a>, + suggestions: &mut Self::Suggestions, + reader: &mut StrReader<'a>, + ) -> ParseResult>; + + /// Does the same as [`Parse::parse`] but doesn't return any value. Useful + /// for values, which contain some stuff that needs heap allocation + fn skip<'a>( + data: &Self::Data<'a>, + suggestions: &mut Self::Suggestions, + reader: &mut StrReader<'a>, + ) -> ParseResult<()> { + Self::parse(data, suggestions, reader).map(|_| ()) + } + + fn brigadier(data: &Self::Data<'_>) -> Option>; + + fn brigadier_suggestions(data: &Self::Data<'_>) -> Option; + + /// Creates a data which will be passed then to + /// [`Parse::suggestions`] method + fn create_suggestions_data( + data: &Self::Data<'_>, + command: ArcStrReader, + executor: CommandExecutorBase, + suggestions: &Self::Suggestions, + param: SystemParamItem, + ) -> Self::SuggestionsAsyncData; + + async fn suggestions( + command: ArcStrReader, + executor: CommandExecutorBase, + suggestions: Box, + async_data: Self::SuggestionsAsyncData, + ) -> StrLocated]>>; +} + +#[derive(Clone, Debug)] +pub(crate) struct ParseResultsWrite(pub Vec); + +// we don't want rust to change variable's places in the struct +#[repr(C)] +pub(crate) struct RawAny { + obj_drop: Option, + tid: TypeId, + obj: I, +} + +impl Drop for RawAny { + fn drop(&mut self) { + if let Some(obj_drop) = self.obj_drop { + // SAFETY: Guarantied by struct + unsafe { obj_drop(&mut self.obj as *mut I) } + } + } +} + +impl RawAny { + pub fn new(obj: I) -> Self { + Self { + obj_drop: if std::mem::needs_drop::() { + Some(std::ptr::drop_in_place) + } else { + None + }, + tid: T::item_id(), + obj, + } + } + + pub fn write(self, vec: &mut Vec) { + let mut to_write = (0, self); + + // Depends on system. if less than 64bit then the size does not change otherwise + // changes + let len = std::mem::size_of::<(usize, RawAny)>() / 8; + + to_write.0 = len; + + // TypeId is an u64, so it shouldn't panic + debug_assert!(std::mem::size_of::<(usize, RawAny)>() % 8 == 0); + + let self_ptr = &to_write as *const (usize, RawAny) as *const u64; + + // SAFETY: self_ptr is the pointer to our struct, the size of u64 we can put + // into the struct is equal to len variable + unsafe { + vec.extend_from_slice(std::slice::from_raw_parts(self_ptr, len)); + } + + // We wrote bytes to the vector and all objects there shouldn't be dropped so we + // can use them in future + std::mem::forget(to_write); + } + + /// # Safety + /// - Slice must be created using [`Self::write`] method + pub unsafe fn read<'a, T: Parse>(slice: &mut &'a [u64]) -> &'a Self { + if slice.len() <= std::mem::size_of::<(usize, RawAny<()>)>() / 8 { + panic!("Slice doesn't contain essential information as length, drop and tid"); + } + + let slice_ptr = slice.as_ptr(); + + // SAFETY: the caller + let empty_any_ptr = unsafe { &*(slice_ptr as *const (usize, RawAny<()>)) }; + + if empty_any_ptr.1.tid != T::item_id() { + panic!("Tried to read the wrong object"); + } + + // SAFETY: we have checked if the type the caller has gave to us is the right + // one. + let right_any_ptr = + unsafe { &*(empty_any_ptr as *const (usize, RawAny<()>) as *const (usize, RawAny)) }; + + *slice = &slice[right_any_ptr.0..]; + + &right_any_ptr.1 + } + + /// # Safety + /// - Vec must be filled using [`Self::write`] method + pub unsafe fn drop_all(vec: &mut Vec) { + let mut slice = vec.as_mut_slice(); + + while !slice.is_empty() { + let slice_ptr = slice.as_mut_ptr(); + + let empty_any = slice_ptr as *mut (usize, RawAny<()>); + + // SAFETY: the pointer is right + unsafe { + std::ptr::drop_in_place(empty_any); + } + + // SAFETY: the pointer is right + slice = &mut slice[unsafe { (&*empty_any).0 }..]; + } + } +} + +impl ParseResultsWrite { + pub fn write<'a, T: Parse>(&mut self, obj: T::Item<'a>) { + RawAny::>::new::(obj).write(&mut self.0); + } +} + +impl Drop for ParseResultsWrite { + fn drop(&mut self) { + // SAFETY: only write method is public + unsafe { RawAny::<()>::drop_all(&mut self.0) } + } +} + +#[derive(Clone, Debug)] +pub struct ParseResultsRead<'a>(&'a [u64]); + +impl<'a> ParseResultsRead<'a> { + pub fn read(&mut self) -> &'a T::Item<'a> { + // SAFETY: slice is from ParseResultsWrite + unsafe { &RawAny::>::read::(&mut self.0).obj } + } +} + +#[derive(Debug)] +pub(crate) struct ParseResults { + command: String, + results: ParseResultsWrite, +} + +impl ParseResults { + pub fn new_empty(command: String) -> Self { + Self { + command, + results: ParseResultsWrite(vec![]), + } + } + + pub fn to_write(&mut self) -> (StrReader, &mut ParseResultsWrite) { + (StrReader::from_command(&self.command), &mut self.results) + } + + pub fn to_read(&self) -> ParseResultsRead { + ParseResultsRead(&self.results.0) + } +} + +pub(crate) trait ParseObject: Sync + Send { + fn parse_id(&self) -> TypeId; + + fn initialize(&mut self, world: &mut World); + + fn obj_parse<'a>( + &self, + reader: &mut StrReader<'a>, + fill: &mut ParseResultsWrite, + ) -> (ParseResult<()>, Box); + + fn obj_skip<'a>(&self, reader: &mut StrReader<'a>) -> (ParseResult<()>, Box); + + fn obj_brigadier(&self) -> Option>; + + fn obj_brigadier_suggestions(&self) -> Option; + + fn obj_suggestions<'f>( + &mut self, + suggestions: Box, + command: ArcStrReader, + executor: CommandExecutorBase, + world: &World, + ) -> Pin]>>> + Send + 'f>>; + + fn obj_apply_deferred(&mut self, world: &mut World); +} + +pub(crate) struct ParseWithData { + pub data: T::Data<'static>, + pub state: Option>, +} + +impl ParseObject for ParseWithData { + fn parse_id(&self) -> TypeId { + T::parse_id() + } + + fn initialize(&mut self, world: &mut World) { + if self.state.is_none() { + self.state = Some(SystemState::new(world)); + } + } + + fn obj_parse<'a>( + &self, + reader: &mut StrReader<'a>, + fill: &mut ParseResultsWrite, + ) -> (ParseResult<()>, Box) { + let mut suggestions = T::Suggestions::default(); + let result = T::parse(&self.data, &mut suggestions, unsafe { + std::mem::transmute(reader) + }); + ( + match result { + Ok(obj) => { + fill.write::(obj); + Ok(()) + } + Err(e) => Err(e), + }, + Box::new(suggestions), + ) + } + + fn obj_skip<'a>(&self, reader: &mut StrReader<'a>) -> (ParseResult<()>, Box) { + let mut suggestions = T::Suggestions::default(); + let result = T::skip(&self.data, &mut suggestions, unsafe { + std::mem::transmute(reader) + }); + (result, Box::new(suggestions)) + } + + fn obj_brigadier(&self) -> Option> { + T::brigadier(&self.data) + } + + fn obj_brigadier_suggestions(&self) -> Option { + T::brigadier_suggestions(&self.data) + } + + fn obj_suggestions<'f>( + &mut self, + suggestion: Box, + command: ArcStrReader, + executor: CommandExecutorBase, + world: &World, + ) -> Pin]>>> + Send + 'f>> + { + let suggestion: Box = suggestion.downcast().unwrap(); + let param = self.state.as_mut().unwrap().get(world); + let data = + T::create_suggestions_data(&self.data, command.clone(), executor, &suggestion, param); + T::suggestions(command, executor, suggestion, data) + } + + fn obj_apply_deferred(&mut self, world: &mut World) { + self.state.as_mut().unwrap().apply(world); + } +} diff --git a/crates/valence_command/src/path.rs b/crates/valence_command/src/path.rs new file mode 100644 index 000000000..e69de29bb diff --git a/crates/valence_command/src/pkt.rs b/crates/valence_command/src/pkt.rs new file mode 100644 index 000000000..f1e04406d --- /dev/null +++ b/crates/valence_command/src/pkt.rs @@ -0,0 +1,458 @@ +use std::borrow::Cow; +use std::io::Write; + +use anyhow::bail; +use byteorder::WriteBytesExt; +use valence_core::__private::VarInt; +use valence_core::ident::Ident; +use valence_core::protocol::{packet_id, Decode, Encode, Packet}; + +use crate::nodes::NodeSuggestion; +use crate::suggestions::Suggestion; + +#[derive(Clone, Debug, Encode, Decode, Packet)] +#[packet(id = packet_id::COMMAND_TREE_S2C)] +pub struct CommandTreeS2c<'a> { + pub commands: Vec>, + pub root_index: VarInt, +} + +#[derive(Clone, Copy, Debug, Packet)] +#[packet(id = packet_id::COMMAND_TREE_S2C)] +pub(crate) struct RawCommandTreeS2c<'a>(pub &'a [u8]); + +impl<'a> Encode for RawCommandTreeS2c<'a> { + fn encode(&self, mut w: impl Write) -> anyhow::Result<()> { + w.write_all(self.0)?; + Ok(()) + } +} + +#[derive(Clone, Debug)] +pub struct Node<'a> { + pub children: Vec, + pub data: NodeData<'a>, + pub executable: bool, + pub redirect_node: Option, +} + +#[derive(Clone, Debug)] +pub enum NodeData<'a> { + Root, + Literal { + name: Cow<'a, str>, + }, + Argument { + name: Cow<'a, str>, + parser: Parser<'a>, + suggestion: Option, + }, +} + +#[derive(Clone, Debug)] +pub enum Parser<'a> { + Bool, + Float { min: Option, max: Option }, + Double { min: Option, max: Option }, + Integer { min: Option, max: Option }, + Long { min: Option, max: Option }, + String(StringArg), + Entity { single: bool, only_players: bool }, + GameProfile, + BlockPos, + ColumnPos, + Vec3, + Vec2, + BlockState, + BlockPredicate, + ItemStack, + ItemPredicate, + Color, + Component, + Message, + NbtCompoundTag, + NbtTag, + NbtPath, + Objective, + ObjectiveCriteria, + Operation, + Particle, + Angle, + Rotation, + ScoreboardSlot, + ScoreHolder { allow_multiple: bool }, + Swizzle, + Team, + ItemSlot, + ResourceLocation, + Function, + EntityAnchor, + IntRange, + FloatRange, + Dimension, + GameMode, + Time, + ResourceOrTag { registry: Ident> }, + ResourceOrTagKey { registry: Ident> }, + Resource { registry: Ident> }, + ResourceKey { registry: Ident> }, + TemplateMirror, + TemplateRotation, + Heightmap, + Uuid, +} + +#[derive(Copy, Clone, PartialEq, Eq, Debug, Encode, Decode, Default)] +pub enum StringArg { + #[default] + SingleWord, + QuotablePhrase, + GreedyPhrase, +} + +impl Encode for Node<'_> { + fn encode(&self, mut w: impl Write) -> anyhow::Result<()> { + let node_type = match &self.data { + NodeData::Root => 0, + NodeData::Literal { .. } => 1, + NodeData::Argument { .. } => 2, + }; + + let has_suggestion = matches!( + &self.data, + NodeData::Argument { + suggestion: Some(_), + .. + } + ); + + let flags: u8 = node_type + | (self.executable as u8 * 0x04) + | (self.redirect_node.is_some() as u8 * 0x08) + | (has_suggestion as u8 * 0x10); + + w.write_u8(flags)?; + + self.children.encode(&mut w)?; + + if let Some(redirect_node) = self.redirect_node { + redirect_node.encode(&mut w)?; + } + + match &self.data { + NodeData::Root => {} + NodeData::Literal { name } => { + name.encode(&mut w)?; + } + NodeData::Argument { + name, + parser, + suggestion, + } => { + name.encode(&mut w)?; + parser.encode(&mut w)?; + + if let Some(suggestion) = suggestion { + match suggestion { + NodeSuggestion::AskServer => "ask_server", + NodeSuggestion::AllRecipes => "all_recipes", + NodeSuggestion::AvailableSounds => "available_sounds", + NodeSuggestion::AvailableBiomes => "available_biomes", + NodeSuggestion::SummonableEntities => "summonable_entities", + } + .encode(&mut w)?; + } + } + } + + Ok(()) + } +} + +impl<'a> Decode<'a> for Node<'a> { + fn decode(r: &mut &'a [u8]) -> anyhow::Result { + let flags = u8::decode(r)?; + + let children = Vec::decode(r)?; + + let redirect_node = if flags & 0x08 != 0 { + Some(VarInt::decode(r)?) + } else { + None + }; + + let node_data = match flags & 0x3 { + 0 => NodeData::Root, + 1 => NodeData::Literal { + name: Cow::decode(r)?, + }, + 2 => NodeData::Argument { + name: Cow::decode(r)?, + parser: Parser::decode(r)?, + suggestion: if flags & 0x10 != 0 { + Some(match Ident::>::decode(r)?.as_str() { + "minecraft:ask_server" => NodeSuggestion::AskServer, + "minecraft:all_recipes" => NodeSuggestion::AllRecipes, + "minecraft:available_sounds" => NodeSuggestion::AvailableSounds, + "minecraft:available_biomes" => NodeSuggestion::AvailableBiomes, + "minecraft:summonable_entities" => NodeSuggestion::SummonableEntities, + other => bail!("unknown command suggestion type of \"{other}\""), + }) + } else { + None + }, + }, + n => bail!("invalid node type of {n}"), + }; + + Ok(Self { + children, + data: node_data, + executable: flags & 0x04 != 0, + redirect_node, + }) + } +} + +impl Encode for Parser<'_> { + fn encode(&self, mut w: impl Write) -> anyhow::Result<()> { + match self { + Parser::Bool => 0u8.encode(&mut w)?, + Parser::Float { min, max } => { + 1u8.encode(&mut w)?; + + (min.is_some() as u8 | (max.is_some() as u8 * 0x2)).encode(&mut w)?; + + if let Some(min) = min { + min.encode(&mut w)?; + } + + if let Some(max) = max { + max.encode(&mut w)?; + } + } + Parser::Double { min, max } => { + 2u8.encode(&mut w)?; + + (min.is_some() as u8 | (max.is_some() as u8 * 0x2)).encode(&mut w)?; + + if let Some(min) = min { + min.encode(&mut w)?; + } + + if let Some(max) = max { + max.encode(&mut w)?; + } + } + Parser::Integer { min, max } => { + 3u8.encode(&mut w)?; + + (min.is_some() as u8 | (max.is_some() as u8 * 0x2)).encode(&mut w)?; + + if let Some(min) = min { + min.encode(&mut w)?; + } + + if let Some(max) = max { + max.encode(&mut w)?; + } + } + Parser::Long { min, max } => { + 4u8.encode(&mut w)?; + + (min.is_some() as u8 | (max.is_some() as u8 * 0x2)).encode(&mut w)?; + + if let Some(min) = min { + min.encode(&mut w)?; + } + + if let Some(max) = max { + max.encode(&mut w)?; + } + } + Parser::String(arg) => { + 5u8.encode(&mut w)?; + arg.encode(&mut w)?; + } + Parser::Entity { + single, + only_players, + } => { + 6u8.encode(&mut w)?; + (*single as u8 | (*only_players as u8 * 0x2)).encode(&mut w)?; + } + Parser::GameProfile => 7u8.encode(&mut w)?, + Parser::BlockPos => 8u8.encode(&mut w)?, + Parser::ColumnPos => 9u8.encode(&mut w)?, + Parser::Vec3 => 10u8.encode(&mut w)?, + Parser::Vec2 => 11u8.encode(&mut w)?, + Parser::BlockState => 12u8.encode(&mut w)?, + Parser::BlockPredicate => 13u8.encode(&mut w)?, + Parser::ItemStack => 14u8.encode(&mut w)?, + Parser::ItemPredicate => 15u8.encode(&mut w)?, + Parser::Color => 16u8.encode(&mut w)?, + Parser::Component => 17u8.encode(&mut w)?, + Parser::Message => 18u8.encode(&mut w)?, + Parser::NbtCompoundTag => 19u8.encode(&mut w)?, + Parser::NbtTag => 20u8.encode(&mut w)?, + Parser::NbtPath => 21u8.encode(&mut w)?, + Parser::Objective => 22u8.encode(&mut w)?, + Parser::ObjectiveCriteria => 23u8.encode(&mut w)?, + Parser::Operation => 24u8.encode(&mut w)?, + Parser::Particle => 25u8.encode(&mut w)?, + Parser::Angle => 26u8.encode(&mut w)?, + Parser::Rotation => 27u8.encode(&mut w)?, + Parser::ScoreboardSlot => 28u8.encode(&mut w)?, + Parser::ScoreHolder { allow_multiple } => { + 29u8.encode(&mut w)?; + allow_multiple.encode(&mut w)?; + } + Parser::Swizzle => 30u8.encode(&mut w)?, + Parser::Team => 31u8.encode(&mut w)?, + Parser::ItemSlot => 32u8.encode(&mut w)?, + Parser::ResourceLocation => 33u8.encode(&mut w)?, + Parser::Function => 34u8.encode(&mut w)?, + Parser::EntityAnchor => 35u8.encode(&mut w)?, + Parser::IntRange => 36u8.encode(&mut w)?, + Parser::FloatRange => 37u8.encode(&mut w)?, + Parser::Dimension => 38u8.encode(&mut w)?, + Parser::GameMode => 39u8.encode(&mut w)?, + Parser::Time => 40u8.encode(&mut w)?, + Parser::ResourceOrTag { registry } => { + 41u8.encode(&mut w)?; + registry.encode(&mut w)?; + } + Parser::ResourceOrTagKey { registry } => { + 42u8.encode(&mut w)?; + registry.encode(&mut w)?; + } + Parser::Resource { registry } => { + 43u8.encode(&mut w)?; + registry.encode(&mut w)?; + } + Parser::ResourceKey { registry } => { + 44u8.encode(&mut w)?; + registry.encode(&mut w)?; + } + Parser::TemplateMirror => 45u8.encode(&mut w)?, + Parser::TemplateRotation => 46u8.encode(&mut w)?, + Parser::Heightmap => 47u8.encode(&mut w)?, + Parser::Uuid => 48u8.encode(&mut w)?, + } + + Ok(()) + } +} + +impl<'a> Decode<'a> for Parser<'a> { + fn decode(r: &mut &'a [u8]) -> anyhow::Result { + fn decode_min_max<'a, T: Decode<'a>>( + r: &mut &'a [u8], + ) -> anyhow::Result<(Option, Option)> { + let flags = u8::decode(r)?; + + let min = if flags & 0x1 != 0 { + Some(T::decode(r)?) + } else { + None + }; + + let max = if flags & 0x2 != 0 { + Some(T::decode(r)?) + } else { + None + }; + + Ok((min, max)) + } + + Ok(match u8::decode(r)? { + 0 => Self::Bool, + 1 => { + let (min, max) = decode_min_max(r)?; + Self::Float { min, max } + } + 2 => { + let (min, max) = decode_min_max(r)?; + Self::Double { min, max } + } + 3 => { + let (min, max) = decode_min_max(r)?; + Self::Integer { min, max } + } + 4 => { + let (min, max) = decode_min_max(r)?; + Self::Long { min, max } + } + 5 => Self::String(StringArg::decode(r)?), + 6 => { + let flags = u8::decode(r)?; + Self::Entity { + single: flags & 0x1 != 0, + only_players: flags & 0x2 != 0, + } + } + 7 => Self::GameProfile, + 8 => Self::BlockPos, + 9 => Self::ColumnPos, + 10 => Self::Vec3, + 11 => Self::Vec2, + 12 => Self::BlockState, + 13 => Self::BlockPredicate, + 14 => Self::ItemStack, + 15 => Self::ItemPredicate, + 16 => Self::Color, + 17 => Self::Component, + 18 => Self::Message, + 19 => Self::NbtCompoundTag, + 20 => Self::NbtTag, + 21 => Self::NbtPath, + 22 => Self::Objective, + 23 => Self::ObjectiveCriteria, + 24 => Self::Operation, + 25 => Self::Particle, + 26 => Self::Angle, + 27 => Self::Rotation, + 28 => Self::ScoreboardSlot, + 29 => Self::ScoreHolder { + allow_multiple: bool::decode(r)?, + }, + 30 => Self::Swizzle, + 31 => Self::Team, + 32 => Self::ItemSlot, + 33 => Self::ResourceLocation, + 34 => Self::Function, + 35 => Self::EntityAnchor, + 36 => Self::IntRange, + 37 => Self::FloatRange, + 38 => Self::Dimension, + 39 => Self::GameMode, + 40 => Self::Time, + 41 => Self::ResourceOrTag { + registry: Ident::decode(r)?, + }, + 42 => Self::ResourceOrTagKey { + registry: Ident::decode(r)?, + }, + 43 => Self::Resource { + registry: Ident::decode(r)?, + }, + 44 => Self::ResourceKey { + registry: Ident::decode(r)?, + }, + 45 => Self::TemplateMirror, + 46 => Self::TemplateRotation, + 47 => Self::Uuid, + n => bail!("unknown command parser ID of {n}"), + }) + } +} + +#[derive(Clone, Debug, Encode, Decode, Packet)] +#[packet(id = packet_id::COMMAND_SUGGESTIONS_S2C)] +pub struct CommandSuggestionsS2c<'a> { + pub id: VarInt, + pub start: VarInt, + pub length: VarInt, + pub matches: Cow<'a, [Suggestion<'a>]>, +} diff --git a/crates/valence_command/src/reader.rs b/crates/valence_command/src/reader.rs new file mode 100644 index 000000000..b0a58f0a3 --- /dev/null +++ b/crates/valence_command/src/reader.rs @@ -0,0 +1,501 @@ +use std::cmp::Ordering; +use std::ops::{Add, AddAssign, Range, Sub, SubAssign}; +use std::str::Chars; +use std::sync::Arc; + +#[derive(Clone, Copy, Debug, PartialEq, Default)] +pub struct StrCursor { + bytes: usize, + chars: usize, +} + +impl AddAssign for StrCursor { + fn add_assign(&mut self, rhs: char) { + self.bytes += rhs.len_utf8(); + self.chars += 1; + } +} + +impl Add for StrCursor { + type Output = Self; + + fn add(mut self, rhs: char) -> Self::Output { + self += rhs; + self + } +} + +impl SubAssign for StrCursor { + fn sub_assign(&mut self, rhs: char) { + self.bytes -= rhs.len_utf8(); + self.chars -= 1; + } +} + +impl Sub for StrCursor { + type Output = Self; + + fn sub(mut self, rhs: char) -> Self::Output { + self -= rhs; + self + } +} + +impl StrCursor { + pub const fn start() -> Self { + Self { bytes: 0, chars: 0 } + } + + pub const fn new(chars: usize, bytes: usize) -> Self { + Self { chars, bytes } + } + + pub fn chars(&self) -> usize { + self.chars + } + + pub fn bytes(&self) -> usize { + self.bytes + } +} + +#[derive(Clone, Copy, Debug, PartialEq, Default)] +pub struct StrSpan { + begin: StrCursor, + end: StrCursor, +} + +impl StrSpan { + /// Valid for any string + pub const ZERO: Self = StrSpan { + begin: StrCursor::new(0, 0), + end: StrCursor::new(0, 0), + }; + + pub const fn new(begin: StrCursor, end: StrCursor) -> Self { + debug_assert!(begin.bytes <= end.bytes); + Self { begin, end } + } + + pub fn join(self, other: Self) -> Self { + Self::new(self.begin, other.end) + } + + pub const fn start() -> Self { + Self { + begin: StrCursor::start(), + end: StrCursor::start(), + } + } + + pub fn begin(&self) -> StrCursor { + self.begin + } + + pub fn end(&self) -> StrCursor { + self.end + } + + pub fn in_str<'a>(&self, str: &'a str) -> Option<&'a str> { + str.get(self.begin.bytes..self.end.bytes) + } + + pub fn is_deeper(&self, o: StrSpan) -> bool { + match self.begin().bytes().cmp(&o.begin().bytes()) { + Ordering::Equal if self.end().bytes() < o.end().bytes() => false, + Ordering::Less => false, + _ => true, + } + } +} + +impl From> for StrSpan { + fn from(value: Range) -> Self { + Self::new(value.start, value.end) + } +} + +#[derive(Clone, Copy, Debug, PartialEq, Default)] +pub struct StrLocated { + pub span: StrSpan, + pub object: T, +} + +impl StrLocated { + pub const fn new(span: StrSpan, object: T) -> Self { + Self { span, object } + } + + pub fn map(self, func: impl FnOnce(T) -> T1) -> StrLocated { + StrLocated { + span: self.span, + object: func(self.object), + } + } +} + +/// [`std::future::Future`] does not support borrowed values, so we need +/// something that has 'static lifetime in order to use borrowed string in an +/// async function +#[derive(Clone, Debug, PartialEq)] +pub struct ArcStrReader { + str: Arc, // TODO: change to weak arc? + cursor: StrCursor, +} + +impl ArcStrReader { + /// # Safety + /// cursor must be a valid cursor for given str + pub unsafe fn new(str: Arc, cursor: StrCursor) -> Self { + Self { str, cursor } + } + + pub fn new_command(str: Arc) -> Self { + let cursor = if str.starts_with('/') { + StrCursor::start() + '/' + } else { + StrCursor::start() + }; + + Self { str, cursor } + } + + pub fn reader(&self) -> StrReader { + StrReader { + str: &self.str, + cursor: self.cursor, + } + } + + pub fn cursor(&self) -> StrCursor { + self.cursor + } + + /// # Safety + /// cursor must be a valid cursor for str + pub unsafe fn set_cursor(&mut self, cursor: StrCursor) { + self.cursor = cursor; + } + + pub fn str(&self) -> Arc { + Arc::clone(&self.str) + } + + pub fn span(&self, span: StrSpan) -> ArcStrReaderSpan { + let str_span = ArcStrReaderSpan { + str: Arc::clone(&self.str), + span, + }; + let _ = str_span.str(); + str_span + } +} + +pub struct ArcStrReaderSpan { + str: Arc, // TODO: change to weak arc? + span: StrSpan, +} + +impl ArcStrReaderSpan { + pub fn str(&self) -> &str { + self.span.in_str(&self.str).unwrap() + } + + pub fn span(&self) -> StrSpan { + self.span + } +} + +#[derive(Clone, Debug, PartialEq)] +pub struct StrReader<'a> { + str: &'a str, + cursor: StrCursor, +} + +#[derive(Clone, Copy, Debug, PartialEq)] +pub enum StrReaderFilter { + /// Reader continues read + Continue, + /// Reader includes char and stops read + IncludeStr, + /// Reader moves cursor but does not include char and stops read + IncludeCursor, + /// Reader does not neither move cursor nor include char in str and then + /// stops read + Exclude, +} + +impl<'a> StrReader<'a> { + pub const fn new(str: &'a str) -> Self { + Self { + str, + cursor: StrCursor { bytes: 0, chars: 0 }, + } + } + + pub fn from_command(command: &'a str) -> Self { + StrReader::new(if command.starts_with('/') { + unsafe { command.get_unchecked('/'.len_utf8()..) } + } else { + command + }) + } + + /// Returns valid cursor + pub const fn cursor(&self) -> StrCursor { + self.cursor + } + + /// # SAFETY + /// Cursor should be valid + pub unsafe fn set_cursor(&mut self, cursor: StrCursor) { + self.cursor = cursor; + } + + pub fn to_end(&mut self) { + self.cursor.chars += self.chars().count(); + self.cursor.bytes = self.str.len(); + } + + /// # Safety + /// Span should be valid + pub unsafe fn get_str(&self, span: impl Into) -> &'a str { + let span = span.into(); + // SAFETY: function accepts only valid spans + unsafe { self.str.get_unchecked(span.begin.bytes..span.end.bytes) } + } + + pub const fn str(&self) -> &'a str { + self.str + } + + pub fn remaining_str(&self) -> &'a str { + // SAFETY: cursor is always valid + unsafe { self.str.get_unchecked(self.cursor.bytes..) } + } + + pub fn used_str(&self) -> &'a str { + // SAFETY: cursor is always valid + unsafe { self.str.get_unchecked(..self.cursor.bytes) } + } + + pub fn chars(&self) -> Chars { + self.remaining_str().chars() + } + + pub fn peek_char(&self) -> Option { + self.chars().next() + } + + pub fn next_char(&mut self) -> Option { + let ch = self.peek_char(); + if let Some(ch) = ch { + self.cursor += ch; + } + ch + } + + pub fn skip_char(&mut self, ch: char) -> bool { + if self.peek_char() == Some(ch) { + self.next_char(); + true + } else { + false + } + } + + pub fn located(&mut self, func: impl FnOnce(&mut Self) -> T) -> StrLocated { + let begin = self.cursor(); + let res = func(self); + StrLocated::new(StrSpan::new(begin, self.cursor()), res) + } + + pub fn err_located( + &mut self, + func: impl FnOnce(&mut Self) -> Result, + ) -> Result> { + let begin = self.cursor(); + let res = func(self); + res.map_err(|e| StrLocated::new(StrSpan::new(begin, self.cursor()), e)) + } + + pub fn span_err_located( + &mut self, + span: &mut StrSpan, + func: impl FnOnce(&mut Self) -> Result, + ) -> Result> { + self.span_located(span, func) + .map_err(|err| StrLocated::new(*span, err)) + } + + pub fn span_located(&mut self, span: &mut StrSpan, func: impl FnOnce(&mut Self) -> T) -> T { + let begin = self.cursor(); + let res = func(self); + *span = StrSpan::new(begin, self.cursor()); + res + } + + /// Skips string using given filter. + /// ### Returns + /// The end of string + pub fn skip_str(&mut self, mut filter: impl FnMut(char) -> StrReaderFilter) -> StrCursor { + loop { + let ch = self.peek_char(); + match ch { + Some(ch) => match filter(ch) { + StrReaderFilter::Continue => { + self.cursor += ch; + } + StrReaderFilter::IncludeStr => { + self.cursor += ch; + break self.cursor; + } + StrReaderFilter::IncludeCursor => { + let end = self.cursor; + self.cursor += ch; + break end; + } + StrReaderFilter::Exclude => break self.cursor, + }, + None => break self.cursor(), + } + } + } + + pub fn read_str(&mut self, filter: impl FnMut(char) -> StrReaderFilter) -> &'a str { + let begin = self.cursor(); + let end = self.skip_str(filter); + // SAFETY: begin and end are valid cursors + unsafe { self.get_str(begin..end) } + } + + pub fn skip_escaped_str( + &mut self, + mut filter: impl FnMut(char) -> StrReaderFilter, + mut chars: impl FnMut(char), + ) -> bool { + let mut next = false; + let mut last = false; + self.skip_str(|ch| match (ch, next) { + ('\\', false) => { + next = true; + StrReaderFilter::Continue + } + (ch, true) => { + chars(ch); + StrReaderFilter::Continue + } + (ch, false) => { + let filter_r = filter(ch); + if let StrReaderFilter::Continue | StrReaderFilter::IncludeStr = filter_r { + chars(ch); + } + if filter_r != StrReaderFilter::Continue { + last = true; + } + filter_r + } + }); + last + } + + pub fn read_escaped_str( + &mut self, + filter: impl FnMut(char) -> StrReaderFilter, + ) -> Option { + let mut result = String::new(); + match self.skip_escaped_str(filter, |ch| result.push(ch)) { + true => Some(result), + false => None, + } + } + + pub fn read_unquoted_str(&mut self) -> &'a str { + self.read_str(|ch| match ch { + '0'..='9' | 'A'..='Z' | 'a'..='z' | '_' | '-' | '.' | '+' => StrReaderFilter::Continue, + _ => StrReaderFilter::Exclude, + }) + } + + pub fn read_delimitted_str(&mut self) -> &'a str { + self.read_str(|ch| match ch { + ' ' => StrReaderFilter::Exclude, + _ => StrReaderFilter::Continue, + }) + } + + pub fn read_resource_location_str(&mut self) -> &'a str { + self.read_str(|ch| match ch { + '0'..='9' | 'a'..='z' | '_' | ':' | '/' | '.' | '-' => StrReaderFilter::Continue, + _ => StrReaderFilter::Exclude, + }) + } + + pub fn read_ident_str(&mut self) -> (Option<&'a str>, &'a str) { + let mut left = false; + let result = self.read_str(|ch| match ch { + '0'..='9' | 'A'..='Z' | 'a'..='z' | '_' | '-' | '.' | '+' => StrReaderFilter::Continue, + ':' => { + left = true; + StrReaderFilter::IncludeCursor + } + _ => StrReaderFilter::Exclude, + }); + + if left { + (Some(result), self.read_unquoted_str()) + } else { + (None, result) + } + } + + pub fn read_started_quoted_str(&mut self) -> Option { + self.read_escaped_str(|ch| match ch { + '"' | '\'' => StrReaderFilter::IncludeCursor, + _ => StrReaderFilter::Continue, + }) + } + + pub fn skip_started_quoted_str(&mut self) -> bool { + self.skip_escaped_str( + |ch| match ch { + '"' | '\'' => StrReaderFilter::IncludeCursor, + _ => StrReaderFilter::Continue, + }, + |_| {}, + ) + } + + pub fn read_num_str(&mut self) -> &'a str { + self.read_str(|ch| match ch { + '0'..='9' | '+' | '-' | 'e' | '.' => StrReaderFilter::Continue, + _ => StrReaderFilter::Exclude, + }) + } + + pub fn is_ended(&self) -> bool { + self.str.len() == self.cursor().bytes + } + + pub fn skip_char_or_end(&mut self, ch: char) -> bool { + self.skip_char(ch) || self.is_ended() + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + pub fn reader_test() { + assert_eq!(StrReader::new("hello n").read_unquoted_str(), "hello"); + assert_eq!( + StrReader::new("minecraft:stick n").read_ident_str(), + (Some("minecraft"), "stick") + ); + assert_eq!( + StrReader::new(r#"hello" n"#).read_started_quoted_str(), + Some("hello".to_string()) + ); + } +} diff --git a/crates/valence_command/src/suggestions.rs b/crates/valence_command/src/suggestions.rs new file mode 100644 index 000000000..abb5bcbb5 --- /dev/null +++ b/crates/valence_command/src/suggestions.rs @@ -0,0 +1,295 @@ +use std::borrow::Cow; +use std::sync::{Arc, Weak}; + +use bevy_ecs::prelude::{DetectChangesMut, Entity, Event, EventReader, EventWriter}; +use bevy_ecs::query::{QueryState, With}; +use bevy_ecs::system::{Commands, Local, ParamSet, Query, Res, ResMut, Resource}; +use bevy_ecs::world::World; +use parking_lot::Mutex; +use tokio::runtime::{Handle, Runtime}; +use valence_client::event_loop::PacketEvent; +use valence_client::Client; +use valence_core::__private::VarInt; +use valence_core::protocol::encode::WritePacket; +use valence_core::protocol::packet::chat::RequestCommandCompletionsC2s; +use valence_core::protocol::{Decode, Encode}; +use valence_core::text::Text; + +use crate::command::CommandExecutorBase; +use crate::compile::CommandCompilerPurpose; +use crate::nodes::{EntityNodeQuery, NodeFlow, NodeGraphInWorld, NodeId, NodeKind, RootNodeId}; +use crate::parse::ParseObject; +use crate::pkt; +use crate::reader::{ArcStrReader, StrLocated, StrSpan}; + +#[derive(Encode, Decode, Clone, Debug)] +pub struct Suggestion<'a> { + pub message: Cow<'a, str>, + pub tooltip: Option, +} + +impl<'a> Suggestion<'a> { + pub const fn new_str(str: &'a str) -> Self { + Self { + message: Cow::Borrowed(str), + tooltip: None, + } + } +} + +pub const CONSOLE_EVENT_ID: u32 = 0; + +#[derive(Event, Debug)] +pub struct SuggestionsAnswerEvent { + pub suggestions: StrLocated]>>, + pub id: u32, +} + +#[derive(Clone, Copy, Debug, PartialEq)] +pub enum SuggestionsTransaction { + Client { client: Entity, id: i32 }, + Event { id: u32 }, +} + +#[derive(Event, Clone, Debug, PartialEq)] +pub struct SuggestionsRequestEvent { + pub transaction: SuggestionsTransaction, + pub executor: CommandExecutorBase, + pub text: String, +} + +impl SuggestionsRequestEvent { + pub fn is_command(&self) -> bool { + self.text.starts_with('/') + } +} + +pub fn suggestions_request_packet( + mut event: EventReader, + mut request: EventWriter, +) { + for packet_event in event.iter() { + if let Some(packet) = packet_event.decode::() { + request.send(SuggestionsRequestEvent { + transaction: SuggestionsTransaction::Client { + client: packet_event.client, + id: packet.transaction_id.0, + }, + executor: CommandExecutorBase::Entity { + entity: packet_event.client, + }, + text: packet.text.to_string(), + }) + } + } +} + +#[derive(Resource, Default)] +pub struct SuggestionsQueue(pub(crate) Arc>>); + +impl SuggestionsQueue { + pub fn get(&self) -> Weak>> { + Arc::downgrade(&self.0) + } +} + +#[derive(Debug)] +pub struct SuggestionsCalculated { + pub transaction: SuggestionsTransaction, + pub suggestions: StrLocated]>>, + pub command: Arc, +} + +#[derive(Resource)] +pub struct SuggestionsTokioRuntime { + handle: Handle, + _runtime: Option, +} + +impl Default for SuggestionsTokioRuntime { + fn default() -> Self { + let runtime = Runtime::new().unwrap(); + Self { + handle: runtime.handle().clone(), + _runtime: Some(runtime), + } + } +} + +impl SuggestionsTokioRuntime { + pub fn with_handle(handle: Handle) -> Self { + Self { + handle, + _runtime: None, + } + } +} + +pub fn suggestions_spawn_tasks( + mut event: EventReader, + entity_node: Query, + mut set: ParamSet<(&World, ResMut)>, + queue: Res, + tokio_runtime: Res, + mut commands: Commands, +) { + let mut graph = set.p1().take(); + + for event in event.iter() { + if !event.is_command() { + continue; + } + + let command = Arc::from(event.text.clone()); + + let mut arc_reader = ArcStrReader::new_command(command); + + let root = graph + .get_root_node( + event + .executor + .node_entity() + .and_then(|e| entity_node.get(e).ok().map(|v| v.get())) + .unwrap_or(RootNodeId::SUPER), + ) + .unwrap(); + + let mut reader = arc_reader.reader(); + + let mut node = NodeId::ROOT; + + let _ = graph.walk_node( + node, + root, + &mut reader, + &mut CommandCompilerPurpose::Suggestions { node: &mut node }, + ); + + let cursor = reader.cursor(); + + // SAFETY: cursor from a reader of this Arc + unsafe { arc_reader.set_cursor(cursor) }; + + // SAFETY: no other references + if let Some(flow) = unsafe { graph.get_mut_children_flow_unsafe(node) } { + let begin = arc_reader.cursor(); + + let mut reader = arc_reader.reader(); + let literal = reader.read_unquoted_str(); + let mut current_span = StrSpan::new(begin, reader.cursor()); + let mut current_suggestions: Vec<_> = flow + .literals + .keys() + .filter(|v| v.starts_with(literal)) + .map(|v| Suggestion { + message: Cow::Owned(v.clone()), + tooltip: None, + }) + .collect(); + + let mut tasks = vec![]; + + for parser in flow.parsers.iter().cloned() { + let mut reader = arc_reader.reader(); + + // SAFETY: NodeChildrenFlow doesn't have a reference to the self node + let node = unsafe { graph.get_mut_node_unsafe(parser).unwrap() }; + let parser = match node.kind { + NodeKind::Argument { ref mut parse, .. } => parse.as_mut(), + NodeKind::Literal { .. } => unreachable!(), + }; + + let (_, suggestions) = parser.obj_skip(&mut reader); + tasks.push(parser.obj_suggestions( + suggestions, + arc_reader.clone(), + event.executor, + set.p0(), + )); + } + + let queue = queue.get(); + + let transaction = event.transaction; + + let _guard = tokio_runtime.handle.enter(); + + // TODO: maybe if all future are already done we must not execute async task + tokio::spawn(async move { + for task in tasks { + let suggestions = task.await; + if suggestions.span == current_span { + match suggestions.object { + Cow::Owned(vec) => current_suggestions.extend(vec), + Cow::Borrowed(slice) => current_suggestions.extend_from_slice(slice), + } + } else if suggestions.span.is_deeper(current_span) { + current_suggestions = suggestions.object.into_owned(); + current_span = suggestions.span; + } + } + + if let Some(queue) = queue.upgrade() { + queue.lock().push(SuggestionsCalculated { + transaction, + suggestions: StrLocated::new(current_span, Cow::Owned(current_suggestions)), + command: arc_reader.str(), + }); + } + }); + } + } + + set.p1().insert(graph); + + // parsers_apply_deferred should be executed in apply_deferred + commands.add(|world: &mut World| parsers_apply_deferred(world)); +} + +/// Applying deferred for each NodeParser, like [`bevy_ecs::system::Commands`] +pub fn parsers_apply_deferred(world: &mut World) { + let mut graph = world.resource_mut::().take(); + + for node in graph.shared.get_mut().nodes_mut().iter_mut() { + if let NodeKind::Argument { ref mut parse, .. } = node.kind { + parse.obj_apply_deferred(world); + } + } + + world.resource_mut::().insert(graph); +} + +pub fn send_calculated_suggestions( + queue: Res, + mut client_query: Query<&mut Client>, + mut event: EventWriter, + mut suggestions_buf: Local>, +) { + { + let mut queue = queue.0.lock(); + std::mem::swap::>(queue.as_mut(), suggestions_buf.as_mut()); + } + + for suggestions in suggestions_buf.drain(..) { + match suggestions.transaction { + SuggestionsTransaction::Client { client, id } => { + if let Ok(mut client) = client_query.get_mut(client) { + client.write_packet(&pkt::CommandSuggestionsS2c { + id: VarInt(id), + start: VarInt((suggestions.suggestions.span.begin().chars()) as i32), + length: VarInt( + (suggestions.suggestions.span.end().chars() + - suggestions.suggestions.span.begin().chars()) + as i32, + ), + matches: Cow::Borrowed(&suggestions.suggestions.object), + }) + } + } + SuggestionsTransaction::Event { id } => event.send(SuggestionsAnswerEvent { + suggestions: suggestions.suggestions, + id, + }), + } + } +} diff --git a/crates/valence_command_macro/Cargo.toml b/crates/valence_command_macro/Cargo.toml new file mode 100644 index 000000000..3948750d3 --- /dev/null +++ b/crates/valence_command_macro/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "valence_command_macro" +version.workspace = true +edition.workspace = true + +[lib] +proc-macro = true + +[dependencies] +proc-macro2.workspace = true +quote.workspace = true +syn.workspace = true diff --git a/crates/valence_command_macro/src/lib.rs b/crates/valence_command_macro/src/lib.rs new file mode 100644 index 000000000..1140efbda --- /dev/null +++ b/crates/valence_command_macro/src/lib.rs @@ -0,0 +1,7 @@ +#[proc_macro_attribute] +pub fn command_macro( + attr: proc_macro::TokenStream, + item: proc_macro::TokenStream, +) -> proc_macro::TokenStream { + todo!() +} diff --git a/crates/valence_core/src/game_mode.rs b/crates/valence_core/src/game_mode.rs index c23c2a6a8..7ebfc6ca2 100644 --- a/crates/valence_core/src/game_mode.rs +++ b/crates/valence_core/src/game_mode.rs @@ -10,3 +10,20 @@ pub enum GameMode { Adventure, Spectator, } + +impl GameMode { + /// Converts gamemode to its number representation + /// ### Values + /// 0. Survival + /// 1. Creative + /// 2. Adventure + /// 3. Spectator + pub fn to_index(self) -> usize { + match self { + Self::Survival => 0, + Self::Creative => 1, + Self::Adventure => 2, + Self::Spectator => 3, + } + } +} diff --git a/crates/valence_core/src/protocol/packet.rs b/crates/valence_core/src/protocol/packet.rs index d11b6ded1..059222e23 100644 --- a/crates/valence_core/src/protocol/packet.rs +++ b/crates/valence_core/src/protocol/packet.rs @@ -242,7 +242,7 @@ pub mod chat { pub id: VarInt, pub start: VarInt, pub length: VarInt, - pub matches: Vec>, + pub matches: Cow<'a, [CommandSuggestionsMatch<'a>]>, } #[derive(Clone, PartialEq, Debug, Encode, Decode)] @@ -763,7 +763,6 @@ pub mod sound { } } -// TODO: move to valence_command pub mod command { use super::*; diff --git a/crates/valence_core_macros/Cargo.toml b/crates/valence_core_macros/Cargo.toml index 4e7ef88aa..3c417283f 100644 --- a/crates/valence_core_macros/Cargo.toml +++ b/crates/valence_core_macros/Cargo.toml @@ -9,4 +9,4 @@ proc-macro = true [dependencies] proc-macro2.workspace = true quote.workspace = true -syn = { workspace = true, features = ["full"] } +syn.workspace = true diff --git a/crates/valence_nbt/src/compound.rs b/crates/valence_nbt/src/compound.rs index 2d2649dd9..93acb1b86 100644 --- a/crates/valence_nbt/src/compound.rs +++ b/crates/valence_nbt/src/compound.rs @@ -178,6 +178,41 @@ impl Compound { self.map.retain(f) } + /// Checks if merging self compound with other compound will give any + /// results. + /// + /// ```rust + /// use valence_nbt::compound; + /// + /// let a = compound! { + /// "a" => 0, + /// "b" => "str", + /// }; + /// + /// assert!(a.contains_compound(&compound! { + /// "a" => 0, + /// })); + /// + /// assert!(!a.contains_compound(&compound! { + /// "a" => 1, + /// })); + /// ``` + pub fn contains_compound(&self, other: &Compound) -> bool { + for (name, value) in self.iter() { + if let Some(other_value) = other.get(name) { + if !match (value, other_value) { + (Value::Compound(this), Value::Compound(other)) => { + this.contains_compound(other) + } + (v, o) => v == o, + } { + return false; + } + } + } + true + } + /// Inserts all items from `other` into `self` recursively. /// /// # Example diff --git a/examples/command.rs b/examples/command.rs new file mode 100644 index 000000000..e1a8459fb --- /dev/null +++ b/examples/command.rs @@ -0,0 +1,121 @@ +#![allow(clippy::type_complexity)] + +use std::borrow::Cow; + +use command::builder::{NodeCommands, NodeGraphCommands}; +use command::command::{CommandExecutorBase, CommandExecutorBridge}; +use command::nodes::NodeGraphInWorld; +use valence::prelude::*; +use valence_command::command::{CommandArguments, RealCommandExecutor}; + +const SPAWN_Y: i32 = 64; + +pub fn main() { + App::new() + .insert_resource(NetworkSettings { + connection_mode: ConnectionMode::Offline, + ..Default::default() + }) + .add_plugins(DefaultPlugins) + .add_systems(Startup, setup) + .add_systems(Update, (init_clients, despawn_disconnected_clients)) + .run(); +} + +macro_rules! gamemode_node { + ($gamemode:expr) => { + |node| { + node.execute( + |In(command_arguments): In, + mut gamemode_query: Query<&mut GameMode>| { + if let RealCommandExecutor::Player(entity) = command_arguments.1 { + if let Ok(mut gamemode_player) = gamemode_query.get_mut(entity) { + gamemode_player.set_if_neq($gamemode); + } + } + }, + ); + } + }; +} + +fn setup( + mut commands: Commands, + server: Res, + dimensions: Res, + biomes: Res, + graph: ResMut, +) { + let mut instance = Instance::new(ident!("overworld"), &dimensions, &biomes, &server); + + for z in -5..5 { + for x in -5..5 { + instance.insert_chunk([x, z], UnloadedChunk::new()); + } + } + + for z in -25..25 { + for x in -25..25 { + instance.set_block([x, SPAWN_Y, z], BlockState::GRASS_BLOCK); + } + } + + commands.spawn(instance); + + let mut commands = NodeGraphCommands { commands, graph }; + + commands + .spawn_literal_node("gamemode".into()) + .with_literal_child("survival".into(), gamemode_node!(GameMode::Survival)) + .with_literal_child("creative".into(), gamemode_node!(GameMode::Creative)) + .with_literal_child("spectator".into(), gamemode_node!(GameMode::Spectator)) + .with_literal_child("adventure".into(), gamemode_node!(GameMode::Adventure)) + .root_node_child(); + + fn teleport_execute( + In(mut arguments): In, + mut query: Query<&mut Position>, + mut cebridge: CommandExecutorBridge, + ) { + if let RealCommandExecutor::Player(client) = arguments.1 { + let x = arguments.0.read::(); + let y = arguments.0.read::(); + let z = arguments.0.read::(); + if let Ok(mut position) = query.get_mut(client) { + position.0 = DVec3::new(*x as _, *y as _, *z as _); + cebridge.send_message( + arguments.1, + Text::text(format!("We teleported you to ({x} {y} {z})")), + ); + } + } + } + + let teleport_id = commands + .spawn_literal_node("teleport".into()) + .with_argument_child::("x".into(), Default::default(), |child| { + child.with_argument_child::("y".into(), Default::default(), |child| { + child.with_argument_child::("z".into(), Default::default(), |child| { + child.execute(teleport_execute); + }); + }); + }) + .root_node_child() + .id; + + commands + .spawn_literal_node("tp".into()) + .set_redirect(teleport_id) + .root_node_child(); +} + +fn init_clients( + mut clients: Query<(&mut Location, &mut Position, &mut GameMode), Added>, + instances: Query>, +) { + for (mut loc, mut pos, mut game_mode) in &mut clients { + loc.0 = instances.single(); + pos.set([0.5, SPAWN_Y as f64 + 1.0, 0.5]); + *game_mode = GameMode::Creative; + } +} diff --git a/src/lib.rs b/src/lib.rs index 87f810878..a37ef54e8 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -123,6 +123,9 @@ pub mod prelude { #[cfg(feature = "player_list")] pub use valence_player_list::{PlayerList, PlayerListEntry}; + #[cfg(feature = "command")] + pub use valence_command as command; + pub use super::DefaultPlugins; } @@ -187,6 +190,11 @@ impl PluginGroup for DefaultPlugins { group = group.add(valence_boss_bar::BossBarPlugin); } + #[cfg(feature = "command")] + { + group = group.add(valence_command::CommandPlugin); + } + group } }