From 738e59965fc9e8f2dba07d557959adca8cb0a43a Mon Sep 17 00:00:00 2001 From: Joshua Barretto Date: Fri, 23 Jul 2021 13:04:16 +0100 Subject: [PATCH 1/2] Added experimental terrain persistence --- CHANGELOG.md | 1 + common/src/cmd.rs | 9 +- common/src/terrain/block.rs | 31 +++ server/Cargo.toml | 1 + server/src/cmd.rs | 46 +++- server/src/lib.rs | 26 +++ server/src/settings.rs | 6 + server/src/sys/msg/in_game.rs | 27 ++- server/src/sys/terrain.rs | 16 +- server/src/terrain_persistence.rs | 344 ++++++++++++++++++++++++++++++ 10 files changed, 488 insertions(+), 19 deletions(-) create mode 100644 server/src/terrain_persistence.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index fa75ef11dd..5aeb9b12dd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -29,6 +29,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Custom error message when a supported graphics backend can not be found - Add server setting with PvE/PvP switch - Can now tilt glider while only wielding it +- Experimental terrain persistence (see server documentation) ### Changed diff --git a/common/src/cmd.rs b/common/src/cmd.rs index 5d6eadf393..207940c007 100644 --- a/common/src/cmd.rs +++ b/common/src/cmd.rs @@ -452,8 +452,13 @@ impl ChatCommand { Some(Admin), ), ChatCommand::MakeBlock => cmd( - vec![Enum("block", BLOCK_KINDS.clone(), Required)], - "Make a block at your location", + vec![ + Enum("block", BLOCK_KINDS.clone(), Required), + Integer("r", 255, Optional), + Integer("g", 255, Optional), + Integer("b", 255, Optional), + ], + "Make a block at your location with a color", Some(Admin), ), ChatCommand::MakeSprite => cmd( diff --git a/common/src/terrain/block.rs b/common/src/terrain/block.rs index c2822b31b5..f1a946c748 100644 --- a/common/src/terrain/block.rs +++ b/common/src/terrain/block.rs @@ -309,15 +309,46 @@ impl Block { Block::air(SpriteKind::Empty) } } + + /// Attempt to convert a [`u32`] to a block + #[inline] + pub fn from_u32(x: u32) -> Option { + let [bk, r, g, b] = x.to_le_bytes(); + Some(Self { + kind: BlockKind::from_u8(bk)?, + attr: [r, g, b], + }) + } + + #[inline] + pub fn to_u32(&self) -> u32 { + u32::from_le_bytes([self.kind as u8, self.attr[0], self.attr[1], self.attr[2]]) + } } #[cfg(test)] mod tests { use super::*; + use strum::IntoEnumIterator; #[test] fn block_size() { assert_eq!(std::mem::size_of::(), 1); assert_eq!(std::mem::size_of::(), 4); } + + #[test] + fn convert_u32() { + for bk in BlockKind::iter() { + let block = Block::new(bk, Rgb::new(165, 90, 204)); // Pretty unique bit patterns + if bk.is_filled() { + assert_eq!(Block::from_u32(block.to_u32()), Some(block)); + } else { + assert_eq!( + Block::from_u32(block.to_u32()), + Some(Block::new(bk, Rgb::zero())), + ); + } + } + } } diff --git a/server/Cargo.toml b/server/Cargo.toml index d43a6a0aee..e3d71259c7 100644 --- a/server/Cargo.toml +++ b/server/Cargo.toml @@ -26,6 +26,7 @@ network = { package = "veloren-network", path = "../network", features = ["metri specs = { git = "https://github.com/amethyst/specs.git", features = ["shred-derive"], rev = "f985bec5d456f7b0dd8aae99848f9473c2cd9d46" } specs-idvs = { git = "https://gitlab.com/veloren/specs-idvs.git", rev = "8be2abcddf8f524cb5876e8dd20a7e47cfaf7573" } +bincode = "1.3.2" num_cpus = "1.0" tracing = "0.1" vek = { version = "0.14.1", features = ["serde"] } diff --git a/server/src/cmd.rs b/server/src/cmd.rs index 643b6df4d1..f706b68a0f 100644 --- a/server/src/cmd.rs +++ b/server/src/cmd.rs @@ -523,13 +523,20 @@ fn handle_make_block( args: Vec, action: &ChatCommand, ) -> CmdResult<()> { - if let Some(block_name) = parse_args!(args, String) { + if let (Some(block_name), r, g, b) = parse_args!(args, String, u8, u8, u8) { if let Ok(bk) = BlockKind::from_str(block_name.as_str()) { let pos = position(server, target, "target")?; - server.state.set_block( - pos.0.map(|e| e.floor() as i32), - Block::new(bk, Rgb::broadcast(255)), - ); + 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); + 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)) @@ -557,6 +564,14 @@ fn handle_make_sprite( .unwrap_or_else(|| Block::air(SpriteKind::Empty)) .with_sprite(sk); server.state.set_block(pos, new_block); + 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)) @@ -1392,12 +1407,23 @@ fn handle_build( .write_storage::() .get_mut(target) { - let toggle_string = if can_build.enabled { "off" } else { "on" }; - let chat_msg = ServerGeneral::server_msg( - ChatType::CommandInfo, - format!("Toggled {:?} build mode!", toggle_string), - ); can_build.enabled ^= true; + + let toggle_string = if can_build.enabled { "on" } else { "off" }; + let msg = format!( + "Toggled build mode {}.{}", + toggle_string, + if !can_build.enabled { + "" + } else if server.settings().experimental_terrain_persistence { + " Experimental terrain persistence is enabled. The server will attempt to persist \ + changes, but this is not guaranteed." + } else { + " Changes will not be persisted when a chunk unloads." + }, + ); + + let chat_msg = ServerGeneral::server_msg(ChatType::CommandInfo, msg); if client != target { server.notify_client(target, chat_msg.clone()); } diff --git a/server/src/lib.rs b/server/src/lib.rs index 33d2c7fffc..c7dca932c8 100644 --- a/server/src/lib.rs +++ b/server/src/lib.rs @@ -31,6 +31,7 @@ pub mod rtsim; pub mod settings; pub mod state_ext; pub mod sys; +pub mod terrain_persistence; #[cfg(not(feature = "worldgen"))] mod test_world; pub mod wiring; @@ -55,6 +56,7 @@ use crate::{ rtsim::RtSim, state_ext::StateExt, sys::sentinel::{DeletedEntities, TrackedComps}, + terrain_persistence::TerrainPersistence, }; #[cfg(not(feature = "worldgen"))] use common::grid::Grid; @@ -216,6 +218,17 @@ impl Server { state.ecs_mut().insert(ecs_system_metrics); state.ecs_mut().insert(tick_metrics); state.ecs_mut().insert(physics_metrics); + if settings.experimental_terrain_persistence { + warn!( + "Experimental terrain persistence support is enabled. This feature may break, be \ + disabled, or otherwise change under your feet at *any time*. Additionally, it is \ + expected to be replaced in the future *without* migration or warning. You have \ + been warned." + ); + state + .ecs_mut() + .insert(TerrainPersistence::new(data_dir.to_owned())); + } state .ecs_mut() .write_resource::() @@ -859,6 +872,12 @@ impl Server { pub fn cleanup(&mut self) { // Cleanup the local state self.state.cleanup(); + + // Maintain persisted terrain + self.state + .ecs() + .try_fetch_mut::() + .map(|mut t| t.maintain()); } fn initialize_client( @@ -1236,6 +1255,13 @@ impl Drop for Server { self.metrics_shutdown.notify_one(); self.state .notify_players(ServerGeneral::Disconnect(DisconnectReason::Shutdown)); + self.state + .ecs() + .try_fetch_mut::() + .map(|mut terrain_persistence| { + info!("Unloading terrain persistence..."); + terrain_persistence.unload_all() + }); } } diff --git a/server/src/settings.rs b/server/src/settings.rs index db41af5a5d..5c17adb5e1 100644 --- a/server/src/settings.rs +++ b/server/src/settings.rs @@ -62,6 +62,11 @@ pub struct Settings { pub spawn_town: Option, pub safe_spawn: bool, pub max_player_for_kill_broadcast: Option, + + /// Experimental feature. No guaranteed forwards-compatibility, may be + /// removed at *any time* with no migration. + #[serde(default, skip_serializing)] + pub experimental_terrain_persistence: bool, } impl Default for Settings { @@ -84,6 +89,7 @@ impl Default for Settings { spawn_town: None, safe_spawn: true, max_player_for_kill_broadcast: None, + experimental_terrain_persistence: false, } } } diff --git a/server/src/sys/msg/in_game.rs b/server/src/sys/msg/in_game.rs index 9aa4e3ee40..ebbe0b103d 100644 --- a/server/src/sys/msg/in_game.rs +++ b/server/src/sys/msg/in_game.rs @@ -1,4 +1,4 @@ -use crate::{client::Client, presence::Presence, Settings}; +use crate::{client::Client, presence::Presence, Settings, TerrainPersistence}; use common::{ comp::{ Admin, CanBuild, ControlEvent, Controller, ForceUpdate, Health, Ori, Player, Pos, SkillSet, @@ -36,6 +36,7 @@ impl Sys { settings: &Read<'_, Settings>, build_areas: &Read<'_, BuildAreas>, player_physics_settings: &mut Write<'_, PlayerPhysicsSettings>, + terrain_persistence: &mut Option>, maybe_player: &Option<&Player>, maybe_admin: &Option<&Admin>, msg: ClientGeneral, @@ -190,7 +191,7 @@ impl Sys { 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 + if let Some(old_block) = build_areas .areas() .get(*area) // TODO: Make this an exclusive check on the upper bound of the AABB @@ -198,13 +199,20 @@ impl Sys { .filter(|aabb| aabb.contains_point(pos)) .and_then(|_| terrain.get(pos).ok()) { - block_changes.set(pos, block.into_vacant()); + let new_block = old_block.into_vacant(); + let was_placed = block_changes.try_set(pos, new_block).is_some(); + if was_placed { + if let Some(terrain_persistence) = terrain_persistence.as_mut() + { + terrain_persistence.set_block(pos, new_block); + } + } } } } } }, - ClientGeneral::PlaceBlock(pos, block) => { + ClientGeneral::PlaceBlock(pos, new_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() { @@ -216,7 +224,13 @@ impl Sys { .filter(|aabb| aabb.contains_point(pos)) .is_some() { - block_changes.try_set(pos, block); + let was_placed = block_changes.try_set(pos, new_block).is_some(); + if was_placed { + if let Some(terrain_persistence) = terrain_persistence.as_mut() + { + terrain_persistence.set_block(pos, new_block); + } + } } } } @@ -287,6 +301,7 @@ impl<'a> System<'a> for Sys { Read<'a, Settings>, Read<'a, BuildAreas>, Write<'a, PlayerPhysicsSettings>, + Option>, ReadStorage<'a, Player>, ReadStorage<'a, Admin>, ); @@ -315,6 +330,7 @@ impl<'a> System<'a> for Sys { settings, build_areas, mut player_physics_settings, + mut terrain_persistence, players, admins, ): Self::SystemData, @@ -349,6 +365,7 @@ impl<'a> System<'a> for Sys { &settings, &build_areas, &mut player_physics_settings, + &mut terrain_persistence, &player, &maybe_admin, msg, diff --git a/server/src/sys/terrain.rs b/server/src/sys/terrain.rs index c68189a39c..836f6a538b 100644 --- a/server/src/sys/terrain.rs +++ b/server/src/sys/terrain.rs @@ -5,7 +5,7 @@ use crate::{ presence::{Presence, RepositionOnChunkLoad}, rtsim::RtSim, settings::Settings, - SpawnPoint, Tick, + SpawnPoint, TerrainPersistence, Tick, }; use common::{ comp::{self, agent, bird_medium, Alignment, BehaviorCapability, ForceUpdate, Pos, Waypoint}, @@ -99,6 +99,7 @@ impl<'a> System<'a> for Sys { WriteExpect<'a, TerrainGrid>, Write<'a, TerrainChanges>, WriteExpect<'a, RtSim>, + Option>, WriteStorage<'a, Pos>, ReadStorage<'a, Presence>, ReadStorage<'a, Client>, @@ -125,6 +126,7 @@ impl<'a> System<'a> for Sys { mut terrain, mut terrain_changes, mut rtsim, + mut terrain_persistence, mut positions, presences, clients, @@ -141,7 +143,7 @@ impl<'a> System<'a> for Sys { // Also, send the chunk data to anybody that is close by. let mut new_chunks = Vec::new(); 'insert_terrain_chunks: while let Some((key, res)) = chunk_generator.recv_new_chunk() { - let (chunk, supplement) = match res { + let (mut chunk, supplement) = match res { Ok((chunk, supplement)) => (chunk, supplement), Err(Some(entity)) => { if let Some(client) = clients.get(entity) { @@ -157,6 +159,11 @@ impl<'a> System<'a> for Sys { }, }; + // Apply changes from terrain persistence to this chunk + if let Some(terrain_persistence) = terrain_persistence.as_mut() { + terrain_persistence.apply_changes(key, &mut chunk); + } + // Arcify the chunk let chunk = Arc::new(chunk); @@ -409,6 +416,11 @@ impl<'a> System<'a> for Sys { }); for key in chunks_to_remove { + // Register the unloading of this chunk from terrain persistence + if let Some(terrain_persistence) = terrain_persistence.as_mut() { + terrain_persistence.unload_chunk(key); + } + // TODO: code duplication for chunk insertion between here and state.rs if terrain.remove(key).is_some() { terrain_changes.removed_chunks.insert(key); diff --git a/server/src/terrain_persistence.rs b/server/src/terrain_persistence.rs new file mode 100644 index 0000000000..bb61c7d213 --- /dev/null +++ b/server/src/terrain_persistence.rs @@ -0,0 +1,344 @@ +use atomicwrites::{AtomicFile, OverwriteBehavior}; +use common::{ + terrain::{Block, TerrainChunk}, + vol::{RectRasterableVol, WriteVol}, +}; +use hashbrown::HashMap; +use serde::{de::DeserializeOwned, Deserialize, Serialize}; +use std::{ + any::{type_name, Any}, + fs::File, + io::{self, Read as _, Write as _}, + path::PathBuf, +}; +use tracing::{debug, error, info, warn}; +use vek::*; + +pub struct TerrainPersistence { + path: PathBuf, + chunks: HashMap, Chunk>, +} + +impl TerrainPersistence { + /// Create a new terrain persistence system using the given data directory. + /// + /// If the `VELOREN_TERRAIN` environment variable is set, this will be used + /// as the persistence directory instead. + pub fn new(mut data_dir: PathBuf) -> Self { + let path = std::env::var("VELOREN_TERRAIN") + .map(PathBuf::from) + .unwrap_or_else(|_| { + data_dir.push("terrain"); + data_dir + }); + + std::fs::create_dir_all(&path).expect("Failed to create terrain persistence directory"); + + info!("Using {:?} as the terrain persistence path", path); + + Self { + path, + chunks: HashMap::default(), + } + } + + /// Apply persistence changes to a newly generated chunk. + pub fn apply_changes(&mut self, key: Vec2, terrain_chunk: &mut TerrainChunk) { + let chunk = self.load_chunk(key); + + let mut resets = Vec::new(); + for (rpos, new_block) in chunk.blocks() { + if let Err(e) = terrain_chunk.map(rpos, |block| { + if block == new_block { + resets.push(rpos); + } + new_block + }) { + warn!( + "Could not set block in chunk {:?} with position {:?} (out of bounds?): {:?}", + key, rpos, e + ); + } + } + + // Reset any unchanged blocks (this is an optimisation only) + for rpos in resets { + chunk.reset_block(rpos); + } + } + + /// Maintain terrain persistence (writing changes changes back to + /// filesystem, etc.) + pub fn maintain(&mut self) { + // Currently, this does nothing because filesystem writeback occurs on + // chunk unload However, this is not a particularly reliable + // mechanism (it doesn't survive power loss, say). Later, a more + // reliable strategy should be implemented here. + } + + fn path_for(&self, key: Vec2) -> PathBuf { + let mut path = self.path.clone(); + path.push(format!("chunk_{}_{}.dat", key.x, key.y)); + path + } + + fn load_chunk(&mut self, key: Vec2) -> &mut Chunk { + let path = self.path_for(key); + self.chunks.entry(key).or_insert_with(|| { + File::open(&path) + .ok() + .map(|f| { + let bytes = match std::io::BufReader::new(f) + .bytes() + .collect::, _>>() + { + Ok(bytes) => bytes, + Err(err) => { + error!( + "Failed to read data for chunk {:?} from file: {:?}", + key, err + ); + return Chunk::default(); + }, + }; + match Chunk::deserialize_from(std::io::Cursor::new(bytes)) { + Some(chunk) => chunk, + None => { + // Find an untaken name for a backup + let mut backup_path = path.clone(); + backup_path.set_extension("dat_backup_0"); + let mut i = 1; + while backup_path.exists() { + backup_path.set_extension(format!("dat_backup_{}", i)); + i += 1; + } + + error!( + "Failed to load chunk {:?}, moving possibly corrupt (or too new) \ + data to {:?} for you to repair.", + key, backup_path + ); + if let Err(err) = std::fs::rename(path, backup_path) { + error!("Failed to rename invalid chunk file: {:?}", err); + } + Chunk::default() + }, + } + }) + .unwrap_or_default() + }) + } + + pub fn unload_chunk(&mut self, key: Vec2) { + if let Some(chunk) = self.chunks.remove(&key) { + // No need to write if no blocks have ever been written + if chunk.blocks.is_empty() { + return; + } + + 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 unload_all(&mut self) { + for key in self.chunks.keys().copied().collect::>() { + self.unload_chunk(key); + } + } + + pub fn set_block(&mut self, pos: Vec3, block: Block) { + let key = pos + .xy() + .map2(TerrainChunk::RECT_SIZE, |e, sz| e.div_euclid(sz as i32)); + self.load_chunk(key) + .blocks + .insert(pos - key * TerrainChunk::RECT_SIZE.map(|e| e as i32), block); + } +} + +impl Drop for TerrainPersistence { + fn drop(&mut self) { self.unload_all(); } +} + +#[derive(Default, Serialize, Deserialize)] +pub struct Chunk { + blocks: HashMap, Block>, +} + +impl Chunk { + fn deserialize_from(reader: R) -> Option { + version::try_load(reader) + } + + fn prepare_raw(self) -> version::Current { self.into() } + + fn blocks(&self) -> impl Iterator, Block)> + '_ { + self.blocks.iter().map(|(k, b)| (*k, *b)) + } + + fn reset_block(&mut self, rpos: Vec3) { self.blocks.remove(&rpos); } +} + +/// # Adding a new chunk format version +/// +/// Chunk formats are designed to be backwards-compatible when loading, but are +/// not required to be backwards-compatible when saving (i.e: we must always be +/// able to load old formats, but we're not required to save old formats because +/// newer formats might contain richer information that is incompatible with an +/// older format). +/// +/// The steps for doing this are as follows: +/// +/// 1. Create a new 'raw format' type that implements [`Serialize`] and +/// `Deserialize`]. Make sure to add a version field. If in doubt, copy the last +/// raw format and increment the version number wherever it appears. Don't +/// forget to increment the version number in the `serde(deserialize_with = +/// ...}` attribute! Conventionally, these types are named `V{N}` where `{N}` is +/// the number succeeding the previous raw format type. +/// +/// 2. Add an implementation of `From<{YourRawFormat}>` for `Chunk`. As before, +/// see previous versions if in doubt. +/// +/// 3. Change the type of [`version::Current`] to your new raw format type. +/// +/// 4. Add an entry for your raw format at the top of the array in +/// [`version::loaders`]. +/// +/// 5. Remove the `Serialize` implementation from the previous raw format type: +/// we don't need it any longer! +mod version { + use super::*; + + /// The newest supported raw format type. This should be changed every time + /// a new raw format is added. + // Step [3] + pub type Current = V3; + + type LoadChunkFn = fn(R) -> Result; + fn loaders<'a, R: io::Read + Clone>() -> &'a [LoadChunkFn] { + // Step [4] + &[load_raw::, load_raw::, load_raw::] + } + + // Convert back to current + + impl From for Current { + fn from(chunk: Chunk) -> Self { + Self { + version: version_magic(3), + blocks: chunk + .blocks + .into_iter() + .map(|(pos, b)| (pos.x as u8, pos.y as u8, pos.z as i16, b.to_u32())) + .collect(), + } + } + } + + /// Version 3 of the raw chunk format. + #[derive(Serialize, Deserialize)] + pub struct V3 { + #[serde(deserialize_with = "version::<_, 3>")] + pub version: u64, + pub blocks: Vec<(u8, u8, i16, u32)>, + } + + impl From for Chunk { + fn from(v3: V3) -> Self { + Self { + blocks: v3 + .blocks + .into_iter() + .map(|(x, y, z, b)| { + ( + Vec3::new(x as i32, y as i32, z as i32), + Block::from_u32(b).unwrap_or_else(Block::empty), + ) + }) + .collect(), + } + } + } + + /// Version 2 of the raw chunk format. + #[derive(Deserialize)] + pub struct V2 { + #[serde(deserialize_with = "version::<_, 2>")] + pub version: u64, + pub blocks: Vec<(u8, u8, i16, Block)>, + } + + impl From for Chunk { + fn from(v2: V2) -> Self { + Self { + blocks: v2 + .blocks + .into_iter() + .map(|(x, y, z, b)| (Vec3::new(x as i32, y as i32, z as i32), b)) + .collect(), + } + } + } + + /// Version 1 of the raw chunk format. + #[derive(Deserialize)] + pub struct V1 { + pub blocks: HashMap, Block>, + } + + impl From for Chunk { + fn from(v1: V1) -> Self { Self { blocks: v1.blocks } } + } + + // Utility things + + fn version_magic(n: u16) -> u64 { (n as u64) | (0x3352ACEEA789 << 16) } + + fn version<'de, D: serde::Deserializer<'de>, const V: u16>(de: D) -> Result { + u64::deserialize(de).and_then(|x| { + if x == version_magic(V) { + Ok(x) + } else { + Err(serde::de::Error::invalid_value( + serde::de::Unexpected::Unsigned(x), + &"incorrect magic/version bytes", + )) + } + }) + } + + fn load_raw + DeserializeOwned, R: io::Read + Clone>( + reader: R, + ) -> Result { + bincode::deserialize_from::<_, RawChunk>(reader) + .map(Into::into) + .map_err(|e| (type_name::(), e)) + } + + pub fn try_load(reader: R) -> Option { + loaders() + .iter() + .find_map(|load_raw| match load_raw(reader.clone()) { + Ok(chunk) => Some(chunk), + Err((raw_name, e)) => { + debug!( + "Attempt to load chunk with raw format `{}` failed: {:?}", + raw_name, e + ); + None + }, + }) + } +} From c2498d81c76892dfcd033392b136bc7be242cefc Mon Sep 17 00:00:00 2001 From: Joshua Barretto Date: Thu, 12 Aug 2021 10:48:00 +0100 Subject: [PATCH 2/2] Added feature flag for terrain persistence --- Cargo.lock | 1 + server-cli/Cargo.toml | 3 ++- server/Cargo.toml | 3 ++- server/src/cmd.rs | 2 ++ server/src/lib.rs | 32 +++++++++++++++++++++++--------- server/src/sys/msg/in_game.rs | 27 ++++++++++++++++++--------- server/src/sys/terrain.rs | 20 +++++++++++++++----- voxygen/Cargo.toml | 2 +- 8 files changed, 64 insertions(+), 26 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 722a0f2cf3..c3fc723502 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6075,6 +6075,7 @@ version = "0.10.0" dependencies = [ "atomicwrites", "authc", + "bincode", "chrono", "crossbeam-channel", "futures-util", diff --git a/server-cli/Cargo.toml b/server-cli/Cargo.toml index bbe0bd15f4..3df973d6d9 100644 --- a/server-cli/Cargo.toml +++ b/server-cli/Cargo.toml @@ -16,9 +16,10 @@ This package includes the official server CLI. [features] worldgen = ["server/worldgen"] +persistent_world = ["server/persistent_world"] # needed to stay compatible with voxygens format default-publish = ["default"] -default = ["worldgen"] +default = ["worldgen", "persistent_world"] tracy = ["common-frontend/tracy"] plugins = ["server/plugins"] diff --git a/server/Cargo.toml b/server/Cargo.toml index e3d71259c7..a1855ae590 100644 --- a/server/Cargo.toml +++ b/server/Cargo.toml @@ -8,8 +8,9 @@ edition = "2018" worldgen = [] simd = ["vek/platform_intrinsics"] plugins = ["common-state/plugins"] +persistent_world = [] -default = ["worldgen", "plugins", "simd"] +default = ["worldgen", "plugins", "simd", "persistent_world"] [dependencies] common = { package = "veloren-common", path = "../common" } diff --git a/server/src/cmd.rs b/server/src/cmd.rs index f706b68a0f..1d3ed22303 100644 --- a/server/src/cmd.rs +++ b/server/src/cmd.rs @@ -529,6 +529,7 @@ fn handle_make_block( 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() @@ -564,6 +565,7 @@ fn handle_make_sprite( .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() diff --git a/server/src/lib.rs b/server/src/lib.rs index c7dca932c8..10e6d06fc6 100644 --- a/server/src/lib.rs +++ b/server/src/lib.rs @@ -31,6 +31,7 @@ pub mod rtsim; pub mod settings; pub mod state_ext; pub mod sys; +#[cfg(feature = "persistent_world")] pub mod terrain_persistence; #[cfg(not(feature = "worldgen"))] mod test_world; pub mod wiring; @@ -44,6 +45,8 @@ pub use crate::{ settings::{EditableSettings, Settings}, }; +#[cfg(feature = "persistent_world")] +use crate::terrain_persistence::TerrainPersistence; use crate::{ alias_validator::AliasValidator, chunk_generator::ChunkGenerator, @@ -56,7 +59,6 @@ use crate::{ rtsim::RtSim, state_ext::StateExt, sys::sentinel::{DeletedEntities, TrackedComps}, - terrain_persistence::TerrainPersistence, }; #[cfg(not(feature = "worldgen"))] use common::grid::Grid; @@ -219,15 +221,23 @@ impl Server { state.ecs_mut().insert(tick_metrics); state.ecs_mut().insert(physics_metrics); if settings.experimental_terrain_persistence { - warn!( - "Experimental terrain persistence support is enabled. This feature may break, be \ - disabled, or otherwise change under your feet at *any time*. Additionally, it is \ - expected to be replaced in the future *without* migration or warning. You have \ - been warned." + #[cfg(feature = "persistent_world")] + { + warn!( + "Experimental terrain persistence support is enabled. This feature may break, \ + be disabled, or otherwise change under your feet at *any time*. \ + Additionally, it is expected to be replaced in the future *without* \ + migration or warning. You have been warned." + ); + state + .ecs_mut() + .insert(TerrainPersistence::new(data_dir.to_owned())); + } + #[cfg(not(feature = "persistent_world"))] + error!( + "Experimental terrain persistence support was requested, but the server was not \ + compiled with the feature. Terrain modifications will *not* be persisted." ); - state - .ecs_mut() - .insert(TerrainPersistence::new(data_dir.to_owned())); } state .ecs_mut() @@ -874,6 +884,7 @@ impl Server { self.state.cleanup(); // Maintain persisted terrain + #[cfg(feature = "persistent_world")] self.state .ecs() .try_fetch_mut::() @@ -1253,8 +1264,11 @@ impl Server { impl Drop for Server { fn drop(&mut self) { self.metrics_shutdown.notify_one(); + self.state .notify_players(ServerGeneral::Disconnect(DisconnectReason::Shutdown)); + + #[cfg(feature = "persistent_world")] self.state .ecs() .try_fetch_mut::() diff --git a/server/src/sys/msg/in_game.rs b/server/src/sys/msg/in_game.rs index ebbe0b103d..296864c78c 100644 --- a/server/src/sys/msg/in_game.rs +++ b/server/src/sys/msg/in_game.rs @@ -1,4 +1,6 @@ -use crate::{client::Client, presence::Presence, Settings, TerrainPersistence}; +#[cfg(feature = "persistent_world")] +use crate::TerrainPersistence; +use crate::{client::Client, presence::Presence, Settings}; use common::{ comp::{ Admin, CanBuild, ControlEvent, Controller, ForceUpdate, Health, Ori, Player, Pos, SkillSet, @@ -16,6 +18,11 @@ use specs::{Entities, Join, Read, ReadExpect, ReadStorage, Write, WriteStorage}; use tracing::{debug, trace, warn}; use vek::*; +#[cfg(feature = "persistent_world")] +pub type TerrainPersistenceData<'a> = Option>; +#[cfg(not(feature = "persistent_world"))] +pub type TerrainPersistenceData<'a> = (); + impl Sys { #[allow(clippy::too_many_arguments)] fn handle_client_in_game_msg( @@ -36,7 +43,7 @@ impl Sys { settings: &Read<'_, Settings>, build_areas: &Read<'_, BuildAreas>, player_physics_settings: &mut Write<'_, PlayerPhysicsSettings>, - terrain_persistence: &mut Option>, + _terrain_persistence: &mut TerrainPersistenceData<'_>, maybe_player: &Option<&Player>, maybe_admin: &Option<&Admin>, msg: ClientGeneral, @@ -200,9 +207,10 @@ impl Sys { .and_then(|_| terrain.get(pos).ok()) { let new_block = old_block.into_vacant(); - let was_placed = block_changes.try_set(pos, new_block).is_some(); - if was_placed { - if let Some(terrain_persistence) = terrain_persistence.as_mut() + let _was_set = block_changes.try_set(pos, new_block).is_some(); + #[cfg(feature = "persistent_world")] + if _was_set { + if let Some(terrain_persistence) = _terrain_persistence.as_mut() { terrain_persistence.set_block(pos, new_block); } @@ -224,9 +232,10 @@ impl Sys { .filter(|aabb| aabb.contains_point(pos)) .is_some() { - let was_placed = block_changes.try_set(pos, new_block).is_some(); - if was_placed { - if let Some(terrain_persistence) = terrain_persistence.as_mut() + let _was_set = block_changes.try_set(pos, new_block).is_some(); + #[cfg(feature = "persistent_world")] + if _was_set { + if let Some(terrain_persistence) = _terrain_persistence.as_mut() { terrain_persistence.set_block(pos, new_block); } @@ -301,7 +310,7 @@ impl<'a> System<'a> for Sys { Read<'a, Settings>, Read<'a, BuildAreas>, Write<'a, PlayerPhysicsSettings>, - Option>, + TerrainPersistenceData<'a>, ReadStorage<'a, Player>, ReadStorage<'a, Admin>, ); diff --git a/server/src/sys/terrain.rs b/server/src/sys/terrain.rs index 836f6a538b..ced500bbd2 100644 --- a/server/src/sys/terrain.rs +++ b/server/src/sys/terrain.rs @@ -1,3 +1,5 @@ +#[cfg(feature = "persistent_world")] +use crate::TerrainPersistence; use crate::{ chunk_generator::ChunkGenerator, client::Client, @@ -5,7 +7,7 @@ use crate::{ presence::{Presence, RepositionOnChunkLoad}, rtsim::RtSim, settings::Settings, - SpawnPoint, TerrainPersistence, Tick, + SpawnPoint, Tick, }; use common::{ comp::{self, agent, bird_medium, Alignment, BehaviorCapability, ForceUpdate, Pos, Waypoint}, @@ -24,6 +26,11 @@ use specs::{Entities, Join, Read, ReadExpect, ReadStorage, Write, WriteExpect, W use std::sync::Arc; use vek::*; +#[cfg(feature = "persistent_world")] +pub type TerrainPersistenceData<'a> = Option>; +#[cfg(not(feature = "persistent_world"))] +pub type TerrainPersistenceData<'a> = (); + pub(crate) struct LazyTerrainMessage { lazy_msg_lo: Option, lazy_msg_hi: Option, @@ -99,7 +106,7 @@ impl<'a> System<'a> for Sys { WriteExpect<'a, TerrainGrid>, Write<'a, TerrainChanges>, WriteExpect<'a, RtSim>, - Option>, + TerrainPersistenceData<'a>, WriteStorage<'a, Pos>, ReadStorage<'a, Presence>, ReadStorage<'a, Client>, @@ -126,7 +133,7 @@ impl<'a> System<'a> for Sys { mut terrain, mut terrain_changes, mut rtsim, - mut terrain_persistence, + mut _terrain_persistence, mut positions, presences, clients, @@ -143,6 +150,7 @@ impl<'a> System<'a> for Sys { // Also, send the chunk data to anybody that is close by. let mut new_chunks = Vec::new(); 'insert_terrain_chunks: while let Some((key, res)) = chunk_generator.recv_new_chunk() { + #[allow(unused_mut)] let (mut chunk, supplement) = match res { Ok((chunk, supplement)) => (chunk, supplement), Err(Some(entity)) => { @@ -160,7 +168,8 @@ impl<'a> System<'a> for Sys { }; // Apply changes from terrain persistence to this chunk - if let Some(terrain_persistence) = terrain_persistence.as_mut() { + #[cfg(feature = "persistent_world")] + if let Some(terrain_persistence) = _terrain_persistence.as_mut() { terrain_persistence.apply_changes(key, &mut chunk); } @@ -417,7 +426,8 @@ impl<'a> System<'a> for Sys { for key in chunks_to_remove { // Register the unloading of this chunk from terrain persistence - if let Some(terrain_persistence) = terrain_persistence.as_mut() { + #[cfg(feature = "persistent_world")] + if let Some(terrain_persistence) = _terrain_persistence.as_mut() { terrain_persistence.unload_chunk(key); } diff --git a/voxygen/Cargo.toml b/voxygen/Cargo.toml index 2b1bebc83f..ddba1a96be 100644 --- a/voxygen/Cargo.toml +++ b/voxygen/Cargo.toml @@ -83,7 +83,7 @@ vek = {version = "=0.14.1", features = ["serde"]} gilrs = {version = "0.8.0", features = ["serde-serialize"]} # Singleplayer -server = {package = "veloren-server", path = "../server", optional = true} +server = { package = "veloren-server", path = "../server", optional = true, default-features = false, features = ["worldgen"] } # Utility backtrace = "0.3.40"