Implement tab completion of enums (/object /time /spawn) and numbers

This commit is contained in:
CapsizeGlimmer 2020-05-08 22:42:51 -04:00
parent 28e94afd3f
commit b486de28ac
5 changed files with 273 additions and 211 deletions

View File

@ -57,17 +57,10 @@ 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());
}
const ASSETS_TMP: [&'static str; 1] = ["common/items/lantern/black_0"];
pub fn iterate() -> impl Iterator<Item = &'static str> {
// TODO FIXME implement this
//ASSETS.read().iter().flat_map(|e| e.keys())
ASSETS_TMP.iter().map(|k| *k)
}
// TODO: Remove this function. It's only used in world/ in a really ugly way.To
// do this properly assets should have all their necessary data in one file. A
// ron file could be used to combine voxel data with positioning data for

View File

@ -1,5 +1,7 @@
use crate::{assets, comp::Player, state::State};
use crate::{assets, comp, npc, state::State};
use lazy_static::lazy_static;
use specs::prelude::{Join, WorldExt};
use std::{ops::Deref, str::FromStr};
/// Struct representing a command that a user can run from server chat.
pub struct ChatCommandData {
@ -85,63 +87,108 @@ pub static CHAT_COMMANDS: &'static [ChatCommand] = &[
ChatCommand::Waypoint,
];
lazy_static! {
static ref ALIGNMENTS: Vec<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();
}
fn items() -> Vec<String> {
if let Ok(assets) = assets::ASSETS.read() {
assets
.iter()
.flat_map(|(k, v)| {
if v.is::<comp::item::Item>() {
Some(k.clone())
} else {
None
}
})
.collect()
} else {
error!("Assets not found");
vec![]
}
}
impl ChatCommand {
pub fn data(&self) -> ChatCommandData {
use ArgumentSpec::*;
use Requirement::*;
let cmd = ChatCommandData::new;
match self {
ChatCommand::Adminify => cmd(
vec![PlayerName(false)],
vec![PlayerName(Required)],
"Temporarily gives a player admin permissions or removes them",
true,
),
ChatCommand::Alias => cmd(vec![Any("name", false)], "Change your alias", false),
ChatCommand::Alias => cmd(vec![Any("name", Required)], "Change your alias", false),
ChatCommand::Build => cmd(vec![], "Toggles build mode on and off", true),
ChatCommand::Debug => cmd(vec![], "Place all debug items into your pack.", true),
ChatCommand::DebugColumn => cmd(
vec![Float("x", f32::NAN, false), Float("y", f32::NAN, false)],
vec![
Integer("x", 15000, Required),
Integer("y", 15000, Required),
],
"Prints some debug information about a column",
false,
),
ChatCommand::Explosion => cmd(
vec![Float("radius", 5.0, false)],
vec![Float("radius", 5.0, Required)],
"Explodes the ground around you",
true,
),
ChatCommand::GiveExp => cmd(
vec![Integer("amount", 50, false)],
vec![Integer("amount", 50, Required)],
"Give experience to yourself",
true,
),
ChatCommand::GiveItem => cmd(
vec![ItemSpec(false), Integer("num", 1, true)],
vec![Enum("item", items(), Required), Integer("num", 1, Optional)],
"Give yourself some items",
true,
),
ChatCommand::Goto => cmd(
vec![
Float("x", 0.0, false),
Float("y", 0.0, false),
Float("z", 0.0, false),
Float("x", 0.0, Required),
Float("y", 0.0, Required),
Float("z", 0.0, Required),
],
"Teleport to a position",
true,
),
ChatCommand::Health => cmd(
vec![Integer("hp", 100, false)],
vec![Integer("hp", 100, Required)],
"Set your current health",
true,
),
ChatCommand::Help => ChatCommandData::new(
vec![Command(true)],
vec![Command(Optional)],
"Display information about commands",
false,
),
ChatCommand::Jump => cmd(
vec![
Float("x", 0.0, false),
Float("y", 0.0, false),
Float("z", 0.0, false),
Float("x", 0.0, Required),
Float("y", 0.0, Required),
Float("z", 0.0, Required),
],
"Offset your current position",
true,
@ -150,50 +197,72 @@ impl ChatCommand {
ChatCommand::KillNpcs => cmd(vec![], "Kill the NPCs", true),
ChatCommand::Lantern => cmd(
vec![
Float("strength", 5.0, false),
Float("r", 1.0, true),
Float("g", 1.0, true),
Float("b", 1.0, true),
Float("strength", 5.0, Required),
Float("r", 1.0, Optional),
Float("g", 1.0, Optional),
Float("b", 1.0, Optional),
],
"Change your lantern's strength and color",
true,
),
ChatCommand::Light => cmd(
vec![
Float("r", 1.0, true),
Float("g", 1.0, true),
Float("b", 1.0, true),
Float("x", 0.0, true),
Float("y", 0.0, true),
Float("z", 0.0, true),
Float("strength", 5.0, true),
Float("r", 1.0, Optional),
Float("g", 1.0, Optional),
Float("b", 1.0, Optional),
Float("x", 0.0, Optional),
Float("y", 0.0, Optional),
Float("z", 0.0, Optional),
Float("strength", 5.0, Optional),
],
"Spawn entity with light",
true,
),
ChatCommand::Object => cmd(vec![/*TODO*/], "Spawn an object", true),
ChatCommand::Object => cmd(
vec![Enum("object", OBJECTS.clone(), Required)],
"Spawn an object",
true,
),
ChatCommand::Players => cmd(vec![], "Lists players currently online", false),
ChatCommand::RemoveLights => cmd(
vec![Float("radius", 20.0, true)],
vec![Float("radius", 20.0, Optional)],
"Removes all lights spawned by players",
true,
),
ChatCommand::SetLevel => {
cmd(vec![Integer("level", 10, false)], "Set player Level", true)
},
ChatCommand::Spawn => cmd(vec![/*TODO*/], "Spawn a test entity", true),
ChatCommand::SetLevel => cmd(
vec![Integer("level", 10, Required)],
"Set player Level",
true,
),
ChatCommand::Spawn => cmd(
vec![
Enum("alignment", ALIGNMENTS.clone(), Required),
Enum("entity", ENTITIES.clone(), Required),
Integer("amount", 1, Optional),
],
"Spawn a test entity",
true,
),
ChatCommand::Sudo => cmd(
vec![PlayerName(false), SubCommand],
vec![PlayerName(Required), SubCommand],
"Run command as if you were another player",
true,
),
ChatCommand::Tell => cmd(
vec![PlayerName(false), Message],
vec![PlayerName(Required), Message],
"Send a message to another player",
false,
),
ChatCommand::Time => cmd(vec![/*TODO*/], "Set the time of day", true),
ChatCommand::Tp => cmd(vec![PlayerName(true)], "Teleport to another player", true),
ChatCommand::Time => cmd(
vec![Enum("time", TIMES.clone(), Optional)],
"Set the time of day",
true,
),
ChatCommand::Tp => cmd(
vec![PlayerName(Optional)],
"Teleport to another player",
true,
),
ChatCommand::Version => cmd(vec![], "Prints server version", false),
ChatCommand::Waypoint => {
cmd(vec![], "Set your waypoint to your current position", true)
@ -250,21 +319,20 @@ impl ChatCommand {
.iter()
.map(|arg| match arg {
ArgumentSpec::PlayerName(_) => "{}",
ArgumentSpec::ItemSpec(_) => "{}",
ArgumentSpec::Float(_, _, _) => "{}",
ArgumentSpec::Integer(_, _, _) => "{d}",
ArgumentSpec::Any(_, _) => "{}",
ArgumentSpec::Command(_) => "{}",
ArgumentSpec::Message => "{/.*/}",
ArgumentSpec::SubCommand => "{} {/.*/}",
ArgumentSpec::OneOf(_, _, _, _) => "{}", // TODO
ArgumentSpec::Enum(_, _, _) => "{}", // TODO
})
.collect::<Vec<_>>()
.join(" ")
}
}
impl std::str::FromStr for ChatCommand {
impl FromStr for ChatCommand {
type Err = ();
fn from_str(keyword: &str) -> Result<ChatCommand, ()> {
@ -282,26 +350,39 @@ impl std::str::FromStr for ChatCommand {
}
}
pub enum Requirement {
Required,
Optional,
}
impl Deref for Requirement {
type Target = bool;
fn deref(&self) -> &bool {
match self {
Requirement::Required => &true,
Requirement::Optional => &false,
}
}
}
/// Representation for chat command arguments
pub enum ArgumentSpec {
/// The argument refers to a player by alias
PlayerName(bool),
/// The argument refers to an item asset by path
ItemSpec(bool),
PlayerName(Requirement),
/// The argument is a float. The associated values are
/// * label
/// * default tab-completion
/// * suggested tab-completion
/// * whether it's optional
Float(&'static str, f32, bool),
Float(&'static str, f32, Requirement),
/// The argument is a float. The associated values are
/// * label
/// * default tab-completion
/// * suggested tab-completion
/// * whether it's optional
Integer(&'static str, i32, bool),
Integer(&'static str, i32, Requirement),
/// The argument is any string that doesn't contain spaces
Any(&'static str, bool),
/// The argument is a command name
Command(bool),
Any(&'static str, Requirement),
/// The argument is a command name (such as in /help)
Command(Requirement),
/// This is the final argument, consuming all characters until the end of
/// input.
Message,
@ -310,68 +391,55 @@ pub enum ArgumentSpec {
/// The argument is likely an enum. The associated values are
/// * label
/// * Predefined string completions
/// * Other completion types
/// * whether it's optional
OneOf(
&'static str,
&'static [&'static str],
Vec<Box<ArgumentSpec>>,
bool,
),
Enum(&'static str, Vec<String>, Requirement),
}
impl ArgumentSpec {
pub fn usage_string(&self) -> String {
match self {
ArgumentSpec::PlayerName(optional) => {
if *optional {
"[player]".to_string()
} else {
ArgumentSpec::PlayerName(req) => {
if **req {
"<player>".to_string()
} else {
"[player]".to_string()
}
},
ArgumentSpec::ItemSpec(optional) => {
if *optional {
"[item]".to_string()
} else {
"<item>".to_string()
}
},
ArgumentSpec::Float(label, _, optional) => {
if *optional {
format!("[{}]", label)
} else {
ArgumentSpec::Float(label, _, req) => {
if **req {
format!("<{}>", label)
}
},
ArgumentSpec::Integer(label, _, optional) => {
if *optional {
} else {
format!("[{}]", label)
} else {
format!("<{}>", label)
}
},
ArgumentSpec::Any(label, optional) => {
if *optional {
ArgumentSpec::Integer(label, _, req) => {
if **req {
format!("<{}>", label)
} else {
format!("[{}]", label)
} else {
format!("<{}>", label)
}
},
ArgumentSpec::Command(optional) => {
if *optional {
"[[/]command]".to_string()
ArgumentSpec::Any(label, req) => {
if **req {
format!("<{}>", label)
} else {
format!("[{}]", label)
}
},
ArgumentSpec::Command(req) => {
if **req {
"<[/]command>".to_string()
} else {
"[[/]command]".to_string()
}
},
ArgumentSpec::Message => "<message>".to_string(),
ArgumentSpec::SubCommand => "<[/]command> [args...]".to_string(),
ArgumentSpec::OneOf(label, _, _, optional) => {
if *optional {
format! {"[{}]", label}
} else {
ArgumentSpec::Enum(label, _, req) => {
if **req {
format! {"<{}>", label}
} else {
format! {"[{}]", label}
}
},
}
@ -380,33 +448,35 @@ impl ArgumentSpec {
pub fn complete(&self, part: &str, state: &State) -> Vec<String> {
match self {
ArgumentSpec::PlayerName(_) => complete_player(part, &state),
ArgumentSpec::ItemSpec(_) => assets::iterate()
.filter(|asset| asset.starts_with(part))
.map(|c| c.to_string())
.collect(),
ArgumentSpec::Float(_, x, _) => vec![format!("{}", x)],
ArgumentSpec::Integer(_, x, _) => vec![format!("{}", x)],
ArgumentSpec::Float(_, x, _) => {
if part.is_empty() {
vec![format!("{:.1}", x)]
} else {
vec![]
}
},
ArgumentSpec::Integer(_, x, _) => {
if part.is_empty() {
vec![format!("{}", x)]
} else {
vec![]
}
},
ArgumentSpec::Any(_, _) => vec![],
ArgumentSpec::Command(_) => complete_command(part),
ArgumentSpec::Message => complete_player(part, &state),
ArgumentSpec::SubCommand => complete_command(part),
ArgumentSpec::OneOf(_, strings, alts, _) => {
let string_completions = strings
.iter()
.filter(|string| string.starts_with(part))
.map(|c| c.to_string());
let alt_completions = alts
.iter()
.flat_map(|b| (*b).complete(part, &state))
.map(|c| c.to_string());
string_completions.chain(alt_completions).collect()
},
ArgumentSpec::Enum(_, strings, _) => strings
.iter()
.filter(|string| string.starts_with(part))
.map(|c| c.to_string())
.collect(),
}
}
}
fn complete_player(part: &str, state: &State) -> Vec<String> {
let storage = state.ecs().read_storage::<Player>();
let storage = state.ecs().read_storage::<comp::Player>();
let mut iter = storage.join();
if let Some(first) = iter.next() {
std::iter::once(first)

View File

@ -65,7 +65,7 @@ impl Body {
}
}
const ALL_OBJECTS: [Body; 52] = [
pub const ALL_OBJECTS: [Body; 53] = [
Body::Arrow,
Body::Bomb,
Body::Scarecrow,
@ -114,8 +114,69 @@ const ALL_OBJECTS: [Body; 52] = [
Body::CarpetHumanSquare,
Body::CarpetHumanSquare2,
Body::CarpetHumanSquircle,
Body::Pouch,
Body::CraftingBench,
Body::BoltFire,
Body::BoltFireBig,
Body::ArrowSnake,
];
impl Body {
pub fn to_string(&self) -> &str {
match self {
Body::Arrow => "arrow",
Body::Bomb => "bomb",
Body::Scarecrow => "scarecrow",
Body::Cauldron => "cauldron",
Body::ChestVines => "chest_vines",
Body::Chest => "chest",
Body::ChestDark => "chest_dark",
Body::ChestDemon => "chest_demon",
Body::ChestGold => "chest_gold",
Body::ChestLight => "chest_light",
Body::ChestOpen => "chest_open",
Body::ChestSkull => "chest_skull",
Body::Pumpkin => "pumpkin",
Body::Pumpkin2 => "pumpkin_2",
Body::Pumpkin3 => "pumpkin_3",
Body::Pumpkin4 => "pumpkin_4",
Body::Pumpkin5 => "pumpkin_5",
Body::Campfire => "campfire",
Body::CampfireLit => "campfire_lit",
Body::LanternGround => "lantern_ground",
Body::LanternGroundOpen => "lantern_ground_open",
Body::LanternStanding => "lantern_standing",
Body::LanternStanding2 => "lantern_standing_2",
Body::PotionRed => "potion_red",
Body::PotionBlue => "potion_blue",
Body::PotionGreen => "potion_green",
Body::Crate => "crate",
Body::Tent => "tent",
Body::WindowSpooky => "window_spooky",
Body::DoorSpooky => "door_spooky",
Body::Anvil => "anvil",
Body::Gravestone => "gravestone",
Body::Gravestone2 => "gravestone_2",
Body::Bench => "bench",
Body::Chair => "chair",
Body::Chair2 => "chair_2",
Body::Chair3 => "chair_3",
Body::Table => "table",
Body::Table2 => "table_2",
Body::Table3 => "table_3",
Body::Drawer => "drawer",
Body::BedBlue => "bed_blue",
Body::Carpet => "carpet",
Body::Bedroll => "bedroll",
Body::CarpetHumanRound => "carpet_human_round",
Body::CarpetHumanSquare => "carpet_human_square",
Body::CarpetHumanSquare2 => "carpet_human_square_2",
Body::CarpetHumanSquircle => "carpet_human_squircle",
Body::Pouch => "pouch",
Body::CraftingBench => "crafting_bench",
Body::BoltFire => "bolt_fire",
Body::BoltFireBig => "bolt_fire_big",
Body::ArrowSnake => "arrow_snake",
}
}
}

View File

@ -78,6 +78,7 @@ fn get_handler(cmd: &ChatCommand) -> CommandHandler {
ChatCommand::Waypoint => handle_waypoint,
}
}
fn handle_give_item(
server: &mut Server,
client: EcsEntity,
@ -616,85 +617,39 @@ fn handle_object(
.with(ori);*/
if let (Some(pos), Some(ori)) = (pos, ori) {
let obj_str_res = obj_type.as_ref().map(String::as_str);
let obj_type = match obj_str_res {
Ok("scarecrow") => comp::object::Body::Scarecrow,
Ok("cauldron") => comp::object::Body::Cauldron,
Ok("chest_vines") => comp::object::Body::ChestVines,
Ok("chest") => comp::object::Body::Chest,
Ok("chest_dark") => comp::object::Body::ChestDark,
Ok("chest_demon") => comp::object::Body::ChestDemon,
Ok("chest_gold") => comp::object::Body::ChestGold,
Ok("chest_light") => comp::object::Body::ChestLight,
Ok("chest_open") => comp::object::Body::ChestOpen,
Ok("chest_skull") => comp::object::Body::ChestSkull,
Ok("pumpkin") => comp::object::Body::Pumpkin,
Ok("pumpkin_2") => comp::object::Body::Pumpkin2,
Ok("pumpkin_3") => comp::object::Body::Pumpkin3,
Ok("pumpkin_4") => comp::object::Body::Pumpkin4,
Ok("pumpkin_5") => comp::object::Body::Pumpkin5,
Ok("campfire") => comp::object::Body::Campfire,
Ok("campfire_lit") => comp::object::Body::CampfireLit,
Ok("lantern_ground") => comp::object::Body::LanternGround,
Ok("lantern_ground_open") => comp::object::Body::LanternGroundOpen,
Ok("lantern_2") => comp::object::Body::LanternStanding2,
Ok("lantern") => comp::object::Body::LanternStanding,
Ok("potion_blue") => comp::object::Body::PotionBlue,
Ok("potion_green") => comp::object::Body::PotionGreen,
Ok("potion_red") => comp::object::Body::PotionRed,
Ok("crate") => comp::object::Body::Crate,
Ok("tent") => comp::object::Body::Tent,
Ok("bomb") => comp::object::Body::Bomb,
Ok("window_spooky") => comp::object::Body::WindowSpooky,
Ok("door_spooky") => comp::object::Body::DoorSpooky,
Ok("carpet") => comp::object::Body::Carpet,
Ok("table_human") => comp::object::Body::Table,
Ok("table_human_2") => comp::object::Body::Table2,
Ok("table_human_3") => comp::object::Body::Table3,
Ok("drawer") => comp::object::Body::Drawer,
Ok("bed_human_blue") => comp::object::Body::BedBlue,
Ok("anvil") => comp::object::Body::Anvil,
Ok("gravestone") => comp::object::Body::Gravestone,
Ok("gravestone_2") => comp::object::Body::Gravestone2,
Ok("chair") => comp::object::Body::Chair,
Ok("chair_2") => comp::object::Body::Chair2,
Ok("chair_3") => comp::object::Body::Chair3,
Ok("bench_human") => comp::object::Body::Bench,
Ok("bedroll") => comp::object::Body::Bedroll,
Ok("carpet_human_round") => comp::object::Body::CarpetHumanRound,
Ok("carpet_human_square") => comp::object::Body::CarpetHumanSquare,
Ok("carpet_human_square_2") => comp::object::Body::CarpetHumanSquare2,
Ok("carpet_human_squircle") => comp::object::Body::CarpetHumanSquircle,
Ok("crafting_bench") => comp::object::Body::CraftingBench,
_ => {
return server.notify_client(
client,
ServerMsg::private(String::from("Object not found!")),
);
},
};
server
.state
.create_object(pos, obj_type)
.with(comp::Ori(
// converts player orientation into a 90° rotation for the object by using the axis
// with the highest value
Dir::from_unnormalized(ori.0.map(|e| {
if e.abs() == ori.0.map(|e| e.abs()).reduce_partial_max() {
e
} else {
0.0
}
}))
.unwrap_or_default(),
))
.build();
server.notify_client(
client,
ServerMsg::private(format!(
"Spawned: {}",
obj_str_res.unwrap_or("<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!")));
}

View File

@ -29,8 +29,6 @@ widget_ids! {
}
const MAX_MESSAGES: usize = 100;
// Maximum completions shown at once
const MAX_COMPLETIONS: usize = 10;
#[derive(WidgetCommon)]
pub struct Chat<'a> {
@ -456,21 +454,6 @@ fn do_tab_completion(cursor: usize, input: &str, word: &str) -> (String, usize)
}
}
fn cursor_index_to_offset(
index: text::cursor::Index,
text: &str,
ui: &Ui,
fonts: &ConrodVoxygenFonts,
) -> Option<usize> {
// Width and font must match that of the chat TextEdit
let width = 460.0;
let font = ui.fonts.get(fonts.opensans.conrod_id)?;
let font_size = fonts.opensans.scale(15);
let infos = text::line::infos(&text, &font, font_size).wrap_by_whitespace(width);
text::glyph::index_after_cursor(infos, index)
}
fn cursor_offset_to_index(
offset: usize,
text: &str,