Add a client-side mutelist

This commit is contained in:
Samantha W 2022-06-14 20:35:01 +00:00 committed by Dominik Broński
parent 0ad359f3c8
commit 57ab1c5767
19 changed files with 965 additions and 649 deletions

View File

@ -8,7 +8,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased] ## [Unreleased]
### Added ### Added
- Chat commands to mute and unmute players
- Waypoints saved between sessions and shared with group members. - Waypoints saved between sessions and shared with group members.
- New rocks - New rocks
- Weapon trails - Weapon trails

View File

@ -1,149 +0,0 @@
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::SiteName(_) => complete_site(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(),
ArgumentSpec::Boolean(_, part, _) => vec!["true", "false"]
.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()
.map(|player_info| &player_info.player_alias)
.filter(|alias| alias.starts_with(part))
.cloned()
.collect()
}
fn complete_site(mut part: &str, client: &Client) -> Vec<String> {
if let Some(p) = part.strip_prefix('"') {
part = p;
}
client
.sites
.values()
.filter_map(|site| match site.site.kind {
common_net::msg::world_msg::SiteKind::Cave => None,
_ => site.site.name.as_ref(),
})
.filter(|name| name.starts_with(part))
.map(|name| {
if name.contains(' ') {
format!("\"{}\"", name)
} else {
name.clone()
}
})
.collect()
}
fn complete_command(part: &str) -> Vec<String> {
let part = part.strip_prefix('/').unwrap_or(part);
ChatCommand::iter_with_keywords()
.map(|(kwd, _)| kwd)
.filter(|kwd| kwd.starts_with(part))
.map(|kwd| format!("/{}", kwd))
.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);
}
}
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.starts_with('/') {
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)
}
}

View File

@ -3,7 +3,6 @@
#![feature(label_break_value, option_zip)] #![feature(label_break_value, option_zip)]
pub mod addr; pub mod addr;
pub mod cmd;
pub mod error; pub mod error;
// Reexports // Reexports

View File

@ -14,6 +14,7 @@ use common::{
terrain::{Block, TerrainChunk, TerrainChunkMeta, TerrainChunkSize}, terrain::{Block, TerrainChunk, TerrainChunkMeta, TerrainChunkSize},
trade::{PendingTrade, SitePrices, TradeId, TradeResult}, trade::{PendingTrade, SitePrices, TradeId, TradeResult},
uid::Uid, uid::Uid,
uuid::Uuid,
}; };
use hashbrown::HashMap; use hashbrown::HashMap;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
@ -229,6 +230,7 @@ pub struct PlayerInfo {
pub is_online: bool, pub is_online: bool,
pub player_alias: String, pub player_alias: String,
pub character: Option<CharacterInfo>, pub character: Option<CharacterInfo>,
pub uuid: Uuid,
} }
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize)]

View File

@ -1,11 +1,11 @@
use veloren_common::cmd::ChatCommand; use veloren_common::cmd::ServerChatCommand;
/// This binary generates the markdown table used for the `players/commands.md` /// This binary generates the markdown table used for the `players/commands.md`
/// page in the Veloren Book. It can be run with `cargo cmd-doc-gen`. /// page in the Veloren Book. It can be run with `cargo cmd-doc-gen`.
fn main() { fn main() {
println!("|Command|Description|Requires|Arguments|"); println!("|Command|Description|Requires|Arguments|");
println!("|-|-|-|-|"); println!("|-|-|-|-|");
for cmd in ChatCommand::iter() { for cmd in ServerChatCommand::iter() {
let args = cmd let args = cmd
.data() .data()
.args .args

View File

@ -39,82 +39,6 @@ impl ChatCommandData {
} }
} }
// Please keep this sorted alphabetically :-)
#[derive(Copy, Clone, strum::EnumIter)]
pub enum ChatCommand {
Adminify,
Airship,
Alias,
ApplyBuff,
Ban,
BattleMode,
BattleModeForce,
Build,
BuildAreaAdd,
BuildAreaList,
BuildAreaRemove,
Campfire,
DebugColumn,
DisconnectAllPlayers,
DropAll,
Dummy,
Explosion,
Faction,
GiveItem,
Goto,
Group,
GroupInvite,
GroupKick,
GroupLeave,
GroupPromote,
Health,
Help,
Home,
JoinFaction,
Jump,
Kick,
Kill,
KillNpcs,
Kit,
Lantern,
Light,
MakeBlock,
MakeNpc,
MakeSprite,
Motd,
Object,
PermitBuild,
Players,
Region,
ReloadChunks,
RemoveLights,
RevokeBuild,
RevokeBuildAll,
Safezone,
Say,
ServerPhysics,
SetMotd,
Ship,
Site,
SkillPoint,
SkillPreset,
Spawn,
Sudo,
Tell,
Time,
Tp,
Unban,
Version,
Waypoint,
Whitelist,
Wiring,
World,
MakeVolume,
Location,
CreateLocation,
DeleteLocation,
}
#[derive(Debug, PartialEq, Serialize, Deserialize, Clone)] #[derive(Debug, PartialEq, Serialize, Deserialize, Clone)]
pub enum KitSpec { pub enum KitSpec {
Item(String), Item(String),
@ -299,30 +223,106 @@ lazy_static! {
}; };
} }
impl ChatCommand { // Please keep this sorted alphabetically :-)
#[derive(Copy, Clone, strum::EnumIter)]
pub enum ServerChatCommand {
Adminify,
Airship,
Alias,
ApplyBuff,
Ban,
BattleMode,
BattleModeForce,
Build,
BuildAreaAdd,
BuildAreaList,
BuildAreaRemove,
Campfire,
DebugColumn,
DisconnectAllPlayers,
DropAll,
Dummy,
Explosion,
Faction,
GiveItem,
Goto,
Group,
GroupInvite,
GroupKick,
GroupLeave,
GroupPromote,
Health,
Help,
Home,
JoinFaction,
Jump,
Kick,
Kill,
KillNpcs,
Kit,
Lantern,
Light,
MakeBlock,
MakeNpc,
MakeSprite,
Motd,
Object,
PermitBuild,
Players,
Region,
ReloadChunks,
RemoveLights,
RevokeBuild,
RevokeBuildAll,
Safezone,
Say,
ServerPhysics,
SetMotd,
Ship,
Site,
SkillPoint,
SkillPreset,
Spawn,
Sudo,
Tell,
Time,
Tp,
Unban,
Version,
Waypoint,
Whitelist,
Wiring,
World,
MakeVolume,
Location,
CreateLocation,
DeleteLocation,
}
impl ServerChatCommand {
pub fn data(&self) -> ChatCommandData { pub fn data(&self) -> ChatCommandData {
use ArgumentSpec::*; use ArgumentSpec::*;
use Requirement::*; use Requirement::*;
use Role::*; use Role::*;
let cmd = ChatCommandData::new; let cmd = ChatCommandData::new;
match self { match self {
ChatCommand::Adminify => cmd( ServerChatCommand::Adminify => cmd(
vec![PlayerName(Required), Enum("role", ROLES.clone(), Optional)], vec![PlayerName(Required), Enum("role", ROLES.clone(), Optional)],
"Temporarily gives a player a restricted admin role or removes the current one \ "Temporarily gives a player a restricted admin role or removes the current one \
(if not given)", (if not given)",
Some(Admin), Some(Admin),
), ),
ChatCommand::Airship => cmd( ServerChatCommand::Airship => cmd(
vec![Float("destination_degrees_ccw_of_east", 90.0, Optional)], vec![Float("destination_degrees_ccw_of_east", 90.0, Optional)],
"Spawns an airship", "Spawns an airship",
Some(Admin), Some(Admin),
), ),
ChatCommand::Alias => cmd( ServerChatCommand::Alias => cmd(
vec![Any("name", Required)], vec![Any("name", Required)],
"Change your alias", "Change your alias",
Some(Moderator), Some(Moderator),
), ),
ChatCommand::ApplyBuff => cmd( ServerChatCommand::ApplyBuff => cmd(
vec![ vec![
Enum("buff", BUFFS.clone(), Required), Enum("buff", BUFFS.clone(), Required),
Float("strength", 0.01, Optional), Float("strength", 0.01, Optional),
@ -331,7 +331,7 @@ impl ChatCommand {
"Cast a buff on player", "Cast a buff on player",
Some(Admin), Some(Admin),
), ),
ChatCommand::Ban => cmd( ServerChatCommand::Ban => cmd(
vec![ vec![
PlayerName(Required), PlayerName(Required),
Boolean("overwrite", "true".to_string(), Optional), Boolean("overwrite", "true".to_string(), Optional),
@ -343,7 +343,7 @@ impl ChatCommand {
Some(Moderator), Some(Moderator),
), ),
#[rustfmt::skip] #[rustfmt::skip]
ChatCommand::BattleMode => cmd( ServerChatCommand::BattleMode => cmd(
vec![Enum( vec![Enum(
"battle mode", "battle mode",
vec!["pvp".to_owned(), "pve".to_owned()], vec!["pvp".to_owned(), "pve".to_owned()],
@ -354,8 +354,9 @@ impl ChatCommand {
* pve (player vs environment).\n\ * pve (player vs environment).\n\
If called without arguments will show current battle mode.", If called without arguments will show current battle mode.",
None, None,
), ),
ChatCommand::BattleModeForce => cmd( ServerChatCommand::BattleModeForce => cmd(
vec![Enum( vec![Enum(
"battle mode", "battle mode",
vec!["pvp".to_owned(), "pve".to_owned()], vec!["pvp".to_owned(), "pve".to_owned()],
@ -364,8 +365,8 @@ impl ChatCommand {
"Change your battle mode flag without any checks", "Change your battle mode flag without any checks",
Some(Admin), Some(Admin),
), ),
ChatCommand::Build => cmd(vec![], "Toggles build mode on and off", None), ServerChatCommand::Build => cmd(vec![], "Toggles build mode on and off", None),
ChatCommand::BuildAreaAdd => cmd( ServerChatCommand::BuildAreaAdd => cmd(
vec![ vec![
Any("name", Required), Any("name", Required),
Integer("xlo", 0, Required), Integer("xlo", 0, Required),
@ -378,40 +379,40 @@ impl ChatCommand {
"Adds a new build area", "Adds a new build area",
Some(Admin), Some(Admin),
), ),
ChatCommand::BuildAreaList => cmd(vec![], "List all build areas", Some(Admin)), ServerChatCommand::BuildAreaList => cmd(vec![], "List all build areas", Some(Admin)),
ChatCommand::BuildAreaRemove => cmd( ServerChatCommand::BuildAreaRemove => cmd(
vec![Any("name", Required)], vec![Any("name", Required)],
"Removes specified build area", "Removes specified build area",
Some(Admin), Some(Admin),
), ),
ChatCommand::Campfire => cmd(vec![], "Spawns a campfire", Some(Admin)), ServerChatCommand::Campfire => cmd(vec![], "Spawns a campfire", Some(Admin)),
ChatCommand::DebugColumn => cmd( ServerChatCommand::DebugColumn => cmd(
vec![Integer("x", 15000, Required), Integer("y", 15000, Required)], vec![Integer("x", 15000, Required), Integer("y", 15000, Required)],
"Prints some debug information about a column", "Prints some debug information about a column",
Some(Moderator), Some(Moderator),
), ),
ChatCommand::DisconnectAllPlayers => cmd( ServerChatCommand::DisconnectAllPlayers => cmd(
vec![Any("confirm", Required)], vec![Any("confirm", Required)],
"Disconnects all players from the server", "Disconnects all players from the server",
Some(Admin), Some(Admin),
), ),
ChatCommand::DropAll => cmd( ServerChatCommand::DropAll => cmd(
vec![], vec![],
"Drops all your items on the ground", "Drops all your items on the ground",
Some(Moderator), Some(Moderator),
), ),
ChatCommand::Dummy => cmd(vec![], "Spawns a training dummy", Some(Admin)), ServerChatCommand::Dummy => cmd(vec![], "Spawns a training dummy", Some(Admin)),
ChatCommand::Explosion => cmd( ServerChatCommand::Explosion => cmd(
vec![Float("radius", 5.0, Required)], vec![Float("radius", 5.0, Required)],
"Explodes the ground around you", "Explodes the ground around you",
Some(Admin), Some(Admin),
), ),
ChatCommand::Faction => cmd( ServerChatCommand::Faction => cmd(
vec![Message(Optional)], vec![Message(Optional)],
"Send messages to your faction", "Send messages to your faction",
None, None,
), ),
ChatCommand::GiveItem => cmd( ServerChatCommand::GiveItem => cmd(
vec![ vec![
Enum("item", ITEM_SPECS.clone(), Required), Enum("item", ITEM_SPECS.clone(), Required),
Integer("num", 1, Optional), Integer("num", 1, Optional),
@ -419,7 +420,7 @@ impl ChatCommand {
"Give yourself some items.\nFor an example or to auto complete use Tab.", "Give yourself some items.\nFor an example or to auto complete use Tab.",
Some(Admin), Some(Admin),
), ),
ChatCommand::Goto => cmd( ServerChatCommand::Goto => cmd(
vec![ vec![
Float("x", 0.0, Required), Float("x", 0.0, Required),
Float("y", 0.0, Required), Float("y", 0.0, Required),
@ -428,40 +429,42 @@ impl ChatCommand {
"Teleport to a position", "Teleport to a position",
Some(Admin), Some(Admin),
), ),
ChatCommand::Group => cmd(vec![Message(Optional)], "Send messages to your group", None), ServerChatCommand::Group => {
ChatCommand::GroupInvite => cmd( cmd(vec![Message(Optional)], "Send messages to your group", None)
},
ServerChatCommand::GroupInvite => cmd(
vec![PlayerName(Required)], vec![PlayerName(Required)],
"Invite a player to join a group", "Invite a player to join a group",
None, None,
), ),
ChatCommand::GroupKick => cmd( ServerChatCommand::GroupKick => cmd(
vec![PlayerName(Required)], vec![PlayerName(Required)],
"Remove a player from a group", "Remove a player from a group",
None, None,
), ),
ChatCommand::GroupLeave => cmd(vec![], "Leave the current group", None), ServerChatCommand::GroupLeave => cmd(vec![], "Leave the current group", None),
ChatCommand::GroupPromote => cmd( ServerChatCommand::GroupPromote => cmd(
vec![PlayerName(Required)], vec![PlayerName(Required)],
"Promote a player to group leader", "Promote a player to group leader",
None, None,
), ),
ChatCommand::Health => cmd( ServerChatCommand::Health => cmd(
vec![Integer("hp", 100, Required)], vec![Integer("hp", 100, Required)],
"Set your current health", "Set your current health",
Some(Admin), Some(Admin),
), ),
ChatCommand::Help => ChatCommandData::new( ServerChatCommand::Help => ChatCommandData::new(
vec![Command(Optional)], vec![Command(Optional)],
"Display information about commands", "Display information about commands",
None, None,
), ),
ChatCommand::Home => cmd(vec![], "Return to the home town", Some(Moderator)), ServerChatCommand::Home => cmd(vec![], "Return to the home town", Some(Moderator)),
ChatCommand::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,
), ),
ChatCommand::Jump => cmd( ServerChatCommand::Jump => cmd(
vec![ vec![
Float("x", 0.0, Required), Float("x", 0.0, Required),
Float("y", 0.0, Required), Float("y", 0.0, Required),
@ -470,19 +473,19 @@ impl ChatCommand {
"Offset your current position", "Offset your current position",
Some(Admin), Some(Admin),
), ),
ChatCommand::Kick => cmd( ServerChatCommand::Kick => cmd(
vec![PlayerName(Required), Message(Optional)], vec![PlayerName(Required), Message(Optional)],
"Kick a player with a given username", "Kick a player with a given username",
Some(Moderator), Some(Moderator),
), ),
ChatCommand::Kill => cmd(vec![], "Kill yourself", None), ServerChatCommand::Kill => cmd(vec![], "Kill yourself", None),
ChatCommand::KillNpcs => cmd(vec![], "Kill the NPCs", Some(Admin)), ServerChatCommand::KillNpcs => cmd(vec![], "Kill the NPCs", Some(Admin)),
ChatCommand::Kit => cmd( ServerChatCommand::Kit => cmd(
vec![Enum("kit_name", KITS.to_vec(), Required)], vec![Enum("kit_name", KITS.to_vec(), Required)],
"Place a set of items into your inventory.", "Place a set of items into your inventory.",
Some(Admin), Some(Admin),
), ),
ChatCommand::Lantern => cmd( ServerChatCommand::Lantern => cmd(
vec![ vec![
Float("strength", 5.0, Required), Float("strength", 5.0, Required),
Float("r", 1.0, Optional), Float("r", 1.0, Optional),
@ -492,7 +495,7 @@ impl ChatCommand {
"Change your lantern's strength and color", "Change your lantern's strength and color",
Some(Admin), Some(Admin),
), ),
ChatCommand::Light => cmd( ServerChatCommand::Light => cmd(
vec![ vec![
Float("r", 1.0, Optional), Float("r", 1.0, Optional),
Float("g", 1.0, Optional), Float("g", 1.0, Optional),
@ -505,7 +508,7 @@ impl ChatCommand {
"Spawn entity with light", "Spawn entity with light",
Some(Admin), Some(Admin),
), ),
ChatCommand::MakeBlock => cmd( ServerChatCommand::MakeBlock => cmd(
vec![ vec![
Enum("block", BLOCK_KINDS.clone(), Required), Enum("block", BLOCK_KINDS.clone(), Required),
Integer("r", 255, Optional), Integer("r", 255, Optional),
@ -515,7 +518,7 @@ impl ChatCommand {
"Make a block at your location with a color", "Make a block at your location with a color",
Some(Admin), Some(Admin),
), ),
ChatCommand::MakeNpc => cmd( ServerChatCommand::MakeNpc => cmd(
vec![ vec![
Enum("entity_config", ENTITY_CONFIGS.clone(), Required), Enum("entity_config", ENTITY_CONFIGS.clone(), Required),
Integer("num", 1, Optional), Integer("num", 1, Optional),
@ -523,59 +526,61 @@ impl ChatCommand {
"Spawn entity from config near you.\nFor an example or to auto complete use Tab.", "Spawn entity from config near you.\nFor an example or to auto complete use Tab.",
Some(Admin), Some(Admin),
), ),
ChatCommand::MakeSprite => cmd( ServerChatCommand::MakeSprite => cmd(
vec![Enum("sprite", SPRITE_KINDS.clone(), Required)], vec![Enum("sprite", SPRITE_KINDS.clone(), Required)],
"Make a sprite at your location", "Make a sprite at your location",
Some(Admin), Some(Admin),
), ),
ChatCommand::Motd => cmd(vec![Message(Optional)], "View the server description", None), ServerChatCommand::Motd => {
ChatCommand::Object => cmd( cmd(vec![Message(Optional)], "View the server description", None)
},
ServerChatCommand::Object => cmd(
vec![Enum("object", OBJECTS.clone(), Required)], vec![Enum("object", OBJECTS.clone(), Required)],
"Spawn an object", "Spawn an object",
Some(Admin), Some(Admin),
), ),
ChatCommand::PermitBuild => cmd( ServerChatCommand::PermitBuild => cmd(
vec![Any("area_name", Required)], vec![Any("area_name", Required)],
"Grants player a bounded box they can build in", "Grants player a bounded box they can build in",
Some(Admin), Some(Admin),
), ),
ChatCommand::Players => cmd(vec![], "Lists players currently online", None), ServerChatCommand::Players => cmd(vec![], "Lists players currently online", None),
ChatCommand::ReloadChunks => cmd( ServerChatCommand::ReloadChunks => cmd(
vec![], vec![],
"Reloads all chunks loaded on the server", "Reloads all chunks loaded on the server",
Some(Admin), Some(Admin),
), ),
ChatCommand::RemoveLights => cmd( ServerChatCommand::RemoveLights => cmd(
vec![Float("radius", 20.0, Optional)], vec![Float("radius", 20.0, Optional)],
"Removes all lights spawned by players", "Removes all lights spawned by players",
Some(Admin), Some(Admin),
), ),
ChatCommand::RevokeBuild => cmd( ServerChatCommand::RevokeBuild => cmd(
vec![Any("area_name", Required)], vec![Any("area_name", Required)],
"Revokes build area permission for player", "Revokes build area permission for player",
Some(Admin), Some(Admin),
), ),
ChatCommand::RevokeBuildAll => cmd( ServerChatCommand::RevokeBuildAll => cmd(
vec![], vec![],
"Revokes all build area permissions for player", "Revokes all build area permissions for player",
Some(Admin), Some(Admin),
), ),
ChatCommand::Region => cmd( ServerChatCommand::Region => cmd(
vec![Message(Optional)], vec![Message(Optional)],
"Send messages to everyone in your region of the world", "Send messages to everyone in your region of the world",
None, None,
), ),
ChatCommand::Safezone => cmd( ServerChatCommand::Safezone => cmd(
vec![Float("range", 100.0, Optional)], vec![Float("range", 100.0, Optional)],
"Creates a safezone", "Creates a safezone",
Some(Moderator), Some(Moderator),
), ),
ChatCommand::Say => cmd( ServerChatCommand::Say => cmd(
vec![Message(Optional)], vec![Message(Optional)],
"Send messages to everyone within shouting distance", "Send messages to everyone within shouting distance",
None, None,
), ),
ChatCommand::ServerPhysics => cmd( ServerChatCommand::ServerPhysics => cmd(
vec![ vec![
PlayerName(Required), PlayerName(Required),
Boolean("enabled", "true".to_string(), Optional), Boolean("enabled", "true".to_string(), Optional),
@ -583,24 +588,24 @@ impl ChatCommand {
"Set/unset server-authoritative physics for an account", "Set/unset server-authoritative physics for an account",
Some(Moderator), Some(Moderator),
), ),
ChatCommand::SetMotd => cmd( ServerChatCommand::SetMotd => cmd(
vec![Message(Optional)], vec![Message(Optional)],
"Set the server description", "Set the server description",
Some(Admin), Some(Admin),
), ),
ChatCommand::Ship => cmd( ServerChatCommand::Ship => cmd(
vec![Float("destination_degrees_ccw_of_east", 90.0, Optional)], vec![Float("destination_degrees_ccw_of_east", 90.0, Optional)],
"Spawns a ship", "Spawns a ship",
Some(Admin), Some(Admin),
), ),
// Uses Message because site names can contain spaces, // Uses Message because site names can contain spaces,
// which would be assumed to be separators otherwise // which would be assumed to be separators otherwise
ChatCommand::Site => cmd( ServerChatCommand::Site => cmd(
vec![SiteName(Required)], vec![SiteName(Required)],
"Teleport to a site", "Teleport to a site",
Some(Moderator), Some(Moderator),
), ),
ChatCommand::SkillPoint => cmd( ServerChatCommand::SkillPoint => cmd(
vec![ vec![
Enum("skill tree", SKILL_TREES.clone(), Required), Enum("skill tree", SKILL_TREES.clone(), Required),
Integer("amount", 1, Optional), Integer("amount", 1, Optional),
@ -608,12 +613,12 @@ impl ChatCommand {
"Give yourself skill points for a particular skill tree", "Give yourself skill points for a particular skill tree",
Some(Admin), Some(Admin),
), ),
ChatCommand::SkillPreset => cmd( ServerChatCommand::SkillPreset => cmd(
vec![Enum("preset_name", PRESET_LIST.to_vec(), Required)], vec![Enum("preset_name", PRESET_LIST.to_vec(), Required)],
"Gives your character desired skills.", "Gives your character desired skills.",
Some(Admin), Some(Admin),
), ),
ChatCommand::Spawn => cmd( ServerChatCommand::Spawn => cmd(
vec![ vec![
Enum("alignment", ALIGNMENTS.clone(), Required), Enum("alignment", ALIGNMENTS.clone(), Required),
Enum("entity", ENTITIES.clone(), Required), Enum("entity", ENTITIES.clone(), Required),
@ -623,58 +628,60 @@ impl ChatCommand {
"Spawn a test entity", "Spawn a test entity",
Some(Admin), Some(Admin),
), ),
ChatCommand::Sudo => cmd( ServerChatCommand::Sudo => cmd(
vec![PlayerName(Required), SubCommand], vec![PlayerName(Required), SubCommand],
"Run command as if you were another player", "Run command as if you were another player",
Some(Moderator), Some(Moderator),
), ),
ChatCommand::Tell => cmd( ServerChatCommand::Tell => cmd(
vec![PlayerName(Required), Message(Optional)], vec![PlayerName(Required), Message(Optional)],
"Send a message to another player", "Send a message to another player",
None, None,
), ),
ChatCommand::Time => cmd( ServerChatCommand::Time => cmd(
vec![Enum("time", TIMES.clone(), Optional)], vec![Enum("time", TIMES.clone(), Optional)],
"Set the time of day", "Set the time of day",
Some(Admin), Some(Admin),
), ),
ChatCommand::Tp => cmd( ServerChatCommand::Tp => cmd(
vec![PlayerName(Optional)], vec![PlayerName(Optional)],
"Teleport to another player", "Teleport to another player",
Some(Moderator), Some(Moderator),
), ),
ChatCommand::Unban => cmd( ServerChatCommand::Unban => cmd(
vec![PlayerName(Required)], vec![PlayerName(Required)],
"Remove the ban for the given username", "Remove the ban for the given username",
Some(Moderator), Some(Moderator),
), ),
ChatCommand::Version => cmd(vec![], "Prints server version", None), ServerChatCommand::Version => cmd(vec![], "Prints server version", None),
ChatCommand::Waypoint => cmd( ServerChatCommand::Waypoint => cmd(
vec![], vec![],
"Set your waypoint to your current position", "Set your waypoint to your current position",
Some(Admin), Some(Admin),
), ),
ChatCommand::Wiring => cmd(vec![], "Create wiring element", Some(Admin)), ServerChatCommand::Wiring => cmd(vec![], "Create wiring element", Some(Admin)),
ChatCommand::Whitelist => cmd( ServerChatCommand::Whitelist => cmd(
vec![Any("add/remove", Required), PlayerName(Required)], vec![Any("add/remove", Required), PlayerName(Required)],
"Adds/removes username to whitelist", "Adds/removes username to whitelist",
Some(Moderator), Some(Moderator),
), ),
ChatCommand::World => cmd( ServerChatCommand::World => cmd(
vec![Message(Optional)], vec![Message(Optional)],
"Send messages to everyone on the server", "Send messages to everyone on the server",
None, None,
), ),
ChatCommand::MakeVolume => cmd(vec![], "Create a volume (experimental)", Some(Admin)), ServerChatCommand::MakeVolume => {
ChatCommand::Location => { cmd(vec![], "Create a volume (experimental)", Some(Admin))
},
ServerChatCommand::Location => {
cmd(vec![Any("name", Required)], "Teleport to a location", None) cmd(vec![Any("name", Required)], "Teleport to a location", None)
}, },
ChatCommand::CreateLocation => cmd( ServerChatCommand::CreateLocation => cmd(
vec![Any("name", Required)], vec![Any("name", Required)],
"Create a location at the current position", "Create a location at the current position",
Some(Moderator), Some(Moderator),
), ),
ChatCommand::DeleteLocation => cmd( ServerChatCommand::DeleteLocation => cmd(
vec![Any("name", Required)], vec![Any("name", Required)],
"Delete a location", "Delete a location",
Some(Moderator), Some(Moderator),
@ -682,80 +689,80 @@ impl ChatCommand {
} }
} }
/// The keyword used to invoke the command, omitting the leading '/'. /// The keyword used to invoke the command, omitting the prefix.
pub fn keyword(&self) -> &'static str { pub fn keyword(&self) -> &'static str {
match self { match self {
ChatCommand::Adminify => "adminify", ServerChatCommand::Adminify => "adminify",
ChatCommand::Airship => "airship", ServerChatCommand::Airship => "airship",
ChatCommand::Alias => "alias", ServerChatCommand::Alias => "alias",
ChatCommand::ApplyBuff => "buff", ServerChatCommand::ApplyBuff => "buff",
ChatCommand::Ban => "ban", ServerChatCommand::Ban => "ban",
ChatCommand::BattleMode => "battlemode", ServerChatCommand::BattleMode => "battlemode",
ChatCommand::BattleModeForce => "battlemode_force", ServerChatCommand::BattleModeForce => "battlemode_force",
ChatCommand::Build => "build", ServerChatCommand::Build => "build",
ChatCommand::BuildAreaAdd => "build_area_add", ServerChatCommand::BuildAreaAdd => "build_area_add",
ChatCommand::BuildAreaList => "build_area_list", ServerChatCommand::BuildAreaList => "build_area_list",
ChatCommand::BuildAreaRemove => "build_area_remove", ServerChatCommand::BuildAreaRemove => "build_area_remove",
ChatCommand::Campfire => "campfire", ServerChatCommand::Campfire => "campfire",
ChatCommand::DebugColumn => "debug_column", ServerChatCommand::DebugColumn => "debug_column",
ChatCommand::DisconnectAllPlayers => "disconnect_all_players", ServerChatCommand::DisconnectAllPlayers => "disconnect_all_players",
ChatCommand::DropAll => "dropall", ServerChatCommand::DropAll => "dropall",
ChatCommand::Dummy => "dummy", ServerChatCommand::Dummy => "dummy",
ChatCommand::Explosion => "explosion", ServerChatCommand::Explosion => "explosion",
ChatCommand::Faction => "faction", ServerChatCommand::Faction => "faction",
ChatCommand::GiveItem => "give_item", ServerChatCommand::GiveItem => "give_item",
ChatCommand::Goto => "goto", ServerChatCommand::Goto => "goto",
ChatCommand::Group => "group", ServerChatCommand::Group => "group",
ChatCommand::GroupInvite => "group_invite", ServerChatCommand::GroupInvite => "group_invite",
ChatCommand::GroupKick => "group_kick", ServerChatCommand::GroupKick => "group_kick",
ChatCommand::GroupPromote => "group_promote", ServerChatCommand::GroupPromote => "group_promote",
ChatCommand::GroupLeave => "group_leave", ServerChatCommand::GroupLeave => "group_leave",
ChatCommand::Health => "health", ServerChatCommand::Health => "health",
ChatCommand::JoinFaction => "join_faction", ServerChatCommand::JoinFaction => "join_faction",
ChatCommand::Help => "help", ServerChatCommand::Help => "help",
ChatCommand::Home => "home", ServerChatCommand::Home => "home",
ChatCommand::Jump => "jump", ServerChatCommand::Jump => "jump",
ChatCommand::Kick => "kick", ServerChatCommand::Kick => "kick",
ChatCommand::Kill => "kill", ServerChatCommand::Kill => "kill",
ChatCommand::Kit => "kit", ServerChatCommand::Kit => "kit",
ChatCommand::KillNpcs => "kill_npcs", ServerChatCommand::KillNpcs => "kill_npcs",
ChatCommand::Lantern => "lantern", ServerChatCommand::Lantern => "lantern",
ChatCommand::Light => "light", ServerChatCommand::Light => "light",
ChatCommand::MakeBlock => "make_block", ServerChatCommand::MakeBlock => "make_block",
ChatCommand::MakeNpc => "make_npc", ServerChatCommand::MakeNpc => "make_npc",
ChatCommand::MakeSprite => "make_sprite", ServerChatCommand::MakeSprite => "make_sprite",
ChatCommand::Motd => "motd", ServerChatCommand::Motd => "motd",
ChatCommand::Object => "object", ServerChatCommand::Object => "object",
ChatCommand::PermitBuild => "permit_build", ServerChatCommand::PermitBuild => "permit_build",
ChatCommand::Players => "players", ServerChatCommand::Players => "players",
ChatCommand::Region => "region", ServerChatCommand::Region => "region",
ChatCommand::ReloadChunks => "reload_chunks", ServerChatCommand::ReloadChunks => "reload_chunks",
ChatCommand::RemoveLights => "remove_lights", ServerChatCommand::RemoveLights => "remove_lights",
ChatCommand::RevokeBuild => "revoke_build", ServerChatCommand::RevokeBuild => "revoke_build",
ChatCommand::RevokeBuildAll => "revoke_build_all", ServerChatCommand::RevokeBuildAll => "revoke_build_all",
ChatCommand::Safezone => "safezone", ServerChatCommand::Safezone => "safezone",
ChatCommand::Say => "say", ServerChatCommand::Say => "say",
ChatCommand::ServerPhysics => "server_physics", ServerChatCommand::ServerPhysics => "server_physics",
ChatCommand::SetMotd => "set_motd", ServerChatCommand::SetMotd => "set_motd",
ChatCommand::Ship => "ship", ServerChatCommand::Ship => "ship",
ChatCommand::Site => "site", ServerChatCommand::Site => "site",
ChatCommand::SkillPoint => "skill_point", ServerChatCommand::SkillPoint => "skill_point",
ChatCommand::SkillPreset => "skill_preset", ServerChatCommand::SkillPreset => "skill_preset",
ChatCommand::Spawn => "spawn", ServerChatCommand::Spawn => "spawn",
ChatCommand::Sudo => "sudo", ServerChatCommand::Sudo => "sudo",
ChatCommand::Tell => "tell", ServerChatCommand::Tell => "tell",
ChatCommand::Time => "time", ServerChatCommand::Time => "time",
ChatCommand::Tp => "tp", ServerChatCommand::Tp => "tp",
ChatCommand::Unban => "unban", ServerChatCommand::Unban => "unban",
ChatCommand::Version => "version", ServerChatCommand::Version => "version",
ChatCommand::Waypoint => "waypoint", ServerChatCommand::Waypoint => "waypoint",
ChatCommand::Wiring => "wiring", ServerChatCommand::Wiring => "wiring",
ChatCommand::Whitelist => "whitelist", ServerChatCommand::Whitelist => "whitelist",
ChatCommand::World => "world", ServerChatCommand::World => "world",
ChatCommand::MakeVolume => "make_volume", ServerChatCommand::MakeVolume => "make_volume",
ChatCommand::Location => "location", ServerChatCommand::Location => "location",
ChatCommand::CreateLocation => "create_location", ServerChatCommand::CreateLocation => "create_location",
ChatCommand::DeleteLocation => "delete_location", ServerChatCommand::DeleteLocation => "delete_location",
} }
} }
@ -763,16 +770,19 @@ impl ChatCommand {
/// Returns None if the command doesn't have a short keyword /// Returns None if the command doesn't have a short keyword
pub fn short_keyword(&self) -> Option<&'static str> { pub fn short_keyword(&self) -> Option<&'static str> {
Some(match self { Some(match self {
ChatCommand::Faction => "f", ServerChatCommand::Faction => "f",
ChatCommand::Group => "g", ServerChatCommand::Group => "g",
ChatCommand::Region => "r", ServerChatCommand::Region => "r",
ChatCommand::Say => "s", ServerChatCommand::Say => "s",
ChatCommand::Tell => "t", ServerChatCommand::Tell => "t",
ChatCommand::World => "w", ServerChatCommand::World => "w",
_ => return None, _ => return None,
}) })
} }
/// Produce an iterator over all the available commands
pub fn iter() -> impl Iterator<Item = Self> { <Self as strum::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 {
let data = self.data(); let data = self.data();
@ -783,9 +793,17 @@ impl ChatCommand {
format!("{}: {}", usage, data.description) format!("{}: {}", usage, data.description)
} }
/// A boolean that is used to check whether the command requires /// Produce an iterator that first goes over all the short keywords
/// administrator permissions or not. /// and their associated commands and then iterates over all the normal
pub fn needs_role(&self) -> Option<Role> { self.data().needs_role } /// keywords with their associated commands
pub fn iter_with_keywords() -> impl Iterator<Item = (&'static str, Self)> {
Self::iter()
// Go through all the shortcuts first
.filter_map(|c| c.short_keyword().map(|s| (s, c)))
.chain(Self::iter().map(|c| (c.keyword(), c)))
}
pub fn needs_role(&self) -> Option<comp::AdminRole> { self.data().needs_role }
/// Returns a format string for parsing arguments with scan_fmt /// Returns a format string for parsing arguments with scan_fmt
pub fn arg_fmt(&self) -> String { pub fn arg_fmt(&self) -> String {
@ -807,34 +825,22 @@ impl ChatCommand {
.collect::<Vec<_>>() .collect::<Vec<_>>()
.join(" ") .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()
// Go through all the shortcuts first
.filter_map(|c| c.short_keyword().map(|s| (s, c)))
.chain(Self::iter().map(|c| (c.keyword(), c)))
}
} }
impl Display for ChatCommand { impl Display for ServerChatCommand {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> {
write!(f, "{}", self.keyword()) write!(f, "{}", self.keyword())
} }
} }
impl FromStr for ChatCommand { impl FromStr for ServerChatCommand {
type Err = (); type Err = ();
fn from_str(keyword: &str) -> Result<ChatCommand, ()> { fn from_str(keyword: &str) -> Result<ServerChatCommand, ()> {
let keyword = keyword.strip_prefix('/').unwrap_or(keyword); Self::iter()
// Go through all the shortcuts first
Self::iter_with_keywords() .filter_map(|c| c.short_keyword().map(|s| (s, c)))
.chain(Self::iter().map(|c| (c.keyword(), c)))
// Find command with matching string as keyword // Find command with matching string as keyword
.find_map(|(kwd, command)| (kwd == keyword).then(|| command)) .find_map(|(kwd, command)| (kwd == keyword).then(|| command))
// Return error if not found // Return error if not found
@ -956,6 +962,38 @@ impl ArgumentSpec {
} }
} }
/// Parse a series of command arguments into values, including collecting all
/// trailing arguments.
#[macro_export]
macro_rules! parse_cmd_args {
($args:expr, $($t:ty),* $(, ..$tail:ty)? $(,)?) => {
{
let mut args = $args.into_iter().peekable();
(
// We only consume the input argument when parsing is successful. If this fails, we
// will then attempt to parse it as the next argument type. This is done regardless
// of whether the argument is optional because that information is not available
// here. Nevertheless, if the caller only precedes to use the parsed arguments when
// all required arguments parse successfully to `Some(val)` this should not create
// any unexpected behavior.
//
// This does mean that optional arguments will be included in the trailing args or
// that one optional arg could be interpreted as another, if the user makes a
// mistake that causes an optional arg to fail to parse. But there is no way to
// discern this in the current model with the optional args and trailing arg being
// solely position based.
$({
let parsed = args.peek().and_then(|s| s.parse::<$t>().ok());
// Consume successfully parsed arg.
if parsed.is_some() { args.next(); }
parsed
}),*
$(, args.map(|s| s.to_string()).collect::<$tail>())?
)
}
};
}
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;

File diff suppressed because it is too large Load Diff

View File

@ -431,6 +431,7 @@ pub fn handle_possess(server: &mut Server, possessor_uid: Uid, possessee_uid: Ui
name: s.name.clone(), name: s.name.clone(),
} }
}), }),
uuid: player.uuid(),
}), }),
); );
let remove_player_msg = ServerGeneral::PlayerListUpdate( let remove_player_msg = ServerGeneral::PlayerListUpdate(

View File

@ -73,7 +73,7 @@ use common::{
assets::AssetExt, assets::AssetExt,
calendar::Calendar, calendar::Calendar,
character::CharacterId, character::CharacterId,
cmd::ChatCommand, cmd::ServerChatCommand,
comp, comp,
event::{EventBus, ServerEvent}, event::{EventBus, ServerEvent},
recipe::{default_component_recipe_book, default_recipe_book}, recipe::{default_component_recipe_book, default_recipe_book},
@ -1210,7 +1210,7 @@ impl Server {
fn process_command(&mut self, entity: EcsEntity, name: String, args: Vec<String>) { fn process_command(&mut self, entity: EcsEntity, name: String, args: Vec<String>) {
// Find the command object and run its handler. // Find the command object and run its handler.
if let Ok(command) = name.parse::<ChatCommand>() { if let Ok(command) = name.parse::<ServerChatCommand>() {
command.execute(self, entity, args); command.execute(self, entity, args);
} else { } else {
#[cfg(feature = "plugins")] #[cfg(feature = "plugins")]

View File

@ -88,6 +88,7 @@ impl<'a> System<'a> for Sys {
character: stats.map(|stats| CharacterInfo { character: stats.map(|stats| CharacterInfo {
name: stats.name.clone(), name: stats.name.clone(),
}), }),
uuid: player.uuid(),
}) })
}) })
.collect::<HashMap<_, _>>(); .collect::<HashMap<_, _>>();
@ -239,6 +240,7 @@ impl<'a> System<'a> for Sys {
is_online: true, is_online: true,
is_moderator: admins.get(entity).is_some(), is_moderator: admins.get(entity).is_some(),
character: None, // new players will be on character select. character: None, // new players will be on character select.
uuid: player.uuid(),
}), }),
))); )));
} }

View File

@ -1,5 +1,5 @@
use crate::{AdminCommandState, EguiAction, EguiActions}; use crate::{AdminCommandState, EguiAction, EguiActions};
use common::cmd::ChatCommand; use common::cmd::ServerChatCommand;
use egui::{CollapsingHeader, CtxRef, Resize, Slider, Ui, Vec2, Window}; use egui::{CollapsingHeader, CtxRef, Resize, Slider, Ui, Vec2, Window};
use lazy_static::lazy_static; use lazy_static::lazy_static;
@ -45,7 +45,7 @@ fn draw_kits(ui: &mut Ui, state: &mut AdminCommandState, egui_actions: &mut Egui
ui.vertical(|ui| { ui.vertical(|ui| {
if ui.button("Give Kit").clicked() { if ui.button("Give Kit").clicked() {
egui_actions.actions.push(EguiAction::ChatCommand { egui_actions.actions.push(EguiAction::ChatCommand {
cmd: ChatCommand::Kit, cmd: ServerChatCommand::Kit,
args: vec![common::cmd::KITS[state.kits_selected_idx].clone()], args: vec![common::cmd::KITS[state.kits_selected_idx].clone()],
}); });
}; };
@ -67,7 +67,7 @@ fn draw_give_items(ui: &mut Ui, state: &mut AdminCommandState, egui_actions: &mu
); );
if ui.button("Give Items").clicked() { if ui.button("Give Items").clicked() {
egui_actions.actions.push(EguiAction::ChatCommand { egui_actions.actions.push(EguiAction::ChatCommand {
cmd: ChatCommand::GiveItem, cmd: ServerChatCommand::GiveItem,
args: vec![ args: vec![
format!( format!(
"common.items.{}", "common.items.{}",

View File

@ -10,6 +10,7 @@ mod widgets;
use client::{Client, Join, World, WorldExt}; use client::{Client, Join, World, WorldExt};
use common::{ use common::{
cmd::ServerChatCommand,
comp, comp,
comp::{inventory::item::armor::Friction, Poise, PoiseState}, comp::{inventory::item::armor::Friction, Poise, PoiseState},
}; };
@ -24,10 +25,7 @@ use crate::{
admin::draw_admin_commands_window, character_states::draw_char_state_group, admin::draw_admin_commands_window, character_states::draw_char_state_group,
experimental_shaders::draw_experimental_shaders_window, widgets::two_col_row, experimental_shaders::draw_experimental_shaders_window, widgets::two_col_row,
}; };
use common::{ use common::comp::{aura::AuraKind::Buff, Body, Fluid};
cmd::ChatCommand,
comp::{aura::AuraKind::Buff, Body, Fluid},
};
use egui_winit_platform::Platform; use egui_winit_platform::Platform;
use std::time::Duration; use std::time::Duration;
#[cfg(feature = "use-dyn-lib")] #[cfg(feature = "use-dyn-lib")]
@ -131,7 +129,10 @@ pub enum EguiDebugShapeAction {
} }
pub enum EguiAction { pub enum EguiAction {
ChatCommand { cmd: ChatCommand, args: Vec<String> }, ChatCommand {
cmd: ServerChatCommand,
args: Vec<String>,
},
DebugShape(EguiDebugShapeAction), DebugShape(EguiDebugShapeAction),
SetExperimentalShader(String, bool), SetExperimentalShader(String, bool),
} }
@ -613,13 +614,20 @@ fn selected_entity_window(
.spacing([40.0, 4.0]) .spacing([40.0, 4.0])
.max_col_width(100.0) .max_col_width(100.0)
.striped(true) .striped(true)
.show(ui, |ui| #[rustfmt::skip] { // Apparently, if the #[rustfmt::skip] is in front of the closure scope, rust-analyzer can't
// parse the code properly. Things will *sometimes* work if the skip is on the other side of
// the opening bracket (even though that should only skip formatting the first line of the
// closure), but things as arbitrary as adding a comment to the code cause it to be formatted
// again. Thus, there is a completely pointless inner scope in this closure, just so that the
// code doesn't take up an unreasonable amount of space when formatted. We need that space for
// interesting and educational code comments like this one.
.show(ui, |ui| { #[rustfmt::skip] {
ui.label("State"); ui.label("State");
poise_state_label(ui, poise); poise_state_label(ui, poise);
ui.end_row(); ui.end_row();
two_col_row(ui, "Current", format!("{:.1}/{:.1}", poise.current(), poise.maximum())); two_col_row(ui, "Current", format!("{:.1}/{:.1}", poise.current(), poise.maximum()));
two_col_row(ui, "Base Max", format!("{:.1}", poise.base_max())); two_col_row(ui, "Base Max", format!("{:.1}", poise.base_max()));
}); }});
}); });
} }

411
voxygen/src/cmd.rs Normal file
View File

@ -0,0 +1,411 @@
use std::str::FromStr;
use crate::GlobalState;
use client::Client;
use common::{cmd::*, parse_cmd_args, uuid::Uuid};
// 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(|| command))
.ok_or(())
}
}
#[derive(Clone, Copy)]
pub enum ChatCommandKind {
Client(ClientChatCommand),
Server(ServerChatCommand),
}
impl FromStr for ChatCommandKind {
type Err = String;
fn from_str(s: &str) -> Result<Self, String> {
if let Ok(cmd) = s.parse::<ClientChatCommand>() {
Ok(ChatCommandKind::Client(cmd))
} else if let Ok(cmd) = s.parse::<ServerChatCommand>() {
Ok(ChatCommandKind::Server(cmd))
} else {
Err(format!("Could not find a command named {}.", s))
}
}
}
/// Represents the feedback shown to the user of a command, if any. Server
/// commands give their feedback as an event, so in those cases this will always
/// be Ok(None). An Err variant will be be displayed with the error icon and
/// text color
type CommandResult = Result<Option<String>, String>;
/// Runs a command by either sending it to the server or processing it
/// locally. Returns a String to be output to the chat.
// Note: it's not clear what data future commands will need access to, so the
// signature of this function might change
pub fn run_command(
client: &mut Client,
global_state: &mut GlobalState,
cmd: &str,
args: Vec<String>,
) -> CommandResult {
let command = ChatCommandKind::from_str(cmd)?;
match command {
ChatCommandKind::Server(cmd) => {
client.send_command(cmd.keyword().into(), args);
Ok(None) // The server will provide a response when the command is run
},
ChatCommandKind::Client(cmd) => {
Ok(Some(run_client_command(client, global_state, cmd, args)?))
},
}
}
fn run_client_command(
client: &mut Client,
global_state: &mut GlobalState,
command: ClientChatCommand,
args: Vec<String>,
) -> Result<String, String> {
match command {
ClientChatCommand::Mute => handle_mute(client, global_state, args),
ClientChatCommand::Unmute => handle_unmute(client, global_state, args),
}
}
fn handle_mute(
client: &Client,
global_state: &mut GlobalState,
args: Vec<String>,
) -> Result<String, String> {
if let Some(alias) = parse_cmd_args!(args, String) {
let target = client
.player_list()
.values()
.find(|p| p.player_alias == alias)
.ok_or_else(|| format!("Could not find a player named {}", alias))?;
if let Some(me) = client.uid().and_then(|uid| client.player_list().get(&uid)) {
if target.uuid == me.uuid {
return Err("You cannot mute yourself.".to_string());
}
}
if global_state
.profile
.mutelist
.insert(target.uuid, alias.clone())
.is_none()
{
Ok(format!("Successfully muted player {}.", alias))
} else {
Err(format!("{} is already muted.", alias))
}
} else {
Err("You must specify a player to mute.".to_string())
}
}
fn handle_unmute(
client: &Client,
global_state: &mut GlobalState,
args: Vec<String>,
) -> Result<String, String> {
// Note that we don't care if this is a real player, so that it's possible
// to unmute someone when they're offline
if let Some(alias) = parse_cmd_args!(args, String) {
if let Some(uuid) = global_state
.profile
.mutelist
.iter()
.find(|(_, v)| **v == alias)
.map(|(k, _)| *k)
{
if let Some(me) = client.uid().and_then(|uid| client.player_list().get(&uid)) {
if uuid == me.uuid {
return Err("You cannot unmute yourself.".to_string());
}
}
global_state.profile.mutelist.remove(&uuid);
Ok(format!("Successfully unmuted player {}.", alias))
} else {
Err(format!("Could not find a muted player named {}.", alias))
}
} else {
Err("You must specify a player to unmute.".to_string())
}
}
/// A helper function to get the Uuid of a player with a given alias
pub fn get_player_uuid(client: &Client, alias: &String) -> Option<Uuid> {
client
.player_list()
.values()
.find(|p| p.player_alias == *alias)
.map(|p| p.uuid)
}
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::SiteName(_) => complete_site(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(),
ArgumentSpec::Boolean(_, part, _) => vec!["true", "false"]
.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()
.map(|player_info| &player_info.player_alias)
.filter(|alias| alias.starts_with(part))
.cloned()
.collect()
}
fn complete_site(mut part: &str, client: &Client) -> Vec<String> {
if let Some(p) = part.strip_prefix('"') {
part = p;
}
client
.sites()
.values()
.filter_map(|site| match site.site.kind {
common_net::msg::world_msg::SiteKind::Cave => None,
_ => site.site.name.as_ref(),
})
.filter(|name| name.starts_with(part))
.map(|name| {
if name.contains(' ') {
format!("\"{}\"", name)
} else {
name.clone()
}
})
.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);
}
}
None
}
fn complete_command(part: &str, prefix: char) -> Vec<String> {
ServerChatCommand::iter_with_keywords()
.map(|(kwd, _)| kwd)
.chain(ClientChatCommand::iter_with_keywords().map(|(kwd, _)| kwd))
.filter(|kwd| kwd.starts_with(part))
.map(|kwd| format!("{}{}", prefix, kwd))
.collect()
}
pub fn complete(line: &str, client: &Client, cmd_prefix: char) -> Vec<String> {
let word = if line.chars().last().map_or(true, char::is_whitespace) {
""
} else {
line.split_whitespace().last().unwrap_or("")
};
if line.starts_with(cmd_prefix) {
let line = line.strip_prefix(cmd_prefix).unwrap_or(line);
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. This is the start of the line so the prefix
// will be part of it
let word = word.strip_prefix(cmd_prefix).unwrap_or(word);
return complete_command(word, cmd_prefix);
}
let args = {
if let Ok(cmd) = cmd.parse::<ServerChatCommand>() {
Some(cmd.data().args)
} else if let Ok(cmd) = cmd.parse::<ClientChatCommand>() {
Some(cmd.data().args)
} else {
None
}
};
if let Some(args) = args {
if let Some(arg) = args.get(i - 1) {
// Complete ith argument
arg.complete(word, client)
} else {
// Complete past the last argument
match args.last() {
Some(ArgumentSpec::SubCommand) => {
if let Some(index) = nth_word(line, args.len()) {
complete(&line[index..], client, cmd_prefix)
} 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)
}
}
#[test]
fn verify_cmd_list_sorted() {
let mut list = ClientChatCommand::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]
fn test_complete_command() {
assert_eq!(complete_command("mu", '/'), vec!["/mute".to_string()]);
assert_eq!(complete_command("unba", '/'), vec!["/unban".to_string()]);
assert_eq!(complete_command("make_", '/'), vec![
"/make_block".to_string(),
"/make_npc".to_string(),
"/make_sprite".to_string(),
"/make_volume".to_string()
]);
}

View File

@ -2,8 +2,8 @@ use super::{
img_ids::Imgs, ChatTab, ERROR_COLOR, FACTION_COLOR, GROUP_COLOR, INFO_COLOR, KILL_COLOR, img_ids::Imgs, ChatTab, ERROR_COLOR, FACTION_COLOR, GROUP_COLOR, INFO_COLOR, KILL_COLOR,
OFFLINE_COLOR, ONLINE_COLOR, REGION_COLOR, SAY_COLOR, TELL_COLOR, TEXT_COLOR, WORLD_COLOR, OFFLINE_COLOR, ONLINE_COLOR, REGION_COLOR, SAY_COLOR, TELL_COLOR, TEXT_COLOR, WORLD_COLOR,
}; };
use crate::{settings::chat::MAX_CHAT_TABS, ui::fonts::Fonts, GlobalState}; use crate::{cmd::complete, settings::chat::MAX_CHAT_TABS, ui::fonts::Fonts, GlobalState};
use client::{cmd, Client}; use client::Client;
use common::comp::{ use common::comp::{
chat::{KillSource, KillType}, chat::{KillSource, KillType},
group::Role, group::Role,
@ -108,7 +108,11 @@ impl<'a> Chat<'a> {
pub fn prepare_tab_completion(mut self, input: String) -> Self { pub fn prepare_tab_completion(mut self, input: String) -> Self {
self.force_completions = if let Some(index) = input.find('\t') { self.force_completions = if let Some(index) = input.find('\t') {
Some(cmd::complete(&input[..index], self.client)) Some(complete(
&input[..index],
self.client,
self.global_state.settings.chat.chat_cmd_prefix,
))
} else { } else {
None None
}; };
@ -659,7 +663,7 @@ impl<'a> Widget for Chat<'a> {
s.history.truncate(self.history_max); s.history.truncate(self.history_max);
} }
}); });
if let Some(msg) = msg.strip_prefix('/') { if let Some(msg) = msg.strip_prefix(chat_settings.chat_cmd_prefix) {
match parse_cmd(msg) { match parse_cmd(msg) {
Ok((name, args)) => events.push(Event::SendCommand(name, args)), Ok((name, args)) => events.push(Event::SendCommand(name, args)),
Err(err) => self.new_messages.push_back(ChatMsg { Err(err) => self.new_messages.push_back(ChatMsg {

View File

@ -53,6 +53,7 @@ use social::Social;
use trade::Trade; use trade::Trade;
use crate::{ use crate::{
cmd::get_player_uuid,
ecs::{comp as vcomp, comp::HpFloaterList}, ecs::{comp as vcomp, comp::HpFloaterList},
game_input::GameInput, game_input::GameInput,
hud::{img_ids::ImgsRot, prompt_dialog::DialogOutcomeEvent}, hud::{img_ids::ImgsRot, prompt_dialog::DialogOutcomeEvent},
@ -1789,6 +1790,21 @@ impl Hud {
self.speech_bubbles self.speech_bubbles
.retain(|_uid, bubble| bubble.timeout > now); .retain(|_uid, bubble| bubble.timeout > now);
// Don't show messages from muted players
self.new_messages.retain(|msg| match msg.uid() {
Some(uid) => match client.player_list().get(&uid) {
Some(player_info) => {
if let Some(uuid) = get_player_uuid(client, &player_info.player_alias) {
!global_state.profile.mutelist.contains_key(&uuid)
} else {
true
}
},
None => true,
},
None => true,
});
// Push speech bubbles // Push speech bubbles
for msg in self.new_messages.iter() { for msg in self.new_messages.iter() {
if let Some((bubble, uid)) = msg.to_bubble() { if let Some((bubble, uid)) = msg.to_bubble() {

View File

@ -18,6 +18,7 @@
#[macro_use] #[macro_use]
pub mod ui; pub mod ui;
pub mod audio; pub mod audio;
pub mod cmd;
pub mod controller; pub mod controller;
mod credits; mod credits;
mod ecs; mod ecs;

View File

@ -1,5 +1,5 @@
use crate::hud; use crate::hud;
use common::character::CharacterId; use common::{character::CharacterId, uuid::Uuid};
use hashbrown::HashMap; use hashbrown::HashMap;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::{ use std::{
@ -57,6 +57,7 @@ impl Default for ServerProfile {
#[serde(default)] #[serde(default)]
pub struct Profile { pub struct Profile {
pub servers: HashMap<String, ServerProfile>, pub servers: HashMap<String, ServerProfile>,
pub mutelist: HashMap<Uuid, String>,
/// Temporary character profile, used when it should /// Temporary character profile, used when it should
/// not be persisted to the disk. /// not be persisted to the disk.
#[serde(skip)] #[serde(skip)]

View File

@ -41,6 +41,7 @@ use common_net::{
use crate::{ use crate::{
audio::sfx::SfxEvent, audio::sfx::SfxEvent,
cmd::run_command,
error::Error, error::Error,
game_input::GameInput, game_input::GameInput,
hud::{ hud::{
@ -1155,7 +1156,16 @@ impl PlayState for SessionState {
self.client.borrow_mut().send_chat(msg); self.client.borrow_mut().send_chat(msg);
}, },
HudEvent::SendCommand(name, args) => { HudEvent::SendCommand(name, args) => {
self.client.borrow_mut().send_command(name, args); match run_command(&mut self.client.borrow_mut(), global_state, &name, args)
{
Ok(Some(info)) => {
self.hud.new_message(ChatType::CommandInfo.chat_msg(&info))
},
Ok(None) => {}, // Server will provide an info message
Err(error) => {
self.hud.new_message(ChatType::CommandError.chat_msg(error))
},
};
}, },
HudEvent::CharacterSelection => { HudEvent::CharacterSelection => {
self.client.borrow_mut().request_remove_character() self.client.borrow_mut().request_remove_character()

View File

@ -73,6 +73,7 @@ pub struct ChatSettings {
pub chat_character_name: bool, pub chat_character_name: bool,
pub chat_tabs: Vec<ChatTab>, pub chat_tabs: Vec<ChatTab>,
pub chat_tab_index: Option<usize>, pub chat_tab_index: Option<usize>,
pub chat_cmd_prefix: char,
} }
impl Default for ChatSettings { impl Default for ChatSettings {
@ -82,6 +83,7 @@ impl Default for ChatSettings {
chat_character_name: true, chat_character_name: true,
chat_tabs: vec![ChatTab::default()], chat_tabs: vec![ChatTab::default()],
chat_tab_index: Some(0), chat_tab_index: Some(0),
chat_cmd_prefix: '/',
} }
} }
} }