diff --git a/assets/world/map/veloren_0_9_0_0.bin b/assets/world/map/veloren_0_9_0_0.bin index 57aaf69c4e..30c83d4b50 100644 --- a/assets/world/map/veloren_0_9_0_0.bin +++ b/assets/world/map/veloren_0_9_0_0.bin @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:72a2fcf02673a527dced20059f48552fa9e7bb27161ada4847c955b60ad218d2 +oid sha256:1b1b666d8bb7f2ffb604c2ed3f633345cc25c96e3797fc403d44b3e1aebb1ec3 size 16777252 diff --git a/common/src/cmd.rs b/common/src/cmd.rs index bcc878172b..de60502c03 100644 --- a/common/src/cmd.rs +++ b/common/src/cmd.rs @@ -108,6 +108,9 @@ pub enum ChatCommand { Wiring, World, MakeVolume, + Location, + CreateLocation, + DeleteLocation, } #[derive(Debug, PartialEq, Serialize, Deserialize, Clone)] @@ -320,7 +323,7 @@ impl ChatCommand { ), ChatCommand::Ban => cmd( vec![ - Any("username", Required), + PlayerName(Required), Boolean("overwrite", "true".to_string(), Optional), Any("ban duration", Optional), Message(Optional), @@ -458,7 +461,7 @@ impl ChatCommand { Some(Admin), ), ChatCommand::Kick => cmd( - vec![Any("username", Required), Message(Optional)], + vec![PlayerName(Required), Message(Optional)], "Kick a player with a given username", Some(Moderator), ), @@ -559,7 +562,7 @@ impl ChatCommand { ), ChatCommand::ServerPhysics => cmd( vec![ - Any("username", Required), + PlayerName(Required), Boolean("enabled", "true".to_string(), Optional), ], "Set/unset server-authoritative physics for an account", @@ -621,7 +624,7 @@ impl ChatCommand { Some(Moderator), ), ChatCommand::Unban => cmd( - vec![Any("username", Required)], + vec![PlayerName(Required)], "Remove the ban for the given username", Some(Moderator), ), @@ -633,7 +636,7 @@ impl ChatCommand { ), ChatCommand::Wiring => cmd(vec![], "Create wiring element", Some(Admin)), ChatCommand::Whitelist => cmd( - vec![Any("add/remove", Required), Any("username", Required)], + vec![Any("add/remove", Required), PlayerName(Required)], "Adds/removes username to whitelist", Some(Moderator), ), @@ -643,6 +646,19 @@ impl ChatCommand { None, ), ChatCommand::MakeVolume => cmd(vec![], "Create a volume (experimental)", Some(Admin)), + ChatCommand::Location => { + cmd(vec![Any("name", Required)], "Teleport to a location", None) + }, + ChatCommand::CreateLocation => cmd( + vec![Any("name", Required)], + "Create a location at the current position", + Some(Moderator), + ), + ChatCommand::DeleteLocation => cmd( + vec![Any("name", Required)], + "Delete a location", + Some(Moderator), + ), } } @@ -715,6 +731,9 @@ impl ChatCommand { ChatCommand::Whitelist => "whitelist", ChatCommand::World => "world", ChatCommand::MakeVolume => "make_volume", + ChatCommand::Location => "location", + ChatCommand::CreateLocation => "create_location", + ChatCommand::DeleteLocation => "delete_location", } } diff --git a/server/src/cmd.rs b/server/src/cmd.rs index 5eb1c8adf8..97eff4ed8c 100644 --- a/server/src/cmd.rs +++ b/server/src/cmd.rs @@ -4,6 +4,7 @@ use crate::{ client::Client, + location::Locations, login_provider::LoginProvider, settings::{ Ban, BanAction, BanInfo, EditableSetting, SettingError, WhitelistInfo, WhitelistRecord, @@ -182,6 +183,9 @@ fn do_command( ChatCommand::Whitelist => handle_whitelist, ChatCommand::World => handle_world, ChatCommand::MakeVolume => handle_make_volume, + ChatCommand::Location => handle_location, + ChatCommand::CreateLocation => handle_create_location, + ChatCommand::DeleteLocation => handle_delete_location, }; handler(server, client, target, args, cmd) @@ -3516,3 +3520,101 @@ fn set_skills(skill_set: &mut comp::SkillSet, preset: &str) -> CmdResult<()> { Err("Such preset doesn't exist".to_owned()) } } + +fn handle_location( + server: &mut Server, + client: EcsEntity, + target: EcsEntity, + args: Vec, + _action: &ChatCommand, +) -> CmdResult<()> { + if let Some(name) = parse_args!(args, String) { + let loc = server.state.ecs().read_resource::().get(&name); + match loc { + Ok(loc) => position_mut(server, target, "target", |target_pos| { + target_pos.0 = loc; + }), + Err(e) => Err(e.to_string()), + } + } else { + let locations = server.state.ecs().read_resource::(); + let mut locations = locations.iter().map(|s| s.as_str()).collect::>(); + locations.sort_unstable(); + server.notify_client( + client, + ServerGeneral::server_msg( + ChatType::CommandInfo, + if locations.is_empty() { + "No locations currently exist".to_owned() + } else { + format!("Available locations:\n{}", locations.join(", ")) + }, + ), + ); + Ok(()) + } +} + +fn handle_create_location( + server: &mut Server, + client: EcsEntity, + target: EcsEntity, + args: Vec, + action: &ChatCommand, +) -> CmdResult<()> { + if let Some(name) = parse_args!(args, String) { + let target_pos = position(server, target, "target")?; + + let res = server + .state + .ecs_mut() + .write_resource::() + .insert(name.clone(), target_pos.0); + match res { + Ok(()) => { + server.notify_client( + client, + ServerGeneral::server_msg( + ChatType::CommandInfo, + format!("Created location '{}'", name), + ), + ); + Ok(()) + }, + Err(e) => Err(e.to_string()), + } + } else { + Err(action.help_string()) + } +} + +fn handle_delete_location( + server: &mut Server, + client: EcsEntity, + _target: EcsEntity, + args: Vec, + action: &ChatCommand, +) -> CmdResult<()> { + if let Some(name) = parse_args!(args, String) { + let res = server + .state + .ecs_mut() + .write_resource::() + .remove(&name); + match res { + Ok(()) => { + server.notify_client( + client, + ServerGeneral::server_msg( + ChatType::CommandInfo, + format!("Deleted location '{}'", name), + ), + ); + Ok(()) + }, + Err(e) => Err(e.to_string()), + } + } else { + Err(action.help_string()) + } +} diff --git a/server/src/lib.rs b/server/src/lib.rs index 1dd69b605a..d77cc6c68a 100644 --- a/server/src/lib.rs +++ b/server/src/lib.rs @@ -23,6 +23,7 @@ mod data_dir; pub mod error; pub mod events; pub mod input; +pub mod location; pub mod login_provider; pub mod metrics; pub mod persistence; @@ -55,6 +56,7 @@ use crate::{ cmd::ChatCommandExt, connection_handler::ConnectionHandler, data_dir::DataDir, + location::Locations, login_provider::LoginProvider, persistence::PersistedComponents, presence::{Presence, RegionSubscription, RepositionOnChunkLoad}, @@ -243,6 +245,7 @@ impl Server { }); state.ecs_mut().insert(EventBus::::default()); state.ecs_mut().insert(Vec::::new()); + state.ecs_mut().insert(Locations::default()); state.ecs_mut().insert(LoginProvider::new( settings.auth_server_address.clone(), Arc::clone(&runtime), diff --git a/server/src/location.rs b/server/src/location.rs new file mode 100644 index 0000000000..89d1392a28 --- /dev/null +++ b/server/src/location.rs @@ -0,0 +1,65 @@ +use hashbrown::HashMap; +use std::fmt; +use vek::*; + +#[derive(Debug)] +pub enum LocationError<'a> { + InvalidName(String), + DuplicateName(String), + DoesNotExist(&'a str), +} + +impl<'a> fmt::Display for LocationError<'a> { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + Self::InvalidName(name) => write!( + f, + "Location name '{}' is invalid. Names may only contain lowercase ASCII and \ + underscores", + name + ), + Self::DuplicateName(name) => write!( + f, + "Location '{}' already exists, consider deleting it first", + name + ), + Self::DoesNotExist(name) => write!(f, "Location '{}' does not exist", name), + } + } +} + +/// Locations are moderator-defined positions that can be teleported between by +/// players. They currently do not persist between server sessions. +#[derive(Default)] +pub struct Locations { + locations: HashMap>, +} + +impl Locations { + pub fn insert(&mut self, name: String, pos: Vec3) -> Result<(), LocationError<'static>> { + if name.chars().all(|c| c.is_ascii_lowercase() || c == '_') { + self.locations + .try_insert(name, pos) + .map(|_| ()) + .map_err(|o| LocationError::DuplicateName(o.entry.key().clone())) + } else { + Err(LocationError::InvalidName(name)) + } + } + + pub fn get<'a>(&self, name: &'a str) -> Result, LocationError<'a>> { + self.locations + .get(name) + .copied() + .ok_or(LocationError::DoesNotExist(name)) + } + + pub fn iter(&self) -> impl Iterator { self.locations.keys() } + + pub fn remove<'a>(&mut self, name: &'a str) -> Result<(), LocationError<'a>> { + self.locations + .remove(name) + .map(|_| ()) + .ok_or(LocationError::DoesNotExist(name)) + } +}