From 57ab1c5767831f7d50014c26605ae743a30375fa Mon Sep 17 00:00:00 2001 From: Samantha W Date: Tue, 14 Jun 2022 20:35:01 +0000 Subject: [PATCH] Add a client-side mutelist --- CHANGELOG.md | 2 +- client/src/cmd.rs | 149 --------- client/src/lib.rs | 1 - common/net/src/msg/server.rs | 2 + common/src/bin/cmd_doc_gen.rs | 4 +- common/src/cmd.rs | 534 ++++++++++++++++++--------------- server/src/cmd.rs | 430 ++++++++++++-------------- server/src/events/player.rs | 1 + server/src/lib.rs | 4 +- server/src/sys/msg/register.rs | 2 + voxygen/egui/src/admin.rs | 6 +- voxygen/egui/src/lib.rs | 22 +- voxygen/src/cmd.rs | 411 +++++++++++++++++++++++++ voxygen/src/hud/chat.rs | 12 +- voxygen/src/hud/mod.rs | 16 + voxygen/src/lib.rs | 1 + voxygen/src/profile.rs | 3 +- voxygen/src/session/mod.rs | 12 +- voxygen/src/settings/chat.rs | 2 + 19 files changed, 965 insertions(+), 649 deletions(-) delete mode 100644 client/src/cmd.rs create mode 100644 voxygen/src/cmd.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index 0a48e155e3..28571cf25a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/client/src/cmd.rs b/client/src/cmd.rs deleted file mode 100644 index dd5dd8ba06..0000000000 --- a/client/src/cmd.rs +++ /dev/null @@ -1,149 +0,0 @@ -use crate::Client; -use common::cmd::*; - -trait TabComplete { - fn complete(&self, part: &str, client: &Client) -> Vec; -} - -impl TabComplete for ArgumentSpec { - fn complete(&self, part: &str, client: &Client) -> Vec { - 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 { - 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 { - 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 { - 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 { - 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 { - 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::() { - 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) - } -} diff --git a/client/src/lib.rs b/client/src/lib.rs index fe02b0031c..13c191ecdd 100644 --- a/client/src/lib.rs +++ b/client/src/lib.rs @@ -3,7 +3,6 @@ #![feature(label_break_value, option_zip)] pub mod addr; -pub mod cmd; pub mod error; // Reexports diff --git a/common/net/src/msg/server.rs b/common/net/src/msg/server.rs index 2223811301..4b233de6bc 100644 --- a/common/net/src/msg/server.rs +++ b/common/net/src/msg/server.rs @@ -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, + pub uuid: Uuid, } #[derive(Debug, Clone, Serialize, Deserialize)] diff --git a/common/src/bin/cmd_doc_gen.rs b/common/src/bin/cmd_doc_gen.rs index 24ecb39fc5..d85c43a875 100644 --- a/common/src/bin/cmd_doc_gen.rs +++ b/common/src/bin/cmd_doc_gen.rs @@ -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 diff --git a/common/src/cmd.rs b/common/src/cmd.rs index 4726980e78..479eae7585 100644 --- a/common/src/cmd.rs +++ b/common/src/cmd.rs @@ -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 { ::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 { 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 { + 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 { 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::>() .join(" ") } - - /// Produce an iterator over all the available commands - pub fn iter() -> impl Iterator { ::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 { - 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 { - let keyword = keyword.strip_prefix('/').unwrap_or(keyword); - - Self::iter_with_keywords() + fn from_str(keyword: &str) -> Result { + 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::*; diff --git a/server/src/cmd.rs b/server/src/cmd.rs index 1ef7b2d810..4b5edf8778 100644 --- a/server/src/cmd.rs +++ b/server/src/cmd.rs @@ -21,7 +21,7 @@ use common::{ assets, calendar::Calendar, cmd::{ - ChatCommand, KitSpec, BUFF_PACK, BUFF_PARSER, ITEM_SPECS, KIT_MANIFEST_PATH, + KitSpec, ServerChatCommand, BUFF_PACK, BUFF_PARSER, ITEM_SPECS, KIT_MANIFEST_PATH, PRESET_MANIFEST_PATH, }, comp::{ @@ -39,6 +39,7 @@ use common::{ link::Is, mounting::Rider, npc::{self, get_npc_name}, + parse_cmd_args, resources::{BattleMode, PlayerPhysicsSettings, Time, TimeOfDay}, terrain::{Block, BlockKind, SpriteKind, TerrainChunkSize}, uid::{Uid, UidAllocator}, @@ -68,7 +69,7 @@ use tracing::{error, info, warn}; pub trait ChatCommandExt { fn execute(&self, server: &mut Server, entity: EcsEntity, args: Vec); } -impl ChatCommandExt for ChatCommand { +impl ChatCommandExt for ServerChatCommand { fn execute(&self, server: &mut Server, entity: EcsEntity, args: Vec) { if let Err(err) = do_command(server, entity, entity, args, self) { server.notify_client( @@ -100,14 +101,14 @@ type CmdResult = Result; /// failed; on failure, the string is sent to the client who initiated the /// command. type CommandHandler = - fn(&mut Server, EcsEntity, EcsEntity, Vec, &ChatCommand) -> CmdResult<()>; + fn(&mut Server, EcsEntity, EcsEntity, Vec, &ServerChatCommand) -> CmdResult<()>; fn do_command( server: &mut Server, client: EcsEntity, target: EcsEntity, args: Vec, - cmd: &ChatCommand, + cmd: &ServerChatCommand, ) -> CmdResult<()> { // Make sure your role is at least high enough to execute this command. if cmd.needs_role() > server.entity_admin_role(client) { @@ -118,77 +119,77 @@ fn do_command( } let handler: CommandHandler = match cmd { - ChatCommand::Adminify => handle_adminify, - ChatCommand::Airship => handle_spawn_airship, - ChatCommand::Alias => handle_alias, - ChatCommand::ApplyBuff => handle_apply_buff, - ChatCommand::Ban => handle_ban, - ChatCommand::BattleMode => handle_battlemode, - ChatCommand::BattleModeForce => handle_battlemode_force, - ChatCommand::Build => handle_build, - ChatCommand::BuildAreaAdd => handle_build_area_add, - ChatCommand::BuildAreaList => handle_build_area_list, - ChatCommand::BuildAreaRemove => handle_build_area_remove, - ChatCommand::Campfire => handle_spawn_campfire, - ChatCommand::DebugColumn => handle_debug_column, - ChatCommand::DisconnectAllPlayers => handle_disconnect_all_players, - ChatCommand::DropAll => handle_drop_all, - ChatCommand::Dummy => handle_spawn_training_dummy, - ChatCommand::Explosion => handle_explosion, - ChatCommand::Faction => handle_faction, - ChatCommand::GiveItem => handle_give_item, - ChatCommand::Goto => handle_goto, - ChatCommand::Group => handle_group, - ChatCommand::GroupInvite => handle_group_invite, - ChatCommand::GroupKick => handle_group_kick, - ChatCommand::GroupLeave => handle_group_leave, - ChatCommand::GroupPromote => handle_group_promote, - ChatCommand::Health => handle_health, - ChatCommand::Help => handle_help, - ChatCommand::Home => handle_home, - ChatCommand::JoinFaction => handle_join_faction, - ChatCommand::Jump => handle_jump, - ChatCommand::Kick => handle_kick, - ChatCommand::Kill => handle_kill, - ChatCommand::KillNpcs => handle_kill_npcs, - ChatCommand::Kit => handle_kit, - ChatCommand::Lantern => handle_lantern, - ChatCommand::Light => handle_light, - ChatCommand::MakeBlock => handle_make_block, - ChatCommand::MakeNpc => handle_make_npc, - ChatCommand::MakeSprite => handle_make_sprite, - ChatCommand::Motd => handle_motd, - ChatCommand::Object => handle_object, - ChatCommand::PermitBuild => handle_permit_build, - ChatCommand::Players => handle_players, - ChatCommand::Region => handle_region, - ChatCommand::ReloadChunks => handle_reload_chunks, - ChatCommand::RemoveLights => handle_remove_lights, - ChatCommand::RevokeBuild => handle_revoke_build, - ChatCommand::RevokeBuildAll => handle_revoke_build_all, - ChatCommand::Safezone => handle_safezone, - ChatCommand::Say => handle_say, - ChatCommand::ServerPhysics => handle_server_physics, - ChatCommand::SetMotd => handle_set_motd, - ChatCommand::Ship => handle_spawn_ship, - ChatCommand::Site => handle_site, - ChatCommand::SkillPoint => handle_skill_point, - ChatCommand::SkillPreset => handle_skill_preset, - ChatCommand::Spawn => handle_spawn, - ChatCommand::Sudo => handle_sudo, - ChatCommand::Tell => handle_tell, - ChatCommand::Time => handle_time, - ChatCommand::Tp => handle_tp, - ChatCommand::Unban => handle_unban, - ChatCommand::Version => handle_version, - ChatCommand::Waypoint => handle_waypoint, - ChatCommand::Wiring => handle_spawn_wiring, - ChatCommand::Whitelist => handle_whitelist, - ChatCommand::World => handle_world, - ChatCommand::MakeVolume => handle_make_volume, - ChatCommand::Location => handle_location, - ChatCommand::CreateLocation => handle_create_location, - ChatCommand::DeleteLocation => handle_delete_location, + ServerChatCommand::Adminify => handle_adminify, + ServerChatCommand::Airship => handle_spawn_airship, + ServerChatCommand::Alias => handle_alias, + ServerChatCommand::ApplyBuff => handle_apply_buff, + ServerChatCommand::Ban => handle_ban, + ServerChatCommand::BattleMode => handle_battlemode, + ServerChatCommand::BattleModeForce => handle_battlemode_force, + ServerChatCommand::Build => handle_build, + ServerChatCommand::BuildAreaAdd => handle_build_area_add, + ServerChatCommand::BuildAreaList => handle_build_area_list, + ServerChatCommand::BuildAreaRemove => handle_build_area_remove, + ServerChatCommand::Campfire => handle_spawn_campfire, + ServerChatCommand::DebugColumn => handle_debug_column, + ServerChatCommand::DisconnectAllPlayers => handle_disconnect_all_players, + ServerChatCommand::DropAll => handle_drop_all, + ServerChatCommand::Dummy => handle_spawn_training_dummy, + ServerChatCommand::Explosion => handle_explosion, + ServerChatCommand::Faction => handle_faction, + ServerChatCommand::GiveItem => handle_give_item, + ServerChatCommand::Goto => handle_goto, + ServerChatCommand::Group => handle_group, + ServerChatCommand::GroupInvite => handle_group_invite, + ServerChatCommand::GroupKick => handle_group_kick, + ServerChatCommand::GroupLeave => handle_group_leave, + ServerChatCommand::GroupPromote => handle_group_promote, + ServerChatCommand::Health => handle_health, + ServerChatCommand::Help => handle_help, + ServerChatCommand::Home => handle_home, + ServerChatCommand::JoinFaction => handle_join_faction, + ServerChatCommand::Jump => handle_jump, + ServerChatCommand::Kick => handle_kick, + ServerChatCommand::Kill => handle_kill, + ServerChatCommand::KillNpcs => handle_kill_npcs, + ServerChatCommand::Kit => handle_kit, + ServerChatCommand::Lantern => handle_lantern, + ServerChatCommand::Light => handle_light, + ServerChatCommand::MakeBlock => handle_make_block, + ServerChatCommand::MakeNpc => handle_make_npc, + ServerChatCommand::MakeSprite => handle_make_sprite, + ServerChatCommand::Motd => handle_motd, + ServerChatCommand::Object => handle_object, + ServerChatCommand::PermitBuild => handle_permit_build, + ServerChatCommand::Players => handle_players, + ServerChatCommand::Region => handle_region, + ServerChatCommand::ReloadChunks => handle_reload_chunks, + ServerChatCommand::RemoveLights => handle_remove_lights, + ServerChatCommand::RevokeBuild => handle_revoke_build, + ServerChatCommand::RevokeBuildAll => handle_revoke_build_all, + ServerChatCommand::Safezone => handle_safezone, + ServerChatCommand::Say => handle_say, + ServerChatCommand::ServerPhysics => handle_server_physics, + ServerChatCommand::SetMotd => handle_set_motd, + ServerChatCommand::Ship => handle_spawn_ship, + ServerChatCommand::Site => handle_site, + ServerChatCommand::SkillPoint => handle_skill_point, + ServerChatCommand::SkillPreset => handle_skill_preset, + ServerChatCommand::Spawn => handle_spawn, + ServerChatCommand::Sudo => handle_sudo, + ServerChatCommand::Tell => handle_tell, + ServerChatCommand::Time => handle_time, + ServerChatCommand::Tp => handle_tp, + ServerChatCommand::Unban => handle_unban, + ServerChatCommand::Version => handle_version, + ServerChatCommand::Waypoint => handle_waypoint, + ServerChatCommand::Wiring => handle_spawn_wiring, + ServerChatCommand::Whitelist => handle_whitelist, + ServerChatCommand::World => handle_world, + ServerChatCommand::MakeVolume => handle_make_volume, + ServerChatCommand::Location => handle_location, + ServerChatCommand::CreateLocation => handle_create_location, + ServerChatCommand::DeleteLocation => handle_delete_location, }; handler(server, client, target, args, cmd) @@ -440,43 +441,12 @@ fn edit_setting_feedback( } } -/// Parse a series of command arguments into values, including collecting all -/// trailing arguments. -macro_rules! parse_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>())? - ) - } - }; -} - fn handle_drop_all( server: &mut Server, _client: EcsEntity, target: EcsEntity, _args: Vec, - _action: &ChatCommand, + _action: &ServerChatCommand, ) -> CmdResult<()> { let pos = position(server, target, "target")?; @@ -518,9 +488,9 @@ fn handle_give_item( _client: EcsEntity, target: EcsEntity, args: Vec, - action: &ChatCommand, + action: &ServerChatCommand, ) -> CmdResult<()> { - if let (Some(item_name), give_amount_opt) = parse_args!(args, String, u32) { + if let (Some(item_name), give_amount_opt) = parse_cmd_args!(args, String, u32) { let give_amount = give_amount_opt.unwrap_or(1); if let Ok(item) = Item::new_from_asset(&item_name.replace('/', ".").replace('\\', ".")) { let mut item: Item = item; @@ -592,9 +562,9 @@ fn handle_make_block( _client: EcsEntity, target: EcsEntity, args: Vec, - action: &ChatCommand, + action: &ServerChatCommand, ) -> CmdResult<()> { - if let (Some(block_name), r, g, b) = parse_args!(args, String, u8, u8, u8) { + if let (Some(block_name), r, g, b) = parse_cmd_args!(args, String, u8, u8, u8) { if let Ok(bk) = BlockKind::from_str(block_name.as_str()) { let pos = position(server, target, "target")?; let new_block = Block::new(bk, Rgb::new(r, g, b).map(|e| e.unwrap_or(255))); @@ -623,9 +593,9 @@ fn handle_make_npc( client: EcsEntity, target: EcsEntity, args: Vec, - action: &ChatCommand, + action: &ServerChatCommand, ) -> CmdResult<()> { - let (entity_config, number) = parse_args!(args, String, i8); + let (entity_config, number) = parse_cmd_args!(args, String, i8); let entity_config = entity_config.ok_or_else(|| action.help_string())?; let number = match number { @@ -716,9 +686,9 @@ fn handle_make_sprite( _client: EcsEntity, target: EcsEntity, args: Vec, - action: &ChatCommand, + action: &ServerChatCommand, ) -> CmdResult<()> { - if let Some(sprite_name) = parse_args!(args, String) { + if let Some(sprite_name) = parse_cmd_args!(args, String) { if let Ok(sk) = SpriteKind::try_from(sprite_name.as_str()) { let pos = position(server, target, "target")?; let pos = pos.0.map(|e| e.floor() as i32); @@ -752,7 +722,7 @@ fn handle_motd( client: EcsEntity, _target: EcsEntity, _args: Vec, - _action: &ChatCommand, + _action: &ServerChatCommand, ) -> CmdResult<()> { server.notify_client( client, @@ -769,14 +739,14 @@ fn handle_set_motd( client: EcsEntity, _target: EcsEntity, args: Vec, - _action: &ChatCommand, + _action: &ServerChatCommand, ) -> CmdResult<()> { let data_dir = server.data_dir(); let client_uuid = uuid(server, client, "client")?; // Ensure the person setting this has a real role in the settings file, since // it's persistent. let _client_real_role = real_role(server, client_uuid, "client")?; - match parse_args!(args, String) { + match parse_cmd_args!(args, String) { Some(msg) => { let edit = server @@ -814,9 +784,9 @@ fn handle_jump( _client: EcsEntity, target: EcsEntity, args: Vec, - action: &ChatCommand, + action: &ServerChatCommand, ) -> CmdResult<()> { - if let (Some(x), Some(y), Some(z)) = parse_args!(args, f32, f32, f32) { + if let (Some(x), Some(y), Some(z)) = parse_cmd_args!(args, f32, f32, f32) { position_mut(server, target, "target", |current_pos| { current_pos.0 += Vec3::new(x, y, z) }) @@ -830,9 +800,9 @@ fn handle_goto( _client: EcsEntity, target: EcsEntity, args: Vec, - action: &ChatCommand, + action: &ServerChatCommand, ) -> CmdResult<()> { - if let (Some(x), Some(y), Some(z)) = parse_args!(args, f32, f32, f32) { + if let (Some(x), Some(y), Some(z)) = parse_cmd_args!(args, f32, f32, f32) { position_mut(server, target, "target", |current_pos| { current_pos.0 = Vec3::new(x, y, z) }) @@ -848,10 +818,10 @@ fn handle_site( _client: EcsEntity, target: EcsEntity, args: Vec, - action: &ChatCommand, + action: &ServerChatCommand, ) -> CmdResult<()> { #[cfg(feature = "worldgen")] - if let Some(dest_name) = parse_args!(args, String) { + if let Some(dest_name) = parse_cmd_args!(args, String) { let site = server .world .civs() @@ -884,7 +854,7 @@ fn handle_home( _client: EcsEntity, target: EcsEntity, _args: Vec, - _action: &ChatCommand, + _action: &ServerChatCommand, ) -> CmdResult<()> { let home_pos = server.state.mut_resource::().0; let time = *server.state.mut_resource::(); @@ -905,7 +875,7 @@ fn handle_kill( _client: EcsEntity, target: EcsEntity, _args: Vec, - _action: &ChatCommand, + _action: &ServerChatCommand, ) -> CmdResult<()> { server .state @@ -921,7 +891,7 @@ fn handle_time( client: EcsEntity, _target: EcsEntity, args: Vec, - _action: &ChatCommand, + _action: &ServerChatCommand, ) -> CmdResult<()> { const DAY: u64 = 86400; @@ -940,7 +910,7 @@ fn handle_time( } }; - let time = parse_args!(args, String); + let time = parse_cmd_args!(args, String); let new_time = match time.as_deref() { Some("midnight") => { next_cycle(NaiveTime::from_hms(0, 0, 0).num_seconds_from_midnight() as f64) @@ -1072,9 +1042,9 @@ fn handle_health( _client: EcsEntity, target: EcsEntity, args: Vec, - _action: &ChatCommand, + _action: &ServerChatCommand, ) -> CmdResult<()> { - if let Some(hp) = parse_args!(args, f32) { + if let Some(hp) = parse_cmd_args!(args, f32) { if let Some(mut health) = server .state .ecs() @@ -1103,9 +1073,9 @@ fn handle_alias( client: EcsEntity, target: EcsEntity, args: Vec, - action: &ChatCommand, + action: &ServerChatCommand, ) -> CmdResult<()> { - if let Some(alias) = parse_args!(args, String) { + if let Some(alias) = parse_cmd_args!(args, String) { // Prevent silly aliases comp::Player::alias_validate(&alias).map_err(|e| e.to_string())?; @@ -1155,9 +1125,9 @@ fn handle_tp( client: EcsEntity, target: EcsEntity, args: Vec, - action: &ChatCommand, + action: &ServerChatCommand, ) -> CmdResult<()> { - let player = if let Some(alias) = parse_args!(args, String) { + let player = if let Some(alias) = parse_cmd_args!(args, String) { find_alias(server.state.ecs(), &alias)?.0 } else if client != target { client @@ -1175,9 +1145,9 @@ fn handle_spawn( client: EcsEntity, target: EcsEntity, args: Vec, - action: &ChatCommand, + action: &ServerChatCommand, ) -> CmdResult<()> { - match parse_args!(args, String, npc::NpcBody, u32, bool) { + match parse_cmd_args!(args, String, npc::NpcBody, u32, bool) { (Some(opt_align), Some(npc::NpcBody(id, mut body)), opt_amount, opt_ai) => { let uid = uid(server, target, "target")?; let alignment = parse_alignment(uid, &opt_align)?; @@ -1269,7 +1239,7 @@ fn handle_spawn_training_dummy( client: EcsEntity, target: EcsEntity, _args: Vec, - _action: &ChatCommand, + _action: &ServerChatCommand, ) -> CmdResult<()> { let pos = position(server, target, "target")?; let vel = Vec3::new( @@ -1311,9 +1281,9 @@ fn handle_spawn_airship( client: EcsEntity, target: EcsEntity, args: Vec, - _action: &ChatCommand, + _action: &ServerChatCommand, ) -> CmdResult<()> { - let angle = parse_args!(args, f32); + let angle = parse_cmd_args!(args, f32); let mut pos = position(server, target, "target")?; pos.0.z += 50.0; const DESTINATION_RADIUS: f32 = 2000.0; @@ -1359,9 +1329,9 @@ fn handle_spawn_ship( client: EcsEntity, target: EcsEntity, args: Vec, - _action: &ChatCommand, + _action: &ServerChatCommand, ) -> CmdResult<()> { - let angle = parse_args!(args, f32); + let angle = parse_cmd_args!(args, f32); let mut pos = position(server, target, "target")?; pos.0.z += 50.0; const DESTINATION_RADIUS: f32 = 2000.0; @@ -1407,7 +1377,7 @@ fn handle_make_volume( client: EcsEntity, target: EcsEntity, _args: Vec, - _action: &ChatCommand, + _action: &ServerChatCommand, ) -> CmdResult<()> { use comp::body::ship::figuredata::VoxelCollider; @@ -1447,7 +1417,7 @@ fn handle_spawn_campfire( client: EcsEntity, target: EcsEntity, _args: Vec, - _action: &ChatCommand, + _action: &ServerChatCommand, ) -> CmdResult<()> { let pos = position(server, target, "target")?; server @@ -1498,9 +1468,9 @@ fn handle_safezone( client: EcsEntity, target: EcsEntity, args: Vec, - _action: &ChatCommand, + _action: &ServerChatCommand, ) -> CmdResult<()> { - let range = parse_args!(args, f32); + let range = parse_cmd_args!(args, f32); let pos = position(server, target, "target")?; server.state.create_safezone(range, pos).build(); @@ -1516,9 +1486,9 @@ fn handle_permit_build( client: EcsEntity, target: EcsEntity, args: Vec, - action: &ChatCommand, + action: &ServerChatCommand, ) -> CmdResult<()> { - if let Some(area_name) = parse_args!(args, String) { + if let Some(area_name) = parse_cmd_args!(args, String) { let bb_id = area(server, &area_name)?; let mut can_build = server.state.ecs().write_storage::(); let entry = can_build @@ -1557,9 +1527,9 @@ fn handle_revoke_build( client: EcsEntity, target: EcsEntity, args: Vec, - action: &ChatCommand, + action: &ServerChatCommand, ) -> CmdResult<()> { - if let Some(area_name) = parse_args!(args, String) { + if let Some(area_name) = parse_cmd_args!(args, String) { let bb_id = area(server, &area_name)?; let mut can_build = server.state.ecs_mut().write_storage::(); if let Some(mut comp_can_build) = can_build.get_mut(target) { @@ -1595,7 +1565,7 @@ fn handle_revoke_build_all( client: EcsEntity, target: EcsEntity, _args: Vec, - _action: &ChatCommand, + _action: &ServerChatCommand, ) -> CmdResult<()> { let ecs = server.state.ecs(); @@ -1621,7 +1591,7 @@ fn handle_players( client: EcsEntity, _target: EcsEntity, _args: Vec, - _action: &ChatCommand, + _action: &ServerChatCommand, ) -> CmdResult<()> { let ecs = server.state.ecs(); @@ -1649,7 +1619,7 @@ fn handle_build( client: EcsEntity, target: EcsEntity, _args: Vec, - _action: &ChatCommand, + _action: &ServerChatCommand, ) -> CmdResult<()> { if let Some(mut can_build) = server .state @@ -1689,10 +1659,10 @@ fn handle_build_area_add( client: EcsEntity, _target: EcsEntity, args: Vec, - action: &ChatCommand, + action: &ServerChatCommand, ) -> CmdResult<()> { if let (Some(area_name), Some(xlo), Some(xhi), Some(ylo), Some(yhi), Some(zlo), Some(zhi)) = - parse_args!(args, String, i32, i32, i32, i32, i32, i32) + parse_cmd_args!(args, String, i32, i32, i32, i32, i32, i32) { let build_areas = server.state.mut_resource::(); let msg = ServerGeneral::server_msg( @@ -1717,7 +1687,7 @@ fn handle_build_area_list( client: EcsEntity, _target: EcsEntity, _args: Vec, - _action: &ChatCommand, + _action: &ServerChatCommand, ) -> CmdResult<()> { let build_areas = server.state.mut_resource::(); let msg = ServerGeneral::server_msg( @@ -1743,9 +1713,9 @@ fn handle_build_area_remove( client: EcsEntity, _target: EcsEntity, args: Vec, - action: &ChatCommand, + action: &ServerChatCommand, ) -> CmdResult<()> { - if let Some(area_name) = parse_args!(args, String) { + if let Some(area_name) = parse_cmd_args!(args, String) { let build_areas = server.state.mut_resource::(); build_areas.remove(&area_name).map_err(|err| match err { @@ -1773,9 +1743,9 @@ fn handle_help( client: EcsEntity, _target: EcsEntity, args: Vec, - _action: &ChatCommand, + _action: &ServerChatCommand, ) -> CmdResult<()> { - if let Some(cmd) = parse_args!(args, ChatCommand) { + if let Some(cmd) = parse_cmd_args!(args, ServerChatCommand) { server.notify_client( client, ServerGeneral::server_msg(ChatType::CommandInfo, cmd.help_string()), @@ -1785,14 +1755,14 @@ fn handle_help( let entity_role = server.entity_admin_role(client); // Iterate through all commands you have permission to use. - ChatCommand::iter() + ServerChatCommand::iter() .filter(|cmd| cmd.needs_role() <= entity_role) .for_each(|cmd| { message += &cmd.help_string(); message += "\n"; }); message += "Additionally, you can use the following shortcuts:"; - ChatCommand::iter() + ServerChatCommand::iter() .filter_map(|cmd| cmd.short_keyword().map(|k| (k, cmd))) .for_each(|(k, cmd)| { message += &format!(" /{} => /{}", k, cmd.keyword()); @@ -1821,9 +1791,9 @@ fn handle_kill_npcs( client: EcsEntity, _target: EcsEntity, args: Vec, - _action: &ChatCommand, + _action: &ServerChatCommand, ) -> CmdResult<()> { - let kill_pets = if let Some(kill_option) = parse_args!(args, String) { + let kill_pets = if let Some(kill_option) = parse_cmd_args!(args, String) { kill_option.contains("--also-pets") } else { false @@ -1869,7 +1839,7 @@ fn handle_kit( client: EcsEntity, target: EcsEntity, args: Vec, - action: &ChatCommand, + action: &ServerChatCommand, ) -> CmdResult<()> { use common::cmd::KitManifest; @@ -1879,7 +1849,7 @@ fn handle_kit( ServerGeneral::server_msg(ChatType::CommandInfo, format!("Gave kit: {}", kit_name)), ); }; - let name = parse_args!(args, String).ok_or_else(|| action.help_string())?; + let name = parse_cmd_args!(args, String).ok_or_else(|| action.help_string())?; match name.as_str() { "all" => { @@ -1989,9 +1959,9 @@ fn handle_object( client: EcsEntity, target: EcsEntity, args: Vec, - _action: &ChatCommand, + _action: &ServerChatCommand, ) -> CmdResult<()> { - let obj_type = parse_args!(args, String); + let obj_type = parse_cmd_args!(args, String); let pos = position(server, target, "target")?; let ori = server @@ -2048,10 +2018,10 @@ fn handle_light( client: EcsEntity, target: EcsEntity, args: Vec, - _action: &ChatCommand, + _action: &ServerChatCommand, ) -> CmdResult<()> { let (opt_r, opt_g, opt_b, opt_x, opt_y, opt_z, opt_s) = - parse_args!(args, f32, f32, f32, f32, f32, f32, f32); + parse_cmd_args!(args, f32, f32, f32, f32, f32, f32, f32); let mut light_emitter = comp::LightEmitter::default(); let mut light_offset_opt = None; @@ -2101,9 +2071,9 @@ fn handle_lantern( client: EcsEntity, target: EcsEntity, args: Vec, - action: &ChatCommand, + action: &ServerChatCommand, ) -> CmdResult<()> { - if let (Some(s), r, g, b) = parse_args!(args, f32, f32, f32, f32) { + if let (Some(s), r, g, b) = parse_cmd_args!(args, f32, f32, f32, f32) { if let Some(mut light) = server .state .ecs() @@ -2148,9 +2118,9 @@ fn handle_explosion( _client: EcsEntity, target: EcsEntity, args: Vec, - _action: &ChatCommand, + _action: &ServerChatCommand, ) -> CmdResult<()> { - let power = parse_args!(args, f32).unwrap_or(8.0); + let power = parse_cmd_args!(args, f32).unwrap_or(8.0); const MIN_POWER: f32 = 0.0; const MAX_POWER: f32 = 512.0; @@ -2202,7 +2172,7 @@ fn handle_waypoint( client: EcsEntity, target: EcsEntity, _args: Vec, - _action: &ChatCommand, + _action: &ServerChatCommand, ) -> CmdResult<()> { let pos = position(server, target, "target")?; let time = *server.state.mut_resource::(); @@ -2228,7 +2198,7 @@ fn handle_spawn_wiring( client: EcsEntity, target: EcsEntity, _args: Vec, - _action: &ChatCommand, + _action: &ServerChatCommand, ) -> CmdResult<()> { // Obviously it is a WIP - use it for debug @@ -2397,9 +2367,9 @@ fn handle_adminify( client: EcsEntity, _target: EcsEntity, args: Vec, - action: &ChatCommand, + action: &ServerChatCommand, ) -> CmdResult<()> { - if let (Some(alias), desired_role) = parse_args!(args, String, String) { + if let (Some(alias), desired_role) = parse_cmd_args!(args, String, String) { let desired_role = if let Some(mut desired_role) = desired_role { desired_role.make_ascii_lowercase(); Some(match &*desired_role { @@ -2507,11 +2477,11 @@ fn handle_tell( client: EcsEntity, target: EcsEntity, args: Vec, - action: &ChatCommand, + action: &ServerChatCommand, ) -> CmdResult<()> { no_sudo(client, target)?; - if let (Some(alias), message_opt) = parse_args!(args, String, ..Vec) { + if let (Some(alias), message_opt) = parse_cmd_args!(args, String, ..Vec) { let ecs = server.state.ecs(); let player = find_alias(ecs, &alias)?.0; @@ -2540,7 +2510,7 @@ fn handle_faction( client: EcsEntity, target: EcsEntity, args: Vec, - _action: &ChatCommand, + _action: &ServerChatCommand, ) -> CmdResult<()> { no_sudo(client, target)?; @@ -2567,7 +2537,7 @@ fn handle_group( client: EcsEntity, target: EcsEntity, args: Vec, - _action: &ChatCommand, + _action: &ServerChatCommand, ) -> CmdResult<()> { no_sudo(client, target)?; @@ -2594,9 +2564,9 @@ fn handle_group_invite( client: EcsEntity, target: EcsEntity, args: Vec, - action: &ChatCommand, + action: &ServerChatCommand, ) -> CmdResult<()> { - if let Some(target_alias) = parse_args!(args, String) { + if let Some(target_alias) = parse_cmd_args!(args, String) { let target_player = find_alias(server.state.ecs(), &target_alias)?.0; let uid = uid(server, target_player, "player")?; @@ -2633,10 +2603,10 @@ fn handle_group_kick( _client: EcsEntity, target: EcsEntity, args: Vec, - action: &ChatCommand, + action: &ServerChatCommand, ) -> CmdResult<()> { // Checking if leader is already done in group_manip - if let Some(target_alias) = parse_args!(args, String) { + if let Some(target_alias) = parse_cmd_args!(args, String) { let target_player = find_alias(server.state.ecs(), &target_alias)?.0; let uid = uid(server, target_player, "player")?; @@ -2655,7 +2625,7 @@ fn handle_group_leave( _client: EcsEntity, target: EcsEntity, _args: Vec, - _action: &ChatCommand, + _action: &ServerChatCommand, ) -> CmdResult<()> { server .state @@ -2669,10 +2639,10 @@ fn handle_group_promote( _client: EcsEntity, target: EcsEntity, args: Vec, - action: &ChatCommand, + action: &ServerChatCommand, ) -> CmdResult<()> { // Checking if leader is already done in group_manip - if let Some(target_alias) = parse_args!(args, String) { + if let Some(target_alias) = parse_cmd_args!(args, String) { let target_player = find_alias(server.state.ecs(), &target_alias)?.0; let uid = uid(server, target_player, "player")?; @@ -2694,7 +2664,7 @@ fn handle_region( client: EcsEntity, target: EcsEntity, args: Vec, - _action: &ChatCommand, + _action: &ServerChatCommand, ) -> CmdResult<()> { no_sudo(client, target)?; @@ -2715,7 +2685,7 @@ fn handle_say( client: EcsEntity, target: EcsEntity, args: Vec, - _action: &ChatCommand, + _action: &ServerChatCommand, ) -> CmdResult<()> { no_sudo(client, target)?; @@ -2736,7 +2706,7 @@ fn handle_world( client: EcsEntity, target: EcsEntity, args: Vec, - _action: &ChatCommand, + _action: &ServerChatCommand, ) -> CmdResult<()> { no_sudo(client, target)?; @@ -2757,12 +2727,12 @@ fn handle_join_faction( _client: EcsEntity, target: EcsEntity, args: Vec, - _action: &ChatCommand, + _action: &ServerChatCommand, ) -> CmdResult<()> { let players = server.state.ecs().read_storage::(); if let Some(alias) = players.get(target).map(|player| player.alias.clone()) { drop(players); - let (faction_leave, mode) = if let Some(faction) = parse_args!(args, String) { + let (faction_leave, mode) = if let Some(faction) = parse_cmd_args!(args, String) { let mode = comp::ChatMode::Faction(faction.clone()); insert_or_replace_component(server, target, mode.clone(), "target")?; let faction_join = server @@ -2808,7 +2778,7 @@ fn handle_debug_column( client: EcsEntity, target: EcsEntity, _args: Vec, - _action: &ChatCommand, + _action: &ServerChatCommand, ) -> CmdResult<()> { Err("Unsupported without worldgen enabled".into()) } @@ -2819,12 +2789,12 @@ fn handle_debug_column( client: EcsEntity, target: EcsEntity, args: Vec, - _action: &ChatCommand, + _action: &ServerChatCommand, ) -> CmdResult<()> { let sim = server.world.sim(); let calendar = (*server.state.ecs().read_resource::()).clone(); let sampler = server.world.sample_columns(); - let wpos = if let (Some(x), Some(y)) = parse_args!(args, i32, i32) { + let wpos = if let (Some(x), Some(y)) = parse_cmd_args!(args, i32, i32) { Vec2::new(x, y) } else { let pos = position(server, target, "target")?; @@ -2895,13 +2865,13 @@ fn handle_disconnect_all_players( client: EcsEntity, _target: EcsEntity, args: Vec, - _action: &ChatCommand, + _action: &ServerChatCommand, ) -> CmdResult<()> { let client_uuid = uuid(server, client, "client")?; // Make sure temporary mods/admins can't run this command. let _role = real_role(server, client_uuid, "role")?; - if parse_args!(args, String).as_deref() != Some("confirm") { + if parse_cmd_args!(args, String).as_deref() != Some("confirm") { return Err( "Please run the command again with the second argument of \"confirm\" to confirm that \ you really want to disconnect all players from the server" @@ -2939,9 +2909,9 @@ fn handle_skill_point( _client: EcsEntity, target: EcsEntity, args: Vec, - action: &ChatCommand, + action: &ServerChatCommand, ) -> CmdResult<()> { - if let (Some(a_skill_tree), Some(sp), a_alias) = parse_args!(args, String, u16, String) { + if let (Some(a_skill_tree), Some(sp), a_alias) = parse_cmd_args!(args, String, u16, String) { let skill_tree = parse_skill_tree(&a_skill_tree)?; let player = a_alias .map(|alias| find_alias(server.state.ecs(), &alias).map(|(target, _)| target)) @@ -2983,7 +2953,7 @@ fn handle_reload_chunks( _client: EcsEntity, _target: EcsEntity, _args: Vec, - _action: &ChatCommand, + _action: &ServerChatCommand, ) -> CmdResult<()> { server.state.clear_terrain(); @@ -2995,9 +2965,9 @@ fn handle_remove_lights( client: EcsEntity, target: EcsEntity, args: Vec, - _action: &ChatCommand, + _action: &ServerChatCommand, ) -> CmdResult<()> { - let opt_radius = parse_args!(args, f32); + let opt_radius = parse_cmd_args!(args, f32); let player_pos = position(server, target, "target")?; let mut to_delete = vec![]; @@ -3039,10 +3009,10 @@ fn handle_sudo( client: EcsEntity, _target: EcsEntity, args: Vec, - action: &ChatCommand, + action: &ServerChatCommand, ) -> CmdResult<()> { if let (Some(player_alias), Some(cmd), cmd_args) = - parse_args!(args, String, String, ..Vec) + parse_cmd_args!(args, String, String, ..Vec) { if let Ok(action) = cmd.parse() { let (player, player_uuid) = find_alias(server.state.ecs(), &player_alias)?; @@ -3071,7 +3041,7 @@ fn handle_version( client: EcsEntity, _target: EcsEntity, _args: Vec, - _action: &ChatCommand, + _action: &ServerChatCommand, ) -> CmdResult<()> { server.notify_client( client, @@ -3092,11 +3062,11 @@ fn handle_whitelist( client: EcsEntity, _target: EcsEntity, args: Vec, - action: &ChatCommand, + action: &ServerChatCommand, ) -> CmdResult<()> { let now = Utc::now(); - if let (Some(whitelist_action), Some(username)) = parse_args!(args, String, String) { + if let (Some(whitelist_action), Some(username)) = parse_cmd_args!(args, String, String) { let client_uuid = uuid(server, client, "client")?; let client_username = uuid_to_username(server, client, client_uuid)?; let client_role = real_role(server, client_uuid, "client")?; @@ -3190,9 +3160,9 @@ fn handle_kick( client: EcsEntity, _target: EcsEntity, args: Vec, - action: &ChatCommand, + action: &ServerChatCommand, ) -> CmdResult<()> { - if let (Some(target_alias), reason_opt) = parse_args!(args, String, String) { + if let (Some(target_alias), reason_opt) = parse_cmd_args!(args, String, String) { let client_uuid = uuid(server, client, "client")?; let reason = reason_opt.unwrap_or_default(); let ecs = server.state.ecs(); @@ -3220,10 +3190,10 @@ fn handle_ban( client: EcsEntity, _target: EcsEntity, args: Vec, - action: &ChatCommand, + action: &ServerChatCommand, ) -> CmdResult<()> { if let (Some(username), overwrite, parse_duration, reason_opt) = - parse_args!(args, String, bool, HumanDuration, String) + parse_cmd_args!(args, String, bool, HumanDuration, String) { let reason = reason_opt.unwrap_or_default(); let overwrite = overwrite.unwrap_or(false); @@ -3298,7 +3268,7 @@ fn handle_battlemode( client: EcsEntity, target: EcsEntity, args: Vec, - _action: &ChatCommand, + _action: &ServerChatCommand, ) -> CmdResult<()> { // TODO: discuss time const COOLDOWN: f64 = 60.0 * 5.0; @@ -3306,7 +3276,7 @@ fn handle_battlemode( let ecs = server.state.ecs(); let time = ecs.read_resource::