diff --git a/CHANGELOG.md b/CHANGELOG.md index 36a1ad083b..649cd0c3f9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,9 +14,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - You can now jump out of rolls for a slight jump boost - Dungeons now have multiple kinds of stairs. - Trades now display item prices in tooltips. +- Admin designated build areas ### Changed +- Permission to build is no longer tied to being an admin + ### Removed ### Fixed diff --git a/common/src/cmd.rs b/common/src/cmd.rs index e53c82609e..a1692dbabf 100644 --- a/common/src/cmd.rs +++ b/common/src/cmd.rs @@ -40,6 +40,9 @@ pub enum ChatCommand { Alias, Ban, Build, + BuildAreaAdd, + BuildAreaList, + BuildAreaRemove, Campfire, Debug, DebugColumn, @@ -68,9 +71,12 @@ pub enum ChatCommand { MakeSprite, Motd, Object, + PermitBuild, Players, Region, RemoveLights, + RevokeBuild, + RevokeBuildAll, Safezone, Say, SetMotd, @@ -94,6 +100,9 @@ pub static CHAT_COMMANDS: &[ChatCommand] = &[ ChatCommand::Alias, ChatCommand::Ban, ChatCommand::Build, + ChatCommand::BuildAreaAdd, + ChatCommand::BuildAreaList, + ChatCommand::BuildAreaRemove, ChatCommand::Campfire, ChatCommand::Debug, ChatCommand::DebugColumn, @@ -122,9 +131,12 @@ pub static CHAT_COMMANDS: &[ChatCommand] = &[ ChatCommand::MakeSprite, ChatCommand::Motd, ChatCommand::Object, + ChatCommand::PermitBuild, ChatCommand::Players, ChatCommand::Region, ChatCommand::RemoveLights, + ChatCommand::RevokeBuild, + ChatCommand::RevokeBuildAll, ChatCommand::Safezone, ChatCommand::Say, ChatCommand::SetMotd, @@ -235,7 +247,26 @@ impl ChatCommand { "Ban a player with a given username", Admin, ), - ChatCommand::Build => cmd(vec![], "Toggles build mode on and off", Admin), + ChatCommand::Build => cmd(vec![], "Toggles build mode on and off", NoAdmin), + ChatCommand::BuildAreaAdd => cmd( + vec![ + Any("name", Required), + Integer("xlo", 0, Required), + Integer("xhi", 10, Required), + Integer("ylo", 0, Required), + Integer("yhi", 10, Required), + Integer("zlo", 0, Required), + Integer("zhi", 10, Required), + ], + "Adds a new build area", + Admin, + ), + ChatCommand::BuildAreaList => cmd(vec![], "List all build areas", Admin), + ChatCommand::BuildAreaRemove => cmd( + vec![Any("name", Required)], + "Removes specified build area", + Admin, + ), ChatCommand::Campfire => cmd(vec![], "Spawns a campfire", Admin), ChatCommand::Debug => cmd(vec![], "Place all debug items into your pack.", Admin), ChatCommand::DebugColumn => cmd( @@ -368,12 +399,27 @@ impl ChatCommand { "Spawn an object", Admin, ), + ChatCommand::PermitBuild => cmd( + vec![Any("area_name", Required)], + "Grants player a bounded box they can build in", + Admin, + ), ChatCommand::Players => cmd(vec![], "Lists players currently online", NoAdmin), ChatCommand::RemoveLights => cmd( vec![Float("radius", 20.0, Optional)], "Removes all lights spawned by players", Admin, ), + ChatCommand::RevokeBuild => cmd( + vec![Any("area_name", Required)], + "Revokes build area permission for player", + Admin, + ), + ChatCommand::RevokeBuildAll => cmd( + vec![], + "Revokes all build area permissions for player", + Admin, + ), ChatCommand::Region => cmd( vec![Message(Optional)], "Send messages to everyone in your region of the world", @@ -460,6 +506,9 @@ impl ChatCommand { ChatCommand::Alias => "alias", ChatCommand::Ban => "ban", ChatCommand::Build => "build", + ChatCommand::BuildAreaAdd => "build_area_add", + ChatCommand::BuildAreaList => "build_area_list", + ChatCommand::BuildAreaRemove => "build_area_remove", ChatCommand::Campfire => "campfire", ChatCommand::Debug => "debug", ChatCommand::DebugColumn => "debug_column", @@ -488,9 +537,12 @@ impl ChatCommand { ChatCommand::MakeSprite => "make_sprite", ChatCommand::Motd => "motd", ChatCommand::Object => "object", + ChatCommand::PermitBuild => "permit_build", ChatCommand::Players => "players", ChatCommand::Region => "region", ChatCommand::RemoveLights => "remove_lights", + ChatCommand::RevokeBuild => "revoke_build", + ChatCommand::RevokeBuildAll => "revoke_build_all", ChatCommand::Safezone => "safezone", ChatCommand::Say => "say", ChatCommand::SetMotd => "set_motd", diff --git a/common/src/comp/inputs.rs b/common/src/comp/inputs.rs index 89e2b0b8b2..fddbe7d4e2 100644 --- a/common/src/comp/inputs.rs +++ b/common/src/comp/inputs.rs @@ -1,8 +1,15 @@ +use crate::depot::Id; use serde::{Deserialize, Serialize}; -use specs::{Component, DerefFlaggedStorage, NullStorage}; +use specs::{Component, DerefFlaggedStorage}; +use specs_idvs::IdvStorage; +use std::collections::HashSet; +use vek::geom::Aabb; #[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)] -pub struct CanBuild; -impl Component for CanBuild { - type Storage = DerefFlaggedStorage>; +pub struct CanBuild { + pub enabled: bool, + pub build_areas: HashSet>>, +} +impl Component for CanBuild { + type Storage = DerefFlaggedStorage>; } diff --git a/common/src/depot.rs b/common/src/depot.rs index be828aa665..8ad8cd1707 100644 --- a/common/src/depot.rs +++ b/common/src/depot.rs @@ -1,3 +1,4 @@ +use serde::{Deserialize, Serialize}; use std::{ cmp::{Eq, Ord, Ordering, PartialEq, PartialOrd}, fmt, hash, @@ -6,6 +7,7 @@ use std::{ }; /// Type safe index into Depot +#[derive(Deserialize, Serialize)] pub struct Id { idx: u32, gen: u32, diff --git a/common/sys/src/state.rs b/common/sys/src/state.rs index 52c4bc356a..5f09492481 100644 --- a/common/sys/src/state.rs +++ b/common/sys/src/state.rs @@ -6,6 +6,7 @@ use crate::plugin::PluginMgr; use common::uid::UidAllocator; use common::{ comp, + depot::{Depot, Id}, event::{EventBus, LocalEvent, ServerEvent}, region::RegionMap, resources::{DeltaTime, GameMode, PlayerEntity, Time, TimeOfDay}, @@ -39,6 +40,21 @@ const DAY_CYCLE_FACTOR: f64 = 24.0 * 2.0; /// avoid such a situation. const MAX_DELTA_TIME: f32 = 1.0; +#[derive(Default)] +pub struct BuildAreas { + pub areas: Depot>, + pub area_names: HashMap>>, +} + +impl BuildAreas { + pub fn new() -> Self { + Self { + areas: Depot::default(), + area_names: HashMap::new(), + } + } +} + #[derive(Default)] pub struct BlockChange { blocks: HashMap, Block>, @@ -204,6 +220,7 @@ impl State { ecs.insert(PlayerEntity(None)); ecs.insert(TerrainGrid::new().unwrap()); ecs.insert(BlockChange::default()); + ecs.insert(BuildAreas::new()); 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 c74c4005fb..90796fce26 100644 --- a/server/src/cmd.rs +++ b/server/src/cmd.rs @@ -30,9 +30,15 @@ use common_net::{ msg::{DisconnectReason, Notification, PlayerListUpdate, ServerGeneral}, sync::WorldSyncExt, }; +use common_sys::state::BuildAreas; use rand::Rng; use specs::{Builder, Entity as EcsEntity, Join, WorldExt}; -use std::{convert::TryFrom, time::Duration}; +use std::{ + collections::HashSet, + convert::TryFrom, + ops::{Deref, DerefMut}, + time::Duration, +}; use vek::*; use world::util::Sampler; @@ -81,6 +87,9 @@ fn get_handler(cmd: &ChatCommand) -> CommandHandler { ChatCommand::Alias => handle_alias, ChatCommand::Ban => handle_ban, ChatCommand::Build => handle_build, + ChatCommand::BuildAreaAdd => handle_build_area_add, + ChatCommand::BuildAreaList => handle_build_area_list, + ChatCommand::BuildAreaRemove => handle_build_area_remove, ChatCommand::Campfire => handle_spawn_campfire, ChatCommand::Debug => handle_debug, ChatCommand::DebugColumn => handle_debug_column, @@ -109,9 +118,12 @@ fn get_handler(cmd: &ChatCommand) -> CommandHandler { ChatCommand::MakeSprite => handle_make_sprite, ChatCommand::Motd => handle_motd, ChatCommand::Object => handle_object, + ChatCommand::PermitBuild => handle_permit_build, ChatCommand::Players => handle_players, ChatCommand::Region => handle_region, ChatCommand::RemoveLights => handle_remove_lights, + ChatCommand::RevokeBuild => handle_revoke_build, + ChatCommand::RevokeBuildAll => handle_revoke_build_all, ChatCommand::Safezone => handle_safezone, ChatCommand::Say => handle_say, ChatCommand::SetMotd => handle_set_motd, @@ -1116,6 +1128,105 @@ fn handle_safezone( } } +fn handle_permit_build( + server: &mut Server, + client: EcsEntity, + target: EcsEntity, + args: String, + action: &ChatCommand, +) { + if let Some(area_name) = scan_fmt_some!(&args, &action.arg_fmt(), String) { + let ecs = server.state.ecs(); + if server + .state + .read_storage::() + .get(target) + .is_none() + { + let _ = ecs + .write_storage::() + .insert(target, comp::CanBuild { + enabled: false, + build_areas: HashSet::new(), + }); + } + if let Some(bb_id) = ecs + .read_resource::() + .deref() + .area_names + .get(&area_name) + { + if let Some(mut comp_can_build) = ecs.write_storage::().get_mut(target) + { + comp_can_build.build_areas.insert(*bb_id); + server.notify_client( + client, + ServerGeneral::server_msg( + ChatType::CommandInfo, + format!("Permission to build in {} granted", area_name), + ), + ); + } + } + } else { + server.notify_client( + client, + ServerGeneral::server_msg(ChatType::CommandError, action.help_string()), + ); + } +} + +fn handle_revoke_build( + server: &mut Server, + client: EcsEntity, + target: EcsEntity, + args: String, + action: &ChatCommand, +) { + if let Some(area_name) = scan_fmt_some!(&args, &action.arg_fmt(), String) { + let ecs = server.state.ecs(); + if let Some(bb_id) = ecs + .read_resource::() + .deref() + .area_names + .get(&area_name) + { + if let Some(mut comp_can_build) = ecs.write_storage::().get_mut(target) + { + comp_can_build.build_areas.retain(|&x| x != *bb_id); + server.notify_client( + client, + ServerGeneral::server_msg( + ChatType::CommandInfo, + format!("Permission to build in {} revoked", area_name), + ), + ); + } + } + } else { + server.notify_client( + client, + ServerGeneral::server_msg(ChatType::CommandError, action.help_string()), + ); + } +} + +fn handle_revoke_build_all( + server: &mut Server, + client: EcsEntity, + target: EcsEntity, + _args: String, + _action: &ChatCommand, +) { + let ecs = server.state.ecs(); + + ecs.write_storage::().remove(target); + server.notify_client( + client, + ServerGeneral::server_msg(ChatType::CommandInfo, "All build permissions revoked"), + ); +} + fn handle_players( server: &mut Server, client: EcsEntity, @@ -1150,30 +1261,152 @@ fn handle_build( _args: String, _action: &ChatCommand, ) { - if server + if let Some(mut can_build) = server .state - .read_storage::() - .get(target) - .is_some() + .ecs() + .write_storage::() + .get_mut(target) { - server - .state - .ecs() - .write_storage::() - .remove(target); - server.notify_client( - client, - ServerGeneral::server_msg(ChatType::CommandInfo, "Toggled off build mode!"), - ); + if can_build.enabled { + can_build.enabled = false; + server.notify_client( + client, + ServerGeneral::server_msg(ChatType::CommandInfo, "Toggled off build mode!"), + ); + } else { + can_build.enabled = true; + server.notify_client( + client, + ServerGeneral::server_msg(ChatType::CommandInfo, "Toggled on build mode!"), + ); + } } else { - let _ = server - .state - .ecs() - .write_storage::() - .insert(target, comp::CanBuild); server.notify_client( client, - ServerGeneral::server_msg(ChatType::CommandInfo, "Toggled on build mode!"), + ServerGeneral::server_msg( + ChatType::CommandInfo, + "You do not have permission to build.", + ), + ); + } +} + +fn handle_build_area_add( + server: &mut Server, + client: EcsEntity, + _target: EcsEntity, + args: String, + action: &ChatCommand, +) { + if let (Some(area_name), Some(xlo), Some(xhi), Some(ylo), Some(yhi), Some(zlo), Some(zhi)) = scan_fmt_some!( + &args, + &action.arg_fmt(), + String, + i32, + i32, + i32, + i32, + i32, + i32 + ) { + let ecs = server.state.ecs(); + if ecs + .read_resource::() + .deref() + .area_names + .contains_key(&area_name) + { + server.notify_client( + client, + ServerGeneral::server_msg( + ChatType::CommandError, + format!("Build zone {} already exists!", area_name), + ), + ); + return; + } + let bb_id = ecs.write_resource::().deref_mut().areas.insert( + Aabb { + min: Vec3::new(xlo, ylo, zlo), + max: Vec3::new(xhi, yhi, zhi), + } + .made_valid(), + ); + ecs.write_resource::() + .deref_mut() + .area_names + .insert(area_name.clone(), bb_id); + server.notify_client( + client, + ServerGeneral::server_msg( + ChatType::CommandInfo, + format!("Created build zone {}", area_name), + ), + ); + } +} + +fn handle_build_area_list( + server: &mut Server, + client: EcsEntity, + _target: EcsEntity, + _args: String, + _action: &ChatCommand, +) { + let ecs = server.state.ecs(); + let build_areas = ecs.read_resource::(); + + server.notify_client( + client, + 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) + } else { + acc + } + }, + ), + ), + ); +} + +fn handle_build_area_remove( + server: &mut Server, + client: EcsEntity, + _target: EcsEntity, + args: String, + action: &ChatCommand, +) { + if let Some(area_name) = scan_fmt_some!(&args, &action.arg_fmt(), String) { + let ecs = server.state.ecs(); + let mut build_areas = ecs.write_resource::(); + + let bb_id = match build_areas.area_names.get(&area_name) { + Some(x) => *x, + None => { + server.notify_client( + client, + ServerGeneral::server_msg( + ChatType::CommandError, + format!("No such build area '{}'", area_name), + ), + ); + return; + }, + }; + + let _ = build_areas.area_names.remove(&area_name); + let _ = build_areas.areas.remove(bb_id); + server.notify_client( + client, + ServerGeneral::server_msg( + ChatType::CommandInfo, + format!("Removed build zone {}", area_name), + ), ); } } diff --git a/server/src/sys/msg/in_game.rs b/server/src/sys/msg/in_game.rs index bcd2f3b90c..ff8fdc3b69 100644 --- a/server/src/sys/msg/in_game.rs +++ b/server/src/sys/msg/in_game.rs @@ -7,7 +7,7 @@ use common::{ }; use common_ecs::{Job, Origin, Phase, System}; use common_net::msg::{ClientGeneral, PresenceKind, ServerGeneral}; -use common_sys::state::BlockChange; +use common_sys::state::{BlockChange, BuildAreas}; use specs::{Entities, Join, Read, ReadExpect, ReadStorage, Write, WriteStorage}; use tracing::{debug, trace}; @@ -29,6 +29,7 @@ impl Sys { orientations: &mut WriteStorage<'_, Ori>, controllers: &mut WriteStorage<'_, Controller>, settings: &Read<'_, Settings>, + build_areas: &Read<'_, BuildAreas>, msg: ClientGeneral, ) -> Result<(), crate::error::Error> { let presence = match maybe_presence { @@ -102,13 +103,39 @@ impl Sys { } }, ClientGeneral::BreakBlock(pos) => { - if let Some(block) = can_build.get(entity).and_then(|_| terrain.get(pos).ok()) { - block_changes.set(pos, block.into_vacant()); + if let Some(comp_can_build) = can_build.get(entity) { + if comp_can_build.enabled { + for area in comp_can_build.build_areas.iter() { + if let Some(block) = build_areas + .areas + .get(*area) + // TODO: Make this an exclusive check on the upper bound of the AABB + // Vek defaults to inclusive which is not optimal + .filter(|aabb| aabb.contains_point(pos)) + .and_then(|_| terrain.get(pos).ok()) + { + block_changes.set(pos, block.into_vacant()); + } + } + } } }, ClientGeneral::PlaceBlock(pos, block) => { - if can_build.get(entity).is_some() { - block_changes.try_set(pos, block); + if let Some(comp_can_build) = can_build.get(entity) { + if comp_can_build.enabled { + for area in comp_can_build.build_areas.iter() { + if build_areas + .areas + .get(*area) + // TODO: Make this an exclusive check on the upper bound of the AABB + // Vek defaults to inclusive which is not optimal + .filter(|aabb| aabb.contains_point(pos)) + .is_some() + { + block_changes.try_set(pos, block); + } + } + } } }, ClientGeneral::UnlockSkill(skill) => { @@ -156,6 +183,7 @@ impl<'a> System<'a> for Sys { WriteStorage<'a, Client>, WriteStorage<'a, Controller>, Read<'a, Settings>, + Read<'a, BuildAreas>, ); const NAME: &'static str = "msg::in_game"; @@ -180,6 +208,7 @@ impl<'a> System<'a> for Sys { mut clients, mut controllers, settings, + build_areas, ): Self::SystemData, ) { let mut server_emitter = server_event_bus.emitter(); @@ -204,6 +233,7 @@ impl<'a> System<'a> for Sys { &mut orientations, &mut controllers, &settings, + &build_areas, msg, ) }); diff --git a/voxygen/src/session.rs b/voxygen/src/session.rs index 9c403a42e0..a7c04828b3 100644 --- a/voxygen/src/session.rs +++ b/voxygen/src/session.rs @@ -317,7 +317,7 @@ impl PlayState for SessionState { .state() .read_storage::() .get(player_entity) - .is_some(); + .map_or_else(|| false, |cb| cb.enabled); let is_mining = self .client