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; +} + +impl TabComplete for ArgumentSpec { + fn complete(&self, part: &str, client: &Client) -> Vec { + 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 { + client + .player_list + .values() + .filter(|alias| alias.starts_with(part)) + .cloned() + .collect() +} + +fn complete_command(part: &str) -> Vec { + 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 { + 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 { + 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::() { + 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>> = RwLock::new(HashMap::new()); + + /// List of item specifiers. Used for tab completing + pub static ref ITEM_SPECS: Vec = { + let base = ASSETS_PATH.join("common").join("items"); + let mut items = vec![]; + fn list_items (path: &Path, base: &Path, mut items: &mut Vec) -> 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 { - 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 { @@ -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 { - 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 { - let storage = state.ecs().read_storage::(); - 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 { - 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 { - 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 { - 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::() { - 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)