diff --git a/assets/voxygen/i18n/en/command.ftl b/assets/voxygen/i18n/en/command.ftl index 6431efb754..6168b95f36 100644 --- a/assets/voxygen/i18n/en/command.ftl +++ b/assets/voxygen/i18n/en/command.ftl @@ -88,6 +88,9 @@ command-transform-invalid-presence = Cannot transform in the current presence command-aura-invalid-buff-parameters = Invalid buff parameters for aura command-aura-spawn = Spawned new aura attached to entity command-aura-spawn-new-entity = Spawned new aura +command-reloaded-chunks = Reloaded { $reloaded } chunks +command-server-no-experimental-terrain-persistence = Server was compiled without terrain persistence enabled +command-experimental-terrain-persistence-disabled = Experimental terrain persistence is disabled # Unreachable/untestable but added for consistency diff --git a/common/src/cmd.rs b/common/src/cmd.rs index a58445f955..ea56970e7b 100644 --- a/common/src/cmd.rs +++ b/common/src/cmd.rs @@ -325,6 +325,7 @@ pub enum ServerChatCommand { Buff, Build, Campfire, + ClearPersistedTerrain, CreateLocation, DebugColumn, DebugWays, @@ -532,6 +533,11 @@ impl ServerChatCommand { Some(Admin), ), ServerChatCommand::Campfire => cmd(vec![], "Spawns a campfire", Some(Admin)), + ServerChatCommand::ClearPersistedTerrain => cmd( + vec![Integer("chunk_radius", 6, Required)], + "Clears nearby persisted terrain", + Some(Admin), + ), ServerChatCommand::DebugColumn => cmd( vec![Integer("x", 15000, Required), Integer("y", 15000, Required)], "Prints some debug information about a column", @@ -632,9 +638,11 @@ impl ServerChatCommand { Some(Moderator), ), ServerChatCommand::Kill => cmd(vec![], "Kill yourself", None), - ServerChatCommand::KillNpcs => { - cmd(vec![Flag("--also-pets")], "Kill the NPCs", Some(Admin)) - }, + ServerChatCommand::KillNpcs => cmd( + vec![Float("radius", 100.0, Optional), Flag("--also-pets")], + "Kill the NPCs", + Some(Admin), + ), ServerChatCommand::Kit => cmd( vec![Enum("kit_name", KITS.to_vec(), Required)], "Place a set of items into your inventory.", @@ -715,8 +723,8 @@ impl ServerChatCommand { Some(Admin), ), ServerChatCommand::ReloadChunks => cmd( - vec![], - "Reloads all chunks loaded on the server", + vec![Integer("chunk_radius", 6, Optional)], + "Reloads chunks loaded on the server", Some(Admin), ), ServerChatCommand::RemoveLights => cmd( @@ -980,6 +988,7 @@ impl ServerChatCommand { ServerChatCommand::Buff => "buff", ServerChatCommand::Build => "build", ServerChatCommand::Campfire => "campfire", + ServerChatCommand::ClearPersistedTerrain => "clear_persisted_terrain", ServerChatCommand::DebugColumn => "debug_column", ServerChatCommand::DebugWays => "debug_ways", ServerChatCommand::DisconnectAllPlayers => "disconnect_all_players", diff --git a/common/state/src/state.rs b/common/state/src/state.rs index 77d668baa5..c6940d3f0a 100644 --- a/common/state/src/state.rs +++ b/common/state/src/state.rs @@ -540,12 +540,15 @@ impl State { } /// Removes every chunk of the terrain. - pub fn clear_terrain(&mut self) { + pub fn clear_terrain(&mut self) -> usize { let removed_chunks = &mut self.ecs.write_resource::().removed_chunks; - self.terrain_mut().drain().for_each(|(key, _)| { - removed_chunks.insert(key); - }); + self.terrain_mut() + .drain() + .map(|(key, _)| { + removed_chunks.insert(key); + }) + .count() } /// Insert the provided chunk into this state's terrain. @@ -570,7 +573,7 @@ impl State { /// Remove the chunk with the given key from this state's terrain, if it /// exists. - pub fn remove_chunk(&mut self, key: Vec2) { + pub fn remove_chunk(&mut self, key: Vec2) -> bool { if self .ecs .write_resource::() @@ -581,6 +584,10 @@ impl State { .write_resource::() .removed_chunks .insert(key); + + true + } else { + false } } diff --git a/server/src/cmd.rs b/server/src/cmd.rs index e2cd5c8e98..65cf2841f7 100644 --- a/server/src/cmd.rs +++ b/server/src/cmd.rs @@ -53,12 +53,13 @@ use common::{ 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, Damage, DamageKind, DamageSource, Explosion, GroupTarget, LoadoutBuilder, - RadiusEffect, + weather, CachedSpatialGrid, Damage, DamageKind, DamageSource, Explosion, GroupTarget, + LoadoutBuilder, RadiusEffect, }; use common_net::{ msg::{DisconnectReason, Notification, PlayerListUpdate, ServerGeneral}, @@ -145,6 +146,7 @@ fn do_command( 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, @@ -2010,6 +2012,57 @@ fn handle_spawn_campfire( Ok(()) } +#[cfg(feature = "persistent_world")] +fn handle_clear_persisted_terrain( + server: &mut Server, + _client: EcsEntity, + target: EcsEntity, + args: Vec, + action: &ServerChatCommand, +) -> CmdResult<()> { + let Some(radius) = parse_cmd_args!(args, i32) else { + return Err(Content::Plain(action.help_string())); + }; + // Clamp the radius to prevent accidentally passing too large radiuses + let radius = radius.clamp(0, 64); + + let pos = position(server, target, "target")?; + let chunk_key = server.state.terrain().pos_key(pos.0.as_()); + + let mut terrain_persistence2 = server + .state + .ecs() + .try_fetch_mut::(); + if let Some(ref mut terrain_persistence) = terrain_persistence2 { + for offset in Spiral2d::with_radius(radius) { + let chunk_key = chunk_key + offset; + terrain_persistence.clear_chunk(chunk_key); + } + + drop(terrain_persistence2); + reload_chunks_inner(server, pos.0, Some(radius)); + + Ok(()) + } else { + Err(Content::localized( + "command-experimental-terrain-persistence-disabled", + )) + } +} + +#[cfg(not(feature = "persistent_world"))] +fn handle_clear_persisted_terrain( + _server: &mut Server, + _client: EcsEntity, + _target: EcsEntity, + _args: Vec, + _action: &ServerChatCommand, +) -> CmdResult<()> { + Err(Content::localized( + "command-server-no-experimental-terrain-persistence", + )) +} + fn handle_safezone( server: &mut Server, client: EcsEntity, @@ -2413,16 +2466,21 @@ fn parse_alignment(owner: Uid, alignment: &str) -> CmdResult { fn handle_kill_npcs( server: &mut Server, client: EcsEntity, - _target: EcsEntity, + target: EcsEntity, args: Vec, _action: &ServerChatCommand, ) -> CmdResult<()> { - let kill_pets = if let Some(kill_option) = parse_cmd_args!(args, String) { + let (radius, options) = parse_cmd_args!(args, f32, String); + let kill_pets = if let Some(kill_option) = options { kill_option.contains("--also-pets") } else { false }; + let position = radius + .map(|_| position(server, target, "target")) + .transpose()?; + let to_kill = { let ecs = server.state.ecs(); let entities = ecs.entities(); @@ -2432,40 +2490,78 @@ fn handle_kill_npcs( let alignments = ecs.read_storage::(); let rtsim_entities = ecs.read_storage::(); let mut rtsim = ecs.write_resource::(); + let spatial_grid; - ( - &entities, - &healths, - !&players, - alignments.maybe(), - &positions, - ) - .join() - .filter_map(|(entity, _health, (), alignment, pos)| { - let should_kill = kill_pets - || if let Some(Alignment::Owned(owned)) = alignment { - ecs.entity_from_uid(*owned) - .map_or(true, |owner| !players.contains(owner)) - } else { - true - }; + let mut iter_a; + let mut iter_b; - if should_kill { - if let Some(rtsim_entity) = rtsim_entities.get(entity).copied() { - rtsim.hook_rtsim_actor_death( - &ecs.read_resource::>(), - ecs.read_resource::().as_index_ref(), - Actor::Npc(rtsim_entity.0), - Some(pos.0), - None, - ); - } - Some(entity) + let iter: &mut dyn Iterator< + Item = ( + EcsEntity, + &comp::Health, + (), + Option<&comp::Alignment>, + &comp::Pos, + ), + > = if let (Some(radius), Some(position)) = (radius, position) { + spatial_grid = ecs.read_resource::(); + iter_a = spatial_grid + .0 + .in_circle_aabr(position.0.xy(), radius) + .filter_map(|entity| { + ( + &entities, + &healths, + !&players, + alignments.maybe(), + &positions, + ) + .lend_join() + .get(entity, &entities) + }) + .filter(move |(_, _, _, _, pos)| { + pos.0.distance_squared(position.0) <= radius.powi(2) + }); + + &mut iter_a as _ + } else { + iter_b = ( + &entities, + &healths, + !&players, + alignments.maybe(), + &positions, + ) + .join(); + + &mut iter_b as _ + }; + + iter.filter_map(|(entity, _health, (), alignment, pos)| { + let should_kill = kill_pets + || if let Some(Alignment::Owned(owned)) = alignment { + ecs.entity_from_uid(*owned) + .map_or(true, |owner| !players.contains(owner)) } else { - None + true + }; + + if should_kill { + if let Some(rtsim_entity) = rtsim_entities.get(entity).copied() { + rtsim.hook_rtsim_actor_death( + &ecs.read_resource::>(), + ecs.read_resource::().as_index_ref(), + Actor::Npc(rtsim_entity.0), + Some(pos.0), + None, + ); } - }) - .collect::>() + Some(entity) + } else { + None + } + }) + .collect::>() }; let count = to_kill.len(); for entity in to_kill { @@ -3605,20 +3701,60 @@ fn parse_skill_tree(skill_tree: &str) -> CmdResult, radius: Option) -> usize { + let mut removed = 0; + + if let Some(radius) = radius { + let chunk_key = server.state.terrain().pos_key(pos.as_()); + + for key_offset in Spiral2d::with_radius(radius) { + let chunk_key = chunk_key + key_offset; + + #[cfg(feature = "persistent_world")] + server + .state + .ecs() + .try_fetch_mut::() + .map(|mut terrain_persistence| terrain_persistence.unload_chunk(chunk_key)); + if server.state.remove_chunk(chunk_key) { + removed += 1; + } + } + } else { + #[cfg(feature = "persistent_world")] + server + .state + .ecs() + .try_fetch_mut::() + .map(|mut terrain_persistence| terrain_persistence.unload_all()); + removed = server.state.clear_terrain(); + } + + removed +} + fn handle_reload_chunks( server: &mut Server, - _client: EcsEntity, - _target: EcsEntity, - _args: Vec, + client: EcsEntity, + target: EcsEntity, + args: Vec, _action: &ServerChatCommand, ) -> CmdResult<()> { - #[cfg(feature = "persistent_world")] - server - .state - .ecs() - .try_fetch_mut::() - .map(|mut terrain_persistence| terrain_persistence.unload_all()); - server.state.clear_terrain(); + let radius = parse_cmd_args!(args, i32); + + let pos = position(server, target, "target")?.0; + let removed = reload_chunks_inner(server, pos, radius.map(|radius| radius.clamp(0, 64))); + + server.notify_client( + client, + ServerGeneral::server_msg( + ChatType::CommandInfo, + Content::localized_with_args("command-reloaded-chunks", [( + "reloaded", + removed.to_string(), + )]), + ), + ); Ok(()) } diff --git a/server/src/events/entity_manipulation.rs b/server/src/events/entity_manipulation.rs index cd8d3b7908..5e86a43c92 100644 --- a/server/src/events/entity_manipulation.rs +++ b/server/src/events/entity_manipulation.rs @@ -2267,6 +2267,7 @@ pub fn transform_entity( set_or_remove_component(server, entity, Some(skill_set))?; set_or_remove_component(server, entity, Some(poise))?; set_or_remove_component(server, entity, health)?; + set_or_remove_component(server, entity, Some(comp::Energy::new(body)))?; set_or_remove_component(server, entity, Some(body))?; set_or_remove_component(server, entity, Some(body.mass()))?; set_or_remove_component(server, entity, Some(body.density()))?; diff --git a/server/src/terrain_persistence.rs b/server/src/terrain_persistence.rs index 25414ccf59..66c240cab6 100644 --- a/server/src/terrain_persistence.rs +++ b/server/src/terrain_persistence.rs @@ -15,7 +15,7 @@ use std::{ use tracing::{debug, error, info, warn}; use vek::*; -const MAX_BLOCK_CACHE: usize = 5_000_000; +const MAX_BLOCK_CACHE: usize = 64_000_000; pub struct TerrainPersistence { path: PathBuf, @@ -157,11 +157,8 @@ impl TerrainPersistence { pub fn unload_chunk(&mut self, key: Vec2) { if let Some(LoadedChunk { chunk, modified }) = self.chunks.remove(&key) { - match (self.cached_chunks.peek(&key), modified) { - (Some(_), false) => {}, - _ => { - self.cached_chunks.insert(key, chunk.clone()); - }, + if modified || self.cached_chunks.peek(&key).is_none() { + self.cached_chunks.insert(key, chunk.clone()); } // Prevent any uneccesarry IO when nothing in this chunk has changed @@ -169,22 +166,40 @@ impl TerrainPersistence { return; } - let bytes = match bincode::serialize::(&chunk.prepare_raw()) { - Err(err) => { - error!("Failed to serialize chunk data: {:?}", err); - return; - }, - Ok(bytes) => bytes, - }; + if chunk.blocks.is_empty() { + let path = self.path_for(key); - let atomic_file = - AtomicFile::new(self.path_for(key), OverwriteBehavior::AllowOverwrite); - if let Err(err) = atomic_file.write(|file| file.write_all(&bytes)) { - error!("Failed to write chunk data to file: {:?}", err); + if path.is_file() { + if let Err(error) = std::fs::remove_file(&path) { + error!(?error, ?path, "Failed to remove file for empty chunk"); + } + } + } else { + let bytes = match bincode::serialize::(&chunk.prepare_raw()) { + Err(err) => { + error!("Failed to serialize chunk data: {:?}", err); + return; + }, + Ok(bytes) => bytes, + }; + + let atomic_file = + AtomicFile::new(self.path_for(key), OverwriteBehavior::AllowOverwrite); + if let Err(err) = atomic_file.write(|file| file.write_all(&bytes)) { + error!("Failed to write chunk data to file: {:?}", err); + } } } } + pub fn clear_chunk(&mut self, chunk: Vec2) { + self.cached_chunks.remove(&chunk); + self.chunks.insert(chunk, LoadedChunk { + chunk: Chunk::default(), + modified: true, + }); + } + pub fn unload_all(&mut self) { for key in self.chunks.keys().copied().collect::>() { self.unload_chunk(key); @@ -202,10 +217,6 @@ impl TerrainPersistence { .insert(pos - key * TerrainChunk::RECT_SIZE.map(|e| e as i32), block); if old_block != Some(block) { loaded_chunk.modified = true; - - if old_block.is_none() { - self.cached_chunks.limiter_mut().add_block(); - } } } } @@ -237,9 +248,6 @@ impl Chunk { } /// LRU limiter that limits by the number of blocks -/// -/// > **Warning**: Make sure to call [`add_block`] and [`remove_block`] when -/// > performing direct mutations to a chunk struct ByBlockLimiter { /// Maximum number of blocks that can be contained block_limit: usize, @@ -251,7 +259,7 @@ impl Limiter, Chunk> for ByBlockLimiter { type KeyToInsert<'a> = Vec2; type LinkType = u32; - fn is_over_the_limit(&self, _length: usize) -> bool { false } + fn is_over_the_limit(&self, _length: usize) -> bool { self.counted_blocks > self.block_limit } fn on_insert( &mut self, @@ -279,7 +287,9 @@ impl Limiter, Chunk> for ByBlockLimiter { ) -> bool { let old_size = old_chunk.len() as isize; // I assume chunks are never larger than a few thousand blocks anyways, cast should be OK let new_size = new_chunk.len() as isize; - let new_total = self.counted_blocks.wrapping_add_signed(new_size - old_size); + let new_total = self + .counted_blocks + .saturating_add_signed(new_size - old_size); if new_total > self.block_limit { false @@ -306,10 +316,6 @@ impl ByBlockLimiter { counted_blocks: 0, } } - - /// This function should only be used when it is guaranteed that a block has - /// been added - fn add_block(&mut self) { self.counted_blocks += 1; } } /// # Adding a new chunk format version