//! # 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, 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}, parse_cmd_args, resources::{BattleMode, PlayerPhysicsSettings, Time, TimeOfDay}, terrain::{Block, BlockKind, SpriteKind, TerrainChunkSize}, uid::{Uid, UidAllocator}, vol::{ReadVol, RectVolSize}, 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, }; 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 = rand::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 = rand::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::(); position_mut(server, target, "target", |current_pos| { current_pos.0 = home_pos })?; insert_or_replace_component( server, target, comp::Waypoint::temp_new(home_pos, time), "target", ) } 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); let new_time = match time.as_deref() { Some("midnight") => { next_cycle(NaiveTime::from_hms(0, 0, 0).num_seconds_from_midnight() as f64) }, Some("night") => { next_cycle(NaiveTime::from_hms(20, 0, 0).num_seconds_from_midnight() as f64) }, Some("dawn") => next_cycle(NaiveTime::from_hms(5, 0, 0).num_seconds_from_midnight() as f64), Some("morning") => { next_cycle(NaiveTime::from_hms(8, 0, 0).num_seconds_from_midnight() as f64) }, Some("day") => next_cycle(NaiveTime::from_hms(10, 0, 0).num_seconds_from_midnight() as f64), Some("noon") => { next_cycle(NaiveTime::from_hms(12, 0, 0).num_seconds_from_midnight() as f64) }, Some("dusk") => { next_cycle(NaiveTime::from_hms(17, 0, 0).num_seconds_from_midnight() as f64) }, Some(n) => match n.parse() { Ok(n) => 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: since in-game epoch) Some(n) => n as f64, None => { return Err(format!("{:?} is not a valid time.", 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 jibberish! 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 gotted 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. Rejoyce! 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; // Update all clients with the new TimeOfDay (without this they would have to // wait for the next 100th tick to receive the update). let mut tod_lazymsg = None; let clients = server.state.ecs().read_storage::(); let calendar = server.state.ecs().read_resource::(); for client in (&clients).join() { let msg = tod_lazymsg.unwrap_or_else(|| { client.prepare(ServerGeneral::TimeOfDay( TimeOfDay(new_time), (*calendar).clone(), )) }); let _ = client.send_prepared(&msg); tod_lazymsg = Some(msg); } if let Some(new_time) = NaiveTime::from_num_seconds_from_midnight_opt(((new_time as u64) % 86400) as u32, 0) { server.notify_client( client, ServerGeneral::server_msg( ChatType::CommandInfo, format!("Time changed to: {}", new_time.format("%H:%M")), ), ); } Ok(()) } fn handle_health( server: &mut Server, _client: EcsEntity, target: EcsEntity, args: Vec, _action: &ServerChatCommand, ) -> CmdResult<()> { if let Some(hp) = parse_cmd_args!(args, f32) { if let Some(mut health) = server .state .ecs() .write_storage::() .get_mut(target) { let time = server.state.ecs().read_resource::