From 307e47867122d8ff18b8cd4b6b9fa45e6eb1369a Mon Sep 17 00:00:00 2001 From: CapsizeGlimmer <> Date: Tue, 5 May 2020 00:06:36 -0400 Subject: [PATCH 1/9] Groundwork for tab completion of player names and command arguments --- common/src/assets/mod.rs | 7 ++ common/src/cmd.rs | 156 +++++++++++++++++++++++++++++++++++++++ common/src/lib.rs | 1 + 3 files changed, 164 insertions(+) create mode 100644 common/src/cmd.rs diff --git a/common/src/assets/mod.rs b/common/src/assets/mod.rs index b7a3e63627..a14ca93cf2 100644 --- a/common/src/assets/mod.rs +++ b/common/src/assets/mod.rs @@ -61,6 +61,13 @@ lazy_static! { RwLock::new(HashMap::new()); } +const ASSETS_TMP: [&'static str; 1] = ["common/items/lantern/black_0"]; +pub fn iterate() -> impl Iterator<Item = &'static str> { + // TODO FIXME implement this + //ASSETS.read().iter().flat_map(|e| e.keys()) + ASSETS_TMP.iter().map(|k| *k) +} + // TODO: Remove this function. It's only used in world/ in a really ugly way.To // do this properly assets should have all their necessary data in one file. A // ron file could be used to combine voxel data with positioning data for diff --git a/common/src/cmd.rs b/common/src/cmd.rs new file mode 100644 index 0000000000..2ab72f83a9 --- /dev/null +++ b/common/src/cmd.rs @@ -0,0 +1,156 @@ +use crate::{assets, comp::Player, state::State}; +use lazy_static::lazy_static; +use specs::prelude::{Join, WorldExt}; + + +/// Struct representing a command that a user can run from server chat. +pub struct ChatCommand { + /// The keyword used to invoke the command, omitting the leading '/'. + pub keyword: &'static str, + /// A format string for parsing arguments. + pub args: Vec<ArgumentSyntax>, + /// A one-line message that explains what the command does + pub description: &'static str, + /// A boolean that is used to check whether the command requires + /// administrator permissions or not. + pub needs_admin: bool, +} + +impl ChatCommand { + pub fn new( + keyword: &'static str, + args: Vec<ArgumentSyntax>, + description: &'static str, + needs_admin: bool, + ) -> Self { + Self { + keyword, + args, + description, + needs_admin, + } + } +} + +lazy_static! { + static ref CHAT_COMMANDS: Vec<ChatCommand> = { + use ArgumentSyntax::*; + vec![ + ChatCommand::new("help", vec![Command(true)], "Display information about commands", false), + ] + }; +} + +/// Representation for chat command arguments +pub enum ArgumentSyntax { + /// The argument refers to a player by alias + PlayerName(bool), + /// The argument refers to an item asset by path + ItemSpec(bool), + /// The argument is a float. The associated values are + /// * label + /// * default tab-completion + /// * whether it's optional + Float(&'static str, f32, bool), + /// The argument is a float. The associated values are + /// * label + /// * default tab-completion + /// * whether it's optional + Integer(&'static str, f32, bool), + /// The argument is a command name + Command(bool), + /// This is the final argument, consuming all characters until the end of input. + Message, + /// The argument is likely an enum. The associated values are + /// * label + /// * Predefined string completions + /// * Other completion types + /// * whether it's optional + OneOf(&'static str, &'static [&'static str], Vec<Box<ArgumentSyntax>>, bool), +} + +impl ArgumentSyntax { + pub fn help_string(arg: &ArgumentSyntax) -> String { + match arg { + ArgumentSyntax::PlayerName(optional) => { + if *optional { + "[player]".to_string() + } else { + "<player>".to_string() + } + }, + ArgumentSyntax::ItemSpec(optional) => { + if *optional { + "[item]".to_string() + } else { + "<item>".to_string() + } + }, + ArgumentSyntax::Float(label, _, optional) => { + if *optional { + format!("[{}]", label) + } else { + format!("<{}>", label) + } + }, + ArgumentSyntax::Integer(label, _, optional) => { + if *optional { + format!("[{}]", label) + } else { + format!("<{}>", label) + } + }, + ArgumentSyntax::Command(optional) => { + if *optional { + "[[/]command]".to_string() + } else { + "<[/]command>".to_string() + } + }, + ArgumentSyntax::Message => { + "<message>".to_string() + }, + ArgumentSyntax::OneOf(label, _, _, optional) => { + if *optional { + format! {"[{}]", label} + } else { + format! {"<{}>", label} + } + }, + } + } + + pub fn complete(&self, state: &State, part: &String) -> Vec<String> { + match self { + ArgumentSyntax::PlayerName(_) => (&state.ecs().read_storage::<Player>()) + .join() + .filter(|player| player.alias.starts_with(part)) + .map(|player| player.alias.clone()) + .collect(), + ArgumentSyntax::ItemSpec(_) => assets::iterate() + .filter(|asset| asset.starts_with(part)) + .map(|c| c.to_string()) + .collect(), + ArgumentSyntax::Float(_, x, _) => vec![format!("{}", x)], + ArgumentSyntax::Integer(_, x, _) => vec![format!("{}", x)], + ArgumentSyntax::Command(_) => CHAT_COMMANDS + .iter() + .map(|com| com.keyword.clone()) + .filter(|kwd| kwd.starts_with(part) || format!("/{}", kwd).starts_with(part)) + .map(|c| c.to_string()) + .collect(), + ArgumentSyntax::Message => vec![], + ArgumentSyntax::OneOf(_, strings, alts, _) => { + let string_completions = strings + .iter() + .filter(|string| string.starts_with(part)) + .map(|c| c.to_string()); + let alt_completions = alts + .iter() + .flat_map(|b| (*b).complete(&state, part)) + .map(|c| c.to_string()); + string_completions.chain(alt_completions).collect() + } + } + } +} diff --git a/common/src/lib.rs b/common/src/lib.rs index 1ea3df4c51..12600ed3d4 100644 --- a/common/src/lib.rs +++ b/common/src/lib.rs @@ -16,6 +16,7 @@ pub mod assets; pub mod astar; pub mod character; pub mod clock; +pub mod cmd; pub mod comp; pub mod effect; pub mod event; From 7ecea34f8525d85c72f5616f3d30a43da29ca669 Mon Sep 17 00:00:00 2001 From: CapsizeGlimmer <> Date: Tue, 5 May 2020 18:33:16 -0400 Subject: [PATCH 2/9] Server server::cmd depends on common::cmd --- common/src/cmd.rs | 346 ++++++++++++++++++++++++++++++++----- server/src/cmd.rs | 422 ++++++++++++++-------------------------------- server/src/lib.rs | 25 ++- 3 files changed, 440 insertions(+), 353 deletions(-) diff --git a/common/src/cmd.rs b/common/src/cmd.rs index 2ab72f83a9..bb134a5741 100644 --- a/common/src/cmd.rs +++ b/common/src/cmd.rs @@ -1,14 +1,10 @@ use crate::{assets, comp::Player, state::State}; -use lazy_static::lazy_static; use specs::prelude::{Join, WorldExt}; - /// Struct representing a command that a user can run from server chat. -pub struct ChatCommand { - /// The keyword used to invoke the command, omitting the leading '/'. - pub keyword: &'static str, +pub struct ChatCommandData { /// A format string for parsing arguments. - pub args: Vec<ArgumentSyntax>, + pub args: Vec<ArgumentSpec>, /// A one-line message that explains what the command does pub description: &'static str, /// A boolean that is used to check whether the command requires @@ -16,15 +12,9 @@ pub struct ChatCommand { pub needs_admin: bool, } -impl ChatCommand { - pub fn new( - keyword: &'static str, - args: Vec<ArgumentSyntax>, - description: &'static str, - needs_admin: bool, - ) -> Self { +impl ChatCommandData { + pub fn new(args: Vec<ArgumentSpec>, description: &'static str, needs_admin: bool) -> Self { Self { - keyword, args, description, needs_admin, @@ -32,17 +22,267 @@ impl ChatCommand { } } -lazy_static! { - static ref CHAT_COMMANDS: Vec<ChatCommand> = { - use ArgumentSyntax::*; - vec![ - ChatCommand::new("help", vec![Command(true)], "Display information about commands", false), - ] - }; +// Please keep this sorted alphabetically :-) +#[derive(Copy, Clone)] +pub enum ChatCommand { + Adminify, + Alias, + Build, + Debug, + DebugColumn, + Explosion, + GiveExp, + GiveItem, + Goto, + Health, + Help, + Jump, + Kill, + KillNpcs, + Lantern, + Light, + Object, + Players, + RemoveLights, + SetLevel, + Spawn, + Sudo, + Tell, + Time, + Tp, + Version, + Waypoint, +} + +// Thank you for keeping this sorted alphabetically :-) +pub static CHAT_COMMANDS: &'static [ChatCommand] = &[ + ChatCommand::Adminify, + ChatCommand::Alias, + ChatCommand::Build, + ChatCommand::Debug, + ChatCommand::DebugColumn, + ChatCommand::Explosion, + ChatCommand::GiveExp, + ChatCommand::GiveItem, + ChatCommand::Goto, + ChatCommand::Health, + ChatCommand::Help, + ChatCommand::Jump, + ChatCommand::Kill, + ChatCommand::KillNpcs, + ChatCommand::Lantern, + ChatCommand::Light, + ChatCommand::Object, + ChatCommand::Players, + ChatCommand::RemoveLights, + ChatCommand::SetLevel, + ChatCommand::Spawn, + ChatCommand::Sudo, + ChatCommand::Tell, + ChatCommand::Time, + ChatCommand::Tp, + ChatCommand::Version, + ChatCommand::Waypoint, +]; + +impl ChatCommand { + pub fn data(&self) -> ChatCommandData { + use ArgumentSpec::*; + let cmd = ChatCommandData::new; + match self { + ChatCommand::Adminify => cmd( + vec![PlayerName(false)], + "Temporarily gives a player admin permissions or removes them", + true, + ), + ChatCommand::Alias => cmd(vec![Any("name", false)], "Change your alias", false), + ChatCommand::Build => cmd(vec![], "Toggles build mode on and off", true), + ChatCommand::Debug => cmd(vec![], "Place all debug items into your pack.", true), + ChatCommand::DebugColumn => cmd( + vec![Float("x", f32::NAN, false), Float("y", f32::NAN, false)], + "Prints some debug information about a column", + false, + ), + ChatCommand::Explosion => cmd( + vec![Float("radius", 5.0, false)], + "Explodes the ground around you", + true, + ), + ChatCommand::GiveExp => cmd( + vec![Integer("amount", 50, false)], + "Give experience to yourself", + true, + ), + ChatCommand::GiveItem => cmd( + vec![ItemSpec(false), Integer("num", 1, true)], + "Give yourself some items", + true, + ), + ChatCommand::Goto => cmd( + vec![ + Float("x", 0.0, false), + Float("y", 0.0, false), + Float("z", 0.0, false), + ], + "Teleport to a position", + true, + ), + ChatCommand::Health => cmd( + vec![Integer("hp", 100, false)], + "Set your current health", + true, + ), + ChatCommand::Help => ChatCommandData::new( + vec![Command(true)], + "Display information about commands", + false, + ), + ChatCommand::Jump => cmd( + vec![ + Float("x", 0.0, false), + Float("y", 0.0, false), + Float("z", 0.0, false), + ], + "Offset your current position", + true, + ), + ChatCommand::Kill => cmd(vec![], "Kill yourself", false), + ChatCommand::KillNpcs => cmd(vec![], "Kill the NPCs", true), + ChatCommand::Lantern => cmd( + vec![ + Float("strength", 5.0, false), + Float("r", 1.0, true), + Float("g", 1.0, true), + Float("b", 1.0, true), + ], + "Change your lantern's strength and color", + true, + ), + ChatCommand::Light => cmd( + vec![ + Float("r", 1.0, true), + Float("g", 1.0, true), + Float("b", 1.0, true), + Float("x", 0.0, true), + Float("y", 0.0, true), + Float("z", 0.0, true), + Float("strength", 5.0, true), + ], + "Spawn entity with light", + true, + ), + ChatCommand::Object => cmd(vec![/*TODO*/], "Spawn an object", true), + ChatCommand::Players => cmd(vec![], "Lists players currently online", false), + ChatCommand::RemoveLights => cmd( + vec![Float("radius", 20.0, true)], + "Removes all lights spawned by players", + true, + ), + ChatCommand::SetLevel => { + cmd(vec![Integer("level", 10, false)], "Set player Level", true) + }, + ChatCommand::Spawn => cmd(vec![/*TODO*/], "Spawn a test entity", true), + ChatCommand::Sudo => cmd( + vec![PlayerName(false), Command(false), Message /* TODO */], + "Run command as if you were another player", + true, + ), + ChatCommand::Tell => cmd( + vec![PlayerName(false), Message], + "Send a message to another player", + false, + ), + ChatCommand::Time => cmd(vec![/*TODO*/], "Set the time of day", true), + ChatCommand::Tp => cmd(vec![PlayerName(true)], "Teleport to another player", true), + ChatCommand::Version => cmd(vec![], "Prints server version", false), + ChatCommand::Waypoint => { + cmd(vec![], "Set your waypoint to your current position", true) + }, + } + } + + pub fn keyword(&self) -> &'static str { + match self { + ChatCommand::Adminify => "adminify", + ChatCommand::Alias => "alias", + ChatCommand::Build => "build", + ChatCommand::Debug => "debug", + ChatCommand::DebugColumn => "debug_column", + ChatCommand::Explosion => "explosion", + ChatCommand::GiveExp => "give_exp", + ChatCommand::GiveItem => "give_item", + ChatCommand::Goto => "goto", + ChatCommand::Health => "health", + ChatCommand::Help => "help", + ChatCommand::Jump => "jump", + ChatCommand::Kill => "kill", + ChatCommand::KillNpcs => "kill_npcs", + ChatCommand::Lantern => "lantern", + ChatCommand::Light => "light", + ChatCommand::Object => "object", + ChatCommand::Players => "players", + ChatCommand::RemoveLights => "remove_lights", + ChatCommand::SetLevel => "set_level", + ChatCommand::Spawn => "spawn", + ChatCommand::Sudo => "sudo", + ChatCommand::Tell => "tell", + ChatCommand::Time => "time", + ChatCommand::Tp => "tp", + ChatCommand::Version => "version", + ChatCommand::Waypoint => "waypoint", + } + } + + pub fn help_string(&self) -> String { + let data = self.data(); + let usage = std::iter::once(format!("/{}", self.keyword())) + .chain(data.args.iter().map(|arg| arg.usage_string())) + .collect::<Vec<_>>() + .join(" "); + format!("{}: {}", usage, data.description) + } + + pub fn needs_admin(&self) -> bool { self.data().needs_admin } + + pub fn arg_fmt(&self) -> String { + self.data() + .args + .iter() + .map(|arg| match arg { + ArgumentSpec::PlayerName(_) => "{}", + ArgumentSpec::ItemSpec(_) => "{}", + ArgumentSpec::Float(_, _, _) => "{f}", + ArgumentSpec::Integer(_, _, _) => "{d}", + ArgumentSpec::Any(_, _) => "{}", + ArgumentSpec::Command(_) => "{}", + ArgumentSpec::Message => "{/.*/}", + ArgumentSpec::OneOf(_, _, _, _) => "{}", // TODO + }) + .collect::<Vec<_>>() + .join(" ") + } +} + +impl std::str::FromStr for ChatCommand { + type Err = (); + + fn from_str(keyword: &str) -> Result<ChatCommand, ()> { + let kwd = if keyword.chars().next() == Some('/') { + &keyword[1..] + } else { + &keyword[..] + }; + for c in CHAT_COMMANDS { + if kwd == c.keyword() { + return Ok(*c); + } + } + return Err(()); + } } /// Representation for chat command arguments -pub enum ArgumentSyntax { +pub enum ArgumentSpec { /// The argument refers to a player by alias PlayerName(bool), /// The argument refers to an item asset by path @@ -56,61 +296,74 @@ pub enum ArgumentSyntax { /// * label /// * default tab-completion /// * whether it's optional - Integer(&'static str, f32, bool), + Integer(&'static str, i32, bool), + /// The argument is any string that doesn't contain spaces + Any(&'static str, bool), /// The argument is a command name Command(bool), - /// This is the final argument, consuming all characters until the end of input. + /// This is the final argument, consuming all characters until the end of + /// input. Message, /// The argument is likely an enum. The associated values are /// * label /// * Predefined string completions /// * Other completion types /// * whether it's optional - OneOf(&'static str, &'static [&'static str], Vec<Box<ArgumentSyntax>>, bool), + OneOf( + &'static str, + &'static [&'static str], + Vec<Box<ArgumentSpec>>, + bool, + ), } -impl ArgumentSyntax { - pub fn help_string(arg: &ArgumentSyntax) -> String { - match arg { - ArgumentSyntax::PlayerName(optional) => { +impl ArgumentSpec { + pub fn usage_string(&self) -> String { + match self { + ArgumentSpec::PlayerName(optional) => { if *optional { "[player]".to_string() } else { "<player>".to_string() } }, - ArgumentSyntax::ItemSpec(optional) => { + ArgumentSpec::ItemSpec(optional) => { if *optional { "[item]".to_string() } else { "<item>".to_string() } }, - ArgumentSyntax::Float(label, _, optional) => { + ArgumentSpec::Float(label, _, optional) => { if *optional { format!("[{}]", label) } else { format!("<{}>", label) } }, - ArgumentSyntax::Integer(label, _, optional) => { + ArgumentSpec::Integer(label, _, optional) => { if *optional { format!("[{}]", label) } else { format!("<{}>", label) } }, - ArgumentSyntax::Command(optional) => { + ArgumentSpec::Any(label, optional) => { + if *optional { + format!("[{}]", label) + } else { + format!("<{}>", label) + } + }, + ArgumentSpec::Command(optional) => { if *optional { "[[/]command]".to_string() } else { "<[/]command>".to_string() } }, - ArgumentSyntax::Message => { - "<message>".to_string() - }, - ArgumentSyntax::OneOf(label, _, _, optional) => { + ArgumentSpec::Message => "<message>".to_string(), + ArgumentSpec::OneOf(label, _, _, optional) => { if *optional { format! {"[{}]", label} } else { @@ -122,25 +375,26 @@ impl ArgumentSyntax { pub fn complete(&self, state: &State, part: &String) -> Vec<String> { match self { - ArgumentSyntax::PlayerName(_) => (&state.ecs().read_storage::<Player>()) + ArgumentSpec::PlayerName(_) => (&state.ecs().read_storage::<Player>()) .join() .filter(|player| player.alias.starts_with(part)) .map(|player| player.alias.clone()) .collect(), - ArgumentSyntax::ItemSpec(_) => assets::iterate() + ArgumentSpec::ItemSpec(_) => assets::iterate() .filter(|asset| asset.starts_with(part)) .map(|c| c.to_string()) .collect(), - ArgumentSyntax::Float(_, x, _) => vec![format!("{}", x)], - ArgumentSyntax::Integer(_, x, _) => vec![format!("{}", x)], - ArgumentSyntax::Command(_) => CHAT_COMMANDS + ArgumentSpec::Float(_, x, _) => vec![format!("{}", x)], + ArgumentSpec::Integer(_, x, _) => vec![format!("{}", x)], + ArgumentSpec::Any(_, _) => vec![], + ArgumentSpec::Command(_) => CHAT_COMMANDS .iter() - .map(|com| com.keyword.clone()) + .map(|com| com.keyword()) .filter(|kwd| kwd.starts_with(part) || format!("/{}", kwd).starts_with(part)) .map(|c| c.to_string()) .collect(), - ArgumentSyntax::Message => vec![], - ArgumentSyntax::OneOf(_, strings, alts, _) => { + ArgumentSpec::Message => vec![], + ArgumentSpec::OneOf(_, strings, alts, _) => { let string_completions = strings .iter() .filter(|string| string.starts_with(part)) @@ -150,7 +404,7 @@ impl ArgumentSyntax { .flat_map(|b| (*b).complete(&state, part)) .map(|c| c.to_string()); string_completions.chain(alt_completions).collect() - } + }, } } } diff --git a/server/src/cmd.rs b/server/src/cmd.rs index 9b63633afc..44a64db825 100644 --- a/server/src/cmd.rs +++ b/server/src/cmd.rs @@ -5,7 +5,9 @@ use crate::{Server, StateExt}; use chrono::{NaiveTime, Timelike}; use common::{ - assets, comp, + assets, + cmd::{ChatCommand, CHAT_COMMANDS}, + comp, event::{EventBus, ServerEvent}, msg::{PlayerListUpdate, ServerMsg}, npc::{self, get_npc_name}, @@ -20,274 +22,70 @@ use specs::{Builder, Entity as EcsEntity, Join, WorldExt}; use vek::*; use world::util::Sampler; -use lazy_static::lazy_static; use log::error; use scan_fmt::{scan_fmt, scan_fmt_some}; -/// Struct representing a command that a user can run from server chat. -pub struct ChatCommand { - /// The keyword used to invoke the command, omitting the leading '/'. - pub keyword: &'static str, - /// A format string for parsing arguments. - arg_fmt: &'static str, - /// A message that explains how the command is used. - help_string: &'static str, - /// A boolean that is used to check whether the command requires - /// administrator permissions or not. - needs_admin: bool, - /// Handler function called when the command is executed. - /// # Arguments - /// * `&mut Server` - the `Server` instance executing the command. - /// * `EcsEntity` - an `Entity` corresponding to the player that invoked the - /// command. - /// * `EcsEntity` - an `Entity` for the player on whom the command is - /// invoked. This differs from the previous argument when using /sudo - /// * `String` - a `String` containing the part of the command after the - /// keyword. - /// * `&ChatCommand` - the command to execute with the above arguments. - /// Handler functions must parse arguments from the the given `String` - /// (`scan_fmt!` is included for this purpose). - handler: fn(&mut Server, EcsEntity, EcsEntity, String, &ChatCommand), +pub trait ChatCommandExt { + fn execute(&self, server: &mut Server, entity: EcsEntity, args: String); } - -impl ChatCommand { - /// Creates a new chat command. - pub fn new( - keyword: &'static str, - arg_fmt: &'static str, - help_string: &'static str, - needs_admin: bool, - handler: fn(&mut Server, EcsEntity, EcsEntity, String, &ChatCommand), - ) -> Self { - Self { - keyword, - arg_fmt, - help_string, - needs_admin, - handler, - } - } - - /// Calls the contained handler function, passing `&self` as the last - /// argument. - pub fn execute(&self, server: &mut Server, entity: EcsEntity, args: String) { - if self.needs_admin { - if !server.entity_is_admin(entity) { - server.notify_client( - entity, - ServerMsg::private(format!( - "You don't have permission to use '/{}'.", - self.keyword - )), - ); - return; - } else { - (self.handler)(server, entity, entity, args, self); - } +impl ChatCommandExt for ChatCommand { + fn execute(&self, server: &mut Server, entity: EcsEntity, args: String) { + let cmd_data = self.data(); + if cmd_data.needs_admin && !server.entity_is_admin(entity) { + server.notify_client( + entity, + ServerMsg::private(format!( + "You don't have permission to use '/{}'.", + self.keyword() + )), + ); + return; } else { - (self.handler)(server, entity, entity, args, self); + get_handler(self)(server, entity, entity, args, &self); } } } -lazy_static! { - /// Static list of chat commands available to the server. - pub static ref CHAT_COMMANDS: Vec<ChatCommand> = vec![ - ChatCommand::new( - "give_item", - "{} {d}", - "/give_item <path to item> [num]\n\ - Example items: common/items/apple, common/items/debug/boost", - true, - handle_give,), - ChatCommand::new( - "jump", - "{d} {d} {d}", - "/jump <dx> <dy> <dz> : Offset your current position", - true, - handle_jump, - ), - ChatCommand::new( - "goto", - "{d} {d} {d}", - "/goto <x> <y> <z> : Teleport to a position", - true, - handle_goto, - ), - ChatCommand::new( - "alias", - "{}", - "/alias <name> : Change your alias", - false, - handle_alias, - ), - ChatCommand::new( - "tp", - "{}", - "/tp <alias> : Teleport to another player", - true, - handle_tp, - ), - ChatCommand::new( - "kill", - "{}", - "/kill : Kill yourself", - false, - handle_kill, - ), - ChatCommand::new( - "time", - "{} {s}", - "/time <XY:XY> or [Time of day] : Set the time of day", - true, - handle_time, - ), - ChatCommand::new( - "spawn", - "{} {} {d}", - "/spawn <alignment> <entity> [amount] : Spawn a test entity", - true, - handle_spawn, - ), - ChatCommand::new( - "players", - "{}", - "/players : Lists players currently online", - false, - handle_players, - ), - ChatCommand::new( - "help", "", "/help: Display this message", false, handle_help), - ChatCommand::new( - "health", - "{}", - "/health : Set your current health", - true, - handle_health, - ), - ChatCommand::new( - "build", - "", - "/build : Toggles build mode on and off", - true, - handle_build, - ), - ChatCommand::new( - "tell", - "{}", - "/tell <alias> <message>: Send a message to another player", - false, - handle_tell, - ), - ChatCommand::new( - "killnpcs", - "{}", - "/killnpcs : Kill the NPCs", - true, - handle_killnpcs, - ), - ChatCommand::new( - "object", - "{}", - "/object [Name]: Spawn an object", - true, - handle_object, - ), - ChatCommand::new( - "light", - "{} {} {} {} {} {} {}", - "/light <opt: <<cr> <cg> <cb>> <<ox> <oy> <oz>> <<strength>>>: Spawn entity with light", - true, - handle_light, - ), - ChatCommand::new( - "lantern", - "{} {} {} {}", - "/lantern <strength> [<r> <g> <b>]: Change your lantern's strength and color", - true, - handle_lantern, - ), - ChatCommand::new( - "explosion", - "{}", - "/explosion <radius> : Explodes the ground around you", - true, - handle_explosion, - ), - ChatCommand::new( - "waypoint", - "{}", - "/waypoint : Set your waypoint to your current position", - true, - handle_waypoint, - ), - ChatCommand::new( - "adminify", - "{}", - "/adminify <playername> : Temporarily gives a player admin permissions or removes them", - true, - handle_adminify, - ), - ChatCommand::new( - "debug_column", - "{} {}", - "/debug_column <x> <y> : Prints some debug information about a column", - false, - handle_debug_column, - ), - ChatCommand::new( - "give_exp", - "{d} {}", - "/give_exp <amount> <playername?> : Give experience to yourself or specify a target player", - true, - handle_exp, - ), - ChatCommand::new( - "set_level", - "{d} {}", - "/set_level <level> <playername?> : Set own Level or specify a target player", - true, - handle_level - ), - ChatCommand::new( - "removelights", - "{}", - "/removelights [radius] : Removes all lights spawned by players", - true, - handle_remove_lights, - ), - ChatCommand::new( - "debug", - "", - "/debug : Place all debug items into your pack.", - true, - handle_debug, - ), - ChatCommand::new( - "sudo", - "{} {} {/.*/}", - "/sudo <player> /<command> [args...] : Run command as if you were another player", - true, - handle_sudo, - ), - ChatCommand::new( - "version", - "", - "/version : Prints server version", - false, - handle_version, - ), - ]; +type CommandHandler = fn(&mut Server, EcsEntity, EcsEntity, String, &ChatCommand); +fn get_handler(cmd: &ChatCommand) -> CommandHandler { + match cmd { + ChatCommand::Adminify => handle_adminify, + ChatCommand::Alias => handle_alias, + ChatCommand::Build => handle_build, + ChatCommand::Debug => handle_debug, + ChatCommand::DebugColumn => handle_debug_column, + ChatCommand::Explosion => handle_explosion, + ChatCommand::GiveExp => handle_give_exp, + ChatCommand::GiveItem => handle_give_item, + ChatCommand::Goto => handle_goto, + ChatCommand::Health => handle_health, + ChatCommand::Help => handle_help, + ChatCommand::Jump => handle_jump, + ChatCommand::Kill => handle_kill, + ChatCommand::KillNpcs => handle_kill_npcs, + ChatCommand::Lantern => handle_lantern, + ChatCommand::Light => handle_light, + ChatCommand::Object => handle_object, + ChatCommand::Players => handle_players, + ChatCommand::RemoveLights => handle_remove_lights, + ChatCommand::SetLevel => handle_set_level, + ChatCommand::Spawn => handle_spawn, + ChatCommand::Sudo => handle_sudo, + ChatCommand::Tell => handle_tell, + ChatCommand::Time => handle_time, + ChatCommand::Tp => handle_tp, + ChatCommand::Version => handle_version, + ChatCommand::Waypoint => handle_waypoint, + } } - -fn handle_give( +fn handle_give_item( server: &mut Server, client: EcsEntity, target: EcsEntity, args: String, action: &ChatCommand, ) { - if let (Some(item_name), give_amount_opt) = scan_fmt_some!(&args, action.arg_fmt, String, u32) { + if let (Some(item_name), give_amount_opt) = scan_fmt_some!(&args, &action.arg_fmt(), String, u32) { let give_amount = give_amount_opt.unwrap_or(1); if let Ok(item) = assets::load_cloned(&item_name) { let mut item: Item = item; @@ -346,7 +144,7 @@ fn handle_give( ); } } else { - server.notify_client(client, ServerMsg::private(String::from(action.help_string))); + server.notify_client(client, ServerMsg::private(String::from(action.help_string()))); } } @@ -357,7 +155,7 @@ fn handle_jump( args: String, action: &ChatCommand, ) { - if let Ok((x, y, z)) = scan_fmt!(&args, action.arg_fmt, f32, f32, f32) { + if let Ok((x, y, z)) = scan_fmt!(&args, &action.arg_fmt(), f32, f32, f32) { match server.state.read_component_cloned::<comp::Pos>(target) { Some(current_pos) => { server @@ -380,7 +178,7 @@ fn handle_goto( args: String, action: &ChatCommand, ) { - if let Ok((x, y, z)) = scan_fmt!(&args, action.arg_fmt, f32, f32, f32) { + if let Ok((x, y, z)) = scan_fmt!(&args, &action.arg_fmt(), f32, f32, f32) { if server .state .read_component_cloned::<comp::Pos>(target) @@ -397,7 +195,10 @@ fn handle_goto( ); } } else { - server.notify_client(client, ServerMsg::private(String::from(action.help_string))); + server.notify_client( + client, + ServerMsg::private(String::from(action.help_string())), + ); } } @@ -432,7 +233,7 @@ fn handle_time( args: String, action: &ChatCommand, ) { - let time = scan_fmt_some!(&args, action.arg_fmt, String); + let time = scan_fmt_some!(&args, &action.arg_fmt(), String); let new_time = match time.as_ref().map(|s| s.as_str()) { Some("midnight") => NaiveTime::from_hms(0, 0, 0), Some("night") => NaiveTime::from_hms(20, 0, 0), @@ -490,7 +291,7 @@ fn handle_health( args: String, action: &ChatCommand, ) { - if let Ok(hp) = scan_fmt!(&args, action.arg_fmt, u32) { + if let Ok(hp) = scan_fmt!(&args, &action.arg_fmt(), u32) { if let Some(stats) = server .state .ecs() @@ -519,7 +320,7 @@ fn handle_alias( args: String, action: &ChatCommand, ) { - if let Ok(alias) = scan_fmt!(&args, action.arg_fmt, String) { + if let Ok(alias) = scan_fmt!(&args, &action.arg_fmt(), String) { server .state .ecs_mut() @@ -540,7 +341,10 @@ fn handle_alias( server.state.notify_registered_clients(msg); } } else { - server.notify_client(client, ServerMsg::private(String::from(action.help_string))); + server.notify_client( + client, + ServerMsg::private(String::from(action.help_string())), + ); } } @@ -551,7 +355,7 @@ fn handle_tp( args: String, action: &ChatCommand, ) { - if let Ok(alias) = scan_fmt!(&args, action.arg_fmt, String) { + if let Ok(alias) = scan_fmt!(&args, &action.arg_fmt(), String) { let ecs = server.state.ecs(); let opt_player = (&ecs.entities(), &ecs.read_storage::<comp::Player>()) .join() @@ -576,7 +380,7 @@ fn handle_tp( ); server.notify_client( client, - ServerMsg::private(String::from(action.help_string)), + ServerMsg::private(String::from(action.help_string())), ); }, }, @@ -585,7 +389,10 @@ fn handle_tp( }, } } else { - server.notify_client(client, ServerMsg::private(String::from(action.help_string))); + server.notify_client( + client, + ServerMsg::private(String::from(action.help_string())), + ); } } @@ -596,7 +403,7 @@ fn handle_spawn( args: String, action: &ChatCommand, ) { - match scan_fmt_some!(&args, action.arg_fmt, String, npc::NpcBody, String) { + match scan_fmt_some!(&args, &action.arg_fmt(), String, npc::NpcBody, String) { (Some(opt_align), Some(npc::NpcBody(id, mut body)), opt_amount) => { if let Some(alignment) = parse_alignment(target, &opt_align) { let amount = opt_amount @@ -659,7 +466,10 @@ fn handle_spawn( } }, _ => { - server.notify_client(client, ServerMsg::private(String::from(action.help_string))); + server.notify_client( + client, + ServerMsg::private(String::from(action.help_string())), + ); }, } } @@ -733,12 +543,16 @@ fn handle_help( server: &mut Server, client: EcsEntity, _target: EcsEntity, - _args: String, - _action: &ChatCommand, + args: String, + action: &ChatCommand, ) { - for cmd in CHAT_COMMANDS.iter() { - if !cmd.needs_admin || server.entity_is_admin(client) { - server.notify_client(client, ServerMsg::private(String::from(cmd.help_string))); + if let Some(cmd) = scan_fmt_some!(&args, &action.arg_fmt(), ChatCommand) { + server.notify_client(client, ServerMsg::private(String::from(cmd.help_string()))); + } else { + for cmd in CHAT_COMMANDS.iter() { + if !cmd.needs_admin() || server.entity_is_admin(client) { + server.notify_client(client, ServerMsg::private(String::from(cmd.help_string()))); + } } } } @@ -753,7 +567,7 @@ fn parse_alignment(owner: EcsEntity, alignment: &str) -> Option<comp::Alignment> } } -fn handle_killnpcs( +fn handle_kill_npcs( server: &mut Server, client: EcsEntity, _target: EcsEntity, @@ -781,9 +595,9 @@ fn handle_object( client: EcsEntity, target: EcsEntity, args: String, - _action: &ChatCommand, + action: &ChatCommand, ) { - let obj_type = scan_fmt!(&args, _action.arg_fmt, String); + let obj_type = scan_fmt!(&args, &action.arg_fmt(), String); let pos = server .state @@ -894,7 +708,7 @@ fn handle_light( action: &ChatCommand, ) { let (opt_r, opt_g, opt_b, opt_x, opt_y, opt_z, opt_s) = - scan_fmt_some!(&args, action.arg_fmt, f32, f32, f32, f32, f32, f32, f32); + scan_fmt_some!(&args, &action.arg_fmt(), f32, f32, f32, f32, f32, f32, f32); let mut light_emitter = comp::LightEmitter::default(); let mut light_offset_opt = None; @@ -955,7 +769,8 @@ fn handle_lantern( args: String, action: &ChatCommand, ) { - if let (Some(s), r, g, b) = scan_fmt_some!(&args, action.arg_fmt, f32, f32, f32, f32) { + println!("args: '{}', fmt: '{}'", &args, &action.arg_fmt()); + if let (Some(s), r, g, b) = scan_fmt_some!(&args, &action.arg_fmt(), f32, f32, f32, f32) { if let Some(light) = server .state .ecs() @@ -987,7 +802,10 @@ fn handle_lantern( ); } } else { - server.notify_client(client, ServerMsg::private(String::from(action.help_string))); + server.notify_client( + client, + ServerMsg::private(String::from(action.help_string())), + ); } } @@ -998,7 +816,7 @@ fn handle_explosion( args: String, action: &ChatCommand, ) { - let power = scan_fmt!(&args, action.arg_fmt, f32).unwrap_or(8.0); + let power = scan_fmt!(&args, &action.arg_fmt(), f32).unwrap_or(8.0); if power > 512.0 { server.notify_client( @@ -1062,7 +880,7 @@ fn handle_adminify( args: String, action: &ChatCommand, ) { - if let Ok(alias) = scan_fmt!(&args, action.arg_fmt, String) { + if let Ok(alias) = scan_fmt!(&args, &action.arg_fmt(), String) { let ecs = server.state.ecs(); let opt_player = (&ecs.entities(), &ecs.read_storage::<comp::Player>()) .join() @@ -1082,11 +900,17 @@ fn handle_adminify( client, ServerMsg::private(format!("Player '{}' not found!", alias)), ); - server.notify_client(client, ServerMsg::private(String::from(action.help_string))); + server.notify_client( + client, + ServerMsg::private(String::from(action.help_string())), + ); }, } } else { - server.notify_client(client, ServerMsg::private(String::from(action.help_string))); + server.notify_client( + client, + ServerMsg::private(String::from(action.help_string())), + ); } } @@ -1104,7 +928,7 @@ fn handle_tell( ); return; } - if let Ok(alias) = scan_fmt!(&args, action.arg_fmt, String) { + if let Ok(alias) = scan_fmt!(&args, &action.arg_fmt(), String) { let ecs = server.state.ecs(); let msg = &args[alias.len()..args.len()]; if let Some(player) = (&ecs.entities(), &ecs.read_storage::<comp::Player>()) @@ -1152,7 +976,10 @@ fn handle_tell( ); } } else { - server.notify_client(client, ServerMsg::private(String::from(action.help_string))); + server.notify_client( + client, + ServerMsg::private(String::from(action.help_string())), + ); } } @@ -1180,7 +1007,7 @@ fn handle_debug_column( ) { let sim = server.world.sim(); let sampler = server.world.sample_columns(); - if let Ok((x, y)) = scan_fmt!(&args, action.arg_fmt, i32, i32) { + if let Ok((x, y)) = scan_fmt!(&args, &action.arg_fmt(), i32, i32) { let wpos = Vec2::new(x, y); /* let chunk_pos = wpos.map2(TerrainChunkSize::RECT_SIZE, |e, sz: u32| { e / sz as i32 @@ -1244,7 +1071,10 @@ spawn_rate {:?} "#, ); } } else { - server.notify_client(client, ServerMsg::private(String::from(action.help_string))); + server.notify_client( + client, + ServerMsg::private(String::from(action.help_string())), + ); } } @@ -1264,14 +1094,14 @@ fn find_target( } } -fn handle_exp( +fn handle_give_exp( server: &mut Server, client: EcsEntity, target: EcsEntity, args: String, action: &ChatCommand, ) { - let (a_exp, a_alias) = scan_fmt_some!(&args, action.arg_fmt, i64, String); + let (a_exp, a_alias) = scan_fmt_some!(&args, &action.arg_fmt(), i64, String); if let Some(exp) = a_exp { let ecs = server.state.ecs_mut(); @@ -1298,14 +1128,14 @@ fn handle_exp( } } -fn handle_level( +fn handle_set_level( server: &mut Server, client: EcsEntity, target: EcsEntity, args: String, action: &ChatCommand, ) { - let (a_lvl, a_alias) = scan_fmt_some!(&args, action.arg_fmt, u32, String); + let (a_lvl, a_alias) = scan_fmt_some!(&args, &action.arg_fmt(), u32, String); if let Some(lvl) = a_lvl { let ecs = server.state.ecs_mut(); @@ -1378,7 +1208,7 @@ fn handle_remove_lights( args: String, action: &ChatCommand, ) { - let opt_radius = scan_fmt_some!(&args, action.arg_fmt, f32); + let opt_radius = scan_fmt_some!(&args, &action.arg_fmt(), f32); let opt_player_pos = server.state.read_component_cloned::<comp::Pos>(target); let mut to_delete = vec![]; @@ -1430,7 +1260,7 @@ fn handle_sudo( action: &ChatCommand, ) { if let (Some(player_alias), Some(cmd), cmd_args) = - scan_fmt_some!(&args, action.arg_fmt, String, String, String) + scan_fmt_some!(&args, &action.arg_fmt(), String, String, String) { let cmd_args = cmd_args.unwrap_or(String::from("")); let cmd = if cmd.chars().next() == Some('/') { @@ -1438,14 +1268,14 @@ fn handle_sudo( } else { cmd }; - if let Some(action) = CHAT_COMMANDS.iter().find(|c| c.keyword == cmd) { + if let Some(action) = CHAT_COMMANDS.iter().find(|c| c.keyword() == cmd) { let ecs = server.state.ecs(); let entity_opt = (&ecs.entities(), &ecs.read_storage::<comp::Player>()) .join() .find(|(_, player)| player.alias == player_alias) .map(|(entity, _)| entity); if let Some(entity) = entity_opt { - (action.handler)(server, client, entity, cmd_args, action); + get_handler(action)(server, client, entity, cmd_args, action); } else { server.notify_client( client, @@ -1459,7 +1289,10 @@ fn handle_sudo( ); } } else { - server.notify_client(client, ServerMsg::private(String::from(action.help_string))); + server.notify_client( + client, + ServerMsg::private(String::from(action.help_string())), + ); } } @@ -1479,3 +1312,4 @@ fn handle_version( )), ); } + diff --git a/server/src/lib.rs b/server/src/lib.rs index 97126cb648..eb29844b80 100644 --- a/server/src/lib.rs +++ b/server/src/lib.rs @@ -22,11 +22,12 @@ use crate::{ auth_provider::AuthProvider, chunk_generator::ChunkGenerator, client::{Client, RegionSubscription}, - cmd::CHAT_COMMANDS, + cmd::ChatCommandExt, state_ext::StateExt, sys::sentinel::{DeletedEntities, TrackedComps}, }; use common::{ + cmd::ChatCommand, comp, event::{EventBus, ServerEvent}, msg::{ClientMsg, ClientState, ServerInfo, ServerMsg}, @@ -534,18 +535,16 @@ impl Server { }; // Find the command object and run its handler. - let action_opt = CHAT_COMMANDS.iter().find(|x| x.keyword == kwd); - match action_opt { - Some(action) => action.execute(self, entity, args), - // Unknown command - None => { - if let Some(client) = self.state.ecs().write_storage::<Client>().get_mut(entity) { - client.notify(ServerMsg::private(format!( - "Unknown command '/{}'.\nType '/help' for available commands", - kwd - ))); - } - }, + if let Ok(command) = kwd.parse::<ChatCommand>() { + command.execute(self, entity, args); + } else { + self.notify_client( + entity, + ServerMsg::private(format!( + "Unknown command '/{}'.\nType '/help' for available commands", + kwd + )), + ); } } From 58b0e9ef75cc7dc6dabb60375927a3b16515e91b Mon Sep 17 00:00:00 2001 From: CapsizeGlimmer <> Date: Tue, 5 May 2020 18:56:58 -0400 Subject: [PATCH 3/9] Move cursor to end when moving through chat history --- voxygen/src/hud/chat.rs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/voxygen/src/hud/chat.rs b/voxygen/src/hud/chat.rs index 2a3197fdaa..c41ba122d9 100644 --- a/voxygen/src/hud/chat.rs +++ b/voxygen/src/hud/chat.rs @@ -137,8 +137,9 @@ impl<'a> Widget for Chat<'a> { } }); + let mut force_cursor = self.force_cursor; + // If up or down are pressed move through history - // TODO: move cursor to the end of the last line match ui.widget_input(state.ids.input).presses().key().fold( (false, false), |(up, down), key_press| match key_press.key { @@ -152,6 +153,7 @@ impl<'a> Widget for Chat<'a> { state.update(|s| { s.history_pos += 1; s.input = s.history.get(s.history_pos - 1).unwrap().to_owned(); + force_cursor = Some(Index{line: 0, char: s.input.len()}); }); } }, @@ -161,6 +163,7 @@ impl<'a> Widget for Chat<'a> { s.history_pos -= 1; if s.history_pos > 0 { s.input = s.history.get(s.history_pos - 1).unwrap().to_owned(); + force_cursor = Some(Index{line: 0, char: s.input.len()}); } else { s.input.clear(); } @@ -190,7 +193,7 @@ impl<'a> Widget for Chat<'a> { .font_size(self.fonts.opensans.scale(15)) .font_id(self.fonts.opensans.conrod_id); - if let Some(pos) = self.force_cursor { + if let Some(pos) = force_cursor { text_edit = text_edit.cursor_pos(pos); } From 24fa23fef4a9bd2dcc32da13fd5f034b81901033 Mon Sep 17 00:00:00 2001 From: CapsizeGlimmer <> Date: Wed, 6 May 2020 14:45:58 -0400 Subject: [PATCH 4/9] Initial client implementation of tab completion --- server/src/cmd.rs | 1 - voxygen/src/hud/chat.rs | 200 ++++++++++++++++++++++++++++++++-------- voxygen/src/hud/mod.rs | 21 +++++ 3 files changed, 181 insertions(+), 41 deletions(-) diff --git a/server/src/cmd.rs b/server/src/cmd.rs index 44a64db825..26a6263484 100644 --- a/server/src/cmd.rs +++ b/server/src/cmd.rs @@ -1312,4 +1312,3 @@ fn handle_version( )), ); } - diff --git a/voxygen/src/hud/chat.rs b/voxygen/src/hud/chat.rs index c41ba122d9..0e12c808bd 100644 --- a/voxygen/src/hud/chat.rs +++ b/voxygen/src/hud/chat.rs @@ -8,9 +8,12 @@ use common::{msg::validate_chat_msg, ChatType}; use conrod_core::{ input::Key, position::Dimension, - text::cursor::Index, + text::{ + self, + cursor::{self, Index}, + }, widget::{self, Button, Id, List, Rectangle, Text, TextEdit}, - widget_ids, Colorable, Positionable, Sizeable, UiCell, Widget, WidgetCommon, + widget_ids, Colorable, Positionable, Sizeable, Ui, UiCell, Widget, WidgetCommon, }; use std::collections::VecDeque; @@ -18,13 +21,16 @@ widget_ids! { struct Ids { message_box, message_box_bg, - input, - input_bg, + chat_input, + chat_input_bg, chat_arrow, + completion_box, } } const MAX_MESSAGES: usize = 100; +// Maximum completions shown at once +const MAX_COMPLETIONS: usize = 10; #[derive(WidgetCommon)] pub struct Chat<'a> { @@ -64,6 +70,9 @@ impl<'a> Chat<'a> { pub fn input(mut self, input: String) -> Self { if let Ok(()) = validate_chat_msg(&input) { + if input.contains('\t') { + println!("Contains tab: '{}'", input); + } self.force_input = Some(input); } self @@ -97,6 +106,10 @@ pub struct State { // Index into the history Vec, history_pos == 0 is history not in use // otherwise index is history_pos -1 history_pos: usize, + completions: Vec<String>, + // Index into the completion Vec, completions_pos == 0 means not in use + // otherwise index is completions_pos -1 + completions_pos: usize, } pub enum Event { @@ -115,6 +128,8 @@ impl<'a> Widget for Chat<'a> { messages: VecDeque::new(), history: VecDeque::new(), history_pos: 0, + completions: Vec::new(), + completions_pos: 0, ids: Ids::new(id_gen), } } @@ -140,51 +155,77 @@ impl<'a> Widget for Chat<'a> { let mut force_cursor = self.force_cursor; // If up or down are pressed move through history - match ui.widget_input(state.ids.input).presses().key().fold( - (false, false), - |(up, down), key_press| match key_press.key { - Key::Up => (true, down), - Key::Down => (up, true), - _ => (up, down), - }, - ) { - (true, false) => { - if state.history_pos < state.history.len() { - state.update(|s| { + let history_move = + ui.widget_input(state.ids.chat_input) + .presses() + .key() + .fold(0, |n, key_press| match key_press.key { + Key::Up => n + 1, + Key::Down => n - 1, + _ => n, + }); + if history_move != 0 { + state.update(|s| { + if history_move > 0 { + if s.history_pos < s.history.len() { s.history_pos += 1; - s.input = s.history.get(s.history_pos - 1).unwrap().to_owned(); - force_cursor = Some(Index{line: 0, char: s.input.len()}); - }); - } - }, - (false, true) => { - if state.history_pos > 0 { - state.update(|s| { + } + } else { + if s.history_pos > 0 { s.history_pos -= 1; - if s.history_pos > 0 { - s.input = s.history.get(s.history_pos - 1).unwrap().to_owned(); - force_cursor = Some(Index{line: 0, char: s.input.len()}); - } else { - s.input.clear(); - } - }); + } } - }, - _ => {}, + if s.history_pos > 0 { + s.input = s.history.get(s.history_pos - 1).unwrap().to_owned(); + force_cursor = + cursor_offset_to_index(s.input.len(), &s.input, &ui, &self.fonts); + } else { + s.input.clear(); + } + }); + } + + // Handle tab-completion + if let Some(cursor) = state.input.find('\t') { + state.update(|s| { + if s.completions_pos > 0 { + if s.completions_pos >= s.completions.len() { + s.completions_pos = 1; + } else { + s.completions_pos += 1; + } + } else { + // TODO FIXME pull completions from common::cmd + s.completions = "a,bc,def,ghi,jklm,nop,qr,stu,v,w,xyz" + .split(",") + .map(|x| x.to_string()) + .collect(); + s.completions_pos = 1; + } + //let index = force_cursor; + //let cursor = index.and_then(|index| cursor_index_to_offset(index, &s.input, + // ui, &self.fonts)).unwrap_or(0); + let replacement = &s.completions[s.completions_pos - 1]; + let (completed, offset) = do_tab_completion(cursor, &s.input, replacement); + force_cursor = cursor_offset_to_index(offset, &completed, &ui, &self.fonts); + s.input = completed; + }); } let keyboard_capturer = ui.global_input().current.widget_capturing_keyboard; if let Some(input) = &self.force_input { - state.update(|s| s.input = input.clone()); + state.update(|s| s.input = input.to_string()); } let input_focused = - keyboard_capturer == Some(state.ids.input) || keyboard_capturer == Some(id); + keyboard_capturer == Some(state.ids.chat_input) || keyboard_capturer == Some(id); // Only show if it has the keyboard captured. // Chat input uses a rectangle as its background. if input_focused { + // Any changes to this TextEdit's width and font size must be reflected in + // `cursor_offset_to_index` below. let mut text_edit = TextEdit::new(&state.input) .w(460.0) .restrict_to_height(false) @@ -205,15 +246,18 @@ impl<'a> Widget for Chat<'a> { .rgba(0.0, 0.0, 0.0, transp + 0.1) .bottom_left_with_margins_on(ui.window, 10.0, 10.0) .w(470.0) - .set(state.ids.input_bg, ui); + .set(state.ids.chat_input_bg, ui); if let Some(str) = text_edit - .top_left_with_margins_on(state.ids.input_bg, 1.0, 1.0) - .set(state.ids.input, ui) + .top_left_with_margins_on(state.ids.chat_input_bg, 1.0, 1.0) + .set(state.ids.chat_input, ui) { let mut input = str.to_owned(); input.retain(|c| c != '\n'); if let Ok(()) = validate_chat_msg(&input) { + if input.contains('\t') { + println!("Contains tab: '{}'", input); + } state.update(|s| s.input = input); } } @@ -224,7 +268,7 @@ impl<'a> Widget for Chat<'a> { .rgba(0.0, 0.0, 0.0, transp) .and(|r| { if input_focused { - r.up_from(state.ids.input_bg, 0.0) + r.up_from(state.ids.chat_input_bg, 0.0) } else { r.bottom_left_with_margins_on(ui.window, 10.0, 10.0) } @@ -304,11 +348,11 @@ impl<'a> Widget for Chat<'a> { // If the chat widget is focused, return a focus event to pass the focus to the // input box. if keyboard_capturer == Some(id) { - Some(Event::Focus(state.ids.input)) + Some(Event::Focus(state.ids.chat_input)) } // If enter is pressed and the input box is not empty, send the current message. else if ui - .widget_input(state.ids.input) + .widget_input(state.ids.chat_input) .presses() .key() .any(|key_press| match key_press.key { @@ -333,3 +377,79 @@ impl<'a> Widget for Chat<'a> { } } } + +fn do_tab_completion(cursor: usize, input: &str, word: &str) -> (String, usize) { + let mut pre_ws = None; + let mut post_ws = None; + for (char_i, (byte_i, c)) in input.char_indices().enumerate() { + if c.is_whitespace() && c != '\t' { + if char_i < cursor { + pre_ws = Some(byte_i); + } else { + assert_eq!(post_ws, None); // TODO debug + post_ws = Some(byte_i); + break; + } + } + } + + match (pre_ws, post_ws) { + (None, None) => (word.to_string(), word.chars().count()), + (None, Some(i)) => ( + format!("{}{}", word, input.split_at(i).1), + word.chars().count(), + ), + (Some(i), None) => { + let l_split = input.split_at(i).0; + let completed = format!("{} {}", l_split, word); + ( + completed, + l_split.chars().count() + 1 + word.chars().count(), + ) + }, + (Some(i), Some(j)) => { + let l_split = input.split_at(i).0; + let r_split = input.split_at(j).1; + let completed = format!("{} {}{}", l_split, word, r_split); + ( + completed, + l_split.chars().count() + 1 + word.chars().count(), + ) + }, + } +} + +fn cursor_index_to_offset( + index: text::cursor::Index, + text: &str, + ui: &Ui, + fonts: &ConrodVoxygenFonts, +) -> Option<usize> { + // Width and font must match that of the chat TextEdit + let width = 460.0; + let font = ui.fonts.get(fonts.opensans.conrod_id)?; + let font_size = fonts.opensans.scale(15); + let infos = text::line::infos(&text, &font, font_size).wrap_by_whitespace(width); + + text::glyph::index_after_cursor(infos, index) +} + +fn cursor_offset_to_index( + offset: usize, + text: &str, + ui: &Ui, + fonts: &ConrodVoxygenFonts, +) -> Option<Index> { + // This moves the cursor to the given offset. Conrod is a pain. + //let iter = cursor::xys_per_line_from_text(&text, &[], &font, font_size, + // Justify::Left, Align::Start, 2.0, Rect{x: Range{start: 0.0, end: width}, y: + // Range{start: 0.0, end: 12.345}}); + // cursor::closest_cursor_index_and_xy([f64::MAX, f64::MAX], iter).map(|(i, _)| + // i) Width and font must match that of the chat TextEdit + let width = 460.0; + let font = ui.fonts.get(fonts.opensans.conrod_id)?; + let font_size = fonts.opensans.scale(15); + let infos = text::line::infos(&text, &font, font_size).wrap_by_whitespace(width); + + cursor::index_before_char(infos, offset) +} diff --git a/voxygen/src/hud/mod.rs b/voxygen/src/hud/mod.rs index 4b8f951c89..5f802f22e3 100644 --- a/voxygen/src/hud/mod.rs +++ b/voxygen/src/hud/mod.rs @@ -2296,6 +2296,27 @@ impl Hud { camera: &Camera, dt: Duration, ) -> Vec<Event> { + // conrod eats tabs. Un-eat a tabstop so tab completion can work + if self.ui.ui.global_input().events().any(|event| { + use conrod_core::{event, input}; + match event { + //event::Event::Raw(event::Input::Press(input::Button::Keyboard(input::Key::Tab))) + // => true, + event::Event::Ui(event::Ui::Press( + _, + event::Press { + button: event::Button::Keyboard(input::Key::Tab), + .. + }, + )) => true, + _ => false, + } + }) { + self.ui + .ui + .handle_event(conrod_core::event::Input::Text("\t".to_string())); + } + if let Some(maybe_id) = self.to_focus.take() { self.ui.focus_widget(maybe_id); } From b0f0d716be4e71c565ed1d59406eebb49b4b88f8 Mon Sep 17 00:00:00 2001 From: CapsizeGlimmer <> Date: Fri, 8 May 2020 01:35:07 -0400 Subject: [PATCH 5/9] Tab completion returns real results --- common/src/cmd.rs | 102 +++++++++++++++++++++++++++++++++++++++------- 1 file changed, 87 insertions(+), 15 deletions(-) diff --git a/common/src/cmd.rs b/common/src/cmd.rs index bb134a5741..e3b47609dc 100644 --- a/common/src/cmd.rs +++ b/common/src/cmd.rs @@ -183,7 +183,7 @@ impl ChatCommand { }, ChatCommand::Spawn => cmd(vec![/*TODO*/], "Spawn a test entity", true), ChatCommand::Sudo => cmd( - vec![PlayerName(false), Command(false), Message /* TODO */], + vec![PlayerName(false), Command(false), SubCommand], "Run command as if you were another player", true, ), @@ -256,6 +256,7 @@ impl ChatCommand { ArgumentSpec::Any(_, _) => "{}", ArgumentSpec::Command(_) => "{}", ArgumentSpec::Message => "{/.*/}", + ArgumentSpec::SubCommand => "{/.*/}", ArgumentSpec::OneOf(_, _, _, _) => "{}", // TODO }) .collect::<Vec<_>>() @@ -304,6 +305,8 @@ pub enum ArgumentSpec { /// This is the final argument, consuming all characters until the end of /// input. Message, + /// This command is followed by another command (such as in /sudo) + SubCommand, /// The argument is likely an enum. The associated values are /// * label /// * Predefined string completions @@ -363,6 +366,7 @@ impl ArgumentSpec { } }, ArgumentSpec::Message => "<message>".to_string(), + ArgumentSpec::SubCommand => "<[/]command> [args...]".to_string(), ArgumentSpec::OneOf(label, _, _, optional) => { if *optional { format! {"[{}]", label} @@ -373,13 +377,9 @@ impl ArgumentSpec { } } - pub fn complete(&self, state: &State, part: &String) -> Vec<String> { + pub fn complete(&self, part: &str, state: &State) -> Vec<String> { match self { - ArgumentSpec::PlayerName(_) => (&state.ecs().read_storage::<Player>()) - .join() - .filter(|player| player.alias.starts_with(part)) - .map(|player| player.alias.clone()) - .collect(), + ArgumentSpec::PlayerName(_) => complete_player(part, &state), ArgumentSpec::ItemSpec(_) => assets::iterate() .filter(|asset| asset.starts_with(part)) .map(|c| c.to_string()) @@ -387,13 +387,9 @@ impl ArgumentSpec { ArgumentSpec::Float(_, x, _) => vec![format!("{}", x)], ArgumentSpec::Integer(_, x, _) => vec![format!("{}", x)], ArgumentSpec::Any(_, _) => vec![], - ArgumentSpec::Command(_) => CHAT_COMMANDS - .iter() - .map(|com| com.keyword()) - .filter(|kwd| kwd.starts_with(part) || format!("/{}", kwd).starts_with(part)) - .map(|c| c.to_string()) - .collect(), - ArgumentSpec::Message => vec![], + ArgumentSpec::Command(_) => complete_command(part), + ArgumentSpec::Message => complete_player(part, &state), + ArgumentSpec::SubCommand => complete_command(part), ArgumentSpec::OneOf(_, strings, alts, _) => { let string_completions = strings .iter() @@ -401,10 +397,86 @@ impl ArgumentSpec { .map(|c| c.to_string()); let alt_completions = alts .iter() - .flat_map(|b| (*b).complete(&state, part)) + .flat_map(|b| (*b).complete(part, &state)) .map(|c| c.to_string()); string_completions.chain(alt_completions).collect() }, } } } + +fn complete_player(part: &str, state: &State) -> Vec<String> { + println!("Player completion: '{}'", part); + state.ecs().read_storage::<Player>() + .join() + .inspect(|player| println!(" player: {}", player.alias)) + .filter(|player| player.alias.starts_with(part)) + .map(|player| player.alias.clone()) + .collect() +} + +fn complete_command(part: &str) -> Vec<String> { + println!("Command completion: '{}'", part); + CHAT_COMMANDS + .iter() + .map(|com| com.keyword()) + .filter(|kwd| kwd.starts_with(part) || format!("/{}", kwd).starts_with(part)) + .map(|c| c.to_string()) + .collect() +} + +fn nth_word(line: &str, n: usize) -> Option<usize> { + let mut is_space = false; + let mut j = 0; + for (i, c) in line.char_indices() { + match (is_space, c.is_whitespace()) { + (true, true) => {} + (true, false) => { is_space = false; } + (false, true) => { is_space = true; j += 1; } + (false, false) => {} + } + if j == n { + return Some(i); + } + } + return None; +} + +pub fn complete(line: &str, state: &State) -> Vec<String> { + let word = line.split_whitespace().last().unwrap_or(""); + if line.chars().next() == Some('/') { + let mut iter = line.split_whitespace(); + let cmd = iter.next().unwrap(); + if let Some((i, word)) = iter.enumerate().last() { + if let Ok(cmd) = cmd.parse::<ChatCommand>() { + if let Some(arg) = cmd.data().args.get(i) { + println!("Arg completion: {}", word); + arg.complete(word, &state) + } else { + match cmd.data().args.last() { + Some(ArgumentSpec::SubCommand) => { + if let Some(index) = nth_word(line, cmd.data().args.len()) { + complete(&line[index..], &state) + } else { + error!("Could not tab-complete SubCommand"); + vec![] + } + } + Some(ArgumentSpec::Message) => complete_player(word, &state), + _ => { vec![] } // End of command. Nothing to complete + } + } + } else { + // Completing for unknown chat command + complete_player(word, &state) + } + } else { + // Completing chat command name + complete_command(word) + } + } else { + // Not completing a command + complete_player(word, &state) + } +} + From 28e94afd3fa6eeee67c4180be26d9bb0763241c4 Mon Sep 17 00:00:00 2001 From: CapsizeGlimmer <> Date: Fri, 8 May 2020 17:38:58 -0400 Subject: [PATCH 6/9] Finish tab completion implementation --- common/src/cmd.rs | 66 +++++++++++-------- voxygen/src/hud/chat.rs | 141 +++++++++++++++++++++++++--------------- voxygen/src/hud/mod.rs | 8 +++ 3 files changed, 137 insertions(+), 78 deletions(-) diff --git a/common/src/cmd.rs b/common/src/cmd.rs index e3b47609dc..b99a3e0442 100644 --- a/common/src/cmd.rs +++ b/common/src/cmd.rs @@ -183,7 +183,7 @@ impl ChatCommand { }, ChatCommand::Spawn => cmd(vec![/*TODO*/], "Spawn a test entity", true), ChatCommand::Sudo => cmd( - vec![PlayerName(false), Command(false), SubCommand], + vec![PlayerName(false), SubCommand], "Run command as if you were another player", true, ), @@ -251,12 +251,12 @@ impl ChatCommand { .map(|arg| match arg { ArgumentSpec::PlayerName(_) => "{}", ArgumentSpec::ItemSpec(_) => "{}", - ArgumentSpec::Float(_, _, _) => "{f}", + ArgumentSpec::Float(_, _, _) => "{}", ArgumentSpec::Integer(_, _, _) => "{d}", ArgumentSpec::Any(_, _) => "{}", ArgumentSpec::Command(_) => "{}", ArgumentSpec::Message => "{/.*/}", - ArgumentSpec::SubCommand => "{/.*/}", + ArgumentSpec::SubCommand => "{} {/.*/}", ArgumentSpec::OneOf(_, _, _, _) => "{}", // TODO }) .collect::<Vec<_>>() @@ -406,34 +406,43 @@ impl ArgumentSpec { } fn complete_player(part: &str, state: &State) -> Vec<String> { - println!("Player completion: '{}'", part); - state.ecs().read_storage::<Player>() - .join() - .inspect(|player| println!(" player: {}", player.alias)) - .filter(|player| player.alias.starts_with(part)) - .map(|player| player.alias.clone()) - .collect() + let storage = state.ecs().read_storage::<Player>(); + let mut iter = storage.join(); + if let Some(first) = iter.next() { + std::iter::once(first) + .chain(iter) + .filter(|player| player.alias.starts_with(part)) + .map(|player| player.alias.clone()) + .collect() + } else { + vec!["singleplayer".to_string()] + } } fn complete_command(part: &str) -> Vec<String> { - println!("Command completion: '{}'", part); CHAT_COMMANDS .iter() .map(|com| com.keyword()) .filter(|kwd| kwd.starts_with(part) || format!("/{}", kwd).starts_with(part)) - .map(|c| c.to_string()) + .map(|c| format!("/{}", c)) .collect() } +// Get the byte index of the nth word. Used in completing "/sudo p /subcmd" fn nth_word(line: &str, n: usize) -> Option<usize> { let mut is_space = false; let mut j = 0; for (i, c) in line.char_indices() { match (is_space, c.is_whitespace()) { - (true, true) => {} - (true, false) => { is_space = false; } - (false, true) => { is_space = true; j += 1; } - (false, false) => {} + (true, true) => {}, + (true, false) => { + is_space = false; + j += 1; + }, + (false, true) => { + is_space = true; + }, + (false, false) => {}, } if j == n { return Some(i); @@ -443,16 +452,25 @@ fn nth_word(line: &str, n: usize) -> Option<usize> { } pub fn complete(line: &str, state: &State) -> Vec<String> { - let word = line.split_whitespace().last().unwrap_or(""); + let word = if line.chars().last().map_or(true, char::is_whitespace) { + "" + } else { + line.split_whitespace().last().unwrap_or("") + }; if line.chars().next() == Some('/') { let mut iter = line.split_whitespace(); let cmd = iter.next().unwrap(); - if let Some((i, word)) = iter.enumerate().last() { + let i = iter.count() + if word.is_empty() { 1 } else { 0 }; + if i == 0 { + // Completing chat command name + complete_command(word) + } else { if let Ok(cmd) = cmd.parse::<ChatCommand>() { - if let Some(arg) = cmd.data().args.get(i) { - println!("Arg completion: {}", word); + if let Some(arg) = cmd.data().args.get(i - 1) { + // Complete ith argument arg.complete(word, &state) } else { + // Complete past the last argument match cmd.data().args.last() { Some(ArgumentSpec::SubCommand) => { if let Some(index) = nth_word(line, cmd.data().args.len()) { @@ -461,22 +479,18 @@ pub fn complete(line: &str, state: &State) -> Vec<String> { error!("Could not tab-complete SubCommand"); vec![] } - } + }, Some(ArgumentSpec::Message) => complete_player(word, &state), - _ => { vec![] } // End of command. Nothing to complete + _ => vec![], // End of command. Nothing to complete } } } else { // Completing for unknown chat command complete_player(word, &state) } - } else { - // Completing chat command name - complete_command(word) } } else { // Not completing a command complete_player(word, &state) } } - diff --git a/voxygen/src/hud/chat.rs b/voxygen/src/hud/chat.rs index 0e12c808bd..50310bfe94 100644 --- a/voxygen/src/hud/chat.rs +++ b/voxygen/src/hud/chat.rs @@ -37,6 +37,7 @@ pub struct Chat<'a> { new_messages: &'a mut VecDeque<ClientEvent>, force_input: Option<String>, force_cursor: Option<Index>, + force_completions: Option<Vec<String>>, global_state: &'a GlobalState, imgs: &'a Imgs, @@ -60,6 +61,7 @@ impl<'a> Chat<'a> { new_messages, force_input: None, force_cursor: None, + force_completions: None, imgs, fonts, global_state, @@ -68,11 +70,17 @@ impl<'a> Chat<'a> { } } + pub fn prepare_tab_completion(mut self, input: String, state: &common::state::State) -> Self { + if let Some(index) = input.find('\t') { + self.force_completions = Some(common::cmd::complete(&input[..index], &state)); + } else { + self.force_completions = None; + } + self + } + pub fn input(mut self, input: String) -> Self { if let Ok(()) = validate_chat_msg(&input) { - if input.contains('\t') { - println!("Contains tab: '{}'", input); - } self.force_input = Some(input); } self @@ -107,12 +115,14 @@ pub struct State { // otherwise index is history_pos -1 history_pos: usize, completions: Vec<String>, - // Index into the completion Vec, completions_pos == 0 means not in use - // otherwise index is completions_pos -1 - completions_pos: usize, + // Index into the completion Vec + completions_index: Option<usize>, + // At which character is tab completion happening + completion_cursor: Option<usize>, } pub enum Event { + TabCompletionStart(String), SendMessage(String), Focus(Id), } @@ -129,7 +139,8 @@ impl<'a> Widget for Chat<'a> { history: VecDeque::new(), history_pos: 0, completions: Vec::new(), - completions_pos: 0, + completions_index: None, + completion_cursor: None, ids: Ids::new(id_gen), } } @@ -152,21 +163,74 @@ impl<'a> Widget for Chat<'a> { } }); + if let Some(comps) = &self.force_completions { + state.update(|s| s.completions = comps.clone()); + } + let mut force_cursor = self.force_cursor; - // If up or down are pressed move through history - let history_move = - ui.widget_input(state.ids.chat_input) - .presses() - .key() - .fold(0, |n, key_press| match key_press.key { - Key::Up => n + 1, - Key::Down => n - 1, - _ => n, - }); - if history_move != 0 { + // If up or down are pressed: move through history + // If any key other than up, down, or tab is pressed: stop completion. + let (history_dir, tab_dir, stop_tab_completion) = + ui.widget_input(state.ids.chat_input).presses().key().fold( + (0isize, 0isize, false), + |(n, m, tc), key_press| match key_press.key { + Key::Up => (n + 1, m - 1, tc), + Key::Down => (n - 1, m + 1, tc), + Key::Tab => (n, m + 1, tc), + _ => (n, m, true), + }, + ); + + // Handle tab completion + let request_tab_completions = if stop_tab_completion { + // End tab completion state.update(|s| { - if history_move > 0 { + if s.completion_cursor.is_some() { + s.completion_cursor = None; + } + s.completions_index = None; + }); + false + } else if let Some(cursor) = state.completion_cursor { + // Cycle through tab completions of the current word + if state.input.contains('\t') { + state.update(|s| s.input.retain(|c| c != '\t')); + //tab_dir + 1 + } + if !state.completions.is_empty() { + if tab_dir != 0 || state.completions_index.is_none() { + state.update(|s| { + let len = s.completions.len(); + s.completions_index = Some( + (s.completions_index.unwrap_or(0) + (tab_dir + len as isize) as usize) + % len, + ); + if let Some(replacement) = &s.completions.get(s.completions_index.unwrap()) + { + let (completed, offset) = + do_tab_completion(cursor, &s.input, replacement); + force_cursor = + cursor_offset_to_index(offset, &completed, &ui, &self.fonts); + s.input = completed; + } + }); + } + } + false + } else if let Some(cursor) = state.input.find('\t') { + // Begin tab completion + state.update(|s| s.completion_cursor = Some(cursor)); + true + } else { + // Not tab completing + false + }; + + // Move through history + if history_dir != 0 && state.completion_cursor.is_none() { + state.update(|s| { + if history_dir > 0 { if s.history_pos < s.history.len() { s.history_pos += 1; } @@ -185,33 +249,6 @@ impl<'a> Widget for Chat<'a> { }); } - // Handle tab-completion - if let Some(cursor) = state.input.find('\t') { - state.update(|s| { - if s.completions_pos > 0 { - if s.completions_pos >= s.completions.len() { - s.completions_pos = 1; - } else { - s.completions_pos += 1; - } - } else { - // TODO FIXME pull completions from common::cmd - s.completions = "a,bc,def,ghi,jklm,nop,qr,stu,v,w,xyz" - .split(",") - .map(|x| x.to_string()) - .collect(); - s.completions_pos = 1; - } - //let index = force_cursor; - //let cursor = index.and_then(|index| cursor_index_to_offset(index, &s.input, - // ui, &self.fonts)).unwrap_or(0); - let replacement = &s.completions[s.completions_pos - 1]; - let (completed, offset) = do_tab_completion(cursor, &s.input, replacement); - force_cursor = cursor_offset_to_index(offset, &completed, &ui, &self.fonts); - s.input = completed; - }); - } - let keyboard_capturer = ui.global_input().current.widget_capturing_keyboard; if let Some(input) = &self.force_input { @@ -255,9 +292,6 @@ impl<'a> Widget for Chat<'a> { let mut input = str.to_owned(); input.retain(|c| c != '\n'); if let Ok(()) = validate_chat_msg(&input) { - if input.contains('\t') { - println!("Contains tab: '{}'", input); - } state.update(|s| s.input = input); } } @@ -345,9 +379,12 @@ impl<'a> Widget for Chat<'a> { } } - // If the chat widget is focused, return a focus event to pass the focus to the - // input box. - if keyboard_capturer == Some(id) { + // We've started a new tab completion. Populate tab completion suggestions. + if request_tab_completions { + Some(Event::TabCompletionStart(state.input.to_string())) + // If the chat widget is focused, return a focus event to pass the focus + // to the input box. + } else if keyboard_capturer == Some(id) { Some(Event::Focus(state.ids.chat_input)) } // If enter is pressed and the input box is not empty, send the current message. diff --git a/voxygen/src/hud/mod.rs b/voxygen/src/hud/mod.rs index 5f802f22e3..eff6563521 100644 --- a/voxygen/src/hud/mod.rs +++ b/voxygen/src/hud/mod.rs @@ -443,6 +443,7 @@ pub struct Hud { force_ungrab: bool, force_chat_input: Option<String>, force_chat_cursor: Option<Index>, + tab_complete: Option<String>, pulse: f32, velocity: f32, voxygen_i18n: std::sync::Arc<VoxygenLocalization>, @@ -516,6 +517,7 @@ impl Hud { force_ungrab: false, force_chat_input: None, force_chat_cursor: None, + tab_complete: None, pulse: 0.0, velocity: 0.0, voxygen_i18n, @@ -1724,9 +1726,15 @@ impl Hud { &self.fonts, ) .and_then(self.force_chat_input.take(), |c, input| c.input(input)) + .and_then(self.tab_complete.take(), |c, input| { + c.prepare_tab_completion(input, &client.state()) + }) .and_then(self.force_chat_cursor.take(), |c, pos| c.cursor_pos(pos)) .set(self.ids.chat, ui_widgets) { + Some(chat::Event::TabCompletionStart(input)) => { + self.tab_complete = Some(input); + }, Some(chat::Event::SendMessage(message)) => { events.push(Event::SendMessage(message)); }, From b486de28ac9b63ac7670621ac6d4c2b07fca8fd1 Mon Sep 17 00:00:00 2001 From: CapsizeGlimmer <> Date: Fri, 8 May 2020 22:42:51 -0400 Subject: [PATCH 7/9] Implement tab completion of enums (/object /time /spawn) and numbers --- common/src/assets/mod.rs | 9 +- common/src/cmd.rs | 282 ++++++++++++++++++++------------- common/src/comp/body/object.rs | 63 +++++++- server/src/cmd.rs | 113 ++++--------- voxygen/src/hud/chat.rs | 17 -- 5 files changed, 273 insertions(+), 211 deletions(-) diff --git a/common/src/assets/mod.rs b/common/src/assets/mod.rs index a14ca93cf2..2810ed50eb 100644 --- a/common/src/assets/mod.rs +++ b/common/src/assets/mod.rs @@ -57,17 +57,10 @@ impl From<std::io::Error> for Error { lazy_static! { /// The HashMap where all loaded assets are stored in. - static ref ASSETS: RwLock<HashMap<String, Arc<dyn Any + 'static + Sync + Send>>> = + pub static ref ASSETS: RwLock<HashMap<String, Arc<dyn Any + 'static + Sync + Send>>> = RwLock::new(HashMap::new()); } -const ASSETS_TMP: [&'static str; 1] = ["common/items/lantern/black_0"]; -pub fn iterate() -> impl Iterator<Item = &'static str> { - // TODO FIXME implement this - //ASSETS.read().iter().flat_map(|e| e.keys()) - ASSETS_TMP.iter().map(|k| *k) -} - // TODO: Remove this function. It's only used in world/ in a really ugly way.To // do this properly assets should have all their necessary data in one file. A // ron file could be used to combine voxel data with positioning data for diff --git a/common/src/cmd.rs b/common/src/cmd.rs index b99a3e0442..ddbe7faec1 100644 --- a/common/src/cmd.rs +++ b/common/src/cmd.rs @@ -1,5 +1,7 @@ -use crate::{assets, comp::Player, state::State}; +use crate::{assets, comp, npc, state::State}; +use lazy_static::lazy_static; use specs::prelude::{Join, WorldExt}; +use std::{ops::Deref, str::FromStr}; /// Struct representing a command that a user can run from server chat. pub struct ChatCommandData { @@ -85,63 +87,108 @@ pub static CHAT_COMMANDS: &'static [ChatCommand] = &[ ChatCommand::Waypoint, ]; +lazy_static! { + static ref ALIGNMENTS: Vec<String> = vec!["wild", "enemy", "npc", "pet"] + .iter() + .map(|s| s.to_string()) + .collect(); + static ref ENTITIES: Vec<String> = { + let npc_names = &*npc::NPC_NAMES; + npc::ALL_NPCS + .iter() + .map(|&npc| npc_names[npc].keyword.clone()) + .collect() + }; + static ref OBJECTS: Vec<String> = comp::object::ALL_OBJECTS + .iter() + .map(|o| o.to_string().to_string()) + .collect(); + static ref TIMES: Vec<String> = vec![ + "midnight", "night", "dawn", "morning", "day", "noon", "dusk" + ] + .iter() + .map(|s| s.to_string()) + .collect(); +} +fn items() -> Vec<String> { + if let Ok(assets) = assets::ASSETS.read() { + assets + .iter() + .flat_map(|(k, v)| { + if v.is::<comp::item::Item>() { + Some(k.clone()) + } else { + None + } + }) + .collect() + } else { + error!("Assets not found"); + vec![] + } +} + impl ChatCommand { pub fn data(&self) -> ChatCommandData { use ArgumentSpec::*; + use Requirement::*; let cmd = ChatCommandData::new; match self { ChatCommand::Adminify => cmd( - vec![PlayerName(false)], + vec![PlayerName(Required)], "Temporarily gives a player admin permissions or removes them", true, ), - ChatCommand::Alias => cmd(vec![Any("name", false)], "Change your alias", false), + ChatCommand::Alias => cmd(vec![Any("name", Required)], "Change your alias", false), ChatCommand::Build => cmd(vec![], "Toggles build mode on and off", true), ChatCommand::Debug => cmd(vec![], "Place all debug items into your pack.", true), ChatCommand::DebugColumn => cmd( - vec![Float("x", f32::NAN, false), Float("y", f32::NAN, false)], + vec![ + Integer("x", 15000, Required), + Integer("y", 15000, Required), + ], "Prints some debug information about a column", false, ), ChatCommand::Explosion => cmd( - vec![Float("radius", 5.0, false)], + vec![Float("radius", 5.0, Required)], "Explodes the ground around you", true, ), ChatCommand::GiveExp => cmd( - vec![Integer("amount", 50, false)], + vec![Integer("amount", 50, Required)], "Give experience to yourself", true, ), ChatCommand::GiveItem => cmd( - vec![ItemSpec(false), Integer("num", 1, true)], + vec![Enum("item", items(), Required), Integer("num", 1, Optional)], "Give yourself some items", true, ), ChatCommand::Goto => cmd( vec![ - Float("x", 0.0, false), - Float("y", 0.0, false), - Float("z", 0.0, false), + Float("x", 0.0, Required), + Float("y", 0.0, Required), + Float("z", 0.0, Required), ], "Teleport to a position", true, ), ChatCommand::Health => cmd( - vec![Integer("hp", 100, false)], + vec![Integer("hp", 100, Required)], "Set your current health", true, ), ChatCommand::Help => ChatCommandData::new( - vec![Command(true)], + vec![Command(Optional)], "Display information about commands", false, ), ChatCommand::Jump => cmd( vec![ - Float("x", 0.0, false), - Float("y", 0.0, false), - Float("z", 0.0, false), + Float("x", 0.0, Required), + Float("y", 0.0, Required), + Float("z", 0.0, Required), ], "Offset your current position", true, @@ -150,50 +197,72 @@ impl ChatCommand { ChatCommand::KillNpcs => cmd(vec![], "Kill the NPCs", true), ChatCommand::Lantern => cmd( vec![ - Float("strength", 5.0, false), - Float("r", 1.0, true), - Float("g", 1.0, true), - Float("b", 1.0, true), + Float("strength", 5.0, Required), + Float("r", 1.0, Optional), + Float("g", 1.0, Optional), + Float("b", 1.0, Optional), ], "Change your lantern's strength and color", true, ), ChatCommand::Light => cmd( vec![ - Float("r", 1.0, true), - Float("g", 1.0, true), - Float("b", 1.0, true), - Float("x", 0.0, true), - Float("y", 0.0, true), - Float("z", 0.0, true), - Float("strength", 5.0, true), + Float("r", 1.0, Optional), + Float("g", 1.0, Optional), + Float("b", 1.0, Optional), + Float("x", 0.0, Optional), + Float("y", 0.0, Optional), + Float("z", 0.0, Optional), + Float("strength", 5.0, Optional), ], "Spawn entity with light", true, ), - ChatCommand::Object => cmd(vec![/*TODO*/], "Spawn an object", true), + ChatCommand::Object => cmd( + vec![Enum("object", OBJECTS.clone(), Required)], + "Spawn an object", + true, + ), ChatCommand::Players => cmd(vec![], "Lists players currently online", false), ChatCommand::RemoveLights => cmd( - vec![Float("radius", 20.0, true)], + vec![Float("radius", 20.0, Optional)], "Removes all lights spawned by players", true, ), - ChatCommand::SetLevel => { - cmd(vec![Integer("level", 10, false)], "Set player Level", true) - }, - ChatCommand::Spawn => cmd(vec![/*TODO*/], "Spawn a test entity", true), + ChatCommand::SetLevel => cmd( + vec![Integer("level", 10, Required)], + "Set player Level", + true, + ), + ChatCommand::Spawn => cmd( + vec![ + Enum("alignment", ALIGNMENTS.clone(), Required), + Enum("entity", ENTITIES.clone(), Required), + Integer("amount", 1, Optional), + ], + "Spawn a test entity", + true, + ), ChatCommand::Sudo => cmd( - vec![PlayerName(false), SubCommand], + vec![PlayerName(Required), SubCommand], "Run command as if you were another player", true, ), ChatCommand::Tell => cmd( - vec![PlayerName(false), Message], + vec![PlayerName(Required), Message], "Send a message to another player", false, ), - ChatCommand::Time => cmd(vec![/*TODO*/], "Set the time of day", true), - ChatCommand::Tp => cmd(vec![PlayerName(true)], "Teleport to another player", true), + ChatCommand::Time => cmd( + vec![Enum("time", TIMES.clone(), Optional)], + "Set the time of day", + true, + ), + ChatCommand::Tp => cmd( + vec![PlayerName(Optional)], + "Teleport to another player", + true, + ), ChatCommand::Version => cmd(vec![], "Prints server version", false), ChatCommand::Waypoint => { cmd(vec![], "Set your waypoint to your current position", true) @@ -250,21 +319,20 @@ impl ChatCommand { .iter() .map(|arg| match arg { ArgumentSpec::PlayerName(_) => "{}", - ArgumentSpec::ItemSpec(_) => "{}", ArgumentSpec::Float(_, _, _) => "{}", ArgumentSpec::Integer(_, _, _) => "{d}", ArgumentSpec::Any(_, _) => "{}", ArgumentSpec::Command(_) => "{}", ArgumentSpec::Message => "{/.*/}", ArgumentSpec::SubCommand => "{} {/.*/}", - ArgumentSpec::OneOf(_, _, _, _) => "{}", // TODO + ArgumentSpec::Enum(_, _, _) => "{}", // TODO }) .collect::<Vec<_>>() .join(" ") } } -impl std::str::FromStr for ChatCommand { +impl FromStr for ChatCommand { type Err = (); fn from_str(keyword: &str) -> Result<ChatCommand, ()> { @@ -282,26 +350,39 @@ impl std::str::FromStr for ChatCommand { } } +pub enum Requirement { + Required, + Optional, +} +impl Deref for Requirement { + type Target = bool; + + fn deref(&self) -> &bool { + match self { + Requirement::Required => &true, + Requirement::Optional => &false, + } + } +} + /// Representation for chat command arguments pub enum ArgumentSpec { /// The argument refers to a player by alias - PlayerName(bool), - /// The argument refers to an item asset by path - ItemSpec(bool), + PlayerName(Requirement), /// The argument is a float. The associated values are /// * label - /// * default tab-completion + /// * suggested tab-completion /// * whether it's optional - Float(&'static str, f32, bool), + Float(&'static str, f32, Requirement), /// The argument is a float. The associated values are /// * label - /// * default tab-completion + /// * suggested tab-completion /// * whether it's optional - Integer(&'static str, i32, bool), + Integer(&'static str, i32, Requirement), /// The argument is any string that doesn't contain spaces - Any(&'static str, bool), - /// The argument is a command name - Command(bool), + Any(&'static str, Requirement), + /// The argument is a command name (such as in /help) + Command(Requirement), /// This is the final argument, consuming all characters until the end of /// input. Message, @@ -310,68 +391,55 @@ pub enum ArgumentSpec { /// The argument is likely an enum. The associated values are /// * label /// * Predefined string completions - /// * Other completion types /// * whether it's optional - OneOf( - &'static str, - &'static [&'static str], - Vec<Box<ArgumentSpec>>, - bool, - ), + Enum(&'static str, Vec<String>, Requirement), } impl ArgumentSpec { pub fn usage_string(&self) -> String { match self { - ArgumentSpec::PlayerName(optional) => { - if *optional { - "[player]".to_string() - } else { + ArgumentSpec::PlayerName(req) => { + if **req { "<player>".to_string() + } else { + "[player]".to_string() } }, - ArgumentSpec::ItemSpec(optional) => { - if *optional { - "[item]".to_string() - } else { - "<item>".to_string() - } - }, - ArgumentSpec::Float(label, _, optional) => { - if *optional { - format!("[{}]", label) - } else { + ArgumentSpec::Float(label, _, req) => { + if **req { format!("<{}>", label) - } - }, - ArgumentSpec::Integer(label, _, optional) => { - if *optional { + } else { format!("[{}]", label) - } else { - format!("<{}>", label) } }, - ArgumentSpec::Any(label, optional) => { - if *optional { + ArgumentSpec::Integer(label, _, req) => { + if **req { + format!("<{}>", label) + } else { format!("[{}]", label) - } else { - format!("<{}>", label) } }, - ArgumentSpec::Command(optional) => { - if *optional { - "[[/]command]".to_string() + ArgumentSpec::Any(label, req) => { + if **req { + format!("<{}>", label) } else { + format!("[{}]", label) + } + }, + ArgumentSpec::Command(req) => { + if **req { "<[/]command>".to_string() + } else { + "[[/]command]".to_string() } }, ArgumentSpec::Message => "<message>".to_string(), ArgumentSpec::SubCommand => "<[/]command> [args...]".to_string(), - ArgumentSpec::OneOf(label, _, _, optional) => { - if *optional { - format! {"[{}]", label} - } else { + ArgumentSpec::Enum(label, _, req) => { + if **req { format! {"<{}>", label} + } else { + format! {"[{}]", label} } }, } @@ -380,33 +448,35 @@ impl ArgumentSpec { pub fn complete(&self, part: &str, state: &State) -> Vec<String> { match self { ArgumentSpec::PlayerName(_) => complete_player(part, &state), - ArgumentSpec::ItemSpec(_) => assets::iterate() - .filter(|asset| asset.starts_with(part)) - .map(|c| c.to_string()) - .collect(), - ArgumentSpec::Float(_, x, _) => vec![format!("{}", x)], - ArgumentSpec::Integer(_, x, _) => vec![format!("{}", x)], + ArgumentSpec::Float(_, x, _) => { + if part.is_empty() { + vec![format!("{:.1}", x)] + } else { + vec![] + } + }, + ArgumentSpec::Integer(_, x, _) => { + if part.is_empty() { + vec![format!("{}", x)] + } else { + vec![] + } + }, ArgumentSpec::Any(_, _) => vec![], ArgumentSpec::Command(_) => complete_command(part), ArgumentSpec::Message => complete_player(part, &state), ArgumentSpec::SubCommand => complete_command(part), - ArgumentSpec::OneOf(_, strings, alts, _) => { - let string_completions = strings - .iter() - .filter(|string| string.starts_with(part)) - .map(|c| c.to_string()); - let alt_completions = alts - .iter() - .flat_map(|b| (*b).complete(part, &state)) - .map(|c| c.to_string()); - string_completions.chain(alt_completions).collect() - }, + ArgumentSpec::Enum(_, strings, _) => strings + .iter() + .filter(|string| string.starts_with(part)) + .map(|c| c.to_string()) + .collect(), } } } fn complete_player(part: &str, state: &State) -> Vec<String> { - let storage = state.ecs().read_storage::<Player>(); + let storage = state.ecs().read_storage::<comp::Player>(); let mut iter = storage.join(); if let Some(first) = iter.next() { std::iter::once(first) diff --git a/common/src/comp/body/object.rs b/common/src/comp/body/object.rs index d179884062..e3121eb9a5 100644 --- a/common/src/comp/body/object.rs +++ b/common/src/comp/body/object.rs @@ -65,7 +65,7 @@ impl Body { } } -const ALL_OBJECTS: [Body; 52] = [ +pub const ALL_OBJECTS: [Body; 53] = [ Body::Arrow, Body::Bomb, Body::Scarecrow, @@ -114,8 +114,69 @@ const ALL_OBJECTS: [Body; 52] = [ Body::CarpetHumanSquare, Body::CarpetHumanSquare2, Body::CarpetHumanSquircle, + Body::Pouch, Body::CraftingBench, Body::BoltFire, Body::BoltFireBig, Body::ArrowSnake, ]; + +impl Body { + pub fn to_string(&self) -> &str { + match self { + Body::Arrow => "arrow", + Body::Bomb => "bomb", + Body::Scarecrow => "scarecrow", + Body::Cauldron => "cauldron", + Body::ChestVines => "chest_vines", + Body::Chest => "chest", + Body::ChestDark => "chest_dark", + Body::ChestDemon => "chest_demon", + Body::ChestGold => "chest_gold", + Body::ChestLight => "chest_light", + Body::ChestOpen => "chest_open", + Body::ChestSkull => "chest_skull", + Body::Pumpkin => "pumpkin", + Body::Pumpkin2 => "pumpkin_2", + Body::Pumpkin3 => "pumpkin_3", + Body::Pumpkin4 => "pumpkin_4", + Body::Pumpkin5 => "pumpkin_5", + Body::Campfire => "campfire", + Body::CampfireLit => "campfire_lit", + Body::LanternGround => "lantern_ground", + Body::LanternGroundOpen => "lantern_ground_open", + Body::LanternStanding => "lantern_standing", + Body::LanternStanding2 => "lantern_standing_2", + Body::PotionRed => "potion_red", + Body::PotionBlue => "potion_blue", + Body::PotionGreen => "potion_green", + Body::Crate => "crate", + Body::Tent => "tent", + Body::WindowSpooky => "window_spooky", + Body::DoorSpooky => "door_spooky", + Body::Anvil => "anvil", + Body::Gravestone => "gravestone", + Body::Gravestone2 => "gravestone_2", + Body::Bench => "bench", + Body::Chair => "chair", + Body::Chair2 => "chair_2", + Body::Chair3 => "chair_3", + Body::Table => "table", + Body::Table2 => "table_2", + Body::Table3 => "table_3", + Body::Drawer => "drawer", + Body::BedBlue => "bed_blue", + Body::Carpet => "carpet", + Body::Bedroll => "bedroll", + Body::CarpetHumanRound => "carpet_human_round", + Body::CarpetHumanSquare => "carpet_human_square", + Body::CarpetHumanSquare2 => "carpet_human_square_2", + Body::CarpetHumanSquircle => "carpet_human_squircle", + Body::Pouch => "pouch", + Body::CraftingBench => "crafting_bench", + Body::BoltFire => "bolt_fire", + Body::BoltFireBig => "bolt_fire_big", + Body::ArrowSnake => "arrow_snake", + } + } +} diff --git a/server/src/cmd.rs b/server/src/cmd.rs index 26a6263484..bb86e079f3 100644 --- a/server/src/cmd.rs +++ b/server/src/cmd.rs @@ -78,6 +78,7 @@ fn get_handler(cmd: &ChatCommand) -> CommandHandler { ChatCommand::Waypoint => handle_waypoint, } } + fn handle_give_item( server: &mut Server, client: EcsEntity, @@ -616,85 +617,39 @@ fn handle_object( .with(ori);*/ if let (Some(pos), Some(ori)) = (pos, ori) { let obj_str_res = obj_type.as_ref().map(String::as_str); - let obj_type = match obj_str_res { - Ok("scarecrow") => comp::object::Body::Scarecrow, - Ok("cauldron") => comp::object::Body::Cauldron, - Ok("chest_vines") => comp::object::Body::ChestVines, - Ok("chest") => comp::object::Body::Chest, - Ok("chest_dark") => comp::object::Body::ChestDark, - Ok("chest_demon") => comp::object::Body::ChestDemon, - Ok("chest_gold") => comp::object::Body::ChestGold, - Ok("chest_light") => comp::object::Body::ChestLight, - Ok("chest_open") => comp::object::Body::ChestOpen, - Ok("chest_skull") => comp::object::Body::ChestSkull, - Ok("pumpkin") => comp::object::Body::Pumpkin, - Ok("pumpkin_2") => comp::object::Body::Pumpkin2, - Ok("pumpkin_3") => comp::object::Body::Pumpkin3, - Ok("pumpkin_4") => comp::object::Body::Pumpkin4, - Ok("pumpkin_5") => comp::object::Body::Pumpkin5, - Ok("campfire") => comp::object::Body::Campfire, - Ok("campfire_lit") => comp::object::Body::CampfireLit, - Ok("lantern_ground") => comp::object::Body::LanternGround, - Ok("lantern_ground_open") => comp::object::Body::LanternGroundOpen, - Ok("lantern_2") => comp::object::Body::LanternStanding2, - Ok("lantern") => comp::object::Body::LanternStanding, - Ok("potion_blue") => comp::object::Body::PotionBlue, - Ok("potion_green") => comp::object::Body::PotionGreen, - Ok("potion_red") => comp::object::Body::PotionRed, - Ok("crate") => comp::object::Body::Crate, - Ok("tent") => comp::object::Body::Tent, - Ok("bomb") => comp::object::Body::Bomb, - Ok("window_spooky") => comp::object::Body::WindowSpooky, - Ok("door_spooky") => comp::object::Body::DoorSpooky, - Ok("carpet") => comp::object::Body::Carpet, - Ok("table_human") => comp::object::Body::Table, - Ok("table_human_2") => comp::object::Body::Table2, - Ok("table_human_3") => comp::object::Body::Table3, - Ok("drawer") => comp::object::Body::Drawer, - Ok("bed_human_blue") => comp::object::Body::BedBlue, - Ok("anvil") => comp::object::Body::Anvil, - Ok("gravestone") => comp::object::Body::Gravestone, - Ok("gravestone_2") => comp::object::Body::Gravestone2, - Ok("chair") => comp::object::Body::Chair, - Ok("chair_2") => comp::object::Body::Chair2, - Ok("chair_3") => comp::object::Body::Chair3, - Ok("bench_human") => comp::object::Body::Bench, - Ok("bedroll") => comp::object::Body::Bedroll, - Ok("carpet_human_round") => comp::object::Body::CarpetHumanRound, - Ok("carpet_human_square") => comp::object::Body::CarpetHumanSquare, - Ok("carpet_human_square_2") => comp::object::Body::CarpetHumanSquare2, - Ok("carpet_human_squircle") => comp::object::Body::CarpetHumanSquircle, - Ok("crafting_bench") => comp::object::Body::CraftingBench, - _ => { - return server.notify_client( - client, - ServerMsg::private(String::from("Object not found!")), - ); - }, - }; - server - .state - .create_object(pos, obj_type) - .with(comp::Ori( - // converts player orientation into a 90° rotation for the object by using the axis - // with the highest value - Dir::from_unnormalized(ori.0.map(|e| { - if e.abs() == ori.0.map(|e| e.abs()).reduce_partial_max() { - e - } else { - 0.0 - } - })) - .unwrap_or_default(), - )) - .build(); - server.notify_client( - client, - ServerMsg::private(format!( - "Spawned: {}", - obj_str_res.unwrap_or("<Unknown object>") - )), - ); + if let Some(obj_type) = comp::object::ALL_OBJECTS + .iter() + .find(|o| Ok(o.to_string()) == obj_str_res) + { + server + .state + .create_object(pos, *obj_type) + .with(comp::Ori( + // converts player orientation into a 90° rotation for the object by using the + // axis with the highest value + Dir::from_unnormalized(ori.0.map(|e| { + if e.abs() == ori.0.map(|e| e.abs()).reduce_partial_max() { + e + } else { + 0.0 + } + })) + .unwrap_or_default(), + )) + .build(); + server.notify_client( + client, + ServerMsg::private(format!( + "Spawned: {}", + obj_str_res.unwrap_or("<Unknown object>") + )), + ); + } else { + return server.notify_client( + client, + ServerMsg::private(String::from("Object not found!")), + ); + } } else { server.notify_client(client, ServerMsg::private(format!("You have no position!"))); } diff --git a/voxygen/src/hud/chat.rs b/voxygen/src/hud/chat.rs index 50310bfe94..bdbb0d8e89 100644 --- a/voxygen/src/hud/chat.rs +++ b/voxygen/src/hud/chat.rs @@ -29,8 +29,6 @@ widget_ids! { } const MAX_MESSAGES: usize = 100; -// Maximum completions shown at once -const MAX_COMPLETIONS: usize = 10; #[derive(WidgetCommon)] pub struct Chat<'a> { @@ -456,21 +454,6 @@ fn do_tab_completion(cursor: usize, input: &str, word: &str) -> (String, usize) } } -fn cursor_index_to_offset( - index: text::cursor::Index, - text: &str, - ui: &Ui, - fonts: &ConrodVoxygenFonts, -) -> Option<usize> { - // Width and font must match that of the chat TextEdit - let width = 460.0; - let font = ui.fonts.get(fonts.opensans.conrod_id)?; - let font_size = fonts.opensans.scale(15); - let infos = text::line::infos(&text, &font, font_size).wrap_by_whitespace(width); - - text::glyph::index_after_cursor(infos, index) -} - fn cursor_offset_to_index( offset: usize, text: &str, From 9d118b55a03e0866b30ad699121ffd20df9ca46b Mon Sep 17 00:00:00 2001 From: CapsizeGlimmer <> Date: Sat, 9 May 2020 16:41:29 -0400 Subject: [PATCH 8/9] Fixed player list tab completion --- client/src/cmd.rs | 121 +++++++++++++++++++++++++++++++ client/src/lib.rs | 1 + common/src/assets/mod.rs | 27 ++++++- common/src/cmd.rs | 149 ++------------------------------------- server/src/cmd.rs | 1 - voxygen/src/hud/chat.rs | 6 +- voxygen/src/hud/mod.rs | 2 +- 7 files changed, 158 insertions(+), 149 deletions(-) create mode 100644 client/src/cmd.rs diff --git a/client/src/cmd.rs b/client/src/cmd.rs new file mode 100644 index 0000000000..9b05f9b779 --- /dev/null +++ b/client/src/cmd.rs @@ -0,0 +1,121 @@ +use crate::Client; +use common::cmd::*; + +trait TabComplete { + fn complete(&self, part: &str, client: &Client) -> Vec<String>; +} + +impl TabComplete for ArgumentSpec { + fn complete(&self, part: &str, client: &Client) -> Vec<String> { + match self { + ArgumentSpec::PlayerName(_) => complete_player(part, &client), + ArgumentSpec::Float(_, x, _) => { + if part.is_empty() { + vec![format!("{:.1}", x)] + } else { + vec![] + } + }, + ArgumentSpec::Integer(_, x, _) => { + if part.is_empty() { + vec![format!("{}", x)] + } else { + vec![] + } + }, + ArgumentSpec::Any(_, _) => vec![], + ArgumentSpec::Command(_) => complete_command(part), + ArgumentSpec::Message => complete_player(part, &client), + ArgumentSpec::SubCommand => complete_command(part), + ArgumentSpec::Enum(_, strings, _) => strings + .iter() + .filter(|string| string.starts_with(part)) + .map(|c| c.to_string()) + .collect(), + } + } +} + +fn complete_player(part: &str, client: &Client) -> Vec<String> { + client + .player_list + .values() + .filter(|alias| alias.starts_with(part)) + .cloned() + .collect() +} + +fn complete_command(part: &str) -> Vec<String> { + CHAT_COMMANDS + .iter() + .map(|com| com.keyword()) + .filter(|kwd| kwd.starts_with(part) || format!("/{}", kwd).starts_with(part)) + .map(|c| format!("/{}", c)) + .collect() +} + +// Get the byte index of the nth word. Used in completing "/sudo p /subcmd" +fn nth_word(line: &str, n: usize) -> Option<usize> { + let mut is_space = false; + let mut j = 0; + for (i, c) in line.char_indices() { + match (is_space, c.is_whitespace()) { + (true, true) => {}, + (true, false) => { + is_space = false; + j += 1; + }, + (false, true) => { + is_space = true; + }, + (false, false) => {}, + } + if j == n { + return Some(i); + } + } + return None; +} + +pub fn complete(line: &str, client: &Client) -> Vec<String> { + let word = if line.chars().last().map_or(true, char::is_whitespace) { + "" + } else { + line.split_whitespace().last().unwrap_or("") + }; + if line.chars().next() == Some('/') { + let mut iter = line.split_whitespace(); + let cmd = iter.next().unwrap(); + let i = iter.count() + if word.is_empty() { 1 } else { 0 }; + if i == 0 { + // Completing chat command name + complete_command(word) + } else { + if let Ok(cmd) = cmd.parse::<ChatCommand>() { + if let Some(arg) = cmd.data().args.get(i - 1) { + // Complete ith argument + arg.complete(word, &client) + } else { + // Complete past the last argument + match cmd.data().args.last() { + Some(ArgumentSpec::SubCommand) => { + if let Some(index) = nth_word(line, cmd.data().args.len()) { + complete(&line[index..], &client) + } else { + vec![] + } + }, + Some(ArgumentSpec::Message) => complete_player(word, &client), + _ => vec![], // End of command. Nothing to complete + } + } + } else { + // Completing for unknown chat command + complete_player(word, &client) + } + } + } else { + // Not completing a command + complete_player(word, &client) + } +} diff --git a/client/src/lib.rs b/client/src/lib.rs index df4b3bd8bb..3c57766420 100644 --- a/client/src/lib.rs +++ b/client/src/lib.rs @@ -1,6 +1,7 @@ #![deny(unsafe_code)] #![feature(label_break_value)] +pub mod cmd; pub mod error; // Reexports diff --git a/common/src/assets/mod.rs b/common/src/assets/mod.rs index 2810ed50eb..3f2ba97805 100644 --- a/common/src/assets/mod.rs +++ b/common/src/assets/mod.rs @@ -12,7 +12,7 @@ use std::{ fmt, fs::{self, File, ReadDir}, io::{BufReader, Read}, - path::PathBuf, + path::{Path, PathBuf}, sync::{Arc, RwLock}, }; @@ -59,6 +59,31 @@ lazy_static! { /// The HashMap where all loaded assets are stored in. pub static ref ASSETS: RwLock<HashMap<String, Arc<dyn Any + 'static + Sync + Send>>> = RwLock::new(HashMap::new()); + + /// List of item specifiers. Used for tab completing + pub static ref ITEM_SPECS: Vec<String> = { + let base = ASSETS_PATH.join("common").join("items"); + let mut items = vec![]; + fn list_items (path: &Path, base: &Path, mut items: &mut Vec<String>) -> std::io::Result<()>{ + for entry in std::fs::read_dir(path)? { + let path = entry?.path(); + if path.is_dir(){ + list_items(&path, &base, &mut items)?; + } else { + if let Ok(path) = path.strip_prefix(base) { + let path = path.to_string_lossy().trim_end_matches(".ron").replace('/', "."); + items.push(path); + } + } + } + Ok(()) + } + if list_items(&base, &ASSETS_PATH, &mut items).is_err() { + warn!("There was a problem listing some item assets"); + } + items.sort(); + items + }; } // TODO: Remove this function. It's only used in world/ in a really ugly way.To diff --git a/common/src/cmd.rs b/common/src/cmd.rs index ddbe7faec1..be265b5203 100644 --- a/common/src/cmd.rs +++ b/common/src/cmd.rs @@ -1,6 +1,5 @@ -use crate::{assets, comp, npc, state::State}; +use crate::{assets, comp, npc}; use lazy_static::lazy_static; -use specs::prelude::{Join, WorldExt}; use std::{ops::Deref, str::FromStr}; /// Struct representing a command that a user can run from server chat. @@ -110,23 +109,6 @@ lazy_static! { .map(|s| s.to_string()) .collect(); } -fn items() -> Vec<String> { - if let Ok(assets) = assets::ASSETS.read() { - assets - .iter() - .flat_map(|(k, v)| { - if v.is::<comp::item::Item>() { - Some(k.clone()) - } else { - None - } - }) - .collect() - } else { - error!("Assets not found"); - vec![] - } -} impl ChatCommand { pub fn data(&self) -> ChatCommandData { @@ -143,10 +125,7 @@ impl ChatCommand { ChatCommand::Build => cmd(vec![], "Toggles build mode on and off", true), ChatCommand::Debug => cmd(vec![], "Place all debug items into your pack.", true), ChatCommand::DebugColumn => cmd( - vec![ - Integer("x", 15000, Required), - Integer("y", 15000, Required), - ], + vec![Integer("x", 15000, Required), Integer("y", 15000, Required)], "Prints some debug information about a column", false, ), @@ -161,7 +140,10 @@ impl ChatCommand { true, ), ChatCommand::GiveItem => cmd( - vec![Enum("item", items(), Required), Integer("num", 1, Optional)], + vec![ + Enum("item", assets::ITEM_SPECS.clone(), Required), + Integer("num", 1, Optional), + ], "Give yourself some items", true, ), @@ -444,123 +426,4 @@ impl ArgumentSpec { }, } } - - pub fn complete(&self, part: &str, state: &State) -> Vec<String> { - match self { - ArgumentSpec::PlayerName(_) => complete_player(part, &state), - ArgumentSpec::Float(_, x, _) => { - if part.is_empty() { - vec![format!("{:.1}", x)] - } else { - vec![] - } - }, - ArgumentSpec::Integer(_, x, _) => { - if part.is_empty() { - vec![format!("{}", x)] - } else { - vec![] - } - }, - ArgumentSpec::Any(_, _) => vec![], - ArgumentSpec::Command(_) => complete_command(part), - ArgumentSpec::Message => complete_player(part, &state), - ArgumentSpec::SubCommand => complete_command(part), - ArgumentSpec::Enum(_, strings, _) => strings - .iter() - .filter(|string| string.starts_with(part)) - .map(|c| c.to_string()) - .collect(), - } - } -} - -fn complete_player(part: &str, state: &State) -> Vec<String> { - let storage = state.ecs().read_storage::<comp::Player>(); - let mut iter = storage.join(); - if let Some(first) = iter.next() { - std::iter::once(first) - .chain(iter) - .filter(|player| player.alias.starts_with(part)) - .map(|player| player.alias.clone()) - .collect() - } else { - vec!["singleplayer".to_string()] - } -} - -fn complete_command(part: &str) -> Vec<String> { - CHAT_COMMANDS - .iter() - .map(|com| com.keyword()) - .filter(|kwd| kwd.starts_with(part) || format!("/{}", kwd).starts_with(part)) - .map(|c| format!("/{}", c)) - .collect() -} - -// Get the byte index of the nth word. Used in completing "/sudo p /subcmd" -fn nth_word(line: &str, n: usize) -> Option<usize> { - let mut is_space = false; - let mut j = 0; - for (i, c) in line.char_indices() { - match (is_space, c.is_whitespace()) { - (true, true) => {}, - (true, false) => { - is_space = false; - j += 1; - }, - (false, true) => { - is_space = true; - }, - (false, false) => {}, - } - if j == n { - return Some(i); - } - } - return None; -} - -pub fn complete(line: &str, state: &State) -> Vec<String> { - let word = if line.chars().last().map_or(true, char::is_whitespace) { - "" - } else { - line.split_whitespace().last().unwrap_or("") - }; - if line.chars().next() == Some('/') { - let mut iter = line.split_whitespace(); - let cmd = iter.next().unwrap(); - let i = iter.count() + if word.is_empty() { 1 } else { 0 }; - if i == 0 { - // Completing chat command name - complete_command(word) - } else { - if let Ok(cmd) = cmd.parse::<ChatCommand>() { - if let Some(arg) = cmd.data().args.get(i - 1) { - // Complete ith argument - arg.complete(word, &state) - } else { - // Complete past the last argument - match cmd.data().args.last() { - Some(ArgumentSpec::SubCommand) => { - if let Some(index) = nth_word(line, cmd.data().args.len()) { - complete(&line[index..], &state) - } else { - error!("Could not tab-complete SubCommand"); - vec![] - } - }, - Some(ArgumentSpec::Message) => complete_player(word, &state), - _ => vec![], // End of command. Nothing to complete - } - } - } else { - // Completing for unknown chat command - complete_player(word, &state) - } - } - } else { - // Not completing a command - complete_player(word, &state) - } } diff --git a/server/src/cmd.rs b/server/src/cmd.rs index bb86e079f3..0d1dca0958 100644 --- a/server/src/cmd.rs +++ b/server/src/cmd.rs @@ -724,7 +724,6 @@ fn handle_lantern( args: String, action: &ChatCommand, ) { - println!("args: '{}', fmt: '{}'", &args, &action.arg_fmt()); if let (Some(s), r, g, b) = scan_fmt_some!(&args, &action.arg_fmt(), f32, f32, f32, f32) { if let Some(light) = server .state diff --git a/voxygen/src/hud/chat.rs b/voxygen/src/hud/chat.rs index bdbb0d8e89..82b5d571eb 100644 --- a/voxygen/src/hud/chat.rs +++ b/voxygen/src/hud/chat.rs @@ -3,7 +3,7 @@ use super::{ META_COLOR, PRIVATE_COLOR, SAY_COLOR, TELL_COLOR, TEXT_COLOR, }; use crate::{ui::fonts::ConrodVoxygenFonts, GlobalState}; -use client::Event as ClientEvent; +use client::{cmd, Client, Event as ClientEvent}; use common::{msg::validate_chat_msg, ChatType}; use conrod_core::{ input::Key, @@ -68,9 +68,9 @@ impl<'a> Chat<'a> { } } - pub fn prepare_tab_completion(mut self, input: String, state: &common::state::State) -> Self { + pub fn prepare_tab_completion(mut self, input: String, client: &Client) -> Self { if let Some(index) = input.find('\t') { - self.force_completions = Some(common::cmd::complete(&input[..index], &state)); + self.force_completions = Some(cmd::complete(&input[..index], &client)); } else { self.force_completions = None; } diff --git a/voxygen/src/hud/mod.rs b/voxygen/src/hud/mod.rs index eff6563521..2bc2e85544 100644 --- a/voxygen/src/hud/mod.rs +++ b/voxygen/src/hud/mod.rs @@ -1727,7 +1727,7 @@ impl Hud { ) .and_then(self.force_chat_input.take(), |c, input| c.input(input)) .and_then(self.tab_complete.take(), |c, input| { - c.prepare_tab_completion(input, &client.state()) + c.prepare_tab_completion(input, &client) }) .and_then(self.force_chat_cursor.take(), |c, pos| c.cursor_pos(pos)) .set(self.ids.chat, ui_widgets) From 3f76d1d702e7e37b353e094614ca6b86842a1084 Mon Sep 17 00:00:00 2001 From: CapsizeGlimmer <> Date: Sat, 9 May 2020 21:17:03 -0400 Subject: [PATCH 9/9] Rework tp command - "/sudo player /tp" is short for "/sudo player /tp sudoer" --- CHANGELOG.md | 4 +++ common/src/assets/mod.rs | 2 +- server/src/cmd.rs | 78 ++++++++++++++++++++++------------------ 3 files changed, 48 insertions(+), 36 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1978b3b1ed..00cde7ef01 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -70,6 +70,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Cultists clothing - You can start the game by pressing "enter" from the character selection menu - Added server-side character saving +- Added tab completion in chat for player names and chat commands ### Changed @@ -87,6 +88,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Rewrote the humanoid skeleton to be more ideal for attack animations - Arrows can no longer hurt their owners - Increased overall character scale +- `/sudo player /tp` is short for `/sudo player /tp me` +- The `/object` command can create any object in comp::object::Body +- The `/help` command takes an optional argument. `/help /sudo` will show you information about only the sudo command. ### Removed diff --git a/common/src/assets/mod.rs b/common/src/assets/mod.rs index 3f2ba97805..003ec94a54 100644 --- a/common/src/assets/mod.rs +++ b/common/src/assets/mod.rs @@ -60,7 +60,7 @@ lazy_static! { pub static ref ASSETS: RwLock<HashMap<String, Arc<dyn Any + 'static + Sync + Send>>> = RwLock::new(HashMap::new()); - /// List of item specifiers. Used for tab completing + /// List of item specifiers. Useful for tab completing pub static ref ITEM_SPECS: Vec<String> = { let base = ASSETS_PATH.join("common").join("items"); let mut items = vec![]; diff --git a/server/src/cmd.rs b/server/src/cmd.rs index 0d1dca0958..9beaf292c3 100644 --- a/server/src/cmd.rs +++ b/server/src/cmd.rs @@ -86,7 +86,9 @@ fn handle_give_item( args: String, action: &ChatCommand, ) { - if let (Some(item_name), give_amount_opt) = scan_fmt_some!(&args, &action.arg_fmt(), String, u32) { + if let (Some(item_name), give_amount_opt) = + scan_fmt_some!(&args, &action.arg_fmt(), String, u32) + { let give_amount = give_amount_opt.unwrap_or(1); if let Ok(item) = assets::load_cloned(&item_name) { let mut item: Item = item; @@ -145,7 +147,10 @@ fn handle_give_item( ); } } else { - server.notify_client(client, ServerMsg::private(String::from(action.help_string()))); + server.notify_client( + client, + ServerMsg::private(String::from(action.help_string())), + ); } } @@ -356,44 +361,47 @@ fn handle_tp( args: String, action: &ChatCommand, ) { - if let Ok(alias) = scan_fmt!(&args, &action.arg_fmt(), String) { + let opt_player = if let Some(alias) = scan_fmt_some!(&args, &action.arg_fmt(), String) { let ecs = server.state.ecs(); - let opt_player = (&ecs.entities(), &ecs.read_storage::<comp::Player>()) + (&ecs.entities(), &ecs.read_storage::<comp::Player>()) .join() .find(|(_, player)| player.alias == alias) - .map(|(entity, _)| entity); - match server.state.read_component_cloned::<comp::Pos>(target) { - Some(_pos) => match opt_player { - Some(player) => match server.state.read_component_cloned::<comp::Pos>(player) { - Some(pos) => { - server.state.write_component(target, pos); - server.state.write_component(target, comp::ForceUpdate); - }, - None => server.notify_client( - client, - ServerMsg::private(format!("Unable to teleport to player '{}'!", alias)), - ), - }, - None => { - server.notify_client( - client, - ServerMsg::private(format!("Player '{}' not found!", alias)), - ); - server.notify_client( - client, - ServerMsg::private(String::from(action.help_string())), - ); - }, - }, - None => { - server.notify_client(client, ServerMsg::private(format!("You have no position!"))); - }, + .map(|(entity, _)| entity) + } else { + if client != target { + Some(client) + } else { + server.notify_client( + client, + ServerMsg::private("You must specify a player name".to_string()), + ); + server.notify_client( + client, + ServerMsg::private(String::from(action.help_string())), + ); + return; + } + }; + if let Some(_pos) = server.state.read_component_cloned::<comp::Pos>(target) { + if let Some(player) = opt_player { + if let Some(pos) = server.state.read_component_cloned::<comp::Pos>(player) { + server.state.write_component(target, pos); + server.state.write_component(target, comp::ForceUpdate); + } else { + server.notify_client( + client, + ServerMsg::private(format!("Unable to teleport to player!")), + ); + } + } else { + server.notify_client(client, ServerMsg::private(format!("Player not found!"))); + server.notify_client( + client, + ServerMsg::private(String::from(action.help_string())), + ); } } else { - server.notify_client( - client, - ServerMsg::private(String::from(action.help_string())), - ); + server.notify_client(client, ServerMsg::private(format!("You have no position!"))); } }