mirror of
https://gitlab.com/veloren/veloren.git
synced 2024-08-30 18:12:32 +00:00
Merge branch 'CapsizeGlimmer/tab_completion' into 'master'
Capsize glimmer/tab completion See merge request veloren/veloren!972
This commit is contained in:
commit
28402e2bc1
@ -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
121
client/src/cmd.rs
Normal 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)
|
||||
}
|
||||
}
|
@ -1,6 +1,7 @@
|
||||
#![deny(unsafe_code)]
|
||||
#![feature(label_break_value)]
|
||||
|
||||
pub mod cmd;
|
||||
pub mod error;
|
||||
|
||||
// Reexports
|
||||
|
@ -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
429
common/src/cmd.rs
Normal 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}
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
@ -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",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -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())),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
)),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user