Merge branch 'CapsizeGlimmer/tab_completion' into 'master'

Capsize glimmer/tab completion

See merge request veloren/veloren!972
This commit is contained in:
Forest Anderson 2020-05-11 17:49:26 +00:00
commit 28402e2bc1
11 changed files with 1066 additions and 458 deletions

View File

@ -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

121
client/src/cmd.rs Normal file
View File

@ -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)
}
}

View File

@ -1,6 +1,7 @@
#![deny(unsafe_code)]
#![feature(label_break_value)]
pub mod cmd;
pub mod error;
// Reexports

View File

@ -12,7 +12,7 @@ use std::{
fmt,
fs::{self, File, ReadDir},
io::{BufReader, Read},
path::PathBuf,
path::{Path, PathBuf},
sync::{Arc, RwLock},
};
@ -57,8 +57,33 @@ 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());
/// 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![];
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

429
common/src/cmd.rs Normal file
View File

@ -0,0 +1,429 @@
use crate::{assets, comp, npc};
use lazy_static::lazy_static;
use std::{ops::Deref, str::FromStr};
/// Struct representing a command that a user can run from server chat.
pub struct ChatCommandData {
/// A format string for parsing arguments.
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
/// administrator permissions or not.
pub needs_admin: bool,
}
impl ChatCommandData {
pub fn new(args: Vec<ArgumentSpec>, description: &'static str, needs_admin: bool) -> Self {
Self {
args,
description,
needs_admin,
}
}
}
// 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,
];
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();
}
impl ChatCommand {
pub fn data(&self) -> ChatCommandData {
use ArgumentSpec::*;
use Requirement::*;
let cmd = ChatCommandData::new;
match self {
ChatCommand::Adminify => cmd(
vec![PlayerName(Required)],
"Temporarily gives a player admin permissions or removes them",
true,
),
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![Integer("x", 15000, Required), Integer("y", 15000, Required)],
"Prints some debug information about a column",
false,
),
ChatCommand::Explosion => cmd(
vec![Float("radius", 5.0, Required)],
"Explodes the ground around you",
true,
),
ChatCommand::GiveExp => cmd(
vec![Integer("amount", 50, Required)],
"Give experience to yourself",
true,
),
ChatCommand::GiveItem => cmd(
vec![
Enum("item", assets::ITEM_SPECS.clone(), Required),
Integer("num", 1, Optional),
],
"Give yourself some items",
true,
),
ChatCommand::Goto => cmd(
vec![
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, Required)],
"Set your current health",
true,
),
ChatCommand::Help => ChatCommandData::new(
vec![Command(Optional)],
"Display information about commands",
false,
),
ChatCommand::Jump => cmd(
vec![
Float("x", 0.0, Required),
Float("y", 0.0, Required),
Float("z", 0.0, Required),
],
"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, 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, 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![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, Optional)],
"Removes all lights spawned by players",
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(Required), SubCommand],
"Run command as if you were another player",
true,
),
ChatCommand::Tell => cmd(
vec![PlayerName(Required), Message],
"Send a message to another player",
false,
),
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)
},
}
}
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::Float(_, _, _) => "{}",
ArgumentSpec::Integer(_, _, _) => "{d}",
ArgumentSpec::Any(_, _) => "{}",
ArgumentSpec::Command(_) => "{}",
ArgumentSpec::Message => "{/.*/}",
ArgumentSpec::SubCommand => "{} {/.*/}",
ArgumentSpec::Enum(_, _, _) => "{}", // TODO
})
.collect::<Vec<_>>()
.join(" ")
}
}
impl 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(());
}
}
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(Requirement),
/// The argument is a float. The associated values are
/// * label
/// * suggested tab-completion
/// * whether it's optional
Float(&'static str, f32, Requirement),
/// The argument is a float. The associated values are
/// * label
/// * suggested tab-completion
/// * whether it's optional
Integer(&'static str, i32, Requirement),
/// The argument is any string that doesn't contain spaces
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,
/// 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
/// * whether it's optional
Enum(&'static str, Vec<String>, Requirement),
}
impl ArgumentSpec {
pub fn usage_string(&self) -> String {
match self {
ArgumentSpec::PlayerName(req) => {
if **req {
"<player>".to_string()
} else {
"[player]".to_string()
}
},
ArgumentSpec::Float(label, _, req) => {
if **req {
format!("<{}>", label)
} else {
format!("[{}]", label)
}
},
ArgumentSpec::Integer(label, _, req) => {
if **req {
format!("<{}>", label)
} else {
format!("[{}]", label)
}
},
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::Enum(label, _, req) => {
if **req {
format! {"<{}>", label}
} else {
format! {"[{}]", label}
}
},
}
}
}

View File

@ -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",
}
}
}

View File

@ -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;

View File

@ -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,73 @@ 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 +147,10 @@ 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 +161,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 +184,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 +201,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 +239,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 +297,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 +326,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 +347,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,41 +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!")));
}
}
@ -596,7 +412,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 +475,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 +552,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 +576,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 +604,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
@ -802,85 +625,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!")));
}
@ -894,7 +671,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 +732,7 @@ 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) {
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 +764,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 +778,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 +842,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 +862,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 +890,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 +938,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 +969,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 +1033,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 +1056,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 +1090,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 +1170,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 +1222,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 +1230,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 +1251,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())),
);
}
}

View File

@ -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
)),
);
}
}

View File

@ -3,14 +3,17 @@ 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,
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,9 +21,10 @@ widget_ids! {
struct Ids {
message_box,
message_box_bg,
input,
input_bg,
chat_input,
chat_input_bg,
chat_arrow,
completion_box,
}
}
@ -31,6 +35,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,
@ -54,6 +59,7 @@ impl<'a> Chat<'a> {
new_messages,
force_input: None,
force_cursor: None,
force_completions: None,
imgs,
fonts,
global_state,
@ -62,6 +68,15 @@ impl<'a> Chat<'a> {
}
}
pub fn prepare_tab_completion(mut self, input: String, client: &Client) -> Self {
if let Some(index) = input.find('\t') {
self.force_completions = Some(cmd::complete(&input[..index], &client));
} else {
self.force_completions = None;
}
self
}
pub fn input(mut self, input: String) -> Self {
if let Ok(()) = validate_chat_msg(&input) {
self.force_input = Some(input);
@ -97,9 +112,15 @@ 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_index: Option<usize>,
// At which character is tab completion happening
completion_cursor: Option<usize>,
}
pub enum Event {
TabCompletionStart(String),
SendMessage(String),
Focus(Id),
}
@ -115,6 +136,9 @@ impl<'a> Widget for Chat<'a> {
messages: VecDeque::new(),
history: VecDeque::new(),
history_pos: 0,
completions: Vec::new(),
completions_index: None,
completion_cursor: None,
ids: Ids::new(id_gen),
}
}
@ -137,51 +161,106 @@ impl<'a> Widget for Chat<'a> {
}
});
// 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 {
Key::Up => (true, down),
Key::Down => (up, true),
_ => (up, down),
},
) {
(true, false) => {
if state.history_pos < state.history.len() {
state.update(|s| {
s.history_pos += 1;
s.input = s.history.get(s.history_pos - 1).unwrap().to_owned();
});
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
// 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 s.completion_cursor.is_some() {
s.completion_cursor = None;
}
},
(false, true) => {
if state.history_pos > 0 {
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| {
s.history_pos -= 1;
if s.history_pos > 0 {
s.input = s.history.get(s.history_pos - 1).unwrap().to_owned();
} else {
s.input.clear();
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;
}
} 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 =
cursor_offset_to_index(s.input.len(), &s.input, &ui, &self.fonts);
} else {
s.input.clear();
}
});
}
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)
@ -190,7 +269,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);
}
@ -202,11 +281,11 @@ 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');
@ -221,7 +300,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)
}
@ -298,14 +377,17 @@ 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))
// 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.
else if ui
.widget_input(state.ids.input)
.widget_input(state.ids.chat_input)
.presses()
.key()
.any(|key_press| match key_press.key {
@ -330,3 +412,64 @@ 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_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)
}

View File

@ -445,6 +445,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>,
@ -518,6 +519,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,
@ -1740,9 +1742,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)
})
.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));
},
@ -2312,6 +2320,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);
}