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]
### Added
- Chat commands to mute and unmute players
- Waypoints saved between sessions and shared with group members.
- New rocks
- 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)]
pub mod addr;
pub mod cmd;
pub mod error;
// Reexports

View File

@ -14,6 +14,7 @@ use common::{
terrain::{Block, TerrainChunk, TerrainChunkMeta, TerrainChunkSize},
trade::{PendingTrade, SitePrices, TradeId, TradeResult},
uid::Uid,
uuid::Uuid,
};
use hashbrown::HashMap;
use serde::{Deserialize, Serialize};
@ -229,6 +230,7 @@ pub struct PlayerInfo {
pub is_online: bool,
pub player_alias: String,
pub character: Option<CharacterInfo>,
pub uuid: Uuid,
}
#[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`
/// page in the Veloren Book. It can be run with `cargo cmd-doc-gen`.
fn main() {
println!("|Command|Description|Requires|Arguments|");
println!("|-|-|-|-|");
for cmd in ChatCommand::iter() {
for cmd in ServerChatCommand::iter() {
let args = cmd
.data()
.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)]
pub enum KitSpec {
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 {
use ArgumentSpec::*;
use Requirement::*;
use Role::*;
let cmd = ChatCommandData::new;
match self {
ChatCommand::Adminify => cmd(
ServerChatCommand::Adminify => cmd(
vec![PlayerName(Required), Enum("role", ROLES.clone(), Optional)],
"Temporarily gives a player a restricted admin role or removes the current one \
(if not given)",
Some(Admin),
),
ChatCommand::Airship => cmd(
ServerChatCommand::Airship => cmd(
vec![Float("destination_degrees_ccw_of_east", 90.0, Optional)],
"Spawns an airship",
Some(Admin),
),
ChatCommand::Alias => cmd(
ServerChatCommand::Alias => cmd(
vec![Any("name", Required)],
"Change your alias",
Some(Moderator),
),
ChatCommand::ApplyBuff => cmd(
ServerChatCommand::ApplyBuff => cmd(
vec![
Enum("buff", BUFFS.clone(), Required),
Float("strength", 0.01, Optional),
@ -331,7 +331,7 @@ impl ChatCommand {
"Cast a buff on player",
Some(Admin),
),
ChatCommand::Ban => cmd(
ServerChatCommand::Ban => cmd(
vec![
PlayerName(Required),
Boolean("overwrite", "true".to_string(), Optional),
@ -343,7 +343,7 @@ impl ChatCommand {
Some(Moderator),
),
#[rustfmt::skip]
ChatCommand::BattleMode => cmd(
ServerChatCommand::BattleMode => cmd(
vec![Enum(
"battle mode",
vec!["pvp".to_owned(), "pve".to_owned()],
@ -354,8 +354,9 @@ impl ChatCommand {
* pve (player vs environment).\n\
If called without arguments will show current battle mode.",
None,
),
ChatCommand::BattleModeForce => cmd(
ServerChatCommand::BattleModeForce => cmd(
vec![Enum(
"battle mode",
vec!["pvp".to_owned(), "pve".to_owned()],
@ -364,8 +365,8 @@ impl ChatCommand {
"Change your battle mode flag without any checks",
Some(Admin),
),
ChatCommand::Build => cmd(vec![], "Toggles build mode on and off", None),
ChatCommand::BuildAreaAdd => cmd(
ServerChatCommand::Build => cmd(vec![], "Toggles build mode on and off", None),
ServerChatCommand::BuildAreaAdd => cmd(
vec![
Any("name", Required),
Integer("xlo", 0, Required),
@ -378,40 +379,40 @@ impl ChatCommand {
"Adds a new build area",
Some(Admin),
),
ChatCommand::BuildAreaList => cmd(vec![], "List all build areas", Some(Admin)),
ChatCommand::BuildAreaRemove => cmd(
ServerChatCommand::BuildAreaList => cmd(vec![], "List all build areas", Some(Admin)),
ServerChatCommand::BuildAreaRemove => cmd(
vec![Any("name", Required)],
"Removes specified build area",
Some(Admin),
),
ChatCommand::Campfire => cmd(vec![], "Spawns a campfire", Some(Admin)),
ChatCommand::DebugColumn => cmd(
ServerChatCommand::Campfire => cmd(vec![], "Spawns a campfire", Some(Admin)),
ServerChatCommand::DebugColumn => cmd(
vec![Integer("x", 15000, Required), Integer("y", 15000, Required)],
"Prints some debug information about a column",
Some(Moderator),
),
ChatCommand::DisconnectAllPlayers => cmd(
ServerChatCommand::DisconnectAllPlayers => cmd(
vec![Any("confirm", Required)],
"Disconnects all players from the server",
Some(Admin),
),
ChatCommand::DropAll => cmd(
ServerChatCommand::DropAll => cmd(
vec![],
"Drops all your items on the ground",
Some(Moderator),
),
ChatCommand::Dummy => cmd(vec![], "Spawns a training dummy", Some(Admin)),
ChatCommand::Explosion => cmd(
ServerChatCommand::Dummy => cmd(vec![], "Spawns a training dummy", Some(Admin)),
ServerChatCommand::Explosion => cmd(
vec![Float("radius", 5.0, Required)],
"Explodes the ground around you",
Some(Admin),
),
ChatCommand::Faction => cmd(
ServerChatCommand::Faction => cmd(
vec![Message(Optional)],
"Send messages to your faction",
None,
),
ChatCommand::GiveItem => cmd(
ServerChatCommand::GiveItem => cmd(
vec![
Enum("item", ITEM_SPECS.clone(), Required),
Integer("num", 1, Optional),
@ -419,7 +420,7 @@ impl ChatCommand {
"Give yourself some items.\nFor an example or to auto complete use Tab.",
Some(Admin),
),
ChatCommand::Goto => cmd(
ServerChatCommand::Goto => cmd(
vec![
Float("x", 0.0, Required),
Float("y", 0.0, Required),
@ -428,40 +429,42 @@ impl ChatCommand {
"Teleport to a position",
Some(Admin),
),
ChatCommand::Group => cmd(vec![Message(Optional)], "Send messages to your group", None),
ChatCommand::GroupInvite => cmd(
ServerChatCommand::Group => {
cmd(vec![Message(Optional)], "Send messages to your group", None)
},
ServerChatCommand::GroupInvite => cmd(
vec![PlayerName(Required)],
"Invite a player to join a group",
None,
),
ChatCommand::GroupKick => cmd(
ServerChatCommand::GroupKick => cmd(
vec![PlayerName(Required)],
"Remove a player from a group",
None,
),
ChatCommand::GroupLeave => cmd(vec![], "Leave the current group", None),
ChatCommand::GroupPromote => cmd(
ServerChatCommand::GroupLeave => cmd(vec![], "Leave the current group", None),
ServerChatCommand::GroupPromote => cmd(
vec![PlayerName(Required)],
"Promote a player to group leader",
None,
),
ChatCommand::Health => cmd(
ServerChatCommand::Health => cmd(
vec![Integer("hp", 100, Required)],
"Set your current health",
Some(Admin),
),
ChatCommand::Help => ChatCommandData::new(
ServerChatCommand::Help => ChatCommandData::new(
vec![Command(Optional)],
"Display information about commands",
None,
),
ChatCommand::Home => cmd(vec![], "Return to the home town", Some(Moderator)),
ChatCommand::JoinFaction => ChatCommandData::new(
ServerChatCommand::Home => cmd(vec![], "Return to the home town", Some(Moderator)),
ServerChatCommand::JoinFaction => ChatCommandData::new(
vec![Any("faction", Optional)],
"Join/leave the specified faction",
None,
),
ChatCommand::Jump => cmd(
ServerChatCommand::Jump => cmd(
vec![
Float("x", 0.0, Required),
Float("y", 0.0, Required),
@ -470,19 +473,19 @@ impl ChatCommand {
"Offset your current position",
Some(Admin),
),
ChatCommand::Kick => cmd(
ServerChatCommand::Kick => cmd(
vec![PlayerName(Required), Message(Optional)],
"Kick a player with a given username",
Some(Moderator),
),
ChatCommand::Kill => cmd(vec![], "Kill yourself", None),
ChatCommand::KillNpcs => cmd(vec![], "Kill the NPCs", Some(Admin)),
ChatCommand::Kit => cmd(
ServerChatCommand::Kill => cmd(vec![], "Kill yourself", None),
ServerChatCommand::KillNpcs => cmd(vec![], "Kill the NPCs", Some(Admin)),
ServerChatCommand::Kit => cmd(
vec![Enum("kit_name", KITS.to_vec(), Required)],
"Place a set of items into your inventory.",
Some(Admin),
),
ChatCommand::Lantern => cmd(
ServerChatCommand::Lantern => cmd(
vec![
Float("strength", 5.0, Required),
Float("r", 1.0, Optional),
@ -492,7 +495,7 @@ impl ChatCommand {
"Change your lantern's strength and color",
Some(Admin),
),
ChatCommand::Light => cmd(
ServerChatCommand::Light => cmd(
vec![
Float("r", 1.0, Optional),
Float("g", 1.0, Optional),
@ -505,7 +508,7 @@ impl ChatCommand {
"Spawn entity with light",
Some(Admin),
),
ChatCommand::MakeBlock => cmd(
ServerChatCommand::MakeBlock => cmd(
vec![
Enum("block", BLOCK_KINDS.clone(), Required),
Integer("r", 255, Optional),
@ -515,7 +518,7 @@ impl ChatCommand {
"Make a block at your location with a color",
Some(Admin),
),
ChatCommand::MakeNpc => cmd(
ServerChatCommand::MakeNpc => cmd(
vec![
Enum("entity_config", ENTITY_CONFIGS.clone(), Required),
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.",
Some(Admin),
),
ChatCommand::MakeSprite => cmd(
ServerChatCommand::MakeSprite => cmd(
vec![Enum("sprite", SPRITE_KINDS.clone(), Required)],
"Make a sprite at your location",
Some(Admin),
),
ChatCommand::Motd => cmd(vec![Message(Optional)], "View the server description", None),
ChatCommand::Object => cmd(
ServerChatCommand::Motd => {
cmd(vec![Message(Optional)], "View the server description", None)
},
ServerChatCommand::Object => cmd(
vec![Enum("object", OBJECTS.clone(), Required)],
"Spawn an object",
Some(Admin),
),
ChatCommand::PermitBuild => cmd(
ServerChatCommand::PermitBuild => cmd(
vec![Any("area_name", Required)],
"Grants player a bounded box they can build in",
Some(Admin),
),
ChatCommand::Players => cmd(vec![], "Lists players currently online", None),
ChatCommand::ReloadChunks => cmd(
ServerChatCommand::Players => cmd(vec![], "Lists players currently online", None),
ServerChatCommand::ReloadChunks => cmd(
vec![],
"Reloads all chunks loaded on the server",
Some(Admin),
),
ChatCommand::RemoveLights => cmd(
ServerChatCommand::RemoveLights => cmd(
vec![Float("radius", 20.0, Optional)],
"Removes all lights spawned by players",
Some(Admin),
),
ChatCommand::RevokeBuild => cmd(
ServerChatCommand::RevokeBuild => cmd(
vec![Any("area_name", Required)],
"Revokes build area permission for player",
Some(Admin),
),
ChatCommand::RevokeBuildAll => cmd(
ServerChatCommand::RevokeBuildAll => cmd(
vec![],
"Revokes all build area permissions for player",
Some(Admin),
),
ChatCommand::Region => cmd(
ServerChatCommand::Region => cmd(
vec![Message(Optional)],
"Send messages to everyone in your region of the world",
None,
),
ChatCommand::Safezone => cmd(
ServerChatCommand::Safezone => cmd(
vec![Float("range", 100.0, Optional)],
"Creates a safezone",
Some(Moderator),
),
ChatCommand::Say => cmd(
ServerChatCommand::Say => cmd(
vec![Message(Optional)],
"Send messages to everyone within shouting distance",
None,
),
ChatCommand::ServerPhysics => cmd(
ServerChatCommand::ServerPhysics => cmd(
vec![
PlayerName(Required),
Boolean("enabled", "true".to_string(), Optional),
@ -583,24 +588,24 @@ impl ChatCommand {
"Set/unset server-authoritative physics for an account",
Some(Moderator),
),
ChatCommand::SetMotd => cmd(
ServerChatCommand::SetMotd => cmd(
vec![Message(Optional)],
"Set the server description",
Some(Admin),
),
ChatCommand::Ship => cmd(
ServerChatCommand::Ship => cmd(
vec![Float("destination_degrees_ccw_of_east", 90.0, Optional)],
"Spawns a ship",
Some(Admin),
),
// Uses Message because site names can contain spaces,
// which would be assumed to be separators otherwise
ChatCommand::Site => cmd(
ServerChatCommand::Site => cmd(
vec![SiteName(Required)],
"Teleport to a site",
Some(Moderator),
),
ChatCommand::SkillPoint => cmd(
ServerChatCommand::SkillPoint => cmd(
vec![
Enum("skill tree", SKILL_TREES.clone(), Required),
Integer("amount", 1, Optional),
@ -608,12 +613,12 @@ impl ChatCommand {
"Give yourself skill points for a particular skill tree",
Some(Admin),
),
ChatCommand::SkillPreset => cmd(
ServerChatCommand::SkillPreset => cmd(
vec![Enum("preset_name", PRESET_LIST.to_vec(), Required)],
"Gives your character desired skills.",
Some(Admin),
),
ChatCommand::Spawn => cmd(
ServerChatCommand::Spawn => cmd(
vec![
Enum("alignment", ALIGNMENTS.clone(), Required),
Enum("entity", ENTITIES.clone(), Required),
@ -623,58 +628,60 @@ impl ChatCommand {
"Spawn a test entity",
Some(Admin),
),
ChatCommand::Sudo => cmd(
ServerChatCommand::Sudo => cmd(
vec![PlayerName(Required), SubCommand],
"Run command as if you were another player",
Some(Moderator),
),
ChatCommand::Tell => cmd(
ServerChatCommand::Tell => cmd(
vec![PlayerName(Required), Message(Optional)],
"Send a message to another player",
None,
),
ChatCommand::Time => cmd(
ServerChatCommand::Time => cmd(
vec![Enum("time", TIMES.clone(), Optional)],
"Set the time of day",
Some(Admin),
),
ChatCommand::Tp => cmd(
ServerChatCommand::Tp => cmd(
vec![PlayerName(Optional)],
"Teleport to another player",
Some(Moderator),
),
ChatCommand::Unban => cmd(
ServerChatCommand::Unban => cmd(
vec![PlayerName(Required)],
"Remove the ban for the given username",
Some(Moderator),
),
ChatCommand::Version => cmd(vec![], "Prints server version", None),
ChatCommand::Waypoint => cmd(
ServerChatCommand::Version => cmd(vec![], "Prints server version", None),
ServerChatCommand::Waypoint => cmd(
vec![],
"Set your waypoint to your current position",
Some(Admin),
),
ChatCommand::Wiring => cmd(vec![], "Create wiring element", Some(Admin)),
ChatCommand::Whitelist => cmd(
ServerChatCommand::Wiring => cmd(vec![], "Create wiring element", Some(Admin)),
ServerChatCommand::Whitelist => cmd(
vec![Any("add/remove", Required), PlayerName(Required)],
"Adds/removes username to whitelist",
Some(Moderator),
),
ChatCommand::World => cmd(
ServerChatCommand::World => cmd(
vec![Message(Optional)],
"Send messages to everyone on the server",
None,
),
ChatCommand::MakeVolume => cmd(vec![], "Create a volume (experimental)", Some(Admin)),
ChatCommand::Location => {
ServerChatCommand::MakeVolume => {
cmd(vec![], "Create a volume (experimental)", Some(Admin))
},
ServerChatCommand::Location => {
cmd(vec![Any("name", Required)], "Teleport to a location", None)
},
ChatCommand::CreateLocation => cmd(
ServerChatCommand::CreateLocation => cmd(
vec![Any("name", Required)],
"Create a location at the current position",
Some(Moderator),
),
ChatCommand::DeleteLocation => cmd(
ServerChatCommand::DeleteLocation => cmd(
vec![Any("name", Required)],
"Delete a location",
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 {
match self {
ChatCommand::Adminify => "adminify",
ChatCommand::Airship => "airship",
ChatCommand::Alias => "alias",
ChatCommand::ApplyBuff => "buff",
ChatCommand::Ban => "ban",
ChatCommand::BattleMode => "battlemode",
ChatCommand::BattleModeForce => "battlemode_force",
ChatCommand::Build => "build",
ChatCommand::BuildAreaAdd => "build_area_add",
ChatCommand::BuildAreaList => "build_area_list",
ChatCommand::BuildAreaRemove => "build_area_remove",
ChatCommand::Campfire => "campfire",
ChatCommand::DebugColumn => "debug_column",
ChatCommand::DisconnectAllPlayers => "disconnect_all_players",
ChatCommand::DropAll => "dropall",
ChatCommand::Dummy => "dummy",
ChatCommand::Explosion => "explosion",
ChatCommand::Faction => "faction",
ChatCommand::GiveItem => "give_item",
ChatCommand::Goto => "goto",
ChatCommand::Group => "group",
ChatCommand::GroupInvite => "group_invite",
ChatCommand::GroupKick => "group_kick",
ChatCommand::GroupPromote => "group_promote",
ChatCommand::GroupLeave => "group_leave",
ChatCommand::Health => "health",
ChatCommand::JoinFaction => "join_faction",
ChatCommand::Help => "help",
ChatCommand::Home => "home",
ChatCommand::Jump => "jump",
ChatCommand::Kick => "kick",
ChatCommand::Kill => "kill",
ChatCommand::Kit => "kit",
ChatCommand::KillNpcs => "kill_npcs",
ChatCommand::Lantern => "lantern",
ChatCommand::Light => "light",
ChatCommand::MakeBlock => "make_block",
ChatCommand::MakeNpc => "make_npc",
ChatCommand::MakeSprite => "make_sprite",
ChatCommand::Motd => "motd",
ChatCommand::Object => "object",
ChatCommand::PermitBuild => "permit_build",
ChatCommand::Players => "players",
ChatCommand::Region => "region",
ChatCommand::ReloadChunks => "reload_chunks",
ChatCommand::RemoveLights => "remove_lights",
ChatCommand::RevokeBuild => "revoke_build",
ChatCommand::RevokeBuildAll => "revoke_build_all",
ChatCommand::Safezone => "safezone",
ChatCommand::Say => "say",
ChatCommand::ServerPhysics => "server_physics",
ChatCommand::SetMotd => "set_motd",
ChatCommand::Ship => "ship",
ChatCommand::Site => "site",
ChatCommand::SkillPoint => "skill_point",
ChatCommand::SkillPreset => "skill_preset",
ChatCommand::Spawn => "spawn",
ChatCommand::Sudo => "sudo",
ChatCommand::Tell => "tell",
ChatCommand::Time => "time",
ChatCommand::Tp => "tp",
ChatCommand::Unban => "unban",
ChatCommand::Version => "version",
ChatCommand::Waypoint => "waypoint",
ChatCommand::Wiring => "wiring",
ChatCommand::Whitelist => "whitelist",
ChatCommand::World => "world",
ChatCommand::MakeVolume => "make_volume",
ChatCommand::Location => "location",
ChatCommand::CreateLocation => "create_location",
ChatCommand::DeleteLocation => "delete_location",
ServerChatCommand::Adminify => "adminify",
ServerChatCommand::Airship => "airship",
ServerChatCommand::Alias => "alias",
ServerChatCommand::ApplyBuff => "buff",
ServerChatCommand::Ban => "ban",
ServerChatCommand::BattleMode => "battlemode",
ServerChatCommand::BattleModeForce => "battlemode_force",
ServerChatCommand::Build => "build",
ServerChatCommand::BuildAreaAdd => "build_area_add",
ServerChatCommand::BuildAreaList => "build_area_list",
ServerChatCommand::BuildAreaRemove => "build_area_remove",
ServerChatCommand::Campfire => "campfire",
ServerChatCommand::DebugColumn => "debug_column",
ServerChatCommand::DisconnectAllPlayers => "disconnect_all_players",
ServerChatCommand::DropAll => "dropall",
ServerChatCommand::Dummy => "dummy",
ServerChatCommand::Explosion => "explosion",
ServerChatCommand::Faction => "faction",
ServerChatCommand::GiveItem => "give_item",
ServerChatCommand::Goto => "goto",
ServerChatCommand::Group => "group",
ServerChatCommand::GroupInvite => "group_invite",
ServerChatCommand::GroupKick => "group_kick",
ServerChatCommand::GroupPromote => "group_promote",
ServerChatCommand::GroupLeave => "group_leave",
ServerChatCommand::Health => "health",
ServerChatCommand::JoinFaction => "join_faction",
ServerChatCommand::Help => "help",
ServerChatCommand::Home => "home",
ServerChatCommand::Jump => "jump",
ServerChatCommand::Kick => "kick",
ServerChatCommand::Kill => "kill",
ServerChatCommand::Kit => "kit",
ServerChatCommand::KillNpcs => "kill_npcs",
ServerChatCommand::Lantern => "lantern",
ServerChatCommand::Light => "light",
ServerChatCommand::MakeBlock => "make_block",
ServerChatCommand::MakeNpc => "make_npc",
ServerChatCommand::MakeSprite => "make_sprite",
ServerChatCommand::Motd => "motd",
ServerChatCommand::Object => "object",
ServerChatCommand::PermitBuild => "permit_build",
ServerChatCommand::Players => "players",
ServerChatCommand::Region => "region",
ServerChatCommand::ReloadChunks => "reload_chunks",
ServerChatCommand::RemoveLights => "remove_lights",
ServerChatCommand::RevokeBuild => "revoke_build",
ServerChatCommand::RevokeBuildAll => "revoke_build_all",
ServerChatCommand::Safezone => "safezone",
ServerChatCommand::Say => "say",
ServerChatCommand::ServerPhysics => "server_physics",
ServerChatCommand::SetMotd => "set_motd",
ServerChatCommand::Ship => "ship",
ServerChatCommand::Site => "site",
ServerChatCommand::SkillPoint => "skill_point",
ServerChatCommand::SkillPreset => "skill_preset",
ServerChatCommand::Spawn => "spawn",
ServerChatCommand::Sudo => "sudo",
ServerChatCommand::Tell => "tell",
ServerChatCommand::Time => "time",
ServerChatCommand::Tp => "tp",
ServerChatCommand::Unban => "unban",
ServerChatCommand::Version => "version",
ServerChatCommand::Waypoint => "waypoint",
ServerChatCommand::Wiring => "wiring",
ServerChatCommand::Whitelist => "whitelist",
ServerChatCommand::World => "world",
ServerChatCommand::MakeVolume => "make_volume",
ServerChatCommand::Location => "location",
ServerChatCommand::CreateLocation => "create_location",
ServerChatCommand::DeleteLocation => "delete_location",
}
}
@ -763,16 +770,19 @@ impl ChatCommand {
/// Returns None if the command doesn't have a short keyword
pub fn short_keyword(&self) -> Option<&'static str> {
Some(match self {
ChatCommand::Faction => "f",
ChatCommand::Group => "g",
ChatCommand::Region => "r",
ChatCommand::Say => "s",
ChatCommand::Tell => "t",
ChatCommand::World => "w",
ServerChatCommand::Faction => "f",
ServerChatCommand::Group => "g",
ServerChatCommand::Region => "r",
ServerChatCommand::Say => "s",
ServerChatCommand::Tell => "t",
ServerChatCommand::World => "w",
_ => 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
pub fn help_string(&self) -> String {
let data = self.data();
@ -783,9 +793,17 @@ impl ChatCommand {
format!("{}: {}", usage, data.description)
}
/// A boolean that is used to check whether the command requires
/// administrator permissions or not.
pub fn needs_role(&self) -> Option<Role> { self.data().needs_role }
/// 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)))
}
pub fn needs_role(&self) -> Option<comp::AdminRole> { self.data().needs_role }
/// Returns a format string for parsing arguments with scan_fmt
pub fn arg_fmt(&self) -> String {
@ -807,34 +825,22 @@ impl ChatCommand {
.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()
// 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> {
write!(f, "{}", self.keyword())
}
}
impl FromStr for ChatCommand {
impl FromStr for ServerChatCommand {
type Err = ();
fn from_str(keyword: &str) -> Result<ChatCommand, ()> {
let keyword = keyword.strip_prefix('/').unwrap_or(keyword);
Self::iter_with_keywords()
fn from_str(keyword: &str) -> Result<ServerChatCommand, ()> {
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)))
// Find command with matching string as keyword
.find_map(|(kwd, command)| (kwd == keyword).then(|| command))
// 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)]
mod tests {
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(),
}
}),
uuid: player.uuid(),
}),
);
let remove_player_msg = ServerGeneral::PlayerListUpdate(

View File

@ -73,7 +73,7 @@ use common::{
assets::AssetExt,
calendar::Calendar,
character::CharacterId,
cmd::ChatCommand,
cmd::ServerChatCommand,
comp,
event::{EventBus, ServerEvent},
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>) {
// 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);
} else {
#[cfg(feature = "plugins")]

View File

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

View File

@ -1,5 +1,5 @@
use crate::{AdminCommandState, EguiAction, EguiActions};
use common::cmd::ChatCommand;
use common::cmd::ServerChatCommand;
use egui::{CollapsingHeader, CtxRef, Resize, Slider, Ui, Vec2, Window};
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| {
if ui.button("Give Kit").clicked() {
egui_actions.actions.push(EguiAction::ChatCommand {
cmd: ChatCommand::Kit,
cmd: ServerChatCommand::Kit,
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() {
egui_actions.actions.push(EguiAction::ChatCommand {
cmd: ChatCommand::GiveItem,
cmd: ServerChatCommand::GiveItem,
args: vec![
format!(
"common.items.{}",

View File

@ -10,6 +10,7 @@ mod widgets;
use client::{Client, Join, World, WorldExt};
use common::{
cmd::ServerChatCommand,
comp,
comp::{inventory::item::armor::Friction, Poise, PoiseState},
};
@ -24,10 +25,7 @@ use crate::{
admin::draw_admin_commands_window, character_states::draw_char_state_group,
experimental_shaders::draw_experimental_shaders_window, widgets::two_col_row,
};
use common::{
cmd::ChatCommand,
comp::{aura::AuraKind::Buff, Body, Fluid},
};
use common::comp::{aura::AuraKind::Buff, Body, Fluid};
use egui_winit_platform::Platform;
use std::time::Duration;
#[cfg(feature = "use-dyn-lib")]
@ -131,7 +129,10 @@ pub enum EguiDebugShapeAction {
}
pub enum EguiAction {
ChatCommand { cmd: ChatCommand, args: Vec<String> },
ChatCommand {
cmd: ServerChatCommand,
args: Vec<String>,
},
DebugShape(EguiDebugShapeAction),
SetExperimentalShader(String, bool),
}
@ -613,13 +614,20 @@ fn selected_entity_window(
.spacing([40.0, 4.0])
.max_col_width(100.0)
.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");
poise_state_label(ui, poise);
ui.end_row();
two_col_row(ui, "Current", format!("{:.1}/{:.1}", poise.current(), poise.maximum()));
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,
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 client::{cmd, Client};
use crate::{cmd::complete, settings::chat::MAX_CHAT_TABS, ui::fonts::Fonts, GlobalState};
use client::Client;
use common::comp::{
chat::{KillSource, KillType},
group::Role,
@ -108,7 +108,11 @@ impl<'a> Chat<'a> {
pub fn prepare_tab_completion(mut self, input: String) -> Self {
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 {
None
};
@ -659,7 +663,7 @@ impl<'a> Widget for Chat<'a> {
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) {
Ok((name, args)) => events.push(Event::SendCommand(name, args)),
Err(err) => self.new_messages.push_back(ChatMsg {

View File

@ -53,6 +53,7 @@ use social::Social;
use trade::Trade;
use crate::{
cmd::get_player_uuid,
ecs::{comp as vcomp, comp::HpFloaterList},
game_input::GameInput,
hud::{img_ids::ImgsRot, prompt_dialog::DialogOutcomeEvent},
@ -1789,6 +1790,21 @@ impl Hud {
self.speech_bubbles
.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
for msg in self.new_messages.iter() {
if let Some((bubble, uid)) = msg.to_bubble() {

View File

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

View File

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

View File

@ -41,6 +41,7 @@ use common_net::{
use crate::{
audio::sfx::SfxEvent,
cmd::run_command,
error::Error,
game_input::GameInput,
hud::{
@ -1155,7 +1156,16 @@ impl PlayState for SessionState {
self.client.borrow_mut().send_chat(msg);
},
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 => {
self.client.borrow_mut().request_remove_character()

View File

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