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
|
- Cultists clothing
|
||||||
- You can start the game by pressing "enter" from the character selection menu
|
- You can start the game by pressing "enter" from the character selection menu
|
||||||
- Added server-side character saving
|
- Added server-side character saving
|
||||||
|
- Added tab completion in chat for player names and chat commands
|
||||||
|
|
||||||
### Changed
|
### 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
|
- Rewrote the humanoid skeleton to be more ideal for attack animations
|
||||||
- Arrows can no longer hurt their owners
|
- Arrows can no longer hurt their owners
|
||||||
- Increased overall character scale
|
- 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
|
### 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)]
|
#![deny(unsafe_code)]
|
||||||
#![feature(label_break_value)]
|
#![feature(label_break_value)]
|
||||||
|
|
||||||
|
pub mod cmd;
|
||||||
pub mod error;
|
pub mod error;
|
||||||
|
|
||||||
// Reexports
|
// Reexports
|
||||||
|
@ -12,7 +12,7 @@ use std::{
|
|||||||
fmt,
|
fmt,
|
||||||
fs::{self, File, ReadDir},
|
fs::{self, File, ReadDir},
|
||||||
io::{BufReader, Read},
|
io::{BufReader, Read},
|
||||||
path::PathBuf,
|
path::{Path, PathBuf},
|
||||||
sync::{Arc, RwLock},
|
sync::{Arc, RwLock},
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -57,8 +57,33 @@ impl From<std::io::Error> for Error {
|
|||||||
|
|
||||||
lazy_static! {
|
lazy_static! {
|
||||||
/// The HashMap where all loaded assets are stored in.
|
/// 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());
|
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
|
// 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::Arrow,
|
||||||
Body::Bomb,
|
Body::Bomb,
|
||||||
Body::Scarecrow,
|
Body::Scarecrow,
|
||||||
@ -114,8 +114,69 @@ const ALL_OBJECTS: [Body; 52] = [
|
|||||||
Body::CarpetHumanSquare,
|
Body::CarpetHumanSquare,
|
||||||
Body::CarpetHumanSquare2,
|
Body::CarpetHumanSquare2,
|
||||||
Body::CarpetHumanSquircle,
|
Body::CarpetHumanSquircle,
|
||||||
|
Body::Pouch,
|
||||||
Body::CraftingBench,
|
Body::CraftingBench,
|
||||||
Body::BoltFire,
|
Body::BoltFire,
|
||||||
Body::BoltFireBig,
|
Body::BoltFireBig,
|
||||||
Body::ArrowSnake,
|
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 astar;
|
||||||
pub mod character;
|
pub mod character;
|
||||||
pub mod clock;
|
pub mod clock;
|
||||||
|
pub mod cmd;
|
||||||
pub mod comp;
|
pub mod comp;
|
||||||
pub mod effect;
|
pub mod effect;
|
||||||
pub mod event;
|
pub mod event;
|
||||||
|
@ -5,7 +5,9 @@
|
|||||||
use crate::{Server, StateExt};
|
use crate::{Server, StateExt};
|
||||||
use chrono::{NaiveTime, Timelike};
|
use chrono::{NaiveTime, Timelike};
|
||||||
use common::{
|
use common::{
|
||||||
assets, comp,
|
assets,
|
||||||
|
cmd::{ChatCommand, CHAT_COMMANDS},
|
||||||
|
comp,
|
||||||
event::{EventBus, ServerEvent},
|
event::{EventBus, ServerEvent},
|
||||||
msg::{PlayerListUpdate, ServerMsg},
|
msg::{PlayerListUpdate, ServerMsg},
|
||||||
npc::{self, get_npc_name},
|
npc::{self, get_npc_name},
|
||||||
@ -20,274 +22,73 @@ use specs::{Builder, Entity as EcsEntity, Join, WorldExt};
|
|||||||
use vek::*;
|
use vek::*;
|
||||||
use world::util::Sampler;
|
use world::util::Sampler;
|
||||||
|
|
||||||
use lazy_static::lazy_static;
|
|
||||||
use log::error;
|
use log::error;
|
||||||
use scan_fmt::{scan_fmt, scan_fmt_some};
|
use scan_fmt::{scan_fmt, scan_fmt_some};
|
||||||
|
|
||||||
/// Struct representing a command that a user can run from server chat.
|
pub trait ChatCommandExt {
|
||||||
pub struct ChatCommand {
|
fn execute(&self, server: &mut Server, entity: EcsEntity, args: String);
|
||||||
/// 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),
|
|
||||||
}
|
}
|
||||||
|
impl ChatCommandExt for ChatCommand {
|
||||||
impl ChatCommand {
|
fn execute(&self, server: &mut Server, entity: EcsEntity, args: String) {
|
||||||
/// Creates a new chat command.
|
let cmd_data = self.data();
|
||||||
pub fn new(
|
if cmd_data.needs_admin && !server.entity_is_admin(entity) {
|
||||||
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(
|
server.notify_client(
|
||||||
entity,
|
entity,
|
||||||
ServerMsg::private(format!(
|
ServerMsg::private(format!(
|
||||||
"You don't have permission to use '/{}'.",
|
"You don't have permission to use '/{}'.",
|
||||||
self.keyword
|
self.keyword()
|
||||||
)),
|
)),
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
} else {
|
} else {
|
||||||
(self.handler)(server, entity, entity, args, self);
|
get_handler(self)(server, entity, entity, args, &self);
|
||||||
}
|
|
||||||
} else {
|
|
||||||
(self.handler)(server, entity, entity, args, self);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
lazy_static! {
|
type CommandHandler = fn(&mut Server, EcsEntity, EcsEntity, String, &ChatCommand);
|
||||||
/// Static list of chat commands available to the server.
|
fn get_handler(cmd: &ChatCommand) -> CommandHandler {
|
||||||
pub static ref CHAT_COMMANDS: Vec<ChatCommand> = vec![
|
match cmd {
|
||||||
ChatCommand::new(
|
ChatCommand::Adminify => handle_adminify,
|
||||||
"give_item",
|
ChatCommand::Alias => handle_alias,
|
||||||
"{} {d}",
|
ChatCommand::Build => handle_build,
|
||||||
"/give_item <path to item> [num]\n\
|
ChatCommand::Debug => handle_debug,
|
||||||
Example items: common/items/apple, common/items/debug/boost",
|
ChatCommand::DebugColumn => handle_debug_column,
|
||||||
true,
|
ChatCommand::Explosion => handle_explosion,
|
||||||
handle_give,),
|
ChatCommand::GiveExp => handle_give_exp,
|
||||||
ChatCommand::new(
|
ChatCommand::GiveItem => handle_give_item,
|
||||||
"jump",
|
ChatCommand::Goto => handle_goto,
|
||||||
"{d} {d} {d}",
|
ChatCommand::Health => handle_health,
|
||||||
"/jump <dx> <dy> <dz> : Offset your current position",
|
ChatCommand::Help => handle_help,
|
||||||
true,
|
ChatCommand::Jump => handle_jump,
|
||||||
handle_jump,
|
ChatCommand::Kill => handle_kill,
|
||||||
),
|
ChatCommand::KillNpcs => handle_kill_npcs,
|
||||||
ChatCommand::new(
|
ChatCommand::Lantern => handle_lantern,
|
||||||
"goto",
|
ChatCommand::Light => handle_light,
|
||||||
"{d} {d} {d}",
|
ChatCommand::Object => handle_object,
|
||||||
"/goto <x> <y> <z> : Teleport to a position",
|
ChatCommand::Players => handle_players,
|
||||||
true,
|
ChatCommand::RemoveLights => handle_remove_lights,
|
||||||
handle_goto,
|
ChatCommand::SetLevel => handle_set_level,
|
||||||
),
|
ChatCommand::Spawn => handle_spawn,
|
||||||
ChatCommand::new(
|
ChatCommand::Sudo => handle_sudo,
|
||||||
"alias",
|
ChatCommand::Tell => handle_tell,
|
||||||
"{}",
|
ChatCommand::Time => handle_time,
|
||||||
"/alias <name> : Change your alias",
|
ChatCommand::Tp => handle_tp,
|
||||||
false,
|
ChatCommand::Version => handle_version,
|
||||||
handle_alias,
|
ChatCommand::Waypoint => handle_waypoint,
|
||||||
),
|
}
|
||||||
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,
|
|
||||||
),
|
|
||||||
];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn handle_give(
|
fn handle_give_item(
|
||||||
server: &mut Server,
|
server: &mut Server,
|
||||||
client: EcsEntity,
|
client: EcsEntity,
|
||||||
target: EcsEntity,
|
target: EcsEntity,
|
||||||
args: String,
|
args: String,
|
||||||
action: &ChatCommand,
|
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);
|
let give_amount = give_amount_opt.unwrap_or(1);
|
||||||
if let Ok(item) = assets::load_cloned(&item_name) {
|
if let Ok(item) = assets::load_cloned(&item_name) {
|
||||||
let mut item: Item = item;
|
let mut item: Item = item;
|
||||||
@ -346,7 +147,10 @@ fn handle_give(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
} else {
|
} 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,
|
args: String,
|
||||||
action: &ChatCommand,
|
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) {
|
match server.state.read_component_cloned::<comp::Pos>(target) {
|
||||||
Some(current_pos) => {
|
Some(current_pos) => {
|
||||||
server
|
server
|
||||||
@ -380,7 +184,7 @@ fn handle_goto(
|
|||||||
args: String,
|
args: String,
|
||||||
action: &ChatCommand,
|
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
|
if server
|
||||||
.state
|
.state
|
||||||
.read_component_cloned::<comp::Pos>(target)
|
.read_component_cloned::<comp::Pos>(target)
|
||||||
@ -397,7 +201,10 @@ fn handle_goto(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
} else {
|
} 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,
|
args: String,
|
||||||
action: &ChatCommand,
|
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()) {
|
let new_time = match time.as_ref().map(|s| s.as_str()) {
|
||||||
Some("midnight") => NaiveTime::from_hms(0, 0, 0),
|
Some("midnight") => NaiveTime::from_hms(0, 0, 0),
|
||||||
Some("night") => NaiveTime::from_hms(20, 0, 0),
|
Some("night") => NaiveTime::from_hms(20, 0, 0),
|
||||||
@ -490,7 +297,7 @@ fn handle_health(
|
|||||||
args: String,
|
args: String,
|
||||||
action: &ChatCommand,
|
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
|
if let Some(stats) = server
|
||||||
.state
|
.state
|
||||||
.ecs()
|
.ecs()
|
||||||
@ -519,7 +326,7 @@ fn handle_alias(
|
|||||||
args: String,
|
args: String,
|
||||||
action: &ChatCommand,
|
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
|
server
|
||||||
.state
|
.state
|
||||||
.ecs_mut()
|
.ecs_mut()
|
||||||
@ -540,7 +347,10 @@ fn handle_alias(
|
|||||||
server.state.notify_registered_clients(msg);
|
server.state.notify_registered_clients(msg);
|
||||||
}
|
}
|
||||||
} else {
|
} 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,
|
args: String,
|
||||||
action: &ChatCommand,
|
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 ecs = server.state.ecs();
|
||||||
let opt_player = (&ecs.entities(), &ecs.read_storage::<comp::Player>())
|
(&ecs.entities(), &ecs.read_storage::<comp::Player>())
|
||||||
.join()
|
.join()
|
||||||
.find(|(_, player)| player.alias == alias)
|
.find(|(_, player)| player.alias == alias)
|
||||||
.map(|(entity, _)| entity);
|
.map(|(entity, _)| entity)
|
||||||
match server.state.read_component_cloned::<comp::Pos>(target) {
|
} else {
|
||||||
Some(_pos) => match opt_player {
|
if client != target {
|
||||||
Some(player) => match server.state.read_component_cloned::<comp::Pos>(player) {
|
Some(client)
|
||||||
Some(pos) => {
|
} 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, pos);
|
||||||
server.state.write_component(target, comp::ForceUpdate);
|
server.state.write_component(target, comp::ForceUpdate);
|
||||||
},
|
} else {
|
||||||
None => server.notify_client(
|
|
||||||
client,
|
|
||||||
ServerMsg::private(format!("Unable to teleport to player '{}'!", alias)),
|
|
||||||
),
|
|
||||||
},
|
|
||||||
None => {
|
|
||||||
server.notify_client(
|
server.notify_client(
|
||||||
client,
|
client,
|
||||||
ServerMsg::private(format!("Player '{}' not found!", alias)),
|
ServerMsg::private(format!("Unable to teleport to player!")),
|
||||||
);
|
);
|
||||||
server.notify_client(
|
|
||||||
client,
|
|
||||||
ServerMsg::private(String::from(action.help_string)),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
None => {
|
|
||||||
server.notify_client(client, ServerMsg::private(format!("You have no position!")));
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
server.notify_client(client, ServerMsg::private(String::from(action.help_string)));
|
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(format!("You have no position!")));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -596,7 +412,7 @@ fn handle_spawn(
|
|||||||
args: String,
|
args: String,
|
||||||
action: &ChatCommand,
|
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) => {
|
(Some(opt_align), Some(npc::NpcBody(id, mut body)), opt_amount) => {
|
||||||
if let Some(alignment) = parse_alignment(target, &opt_align) {
|
if let Some(alignment) = parse_alignment(target, &opt_align) {
|
||||||
let amount = opt_amount
|
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,
|
server: &mut Server,
|
||||||
client: EcsEntity,
|
client: EcsEntity,
|
||||||
_target: EcsEntity,
|
_target: EcsEntity,
|
||||||
_args: String,
|
args: String,
|
||||||
_action: &ChatCommand,
|
action: &ChatCommand,
|
||||||
) {
|
) {
|
||||||
|
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() {
|
for cmd in CHAT_COMMANDS.iter() {
|
||||||
if !cmd.needs_admin || server.entity_is_admin(client) {
|
if !cmd.needs_admin() || server.entity_is_admin(client) {
|
||||||
server.notify_client(client, ServerMsg::private(String::from(cmd.help_string)));
|
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,
|
server: &mut Server,
|
||||||
client: EcsEntity,
|
client: EcsEntity,
|
||||||
_target: EcsEntity,
|
_target: EcsEntity,
|
||||||
@ -781,9 +604,9 @@ fn handle_object(
|
|||||||
client: EcsEntity,
|
client: EcsEntity,
|
||||||
target: EcsEntity,
|
target: EcsEntity,
|
||||||
args: String,
|
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
|
let pos = server
|
||||||
.state
|
.state
|
||||||
@ -802,68 +625,16 @@ fn handle_object(
|
|||||||
.with(ori);*/
|
.with(ori);*/
|
||||||
if let (Some(pos), Some(ori)) = (pos, ori) {
|
if let (Some(pos), Some(ori)) = (pos, ori) {
|
||||||
let obj_str_res = obj_type.as_ref().map(String::as_str);
|
let obj_str_res = obj_type.as_ref().map(String::as_str);
|
||||||
let obj_type = match obj_str_res {
|
if let Some(obj_type) = comp::object::ALL_OBJECTS
|
||||||
Ok("scarecrow") => comp::object::Body::Scarecrow,
|
.iter()
|
||||||
Ok("cauldron") => comp::object::Body::Cauldron,
|
.find(|o| Ok(o.to_string()) == obj_str_res)
|
||||||
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
|
server
|
||||||
.state
|
.state
|
||||||
.create_object(pos, obj_type)
|
.create_object(pos, *obj_type)
|
||||||
.with(comp::Ori(
|
.with(comp::Ori(
|
||||||
// converts player orientation into a 90° rotation for the object by using the axis
|
// converts player orientation into a 90° rotation for the object by using the
|
||||||
// with the highest value
|
// axis with the highest value
|
||||||
Dir::from_unnormalized(ori.0.map(|e| {
|
Dir::from_unnormalized(ori.0.map(|e| {
|
||||||
if e.abs() == ori.0.map(|e| e.abs()).reduce_partial_max() {
|
if e.abs() == ori.0.map(|e| e.abs()).reduce_partial_max() {
|
||||||
e
|
e
|
||||||
@ -881,6 +652,12 @@ fn handle_object(
|
|||||||
obj_str_res.unwrap_or("<Unknown object>")
|
obj_str_res.unwrap_or("<Unknown object>")
|
||||||
)),
|
)),
|
||||||
);
|
);
|
||||||
|
} else {
|
||||||
|
return server.notify_client(
|
||||||
|
client,
|
||||||
|
ServerMsg::private(String::from("Object not found!")),
|
||||||
|
);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
server.notify_client(client, ServerMsg::private(format!("You have no position!")));
|
server.notify_client(client, ServerMsg::private(format!("You have no position!")));
|
||||||
}
|
}
|
||||||
@ -894,7 +671,7 @@ fn handle_light(
|
|||||||
action: &ChatCommand,
|
action: &ChatCommand,
|
||||||
) {
|
) {
|
||||||
let (opt_r, opt_g, opt_b, opt_x, opt_y, opt_z, opt_s) =
|
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_emitter = comp::LightEmitter::default();
|
||||||
let mut light_offset_opt = None;
|
let mut light_offset_opt = None;
|
||||||
@ -955,7 +732,7 @@ fn handle_lantern(
|
|||||||
args: String,
|
args: String,
|
||||||
action: &ChatCommand,
|
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
|
if let Some(light) = server
|
||||||
.state
|
.state
|
||||||
.ecs()
|
.ecs()
|
||||||
@ -987,7 +764,10 @@ fn handle_lantern(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
} else {
|
} 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,
|
args: String,
|
||||||
action: &ChatCommand,
|
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 {
|
if power > 512.0 {
|
||||||
server.notify_client(
|
server.notify_client(
|
||||||
@ -1062,7 +842,7 @@ fn handle_adminify(
|
|||||||
args: String,
|
args: String,
|
||||||
action: &ChatCommand,
|
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 ecs = server.state.ecs();
|
||||||
let opt_player = (&ecs.entities(), &ecs.read_storage::<comp::Player>())
|
let opt_player = (&ecs.entities(), &ecs.read_storage::<comp::Player>())
|
||||||
.join()
|
.join()
|
||||||
@ -1082,11 +862,17 @@ fn handle_adminify(
|
|||||||
client,
|
client,
|
||||||
ServerMsg::private(format!("Player '{}' not found!", alias)),
|
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 {
|
} 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;
|
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 ecs = server.state.ecs();
|
||||||
let msg = &args[alias.len()..args.len()];
|
let msg = &args[alias.len()..args.len()];
|
||||||
if let Some(player) = (&ecs.entities(), &ecs.read_storage::<comp::Player>())
|
if let Some(player) = (&ecs.entities(), &ecs.read_storage::<comp::Player>())
|
||||||
@ -1152,7 +938,10 @@ fn handle_tell(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
} else {
|
} 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 sim = server.world.sim();
|
||||||
let sampler = server.world.sample_columns();
|
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 wpos = Vec2::new(x, y);
|
||||||
/* let chunk_pos = wpos.map2(TerrainChunkSize::RECT_SIZE, |e, sz: u32| {
|
/* let chunk_pos = wpos.map2(TerrainChunkSize::RECT_SIZE, |e, sz: u32| {
|
||||||
e / sz as i32
|
e / sz as i32
|
||||||
@ -1244,7 +1033,10 @@ spawn_rate {:?} "#,
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
} else {
|
} 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,
|
server: &mut Server,
|
||||||
client: EcsEntity,
|
client: EcsEntity,
|
||||||
target: EcsEntity,
|
target: EcsEntity,
|
||||||
args: String,
|
args: String,
|
||||||
action: &ChatCommand,
|
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 {
|
if let Some(exp) = a_exp {
|
||||||
let ecs = server.state.ecs_mut();
|
let ecs = server.state.ecs_mut();
|
||||||
@ -1298,14 +1090,14 @@ fn handle_exp(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn handle_level(
|
fn handle_set_level(
|
||||||
server: &mut Server,
|
server: &mut Server,
|
||||||
client: EcsEntity,
|
client: EcsEntity,
|
||||||
target: EcsEntity,
|
target: EcsEntity,
|
||||||
args: String,
|
args: String,
|
||||||
action: &ChatCommand,
|
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 {
|
if let Some(lvl) = a_lvl {
|
||||||
let ecs = server.state.ecs_mut();
|
let ecs = server.state.ecs_mut();
|
||||||
@ -1378,7 +1170,7 @@ fn handle_remove_lights(
|
|||||||
args: String,
|
args: String,
|
||||||
action: &ChatCommand,
|
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 opt_player_pos = server.state.read_component_cloned::<comp::Pos>(target);
|
||||||
let mut to_delete = vec![];
|
let mut to_delete = vec![];
|
||||||
|
|
||||||
@ -1430,7 +1222,7 @@ fn handle_sudo(
|
|||||||
action: &ChatCommand,
|
action: &ChatCommand,
|
||||||
) {
|
) {
|
||||||
if let (Some(player_alias), Some(cmd), cmd_args) =
|
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_args = cmd_args.unwrap_or(String::from(""));
|
||||||
let cmd = if cmd.chars().next() == Some('/') {
|
let cmd = if cmd.chars().next() == Some('/') {
|
||||||
@ -1438,14 +1230,14 @@ fn handle_sudo(
|
|||||||
} else {
|
} else {
|
||||||
cmd
|
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 ecs = server.state.ecs();
|
||||||
let entity_opt = (&ecs.entities(), &ecs.read_storage::<comp::Player>())
|
let entity_opt = (&ecs.entities(), &ecs.read_storage::<comp::Player>())
|
||||||
.join()
|
.join()
|
||||||
.find(|(_, player)| player.alias == player_alias)
|
.find(|(_, player)| player.alias == player_alias)
|
||||||
.map(|(entity, _)| entity);
|
.map(|(entity, _)| entity);
|
||||||
if let Some(entity) = entity_opt {
|
if let Some(entity) = entity_opt {
|
||||||
(action.handler)(server, client, entity, cmd_args, action);
|
get_handler(action)(server, client, entity, cmd_args, action);
|
||||||
} else {
|
} else {
|
||||||
server.notify_client(
|
server.notify_client(
|
||||||
client,
|
client,
|
||||||
@ -1459,7 +1251,10 @@ fn handle_sudo(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
} else {
|
} 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,
|
auth_provider::AuthProvider,
|
||||||
chunk_generator::ChunkGenerator,
|
chunk_generator::ChunkGenerator,
|
||||||
client::{Client, RegionSubscription},
|
client::{Client, RegionSubscription},
|
||||||
cmd::CHAT_COMMANDS,
|
cmd::ChatCommandExt,
|
||||||
state_ext::StateExt,
|
state_ext::StateExt,
|
||||||
sys::sentinel::{DeletedEntities, TrackedComps},
|
sys::sentinel::{DeletedEntities, TrackedComps},
|
||||||
};
|
};
|
||||||
use common::{
|
use common::{
|
||||||
|
cmd::ChatCommand,
|
||||||
comp,
|
comp,
|
||||||
event::{EventBus, ServerEvent},
|
event::{EventBus, ServerEvent},
|
||||||
msg::{ClientMsg, ClientState, ServerInfo, ServerMsg},
|
msg::{ClientMsg, ClientState, ServerInfo, ServerMsg},
|
||||||
@ -534,18 +535,16 @@ impl Server {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Find the command object and run its handler.
|
// Find the command object and run its handler.
|
||||||
let action_opt = CHAT_COMMANDS.iter().find(|x| x.keyword == kwd);
|
if let Ok(command) = kwd.parse::<ChatCommand>() {
|
||||||
match action_opt {
|
command.execute(self, entity, args);
|
||||||
Some(action) => action.execute(self, entity, args),
|
} else {
|
||||||
// Unknown command
|
self.notify_client(
|
||||||
None => {
|
entity,
|
||||||
if let Some(client) = self.state.ecs().write_storage::<Client>().get_mut(entity) {
|
ServerMsg::private(format!(
|
||||||
client.notify(ServerMsg::private(format!(
|
|
||||||
"Unknown command '/{}'.\nType '/help' for available commands",
|
"Unknown command '/{}'.\nType '/help' for available commands",
|
||||||
kwd
|
kwd
|
||||||
)));
|
)),
|
||||||
}
|
);
|
||||||
},
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -3,14 +3,17 @@ use super::{
|
|||||||
META_COLOR, PRIVATE_COLOR, SAY_COLOR, TELL_COLOR, TEXT_COLOR,
|
META_COLOR, PRIVATE_COLOR, SAY_COLOR, TELL_COLOR, TEXT_COLOR,
|
||||||
};
|
};
|
||||||
use crate::{ui::fonts::ConrodVoxygenFonts, GlobalState};
|
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 common::{msg::validate_chat_msg, ChatType};
|
||||||
use conrod_core::{
|
use conrod_core::{
|
||||||
input::Key,
|
input::Key,
|
||||||
position::Dimension,
|
position::Dimension,
|
||||||
text::cursor::Index,
|
text::{
|
||||||
|
self,
|
||||||
|
cursor::{self, Index},
|
||||||
|
},
|
||||||
widget::{self, Button, Id, List, Rectangle, Text, TextEdit},
|
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;
|
use std::collections::VecDeque;
|
||||||
|
|
||||||
@ -18,9 +21,10 @@ widget_ids! {
|
|||||||
struct Ids {
|
struct Ids {
|
||||||
message_box,
|
message_box,
|
||||||
message_box_bg,
|
message_box_bg,
|
||||||
input,
|
chat_input,
|
||||||
input_bg,
|
chat_input_bg,
|
||||||
chat_arrow,
|
chat_arrow,
|
||||||
|
completion_box,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -31,6 +35,7 @@ pub struct Chat<'a> {
|
|||||||
new_messages: &'a mut VecDeque<ClientEvent>,
|
new_messages: &'a mut VecDeque<ClientEvent>,
|
||||||
force_input: Option<String>,
|
force_input: Option<String>,
|
||||||
force_cursor: Option<Index>,
|
force_cursor: Option<Index>,
|
||||||
|
force_completions: Option<Vec<String>>,
|
||||||
|
|
||||||
global_state: &'a GlobalState,
|
global_state: &'a GlobalState,
|
||||||
imgs: &'a Imgs,
|
imgs: &'a Imgs,
|
||||||
@ -54,6 +59,7 @@ impl<'a> Chat<'a> {
|
|||||||
new_messages,
|
new_messages,
|
||||||
force_input: None,
|
force_input: None,
|
||||||
force_cursor: None,
|
force_cursor: None,
|
||||||
|
force_completions: None,
|
||||||
imgs,
|
imgs,
|
||||||
fonts,
|
fonts,
|
||||||
global_state,
|
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 {
|
pub fn input(mut self, input: String) -> Self {
|
||||||
if let Ok(()) = validate_chat_msg(&input) {
|
if let Ok(()) = validate_chat_msg(&input) {
|
||||||
self.force_input = Some(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
|
// Index into the history Vec, history_pos == 0 is history not in use
|
||||||
// otherwise index is history_pos -1
|
// otherwise index is history_pos -1
|
||||||
history_pos: usize,
|
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 {
|
pub enum Event {
|
||||||
|
TabCompletionStart(String),
|
||||||
SendMessage(String),
|
SendMessage(String),
|
||||||
Focus(Id),
|
Focus(Id),
|
||||||
}
|
}
|
||||||
@ -115,6 +136,9 @@ impl<'a> Widget for Chat<'a> {
|
|||||||
messages: VecDeque::new(),
|
messages: VecDeque::new(),
|
||||||
history: VecDeque::new(),
|
history: VecDeque::new(),
|
||||||
history_pos: 0,
|
history_pos: 0,
|
||||||
|
completions: Vec::new(),
|
||||||
|
completions_index: None,
|
||||||
|
completion_cursor: None,
|
||||||
ids: Ids::new(id_gen),
|
ids: Ids::new(id_gen),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -137,51 +161,106 @@ impl<'a> Widget for Chat<'a> {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// If up or down are pressed move through history
|
if let Some(comps) = &self.force_completions {
|
||||||
// TODO: move cursor to the end of the last line
|
state.update(|s| s.completions = comps.clone());
|
||||||
match ui.widget_input(state.ids.input).presses().key().fold(
|
}
|
||||||
(false, false),
|
|
||||||
|(up, down), key_press| match key_press.key {
|
let mut force_cursor = self.force_cursor;
|
||||||
Key::Up => (true, down),
|
|
||||||
Key::Down => (up, true),
|
// If up or down are pressed: move through history
|
||||||
_ => (up, down),
|
// 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),
|
||||||
},
|
},
|
||||||
) {
|
);
|
||||||
(true, false) => {
|
|
||||||
if state.history_pos < state.history.len() {
|
// Handle tab completion
|
||||||
|
let request_tab_completions = if stop_tab_completion {
|
||||||
|
// End tab completion
|
||||||
state.update(|s| {
|
state.update(|s| {
|
||||||
s.history_pos += 1;
|
if s.completion_cursor.is_some() {
|
||||||
s.input = s.history.get(s.history_pos - 1).unwrap().to_owned();
|
s.completion_cursor = None;
|
||||||
|
}
|
||||||
|
s.completions_index = None;
|
||||||
|
});
|
||||||
|
false
|
||||||
|
} else if let Some(cursor) = state.completion_cursor {
|
||||||
|
// Cycle through tab completions of the current word
|
||||||
|
if state.input.contains('\t') {
|
||||||
|
state.update(|s| s.input.retain(|c| c != '\t'));
|
||||||
|
//tab_dir + 1
|
||||||
|
}
|
||||||
|
if !state.completions.is_empty() {
|
||||||
|
if tab_dir != 0 || state.completions_index.is_none() {
|
||||||
|
state.update(|s| {
|
||||||
|
let len = s.completions.len();
|
||||||
|
s.completions_index = Some(
|
||||||
|
(s.completions_index.unwrap_or(0) + (tab_dir + len as isize) as usize)
|
||||||
|
% len,
|
||||||
|
);
|
||||||
|
if let Some(replacement) = &s.completions.get(s.completions_index.unwrap())
|
||||||
|
{
|
||||||
|
let (completed, offset) =
|
||||||
|
do_tab_completion(cursor, &s.input, replacement);
|
||||||
|
force_cursor =
|
||||||
|
cursor_offset_to_index(offset, &completed, &ui, &self.fonts);
|
||||||
|
s.input = completed;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
(false, true) => {
|
false
|
||||||
if state.history_pos > 0 {
|
} 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| {
|
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;
|
s.history_pos -= 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
if s.history_pos > 0 {
|
if s.history_pos > 0 {
|
||||||
s.input = s.history.get(s.history_pos - 1).unwrap().to_owned();
|
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 {
|
} else {
|
||||||
s.input.clear();
|
s.input.clear();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
},
|
|
||||||
_ => {},
|
|
||||||
}
|
|
||||||
|
|
||||||
let keyboard_capturer = ui.global_input().current.widget_capturing_keyboard;
|
let keyboard_capturer = ui.global_input().current.widget_capturing_keyboard;
|
||||||
|
|
||||||
if let Some(input) = &self.force_input {
|
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 =
|
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.
|
// Only show if it has the keyboard captured.
|
||||||
// Chat input uses a rectangle as its background.
|
// Chat input uses a rectangle as its background.
|
||||||
if input_focused {
|
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)
|
let mut text_edit = TextEdit::new(&state.input)
|
||||||
.w(460.0)
|
.w(460.0)
|
||||||
.restrict_to_height(false)
|
.restrict_to_height(false)
|
||||||
@ -190,7 +269,7 @@ impl<'a> Widget for Chat<'a> {
|
|||||||
.font_size(self.fonts.opensans.scale(15))
|
.font_size(self.fonts.opensans.scale(15))
|
||||||
.font_id(self.fonts.opensans.conrod_id);
|
.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);
|
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)
|
.rgba(0.0, 0.0, 0.0, transp + 0.1)
|
||||||
.bottom_left_with_margins_on(ui.window, 10.0, 10.0)
|
.bottom_left_with_margins_on(ui.window, 10.0, 10.0)
|
||||||
.w(470.0)
|
.w(470.0)
|
||||||
.set(state.ids.input_bg, ui);
|
.set(state.ids.chat_input_bg, ui);
|
||||||
|
|
||||||
if let Some(str) = text_edit
|
if let Some(str) = text_edit
|
||||||
.top_left_with_margins_on(state.ids.input_bg, 1.0, 1.0)
|
.top_left_with_margins_on(state.ids.chat_input_bg, 1.0, 1.0)
|
||||||
.set(state.ids.input, ui)
|
.set(state.ids.chat_input, ui)
|
||||||
{
|
{
|
||||||
let mut input = str.to_owned();
|
let mut input = str.to_owned();
|
||||||
input.retain(|c| c != '\n');
|
input.retain(|c| c != '\n');
|
||||||
@ -221,7 +300,7 @@ impl<'a> Widget for Chat<'a> {
|
|||||||
.rgba(0.0, 0.0, 0.0, transp)
|
.rgba(0.0, 0.0, 0.0, transp)
|
||||||
.and(|r| {
|
.and(|r| {
|
||||||
if input_focused {
|
if input_focused {
|
||||||
r.up_from(state.ids.input_bg, 0.0)
|
r.up_from(state.ids.chat_input_bg, 0.0)
|
||||||
} else {
|
} else {
|
||||||
r.bottom_left_with_margins_on(ui.window, 10.0, 10.0)
|
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
|
// We've started a new tab completion. Populate tab completion suggestions.
|
||||||
// input box.
|
if request_tab_completions {
|
||||||
if keyboard_capturer == Some(id) {
|
Some(Event::TabCompletionStart(state.input.to_string()))
|
||||||
Some(Event::Focus(state.ids.input))
|
// 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.
|
// If enter is pressed and the input box is not empty, send the current message.
|
||||||
else if ui
|
else if ui
|
||||||
.widget_input(state.ids.input)
|
.widget_input(state.ids.chat_input)
|
||||||
.presses()
|
.presses()
|
||||||
.key()
|
.key()
|
||||||
.any(|key_press| match key_press.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_ungrab: bool,
|
||||||
force_chat_input: Option<String>,
|
force_chat_input: Option<String>,
|
||||||
force_chat_cursor: Option<Index>,
|
force_chat_cursor: Option<Index>,
|
||||||
|
tab_complete: Option<String>,
|
||||||
pulse: f32,
|
pulse: f32,
|
||||||
velocity: f32,
|
velocity: f32,
|
||||||
voxygen_i18n: std::sync::Arc<VoxygenLocalization>,
|
voxygen_i18n: std::sync::Arc<VoxygenLocalization>,
|
||||||
@ -518,6 +519,7 @@ impl Hud {
|
|||||||
force_ungrab: false,
|
force_ungrab: false,
|
||||||
force_chat_input: None,
|
force_chat_input: None,
|
||||||
force_chat_cursor: None,
|
force_chat_cursor: None,
|
||||||
|
tab_complete: None,
|
||||||
pulse: 0.0,
|
pulse: 0.0,
|
||||||
velocity: 0.0,
|
velocity: 0.0,
|
||||||
voxygen_i18n,
|
voxygen_i18n,
|
||||||
@ -1740,9 +1742,15 @@ impl Hud {
|
|||||||
&self.fonts,
|
&self.fonts,
|
||||||
)
|
)
|
||||||
.and_then(self.force_chat_input.take(), |c, input| c.input(input))
|
.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))
|
.and_then(self.force_chat_cursor.take(), |c, pos| c.cursor_pos(pos))
|
||||||
.set(self.ids.chat, ui_widgets)
|
.set(self.ids.chat, ui_widgets)
|
||||||
{
|
{
|
||||||
|
Some(chat::Event::TabCompletionStart(input)) => {
|
||||||
|
self.tab_complete = Some(input);
|
||||||
|
},
|
||||||
Some(chat::Event::SendMessage(message)) => {
|
Some(chat::Event::SendMessage(message)) => {
|
||||||
events.push(Event::SendMessage(message));
|
events.push(Event::SendMessage(message));
|
||||||
},
|
},
|
||||||
@ -2312,6 +2320,27 @@ impl Hud {
|
|||||||
camera: &Camera,
|
camera: &Camera,
|
||||||
dt: Duration,
|
dt: Duration,
|
||||||
) -> Vec<Event> {
|
) -> 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() {
|
if let Some(maybe_id) = self.to_focus.take() {
|
||||||
self.ui.focus_widget(maybe_id);
|
self.ui.focus_widget(maybe_id);
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user