From b486de28ac9b63ac7670621ac6d4c2b07fca8fd1 Mon Sep 17 00:00:00 2001 From: CapsizeGlimmer <> Date: Fri, 8 May 2020 22:42:51 -0400 Subject: [PATCH] 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 for Error { lazy_static! { /// The HashMap where all loaded assets are stored in. - static ref ASSETS: RwLock>> = + pub static ref ASSETS: RwLock>> = RwLock::new(HashMap::new()); } -const ASSETS_TMP: [&'static str; 1] = ["common/items/lantern/black_0"]; -pub fn iterate() -> impl Iterator { - // 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 = vec!["wild", "enemy", "npc", "pet"] + .iter() + .map(|s| s.to_string()) + .collect(); + static ref ENTITIES: Vec = { + let npc_names = &*npc::NPC_NAMES; + npc::ALL_NPCS + .iter() + .map(|&npc| npc_names[npc].keyword.clone()) + .collect() + }; + static ref OBJECTS: Vec = comp::object::ALL_OBJECTS + .iter() + .map(|o| o.to_string().to_string()) + .collect(); + static ref TIMES: Vec = vec![ + "midnight", "night", "dawn", "morning", "day", "noon", "dusk" + ] + .iter() + .map(|s| s.to_string()) + .collect(); +} +fn items() -> Vec { + if let Ok(assets) = assets::ASSETS.read() { + assets + .iter() + .flat_map(|(k, v)| { + if v.is::() { + 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::>() .join(" ") } } -impl std::str::FromStr for ChatCommand { +impl FromStr for ChatCommand { type Err = (); fn from_str(keyword: &str) -> Result { @@ -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>, - bool, - ), + Enum(&'static str, Vec, 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 { "".to_string() + } else { + "[player]".to_string() } }, - ArgumentSpec::ItemSpec(optional) => { - if *optional { - "[item]".to_string() - } else { - "".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 => "".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 { 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 { - let storage = state.ecs().read_storage::(); + let storage = state.ecs().read_storage::(); 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("") - )), - ); + 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("") + )), + ); + } 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 { - // 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,