diff --git a/CHANGELOG.md b/CHANGELOG.md index 9a2f2f76a5..a7655fc9af 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -50,6 +50,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Some sprites can be sat on. - Pet birds can now sit on the player's shoulder as they explore the world. - Adlet caves +- Durability free areas (`/area_add no_durability ...`) ### Changed @@ -65,6 +66,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - The game now starts in fullscreen by default - Default audio volume should be less likely to destroy ear drums - Creatures flee less quickly when low on health +- All `/build_area_*` commands have been renamed to `/area_*`, and you will have to pass an additional area type ### Removed diff --git a/common/src/cmd.rs b/common/src/cmd.rs index 528ed79474..83bb096b71 100644 --- a/common/src/cmd.rs +++ b/common/src/cmd.rs @@ -12,7 +12,7 @@ use std::{ fmt::{self, Display}, str::FromStr, }; -use strum::IntoEnumIterator; +use strum::{AsRefStr, EnumIter, EnumString, IntoEnumIterator}; use tracing::warn; /// Struct representing a command that a user can run from server chat. @@ -66,6 +66,15 @@ impl assets::Asset for SkillPresetManifest { pub const KIT_MANIFEST_PATH: &str = "server.manifests.kits"; pub const PRESET_MANIFEST_PATH: &str = "server.manifests.presets"; +/// Enum for all possible area types +#[derive(Debug, Clone, EnumIter, EnumString, AsRefStr)] +pub enum AreaKind { + #[strum(serialize = "build")] + Build, + #[strum(serialize = "no_durability")] + NoDurability, +} + lazy_static! { static ref ALIGNMENTS: Vec = vec!["wild", "enemy", "npc", "pet"] .iter() @@ -114,6 +123,7 @@ lazy_static! { souls }; + static ref AREA_KINDS: Vec = AreaKind::iter().map(|kind| kind.as_ref().to_string()).collect(); static ref OBJECTS: Vec = comp::object::ALL_OBJECTS .iter() .map(|o| o.to_string().to_string()) @@ -247,15 +257,15 @@ pub enum ServerChatCommand { Adminify, Airship, Alias, + AreaAdd, + AreaList, + AreaRemove, Ban, BattleMode, BattleModeForce, Body, Buff, Build, - BuildAreaAdd, - BuildAreaList, - BuildAreaRemove, Campfire, CreateLocation, DebugColumn, @@ -400,9 +410,10 @@ impl ServerChatCommand { Some(Admin), ), ServerChatCommand::Build => cmd(vec![], "Toggles build mode on and off", None), - ServerChatCommand::BuildAreaAdd => cmd( + ServerChatCommand::AreaAdd => cmd( vec![ Any("name", Required), + Enum("kind", AREA_KINDS.clone(), Required), Integer("xlo", 0, Required), Integer("xhi", 10, Required), Integer("ylo", 0, Required), @@ -413,9 +424,12 @@ impl ServerChatCommand { "Adds a new build area", Some(Admin), ), - ServerChatCommand::BuildAreaList => cmd(vec![], "List all build areas", Some(Admin)), - ServerChatCommand::BuildAreaRemove => cmd( - vec![Any("name", Required)], + ServerChatCommand::AreaList => cmd(vec![], "List all build areas", Some(Admin)), + ServerChatCommand::AreaRemove => cmd( + vec![ + Any("name", Required), + Enum("kind", AREA_KINDS.clone(), Required), + ], "Removes specified build area", Some(Admin), ), @@ -807,9 +821,9 @@ impl ServerChatCommand { ServerChatCommand::Body => "body", ServerChatCommand::Buff => "buff", ServerChatCommand::Build => "build", - ServerChatCommand::BuildAreaAdd => "build_area_add", - ServerChatCommand::BuildAreaList => "build_area_list", - ServerChatCommand::BuildAreaRemove => "build_area_remove", + ServerChatCommand::AreaAdd => "area_add", + ServerChatCommand::AreaList => "area_list", + ServerChatCommand::AreaRemove => "area_remove", ServerChatCommand::Campfire => "campfire", ServerChatCommand::DebugColumn => "debug_column", ServerChatCommand::DebugWays => "debug_ways", diff --git a/common/state/src/lib.rs b/common/state/src/lib.rs index 34025380bf..53c52c3c7d 100644 --- a/common/state/src/lib.rs +++ b/common/state/src/lib.rs @@ -1,9 +1,9 @@ //! This crate contains the [`State`] and shared between //! server (`veloren-server`) and the client (`veloren-client`) -mod build_areas; #[cfg(feature = "plugins")] pub mod plugin; +mod special_areas; mod state; // TODO: breakup state module and remove glob -pub use build_areas::{BuildAreaError, BuildAreas}; +pub use special_areas::*; pub use state::{BlockChange, BlockDiff, State, TerrainChanges}; diff --git a/common/state/src/build_areas.rs b/common/state/src/special_areas.rs similarity index 62% rename from common/state/src/build_areas.rs rename to common/state/src/special_areas.rs index 7e520f7ac0..6922f3520f 100644 --- a/common/state/src/build_areas.rs +++ b/common/state/src/special_areas.rs @@ -1,17 +1,30 @@ use common::depot::{Depot, Id}; use hashbrown::{hash_map, HashMap}; +use std::{ + marker::PhantomData, + ops::{Deref, DerefMut}, +}; use vek::*; +#[derive(Default)] +pub struct AreasContainer(Areas, PhantomData); + +#[derive(Default)] +pub struct BuildArea; + +#[derive(Default)] +pub struct NoDurabilityArea; + /// NOTE: Please don't add `Deserialize` without checking to make sure we /// can guarantee the invariant that every entry in `area_names` points to a /// valid id in `areas`. #[derive(Default)] -pub struct BuildAreas { +pub struct Areas { areas: Depot>, area_names: HashMap>>, } -pub enum BuildAreaError { +pub enum SpecialAreaError { /// This build area name is reserved by the system. Reserved, /// The build area name was not found. @@ -21,10 +34,10 @@ pub enum BuildAreaError { /// Build area names that can only be inserted, not removed. const RESERVED_BUILD_AREA_NAMES: &[&str] = &["world"]; -impl BuildAreas { +impl Areas { pub fn areas(&self) -> &Depot> { &self.areas } - pub fn area_names(&self) -> &HashMap>> { &self.area_names } + pub fn area_metas(&self) -> &HashMap>> { &self.area_names } /// If the area_name is already in the map, returns Err(area_name). pub fn insert(&mut self, area_name: String, area: Aabb) -> Result>, String> { @@ -37,14 +50,14 @@ impl BuildAreas { Ok(bb_id) } - pub fn remove(&mut self, area_name: &str) -> Result, BuildAreaError> { + pub fn remove(&mut self, area_name: &str) -> Result, SpecialAreaError> { if RESERVED_BUILD_AREA_NAMES.contains(&area_name) { - return Err(BuildAreaError::Reserved); + return Err(SpecialAreaError::Reserved); } let bb_id = self .area_names .remove(area_name) - .ok_or(BuildAreaError::NotFound)?; + .ok_or(SpecialAreaError::NotFound)?; let area = self.areas.remove(bb_id).expect( "Entries in `areas` are added before entries in `area_names` in `insert`, and that is \ the only exposed way to add elements to `area_names`.", @@ -52,3 +65,31 @@ impl BuildAreas { Ok(area) } } + +impl Deref for AreasContainer +where + Kind: AreaKind, +{ + type Target = Areas; + + fn deref(&self) -> &Self::Target { &self.0 } +} + +impl DerefMut for AreasContainer +where + Kind: AreaKind, +{ + fn deref_mut(&mut self) -> &mut Self::Target { &mut self.0 } +} + +pub trait AreaKind { + fn display() -> &'static str; +} + +impl AreaKind for BuildArea { + fn display() -> &'static str { "build" } +} + +impl AreaKind for NoDurabilityArea { + fn display() -> &'static str { "durability free" } +} diff --git a/common/state/src/state.rs b/common/state/src/state.rs index 5293f7b548..3464bb01e4 100644 --- a/common/state/src/state.rs +++ b/common/state/src/state.rs @@ -2,6 +2,7 @@ use crate::plugin::memory_manager::EcsWorld; #[cfg(feature = "plugins")] use crate::plugin::PluginMgr; +use crate::{BuildArea, NoDurabilityArea}; #[cfg(feature = "plugins")] use common::uid::UidAllocator; use common::{ @@ -275,7 +276,8 @@ impl State { ecs.insert(TerrainGrid::new(map_size_lg, default_chunk).unwrap()); ecs.insert(BlockChange::default()); ecs.insert(ScheduledBlockChange::default()); - ecs.insert(crate::build_areas::BuildAreas::default()); + ecs.insert(crate::special_areas::AreasContainer::::default()); + ecs.insert(crate::special_areas::AreasContainer::::default()); ecs.insert(TerrainChanges::default()); ecs.insert(EventBus::::default()); ecs.insert(game_mode); diff --git a/server/src/cmd.rs b/server/src/cmd.rs index ccfd38df08..65e0c09167 100644 --- a/server/src/cmd.rs +++ b/server/src/cmd.rs @@ -21,8 +21,8 @@ use common::{ assets, calendar::Calendar, cmd::{ - KitSpec, ServerChatCommand, BUFF_PACK, BUFF_PARSER, ITEM_SPECS, KIT_MANIFEST_PATH, - PRESET_MANIFEST_PATH, + AreaKind, KitSpec, ServerChatCommand, BUFF_PACK, BUFF_PARSER, ITEM_SPECS, + KIT_MANIFEST_PATH, PRESET_MANIFEST_PATH, }, comp::{ self, @@ -55,7 +55,7 @@ use common_net::{ msg::{DisconnectReason, Notification, PlayerListUpdate, ServerGeneral}, sync::WorldSyncExt, }; -use common_state::{BuildAreaError, BuildAreas}; +use common_state::{Areas, AreasContainer, BuildArea, NoDurabilityArea, SpecialAreaError, State}; use core::{cmp::Ordering, convert::TryFrom}; use hashbrown::{HashMap, HashSet}; use humantime::Duration as HumanDuration; @@ -63,7 +63,7 @@ use rand::{thread_rng, Rng}; use specs::{ saveload::MarkerAllocator, storage::StorageEntry, Builder, Entity as EcsEntity, Join, WorldExt, }; -use std::{fmt::Write, str::FromStr, sync::Arc}; +use std::{fmt::Write, ops::DerefMut, str::FromStr, sync::Arc}; use vek::*; use wiring::{Circuit, Wire, WireNode, WiringAction, WiringActionEffect, WiringElement}; use world::util::{Sampler, LOCALITY}; @@ -133,9 +133,9 @@ fn do_command( ServerChatCommand::Body => handle_body, ServerChatCommand::Buff => handle_buff, ServerChatCommand::Build => handle_build, - ServerChatCommand::BuildAreaAdd => handle_build_area_add, - ServerChatCommand::BuildAreaList => handle_build_area_list, - ServerChatCommand::BuildAreaRemove => handle_build_area_remove, + ServerChatCommand::AreaAdd => handle_area_add, + ServerChatCommand::AreaList => handle_area_list, + ServerChatCommand::AreaRemove => handle_area_remove, ServerChatCommand::Campfire => handle_spawn_campfire, ServerChatCommand::DebugColumn => handle_debug_column, ServerChatCommand::DebugWays => handle_debug_ways, @@ -351,11 +351,9 @@ fn uid(server: &Server, target: EcsEntity, descriptor: &str) -> CmdResult { .ok_or_else(|| format!("Cannot get uid for {:?}", descriptor)) } -fn area(server: &mut Server, area_name: &str) -> CmdResult>> { - server - .state - .mut_resource::() - .area_names() +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(|| format!("Area name not found: {}", area_name)) @@ -1841,7 +1839,7 @@ fn handle_permit_build( action: &ServerChatCommand, ) -> CmdResult<()> { if let Some(area_name) = parse_cmd_args!(args, String) { - let bb_id = area(server, &area_name)?; + let bb_id = area(server, &area_name, "build")?; let mut can_build = server.state.ecs().write_storage::(); let entry = can_build .entry(target) @@ -1882,7 +1880,7 @@ fn handle_revoke_build( action: &ServerChatCommand, ) -> CmdResult<()> { if let Some(area_name) = parse_cmd_args!(args, String) { - let bb_id = area(server, &area_name)?; + let bb_id = area(server, &area_name, "build")?; let mut can_build = server.state.ecs_mut().write_storage::(); if let Some(mut comp_can_build) = can_build.get_mut(target) { comp_can_build.build_areas.retain(|&x| x != bb_id); @@ -2006,27 +2004,47 @@ fn handle_build( } } -fn handle_build_area_add( +fn get_areas_mut<'l>(kind: &str, state: &'l mut State) -> CmdResult<&'l mut Areas> { + Ok(match AreaKind::from_str(kind).ok() { + Some(AreaKind::Build) => state + .mut_resource::>() + .deref_mut(), + Some(AreaKind::NoDurability) => state + .mut_resource::>() + .deref_mut(), + None => Err(format!("Invalid area type '{kind}'"))?, + }) +} + +fn handle_area_add( server: &mut Server, client: EcsEntity, _target: EcsEntity, args: Vec, action: &ServerChatCommand, ) -> CmdResult<()> { - if let (Some(area_name), Some(xlo), Some(xhi), Some(ylo), Some(yhi), Some(zlo), Some(zhi)) = - parse_cmd_args!(args, String, i32, i32, i32, i32, i32, i32) + if let ( + Some(area_name), + Some(kind), + Some(xlo), + Some(xhi), + Some(ylo), + Some(yhi), + Some(zlo), + Some(zhi), + ) = parse_cmd_args!(args, String, String, i32, i32, i32, i32, i32, i32) { - let build_areas = server.state.mut_resource::(); + let special_areas = get_areas_mut(&kind, &mut server.state)?; let msg = ServerGeneral::server_msg( ChatType::CommandInfo, - format!("Created build zone {}", area_name), + format!("Created {kind} zone {}", area_name), ); - build_areas + special_areas .insert(area_name, Aabb { min: Vec3::new(xlo, ylo, zlo), max: Vec3::new(xhi, yhi, zhi), }) - .map_err(|area_name| format!("Build zone {} already exists!", area_name))?; + .map_err(|area_name| format!("{kind} zone {} already exists!", area_name))?; server.notify_client(client, msg); Ok(()) } else { @@ -2034,54 +2052,67 @@ fn handle_build_area_add( } } -fn handle_build_area_list( +fn handle_area_list( server: &mut Server, client: EcsEntity, _target: EcsEntity, _args: Vec, _action: &ServerChatCommand, ) -> CmdResult<()> { - let build_areas = server.state.mut_resource::(); - let msg = ServerGeneral::server_msg( - ChatType::CommandInfo, - build_areas.area_names().iter().fold( - "Build Areas:".to_string(), - |acc, (area_name, bb_id)| { - if let Some(aabb) = build_areas.areas().get(*bb_id) { - format!("{}\n{}: {} to {}", acc, area_name, aabb.min, aabb.max) + let format_areas = |areas: &Areas, kind: &str| { + areas + .area_metas() + .iter() + .fold(format!("{kind} areas:"), |acc, (area_name, bb_id)| { + if let Some(aabb) = areas.areas().get(*bb_id) { + format!("{}\n{}: {} to {} ()", acc, area_name, aabb.min, aabb.max,) } else { acc } - }, - ), + }) + }; + let build_message = format_areas( + server.state.mut_resource::>(), + "Build", + ); + let no_dura_message = format_areas( + server + .state + .mut_resource::>(), + "Durability free", + ); + + let msg = ServerGeneral::server_msg( + ChatType::CommandInfo, + [build_message, no_dura_message].join("\n"), ); server.notify_client(client, msg); Ok(()) } -fn handle_build_area_remove( +fn handle_area_remove( server: &mut Server, client: EcsEntity, _target: EcsEntity, args: Vec, action: &ServerChatCommand, ) -> CmdResult<()> { - if let Some(area_name) = parse_cmd_args!(args, String) { - let build_areas = server.state.mut_resource::(); + if let (Some(area_name), Some(kind)) = parse_cmd_args!(args, String, String) { + let areas = get_areas_mut(&kind, &mut server.state)?; - build_areas.remove(&area_name).map_err(|err| match err { - BuildAreaError::Reserved => format!( - "Build area is reserved and cannot be removed: {}", + areas.remove(&area_name).map_err(|err| match err { + SpecialAreaError::Reserved => format!( + "Special area is reserved and cannot be removed: {}", area_name ), - BuildAreaError::NotFound => format!("No such build area {}", area_name), + SpecialAreaError::NotFound => format!("No such build area {}", area_name), })?; server.notify_client( client, ServerGeneral::server_msg( ChatType::CommandInfo, - format!("Removed build zone {}", area_name), + format!("Removed {kind} zone {area_name}"), ), ); Ok(()) diff --git a/server/src/events/entity_manipulation.rs b/server/src/events/entity_manipulation.rs index 2a5a08ca03..5a87fab846 100644 --- a/server/src/events/entity_manipulation.rs +++ b/server/src/events/entity_manipulation.rs @@ -38,7 +38,7 @@ use common::{ Damage, DamageKind, DamageSource, Explosion, GroupTarget, RadiusEffect, }; use common_net::{msg::ServerGeneral, sync::WorldSyncExt}; -use common_state::BlockChange; +use common_state::{AreasContainer, BlockChange, NoDurabilityArea}; use hashbrown::HashSet; use rand::Rng; use specs::{ @@ -521,9 +521,28 @@ pub fn handle_destroy(server: &mut Server, entity: EcsEntity, last_change: Healt true }; + let resists_durability = state + .ecs() + .read_storage::() + .get(entity) + .cloned() + .map_or(false, |our_pos| { + let areas_container = state + .ecs() + .read_resource::>(); + let our_pos = our_pos.0.map(|i| i as i32); + + let is_in_area = areas_container + .areas() + .iter() + .any(|(_, area)| area.contains_point(our_pos)); + + is_in_area + }); + // TODO: Do we need to do this if `should_delete` is true? // Modify durability on all equipped items - if let Some(mut inventory) = state.ecs().write_storage::().get_mut(entity) { + if !resists_durability && let Some(mut inventory) = state.ecs().write_storage::().get_mut(entity) { let ecs = state.ecs(); let ability_map = ecs.read_resource::(); let msm = ecs.read_resource::(); diff --git a/server/src/lib.rs b/server/src/lib.rs index e07e2eb4ca..2a6819fd18 100644 --- a/server/src/lib.rs +++ b/server/src/lib.rs @@ -89,7 +89,7 @@ use common_net::{ msg::{ClientType, DisconnectReason, ServerGeneral, ServerInfo, ServerMsg}, sync::WorldSyncExt, }; -use common_state::{BlockDiff, BuildAreas, State}; +use common_state::{AreasContainer, BlockDiff, BuildArea, State}; use common_systems::add_local_systems; use metrics::{EcsSystemMetrics, PhysicsMetrics, TickMetrics}; use network::{ListenAddr, Network, Pid}; @@ -447,7 +447,7 @@ impl Server { state .ecs() - .write_resource::() + .write_resource::>() .insert("world".to_string(), world_aabb) .expect("The initial insert should always work."); } diff --git a/server/src/state_ext.rs b/server/src/state_ext.rs index 83100eca1e..a15f4fdbac 100644 --- a/server/src/state_ext.rs +++ b/server/src/state_ext.rs @@ -496,6 +496,7 @@ impl StateExt for State { buff::{BuffCategory, BuffData, BuffKind, BuffSource}, }; let time = self.get_time(); + // TODO: Consider using the area system for this self.ecs_mut() .create_entity_synced() .with(pos) diff --git a/server/src/sys/msg/in_game.rs b/server/src/sys/msg/in_game.rs index e87d919429..a1e3d4bdf1 100644 --- a/server/src/sys/msg/in_game.rs +++ b/server/src/sys/msg/in_game.rs @@ -16,7 +16,7 @@ use common::{ }; use common_ecs::{Job, Origin, Phase, System}; use common_net::msg::{ClientGeneral, ServerGeneral}; -use common_state::{BlockChange, BuildAreas}; +use common_state::{AreasContainer, BlockChange, BuildArea}; use core::mem; use rayon::prelude::*; use specs::{Entities, Join, Read, ReadExpect, ReadStorage, Write, WriteStorage}; @@ -62,7 +62,7 @@ impl Sys { orientation: Option<&mut Ori>, controller: Option<&mut Controller>, settings: &Read<'_, Settings>, - build_areas: &Read<'_, BuildAreas>, + build_areas: &Read<'_, AreasContainer>, player_physics_setting: Option<&mut PlayerPhysicsSetting>, maybe_admin: &Option<&Admin>, time_for_vd_changes: Instant, @@ -335,7 +335,7 @@ impl<'a> System<'a> for Sys { WriteStorage<'a, Client>, WriteStorage<'a, Controller>, Read<'a, Settings>, - Read<'a, BuildAreas>, + Read<'a, AreasContainer>, Write<'a, PlayerPhysicsSettings>, TerrainPersistenceData<'a>, ReadStorage<'a, Player>,