//! # 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::{ server_description::ServerDescription, Ban, BanAction, BanInfo, EditableSetting, SettingError, WhitelistInfo, WhitelistRecord, }, sys::terrain::SpawnEntityData, weather::WeatherJob, wiring, wiring::OutputFormula, Server, Settings, StateExt, }; use assets::AssetExt; use authc::Uuid; use chrono::{NaiveTime, Timelike, Utc}; use common::{ assets, calendar::Calendar, cmd::{ AreaKind, EntityTarget, KitSpec, ServerChatCommand, BUFF_PACK, BUFF_PARSER, KIT_MANIFEST_PATH, PRESET_MANIFEST_PATH, }, comp::{ self, aura::{AuraKindVariant, AuraTarget}, buff::{Buff, BuffData, BuffKind, BuffSource, MiscBuffData}, inventory::{ item::{all_items_expect, tool::AbilityMap, MaterialStatManifest, Quality}, slot::Slot, }, invite::InviteKind, misc::PortalData, AdminRole, Aura, AuraKind, BuffCategory, ChatType, Content, Inventory, Item, LightEmitter, WaypointArea, }, depot, effect::Effect, event::{ ClientDisconnectEvent, CreateNpcEvent, CreateSpecialEntityEvent, EventBus, ExplosionEvent, GroupManipEvent, InitiateInviteEvent, TamePetEvent, }, generation::{EntityConfig, EntityInfo, SpecialEntity}, link::Is, mounting::{Rider, Volume, VolumeRider}, npc::{self, get_npc_name}, outcome::Outcome, parse_cmd_args, resources::{BattleMode, PlayerPhysicsSettings, ProgramTime, Secs, Time, TimeOfDay, TimeScale}, rtsim::{Actor, Role}, spiral::Spiral2d, terrain::{Block, BlockKind, CoordinateConversions, SpriteKind, TerrainChunkSize}, tether::Tethered, uid::Uid, vol::ReadVol, weather, CachedSpatialGrid, Damage, DamageKind, DamageSource, Explosion, GroupTarget, LoadoutBuilder, RadiusEffect, }; use common_net::{ msg::{DisconnectReason, Notification, PlayerListUpdate, ServerGeneral}, sync::WorldSyncExt, }; use common_state::{Areas, AreasContainer, BuildArea, NoDurabilityArea, SpecialAreaError, State}; use core::{cmp::Ordering, convert::TryFrom}; use hashbrown::{HashMap, HashSet}; use humantime::Duration as HumanDuration; use rand::{thread_rng, Rng}; use specs::{storage::StorageEntry, Builder, Entity as EcsEntity, Join, LendJoin, WorldExt}; use std::{fmt::Write, ops::DerefMut, str::FromStr, sync::Arc, time::Duration}; use vek::*; use wiring::{Circuit, Wire, WireNode, WiringAction, WiringActionEffect, WiringElement}; use world::util::{Sampler, LOCALITY}; 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(Content::localized_with_args("command-no-permission", [( "command_name", cmd.keyword(), )])); } let handler: CommandHandler = match cmd { ServerChatCommand::Adminify => handle_adminify, ServerChatCommand::Airship => handle_spawn_airship, ServerChatCommand::Alias => handle_alias, ServerChatCommand::AreaAdd => handle_area_add, ServerChatCommand::AreaList => handle_area_list, ServerChatCommand::AreaRemove => handle_area_remove, ServerChatCommand::Aura => handle_aura, ServerChatCommand::Ban => handle_ban, ServerChatCommand::BattleMode => handle_battlemode, ServerChatCommand::BattleModeForce => handle_battlemode_force, ServerChatCommand::Body => handle_body, ServerChatCommand::Buff => handle_buff, ServerChatCommand::Build => handle_build, ServerChatCommand::Campfire => handle_spawn_campfire, ServerChatCommand::ClearPersistedTerrain => handle_clear_persisted_terrain, ServerChatCommand::DebugColumn => handle_debug_column, ServerChatCommand::DebugWays => handle_debug_ways, 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::IntoNpc => handle_into_npc, 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::Portal => handle_spawn_portal, ServerChatCommand::Region => handle_region, ServerChatCommand::ReloadChunks => handle_reload_chunks, ServerChatCommand::RemoveLights => handle_remove_lights, ServerChatCommand::Respawn => handle_respawn, 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::TimeScale => handle_time_scale, ServerChatCommand::Tp => handle_tp, ServerChatCommand::RtsimTp => handle_rtsim_tp, ServerChatCommand::RtsimInfo => handle_rtsim_info, ServerChatCommand::RtsimNpc => handle_rtsim_npc, ServerChatCommand::RtsimPurge => handle_rtsim_purge, ServerChatCommand::RtsimChunk => handle_rtsim_chunk, 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, ServerChatCommand::Scale => handle_scale, ServerChatCommand::RepairEquipment => handle_repair_equipment, ServerChatCommand::Tether => handle_tether, ServerChatCommand::DestroyTethers => handle_destroy_tethers, ServerChatCommand::Mount => handle_mount, ServerChatCommand::Dismount => handle_dismount, }; 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(|| { Content::localized_with_args("command-position-unavailable", [("target", descriptor)]) }) } 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(|_| Content::localized_with_args("command-entity-dead", [("entity", descriptor)])) } fn uuid(server: &Server, entity: EcsEntity, descriptor: &str) -> CmdResult { server .state .ecs() .read_storage::() .get(entity) .map(|player| player.uuid()) .ok_or_else(|| { Content::localized_with_args("command-player-info-unavailable", [( "target", 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(|| { Content::localized_with_args("command-player-role-unavailable", [( "target", 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(|| { Content::localized_with_args("command-uid-unavailable", [("target", descriptor)]) }) } fn area(server: &mut Server, area_name: &str, kind: &str) -> CmdResult>> { get_areas_mut(kind, &mut server.state)? .area_metas() .get(area_name) .copied() .ok_or_else(|| { Content::localized_with_args("command-area-not-found", [("area", 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(Content::localized("command-no-sudo")) } } /// 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(|| { Content::localized_with_args("command-player-not-found", [("player", 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(|| { Content::localized_with_args("command-player-uuid-not-found", [( "uuid", uuid.to_string(), )]) }) } fn find_username(server: &mut Server, username: &str) -> CmdResult { server .state .mut_resource::() .username_to_uuid(username) .map_err(|_| { Content::localized_with_args("command-username-uuid-unavailable", [( "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 = || { Content::localized_with_args("command-uuid-username-unavailable", [( "uuid", uuid.to_string(), )]) }; 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(Content::localized_with_args( "command-error-while-evaluating-request", [("error", format!("{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( 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, )), comp::Ori::default(), comp::Vel(vel), comp::PickupItem::new(item, ProgramTime(server.state.get_program_time())), None, ); } 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(['/', '\\'], ".")) { 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(Content::localized_with_args( "command-give-inventory-full", [("total", give_amount as u64), ("given", 0)], )); } }); } 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(Content::localized_with_args( "command-give-inventory-full", [("total", give_amount as u64), ("given", i as u64)], )); break; } } }); } let mut inventory_update = server .state .ecs_mut() .write_storage::(); if let Some(update) = inventory_update.get_mut(target) { update.push(comp::InventoryUpdateEvent::Given); } else { inventory_update .insert( target, comp::InventoryUpdate::new(comp::InventoryUpdateEvent::Given), ) .map_err(|_| "Entity target is dead!")?; } res } else { Err(Content::localized_with_args("command-invalid-item", [( "item", item_name, )])) } } else { Err(Content::Plain(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(Content::localized_with_args( "command-invalid-block-kind", [("kind", block_name)], )) } } else { Err(Content::Plain(action.help_string())) } } fn handle_into_npc( server: &mut Server, client: EcsEntity, target: EcsEntity, args: Vec, action: &ServerChatCommand, ) -> CmdResult<()> { use crate::events::shared::{transform_entity, TransformEntityError}; if client != target { server.notify_client( client, ServerGeneral::server_msg( ChatType::CommandInfo, Content::Plain("I hope you aren't abusing this!".to_owned()), ), ); } let Some(entity_config) = parse_cmd_args!(args, String) else { return Err(Content::Plain(action.help_string())); }; let config = match EntityConfig::load(&entity_config) { Ok(asset) => asset.read(), Err(_err) => { return Err(Content::localized_with_args( "command-entity-load-failed", [("config", entity_config)], )); }, }; let mut loadout_rng = thread_rng(); let dummy = Vec3::zero(); let entity_info = EntityInfo::at(dummy).with_entity_config( config.clone(), Some(&entity_config), &mut loadout_rng, None, ); transform_entity(server, target, entity_info, true).map_err(|error| match error { TransformEntityError::EntityDead => { Content::localized_with_args("command-entity-dead", [("entity", "target")]) }, TransformEntityError::UnexpectedSpecialEntity => { Content::localized("command-unimplemented-spawn-special") }, TransformEntityError::LoadingCharacter => { Content::localized("command-transform-invalid-presence") }, TransformEntityError::EntityIsPlayer => { unreachable!( "Transforming players must be valid as we explicitly allowed player transformation" ); }, }) } 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 { // Number of entities must be larger than 1 Some(i8::MIN..=0) => { return Err(Content::localized("command-nof-entities-at-least")); }, // But lower than 50 Some(50..=i8::MAX) => { return Err(Content::localized("command-nof-entities-less-than")); }, Some(number) => number, None => 1, }; let config = match EntityConfig::load(&entity_config) { Ok(asset) => asset.read(), Err(_err) => { return Err(Content::localized_with_args( "command-entity-load-failed", [("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, None, ); match SpawnEntityData::from_entity_info(entity_info) { SpawnEntityData::Special(_, _) => { return Err(Content::localized("command-unimplemented-spawn-special")); }, SpawnEntityData::Npc(data) => { let (npc_builder, _pos) = data.to_npc_builder(); server .state .ecs() .read_resource::>() .emit_now(CreateNpcEvent { pos: comp::Pos(pos), ori: comp::Ori::default(), npc: npc_builder, rider: None, }); }, }; } server.notify_client( client, ServerGeneral::server_msg( ChatType::CommandInfo, Content::localized_with_args("command-spawned-entities-config", [ ("n", number.to_string()), ("config", 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(Content::localized_with_args("command-invalid-sprite", [( "kind", sprite_name, )])) } } else { Err(Content::Plain(action.help_string())) } } fn handle_motd( server: &mut Server, client: EcsEntity, _target: EcsEntity, _args: Vec, _action: &ServerChatCommand, ) -> CmdResult<()> { let locale = server .state .ecs() .read_storage::() .get(client) .and_then(|client| client.locale.clone()); server.notify_client( client, ServerGeneral::server_msg( ChatType::CommandInfo, server .editable_settings() .server_description .get(locale.as_deref()) .map_or("", |d| &d.motd) .to_string(), ), ); 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, String) { (Some(locale), Some(msg)) => { let edit = server .editable_settings_mut() .server_description .edit(data_dir.as_ref(), |d| { let info = format!("Server message of the day set to {:?}", msg); if let Some(description) = d.descriptions.get_mut(&locale) { description.motd = msg; } else { d.descriptions.insert(locale, ServerDescription { motd: msg, rules: None, }); } Some(info) }); drop(data_dir); edit_setting_feedback(server, client, edit, || { unreachable!("edit always returns Some") }) }, (Some(locale), None) => { let edit = server .editable_settings_mut() .server_description .edit(data_dir.as_ref(), |d| { if let Some(description) = d.descriptions.get_mut(&locale) { description.motd.clear(); Some("Removed server message of the day".to_string()) } else { Some("This locale had no motd set".to_string()) } }); drop(data_dir); edit_setting_feedback(server, client, edit, || { unreachable!("edit always returns Some") }) }, _ => Err(Content::Plain(action.help_string())), } } fn handle_jump( server: &mut Server, _client: EcsEntity, target: EcsEntity, args: Vec, action: &ServerChatCommand, ) -> CmdResult<()> { if let (Some(x), Some(y), Some(z), dismount_volume) = parse_cmd_args!(args, f32, f32, f32, bool) { server .state .position_mut(target, dismount_volume.unwrap_or(true), |current_pos| { current_pos.0 += Vec3::new(x, y, z) }) } else { Err(Content::Plain(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), dismount_volume) = parse_cmd_args!(args, f32, f32, f32, bool) { server .state .position_mut(target, dismount_volume.unwrap_or(true), |current_pos| { current_pos.0 = Vec3::new(x, y, z) }) } else { Err(Content::Plain(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), dismount_volume) = parse_cmd_args!(args, String, bool) { 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, ); server .state .position_mut(target, dismount_volume.unwrap_or(true), |current_pos| { current_pos.0 = site_pos }) } else { Err(Content::Plain(action.help_string())) } #[cfg(not(feature = "worldgen"))] Ok(()) } fn handle_respawn( server: &mut Server, _client: EcsEntity, target: EcsEntity, _args: Vec, _action: &ServerChatCommand, ) -> CmdResult<()> { let waypoint = server .state .read_storage::() .get(target) .ok_or("No waypoint set")? .get_pos(); server.state.position_mut(target, true, |current_pos| { current_pos.0 = waypoint; }) } fn handle_kill( server: &mut Server, _client: EcsEntity, target: EcsEntity, _args: Vec, _action: &ServerChatCommand, ) -> CmdResult<()> { server .state .ecs_mut() .write_storage::() .get_mut(target) .map(|mut h| h.kill()); Ok(()) } fn handle_time( server: &mut Server, client: EcsEntity, _target: EcsEntity, args: Vec, _action: &ServerChatCommand, ) -> CmdResult<()> { const DAY: u64 = 86400; let time_in_seconds = server.state.mut_resource::().0; let current_day = time_in_seconds as u64 / DAY; let day_start = (current_day * DAY) as f64; // Find the next occurence of the given time in the day/night cycle let next_cycle = |time| { let new_time = day_start + time; new_time + if new_time < time_in_seconds { DAY as f64 } else { 0.0 } }; let time = parse_cmd_args!(args, String); const EMSG: &str = "time always valid"; let new_time = match time.as_deref() { Some("midnight") => next_cycle( NaiveTime::from_hms_opt(0, 0, 0) .expect(EMSG) .num_seconds_from_midnight() as f64, ), Some("night") => next_cycle( NaiveTime::from_hms_opt(20, 0, 0) .expect(EMSG) .num_seconds_from_midnight() as f64, ), Some("dawn") => next_cycle( NaiveTime::from_hms_opt(5, 0, 0) .expect(EMSG) .num_seconds_from_midnight() as f64, ), Some("morning") => next_cycle( NaiveTime::from_hms_opt(8, 0, 0) .expect(EMSG) .num_seconds_from_midnight() as f64, ), Some("day") => next_cycle( NaiveTime::from_hms_opt(10, 0, 0) .expect(EMSG) .num_seconds_from_midnight() as f64, ), Some("noon") => next_cycle( NaiveTime::from_hms_opt(12, 0, 0) .expect(EMSG) .num_seconds_from_midnight() as f64, ), Some("dusk") => next_cycle( NaiveTime::from_hms_opt(17, 0, 0) .expect(EMSG) .num_seconds_from_midnight() as f64, ), Some(n) => match n.parse::() { Ok(n) => { // Incase the number of digits in the number is greater than 16 if n >= 1e17 { return Err(Content::localized_with_args( "command-time-parse-too-large", [("n", n.to_string())], )); } if n < 0.0 { return Err(Content::localized_with_args( "command-time-parse-negative", [("n", n.to_string())], )); } // Seconds from next midnight next_cycle(0.0) + n }, Err(_) => match NaiveTime::parse_from_str(n, "%H:%M") { // Relative to current day Ok(time) => next_cycle(time.num_seconds_from_midnight() as f64), // Accept `u12345`, seconds since midnight day 0 Err(_) => match n .get(1..) .filter(|_| n.starts_with('u')) .and_then(|n| n.trim_start_matches('u').parse::().ok()) { // Absolute time (i.e. from world epoch) Some(n) => { if (n as f64) < time_in_seconds { return Err(Content::localized_with_args("command-time-backwards", [ ("t", n), ])); } n as f64 }, None => { return Err(Content::localized_with_args("command-time-invalid", [( "t", n, )])); }, }, }, }, None => { // Would this ever change? Perhaps in a few hundred thousand years some // game archeologists of the future will resurrect the best game of all // time which, obviously, would be Veloren. By that time, the inescapable // laws of thermodynamics will mean that the earth's rotation period // would be slower. Of course, a few hundred thousand years is enough // for the circadian rhythm of human biology to have shifted to account // accordingly. When booting up Veloren for the first time in 337,241 // years, they might feel a touch of anguish at the fact that their // earth days and the days within the game do not neatly divide into // one-another. Understandably, they'll want to change this. Who // wouldn't? It would be like turning the TV volume up to an odd number // or having a slightly untuned radio (assuming they haven't begun // broadcasting information directly into their brains). Totally // unacceptable. No, the correct and proper thing to do would be to // release a retroactive definitive edition DLC for $99 with the very // welcome addition of shorter day periods and a complementary // 'developer commentary' mode created by digging up the long-decayed // skeletons of the Veloren team, measuring various attributes of their // jawlines, and using them to recreate their voices. But how to go about // this Herculean task? This code is gibberish! The last of the core Rust // dev team died exactly 337,194 years ago! Rust is now a long-forgotten // dialect of the ancient ones, lost to the sands of time. Ashes to ashes, // dust to dust. When all hope is lost, one particularly intrepid // post-human hominid exployed by the 'Veloren Revival Corp' (no doubt we // still won't have gotten rid of this blasted 'capitalism' thing by then) // might notice, after years of searching, a particularly curious // inscription within the code. The letters `D`, `A`, `Y`. Curious! She // consults the post-human hominid scholars of the old. Care to empathise // with her shock when she discovers that these symbols, as alien as they // may seem, correspond exactly to the word `ⓕя𝐢ᵇᵇ𝔩E`, the word for // 'day' in the post-human hominid language, which is of course universal. // Imagine also her surprise when, after much further translating, she // finds a comment predicting her very existence and her struggle to // decode this great mystery. Rejoice! The Veloren Revival Corp. may now // persist with their great Ultimate Edition DLC because the day period // might now be changed because they have found the constant that controls // it! Everybody was henceforth happy until the end of time. // // This one's for you, xMac ;) let current_time = NaiveTime::from_num_seconds_from_midnight_opt( // Wraps around back to 0s if it exceeds 24 hours (24 hours = 86400s) (time_in_seconds as u64 % DAY) as u32, 0, ); let msg = match current_time { Some(time) => format!("It is {}", time.format("%H:%M")), None => String::from("Unknown Time"), }; server.notify_client( client, ServerGeneral::server_msg(ChatType::CommandInfo, msg), ); return Ok(()); }, }; server.state.mut_resource::().0 = new_time; let time = server.state.ecs().read_resource::