//! # Implementing new commands. //! To implement a new command provide a handler function //! in [do_command]. use crate::{ client::Client, location::Locations, login_provider::LoginProvider, settings::{ Ban, BanAction, BanInfo, EditableSetting, SettingError, WhitelistInfo, WhitelistRecord, }, sys::terrain::NpcData, weather::WeatherSim, wiring, wiring::OutputFormula, Server, Settings, SpawnPoint, StateExt, }; use assets::AssetExt; use authc::Uuid; use chrono::{NaiveTime, Timelike, Utc}; use common::{ assets, calendar::Calendar, cmd::{ KitSpec, ServerChatCommand, BUFF_PACK, BUFF_PARSER, ITEM_SPECS, KIT_MANIFEST_PATH, PRESET_MANIFEST_PATH, }, comp::{ self, aura::{Aura, AuraKind, AuraTarget}, buff::{Buff, BuffCategory, BuffData, BuffKind, BuffSource}, inventory::item::{tool::AbilityMap, MaterialStatManifest, Quality}, invite::InviteKind, AdminRole, ChatType, Inventory, Item, LightEmitter, WaypointArea, }, depot, effect::Effect, event::{EventBus, ServerEvent}, generation::{EntityConfig, EntityInfo}, link::Is, mounting::Rider, npc::{self, get_npc_name}, outcome::Outcome, parse_cmd_args, resources::{BattleMode, PlayerPhysicsSettings, Time, TimeOfDay}, terrain::{Block, BlockKind, SpriteKind, TerrainChunkSize}, uid::{Uid, UidAllocator}, vol::{ReadVol, RectVolSize}, weather, Damage, DamageKind, DamageSource, Explosion, LoadoutBuilder, RadiusEffect, }; use common_net::{ msg::{DisconnectReason, Notification, PlayerListUpdate, ServerGeneral}, sync::WorldSyncExt, }; use common_state::{BuildAreaError, BuildAreas}; use core::{cmp::Ordering, convert::TryFrom, time::Duration}; use hashbrown::{HashMap, HashSet}; use humantime::Duration as HumanDuration; use rand::{thread_rng, Rng}; use specs::{ saveload::MarkerAllocator, storage::StorageEntry, Builder, Entity as EcsEntity, Join, WorldExt, }; use std::{str::FromStr, sync::Arc}; use vek::*; use wiring::{Circuit, Wire, WireNode, WiringAction, WiringActionEffect, WiringElement}; use world::util::Sampler; use common::comp::Alignment; use tracing::{error, info, warn}; pub trait ChatCommandExt { fn execute(&self, server: &mut Server, entity: EcsEntity, args: Vec); } 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( entity, ServerGeneral::server_msg(ChatType::CommandError, err), ); } } } type CmdResult = Result; /// Handler function called when the command is executed. /// # Arguments /// * `&mut Server` - the `Server` instance executing the command. /// * `EcsEntity` - an `Entity` corresponding to the player that invoked the /// command. /// * `EcsEntity` - an `Entity` for the player on whom the command is invoked. /// This differs from the previous argument when using /sudo /// * `Vec` - a `Vec` containing the arguments of the command /// after the keyword. /// * `&ChatCommand` - the command to execute with the above arguments. /// Handler functions must parse arguments from the the given `String` /// (`parse_args!` exists for this purpose). /// /// # Returns /// /// A `Result` that is `Ok` if the command went smoothly, and `Err` if it /// failed; on failure, the string is sent to the client who initiated the /// command. type CommandHandler = fn(&mut Server, EcsEntity, EcsEntity, Vec, &ServerChatCommand) -> CmdResult<()>; fn do_command( server: &mut Server, client: EcsEntity, target: EcsEntity, args: Vec, 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) { return Err(format!( "You don't have permission to use '/{}'.", cmd.keyword() )); } let handler: CommandHandler = match cmd { 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, ServerChatCommand::WeatherZone => handle_weather_zone, ServerChatCommand::Lightning => handle_lightning, }; handler(server, client, target, args, cmd) } // Fallibly get position of entity with the given descriptor (used for error // message). fn position(server: &Server, entity: EcsEntity, descriptor: &str) -> CmdResult { server .state .ecs() .read_storage::() .get(entity) .copied() .ok_or_else(|| format!("Cannot get position for {:?}!", descriptor)) } fn position_mut( server: &mut Server, entity: EcsEntity, descriptor: &str, f: impl for<'a> FnOnce(&'a mut comp::Pos) -> T, ) -> CmdResult { let entity = server .state .ecs() .read_storage::>() .get(entity) .and_then(|is_rider| { server .state .ecs() .read_resource::() .retrieve_entity_internal(is_rider.mount.into()) }) .unwrap_or(entity); let res = server .state .ecs() .write_storage::() .get_mut(entity) .map(f) .ok_or_else(|| format!("Cannot get position for {:?}!", descriptor)); if res.is_ok() { let _ = server .state .ecs() .write_storage::() .insert(entity, comp::ForceUpdate); } res } fn insert_or_replace_component( server: &mut Server, entity: EcsEntity, component: C, descriptor: &str, ) -> CmdResult<()> { server .state .ecs_mut() .write_storage() .insert(entity, component) .and(Ok(())) .map_err(|_| format!("Entity {:?} is dead!", descriptor)) } fn uuid(server: &Server, entity: EcsEntity, descriptor: &str) -> CmdResult { server .state .ecs() .read_storage::() .get(entity) .map(|player| player.uuid()) .ok_or_else(|| format!("Cannot get player information for {:?}", descriptor)) } fn real_role(server: &Server, uuid: Uuid, descriptor: &str) -> CmdResult { server .editable_settings() .admins .get(&uuid) .map(|record| record.role.into()) .ok_or_else(|| format!("Cannot get administrator roles for {:?} uuid", descriptor)) } // Fallibly get uid of entity with the given descriptor (used for error // message). fn uid(server: &Server, target: EcsEntity, descriptor: &str) -> CmdResult { server .state .ecs() .read_storage::() .get(target) .copied() .ok_or_else(|| format!("Cannot get uid for {:?}", descriptor)) } fn area(server: &mut Server, area_name: &str) -> CmdResult>> { server .state .mut_resource::() .area_names() .get(area_name) .copied() .ok_or_else(|| format!("Area name not found: {}", area_name)) } // Prevent use through sudo. fn no_sudo(client: EcsEntity, target: EcsEntity) -> CmdResult<()> { if client == target { Ok(()) } else { // This happens when [ab]using /sudo Err("It's rude to impersonate people".into()) } } /// Ensure that client role is above target role, for the purpose of performing /// some (often permanent) administrative action on the target. Note that this /// function is *not* a replacement for actually verifying that the client /// should be able to execute the command at all, which still needs to be /// rechecked, nor does it guarantee that either the client or the target /// actually have an entry in the admin settings file. /// /// For our purposes, there are *two* roles--temporary role, and permanent role. /// For the purpose of these checks, currently *any* permanent role overrides /// *any* temporary role (this may change if more roles are added that aren't /// moderator or administrator). If the permanent roles match, the temporary /// roles are used as a tiebreaker. /adminify should ensure that no one's /// temporary role can be different from their permanent role without someone /// with a higher role than their permanent role allowing it, and only permanent /// roles should be recorded in the settings files. fn verify_above_role( server: &mut Server, (client, client_uuid): (EcsEntity, Uuid), (player, player_uuid): (EcsEntity, Uuid), reason: &str, ) -> CmdResult<()> { let client_temp = server.entity_admin_role(client); let client_perm = server .editable_settings() .admins .get(&client_uuid) .map(|record| record.role); let player_temp = server.entity_admin_role(player); let player_perm = server .editable_settings() .admins .get(&player_uuid) .map(|record| record.role); if client_perm > player_perm || client_perm == player_perm && client_temp > player_temp { Ok(()) } else { Err(reason.into()) } } fn find_alias(ecs: &specs::World, alias: &str) -> CmdResult<(EcsEntity, Uuid)> { (&ecs.entities(), &ecs.read_storage::()) .join() .find(|(_, player)| player.alias == alias) .map(|(entity, player)| (entity, player.uuid())) .ok_or_else(|| format!("Player {:?} not found!", alias)) } fn find_uuid(ecs: &specs::World, uuid: Uuid) -> CmdResult { (&ecs.entities(), &ecs.read_storage::()) .join() .find(|(_, player)| player.uuid() == uuid) .map(|(entity, _)| entity) .ok_or_else(|| format!("Player with UUID {:?} not found!", uuid)) } fn find_username(server: &mut Server, username: &str) -> CmdResult { server .state .mut_resource::() .username_to_uuid(username) .map_err(|_| format!("Unable to determine UUID for username {:?}", username)) } /// NOTE: Intended to be run only on logged-in clients. fn uuid_to_username( server: &mut Server, fallback_entity: EcsEntity, uuid: Uuid, ) -> CmdResult { let make_err = || format!("Unable to determine username for UUID {:?}", uuid); let player_storage = server.state.ecs().read_storage::(); let fallback_alias = &player_storage .get(fallback_entity) .ok_or_else(make_err)? .alias; server .state .ecs() .read_resource::() .uuid_to_username(uuid, fallback_alias) .map_err(|_| make_err()) } fn edit_setting_feedback( server: &mut Server, client: EcsEntity, result: Option<(String, Result<(), SettingError>)>, failure: impl FnOnce() -> String, ) -> CmdResult<()> { let (info, result) = result.ok_or_else(failure)?; match result { Ok(()) => { server.notify_client( client, ServerGeneral::server_msg(ChatType::CommandInfo, info), ); Ok(()) }, Err(SettingError::Io(err)) => { warn!( ?err, "Failed to write settings file to disk, but succeeded in memory (success message: \ {})", info, ); server.notify_client( client, ServerGeneral::server_msg( ChatType::CommandError, format!( "Failed to write settings file to disk, but succeeded in memory.\n Error (storage): {:?}\n Success (memory): {}", err, info ), ), ); Ok(()) }, Err(SettingError::Integrity(err)) => Err(format!( "Encountered an error while validating the request: {:?}", err )), } } fn handle_drop_all( server: &mut Server, _client: EcsEntity, target: EcsEntity, _args: Vec, _action: &ServerChatCommand, ) -> CmdResult<()> { let pos = position(server, target, "target")?; let mut items = Vec::new(); if let Some(mut inventory) = server .state .ecs() .write_storage::() .get_mut(target) { items = inventory.drain().collect(); } let mut rng = thread_rng(); let item_to_place = items .into_iter() .filter(|i| !matches!(i.quality(), Quality::Debug)); for item in item_to_place { let vel = Vec3::new(rng.gen_range(-0.1..0.1), rng.gen_range(-0.1..0.1), 0.5); server .state .create_item_drop(Default::default(), item) .with(comp::Pos(Vec3::new( pos.0.x + rng.gen_range(5.0..10.0), pos.0.y + rng.gen_range(5.0..10.0), pos.0.z + 5.0, ))) .with(comp::Vel(vel)) .build(); } Ok(()) } fn handle_give_item( server: &mut Server, _client: EcsEntity, target: EcsEntity, args: Vec, action: &ServerChatCommand, ) -> CmdResult<()> { 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; let mut res = Ok(()); const MAX_GIVE_AMOUNT: u32 = 2000; // Cap give_amount for non-stackable items let give_amount = if item.is_stackable() { give_amount } else { give_amount.min(MAX_GIVE_AMOUNT) }; if let Ok(()) = item.set_amount(give_amount) { server .state .ecs() .write_storage::() .get_mut(target) .map(|mut inv| { // NOTE: Deliberately ignores items that couldn't be pushed. if inv.push(item).is_err() { res = Err(format!( "Player inventory full. Gave 0 of {} items.", give_amount )); } }); } else { let ability_map = server.state.ecs().read_resource::(); let msm = server.state.ecs().read_resource::(); // This item can't stack. Give each item in a loop. server .state .ecs() .write_storage::() .get_mut(target) .map(|mut inv| { for i in 0..give_amount { // NOTE: Deliberately ignores items that couldn't be pushed. if inv.push(item.duplicate(&ability_map, &msm)).is_err() { res = Err(format!( "Player inventory full. Gave {} of {} items.", i, give_amount )); break; } } }); } insert_or_replace_component( server, target, comp::InventoryUpdate::new(comp::InventoryUpdateEvent::Given), "target", )?; res } else { Err(format!("Invalid item: {}", item_name)) } } else { Err(action.help_string()) } } fn handle_make_block( server: &mut Server, _client: EcsEntity, target: EcsEntity, args: Vec, action: &ServerChatCommand, ) -> CmdResult<()> { 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))); let pos = pos.0.map(|e| e.floor() as i32); server.state.set_block(pos, new_block); #[cfg(feature = "persistent_world")] if let Some(terrain_persistence) = server .state .ecs() .try_fetch_mut::() .as_mut() { terrain_persistence.set_block(pos, new_block); } Ok(()) } else { Err(format!("Invalid block kind: {}", block_name)) } } else { Err(action.help_string()) } } fn handle_make_npc( server: &mut Server, client: EcsEntity, target: EcsEntity, args: Vec, action: &ServerChatCommand, ) -> CmdResult<()> { 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 { Some(i8::MIN..=0) => { return Err("Number of entities should be at least 1".to_owned()); }, Some(50..=i8::MAX) => { return Err("Number of entities should be less than 50".to_owned()); }, Some(number) => number, None => 1, }; let config = match EntityConfig::load(&entity_config) { Ok(asset) => asset.read(), Err(_err) => return Err(format!("Failed to load entity config: {}", entity_config)), }; let mut loadout_rng = thread_rng(); for _ in 0..number { let comp::Pos(pos) = position(server, target, "target")?; let entity_info = EntityInfo::at(pos).with_entity_config( config.clone(), Some(&entity_config), &mut loadout_rng, ); match NpcData::from_entity_info(entity_info) { NpcData::Waypoint(_) => { return Err("Waypoint spawning is not implemented".to_owned()); }, NpcData::Data { inventory, pos, stats, skill_set, poise, health, body, agent, alignment, scale, loot, } => { let mut entity_builder = server .state .create_npc(pos, stats, skill_set, health, poise, inventory, body) .with(alignment) .with(scale) .with(comp::Vel(Vec3::new(0.0, 0.0, 0.0))); if let Some(agent) = agent { entity_builder = entity_builder.with(agent); } if let Some(drop_item) = loot.to_item() { entity_builder = entity_builder.with(comp::ItemDrop(drop_item)); } // Some would say it's a hack, some would say it's incomplete // simulation. But this is what we do to avoid PvP between npc. let npc_group = match alignment { Alignment::Enemy => Some(comp::group::ENEMY), Alignment::Npc | Alignment::Tame => Some(comp::group::NPC), Alignment::Wild | Alignment::Passive | Alignment::Owned(_) => None, }; if let Some(group) = npc_group { entity_builder = entity_builder.with(group); } entity_builder.build(); }, }; } server.notify_client( client, ServerGeneral::server_msg( ChatType::CommandInfo, format!("Spawned {} entities from config: {}", number, entity_config), ), ); Ok(()) } fn handle_make_sprite( server: &mut Server, _client: EcsEntity, target: EcsEntity, args: Vec, action: &ServerChatCommand, ) -> CmdResult<()> { 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); let new_block = server .state .get_block(pos) // TODO: Make more principled. .unwrap_or_else(|| Block::air(SpriteKind::Empty)) .with_sprite(sk); server.state.set_block(pos, new_block); #[cfg(feature = "persistent_world")] if let Some(terrain_persistence) = server .state .ecs() .try_fetch_mut::() .as_mut() { terrain_persistence.set_block(pos, new_block); } Ok(()) } else { Err(format!("Invalid sprite kind: {}", sprite_name)) } } else { Err(action.help_string()) } } fn handle_motd( server: &mut Server, client: EcsEntity, _target: EcsEntity, _args: Vec, _action: &ServerChatCommand, ) -> CmdResult<()> { server.notify_client( client, ServerGeneral::server_msg( ChatType::CommandInfo, (*server.editable_settings().server_description).clone(), ), ); Ok(()) } fn handle_set_motd( server: &mut Server, client: EcsEntity, _target: EcsEntity, args: Vec, _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_cmd_args!(args, String) { Some(msg) => { let edit = server .editable_settings_mut() .server_description .edit(data_dir.as_ref(), |d| { let info = format!("Server description set to {:?}", msg); **d = msg; Some(info) }); drop(data_dir); edit_setting_feedback(server, client, edit, || { unreachable!("edit always returns Some") }) }, None => { let edit = server .editable_settings_mut() .server_description .edit(data_dir.as_ref(), |d| { d.clear(); Some("Removed server description".to_string()) }); drop(data_dir); edit_setting_feedback(server, client, edit, || { unreachable!("edit always returns Some") }) }, } } fn handle_jump( server: &mut Server, _client: EcsEntity, target: EcsEntity, args: Vec, action: &ServerChatCommand, ) -> CmdResult<()> { 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) }) } else { Err(action.help_string()) } } fn handle_goto( server: &mut Server, _client: EcsEntity, target: EcsEntity, args: Vec, action: &ServerChatCommand, ) -> CmdResult<()> { 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) }) } else { Err(action.help_string()) } } /// TODO: Add autocompletion if possible (might require modifying enum to handle /// dynamic values). fn handle_site( server: &mut Server, _client: EcsEntity, target: EcsEntity, args: Vec, action: &ServerChatCommand, ) -> CmdResult<()> { #[cfg(feature = "worldgen")] if let Some(dest_name) = parse_cmd_args!(args, String) { let site = server .world .civs() .sites() .find(|site| { site.site_tmp .map_or(false, |id| server.index.sites[id].name() == dest_name) }) .ok_or_else(|| "Site not found".to_string())?; let site_pos = server.world.find_accessible_pos( server.index.as_index_ref(), TerrainChunkSize::center_wpos(site.center), false, ); position_mut(server, target, "target", |current_pos| { current_pos.0 = site_pos }) } else { Err(action.help_string()) } #[cfg(not(feature = "worldgen"))] Ok(()) } fn handle_home( server: &mut Server, _client: EcsEntity, target: EcsEntity, _args: Vec, _action: &ServerChatCommand, ) -> CmdResult<()> { let home_pos = server.state.mut_resource::().0; let time = *server.state.mut_resource::