use crate::{assets, comp, npc, terrain}; use assets::AssetExt; use hashbrown::HashMap; use lazy_static::lazy_static; use serde::{Deserialize, Serialize}; use std::{ fmt::{self, Display}, path::Path, str::FromStr, }; use tracing::warn; /// Struct representing a command that a user can run from server chat. pub struct ChatCommandData { /// A list of arguments useful for both tab completion and parsing pub args: Vec, /// A one-line message that explains what the command does pub description: &'static str, /// Whether the command requires administrator permissions. pub needs_admin: IsAdminOnly, } impl ChatCommandData { pub fn new( args: Vec, description: &'static str, needs_admin: IsAdminOnly, ) -> Self { Self { args, description, needs_admin, } } } // Please keep this sorted alphabetically :-) #[derive(Copy, Clone)] pub enum ChatCommand { Adminify, Airship, Alias, ApplyBuff, Ban, 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, MakeSprite, Motd, Object, PermitBuild, Players, Region, RemoveLights, RevokeBuild, RevokeBuildAll, Safezone, Say, ServerPhysics, SetMotd, Site, SkillPoint, Spawn, Sudo, Tell, Time, Tp, Unban, Version, Waypoint, Wiring, Whitelist, World, } // Thank you for keeping this sorted alphabetically :-) pub static CHAT_COMMANDS: &[ChatCommand] = &[ ChatCommand::Adminify, ChatCommand::Airship, ChatCommand::Alias, ChatCommand::ApplyBuff, ChatCommand::Ban, ChatCommand::Build, ChatCommand::BuildAreaAdd, ChatCommand::BuildAreaList, ChatCommand::BuildAreaRemove, ChatCommand::Campfire, ChatCommand::DebugColumn, ChatCommand::DisconnectAllPlayers, ChatCommand::DropAll, ChatCommand::Dummy, ChatCommand::Explosion, ChatCommand::Faction, ChatCommand::GiveItem, ChatCommand::Goto, ChatCommand::Group, ChatCommand::GroupInvite, ChatCommand::GroupKick, ChatCommand::GroupLeave, ChatCommand::GroupPromote, ChatCommand::Health, ChatCommand::Help, ChatCommand::Home, ChatCommand::JoinFaction, ChatCommand::Jump, ChatCommand::Kick, ChatCommand::Kill, ChatCommand::KillNpcs, ChatCommand::Kit, ChatCommand::Lantern, ChatCommand::Light, ChatCommand::MakeBlock, ChatCommand::MakeSprite, ChatCommand::Motd, ChatCommand::Object, ChatCommand::PermitBuild, ChatCommand::Players, ChatCommand::Region, ChatCommand::RemoveLights, ChatCommand::RevokeBuild, ChatCommand::RevokeBuildAll, ChatCommand::Safezone, ChatCommand::Say, ChatCommand::ServerPhysics, ChatCommand::SetMotd, ChatCommand::Site, ChatCommand::SkillPoint, ChatCommand::Spawn, ChatCommand::Sudo, ChatCommand::Tell, ChatCommand::Time, ChatCommand::Tp, ChatCommand::Unban, ChatCommand::Version, ChatCommand::Waypoint, ChatCommand::Wiring, ChatCommand::Whitelist, ChatCommand::World, ]; #[derive(Debug, PartialEq, Serialize, Deserialize, Clone)] pub struct KitManifest(pub HashMap>); impl assets::Asset for KitManifest { type Loader = assets::RonLoader; const EXTENSION: &'static str = "ron"; } lazy_static! { pub static ref CHAT_SHORTCUTS: HashMap = [ ('f', ChatCommand::Faction), ('g', ChatCommand::Group), ('r', ChatCommand::Region), ('s', ChatCommand::Say), ('t', ChatCommand::Tell), ('w', ChatCommand::World), ].iter().cloned().collect(); static ref ALIGNMENTS: Vec = vec!["wild", "enemy", "npc", "pet"] .iter() .map(|s| s.to_string()) .collect(); static ref SKILL_TREES: Vec = vec!["general", "sword", "axe", "hammer", "bow", "staff", "sceptre"] .iter() .map(|s| s.to_string()) .collect(); /// TODO: Make this use hot-reloading static ref ENTITIES: Vec = { let npc_names = &*npc::NPC_NAMES.read(); npc::ALL_NPCS .iter() .map(|&npc| npc_names[npc].keyword.clone()) .collect() }; static ref OBJECTS: Vec = comp::object::ALL_OBJECTS .iter() .map(|o| o.to_string().to_string()) .collect(); static ref TIMES: Vec = vec![ "midnight", "night", "dawn", "morning", "day", "noon", "dusk" ] .iter() .map(|s| s.to_string()) .collect(); static ref BUFFS: Vec = vec![ // Debuffs "burning", "bleeding", "curse", // Heal "regeneration", "saturation", "potion", "campfire_heal", // Outmaxing stats "increase_max_energy", "increase_max_health", // Defensive buffs "invulnerability", "protecting_ward", // One command to rule them all "all", ] .iter() .map(|b| b.to_string()) .collect(); static ref BLOCK_KINDS: Vec = terrain::block::BLOCK_KINDS .keys() .cloned() .collect(); static ref SPRITE_KINDS: Vec = terrain::sprite::SPRITE_KINDS .keys() .cloned() .collect(); /// List of item specifiers. Useful for tab completing static ref ITEM_SPECS: Vec = { let path = assets::ASSETS_PATH.join("common").join("items"); let mut items = vec![]; fn list_items (path: &Path, base: &Path, mut items: &mut Vec) -> std::io::Result<()>{ for entry in std::fs::read_dir(path)? { let path = entry?.path(); if path.is_dir(){ list_items(&path, &base, &mut items)?; } else if let Ok(path) = path.strip_prefix(base) { let path = path.to_string_lossy().trim_end_matches(".ron").replace('/', "."); items.push(path); } } Ok(()) } if list_items(&path, &assets::ASSETS_PATH, &mut items).is_err() { warn!("There was a problem listing item assets"); } items.sort(); items }; static ref KITS: Vec = { if let Ok(kits) = KitManifest::load("server.manifests.kits") { kits.read().0.keys().cloned().collect() } else { Vec::new() } }; } impl ChatCommand { pub fn data(&self) -> ChatCommandData { use ArgumentSpec::*; use IsAdminOnly::*; use Requirement::*; let cmd = ChatCommandData::new; match self { ChatCommand::Adminify => cmd( vec![PlayerName(Required)], "Temporarily gives a player admin permissions or removes them", Admin, ), ChatCommand::Airship => cmd( vec![Float("destination_degrees_ccw_of_east", 90.0, Optional)], "Spawns an airship", Admin, ), ChatCommand::Alias => cmd(vec![Any("name", Required)], "Change your alias", NoAdmin), ChatCommand::ApplyBuff => cmd( vec![ Enum("buff", BUFFS.clone(), Required), Float("strength", 0.01, Optional), Float("duration", 10.0, Optional), ], "Cast a buff on player", Admin, ), ChatCommand::Ban => cmd( vec![Any("username", Required), Message(Optional)], "Ban a player with a given username", Admin, ), ChatCommand::Build => cmd(vec![], "Toggles build mode on and off", NoAdmin), ChatCommand::BuildAreaAdd => cmd( vec![ Any("name", Required), Integer("xlo", 0, Required), Integer("xhi", 10, Required), Integer("ylo", 0, Required), Integer("yhi", 10, Required), Integer("zlo", 0, Required), Integer("zhi", 10, Required), ], "Adds a new build area", Admin, ), ChatCommand::BuildAreaList => cmd(vec![], "List all build areas", Admin), ChatCommand::BuildAreaRemove => cmd( vec![Any("name", Required)], "Removes specified build area", Admin, ), ChatCommand::Campfire => cmd(vec![], "Spawns a campfire", Admin), ChatCommand::DebugColumn => cmd( vec![Integer("x", 15000, Required), Integer("y", 15000, Required)], "Prints some debug information about a column", NoAdmin, ), ChatCommand::DisconnectAllPlayers => cmd( vec![Any("confirm", Required)], "Disconnects all players from the server", Admin, ), ChatCommand::DropAll => cmd(vec![], "Drops all your items on the ground", Admin), ChatCommand::Dummy => cmd(vec![], "Spawns a training dummy", Admin), ChatCommand::Explosion => cmd( vec![Float("radius", 5.0, Required)], "Explodes the ground around you", Admin, ), ChatCommand::Faction => cmd( vec![Message(Optional)], "Send messages to your faction", NoAdmin, ), ChatCommand::GiveItem => cmd( vec![ Enum("item", ITEM_SPECS.clone(), Required), Integer("num", 1, Optional), ], "Give yourself some items", Admin, ), ChatCommand::Goto => cmd( vec![ Float("x", 0.0, Required), Float("y", 0.0, Required), Float("z", 0.0, Required), ], "Teleport to a position", Admin, ), ChatCommand::Group => cmd( vec![Message(Optional)], "Send messages to your group", NoAdmin, ), ChatCommand::GroupInvite => cmd( vec![PlayerName(Required)], "Invite a player to join a group", NoAdmin, ), ChatCommand::GroupKick => cmd( vec![PlayerName(Required)], "Remove a player from a group", NoAdmin, ), ChatCommand::GroupLeave => cmd(vec![], "Leave the current group", NoAdmin), ChatCommand::GroupPromote => cmd( vec![PlayerName(Required)], "Promote a player to group leader", NoAdmin, ), ChatCommand::Health => cmd( vec![Integer("hp", 100, Required)], "Set your current health", Admin, ), ChatCommand::Help => ChatCommandData::new( vec![Command(Optional)], "Display information about commands", NoAdmin, ), ChatCommand::Home => cmd(vec![], "Return to the home town", NoAdmin), ChatCommand::JoinFaction => ChatCommandData::new( vec![Any("faction", Optional)], "Join/leave the specified faction", NoAdmin, ), ChatCommand::Jump => cmd( vec![ Float("x", 0.0, Required), Float("y", 0.0, Required), Float("z", 0.0, Required), ], "Offset your current position", Admin, ), ChatCommand::Kick => cmd( vec![Any("username", Required), Message(Optional)], "Kick a player with a given username", Admin, ), ChatCommand::Kill => cmd(vec![], "Kill yourself", NoAdmin), ChatCommand::KillNpcs => cmd(vec![], "Kill the NPCs", Admin), ChatCommand::Kit => cmd( vec![Enum("kit_name", KITS.to_vec(), Required)], "Place a set of items into your inventory.", Admin, ), ChatCommand::Lantern => cmd( vec![ Float("strength", 5.0, Required), Float("r", 1.0, Optional), Float("g", 1.0, Optional), Float("b", 1.0, Optional), ], "Change your lantern's strength and color", Admin, ), ChatCommand::Light => cmd( vec![ Float("r", 1.0, Optional), Float("g", 1.0, Optional), Float("b", 1.0, Optional), Float("x", 0.0, Optional), Float("y", 0.0, Optional), Float("z", 0.0, Optional), Float("strength", 5.0, Optional), ], "Spawn entity with light", Admin, ), ChatCommand::MakeBlock => cmd( vec![Enum("block", BLOCK_KINDS.clone(), Required)], "Make a block at your location", Admin, ), ChatCommand::MakeSprite => cmd( vec![Enum("sprite", SPRITE_KINDS.clone(), Required)], "Make a sprite at your location", Admin, ), ChatCommand::Motd => cmd( vec![Message(Optional)], "View the server description", NoAdmin, ), ChatCommand::Object => cmd( vec![Enum("object", OBJECTS.clone(), Required)], "Spawn an object", Admin, ), ChatCommand::PermitBuild => cmd( vec![Any("area_name", Required)], "Grants player a bounded box they can build in", Admin, ), ChatCommand::Players => cmd(vec![], "Lists players currently online", NoAdmin), ChatCommand::RemoveLights => cmd( vec![Float("radius", 20.0, Optional)], "Removes all lights spawned by players", Admin, ), ChatCommand::RevokeBuild => cmd( vec![Any("area_name", Required)], "Revokes build area permission for player", Admin, ), ChatCommand::RevokeBuildAll => cmd( vec![], "Revokes all build area permissions for player", Admin, ), ChatCommand::Region => cmd( vec![Message(Optional)], "Send messages to everyone in your region of the world", NoAdmin, ), ChatCommand::Safezone => cmd( vec![Float("range", 100.0, Optional)], "Creates a safezone", Admin, ), ChatCommand::Say => cmd( vec![Message(Optional)], "Send messages to everyone within shouting distance", NoAdmin, ), ChatCommand::ServerPhysics => cmd( vec![ Any("username", Required), Boolean("enabled", "true".to_string(), Optional), ], "Set/unset server-authoritative physics for an account", Admin, ), ChatCommand::SetMotd => { cmd(vec![Message(Optional)], "Set the server description", Admin) }, // Uses Message because site names can contain spaces, which would be assumed to be // separators otherwise ChatCommand::Site => cmd(vec![Message(Required)], "Teleport to a site", Admin), ChatCommand::SkillPoint => cmd( vec![ Enum("skill tree", SKILL_TREES.clone(), Required), Integer("amount", 1, Optional), ], "Give yourself skill points for a particular skill tree", Admin, ), ChatCommand::Spawn => cmd( vec![ Enum("alignment", ALIGNMENTS.clone(), Required), Enum("entity", ENTITIES.clone(), Required), Integer("amount", 1, Optional), Boolean("ai", "true".to_string(), Optional), ], "Spawn a test entity", Admin, ), ChatCommand::Sudo => cmd( vec![PlayerName(Required), SubCommand], "Run command as if you were another player", Admin, ), ChatCommand::Tell => cmd( vec![PlayerName(Required), Message(Optional)], "Send a message to another player", NoAdmin, ), ChatCommand::Time => cmd( vec![Enum("time", TIMES.clone(), Optional)], "Set the time of day", Admin, ), ChatCommand::Tp => cmd( vec![PlayerName(Optional)], "Teleport to another player", Admin, ), ChatCommand::Unban => cmd( vec![Any("username", Required)], "Remove the ban for the given username", Admin, ), ChatCommand::Version => cmd(vec![], "Prints server version", NoAdmin), ChatCommand::Waypoint => { cmd(vec![], "Set your waypoint to your current position", Admin) }, ChatCommand::Wiring => cmd(vec![], "Create wiring element", Admin), ChatCommand::Whitelist => cmd( vec![Any("add/remove", Required), Any("username", Required)], "Adds/removes username to whitelist", Admin, ), ChatCommand::World => cmd( vec![Message(Optional)], "Send messages to everyone on the server", NoAdmin, ), } } /// The keyword used to invoke the command, omitting the leading '/'. pub fn keyword(&self) -> &'static str { match self { ChatCommand::Adminify => "adminify", ChatCommand::Airship => "airship", ChatCommand::Alias => "alias", ChatCommand::ApplyBuff => "buff", ChatCommand::Ban => "ban", 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::MakeSprite => "make_sprite", ChatCommand::Motd => "motd", ChatCommand::Object => "object", ChatCommand::PermitBuild => "permit_build", ChatCommand::Players => "players", ChatCommand::Region => "region", 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::Site => "site", ChatCommand::SkillPoint => "skill_point", 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", } } /// 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::>() .join(" "); format!("{}: {}", usage, data.description) } /// A boolean that is used to check whether the command requires /// administrator permissions or not. pub fn needs_admin(&self) -> bool { IsAdminOnly::Admin == self.data().needs_admin } /// 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::Float(_, _, _) => "{}", ArgumentSpec::Integer(_, _, _) => "{d}", ArgumentSpec::Any(_, _) => "{}", ArgumentSpec::Command(_) => "{}", ArgumentSpec::Message(_) => "{/.*/}", ArgumentSpec::SubCommand => "{} {/.*/}", ArgumentSpec::Enum(_, _, _) => "{}", ArgumentSpec::Boolean(_, _, _) => "{}", }) .collect::>() .join(" ") } } impl Display for ChatCommand { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> { write!(f, "{}", self.keyword()) } } impl FromStr for ChatCommand { type Err = (); fn from_str(keyword: &str) -> Result { let kwd = if let Some(stripped) = keyword.strip_prefix('/') { stripped } else { &keyword }; if keyword.len() == 1 { if let Some(c) = keyword .chars() .next() .as_ref() .and_then(|k| CHAT_SHORTCUTS.get(k)) { return Ok(*c); } } else { for c in CHAT_COMMANDS { if kwd == c.keyword() { return Ok(*c); } } } Err(()) } } #[derive(Eq, PartialEq, Debug)] pub enum IsAdminOnly { Admin, NoAdmin, } #[derive(Eq, PartialEq, Debug)] pub enum Requirement { Required, Optional, } /// Representation for chat command arguments pub enum ArgumentSpec { /// The argument refers to a player by alias PlayerName(Requirement), /// The argument is a float. The associated values are /// * label /// * suggested tab-completion /// * whether it's optional Float(&'static str, f32, Requirement), /// The argument is a float. The associated values are /// * label /// * suggested tab-completion /// * whether it's optional Integer(&'static str, i32, Requirement), /// The argument is any string that doesn't contain spaces Any(&'static str, Requirement), /// The argument is a command name (such as in /help) Command(Requirement), /// This is the final argument, consuming all characters until the end of /// input. Message(Requirement), /// This command is followed by another command (such as in /sudo) SubCommand, /// The argument is likely an enum. The associated values are /// * label /// * Predefined string completions /// * whether it's optional Enum(&'static str, Vec, Requirement), /// The argument is likely a boolean. The associated values are /// * label /// * suggested tab-completion /// * whether it's optional Boolean(&'static str, String, Requirement), } impl ArgumentSpec { pub fn usage_string(&self) -> String { match self { ArgumentSpec::PlayerName(req) => { if &Requirement::Required == req { "".to_string() } else { "[player]".to_string() } }, ArgumentSpec::Float(label, _, req) => { if &Requirement::Required == req { format!("<{}>", label) } else { format!("[{}]", label) } }, ArgumentSpec::Integer(label, _, req) => { if &Requirement::Required == req { format!("<{}>", label) } else { format!("[{}]", label) } }, ArgumentSpec::Any(label, req) => { if &Requirement::Required == req { format!("<{}>", label) } else { format!("[{}]", label) } }, ArgumentSpec::Command(req) => { if &Requirement::Required == req { "<[/]command>".to_string() } else { "[[/]command]".to_string() } }, ArgumentSpec::Message(req) => { if &Requirement::Required == req { "".to_string() } else { "[message]".to_string() } }, ArgumentSpec::SubCommand => "<[/]command> [args...]".to_string(), ArgumentSpec::Enum(label, _, req) => { if &Requirement::Required == req { format! {"<{}>", label} } else { format! {"[{}]", label} } }, ArgumentSpec::Boolean(label, _, req) => { if &Requirement::Required == req { format!("<{}>", label) } else { format!("[{}]", label) } }, } } }