mirror of
https://gitlab.com/veloren/veloren.git
synced 2024-08-30 18:12:32 +00:00
move client commands to voxygen + add Admin to be shared with client for permissions + unit test for alphabetical server command
This commit is contained in:
parent
361e5204e3
commit
74cb514094
2
Cargo.lock
generated
2
Cargo.lock
generated
@ -6970,7 +6970,6 @@ dependencies = [
|
|||||||
"humantime",
|
"humantime",
|
||||||
"itertools",
|
"itertools",
|
||||||
"lazy_static",
|
"lazy_static",
|
||||||
"levenshtein",
|
|
||||||
"noise",
|
"noise",
|
||||||
"num_cpus",
|
"num_cpus",
|
||||||
"parking_lot 0.12.0",
|
"parking_lot 0.12.0",
|
||||||
@ -7087,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",
|
||||||
|
@ -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;
|
|
||||||
}
|
|
||||||
|
@ -236,103 +236,17 @@ lazy_static! {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// Please keep this sorted alphabetically, same as with server commands :-)
|
|
||||||
#[derive(Clone, Copy, strum::EnumIter)]
|
|
||||||
pub enum ClientChatCommand {
|
|
||||||
Mute,
|
|
||||||
Unmute,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ClientChatCommand {
|
|
||||||
pub fn data(&self) -> ChatCommandData {
|
|
||||||
use ArgumentSpec::*;
|
|
||||||
use Requirement::*;
|
|
||||||
let cmd = ChatCommandData::new;
|
|
||||||
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,
|
|
||||||
),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn keyword(&self) -> &'static str {
|
|
||||||
match self {
|
|
||||||
ClientChatCommand::Mute => "mute",
|
|
||||||
ClientChatCommand::Unmute => "unmute",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A message that explains what the command does
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns a format string for parsing arguments with scan_fmt
|
|
||||||
pub fn arg_fmt(&self) -> String {
|
|
||||||
self.data()
|
|
||||||
.args
|
|
||||||
.iter()
|
|
||||||
.map(|arg| match arg {
|
|
||||||
ArgumentSpec::PlayerName(_) => "{}",
|
|
||||||
ArgumentSpec::SiteName(_) => "{/.*/}",
|
|
||||||
ArgumentSpec::Float(_, _, _) => "{}",
|
|
||||||
ArgumentSpec::Integer(_, _, _) => "{d}",
|
|
||||||
ArgumentSpec::Any(_, _) => "{}",
|
|
||||||
ArgumentSpec::Command(_) => "{}",
|
|
||||||
ArgumentSpec::Message(_) => "{/.*/}",
|
|
||||||
ArgumentSpec::SubCommand => "{} {/.*/}",
|
|
||||||
ArgumentSpec::Enum(_, _, _) => "{}",
|
|
||||||
ArgumentSpec::Boolean(_, _, _) => "{}",
|
|
||||||
})
|
|
||||||
.collect::<Vec<_>>()
|
|
||||||
.join(" ")
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Produce an iterator over all the available commands
|
|
||||||
pub fn iter() -> impl Iterator<Item = Self> { <Self as strum::IntoEnumIterator>::iter() }
|
|
||||||
|
|
||||||
/// Produce an iterator that first goes over all the short keywords
|
|
||||||
/// and their associated commands and then iterates over all the normal
|
|
||||||
/// keywords with their associated commands
|
|
||||||
pub fn iter_with_keywords() -> impl Iterator<Item = (&'static str, Self)> {
|
|
||||||
Self::iter().map(|c| (c.keyword(), c))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl FromStr for ClientChatCommand {
|
|
||||||
type Err = ();
|
|
||||||
|
|
||||||
fn from_str(keyword: &str) -> Result<ClientChatCommand, ()> {
|
|
||||||
Self::iter()
|
|
||||||
.map(|c| (c.keyword(), c))
|
|
||||||
.find_map(|(kwd, command)| (kwd == keyword).then_some(command))
|
|
||||||
.ok_or(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Please keep this sorted alphabetically :-)
|
// Please keep this sorted alphabetically :-)
|
||||||
#[derive(Copy, Clone, strum::EnumIter)]
|
#[derive(Copy, Clone, strum::EnumIter)]
|
||||||
pub enum ServerChatCommand {
|
pub enum ServerChatCommand {
|
||||||
Adminify,
|
Adminify,
|
||||||
Airship,
|
Airship,
|
||||||
Alias,
|
Alias,
|
||||||
ApplyBuff,
|
|
||||||
Ban,
|
Ban,
|
||||||
BattleMode,
|
BattleMode,
|
||||||
BattleModeForce,
|
BattleModeForce,
|
||||||
Body,
|
Body,
|
||||||
|
ApplyBuff,
|
||||||
Build,
|
Build,
|
||||||
BuildAreaAdd,
|
BuildAreaAdd,
|
||||||
BuildAreaList,
|
BuildAreaList,
|
||||||
@ -355,9 +269,7 @@ pub enum ServerChatCommand {
|
|||||||
GroupLeave,
|
GroupLeave,
|
||||||
GroupPromote,
|
GroupPromote,
|
||||||
Health,
|
Health,
|
||||||
Help,
|
|
||||||
Home,
|
Home,
|
||||||
InvalidCommand,
|
|
||||||
JoinFaction,
|
JoinFaction,
|
||||||
Jump,
|
Jump,
|
||||||
Kick,
|
Kick,
|
||||||
@ -567,18 +479,12 @@ impl ServerChatCommand {
|
|||||||
"Set your current health",
|
"Set your current health",
|
||||||
Some(Admin),
|
Some(Admin),
|
||||||
),
|
),
|
||||||
ServerChatCommand::Help => ChatCommandData::new(
|
|
||||||
vec![Command(Optional)],
|
|
||||||
"Display information about commands",
|
|
||||||
None,
|
|
||||||
),
|
|
||||||
ServerChatCommand::Home => cmd(vec![], "Return to the home town", Some(Moderator)),
|
ServerChatCommand::Home => cmd(vec![], "Return to the home town", Some(Moderator)),
|
||||||
ServerChatCommand::JoinFaction => ChatCommandData::new(
|
ServerChatCommand::JoinFaction => ChatCommandData::new(
|
||||||
vec![Any("faction", Optional)],
|
vec![Any("faction", Optional)],
|
||||||
"Join/leave the specified faction",
|
"Join/leave the specified faction",
|
||||||
None,
|
None,
|
||||||
),
|
),
|
||||||
ServerChatCommand::InvalidCommand => cmd(vec![], "", Some(Moderator)),
|
|
||||||
ServerChatCommand::Jump => cmd(
|
ServerChatCommand::Jump => cmd(
|
||||||
vec![
|
vec![
|
||||||
Float("x", 0.0, Required),
|
Float("x", 0.0, Required),
|
||||||
@ -847,15 +753,13 @@ 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::Home => "home",
|
ServerChatCommand::Home => "home",
|
||||||
ServerChatCommand::InvalidCommand => "invalid_command",
|
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",
|
||||||
@ -913,7 +817,7 @@ 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 {
|
||||||
@ -1131,6 +1035,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); }
|
||||||
|
|
||||||
|
@ -1,17 +1,18 @@
|
|||||||
use clap::arg_enum;
|
use clap::arg_enum;
|
||||||
use specs::Component;
|
use serde::{Deserialize, Serialize};
|
||||||
|
use specs::{Component, DenseVecStorage, DerefFlaggedStorage};
|
||||||
|
|
||||||
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, DenseVecStorage<Self>>;
|
||||||
}
|
}
|
||||||
|
@ -227,6 +227,10 @@ pub enum ServerEvent {
|
|||||||
entity: EcsEntity,
|
entity: EcsEntity,
|
||||||
update: comp::MapMarkerChange,
|
update: comp::MapMarkerChange,
|
||||||
},
|
},
|
||||||
|
MakeAdmin {
|
||||||
|
entity: EcsEntity,
|
||||||
|
admin: comp::Admin,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct EventBus<E> {
|
pub struct EventBus<E> {
|
||||||
|
@ -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>();
|
||||||
|
@ -71,5 +71,3 @@ refinery = { git = "https://gitlab.com/veloren/refinery.git", rev = "8ecf4b4772d
|
|||||||
|
|
||||||
# Plugins
|
# Plugins
|
||||||
plugin-api = { package = "veloren-plugin-api", path = "../plugin/api"}
|
plugin-api = { package = "veloren-plugin-api", path = "../plugin/api"}
|
||||||
|
|
||||||
levenshtein = "1.0.5"
|
|
||||||
|
@ -1,9 +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].
|
||||||
|
|
||||||
extern crate levenshtein;
|
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
client::Client,
|
client::Client,
|
||||||
location::Locations,
|
location::Locations,
|
||||||
@ -25,7 +22,7 @@ use common::{
|
|||||||
assets,
|
assets,
|
||||||
calendar::Calendar,
|
calendar::Calendar,
|
||||||
cmd::{
|
cmd::{
|
||||||
ClientChatCommand, KitSpec, ServerChatCommand, BUFF_PACK, BUFF_PARSER, ITEM_SPECS, KIT_MANIFEST_PATH,
|
KitSpec, ServerChatCommand, BUFF_PACK, BUFF_PARSER, ITEM_SPECS, KIT_MANIFEST_PATH,
|
||||||
PRESET_MANIFEST_PATH,
|
PRESET_MANIFEST_PATH,
|
||||||
},
|
},
|
||||||
comp::{
|
comp::{
|
||||||
@ -59,8 +56,6 @@ use common_state::{BuildAreaError, BuildAreas};
|
|||||||
use core::{cmp::Ordering, convert::TryFrom, time::Duration};
|
use core::{cmp::Ordering, convert::TryFrom, time::Duration};
|
||||||
use hashbrown::{HashMap, HashSet};
|
use hashbrown::{HashMap, HashSet};
|
||||||
use humantime::Duration as HumanDuration;
|
use humantime::Duration as HumanDuration;
|
||||||
use itertools::Itertools;
|
|
||||||
use levenshtein::levenshtein;
|
|
||||||
use rand::{thread_rng, Rng};
|
use rand::{thread_rng, Rng};
|
||||||
use specs::{
|
use specs::{
|
||||||
saveload::MarkerAllocator, storage::StorageEntry, Builder, Entity as EcsEntity, Join, WorldExt,
|
saveload::MarkerAllocator, storage::StorageEntry, Builder, Entity as EcsEntity, Join, WorldExt,
|
||||||
@ -154,9 +149,7 @@ fn do_command(
|
|||||||
ServerChatCommand::GroupLeave => handle_group_leave,
|
ServerChatCommand::GroupLeave => handle_group_leave,
|
||||||
ServerChatCommand::GroupPromote => handle_group_promote,
|
ServerChatCommand::GroupPromote => handle_group_promote,
|
||||||
ServerChatCommand::Health => handle_health,
|
ServerChatCommand::Health => handle_health,
|
||||||
ServerChatCommand::Help => handle_help,
|
|
||||||
ServerChatCommand::Home => handle_home,
|
ServerChatCommand::Home => handle_home,
|
||||||
ServerChatCommand::InvalidCommand => handle_invalid_command,
|
|
||||||
ServerChatCommand::JoinFaction => handle_join_faction,
|
ServerChatCommand::JoinFaction => handle_join_faction,
|
||||||
ServerChatCommand::Jump => handle_jump,
|
ServerChatCommand::Jump => handle_jump,
|
||||||
ServerChatCommand::Kick => handle_kick,
|
ServerChatCommand::Kick => handle_kick,
|
||||||
@ -810,37 +803,6 @@ fn handle_set_motd(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn handle_invalid_command(
|
|
||||||
server: &mut Server,
|
|
||||||
client: EcsEntity,
|
|
||||||
_target: EcsEntity,
|
|
||||||
args: Vec<String>,
|
|
||||||
_action: &ServerChatCommand,
|
|
||||||
) -> CmdResult<()> {
|
|
||||||
if let Some(user_entered_invalid_command) = parse_cmd_args!(args, String) {
|
|
||||||
let entity_role = server.entity_admin_role(client);
|
|
||||||
|
|
||||||
let most_similar_str = ServerChatCommand::iter()
|
|
||||||
.filter(|cmd| cmd.needs_role() <= entity_role)
|
|
||||||
.map(|cmd| cmd.keyword())
|
|
||||||
.chain(ClientChatCommand::iter().map(|cmd| cmd.keyword()))
|
|
||||||
.sorted_by_key(|cmd| levenshtein(&*user_entered_invalid_command, cmd))
|
|
||||||
.collect::<Vec<&str>>()[0];
|
|
||||||
|
|
||||||
let commands_with_same_prefix = ServerChatCommand::iter()
|
|
||||||
.filter(|cmd|cmd.needs_role() <= entity_role)
|
|
||||||
.map(|cmd| cmd.keyword())
|
|
||||||
.chain(ClientChatCommand::iter().map(|cmd| cmd.keyword()))
|
|
||||||
.filter(|cmd| cmd.starts_with(&*user_entered_invalid_command) && cmd != &most_similar_str);
|
|
||||||
|
|
||||||
return Err(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)))
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn handle_jump(
|
fn handle_jump(
|
||||||
server: &mut Server,
|
server: &mut Server,
|
||||||
_client: EcsEntity,
|
_client: EcsEntity,
|
||||||
@ -1817,50 +1779,6 @@ fn handle_build_area_remove(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn handle_help(
|
|
||||||
server: &mut Server,
|
|
||||||
client: EcsEntity,
|
|
||||||
_target: EcsEntity,
|
|
||||||
args: Vec<String>,
|
|
||||||
_action: &ServerChatCommand,
|
|
||||||
) -> CmdResult<()> {
|
|
||||||
if let Some(cmd) = parse_cmd_args!(args, ServerChatCommand) {
|
|
||||||
server.notify_client(
|
|
||||||
client,
|
|
||||||
ServerGeneral::server_msg(ChatType::CommandInfo, cmd.help_string()),
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
let mut message = String::new();
|
|
||||||
let entity_role = server.entity_admin_role(client);
|
|
||||||
|
|
||||||
// Iterate through the ClientChatCommands Mute and Unmute.
|
|
||||||
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 && cmd.keyword() != "invalid_command")
|
|
||||||
.for_each(|cmd| {
|
|
||||||
message += &cmd.help_string();
|
|
||||||
message += "\n";
|
|
||||||
});
|
|
||||||
message += "Additionally, you can use the following shortcuts:";
|
|
||||||
ServerChatCommand::iter()
|
|
||||||
.filter_map(|cmd| cmd.short_keyword().map(|k| (k, cmd)))
|
|
||||||
.for_each(|(k, cmd)| {
|
|
||||||
message += &format!(" /{} => /{}", k, cmd.keyword());
|
|
||||||
});
|
|
||||||
|
|
||||||
server.notify_client(
|
|
||||||
client,
|
|
||||||
ServerGeneral::server_msg(ChatType::CommandInfo, message),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn parse_alignment(owner: Uid, alignment: &str) -> CmdResult<Alignment> {
|
fn parse_alignment(owner: Uid, alignment: &str) -> CmdResult<Alignment> {
|
||||||
match alignment {
|
match alignment {
|
||||||
"wild" => Ok(Alignment::Wild),
|
"wild" => Ok(Alignment::Wild),
|
||||||
|
@ -288,6 +288,9 @@ 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 } => {
|
||||||
|
let _ = self.state.ecs_mut().write_storage().insert(entity, admin);
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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)| {
|
||||||
@ -396,9 +396,10 @@ 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
|
event_bus.emit_now(ServerEvent::MakeAdmin {
|
||||||
.insert(entity, Admin(admin.role.into()))
|
entity,
|
||||||
.expect("Inserting into players proves the entity exists.");
|
admin: Admin(admin.role.into()),
|
||||||
|
});
|
||||||
}
|
}
|
||||||
msg
|
msg
|
||||||
})
|
})
|
||||||
|
@ -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"]}
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
extern crate levenshtein;
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
@ -6,11 +7,16 @@ use crate::{
|
|||||||
use client::Client;
|
use client::Client;
|
||||||
use common::{cmd::*, parse_cmd_args, uuid::Uuid};
|
use common::{cmd::*, parse_cmd_args, uuid::Uuid};
|
||||||
use strum::IntoEnumIterator;
|
use strum::IntoEnumIterator;
|
||||||
|
use common_net::synced_components::Admin;
|
||||||
|
|
||||||
|
use itertools::Itertools;
|
||||||
|
use levenshtein::levenshtein;
|
||||||
|
|
||||||
// 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 +27,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 +38,26 @@ impl ClientChatCommand {
|
|||||||
"Toggles an experimental shader.",
|
"Toggles an experimental shader.",
|
||||||
None,
|
None,
|
||||||
),
|
),
|
||||||
|
ClientChatCommand::Help => cmd(vec![], "", 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,7 @@ 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
|
||||||
@ -110,19 +118,18 @@ impl FromStr for ClientChatCommand {
|
|||||||
pub enum ChatCommandKind {
|
pub enum ChatCommandKind {
|
||||||
Client(ClientChatCommand),
|
Client(ClientChatCommand),
|
||||||
Server(ServerChatCommand),
|
Server(ServerChatCommand),
|
||||||
InvalidCommand
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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 {
|
||||||
Ok(ChatCommandKind::InvalidCommand)
|
Err(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -143,21 +150,19 @@ 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)?))
|
||||||
},
|
},
|
||||||
ChatCommandKind::InvalidCommand => {
|
Err(()) => {
|
||||||
//The invalid command that user typed is passed as argument
|
Ok(Some(handle_invalid_command(client, global_state, vec![cmd.to_string()])?))
|
||||||
client.send_command(ServerChatCommand::InvalidCommand.keyword().into(), vec![cmd.to_string()]);
|
},
|
||||||
Ok(None)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -168,14 +173,89 @@ 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_map(|cmd| cmd.short_keyword().map(|k| (k, cmd)))
|
||||||
|
.for_each(|(k, cmd)| {
|
||||||
|
message += &format!(" /{} => /{}", k, cmd.keyword());
|
||||||
|
});
|
||||||
|
Ok(message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn handle_invalid_command(
|
||||||
|
client: &Client,
|
||||||
|
_global_state: &mut GlobalState,
|
||||||
|
args: Vec<String>,
|
||||||
|
) -> Result<String, String> {
|
||||||
|
if let Some(user_entered_invalid_command) = parse_cmd_args!(args, 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()
|
||||||
|
.sorted_by_key(|cmd| levenshtein(&user_entered_invalid_command, cmd))
|
||||||
|
.collect::<Vec<&str>>()[0];
|
||||||
|
|
||||||
|
let commands_with_same_prefix = usable_commands
|
||||||
|
.filter(|cmd| {
|
||||||
|
cmd.starts_with(&user_entered_invalid_command) && cmd != &most_similar_str
|
||||||
|
});
|
||||||
|
|
||||||
|
return Err(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)
|
||||||
|
));
|
||||||
|
}
|
||||||
|
Err("Command failed parsing".to_string())
|
||||||
|
}
|
||||||
|
|
||||||
fn handle_mute(
|
fn handle_mute(
|
||||||
client: &Client,
|
client: &Client,
|
||||||
global_state: &mut GlobalState,
|
global_state: &mut GlobalState,
|
||||||
|
Loading…
Reference in New Issue
Block a user