Merge branch 'veloren-poggly/chat_cmd_suggestion_and_links' into 'master'

[ #1286] Chat command suggestions + Fixing /help + code refactor

See merge request veloren/veloren!3699
This commit is contained in:
Isse 2023-01-28 02:06:23 +00:00
commit 0543c265c8
13 changed files with 189 additions and 52 deletions

View File

@ -11,6 +11,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Command to toggle experimental shaders. - Command to toggle experimental shaders.
- Faster Energy Regeneration while sitting. - Faster Energy Regeneration while sitting.
- Lantern glow for dropped lanterns. - Lantern glow for dropped lanterns.
- Suggests commands when an invalid one is entered in chat and added Client-side commands to /help.
### Changed ### Changed
- Bats move slower and use a simple proportional controller to maintain altitude - Bats move slower and use a simple proportional controller to maintain altitude

7
Cargo.lock generated
View File

@ -3216,6 +3216,12 @@ version = "0.2.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "884e2677b40cc8c339eaefcb701c32ef1fd2493d71118dc0ca4b6a736c93bd67" checksum = "884e2677b40cc8c339eaefcb701c32ef1fd2493d71118dc0ca4b6a736c93bd67"
[[package]]
name = "levenshtein"
version = "1.0.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "db13adb97ab515a3691f56e4dbab09283d0b86cb45abd991d8634a9d6f501760"
[[package]] [[package]]
name = "lewton" name = "lewton"
version = "0.10.2" version = "0.10.2"
@ -7080,6 +7086,7 @@ dependencies = [
"itertools", "itertools",
"keyboard-keynames", "keyboard-keynames",
"lazy_static", "lazy_static",
"levenshtein",
"mimalloc", "mimalloc",
"mumble-link", "mumble-link",
"native-dialog", "native-dialog",

View File

@ -28,6 +28,7 @@ macro_rules! synced_components {
health: Health, health: Health,
poise: Poise, poise: Poise,
light_emitter: LightEmitter, light_emitter: LightEmitter,
loot_owner: LootOwner,
item: Item, item: Item,
scale: Scale, scale: Scale,
group: Group, group: Group,
@ -56,10 +57,10 @@ macro_rules! synced_components {
// Synced to the client only for its own entity // Synced to the client only for its own entity
admin: Admin,
combo: Combo, combo: Combo,
active_abilities: ActiveAbilities, active_abilities: ActiveAbilities,
can_build: CanBuild, can_build: CanBuild,
loot_owner: LootOwner,
} }
}; };
} }
@ -151,6 +152,10 @@ impl NetSync for LightEmitter {
const SYNC_FROM: SyncFrom = SyncFrom::AnyEntity; const SYNC_FROM: SyncFrom = SyncFrom::AnyEntity;
} }
impl NetSync for LootOwner {
const SYNC_FROM: SyncFrom = SyncFrom::AnyEntity;
}
impl NetSync for Item { impl NetSync for Item {
const SYNC_FROM: SyncFrom = SyncFrom::AnyEntity; const SYNC_FROM: SyncFrom = SyncFrom::AnyEntity;
} }
@ -217,6 +222,10 @@ impl NetSync for SkillSet {
// These are synced only from the client's own entity. // These are synced only from the client's own entity.
impl NetSync for Admin {
const SYNC_FROM: SyncFrom = SyncFrom::ClientEntity;
}
impl NetSync for Combo { impl NetSync for Combo {
const SYNC_FROM: SyncFrom = SyncFrom::ClientEntity; const SYNC_FROM: SyncFrom = SyncFrom::ClientEntity;
} }
@ -228,7 +237,3 @@ impl NetSync for ActiveAbilities {
impl NetSync for CanBuild { impl NetSync for CanBuild {
const SYNC_FROM: SyncFrom = SyncFrom::ClientEntity; const SYNC_FROM: SyncFrom = SyncFrom::ClientEntity;
} }
impl NetSync for LootOwner {
const SYNC_FROM: SyncFrom = SyncFrom::AnyEntity;
}

View File

@ -242,18 +242,20 @@ pub enum ServerChatCommand {
Adminify, Adminify,
Airship, Airship,
Alias, Alias,
ApplyBuff,
Ban, Ban,
BattleMode, BattleMode,
BattleModeForce, BattleModeForce,
Body, Body,
Buff,
Build, Build,
BuildAreaAdd, BuildAreaAdd,
BuildAreaList, BuildAreaList,
BuildAreaRemove, BuildAreaRemove,
Campfire, Campfire,
CreateLocation,
DebugColumn, DebugColumn,
DebugWays, DebugWays,
DeleteLocation,
DisconnectAllPlayers, DisconnectAllPlayers,
DropAll, DropAll,
Dummy, Dummy,
@ -277,9 +279,12 @@ pub enum ServerChatCommand {
Kit, Kit,
Lantern, Lantern,
Light, Light,
Lightning,
Location,
MakeBlock, MakeBlock,
MakeNpc, MakeNpc,
MakeSprite, MakeSprite,
MakeVolume,
Motd, Motd,
Object, Object,
PermitBuild, PermitBuild,
@ -305,15 +310,10 @@ pub enum ServerChatCommand {
Unban, Unban,
Version, Version,
Waypoint, Waypoint,
WeatherZone,
Whitelist, Whitelist,
Wiring, Wiring,
World, World,
MakeVolume,
Location,
CreateLocation,
DeleteLocation,
WeatherZone,
Lightning,
} }
impl ServerChatCommand { impl ServerChatCommand {
@ -339,7 +339,7 @@ impl ServerChatCommand {
"Change your alias", "Change your alias",
Some(Moderator), Some(Moderator),
), ),
ServerChatCommand::ApplyBuff => cmd( ServerChatCommand::Buff => cmd(
vec![ vec![
Enum("buff", BUFFS.clone(), Required), Enum("buff", BUFFS.clone(), Required),
Float("strength", 0.01, Optional), Float("strength", 0.01, Optional),
@ -734,11 +734,11 @@ impl ServerChatCommand {
ServerChatCommand::Adminify => "adminify", ServerChatCommand::Adminify => "adminify",
ServerChatCommand::Airship => "airship", ServerChatCommand::Airship => "airship",
ServerChatCommand::Alias => "alias", ServerChatCommand::Alias => "alias",
ServerChatCommand::ApplyBuff => "buff",
ServerChatCommand::Ban => "ban", ServerChatCommand::Ban => "ban",
ServerChatCommand::BattleMode => "battlemode", ServerChatCommand::BattleMode => "battlemode",
ServerChatCommand::BattleModeForce => "battlemode_force", ServerChatCommand::BattleModeForce => "battlemode_force",
ServerChatCommand::Body => "body", ServerChatCommand::Body => "body",
ServerChatCommand::Buff => "buff",
ServerChatCommand::Build => "build", ServerChatCommand::Build => "build",
ServerChatCommand::BuildAreaAdd => "build_area_add", ServerChatCommand::BuildAreaAdd => "build_area_add",
ServerChatCommand::BuildAreaList => "build_area_list", ServerChatCommand::BuildAreaList => "build_area_list",
@ -759,14 +759,14 @@ impl ServerChatCommand {
ServerChatCommand::GroupPromote => "group_promote", ServerChatCommand::GroupPromote => "group_promote",
ServerChatCommand::GroupLeave => "group_leave", ServerChatCommand::GroupLeave => "group_leave",
ServerChatCommand::Health => "health", ServerChatCommand::Health => "health",
ServerChatCommand::JoinFaction => "join_faction",
ServerChatCommand::Help => "help", ServerChatCommand::Help => "help",
ServerChatCommand::Home => "home", ServerChatCommand::Home => "home",
ServerChatCommand::JoinFaction => "join_faction",
ServerChatCommand::Jump => "jump", ServerChatCommand::Jump => "jump",
ServerChatCommand::Kick => "kick", ServerChatCommand::Kick => "kick",
ServerChatCommand::Kill => "kill", ServerChatCommand::Kill => "kill",
ServerChatCommand::Kit => "kit",
ServerChatCommand::KillNpcs => "kill_npcs", ServerChatCommand::KillNpcs => "kill_npcs",
ServerChatCommand::Kit => "kit",
ServerChatCommand::Lantern => "lantern", ServerChatCommand::Lantern => "lantern",
ServerChatCommand::Light => "light", ServerChatCommand::Light => "light",
ServerChatCommand::MakeBlock => "make_block", ServerChatCommand::MakeBlock => "make_block",
@ -824,7 +824,9 @@ impl ServerChatCommand {
} }
/// Produce an iterator over all the available commands /// Produce an iterator over all the available commands
pub fn iter() -> impl Iterator<Item = Self> { <Self as IntoEnumIterator>::iter() } pub fn iter() -> impl Iterator<Item = Self> + Clone {
<Self as IntoEnumIterator>::iter()
}
/// A message that explains what the command does /// A message that explains what the command does
pub fn help_string(&self) -> String { pub fn help_string(&self) -> String {
@ -1042,6 +1044,18 @@ mod tests {
use super::*; use super::*;
use crate::comp::Item; use crate::comp::Item;
#[test]
fn verify_cmd_list_sorted() {
let mut list = ServerChatCommand::iter()
.map(|c| c.keyword())
.collect::<Vec<_>>();
// Vec::is_sorted is unstable, so we do it the hard way
let list2 = list.clone();
list.sort_unstable();
assert_eq!(list, list2);
}
#[test] #[test]
fn test_loading_skill_presets() { SkillPresetManifest::load_expect(PRESET_MANIFEST_PATH); } fn test_loading_skill_presets() { SkillPresetManifest::load_expect(PRESET_MANIFEST_PATH); }

View File

@ -1,17 +1,18 @@
use clap::arg_enum; use clap::arg_enum;
use specs::Component; use serde::{Deserialize, Serialize};
use specs::{Component, DerefFlaggedStorage, VecStorage};
arg_enum! { arg_enum! {
#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)] #[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd, Serialize, Deserialize)]
pub enum AdminRole { pub enum AdminRole {
Moderator = 0, Moderator = 0,
Admin = 1, Admin = 1,
} }
} }
#[derive(Clone, Copy)] #[derive(Copy, Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct Admin(pub AdminRole); pub struct Admin(pub AdminRole);
impl Component for Admin { impl Component for Admin {
type Storage = specs::VecStorage<Self>; type Storage = DerefFlaggedStorage<Self, VecStorage<Self>>;
} }

View File

@ -18,6 +18,7 @@ use crate::{
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use specs::Entity as EcsEntity; use specs::Entity as EcsEntity;
use std::{collections::VecDeque, ops::DerefMut, sync::Mutex}; use std::{collections::VecDeque, ops::DerefMut, sync::Mutex};
use uuid::Uuid;
use vek::*; use vek::*;
pub type SiteId = u64; pub type SiteId = u64;
@ -227,6 +228,11 @@ pub enum ServerEvent {
entity: EcsEntity, entity: EcsEntity,
update: comp::MapMarkerChange, update: comp::MapMarkerChange,
}, },
MakeAdmin {
entity: EcsEntity,
admin: comp::Admin,
uuid: Uuid,
},
} }
pub struct EventBus<E> { pub struct EventBus<E> {

View File

@ -203,6 +203,7 @@ impl State {
ecs.register::<comp::BeamSegment>(); ecs.register::<comp::BeamSegment>();
ecs.register::<comp::Alignment>(); ecs.register::<comp::Alignment>();
ecs.register::<comp::LootOwner>(); ecs.register::<comp::LootOwner>();
ecs.register::<comp::Admin>();
// Register components send from clients -> server // Register components send from clients -> server
ecs.register::<comp::Controller>(); ecs.register::<comp::Controller>();
@ -236,7 +237,6 @@ impl State {
ecs.register::<comp::WaypointArea>(); ecs.register::<comp::WaypointArea>();
ecs.register::<comp::ForceUpdate>(); ecs.register::<comp::ForceUpdate>();
ecs.register::<comp::InventoryUpdate>(); ecs.register::<comp::InventoryUpdate>();
ecs.register::<comp::Admin>();
ecs.register::<comp::Waypoint>(); ecs.register::<comp::Waypoint>();
ecs.register::<comp::MapMarker>(); ecs.register::<comp::MapMarker>();
ecs.register::<comp::Projectile>(); ecs.register::<comp::Projectile>();

View File

@ -1,7 +1,6 @@
//! # Implementing new commands. //! # Implementing new commands.
//! To implement a new command provide a handler function //! To implement a new command provide a handler function
//! in [do_command]. //! in [do_command].
use crate::{ use crate::{
client::Client, client::Client,
location::Locations, location::Locations,
@ -125,11 +124,11 @@ fn do_command(
ServerChatCommand::Adminify => handle_adminify, ServerChatCommand::Adminify => handle_adminify,
ServerChatCommand::Airship => handle_spawn_airship, ServerChatCommand::Airship => handle_spawn_airship,
ServerChatCommand::Alias => handle_alias, ServerChatCommand::Alias => handle_alias,
ServerChatCommand::ApplyBuff => handle_apply_buff,
ServerChatCommand::Ban => handle_ban, ServerChatCommand::Ban => handle_ban,
ServerChatCommand::BattleMode => handle_battlemode, ServerChatCommand::BattleMode => handle_battlemode,
ServerChatCommand::BattleModeForce => handle_battlemode_force, ServerChatCommand::BattleModeForce => handle_battlemode_force,
ServerChatCommand::Body => handle_body, ServerChatCommand::Body => handle_body,
ServerChatCommand::Buff => handle_buff,
ServerChatCommand::Build => handle_build, ServerChatCommand::Build => handle_build,
ServerChatCommand::BuildAreaAdd => handle_build_area_add, ServerChatCommand::BuildAreaAdd => handle_build_area_add,
ServerChatCommand::BuildAreaList => handle_build_area_list, ServerChatCommand::BuildAreaList => handle_build_area_list,
@ -3510,7 +3509,7 @@ fn handle_server_physics(
} }
} }
fn handle_apply_buff( fn handle_buff(
server: &mut Server, server: &mut Server,
_client: EcsEntity, _client: EcsEntity,
target: EcsEntity, target: EcsEntity,

View File

@ -11,6 +11,7 @@ use crate::{
sys::terrain::SAFE_ZONE_RADIUS, sys::terrain::SAFE_ZONE_RADIUS,
Server, SpawnPoint, StateExt, Server, SpawnPoint, StateExt,
}; };
use authc::Uuid;
use common::{ use common::{
combat, combat,
combat::DamageContributor, combat::DamageContributor,
@ -1458,3 +1459,16 @@ pub fn handle_update_map_marker(
} }
} }
} }
pub fn handle_make_admin(server: &mut Server, entity: EcsEntity, admin: comp::Admin, uuid: Uuid) {
if server
.state
.read_storage::<Player>()
.get(entity)
.map_or(false, |player| player.uuid() == uuid)
{
server
.state
.write_component_ignore_entity_dead(entity, admin);
}
}

View File

@ -13,7 +13,8 @@ use entity_manipulation::{
handle_aura, handle_bonk, handle_buff, handle_change_ability, handle_combo_change, handle_aura, handle_bonk, handle_buff, handle_change_ability, handle_combo_change,
handle_delete, handle_destroy, handle_energy_change, handle_entity_attacked_hook, handle_delete, handle_destroy, handle_energy_change, handle_entity_attacked_hook,
handle_explosion, handle_health_change, handle_knockback, handle_land_on_ground, handle_explosion, handle_health_change, handle_knockback, handle_land_on_ground,
handle_parry_hook, handle_poise, handle_respawn, handle_teleport_to, handle_update_map_marker, handle_make_admin, handle_parry_hook, handle_poise, handle_respawn, handle_teleport_to,
handle_update_map_marker,
}; };
use group_manip::handle_group; use group_manip::handle_group;
use information::handle_site_info; use information::handle_site_info;
@ -288,6 +289,11 @@ impl Server {
ServerEvent::UpdateMapMarker { entity, update } => { ServerEvent::UpdateMapMarker { entity, update } => {
handle_update_map_marker(self, entity, update) handle_update_map_marker(self, entity, update)
}, },
ServerEvent::MakeAdmin {
entity,
admin,
uuid,
} => handle_make_admin(self, entity, admin, uuid),
} }
} }

View File

@ -61,10 +61,10 @@ pub struct ReadData<'a> {
pub struct Sys; pub struct Sys;
impl<'a> System<'a> for Sys { impl<'a> System<'a> for Sys {
type SystemData = ( type SystemData = (
Read<'a, EventBus<ServerEvent>>,
ReadData<'a>, ReadData<'a>,
WriteStorage<'a, Client>, WriteStorage<'a, Client>,
WriteStorage<'a, Player>, WriteStorage<'a, Player>,
WriteStorage<'a, Admin>,
WriteStorage<'a, PendingLogin>, WriteStorage<'a, PendingLogin>,
); );
@ -74,7 +74,7 @@ impl<'a> System<'a> for Sys {
fn run( fn run(
_job: &mut Job<Self>, _job: &mut Job<Self>,
(read_data, mut clients, mut players, mut admins, mut pending_logins): Self::SystemData, (event_bus, read_data, mut clients, mut players, mut pending_logins): Self::SystemData,
) { ) {
// Player list to send new players, and lookup from UUID to entity (so we don't // Player list to send new players, and lookup from UUID to entity (so we don't
// have to do a linear scan over all entities on each login to see if // have to do a linear scan over all entities on each login to see if
@ -87,7 +87,7 @@ impl<'a> System<'a> for Sys {
&read_data.uids, &read_data.uids,
&players, &players,
read_data.stats.maybe(), read_data.stats.maybe(),
admins.maybe(), read_data.trackers.admin.maybe(),
) )
.join() .join()
.map(|(entity, uid, player, stats, admin)| { .map(|(entity, uid, player, stats, admin)| {
@ -379,6 +379,7 @@ impl<'a> System<'a> for Sys {
.into_values() .into_values()
.map(|(entity, player, admin, msg)| { .map(|(entity, player, admin, msg)| {
let username = &player.alias; let username = &player.alias;
let uuid = player.uuid();
info!(?username, "New User"); info!(?username, "New User");
// Add Player component to this client. // Add Player component to this client.
// //
@ -396,9 +397,13 @@ impl<'a> System<'a> for Sys {
// Give the Admin component to the player if their name exists in // Give the Admin component to the player if their name exists in
// admin list // admin list
if let Some(admin) = admin { if let Some(admin) = admin {
admins // We need to defer writing to the Admin storage since it's borrowed immutably
.insert(entity, Admin(admin.role.into())) // by this system via TrackedStorages.
.expect("Inserting into players proves the entity exists."); event_bus.emit_now(ServerEvent::MakeAdmin {
entity,
admin: Admin(admin.role.into()),
uuid,
});
} }
msg msg
}) })

View File

@ -87,6 +87,7 @@ specs = { version = "0.18", features = ["serde", "storage-event-control", "deriv
# Mathematics # Mathematics
vek = {version = "0.15.8", features = ["serde"]} vek = {version = "0.15.8", features = ["serde"]}
levenshtein = "1.0.5"
# Controller # Controller
gilrs = {version = "0.10.0", features = ["serde-serialize"]} gilrs = {version = "0.10.0", features = ["serde-serialize"]}

View File

@ -4,13 +4,15 @@ use crate::{
render::ExperimentalShader, session::settings_change::change_render_mode, GlobalState, render::ExperimentalShader, session::settings_change::change_render_mode, GlobalState,
}; };
use client::Client; use client::Client;
use common::{cmd::*, parse_cmd_args, uuid::Uuid}; use common::{cmd::*, comp::Admin, parse_cmd_args, uuid::Uuid};
use levenshtein::levenshtein;
use strum::IntoEnumIterator; use strum::IntoEnumIterator;
// Please keep this sorted alphabetically, same as with server commands :-) // Please keep this sorted alphabetically, same as with server commands :-)
#[derive(Clone, Copy, strum::EnumIter)] #[derive(Clone, Copy, strum::EnumIter)]
pub enum ClientChatCommand { pub enum ClientChatCommand {
ExperimentalShader, ExperimentalShader,
Help,
Mute, Mute,
Unmute, Unmute,
} }
@ -21,16 +23,6 @@ impl ClientChatCommand {
use Requirement::*; use Requirement::*;
let cmd = ChatCommandData::new; let cmd = ChatCommandData::new;
match self { match self {
ClientChatCommand::Mute => cmd(
vec![PlayerName(Required)],
"Mutes chat messages from a player.",
None,
),
ClientChatCommand::Unmute => cmd(
vec![PlayerName(Required)],
"Unmutes a player muted with the 'mute' command.",
None,
),
ClientChatCommand::ExperimentalShader => cmd( ClientChatCommand::ExperimentalShader => cmd(
vec![Enum( vec![Enum(
"Shader", "Shader",
@ -42,14 +34,30 @@ impl ClientChatCommand {
"Toggles an experimental shader.", "Toggles an experimental shader.",
None, None,
), ),
ClientChatCommand::Help => cmd(
vec![Command(Optional)],
"Display information about commands",
None,
),
ClientChatCommand::Mute => cmd(
vec![PlayerName(Required)],
"Mutes chat messages from a player.",
None,
),
ClientChatCommand::Unmute => cmd(
vec![PlayerName(Required)],
"Unmutes a player muted with the 'mute' command.",
None,
),
} }
} }
pub fn keyword(&self) -> &'static str { pub fn keyword(&self) -> &'static str {
match self { match self {
ClientChatCommand::ExperimentalShader => "experimental_shader",
ClientChatCommand::Help => "help",
ClientChatCommand::Mute => "mute", ClientChatCommand::Mute => "mute",
ClientChatCommand::Unmute => "unmute", ClientChatCommand::Unmute => "unmute",
ClientChatCommand::ExperimentalShader => "experimental_shader",
} }
} }
@ -85,7 +93,9 @@ impl ClientChatCommand {
} }
/// Produce an iterator over all the available commands /// Produce an iterator over all the available commands
pub fn iter() -> impl Iterator<Item = Self> { <Self as strum::IntoEnumIterator>::iter() } pub fn iter() -> impl Iterator<Item = Self> + Clone {
<Self as strum::IntoEnumIterator>::iter()
}
/// Produce an iterator that first goes over all the short keywords /// Produce an iterator that first goes over all the short keywords
/// and their associated commands and then iterates over all the normal /// and their associated commands and then iterates over all the normal
@ -113,15 +123,15 @@ pub enum ChatCommandKind {
} }
impl FromStr for ChatCommandKind { impl FromStr for ChatCommandKind {
type Err = String; type Err = ();
fn from_str(s: &str) -> Result<Self, String> { fn from_str(s: &str) -> Result<Self, ()> {
if let Ok(cmd) = s.parse::<ClientChatCommand>() { if let Ok(cmd) = s.parse::<ClientChatCommand>() {
Ok(ChatCommandKind::Client(cmd)) Ok(ChatCommandKind::Client(cmd))
} else if let Ok(cmd) = s.parse::<ServerChatCommand>() { } else if let Ok(cmd) = s.parse::<ServerChatCommand>() {
Ok(ChatCommandKind::Server(cmd)) Ok(ChatCommandKind::Server(cmd))
} else { } else {
Err(format!("Could not find a command named {}.", s)) Err(())
} }
} }
} }
@ -142,19 +152,49 @@ pub fn run_command(
cmd: &str, cmd: &str,
args: Vec<String>, args: Vec<String>,
) -> CommandResult { ) -> CommandResult {
let command = ChatCommandKind::from_str(cmd)?; let command = ChatCommandKind::from_str(cmd);
match command { match command {
ChatCommandKind::Server(cmd) => { Ok(ChatCommandKind::Server(cmd)) => {
client.send_command(cmd.keyword().into(), args); client.send_command(cmd.keyword().into(), args);
Ok(None) // The server will provide a response when the command is run Ok(None) // The server will provide a response when the command is run
}, },
ChatCommandKind::Client(cmd) => { Ok(ChatCommandKind::Client(cmd)) => {
Ok(Some(run_client_command(client, global_state, cmd, args)?)) Ok(Some(run_client_command(client, global_state, cmd, args)?))
}, },
Err(()) => Err(invalid_command_message(client, cmd.to_string())),
} }
} }
fn invalid_command_message(client: &Client, user_entered_invalid_command: String) -> String {
let entity_role = client
.state()
.read_storage::<Admin>()
.get(client.entity())
.map(|admin| admin.0);
let usable_commands = ServerChatCommand::iter()
.filter(|cmd| cmd.needs_role() <= entity_role)
.map(|cmd| cmd.keyword())
.chain(ClientChatCommand::iter().map(|cmd| cmd.keyword()));
let most_similar_str = usable_commands
.clone()
.min_by_key(|cmd| levenshtein(&user_entered_invalid_command, cmd))
.expect("At least one command exists.");
let commands_with_same_prefix = usable_commands
.filter(|cmd| cmd.starts_with(&user_entered_invalid_command) && cmd != &most_similar_str);
format!(
"Could not find a command named {}. Did you mean any of the following? \n/{} {} \n\nType \
/help to see a list of all commands.",
user_entered_invalid_command,
most_similar_str,
commands_with_same_prefix.fold(String::new(), |s, arg| s + "\n/" + arg)
)
}
fn run_client_command( fn run_client_command(
client: &mut Client, client: &mut Client,
global_state: &mut GlobalState, global_state: &mut GlobalState,
@ -162,14 +202,52 @@ fn run_client_command(
args: Vec<String>, args: Vec<String>,
) -> Result<String, String> { ) -> Result<String, String> {
let command = match command { let command = match command {
ClientChatCommand::ExperimentalShader => handle_experimental_shader,
ClientChatCommand::Help => handle_help,
ClientChatCommand::Mute => handle_mute, ClientChatCommand::Mute => handle_mute,
ClientChatCommand::Unmute => handle_unmute, ClientChatCommand::Unmute => handle_unmute,
ClientChatCommand::ExperimentalShader => handle_experimental_shader,
}; };
command(client, global_state, args) command(client, global_state, args)
} }
fn handle_help(
client: &Client,
_global_state: &mut GlobalState,
args: Vec<String>,
) -> Result<String, String> {
if let Some(cmd) = parse_cmd_args!(args, ServerChatCommand) {
Ok(cmd.help_string())
} else {
let mut message = String::new();
let entity_role = client
.state()
.read_storage::<Admin>()
.get(client.entity())
.map(|admin| admin.0);
ClientChatCommand::iter().for_each(|cmd| {
message += &cmd.help_string();
message += "\n";
});
// Iterate through all ServerChatCommands you have permission to use.
ServerChatCommand::iter()
.filter(|cmd| cmd.needs_role() <= entity_role)
.for_each(|cmd| {
message += &cmd.help_string();
message += "\n";
});
message += "Additionally, you can use the following shortcuts:";
ServerChatCommand::iter()
.filter(|cmd| cmd.needs_role() <= entity_role)
.filter_map(|cmd| cmd.short_keyword().map(|k| (k, cmd)))
.for_each(|(k, cmd)| {
message += &format!(" /{} => /{}", k, cmd.keyword());
});
Ok(message)
}
}
fn handle_mute( fn handle_mute(
client: &Client, client: &Client,
global_state: &mut GlobalState, global_state: &mut GlobalState,